Fix boot safely, add social OG image, restore performance gains

Guard fitCanvas until Engine is ready, show a boot placeholder during shader compile, generate a 1200x630 OG image for X and WhatsApp, and point repo meta to lumenshaders.vercel.app.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonxlnx
2026-06-11 17:39:45 +02:00
parent bf0e0517e2
commit 87e9af6b0f
7 changed files with 200 additions and 89 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+8 -1
View File
@@ -7,7 +7,10 @@
<meta name="description" content="How to use LUMEN: styles, synth styles, color, motion, share codes, gradient sets and exporting PNG, video or GIF." />
<link rel="canonical" href="https://lumenshaders.vercel.app/docs.html" />
<meta property="og:url" content="https://lumenshaders.vercel.app/docs.html" />
<meta property="og:image" content="https://lumenshaders.vercel.app/assets/og.png" />
<meta property="og:image" content="https://lumenshaders.vercel.app/assets/og.jpg" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<link rel="icon" type="image/png" sizes="64x64" href="assets/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -173,5 +176,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>
+18 -4
View File
@@ -11,16 +11,23 @@
<meta property="og:description" content="Looping abstract shader art in your browser. Randomize, fine-tune, share design codes and export PNG, video or GIF. No dependencies." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://lumenshaders.vercel.app/" />
<meta property="og:image" content="https://lumenshaders.vercel.app/assets/og.png" />
<meta property="og:image" content="https://lumenshaders.vercel.app/assets/og.jpg" />
<meta property="og:image:secure_url" content="https://lumenshaders.vercel.app/assets/og.jpg" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="LUMEN generative shader studio with reeded glass ember art" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://lumenshaders.vercel.app/assets/og.png" />
<meta name="twitter:title" content="LUMEN, the generative shader studio" />
<meta name="twitter:description" content="Looping abstract shader art in your browser. Randomize, fine-tune, share design codes and export PNG, video or GIF." />
<meta name="twitter:image" content="https://lumenshaders.vercel.app/assets/og.jpg" />
<link rel="canonical" href="https://lumenshaders.vercel.app/" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<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">
@@ -69,7 +76,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">
@@ -116,5 +126,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>
+19 -3
View File
@@ -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,
+99 -76
View File
@@ -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 =
'<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 +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 =
'<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;
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();
});
});
});
});
+56 -5
View File
@@ -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 ---------- */