Fix startup freeze on weak devices and add Vercel Analytics
Defer WebGL shader compile behind a visible boot placeholder, lower initial DPR, disable glass blur during boot, and lighten the intro so the page stays responsive instead of looking half-loaded. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -170,5 +170,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
|
||||
</script>
|
||||
<script defer src="/_vercel/insights/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+9
-2
@@ -19,7 +19,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<body class="booting">
|
||||
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
@@ -68,7 +68,10 @@
|
||||
<main class="app">
|
||||
<section class="stage" id="stage">
|
||||
<div class="canvas-frame" id="canvas-frame">
|
||||
<canvas id="view"></canvas>
|
||||
<div class="canvas-shell" id="canvas-shell">
|
||||
<div class="canvas-boot" id="canvas-boot" aria-hidden="true"></div>
|
||||
<canvas id="view"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stage-meta">
|
||||
<div class="meta-left">
|
||||
@@ -115,5 +118,9 @@
|
||||
<script src="js/modals.js"></script>
|
||||
<script src="js/ui.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
<script>
|
||||
window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
|
||||
</script>
|
||||
<script defer src="/_vercel/insights/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-2
@@ -5,6 +5,7 @@ var Engine = (function () {
|
||||
var canvas, gl, program, uniforms = {};
|
||||
var playing = true;
|
||||
var suspended = false;
|
||||
var started = false;
|
||||
var loopT = 0; // seconds into current loop
|
||||
var lastTick = 0;
|
||||
var fps = 60, fpsAcc = 0, fpsN = 0, fpsCb = null;
|
||||
@@ -32,13 +33,14 @@ var Engine = (function () {
|
||||
return sh;
|
||||
}
|
||||
|
||||
function init(canvasEl, paramsGetter) {
|
||||
function init(canvasEl, paramsGetter, opts) {
|
||||
opts = opts || {};
|
||||
canvas = canvasEl;
|
||||
getParams = paramsGetter;
|
||||
gl = canvas.getContext("webgl2", {
|
||||
antialias: false,
|
||||
preserveDrawingBuffer: true,
|
||||
powerPreference: "high-performance"
|
||||
powerPreference: "default"
|
||||
});
|
||||
if (!gl) throw new Error("WebGL2 not available");
|
||||
|
||||
@@ -61,6 +63,15 @@ var Engine = (function () {
|
||||
uniforms[n] = gl.getUniformLocation(program, n);
|
||||
});
|
||||
|
||||
lastTick = performance.now();
|
||||
started = false;
|
||||
if (opts.autostart) start();
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
suspended = false;
|
||||
lastTick = performance.now();
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
@@ -168,6 +179,7 @@ var Engine = (function () {
|
||||
|
||||
return {
|
||||
init: init,
|
||||
start: start,
|
||||
setSize: setSize,
|
||||
renderAt: renderAt,
|
||||
readPixels: readPixels,
|
||||
|
||||
+93
-73
@@ -483,7 +483,7 @@ function updateMeta() {
|
||||
document.getElementById("meta-res").textContent = s[0] + "\u00d7" + s[1];
|
||||
}
|
||||
|
||||
function fitCanvas() {
|
||||
function fitCanvas(preview) {
|
||||
var frame = document.getElementById("canvas-frame");
|
||||
var availW = frame.clientWidth - 80;
|
||||
var availH = frame.clientHeight - 52;
|
||||
@@ -491,10 +491,18 @@ function fitCanvas() {
|
||||
var ar = ASPECTS[P.aspect];
|
||||
var w = availW, h = w / ar;
|
||||
if (h > availH) { h = availH; w = h * ar; }
|
||||
w = Math.round(w);
|
||||
h = Math.round(h);
|
||||
var canvas = Engine.canvas();
|
||||
canvas.style.width = Math.round(w) + "px";
|
||||
canvas.style.height = Math.round(h) + "px";
|
||||
var dpr = Math.min(window.devicePixelRatio || 1, 1.35);
|
||||
canvas.style.width = w + "px";
|
||||
canvas.style.height = h + "px";
|
||||
var boot = document.getElementById("canvas-boot");
|
||||
if (boot) {
|
||||
boot.style.width = w + "px";
|
||||
boot.style.height = h + "px";
|
||||
}
|
||||
var dprCap = preview ? 0.75 : 1.35;
|
||||
var dpr = Math.min(window.devicePixelRatio || 1, dprCap);
|
||||
Engine.setSize(2 * Math.round(w * dpr / 2), 2 * Math.round(h * dpr / 2));
|
||||
updateMeta();
|
||||
}
|
||||
@@ -507,21 +515,35 @@ function setPlayingUI(v) {
|
||||
|
||||
/* ---------------- boot ---------------- */
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var canvas = document.getElementById("view");
|
||||
try {
|
||||
Engine.init(canvas, function () { return P; });
|
||||
} catch (e) {
|
||||
document.querySelector(".canvas-frame").innerHTML =
|
||||
'<div style="color:#9b9ba4;font-size:13px;max-width:380px;text-align:center;line-height:1.6">' +
|
||||
"WebGL2 is required. Please use a recent Chrome, Edge or Firefox.<br><span style=\"color:#5e5e68;font-size:11px\">" +
|
||||
String(e.message).split("\n")[0] + "</span></div>";
|
||||
return;
|
||||
}
|
||||
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); });
|
||||
|
||||
buildRail();
|
||||
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);
|
||||
}
|
||||
|
||||
/* aspect segmented control */
|
||||
function wireControls() {
|
||||
var segRefresh = UI.segmented(
|
||||
document.getElementById("aspect-seg"),
|
||||
Object.keys(ASPECTS).map(function (k) { return [k, k]; }),
|
||||
@@ -530,61 +552,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
);
|
||||
reg(segRefresh);
|
||||
|
||||
/* load a shared design from the URL hash, e.g. .../#LMN1.xxxx */
|
||||
var hash = decodeURIComponent(location.hash.slice(1) || "");
|
||||
if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) {
|
||||
UI.toast("Shared design loaded");
|
||||
}
|
||||
|
||||
fitCanvas();
|
||||
new ResizeObserver(fitCanvas).observe(document.getElementById("canvas-frame"));
|
||||
|
||||
/* load a shared design from the URL hash, e.g. site/#LMN1.xxxx */
|
||||
if (location.hash.length > 6) {
|
||||
if (decodeDesign(decodeURIComponent(location.hash.slice(1)))) {
|
||||
UI.toast("Shared design loaded");
|
||||
}
|
||||
}
|
||||
|
||||
/* intro: every element pops in big and settles into place,
|
||||
one after another. transform/opacity only, fully compositor-driven. */
|
||||
(function intro() {
|
||||
var items = [];
|
||||
document.querySelectorAll(".brand, .segmented button, .topbar-actions > *").forEach(function (n) { items.push(n); });
|
||||
items.push(document.querySelector(".canvas-frame"));
|
||||
items.push(document.querySelector(".stage-meta"));
|
||||
document.querySelectorAll(".rail-section").forEach(function (sec) {
|
||||
Array.prototype.forEach.call(sec.children, function (child) {
|
||||
if (child.classList.contains("mode-grid") ||
|
||||
child.classList.contains("preset-row") ||
|
||||
child.classList.contains("export-grid")) {
|
||||
Array.prototype.forEach.call(child.children, function (n) { items.push(n); });
|
||||
} else {
|
||||
items.push(child);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var d = 0;
|
||||
items.forEach(function (n, i) {
|
||||
if (!n) return;
|
||||
/* fast cadence inside the rail, slower for the hero pieces */
|
||||
d += i < 12 ? 50 : 16;
|
||||
n.classList.add("stagger-item");
|
||||
n.style.setProperty("--intro-d", Math.min(d, 2100) + "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");
|
||||
n.style.willChange = "auto";
|
||||
});
|
||||
}, 3000);
|
||||
})();
|
||||
|
||||
Engine.onFps(function (fps) {
|
||||
document.getElementById("meta-fps").textContent = fps + " fps";
|
||||
});
|
||||
@@ -605,6 +572,59 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
else if (e.key === "r" || e.key === "R") { randomizeAll(); }
|
||||
else if (e.key === "s" || e.key === "S") { Exporter.exportPNG(P, ASPECTS[P.aspect]); }
|
||||
});
|
||||
}
|
||||
|
||||
updateMeta();
|
||||
function showBootError(msg) {
|
||||
document.body.classList.remove("booting");
|
||||
document.querySelector(".canvas-frame").innerHTML =
|
||||
'<div style="color:#9b9ba4;font-size:13px;max-width:380px;text-align:center;line-height:1.6">' +
|
||||
"WebGL2 is required. Please use a recent Chrome, Edge or Firefox.<br><span style=\"color:#5e5e68;font-size:11px\">" +
|
||||
String(msg).split("\n")[0] + "</span></div>";
|
||||
}
|
||||
|
||||
function finishBoot(opts) {
|
||||
document.body.classList.remove("booting");
|
||||
document.body.classList.add("ready");
|
||||
Engine.start();
|
||||
if (opts.intro) runIntro();
|
||||
setTimeout(function () { fitCanvas(false); }, 1400);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var canvas = document.getElementById("view");
|
||||
var lowPower = (navigator.hardwareConcurrency || 8) <= 4 ||
|
||||
((navigator.deviceMemory || 8) <= 4);
|
||||
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
/* paint the shell first, then build UI, then compile shaders off the first frame */
|
||||
requestAnimationFrame(function () {
|
||||
buildRail();
|
||||
wireControls();
|
||||
|
||||
var hash = decodeURIComponent(location.hash.slice(1) || "");
|
||||
if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) {
|
||||
UI.toast("Shared design loaded");
|
||||
}
|
||||
|
||||
fitCanvas(true);
|
||||
new ResizeObserver(function () {
|
||||
fitCanvas(!document.body.classList.contains("ready"));
|
||||
}).observe(document.getElementById("canvas-frame"));
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
try {
|
||||
Engine.init(canvas, function () { return P; }, { autostart: false });
|
||||
} catch (e) {
|
||||
showBootError(e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
Engine.renderAt(0);
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
finishBoot({ intro: !lowPower && !reduceMotion });
|
||||
updateMeta();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+57
-5
@@ -189,6 +189,38 @@ button { font: inherit; color: inherit; cursor: pointer; background: none; borde
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.canvas-shell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.canvas-boot {
|
||||
position: absolute;
|
||||
border-radius: var(--radius-l);
|
||||
background:
|
||||
linear-gradient(135deg, #120804 0%, #3d1204 38%, #ff6a00 72%, #ffb347 100%);
|
||||
background-size: 220% 220%;
|
||||
animation: boot-shimmer 2.2s ease-in-out infinite;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.07),
|
||||
inset 0 1px 0 rgba(255,255,255,0.08),
|
||||
0 30px 80px -20px rgba(0,0,0,0.9),
|
||||
0 8px 26px -10px rgba(0,0,0,0.65);
|
||||
transition: opacity 0.45s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes boot-shimmer {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
body.ready .canvas-boot { opacity: 0; }
|
||||
|
||||
#view {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -199,8 +231,12 @@ button { font: inherit; color: inherit; cursor: pointer; background: none; borde
|
||||
0 30px 80px -20px rgba(0,0,0,0.9),
|
||||
0 8px 26px -10px rgba(0,0,0,0.65);
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.45s ease;
|
||||
}
|
||||
|
||||
body.ready #view { opacity: 1; }
|
||||
|
||||
.stage-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -818,19 +854,35 @@ select { cursor: pointer; appearance: none; padding-right: 22px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* ---------- boot: keep shell visible, defer heavy glass ---------- */
|
||||
|
||||
body.booting .topbar,
|
||||
body.booting .rail {
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
body.booting .topbar { background: var(--bg); }
|
||||
body.booting .rail {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
body.ready .rail {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.35s ease;
|
||||
}
|
||||
|
||||
/* ---------- intro stagger animation ---------- */
|
||||
|
||||
@keyframes intro-pop {
|
||||
0% { opacity: 0; transform: scale(1.45) translateY(-4px); }
|
||||
62% { opacity: 1; }
|
||||
0% { opacity: 0; transform: scale(1.1) translateY(6px); }
|
||||
100% { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
.intro .stagger-item {
|
||||
animation: intro-pop 560ms cubic-bezier(.22,1.3,.36,1) both;
|
||||
animation: intro-pop 420ms cubic-bezier(.22,1,.36,1) both;
|
||||
animation-delay: var(--intro-d, 0ms);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.intro .canvas-frame.stagger-item { animation-duration: 780ms; }
|
||||
.intro .canvas-shell.stagger-item { animation-duration: 520ms; }
|
||||
|
||||
/* ---------- download success effect ---------- */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user