diff --git a/assets/og.jpg b/assets/og.jpg new file mode 100644 index 0000000..60a2e34 Binary files /dev/null and b/assets/og.jpg differ diff --git a/assets/og.png b/assets/og.png index d1a5a1c..f87ed9b 100644 Binary files a/assets/og.png and b/assets/og.png differ diff --git a/docs.html b/docs.html index 01918bf..15daf23 100644 --- a/docs.html +++ b/docs.html @@ -7,7 +7,10 @@ - + + + + @@ -173,5 +176,9 @@ + + diff --git a/index.html b/index.html index 8016f95..9eab2b5 100644 --- a/index.html +++ b/index.html @@ -11,16 +11,23 @@ - + + + + + + - + + + - +
@@ -69,7 +76,10 @@
- +
+ + +
@@ -116,5 +126,9 @@ + + diff --git a/js/engine.js b/js/engine.js index 326b353..0720552 100644 --- a/js/engine.js +++ b/js/engine.js @@ -5,6 +5,8 @@ var Engine = (function () { var canvas, gl, program, uniforms = {}; var playing = true; var suspended = false; + var started = false; + var ready = false; var loopT = 0; // seconds into current loop var lastTick = 0; var fps = 60, fpsAcc = 0, fpsN = 0, fpsCb = null; @@ -32,13 +34,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,14 +64,25 @@ var Engine = (function () { uniforms[n] = gl.getUniformLocation(program, n); }); + ready = true; + lastTick = performance.now(); + started = false; + if (opts.autostart !== false) start(); + } + + function start() { + if (!ready || started) return; + started = true; + suspended = false; lastTick = performance.now(); requestAnimationFrame(tick); } function setSize(w, h) { + if (!canvas) return; canvas.width = w; canvas.height = h; - gl.viewport(0, 0, w, h); + if (gl) gl.viewport(0, 0, w, h); } function pushUniforms(P, phase) { @@ -168,6 +182,8 @@ var Engine = (function () { return { init: init, + start: start, + isReady: function () { return ready; }, setSize: setSize, renderAt: renderAt, readPixels: readPixels, diff --git a/js/main.js b/js/main.js index ca3c679..c8ea4ad 100644 --- a/js/main.js +++ b/js/main.js @@ -479,11 +479,13 @@ function updateMeta() { document.getElementById("meta-mode").textContent = styleName(); document.getElementById("meta-seed").textContent = "seed " + String(Math.round(P.seed)).padStart(4, "0"); document.getElementById("meta-loop").textContent = P.loop.toFixed(1) + "s loop"; - var s = Engine.size(); - document.getElementById("meta-res").textContent = s[0] + "\u00d7" + s[1]; + if (Engine.isReady()) { + var s = Engine.size(); + 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 +493,19 @@ function fitCanvas() { var ar = ASPECTS[P.aspect]; var w = availW, h = w / ar; if (h > availH) { h = availH; w = h * ar; } - 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); + w = Math.round(w); + h = Math.round(h); + var canvasEl = document.getElementById("view"); + canvasEl.style.width = w + "px"; + canvasEl.style.height = h + "px"; + var boot = document.getElementById("canvas-boot"); + if (boot) { + boot.style.width = w + "px"; + boot.style.height = h + "px"; + } + if (!Engine.isReady()) return; + 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 +518,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 = - '
' + - "WebGL2 is required. Please use a recent Chrome, Edge or Firefox.
" + - String(e.message).split("\n")[0] + "
"; - 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 +555,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 +575,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 = + '
' + + "WebGL2 is required. Please use a recent Chrome, Edge or Firefox.
" + + String(msg).split("\n")[0] + "
"; +} + +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; + + requestAnimationFrame(function () { + fitCanvas(true); + buildRail(); + wireControls(); + + 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")); + + 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 }); + updateMeta(); + }); + }); + }); }); diff --git a/styles.css b/styles.css index a828e63..74f7df7 100644 --- a/styles.css +++ b/styles.css @@ -189,6 +189,37 @@ 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 +230,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 +853,35 @@ select { cursor: pointer; appearance: none; padding-right: 22px; border-radius: 5px; } +/* ---------- boot: shell visible while WebGL compiles ---------- */ + +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 ---------- */