Testeffekt: Dithering-Shader (Sphere/Random) auf den Modell-Chips im Create-Screen

WebGL2-Port von designali-in/dithering-shader als Vanilla-Modul (mountDither):
Shader laeuft als Canvas-Layer in den drei Chips, ausgewaehlt = weiss/silber
(#f2f2f2), nicht ausgewaehlt = dunkelgrau (#555); Auswahlwechsel aendert nur
die Front-Farbe (keine neuen GL-Kontexte). Mount lazy beim ersten Oeffnen des
Create-Screens; ohne WebGL2 bleibt der normale Chip-Look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
root
2026-07-23 15:02:28 +00:00
parent c1d278b6e5
commit 2f2ec31466

View File

@@ -255,6 +255,13 @@
.c-scroll::-webkit-scrollbar{display:none;}
.c-scroll.kb-open{padding-bottom:380px;}
.c-optlabel{font-size:11px; font-weight:700; letter-spacing:.12em; color:rgba(255,255,255,.4); margin-bottom:9px;}
/* Dithering-Shader-Chips (Testeffekt auf den Modell-Chips) */
.chip.dither{position:relative; overflow:hidden; background:#000; color:#fff;
border:1px solid rgba(255,255,255,.14);}
.chip.dither.on{background:#000; color:#fff; border-color:rgba(255,255,255,.55);}
.chip.dither canvas{position:absolute; inset:0; width:100%; height:100%;}
.chip.dither > *:not(canvas){position:relative; z-index:1;}
.chip.dither span{text-shadow:0 1px 2px rgba(0,0,0,.7);}
.c-behind{position:absolute; top:0; left:50%; border-radius:22px; background:#1b1b1b;
border:1px solid rgba(255,255,255,.09); transform-origin:top center; z-index:0;}
.c-preview{position:relative; border-radius:22px; overflow:hidden; z-index:1;
@@ -948,6 +955,7 @@ function go(id){
document.querySelectorAll('.nav button').forEach(b=>b.classList.remove('on'));
if(navMap[id]) document.getElementById(navMap[id]).classList.add('on');
document.body.classList.toggle('noscroll', id==='explore'||id==='create');
if(id==='create') requestAnimationFrame(initCreateDither);
window.scrollTo(0,0);
}
function pick(el){
@@ -1265,6 +1273,209 @@ document.getElementById('remakeBtn').onclick=function(){
},900);
};
/* ---------- Dithering-Shader (WebGL2-Port von designali-in/dithering-shader) ----------
Testweise auf den Modell-Chips im Create-Screen: Sphere/Random,
ausgewaehlt = weiss/silber, nicht ausgewaehlt = dunkelgrau. */
var DITHER_VERT='#version 300 es\nprecision mediump float;\nlayout(location = 0) in vec4 a_position;\nvoid main(){ gl_Position = a_position; }';
var DITHER_FRAG=[
'#version 300 es',
'precision mediump float;',
'uniform float u_time;',
'uniform vec2 u_resolution;',
'uniform vec4 u_colorBack;',
'uniform vec4 u_colorFront;',
'uniform float u_shape;',
'uniform float u_type;',
'uniform float u_pxSize;',
'out vec4 fragColor;',
'vec3 permute(vec3 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }',
'float snoise(vec2 v) {',
' const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);',
' vec2 i = floor(v + dot(v, C.yy));',
' vec2 x0 = v - i + dot(i, C.xx);',
' vec2 i1;',
' i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);',
' vec4 x12 = x0.xyxy + C.xxzz;',
' x12.xy -= i1;',
' i = mod(i, 289.0);',
' vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));',
' vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);',
' m = m * m;',
' m = m * m;',
' vec3 x = 2.0 * fract(p * C.www) - 1.0;',
' vec3 h = abs(x) - 0.5;',
' vec3 ox = floor(x + 0.5);',
' vec3 a0 = x - ox;',
' m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);',
' vec3 g;',
' g.x = a0.x * x0.x + h.x * x0.y;',
' g.yz = a0.yz * x12.xz + h.yz * x12.yw;',
' return 130.0 * dot(m, g);',
'}',
'#define TWO_PI 6.28318530718',
'#define PI 3.14159265358979323846',
'float hash11(float p) {',
' p = fract(p * 0.3183099) + 0.1;',
' p *= p + 19.19;',
' return fract(p * p);',
'}',
'float hash21(vec2 p) {',
' p = fract(p * vec2(0.3183099, 0.3678794)) + 0.1;',
' p += dot(p, p + 19.19);',
' return fract(p.x * p.y);',
'}',
'float getSimplexNoise(vec2 uv, float t) {',
' float noise = .5 * snoise(uv - vec2(0., .3 * t));',
' noise += .5 * snoise(2. * uv + vec2(0., .32 * t));',
' return noise;',
'}',
'const int bayer2x2[4] = int[4](0, 2, 3, 1);',
'const int bayer4x4[16] = int[16](0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5);',
'const int bayer8x8[64] = int[64](0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26, 12, 44, 4, 36, 14, 46, 6, 38, 60, 28, 52, 20, 62, 30, 54, 22, 3, 35, 11, 43, 1, 33, 9, 41, 51, 19, 59, 27, 49, 17, 57, 25, 15, 47, 7, 39, 13, 45, 5, 37, 63, 31, 55, 23, 61, 29, 53, 21);',
'float getBayerValue(vec2 uv, int size) {',
' ivec2 pos = ivec2(mod(uv, float(size)));',
' int index = pos.y * size + pos.x;',
' if (size == 2) { return float(bayer2x2[index]) / 4.0; }',
' else if (size == 4) { return float(bayer4x4[index]) / 16.0; }',
' else if (size == 8) { return float(bayer8x8[index]) / 64.0; }',
' return 0.0;',
'}',
'void main() {',
' float t = .5 * u_time;',
' vec2 uv = gl_FragCoord.xy / u_resolution.xy;',
' uv -= .5;',
' float pxSize = u_pxSize;',
' vec2 pxSizeUv = gl_FragCoord.xy;',
' pxSizeUv -= .5 * u_resolution;',
' pxSizeUv /= pxSize;',
' vec2 pixelizedUv = floor(pxSizeUv) * pxSize / u_resolution.xy;',
' pixelizedUv += .5;',
' pixelizedUv -= .5;',
' vec2 shape_uv = pixelizedUv;',
' vec2 dithering_uv = pxSizeUv;',
' vec2 ditheringNoise_uv = uv * u_resolution;',
' float shape = 0.;',
' if (u_shape < 1.5) {',
' shape_uv *= .001;',
' shape = 0.5 + 0.5 * getSimplexNoise(shape_uv, t);',
' shape = smoothstep(0.3, 0.9, shape);',
' } else if (u_shape < 2.5) {',
' shape_uv *= .003;',
' for (float i = 1.0; i < 6.0; i++) {',
' shape_uv.x += 0.6 / i * cos(i * 2.5 * shape_uv.y + t);',
' shape_uv.y += 0.6 / i * cos(i * 1.5 * shape_uv.x + t);',
' }',
' shape = .15 / abs(sin(t - shape_uv.y - shape_uv.x));',
' shape = smoothstep(0.02, 1., shape);',
' } else if (u_shape < 3.5) {',
' shape_uv *= .05;',
' float stripeIdx = floor(2. * shape_uv.x / TWO_PI);',
' float rand = hash11(stripeIdx * 10.);',
' rand = sign(rand - .5) * pow(.1 + abs(rand), .4);',
' shape = sin(shape_uv.x) * cos(shape_uv.y - 5. * rand * t);',
' shape = pow(abs(shape), 6.);',
' } else if (u_shape < 4.5) {',
' shape_uv *= 4.;',
' float wave = cos(.5 * shape_uv.x - 2. * t) * sin(1.5 * shape_uv.x + t) * (.75 + .25 * cos(3. * t));',
' shape = 1. - smoothstep(-1., 1., shape_uv.y + wave);',
' } else if (u_shape < 5.5) {',
' float dist = length(shape_uv);',
' float waves = sin(pow(dist, 1.7) * 7. - 3. * t) * .5 + .5;',
' shape = waves;',
' } else if (u_shape < 6.5) {',
' float l = length(shape_uv);',
' float angle = 6. * atan(shape_uv.y, shape_uv.x) + 4. * t;',
' float twist = 1.2;',
' float offset = pow(l, -twist) + angle / TWO_PI;',
' float mid = smoothstep(0., 1., pow(l, twist));',
' shape = mix(0., fract(offset), mid);',
' } else {',
' shape_uv *= 2.;',
' float d = 1. - pow(length(shape_uv), 2.);',
' vec3 pos = vec3(shape_uv, sqrt(d));',
' vec3 lightPos = normalize(vec3(cos(1.5 * t), .8, sin(1.25 * t)));',
' shape = .5 + .5 * dot(lightPos, pos);',
' shape *= step(0., d);',
' }',
' int type = int(floor(u_type));',
' float dithering = 0.0;',
' switch (type) {',
' case 1: {',
' dithering = step(hash21(ditheringNoise_uv), shape);',
' } break;',
' case 2:',
' dithering = getBayerValue(dithering_uv, 2);',
' break;',
' case 3:',
' dithering = getBayerValue(dithering_uv, 4);',
' break;',
' default:',
' dithering = getBayerValue(dithering_uv, 8);',
' break;',
' }',
' dithering -= .5;',
' float res = step(.5, shape + dithering);',
' vec3 fgColor = u_colorFront.rgb * u_colorFront.a;',
' float fgOpacity = u_colorFront.a;',
' vec3 bgColor = u_colorBack.rgb * u_colorBack.a;',
' float bgOpacity = u_colorBack.a;',
' vec3 color = fgColor * res;',
' float opacity = fgOpacity * res;',
' color += bgColor * (1. - opacity);',
' opacity += bgOpacity * (1. - opacity);',
' fragColor = vec4(color, opacity);',
'}'
].join('\n');
function ditherHexToRgba(hex){
var m=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if(!m) return [0,0,0,1];
return [parseInt(m[1],16)/255, parseInt(m[2],16)/255, parseInt(m[3],16)/255, 1];
}
function mountDither(canvas, opts){
var gl=canvas.getContext('webgl2');
if(!gl) return null;
function sh(type,src){
var s=gl.createShader(type);
gl.shaderSource(s,src); gl.compileShader(s);
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){ console.error(gl.getShaderInfoLog(s)); return null; }
return s;
}
var vs=sh(gl.VERTEX_SHADER,DITHER_VERT), fs=sh(gl.FRAGMENT_SHADER,DITHER_FRAG);
if(!vs||!fs) return null;
var prog=gl.createProgram();
gl.attachShader(prog,vs); gl.attachShader(prog,fs); gl.linkProgram(prog);
if(!gl.getProgramParameter(prog,gl.LINK_STATUS)){ console.error(gl.getProgramInfoLog(prog)); return null; }
var L={};
['u_time','u_resolution','u_colorBack','u_colorFront','u_shape','u_type','u_pxSize'].forEach(function(n){ L[n]=gl.getUniformLocation(prog,n); });
var buf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]),gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0,2,gl.FLOAT,false,0,0);
var dpr=window.devicePixelRatio||1;
var r=canvas.parentElement.getBoundingClientRect();
canvas.width=Math.max(2,Math.round((r.width||120)*dpr));
canvas.height=Math.max(2,Math.round((r.height||36)*dpr));
gl.viewport(0,0,canvas.width,canvas.height);
var front=ditherHexToRgba(opts.front||'#ffffff');
var back=ditherHexToRgba(opts.back||'#000000');
var t0=Date.now();
(function frame(){
var t=(Date.now()-t0)*.001*(opts.speed||1.5);
gl.useProgram(prog);
gl.uniform1f(L.u_time,t);
gl.uniform2f(L.u_resolution,canvas.width,canvas.height);
gl.uniform4fv(L.u_colorBack,back);
gl.uniform4fv(L.u_colorFront,front);
gl.uniform1f(L.u_shape,opts.shape||7);
gl.uniform1f(L.u_type,opts.type||1);
gl.uniform1f(L.u_pxSize,opts.pxSize||2);
gl.drawArrays(gl.TRIANGLES,0,6);
requestAnimationFrame(frame);
})();
return { setFront:function(hex){ front=ditherHexToRgba(hex); } };
}
/* ---------- Create: Vorschau-Stack / Optionen / Prompt-Beam / Fake-Tastatur ---------- */
var C={model:0, count:1, format:'4:5', mediaType:'image', optOpen:false, kbOpen:false, generating:0};
var C_MODELS=[
@@ -1320,13 +1531,34 @@ function renderCreateOpts(){
pills('countRow',[{v:'1',label:'1 Bild'},{v:'3',label:'3er'},{v:'5',label:'5er'}],String(C.count),function(v){C.count=+v; renderCreateOpts(); renderCreateStack();});
}
function toggleOpts(){ C.optOpen=!C.optOpen; renderCreateStack(); }
var DCHIPS={built:false, mounted:false, insts:[]};
var DITHER_ON='#f2f2f2', DITHER_OFF='#555555';
function renderCreateModels(){
var host=document.getElementById('createModels');
host.innerHTML=C_MODELS.map(function(m,i){
return '<div class="chip'+(i===C.model?' on':'')+'" data-i="'+i+'"><div class="dot"></div>'+m.name+'</div>';
}).join('');
host.querySelectorAll('.chip').forEach(function(c){
c.onclick=function(){ C.model=+c.dataset.i; renderCreateModels(); };
if(!DCHIPS.built){
host.innerHTML=C_MODELS.map(function(m,i){
return '<div class="chip dither" data-i="'+i+'"><canvas></canvas><div class="dot"></div><span>'+m.name+'</span></div>';
}).join('');
host.querySelectorAll('.chip').forEach(function(c){
c.onclick=function(){ C.model=+c.dataset.i; renderCreateModels(); };
});
DCHIPS.built=true;
}
host.querySelectorAll('.chip').forEach(function(c,i){
var on=(i===C.model);
c.classList.toggle('on',on);
c.querySelector('.dot').style.background=on?'var(--accent)':'rgba(255,255,255,.3)';
if(DCHIPS.insts[i]) DCHIPS.insts[i].setFront(on?DITHER_ON:DITHER_OFF);
});
}
/* Shader erst mounten, wenn der Create-Screen sichtbar ist (sonst Groesse 0) */
function initCreateDither(){
if(DCHIPS.mounted) return;
var chips=document.querySelectorAll('#createModels .chip.dither');
if(!chips.length) return;
DCHIPS.mounted=true;
chips.forEach(function(c,i){
DCHIPS.insts[i]=mountDither(c.querySelector('canvas'), {front:(i===C.model)?DITHER_ON:DITHER_OFF, shape:7, type:1, pxSize:2, speed:1.5});
});
}
/* Prompt-Editing über die Fake-Tastatur (Caret-genau wie im Design) */