From 3c544000253721f4da8be28c70e85ddb7911cb3f Mon Sep 17 00:00:00 2001 From: Leonxlnx Date: Fri, 12 Jun 2026 15:51:58 +0200 Subject: [PATCH] Fix WebGL freeze and crash loop with safer boot and lighter shaders. Guard canvas lifecycle to stop the fitCanvas TypeError loop, load a slim base shader first while the full genome program compiles in the background, and trim GPU-heavy paths so weak devices stay responsive. Co-authored-by: Cursor --- index.html | 7 ++- js/engine.js | 134 ++++++++++++++++++++++++++++++++++-------- js/main.js | 159 +++++++++++++++++++++++++++++--------------------- js/shaders.js | 135 ++++++++++++++++-------------------------- styles.css | 17 ++++++ 5 files changed, 273 insertions(+), 179 deletions(-) diff --git a/index.html b/index.html index 9eab2b5..28d10e0 100644 --- a/index.html +++ b/index.html @@ -79,6 +79,7 @@
+
@@ -116,8 +117,8 @@ - - + + @@ -125,7 +126,7 @@ - + diff --git a/js/engine.js b/js/engine.js index 0720552..83fe6c3 100644 --- a/js/engine.js +++ b/js/engine.js @@ -10,6 +10,7 @@ var Engine = (function () { var loopT = 0; // seconds into current loop var lastTick = 0; var fps = 60, fpsAcc = 0, fpsN = 0, fpsCb = null; + var maxFps = 60, minFrameMs = 1000 / 60, lastDraw = 0; var getParams = null; // injected: () => P var UNIFORM_NAMES = [ @@ -34,6 +35,49 @@ var Engine = (function () { return sh; } + /* Two-stage boot: the slim base shader (no genome) links in a second + or two and gets pixels on screen; the full shader compiles in the + background via KHR_parallel_shader_compile and is swapped in when + the driver finishes. The main thread never blocks on a long link. */ + + var fullReady = false; + + function buildProgram(fragSrc, parallel, cb) { + var prog = gl.createProgram(); + try { + gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT_SRC)); + gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, fragSrc)); + } catch (e) { + cb(null, String(e.message || e)); + return; + } + gl.linkProgram(prog); + + function check() { + if (gl.isContextLost()) { cb(null, "context lost"); return; } + if (gl.getProgramParameter(prog, gl.LINK_STATUS)) cb(prog, null); + else cb(null, gl.getProgramInfoLog(prog) || "unknown link error"); + } + + if (parallel) { + (function poll() { + if (gl.isContextLost()) { cb(null, "context lost"); return; } + if (gl.getProgramParameter(prog, parallel.COMPLETION_STATUS_KHR)) check(); + else setTimeout(poll, 80); + })(); + } else { + setTimeout(check, 30); + } + } + + function adoptProgram(prog) { + program = prog; + gl.useProgram(program); + UNIFORM_NAMES.forEach(function (n) { + uniforms[n] = gl.getUniformLocation(program, n); + }); + } + function init(canvasEl, paramsGetter, opts) { opts = opts || {}; canvas = canvasEl; @@ -43,16 +87,20 @@ var Engine = (function () { preserveDrawingBuffer: true, powerPreference: "default" }); - if (!gl) throw new Error("WebGL2 not available"); - - program = gl.createProgram(); - gl.attachShader(program, compile(gl.VERTEX_SHADER, VERT_SRC)); - gl.attachShader(program, compile(gl.FRAGMENT_SHADER, FRAG_SRC)); - gl.linkProgram(program); - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error("Program link error:\n" + gl.getProgramInfoLog(program)); + if (!gl) { + if (opts.onError) { opts.onError("WebGL2 not available"); return; } + throw new Error("WebGL2 not available"); } - gl.useProgram(program); + + canvas.addEventListener("webglcontextlost", function (e) { + e.preventDefault(); + suspended = true; + started = false; + ready = false; + if (opts.onContextLost) opts.onContextLost(); + }, false); + + var parallel = gl.getExtension("KHR_parallel_shader_compile"); var buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); @@ -60,18 +108,40 @@ var Engine = (function () { gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); - UNIFORM_NAMES.forEach(function (n) { - uniforms[n] = gl.getUniformLocation(program, n); - }); + buildProgram(FRAG_SRC_BASE, parallel, function (baseProg, err) { + if (!baseProg) { + if (opts.onError) opts.onError("Program link error: " + err); + return; + } + adoptProgram(baseProg); + ready = true; + lastTick = performance.now(); + started = false; - ready = true; - lastTick = performance.now(); - started = false; - if (opts.autostart !== false) start(); + if (opts.onReady) opts.onReady(); + if (opts.autostart !== false) start(); + + /* upgrade to the full shader (genome styles) in the background */ + buildProgram(FRAG_SRC_FULL, parallel, function (fullProg, ferr) { + if (!fullProg) return; /* keep base; genome styles unavailable */ + adoptProgram(fullProg); + gl.deleteProgram(baseProg); + fullReady = true; + if (opts.onFullReady) opts.onFullReady(); + }); + }); + } + + function isLost() { + return !gl || gl.isContextLost(); + } + + function canRender() { + return ready && gl && !gl.isContextLost(); } function start() { - if (!ready || started) return; + if (!canRender() || started) return; started = true; suspended = false; lastTick = performance.now(); @@ -79,13 +149,14 @@ var Engine = (function () { } function setSize(w, h) { - if (!canvas) return; + if (!canvas || !canRender()) return; canvas.width = w; canvas.height = h; - if (gl) gl.viewport(0, 0, w, h); + gl.viewport(0, 0, w, h); } function pushUniforms(P, phase) { + if (!canRender()) return; gl.uniform2f(uniforms.u_res, canvas.width, canvas.height); gl.uniform1f(uniforms.u_phase, phase); gl.uniform1f(uniforms.u_seed, P.seed); @@ -136,6 +207,7 @@ var Engine = (function () { } function renderAt(phase) { + if (!canRender()) return; var P = getParams(); pushUniforms(P, phase); gl.drawArrays(gl.TRIANGLES, 0, 3); @@ -147,7 +219,12 @@ var Engine = (function () { } function tick(now) { - if (suspended) return; + if (suspended || !canRender()) return; + if (now - lastDraw < minFrameMs) { + requestAnimationFrame(tick); + return; + } + lastDraw = now; var dt = Math.min((now - lastTick) / 1000, 0.1); lastTick = now; @@ -167,6 +244,11 @@ var Engine = (function () { requestAnimationFrame(tick); } + function setMaxFps(n) { + maxFps = Math.max(15, Math.min(n, 60)); + minFrameMs = 1000 / maxFps; + } + function readPixels() { var w = canvas.width, h = canvas.height; var buf = new Uint8Array(w * h * 4); @@ -190,16 +272,20 @@ var Engine = (function () { currentPhase: currentPhase, resetTime: function () { loopT = 0; }, setLoopTime: function (t) { loopT = t; }, - suspend: function () { suspended = true; }, + suspend: function () { suspended = true; started = false; }, resume: function () { + if (!canRender()) return; suspended = false; - lastTick = performance.now(); - requestAnimationFrame(tick); + started = false; + start(); }, + isLost: isLost, + setMaxFps: setMaxFps, + hasGenome: function () { return fullReady; }, setPlaying: function (v) { playing = v; lastTick = performance.now(); }, isPlaying: function () { return playing; }, onFps: function (cb) { fpsCb = cb; }, canvas: function () { return canvas; }, - size: function () { return [canvas.width, canvas.height]; } + size: function () { return canvas ? [canvas.width, canvas.height] : [0, 0]; } }; })(); diff --git a/js/main.js b/js/main.js index 9f7233e..f850fa8 100644 --- a/js/main.js +++ b/js/main.js @@ -241,7 +241,7 @@ function styleName() { function randomizeAll() { if (!P.lockStyle) { - if (rnd() < 0.42) { + if (rnd() < 0.42 && Engine.hasGenome()) { generateGenomeStyle(); } else { P.synthOn = false; @@ -401,6 +401,10 @@ function buildRail() { var btnSynth = UI.el("button", "mini-btn", synthRow); btnSynth.innerHTML = 'New style'; btnSynth.addEventListener("click", function () { + if (!Engine.hasGenome()) { + UI.toast("Style synthesizer is still warming up, try again in a moment"); + return; + } generateGenomeStyle(); newSeed(); refreshAll(); @@ -553,8 +557,12 @@ function updateMeta() { } } +var stage = { failed: false, resizeObs: null, resizeTimer: null, bufW: 0, bufH: 0 }; + function fitCanvas(preview) { + if (stage.failed) return; var frame = document.getElementById("canvas-frame"); + if (!frame) return; var availW = frame.clientWidth - 80; var availH = frame.clientHeight - 52; if (availW <= 0 || availH <= 0) return; @@ -564,6 +572,7 @@ function fitCanvas(preview) { w = Math.round(w); h = Math.round(h); var canvasEl = document.getElementById("view"); + if (!canvasEl || !canvasEl.isConnected) return; canvasEl.style.width = w + "px"; canvasEl.style.height = h + "px"; var boot = document.getElementById("canvas-boot"); @@ -571,13 +580,37 @@ function fitCanvas(preview) { boot.style.width = w + "px"; boot.style.height = h + "px"; } - if (!Engine.isReady()) return; - var dprCap = preview ? 0.75 : 1.35; + if (!Engine.isReady() || Engine.isLost()) return; + var lowMem = (navigator.deviceMemory || 8) <= 4; + var dprCap = preview ? 0.65 : (lowMem ? 0.75 : 0.85); var dpr = Math.min(window.devicePixelRatio || 1, dprCap); - Engine.setSize(2 * Math.round(w * dpr / 2), 2 * Math.round(h * dpr / 2)); + var bufW = 2 * Math.round(w * dpr / 2); + var bufH = 2 * Math.round(h * dpr / 2); + if (stage.bufW === bufW && stage.bufH === bufH) { + updateMeta(); + return; + } + stage.bufW = bufW; + stage.bufH = bufH; + Engine.setSize(bufW, bufH); updateMeta(); } +function onStageResize() { + clearTimeout(stage.resizeTimer); + stage.resizeTimer = setTimeout(function () { + fitCanvas(false); + }, 250); +} + +function detachStageResize() { + if (stage.resizeObs) { + stage.resizeObs.disconnect(); + stage.resizeObs = null; + } + clearTimeout(stage.resizeTimer); +} + function setPlayingUI(v) { Engine.setPlaying(v); document.getElementById("icon-pause").style.display = v ? "" : "none"; @@ -586,34 +619,6 @@ function setPlayingUI(v) { /* ---------------- boot ---------------- */ -function runIntro() { - var items = []; - document.querySelectorAll(".brand, .segmented button, .topbar-actions > *").forEach(function (n) { - items.push(n); - }); - items.push(document.getElementById("canvas-shell")); - items.push(document.querySelector(".stage-meta")); - document.querySelectorAll(".rail-section").forEach(function (n) { items.push(n); }); - document.querySelectorAll(".mode-grid > *, .export-grid > *").forEach(function (n) { items.push(n); }); - - var d = 0; - items.forEach(function (n, i) { - if (!n) return; - d += i < 10 ? 40 : 24; - n.classList.add("stagger-item"); - n.style.setProperty("--intro-d", Math.min(d, 820) + "ms"); - }); - document.body.classList.add("intro"); - setTimeout(function () { - document.body.classList.remove("intro"); - items.forEach(function (n) { - if (!n) return; - n.classList.remove("stagger-item"); - n.style.removeProperty("--intro-d"); - }); - }, 1500); -} - function wireControls() { var segRefresh = UI.segmented( document.getElementById("aspect-seg"), @@ -645,57 +650,75 @@ function wireControls() { }); } -function showBootError(msg) { - document.body.classList.remove("booting"); - document.querySelector(".canvas-frame").innerHTML = - '
' + - "WebGL2 is required. Please use a recent Chrome, Edge or Firefox.
" + - String(msg).split("\n")[0] + "
"; +function showStageError(msg) { + var el = document.getElementById("stage-error"); + if (!el) return; + el.innerHTML = + "WebGL2 is required. Please use a recent Chrome, Edge or Firefox.
" + + '' + String(msg).split("\n")[0] + ""; + el.hidden = false; } -function finishBoot(opts) { +function showBootError(msg) { + stage.failed = true; + detachStageResize(); + Engine.suspend(); document.body.classList.remove("booting"); - document.body.classList.add("ready"); - Engine.start(); - if (opts.intro) runIntro(); - setTimeout(function () { fitCanvas(false); }, 1400); + document.body.classList.remove("ready"); + showStageError(msg); +} + +function onContextLost() { + stage.failed = true; + detachStageResize(); + Engine.suspend(); + document.body.classList.remove("ready"); + showStageError("WebGL context lost. Reload the page."); + UI.toast("WebGL context lost. Reload the page."); } document.addEventListener("DOMContentLoaded", function () { var canvas = document.getElementById("view"); + if (!canvas) return; + var lowPower = (navigator.hardwareConcurrency || 8) <= 4 || ((navigator.deviceMemory || 8) <= 4); - var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + document.addEventListener("visibilitychange", function () { + if (stage.failed) return; + if (document.hidden) Engine.suspend(); + else if (Engine.isReady() && !Engine.isLost()) Engine.resume(); + }); + + /* show UI shell immediately; the shader compiles on a driver + thread (KHR_parallel_shader_compile) and onReady fires when done, + so the page never freezes during boot. */ + fitCanvas(true); + buildRail(); + wireControls(); requestAnimationFrame(function () { - fitCanvas(true); - buildRail(); - wireControls(); + Engine.init(canvas, function () { return P; }, { + autostart: false, + onContextLost: onContextLost, + onError: showBootError, + onReady: function () { + Engine.setMaxFps(lowPower ? 30 : 45); - var hash = decodeURIComponent(location.hash.slice(1) || ""); - if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) { - UI.toast("Shared design loaded"); - } + var hash = decodeURIComponent(location.hash.slice(1) || ""); + if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) { + UI.toast("Shared design loaded"); + } - new ResizeObserver(function () { - fitCanvas(!document.body.classList.contains("ready")); - }).observe(document.getElementById("canvas-frame")); + fitCanvas(false); + stage.resizeObs = new ResizeObserver(onStageResize); + stage.resizeObs.observe(document.getElementById("canvas-frame")); - requestAnimationFrame(function () { - try { - Engine.init(canvas, function () { return P; }, { autostart: false }); - } catch (e) { - showBootError(e.message); - return; - } - - fitCanvas(true); - Engine.renderAt(0); - - requestAnimationFrame(function () { - finishBoot({ intro: !lowPower && !reduceMotion }); + document.body.classList.remove("booting"); + document.body.classList.add("ready"); + Engine.start(); updateMeta(); - }); + } }); }); }); diff --git a/js/shaders.js b/js/shaders.js index 9b6ab58..ffca178 100644 --- a/js/shaders.js +++ b/js/shaders.js @@ -6,7 +6,10 @@ var VERT_SRC = "#version 300 es\n" + "layout(location=0) in vec2 a_pos;\n" + "void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); }\n"; -var FRAG_SRC = `#version 300 es +/* The fragment source is built twice: a slim "base" variant without the + genome synthesizer (links fast, used at boot) and the full variant + compiled lazily in the background. */ +var FRAG_BODY = ` precision highp float; precision highp int; @@ -179,10 +182,6 @@ vec3 sceneChrome(vec2 uv){ col += alb2 * spec2 * u_light * 1.35; col += palette(clamp(fres*0.85 + u_irid*n.y*0.4, 0.0, 1.0)) * fres * u_light * 0.55; col += vec3(1.0) * pow(spec, 3.0) * u_light * 0.5; - /* clearcoat glass sheen on top of the metal */ - float coat = pow(max(dot(n, normalize(vec3(0.0, 0.0, 1.0))), 0.0), 18.0); - col += vec3(1.0) * coat * u_light * 0.22; - col = mix(col, col + palette(fres*0.5)*0.08, fres*0.35); return col; } @@ -387,37 +386,22 @@ vec3 boldField(vec2 p){ return col; } -vec3 sampleReeded(vec2 suv, float blur){ - vec3 c0 = boldField(toP(suv)*0.8); - vec3 c1 = boldField(toP(suv + vec2(blur, blur*0.22))*0.8); - vec3 c2 = boldField(toP(suv - vec2(blur, blur*0.22))*0.8); - return c0*0.52 + c1*0.24 + c2*0.24; -} - vec3 sceneReeded(vec2 uv){ float ridgeFreq = max(u_lines*0.55, 6.0); float nx = uv.x * ridgeFreq; float ci = floor(nx); float lx = fract(nx) - 0.5; - - /* lens refraction across each flute */ float lens = sin(lx*PI); - float refr = lx*0.28*u_warp + lens*0.11*u_warp; + float refr = lx*0.22*u_warp + lens*0.08*u_warp; float srcX = (ci + 0.5 + refr) / ridgeFreq; - float blur = 0.0035*u_soft*(0.6 + u_warp*0.5); - vec3 col = sampleReeded(vec2(srcX, uv.y), blur); + vec3 col = boldField(toP(vec2(srcX, uv.y))*0.8); float ridge = cos(lx*PI); - float shade = 0.74 + 0.30*ridge; + float shade = 0.78 + 0.28*ridge; float groove = smoothstep(0.48, 0.40, abs(lx)); - col *= mix(0.50, shade, groove); - - /* fresnel rim + peak specular */ - float fres = pow(1.0 - abs(ridge), 2.6); - float specPow = mix(10.0, 48.0, clamp(u_gloss/120.0, 0.0, 1.0)); - float spec = pow(max(ridge, 0.0), specPow); - col += vec3(1.0)*spec*u_light*0.20; - col += mix(vec3(0.0), col*0.35 + vec3(0.06, 0.05, 0.04), fres*u_light*0.42); + col *= mix(0.54, shade, groove); + float spec = pow(max(ridge, 0.0), mix(12.0, 36.0, clamp(u_gloss/120.0, 0.0, 1.0))); + col += vec3(1.0)*spec*u_light*0.14; return col; } @@ -441,6 +425,7 @@ vec3 sceneMosaic(vec2 uv){ 12 genes select field, domain geometry, color mapping, shading and overlay. Each combination is a distinct style. ========================================================= */ +#if HAS_GENOME float gnVoro(vec2 p){ vec2 i = floor(p), f = fract(p); @@ -476,7 +461,7 @@ float gnField(int ft, vec2 p){ } if (ft == 5) { /* flow */ float f1 = fbm(p + so + lt); - return fbm(p + 2.4*vec2(f1, fbm(p + so*1.3 - lt)) + so); + return fbm(p + 2.4*vec2(f1, 1.0 - f1) + so); } if (ft == 6) { /* cross bands */ float bx = sin(p.x*(2.2 + u_g3.x*4.5) + lt.x*2.0); @@ -485,8 +470,9 @@ float gnField(int ft, vec2 p){ } if (ft == 7) { /* plaid grid */ vec2 q = p*(1.8 + u_g3.z*1.2); - float gx = step(0.5, fract(q.x + fbm(p*0.35 + so)*0.15)); - float gy = step(0.5, fract(q.y + fbm(p.yx*0.35 - so)*0.15)); + float j = fbm(p*0.35 + so)*0.15; + float gx = step(0.5, fract(q.x + j)); + float gy = step(0.5, fract(q.y - j)); return mix(gx*gy, 1.0 - gx*gy, 0.5 + 0.5*sin(lt.x*3.0)); } if (ft == 8) { /* hex lattice edges */ @@ -577,14 +563,18 @@ vec3 sceneGenome(vec2 uv){ vec2 so = SO(); p += u_g1.z*u_warp*0.8*vec2(fbm(p*0.6 + so) - 0.5, fbm(p*0.6 - so) - 0.5)*2.0; - float f = gnField(ft, p); + /* the field and its gradient are sampled exactly once and shared by + every shading branch: keeps the D3D static-inline budget low */ + float e = 0.05; + float f = gnField(ft, p); + float fx = gnField(ft, p + vec2(e, 0.0)); + float fy = gnField(ft, p + vec2(0.0, e)); + vec2 grad = vec2((fx - f)/e, (fy - f)/e); + vec3 col = gnColor(cm, f*1.15 - 0.05, p); if (sh == 1) { /* embossed light */ - float e = 0.05; - float fx = gnField(ft, p + vec2(e, 0.0)); - float fy = gnField(ft, p + vec2(0.0, e)); - vec3 n = normalize(vec3(-(fx - f)/e*2.0, -(fy - f)/e*2.0, 1.0)); + vec3 n = normalize(vec3(-grad.x*2.0, -grad.y*2.0, 1.0)); float la = u_lightAngle*PI/180.0; vec3 L = normalize(vec3(cos(la), sin(la), 0.6)); float diff = max(dot(n, L), 0.0); @@ -592,27 +582,16 @@ vec3 sceneGenome(vec2 uv){ col *= 0.35 + 0.8*diff; col += vec3(1.0)*spec*u_light*0.7; } else if (sh == 2) { /* glowing edges */ - float e = 0.04; - float g = abs(gnField(ft, p + vec2(e,0.0)) - f) + abs(gnField(ft, p + vec2(0.0,e)) - f); + float g = (abs(fx - f) + abs(fy - f))*1.25; col = mix(u_bg, col, 0.35); col += palette(clamp(f + 0.2, 0.0, 1.0)) * smoothstep(0.01, 0.14, g) * u_light * 1.4; } else if (sh == 3) { /* deep contrast carve */ col *= smoothstep(0.0, 0.55, f)*1.15; col = mix(u_bg, col, smoothstep(0.08, 0.35, f)); - } else if (sh == 4) { /* glass refraction */ - float e = 0.055; - float fx = gnField(ft, p + vec2(e, 0.0)); - float fy = gnField(ft, p + vec2(0.0, e)); - vec2 grad = vec2((fx - f)/e, (fy - f)/e); - vec2 pRef = p - grad*(0.12 + u_g1.z*0.22); - float f2 = gnField(ft, pRef); - col = gnColor(cm, f2*1.08 - 0.04, pRef); - float fres = pow(1.0 - clamp(length(grad)*1.8, 0.0, 1.0), 2.4); - float flute = 0.86 + 0.14*abs(sin(p0.x*u_lines*0.32 + p0.y*0.18)); - col *= flute; - col += vec3(1.0)*fres*u_light*0.14; - float spec = pow(max(1.0 - abs(grad.x + grad.y)*0.5, 0.0), mix(8.0, 36.0, u_gloss/120.0)); - col += vec3(1.0)*spec*u_light*0.18; + } else if (sh == 4) { /* glass sheen */ + float fres = pow(1.0 - clamp(length(grad)*1.4, 0.0, 1.0), 2.2); + col *= 0.86 + 0.14*abs(sin(p0.x*u_lines*0.28)); + col += vec3(1.0)*fres*u_light*0.16; } if (ov == 1) { /* stripes */ @@ -631,6 +610,7 @@ vec3 sceneGenome(vec2 uv){ return col; } +#endif /* ---------------- dispatch + post ---------------- */ @@ -646,47 +626,30 @@ vec3 sceneFor(int m, vec2 uv){ return sceneMosaic(uv); } +/* single scene call per fragment: keeps the D3D/ANGLE static + inlining budget low so the program links fast everywhere. + legacy synth share codes fall back to mode A. */ vec3 scene(vec2 uv){ +#if HAS_GENOME if (u_genome == 1) return sceneGenome(uv); - vec3 a = sceneFor(u_mode, uv); - if (u_synth == 0) return a; - - vec3 b = sceneFor(u_modeB, uv); - float asp = u_res.x/u_res.y; - vec2 c = (uv - 0.5)*vec2(asp, 1.0); - - if (u_mixOp == 1) { /* screen: layered light */ - return mix(a, 1.0 - (1.0 - a)*(1.0 - b), u_blend); - } - if (u_mixOp == 2) { /* multiply with lift */ - return mix(a, a*b*1.6 + a*0.12, u_blend); - } - if (u_mixOp == 3) { /* radial: B grows from center */ - float m = smoothstep(0.15, 0.85, length(c)*1.15); - return mix(b, a, mix(1.0, m, u_blend)); - } - if (u_mixOp == 4) { /* soft diagonal split */ - float ang = TAU*hash11(u_seed*0.091 + 5.0); - float m = smoothstep(-0.45, 0.45, cos(ang)*c.x + sin(ang)*c.y); - return mix(a, b, m*u_blend); - } - /* default: organic noise mask */ - float m = fbm(c*1.6 + SO()*0.7 + LT()*0.5); - m = smoothstep(0.32, 0.68, m); - return mix(a, b, m*u_blend); +#endif + return sceneFor(u_mode, uv); } void main(){ vec2 uv = gl_FragCoord.xy/u_res; - vec3 col; + vec3 col = scene(uv); - /* CA re-renders per channel. It calls the single-mode renderer directly - to keep the D3D linker's static inlining budget low. */ - if (u_ca > 0.004 && u_synth == 0 && u_genome == 0){ - vec2 off = (uv-0.5)*u_ca*0.016; - col = vec3(sceneFor(u_mode, uv-off).r, sceneFor(u_mode, uv).g, sceneFor(u_mode, uv+off).b); - } else { - col = scene(uv); + /* chromatic fringe: cheap radial channel split, no scene re-render */ + if (u_ca > 0.004){ + float asp0 = u_res.x/u_res.y; + float r2 = length((uv - 0.5)*vec2(asp0, 1.0)); + float w = clamp(u_ca, 0.0, 1.0)*smoothstep(0.18, 0.85, r2)*0.45; + vec3 shifted = vec3( + hueRotate(col, 10.0).r, + col.g, + hueRotate(col, -10.0).b); + col = mix(col, shifted, w); } /* glow: soft luminance knee */ @@ -713,3 +676,7 @@ void main(){ fragColor = vec4(clamp(col, 0.0, 1.0), 1.0); } `; + +var FRAG_SRC_BASE = "#version 300 es\n#define HAS_GENOME 0\n" + FRAG_BODY; +var FRAG_SRC_FULL = "#version 300 es\n#define HAS_GENOME 1\n" + FRAG_BODY; +var FRAG_SRC = FRAG_SRC_FULL; /* legacy alias */ diff --git a/styles.css b/styles.css index 74f7df7..f89be0b 100644 --- a/styles.css +++ b/styles.css @@ -236,6 +236,23 @@ body.ready .canvas-boot { opacity: 0; } body.ready #view { opacity: 1; } +.stage-error { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + text-align: center; + color: #9b9ba4; + font-size: 13px; + line-height: 1.6; + background: rgba(9, 9, 11, 0.92); + border-radius: var(--radius-l); + z-index: 2; +} +.stage-error[hidden] { display: none !important; } + .stage-meta { display: flex; align-items: center;