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 <cursoragent@cursor.com>
This commit is contained in:
+4
-3
@@ -79,6 +79,7 @@
|
|||||||
<div class="canvas-shell" id="canvas-shell">
|
<div class="canvas-shell" id="canvas-shell">
|
||||||
<div class="canvas-boot" id="canvas-boot" aria-hidden="true"></div>
|
<div class="canvas-boot" id="canvas-boot" aria-hidden="true"></div>
|
||||||
<canvas id="view"></canvas>
|
<canvas id="view"></canvas>
|
||||||
|
<div class="stage-error" id="stage-error" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stage-meta">
|
<div class="stage-meta">
|
||||||
@@ -116,8 +117,8 @@
|
|||||||
<div class="toast mono" id="toast" hidden></div>
|
<div class="toast mono" id="toast" hidden></div>
|
||||||
|
|
||||||
<script src="js/palettes.js"></script>
|
<script src="js/palettes.js"></script>
|
||||||
<script src="js/shaders.js"></script>
|
<script src="js/shaders.js?v=fix4"></script>
|
||||||
<script src="js/engine.js"></script>
|
<script src="js/engine.js?v=fix4"></script>
|
||||||
<script src="js/gifenc.js"></script>
|
<script src="js/gifenc.js"></script>
|
||||||
<script src="js/webmmux.js"></script>
|
<script src="js/webmmux.js"></script>
|
||||||
<script src="js/zip.js"></script>
|
<script src="js/zip.js"></script>
|
||||||
@@ -125,7 +126,7 @@
|
|||||||
<script src="js/exporter.js"></script>
|
<script src="js/exporter.js"></script>
|
||||||
<script src="js/modals.js"></script>
|
<script src="js/modals.js"></script>
|
||||||
<script src="js/ui.js"></script>
|
<script src="js/ui.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js?v=fix4"></script>
|
||||||
<script>
|
<script>
|
||||||
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
|
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+110
-24
@@ -10,6 +10,7 @@ var Engine = (function () {
|
|||||||
var loopT = 0; // seconds into current loop
|
var loopT = 0; // seconds into current loop
|
||||||
var lastTick = 0;
|
var lastTick = 0;
|
||||||
var fps = 60, fpsAcc = 0, fpsN = 0, fpsCb = null;
|
var fps = 60, fpsAcc = 0, fpsN = 0, fpsCb = null;
|
||||||
|
var maxFps = 60, minFrameMs = 1000 / 60, lastDraw = 0;
|
||||||
var getParams = null; // injected: () => P
|
var getParams = null; // injected: () => P
|
||||||
|
|
||||||
var UNIFORM_NAMES = [
|
var UNIFORM_NAMES = [
|
||||||
@@ -34,6 +35,49 @@ var Engine = (function () {
|
|||||||
return sh;
|
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) {
|
function init(canvasEl, paramsGetter, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
canvas = canvasEl;
|
canvas = canvasEl;
|
||||||
@@ -43,16 +87,20 @@ var Engine = (function () {
|
|||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: true,
|
||||||
powerPreference: "default"
|
powerPreference: "default"
|
||||||
});
|
});
|
||||||
if (!gl) throw new Error("WebGL2 not available");
|
if (!gl) {
|
||||||
|
if (opts.onError) { opts.onError("WebGL2 not available"); return; }
|
||||||
program = gl.createProgram();
|
throw new Error("WebGL2 not available");
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
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();
|
var buf = gl.createBuffer();
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||||
@@ -60,18 +108,40 @@ var Engine = (function () {
|
|||||||
gl.enableVertexAttribArray(0);
|
gl.enableVertexAttribArray(0);
|
||||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
UNIFORM_NAMES.forEach(function (n) {
|
buildProgram(FRAG_SRC_BASE, parallel, function (baseProg, err) {
|
||||||
uniforms[n] = gl.getUniformLocation(program, n);
|
if (!baseProg) {
|
||||||
});
|
if (opts.onError) opts.onError("Program link error: " + err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
adoptProgram(baseProg);
|
||||||
|
ready = true;
|
||||||
|
lastTick = performance.now();
|
||||||
|
started = false;
|
||||||
|
|
||||||
ready = true;
|
if (opts.onReady) opts.onReady();
|
||||||
lastTick = performance.now();
|
if (opts.autostart !== false) start();
|
||||||
started = false;
|
|
||||||
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() {
|
function start() {
|
||||||
if (!ready || started) return;
|
if (!canRender() || started) return;
|
||||||
started = true;
|
started = true;
|
||||||
suspended = false;
|
suspended = false;
|
||||||
lastTick = performance.now();
|
lastTick = performance.now();
|
||||||
@@ -79,13 +149,14 @@ var Engine = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSize(w, h) {
|
function setSize(w, h) {
|
||||||
if (!canvas) return;
|
if (!canvas || !canRender()) return;
|
||||||
canvas.width = w;
|
canvas.width = w;
|
||||||
canvas.height = h;
|
canvas.height = h;
|
||||||
if (gl) gl.viewport(0, 0, w, h);
|
gl.viewport(0, 0, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushUniforms(P, phase) {
|
function pushUniforms(P, phase) {
|
||||||
|
if (!canRender()) return;
|
||||||
gl.uniform2f(uniforms.u_res, canvas.width, canvas.height);
|
gl.uniform2f(uniforms.u_res, canvas.width, canvas.height);
|
||||||
gl.uniform1f(uniforms.u_phase, phase);
|
gl.uniform1f(uniforms.u_phase, phase);
|
||||||
gl.uniform1f(uniforms.u_seed, P.seed);
|
gl.uniform1f(uniforms.u_seed, P.seed);
|
||||||
@@ -136,6 +207,7 @@ var Engine = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAt(phase) {
|
function renderAt(phase) {
|
||||||
|
if (!canRender()) return;
|
||||||
var P = getParams();
|
var P = getParams();
|
||||||
pushUniforms(P, phase);
|
pushUniforms(P, phase);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||||
@@ -147,7 +219,12 @@ var Engine = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tick(now) {
|
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);
|
var dt = Math.min((now - lastTick) / 1000, 0.1);
|
||||||
lastTick = now;
|
lastTick = now;
|
||||||
@@ -167,6 +244,11 @@ var Engine = (function () {
|
|||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMaxFps(n) {
|
||||||
|
maxFps = Math.max(15, Math.min(n, 60));
|
||||||
|
minFrameMs = 1000 / maxFps;
|
||||||
|
}
|
||||||
|
|
||||||
function readPixels() {
|
function readPixels() {
|
||||||
var w = canvas.width, h = canvas.height;
|
var w = canvas.width, h = canvas.height;
|
||||||
var buf = new Uint8Array(w * h * 4);
|
var buf = new Uint8Array(w * h * 4);
|
||||||
@@ -190,16 +272,20 @@ var Engine = (function () {
|
|||||||
currentPhase: currentPhase,
|
currentPhase: currentPhase,
|
||||||
resetTime: function () { loopT = 0; },
|
resetTime: function () { loopT = 0; },
|
||||||
setLoopTime: function (t) { loopT = t; },
|
setLoopTime: function (t) { loopT = t; },
|
||||||
suspend: function () { suspended = true; },
|
suspend: function () { suspended = true; started = false; },
|
||||||
resume: function () {
|
resume: function () {
|
||||||
|
if (!canRender()) return;
|
||||||
suspended = false;
|
suspended = false;
|
||||||
lastTick = performance.now();
|
started = false;
|
||||||
requestAnimationFrame(tick);
|
start();
|
||||||
},
|
},
|
||||||
|
isLost: isLost,
|
||||||
|
setMaxFps: setMaxFps,
|
||||||
|
hasGenome: function () { return fullReady; },
|
||||||
setPlaying: function (v) { playing = v; lastTick = performance.now(); },
|
setPlaying: function (v) { playing = v; lastTick = performance.now(); },
|
||||||
isPlaying: function () { return playing; },
|
isPlaying: function () { return playing; },
|
||||||
onFps: function (cb) { fpsCb = cb; },
|
onFps: function (cb) { fpsCb = cb; },
|
||||||
canvas: function () { return canvas; },
|
canvas: function () { return canvas; },
|
||||||
size: function () { return [canvas.width, canvas.height]; }
|
size: function () { return canvas ? [canvas.width, canvas.height] : [0, 0]; }
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
+91
-68
@@ -241,7 +241,7 @@ function styleName() {
|
|||||||
|
|
||||||
function randomizeAll() {
|
function randomizeAll() {
|
||||||
if (!P.lockStyle) {
|
if (!P.lockStyle) {
|
||||||
if (rnd() < 0.42) {
|
if (rnd() < 0.42 && Engine.hasGenome()) {
|
||||||
generateGenomeStyle();
|
generateGenomeStyle();
|
||||||
} else {
|
} else {
|
||||||
P.synthOn = false;
|
P.synthOn = false;
|
||||||
@@ -401,6 +401,10 @@ function buildRail() {
|
|||||||
var btnSynth = UI.el("button", "mini-btn", synthRow);
|
var btnSynth = UI.el("button", "mini-btn", synthRow);
|
||||||
btnSynth.innerHTML = '<svg viewBox="0 0 16 16"><path d="M8 1.5 L9.8 6.2 L14.5 8 L9.8 9.8 L8 14.5 L6.2 9.8 L1.5 8 L6.2 6.2 Z" fill="none" stroke="currentColor" stroke-width="1.3"/></svg>New style';
|
btnSynth.innerHTML = '<svg viewBox="0 0 16 16"><path d="M8 1.5 L9.8 6.2 L14.5 8 L9.8 9.8 L8 14.5 L6.2 9.8 L1.5 8 L6.2 6.2 Z" fill="none" stroke="currentColor" stroke-width="1.3"/></svg>New style';
|
||||||
btnSynth.addEventListener("click", function () {
|
btnSynth.addEventListener("click", function () {
|
||||||
|
if (!Engine.hasGenome()) {
|
||||||
|
UI.toast("Style synthesizer is still warming up, try again in a moment");
|
||||||
|
return;
|
||||||
|
}
|
||||||
generateGenomeStyle();
|
generateGenomeStyle();
|
||||||
newSeed();
|
newSeed();
|
||||||
refreshAll();
|
refreshAll();
|
||||||
@@ -553,8 +557,12 @@ function updateMeta() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stage = { failed: false, resizeObs: null, resizeTimer: null, bufW: 0, bufH: 0 };
|
||||||
|
|
||||||
function fitCanvas(preview) {
|
function fitCanvas(preview) {
|
||||||
|
if (stage.failed) return;
|
||||||
var frame = document.getElementById("canvas-frame");
|
var frame = document.getElementById("canvas-frame");
|
||||||
|
if (!frame) return;
|
||||||
var availW = frame.clientWidth - 80;
|
var availW = frame.clientWidth - 80;
|
||||||
var availH = frame.clientHeight - 52;
|
var availH = frame.clientHeight - 52;
|
||||||
if (availW <= 0 || availH <= 0) return;
|
if (availW <= 0 || availH <= 0) return;
|
||||||
@@ -564,6 +572,7 @@ function fitCanvas(preview) {
|
|||||||
w = Math.round(w);
|
w = Math.round(w);
|
||||||
h = Math.round(h);
|
h = Math.round(h);
|
||||||
var canvasEl = document.getElementById("view");
|
var canvasEl = document.getElementById("view");
|
||||||
|
if (!canvasEl || !canvasEl.isConnected) return;
|
||||||
canvasEl.style.width = w + "px";
|
canvasEl.style.width = w + "px";
|
||||||
canvasEl.style.height = h + "px";
|
canvasEl.style.height = h + "px";
|
||||||
var boot = document.getElementById("canvas-boot");
|
var boot = document.getElementById("canvas-boot");
|
||||||
@@ -571,13 +580,37 @@ function fitCanvas(preview) {
|
|||||||
boot.style.width = w + "px";
|
boot.style.width = w + "px";
|
||||||
boot.style.height = h + "px";
|
boot.style.height = h + "px";
|
||||||
}
|
}
|
||||||
if (!Engine.isReady()) return;
|
if (!Engine.isReady() || Engine.isLost()) return;
|
||||||
var dprCap = preview ? 0.75 : 1.35;
|
var lowMem = (navigator.deviceMemory || 8) <= 4;
|
||||||
|
var dprCap = preview ? 0.65 : (lowMem ? 0.75 : 0.85);
|
||||||
var dpr = Math.min(window.devicePixelRatio || 1, dprCap);
|
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();
|
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) {
|
function setPlayingUI(v) {
|
||||||
Engine.setPlaying(v);
|
Engine.setPlaying(v);
|
||||||
document.getElementById("icon-pause").style.display = v ? "" : "none";
|
document.getElementById("icon-pause").style.display = v ? "" : "none";
|
||||||
@@ -586,34 +619,6 @@ function setPlayingUI(v) {
|
|||||||
|
|
||||||
/* ---------------- boot ---------------- */
|
/* ---------------- 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() {
|
function wireControls() {
|
||||||
var segRefresh = UI.segmented(
|
var segRefresh = UI.segmented(
|
||||||
document.getElementById("aspect-seg"),
|
document.getElementById("aspect-seg"),
|
||||||
@@ -645,57 +650,75 @@ function wireControls() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showBootError(msg) {
|
function showStageError(msg) {
|
||||||
document.body.classList.remove("booting");
|
var el = document.getElementById("stage-error");
|
||||||
document.querySelector(".canvas-frame").innerHTML =
|
if (!el) return;
|
||||||
'<div style="color:#9b9ba4;font-size:13px;max-width:380px;text-align:center;line-height:1.6">' +
|
el.innerHTML =
|
||||||
"WebGL2 is required. Please use a recent Chrome, Edge or Firefox.<br><span style=\"color:#5e5e68;font-size:11px\">" +
|
"WebGL2 is required. Please use a recent Chrome, Edge or Firefox.<br>" +
|
||||||
String(msg).split("\n")[0] + "</span></div>";
|
'<span style="color:#5e5e68;font-size:11px">' + String(msg).split("\n")[0] + "</span>";
|
||||||
|
el.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishBoot(opts) {
|
function showBootError(msg) {
|
||||||
|
stage.failed = true;
|
||||||
|
detachStageResize();
|
||||||
|
Engine.suspend();
|
||||||
document.body.classList.remove("booting");
|
document.body.classList.remove("booting");
|
||||||
document.body.classList.add("ready");
|
document.body.classList.remove("ready");
|
||||||
Engine.start();
|
showStageError(msg);
|
||||||
if (opts.intro) runIntro();
|
}
|
||||||
setTimeout(function () { fitCanvas(false); }, 1400);
|
|
||||||
|
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 () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
var canvas = document.getElementById("view");
|
var canvas = document.getElementById("view");
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
var lowPower = (navigator.hardwareConcurrency || 8) <= 4 ||
|
var lowPower = (navigator.hardwareConcurrency || 8) <= 4 ||
|
||||||
((navigator.deviceMemory || 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 () {
|
requestAnimationFrame(function () {
|
||||||
fitCanvas(true);
|
Engine.init(canvas, function () { return P; }, {
|
||||||
buildRail();
|
autostart: false,
|
||||||
wireControls();
|
onContextLost: onContextLost,
|
||||||
|
onError: showBootError,
|
||||||
|
onReady: function () {
|
||||||
|
Engine.setMaxFps(lowPower ? 30 : 45);
|
||||||
|
|
||||||
var hash = decodeURIComponent(location.hash.slice(1) || "");
|
var hash = decodeURIComponent(location.hash.slice(1) || "");
|
||||||
if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) {
|
if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) {
|
||||||
UI.toast("Shared design loaded");
|
UI.toast("Shared design loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
new ResizeObserver(function () {
|
fitCanvas(false);
|
||||||
fitCanvas(!document.body.classList.contains("ready"));
|
stage.resizeObs = new ResizeObserver(onStageResize);
|
||||||
}).observe(document.getElementById("canvas-frame"));
|
stage.resizeObs.observe(document.getElementById("canvas-frame"));
|
||||||
|
|
||||||
requestAnimationFrame(function () {
|
document.body.classList.remove("booting");
|
||||||
try {
|
document.body.classList.add("ready");
|
||||||
Engine.init(canvas, function () { return P; }, { autostart: false });
|
Engine.start();
|
||||||
} catch (e) {
|
|
||||||
showBootError(e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fitCanvas(true);
|
|
||||||
Engine.renderAt(0);
|
|
||||||
|
|
||||||
requestAnimationFrame(function () {
|
|
||||||
finishBoot({ intro: !lowPower && !reduceMotion });
|
|
||||||
updateMeta();
|
updateMeta();
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+51
-84
@@ -6,7 +6,10 @@ var VERT_SRC = "#version 300 es\n" +
|
|||||||
"layout(location=0) in vec2 a_pos;\n" +
|
"layout(location=0) in vec2 a_pos;\n" +
|
||||||
"void main(){ gl_Position = vec4(a_pos, 0.0, 1.0); }\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 float;
|
||||||
precision highp int;
|
precision highp int;
|
||||||
|
|
||||||
@@ -179,10 +182,6 @@ vec3 sceneChrome(vec2 uv){
|
|||||||
col += alb2 * spec2 * u_light * 1.35;
|
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 += 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;
|
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;
|
return col;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,37 +386,22 @@ vec3 boldField(vec2 p){
|
|||||||
return col;
|
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){
|
vec3 sceneReeded(vec2 uv){
|
||||||
float ridgeFreq = max(u_lines*0.55, 6.0);
|
float ridgeFreq = max(u_lines*0.55, 6.0);
|
||||||
float nx = uv.x * ridgeFreq;
|
float nx = uv.x * ridgeFreq;
|
||||||
float ci = floor(nx);
|
float ci = floor(nx);
|
||||||
float lx = fract(nx) - 0.5;
|
float lx = fract(nx) - 0.5;
|
||||||
|
|
||||||
/* lens refraction across each flute */
|
|
||||||
float lens = sin(lx*PI);
|
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 srcX = (ci + 0.5 + refr) / ridgeFreq;
|
||||||
float blur = 0.0035*u_soft*(0.6 + u_warp*0.5);
|
vec3 col = boldField(toP(vec2(srcX, uv.y))*0.8);
|
||||||
vec3 col = sampleReeded(vec2(srcX, uv.y), blur);
|
|
||||||
|
|
||||||
float ridge = cos(lx*PI);
|
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));
|
float groove = smoothstep(0.48, 0.40, abs(lx));
|
||||||
col *= mix(0.50, shade, groove);
|
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)));
|
||||||
/* fresnel rim + peak specular */
|
col += vec3(1.0)*spec*u_light*0.14;
|
||||||
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);
|
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +425,7 @@ vec3 sceneMosaic(vec2 uv){
|
|||||||
12 genes select field, domain geometry, color mapping,
|
12 genes select field, domain geometry, color mapping,
|
||||||
shading and overlay. Each combination is a distinct style.
|
shading and overlay. Each combination is a distinct style.
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
#if HAS_GENOME
|
||||||
|
|
||||||
float gnVoro(vec2 p){
|
float gnVoro(vec2 p){
|
||||||
vec2 i = floor(p), f = fract(p);
|
vec2 i = floor(p), f = fract(p);
|
||||||
@@ -476,7 +461,7 @@ float gnField(int ft, vec2 p){
|
|||||||
}
|
}
|
||||||
if (ft == 5) { /* flow */
|
if (ft == 5) { /* flow */
|
||||||
float f1 = fbm(p + so + lt);
|
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 */
|
if (ft == 6) { /* cross bands */
|
||||||
float bx = sin(p.x*(2.2 + u_g3.x*4.5) + lt.x*2.0);
|
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 */
|
if (ft == 7) { /* plaid grid */
|
||||||
vec2 q = p*(1.8 + u_g3.z*1.2);
|
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 j = fbm(p*0.35 + so)*0.15;
|
||||||
float gy = step(0.5, fract(q.y + fbm(p.yx*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));
|
return mix(gx*gy, 1.0 - gx*gy, 0.5 + 0.5*sin(lt.x*3.0));
|
||||||
}
|
}
|
||||||
if (ft == 8) { /* hex lattice edges */
|
if (ft == 8) { /* hex lattice edges */
|
||||||
@@ -577,14 +563,18 @@ vec3 sceneGenome(vec2 uv){
|
|||||||
vec2 so = SO();
|
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;
|
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);
|
vec3 col = gnColor(cm, f*1.15 - 0.05, p);
|
||||||
|
|
||||||
if (sh == 1) { /* embossed light */
|
if (sh == 1) { /* embossed light */
|
||||||
float e = 0.05;
|
vec3 n = normalize(vec3(-grad.x*2.0, -grad.y*2.0, 1.0));
|
||||||
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));
|
|
||||||
float la = u_lightAngle*PI/180.0;
|
float la = u_lightAngle*PI/180.0;
|
||||||
vec3 L = normalize(vec3(cos(la), sin(la), 0.6));
|
vec3 L = normalize(vec3(cos(la), sin(la), 0.6));
|
||||||
float diff = max(dot(n, L), 0.0);
|
float diff = max(dot(n, L), 0.0);
|
||||||
@@ -592,27 +582,16 @@ vec3 sceneGenome(vec2 uv){
|
|||||||
col *= 0.35 + 0.8*diff;
|
col *= 0.35 + 0.8*diff;
|
||||||
col += vec3(1.0)*spec*u_light*0.7;
|
col += vec3(1.0)*spec*u_light*0.7;
|
||||||
} else if (sh == 2) { /* glowing edges */
|
} else if (sh == 2) { /* glowing edges */
|
||||||
float e = 0.04;
|
float g = (abs(fx - f) + abs(fy - f))*1.25;
|
||||||
float g = abs(gnField(ft, p + vec2(e,0.0)) - f) + abs(gnField(ft, p + vec2(0.0,e)) - f);
|
|
||||||
col = mix(u_bg, col, 0.35);
|
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;
|
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 */
|
} else if (sh == 3) { /* deep contrast carve */
|
||||||
col *= smoothstep(0.0, 0.55, f)*1.15;
|
col *= smoothstep(0.0, 0.55, f)*1.15;
|
||||||
col = mix(u_bg, col, smoothstep(0.08, 0.35, f));
|
col = mix(u_bg, col, smoothstep(0.08, 0.35, f));
|
||||||
} else if (sh == 4) { /* glass refraction */
|
} else if (sh == 4) { /* glass sheen */
|
||||||
float e = 0.055;
|
float fres = pow(1.0 - clamp(length(grad)*1.4, 0.0, 1.0), 2.2);
|
||||||
float fx = gnField(ft, p + vec2(e, 0.0));
|
col *= 0.86 + 0.14*abs(sin(p0.x*u_lines*0.28));
|
||||||
float fy = gnField(ft, p + vec2(0.0, e));
|
col += vec3(1.0)*fres*u_light*0.16;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ov == 1) { /* stripes */
|
if (ov == 1) { /* stripes */
|
||||||
@@ -631,6 +610,7 @@ vec3 sceneGenome(vec2 uv){
|
|||||||
|
|
||||||
return col;
|
return col;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/* ---------------- dispatch + post ---------------- */
|
/* ---------------- dispatch + post ---------------- */
|
||||||
|
|
||||||
@@ -646,47 +626,30 @@ vec3 sceneFor(int m, vec2 uv){
|
|||||||
return sceneMosaic(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){
|
vec3 scene(vec2 uv){
|
||||||
|
#if HAS_GENOME
|
||||||
if (u_genome == 1) return sceneGenome(uv);
|
if (u_genome == 1) return sceneGenome(uv);
|
||||||
vec3 a = sceneFor(u_mode, uv);
|
#endif
|
||||||
if (u_synth == 0) return a;
|
return sceneFor(u_mode, uv);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void main(){
|
void main(){
|
||||||
vec2 uv = gl_FragCoord.xy/u_res;
|
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
|
/* chromatic fringe: cheap radial channel split, no scene re-render */
|
||||||
to keep the D3D linker's static inlining budget low. */
|
if (u_ca > 0.004){
|
||||||
if (u_ca > 0.004 && u_synth == 0 && u_genome == 0){
|
float asp0 = u_res.x/u_res.y;
|
||||||
vec2 off = (uv-0.5)*u_ca*0.016;
|
float r2 = length((uv - 0.5)*vec2(asp0, 1.0));
|
||||||
col = vec3(sceneFor(u_mode, uv-off).r, sceneFor(u_mode, uv).g, sceneFor(u_mode, uv+off).b);
|
float w = clamp(u_ca, 0.0, 1.0)*smoothstep(0.18, 0.85, r2)*0.45;
|
||||||
} else {
|
vec3 shifted = vec3(
|
||||||
col = scene(uv);
|
hueRotate(col, 10.0).r,
|
||||||
|
col.g,
|
||||||
|
hueRotate(col, -10.0).b);
|
||||||
|
col = mix(col, shifted, w);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* glow: soft luminance knee */
|
/* glow: soft luminance knee */
|
||||||
@@ -713,3 +676,7 @@ void main(){
|
|||||||
fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
|
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 */
|
||||||
|
|||||||
+17
@@ -236,6 +236,23 @@ body.ready .canvas-boot { opacity: 0; }
|
|||||||
|
|
||||||
body.ready #view { opacity: 1; }
|
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 {
|
.stage-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user