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:
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 ---------- */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user