True style synthesizer, success effects, granular intro, dark selects

- New style button now runs a 12-gene genome renderer (6 fields x 5
  domain geometries x 4 color maps x 4 shading models x 4 overlays)
  producing standalone styles instead of blending existing modes
- Share codes v3 carry the genome, v1/v2 codes still decode
- Download completion fires a palette-colored particle burst, pulse
  ring and animated success check toast
- Intro animation staggers every individual control, compositor-only
- color-scheme dark fixes white native select dropdowns in modals
- Sharper liquid-glass diamond favicon and touch icons

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonxlnx
2026-06-10 17:32:28 +02:00
parent 6d8c96d1db
commit 458cac66b7
13 changed files with 392 additions and 58 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ Requires a browser with WebGL2 (Chrome, Edge, Firefox).
- **Randomize** (or press `R`): new style, palette, form, lighting and seed, tuned per art style so results stay good. Sometimes it invents a brand-new synth style.
- **Style**: 9 art modes, each with its own renderer. "Keep style" locks the mode while randomizing.
- **Synth styles**: "New synth style" blends two renderers with a random mix operation (organic mask, screen, multiply, radial, diagonal) into a completely new look, with a blend slider. "Save style" keeps favorites in your browser as one-click chips.
- **Generated styles**: "New style" runs a 12-gene style synthesizer (6 field types x 5 domain geometries x 4 color mappings x 4 shading models x 4 overlays) that produces standalone styles which do not exist in the base set, like FLUX, RIDGE, WAVE, RING, CELL and FLOW archetypes. "Save style" keeps favorites in your browser as one-click chips.
- **Gradient sets**: the Set button renders 4 to 12 variations of the current design (same style and palette, different seeds) and downloads them as a ZIP of PNGs, ideal for using consistent art across one website.
- **Color**: 16 curated palettes plus a harmonic palette generator, 4 editable colors + background, hue / saturation / exposure.
- **Form**: seed, zoom, fbm detail, domain warp, turbulence, anisotropic stretch.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

After

Width:  |  Height:  |  Size: 442 KiB

+1 -1
View File
@@ -101,7 +101,7 @@
<div class="docs-section">
<h2>Styles</h2>
<p>Pick one of the 9 base styles in the <strong>Style</strong> section: Chrome, Silk, Bloom, Aura, Rays, Halftone, Glyphs, Reeded and Mosaic. Each one is its own renderer with its own character.</p>
<p><strong>Synth styles</strong> go beyond the base set: <code>New synth style</code> blends two renderers with a random mix operation (organic mask, screen, multiply, radial or diagonal split) into a completely new look. The <strong>Synth blend</strong> slider controls how strongly the second layer shows. The big <strong>Randomize</strong> button also produces synth styles some of the time.</p>
<p><strong>Generated styles</strong> go far beyond the base set: <code>New style</code> runs a style synthesizer with 12 genes covering field type, domain geometry, color mapping, shading model and surface overlay. Every gene set is a standalone style that does not exist in the base nine, named by its field archetype: FLUX, RIDGE, WAVE, RING, CELL or FLOW. The big <strong>Randomize</strong> button also discovers new styles some of the time.</p>
<p>Found something you love? <code>Save style</code> stores it in your browser. Saved styles appear as chips you can reload with one click, and they survive page reloads.</p>
</div>
+1
View File
@@ -110,6 +110,7 @@
<script src="js/gifenc.js"></script>
<script src="js/webmmux.js"></script>
<script src="js/zip.js"></script>
<script src="js/fx.js"></script>
<script src="js/exporter.js"></script>
<script src="js/modals.js"></script>
<script src="js/ui.js"></script>
+8 -1
View File
@@ -18,7 +18,8 @@ var Engine = (function () {
"u_light", "u_gloss", "u_lightAngle", "u_irid", "u_glow",
"u_grain", "u_cell", "u_lines", "u_ca", "u_vig", "u_soft",
"u_travel",
"u_synth", "u_modeB", "u_mixOp", "u_blend"
"u_synth", "u_modeB", "u_mixOp", "u_blend",
"u_genome", "u_g1", "u_g2", "u_g3"
];
function compile(type, src) {
@@ -112,6 +113,12 @@ var Engine = (function () {
gl.uniform1i(uniforms.u_modeB, P.modeB | 0);
gl.uniform1i(uniforms.u_mixOp, P.mixOp | 0);
gl.uniform1f(uniforms.u_blend, P.blend);
var g = P.genes || [0,0,0,0, 0,0,0,0, 0,0,0,0];
gl.uniform1i(uniforms.u_genome, P.genomeOn ? 1 : 0);
gl.uniform4f(uniforms.u_g1, g[0], g[1], g[2], g[3]);
gl.uniform4f(uniforms.u_g2, g[4], g[5], g[6], g[7]);
gl.uniform4f(uniforms.u_g3, g[8], g[9], g[10], g[11]);
}
function renderAt(phase) {
+3 -3
View File
@@ -49,7 +49,7 @@ var Exporter = (function () {
busy = false;
if (blob) {
download(blob, stamp(P, "png"));
UI.toast("Saved " + w + "\u00d7" + h + " PNG");
FX.celebrate("Saved " + w + "\u00d7" + h + " PNG");
}
}, "image/png");
}
@@ -193,7 +193,7 @@ var Exporter = (function () {
hideOverlay();
busy = false;
download(new Blob([webm], { type: "video/webm" }), stamp(P, "webm"));
UI.toast("Saved " + totalSec.toFixed(1) + "s WEBM \u00b7 " + nFrames + " frames \u00b7 " + w + "\u00d7" + h + " @ " + fps + "fps");
FX.celebrate("Saved " + totalSec.toFixed(1) + "s video \u00b7 " + w + "\u00d7" + h + " @ " + fps + "fps");
function restore() {
Engine.setSize(prev[0], prev[1]);
@@ -246,7 +246,7 @@ var Exporter = (function () {
busy = false;
if (data && !cancelled) {
download(new Blob([data], { type: "image/gif" }), stamp(P, "gif"));
UI.toast("Saved " + nFrames + "-frame looping GIF (" + w + "\u00d7" + h + ")");
FX.celebrate("Saved " + nFrames + "-frame looping GIF (" + w + "\u00d7" + h + ")");
}
}
+99
View File
@@ -0,0 +1,99 @@
/* Celebration effect for finished exports: a palette-colored particle
burst with an expanding ring and a success toast with a drawn check.
Pure transform/opacity + one lightweight 2d canvas, auto-cleans. */
var FX = (function () {
function celebrate(message) {
burst();
successToast(message);
}
function burst() {
var c = document.createElement("canvas");
c.className = "fx-layer";
var dpr = Math.min(window.devicePixelRatio || 1, 2);
c.width = innerWidth * dpr;
c.height = innerHeight * dpr;
document.body.appendChild(c);
var ctx = c.getContext("2d");
ctx.scale(dpr, dpr);
var cx = innerWidth / 2;
var cy = innerHeight * 0.42;
var ring = document.createElement("div");
ring.className = "fx-ring";
ring.style.left = cx + "px";
ring.style.top = cy + "px";
document.body.appendChild(ring);
setTimeout(function () { ring.remove(); }, 750);
var colors = [P.c1, P.c2, P.c3, P.c4, "#ffffff"];
var parts = [];
var N = 110;
for (var i = 0; i < N; i++) {
var a = Math.random() * Math.PI * 2;
var sp = 4 + Math.random() * 13;
parts.push({
x: cx, y: cy,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp - 3,
r: 1.5 + Math.random() * 3.2,
rot: Math.random() * Math.PI,
vr: (Math.random() - 0.5) * 0.3,
col: colors[(Math.random() * colors.length) | 0],
shape: Math.random() < 0.4 ? 1 : 0,
life: 1
});
}
var t0 = performance.now();
function frame(now) {
var dt = Math.min((now - t0) / 1000, 2);
ctx.clearRect(0, 0, innerWidth, innerHeight);
var alive = false;
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
p.x += p.vx; p.y += p.vy;
p.vx *= 0.965; p.vy = p.vy * 0.965 + 0.32;
p.rot += p.vr;
p.life -= 0.012 + Math.random() * 0.006;
if (p.life <= 0) continue;
alive = true;
ctx.globalAlpha = Math.max(p.life, 0);
ctx.fillStyle = p.col;
if (p.shape === 1) {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillRect(-p.r, -p.r * 0.55, p.r * 2, p.r * 1.1);
ctx.restore();
} else {
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * p.life, 0, Math.PI * 2);
ctx.fill();
}
}
if (alive && dt < 2) requestAnimationFrame(frame);
else c.remove();
}
requestAnimationFrame(frame);
}
var toastTimer = null;
function successToast(message) {
var t = document.getElementById("toast");
t.innerHTML = '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="6.6" fill="none" stroke="#7de8a4" stroke-width="1.4" opacity="0.6"/><path d="M5 8.2 L7.2 10.4 L11.2 5.8" fill="none" stroke="#7de8a4" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg><span>' + message + "</span>";
t.classList.add("success");
t.hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
t.hidden = true;
t.classList.remove("success");
t.textContent = "";
}, 3400);
}
return { celebrate: celebrate };
})();
+90 -36
View File
@@ -37,6 +37,7 @@ var P = {
grain: 0.024, cell: 113, lines: 67, ca: 0.018, vig: 0.079, soft: 1.14,
travel: 0.72, loop: 7.5,
synthOn: false, modeB: 2, mixOp: 0, blend: 0.6,
genomeOn: false, genes: [0,0,0.5,3, 0,0,0,0.5, 0.5,0.5,0.5,0],
lockStyle: false,
imgRes: "2160", vidRes: "1080", vidFps: "30", vidLen: "l2",
gifW: "640", gifFps: 25, gifDither: true, gifLoop: true,
@@ -135,33 +136,50 @@ function randomizeKeys(keys) {
function newSeed() { P.seed = Math.floor(rnd() * 10000); }
/* pairs that blend into something coherent instead of mud */
var SYNTH_PAIRS = [
[0, 2], [0, 3], [0, 5], [1, 2], [1, 4], [2, 5], [2, 6], [3, 4],
[3, 5], [4, 5], [4, 6], [2, 7], [3, 8], [0, 8], [1, 5], [4, 8]
];
function generateSynthStyle() {
var pair = SYNTH_PAIRS[Math.floor(rnd() * SYNTH_PAIRS.length)];
var flip = rnd() < 0.5;
P.mode = flip ? pair[1] : pair[0];
P.modeB = flip ? pair[0] : pair[1];
P.mixOp = Math.floor(rnd() * 5);
P.blend = 0.4 + rnd() * 0.55;
P.synthOn = true;
/* genome: 12 genes define a complete standalone style that does not
exist in the base set. fields x domains x colors x shading x overlays
gives thousands of distinct archetypes. */
function generateGenomeStyle() {
P.genes = [
Math.floor(rnd() * 6), // 0 field type
Math.floor(rnd() * 5), // 1 domain op
rnd() * 1.6, // 2 extra warp
2 + Math.floor(rnd() * 6), // 3 fold count
Math.floor(rnd() * 4), // 4 color map
Math.floor(rnd() * 4), // 5 shading
Math.floor(rnd() * 4), // 6 overlay
0.3 + rnd() * 1.2, // 7 overlay scale
rnd(), // 8 ridge sharpness
rnd(), // 9 poster steps
0.2 + rnd() * 0.9, // 10 field scale
rnd() // 11 rotation
];
P.genomeOn = true;
P.synthOn = false;
}
function synthName() {
return "SYN " + MODES[P.mode].name.slice(0, 3).toUpperCase() + "+" +
function genomeName() {
var g = P.genes;
var n = (g[0] * 7 + g[1] * 13 + g[4] * 29 + g[5] * 47 + g[6] * 71 +
Math.round(g[11] * 99)) % 1000;
var FIELD = ["FLUX", "RIDGE", "WAVE", "RING", "CELL", "FLOW"];
return FIELD[g[0]] + " " + String(Math.round(n)).padStart(3, "0");
}
function styleName() {
if (P.genomeOn) return genomeName();
if (P.synthOn) return "SYN " + MODES[P.mode].name.slice(0, 3).toUpperCase() + "+" +
MODES[P.modeB].name.slice(0, 3).toUpperCase();
return MODES[P.mode].full;
}
function randomizeAll() {
if (!P.lockStyle) {
if (rnd() < 0.38) {
generateSynthStyle();
if (rnd() < 0.35) {
generateGenomeStyle();
} else {
P.synthOn = false;
P.genomeOn = false;
P.mode = Math.floor(rnd() * MODES.length);
}
}
@@ -184,8 +202,7 @@ function persistSavedStyles(list) {
}
function saveCurrentStyle() {
var list = loadSavedStyles();
var base = P.synthOn ? synthName() : MODES[P.mode].name;
var name = base + " " + String(Math.round(P.seed)).padStart(4, "0");
var name = styleName() + " " + String(Math.round(P.seed)).padStart(4, "0");
list.unshift({ name: name, code: encodeDesign(), ts: Date.now() });
if (list.length > 24) list.length = 24;
persistSavedStyles(list);
@@ -205,11 +222,13 @@ var SHARE_NUMS = [
];
function encodeDesign() {
var arr = [2, P.mode, Math.round(P.seed),
var arr = [3, P.mode, Math.round(P.seed),
P.c1.slice(1), P.c2.slice(1), P.c3.slice(1), P.c4.slice(1), P.bg.slice(1),
P.aspect];
SHARE_NUMS.forEach(function (k) { arr.push(Math.round(P[k] * 1000) / 1000); });
arr.push(P.synthOn ? 1 : 0, P.modeB | 0, P.mixOp | 0, Math.round(P.blend * 1000) / 1000);
arr.push(P.genomeOn ? 1 : 0);
P.genes.forEach(function (g) { arr.push(Math.round(g * 1000) / 1000); });
var b64 = btoa(JSON.stringify(arr))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return "LMN1." + b64;
@@ -225,7 +244,8 @@ function decodeDesign(code) {
var baseLen = 9 + SHARE_NUMS.length;
var isV1 = Array.isArray(arr) && arr[0] === 1 && arr.length === baseLen;
var isV2 = Array.isArray(arr) && arr[0] === 2 && arr.length === baseLen + 4;
if (!isV1 && !isV2) return false;
var isV3 = Array.isArray(arr) && arr[0] === 3 && arr.length === baseLen + 17;
if (!isV1 && !isV2 && !isV3) return false;
var hexOk = function (h) { return /^[0-9a-fA-F]{6}$/.test(h); };
if (![arr[3], arr[4], arr[5], arr[6], arr[7]].every(hexOk)) return false;
@@ -239,7 +259,7 @@ function decodeDesign(code) {
var v = Number(arr[9 + i]);
if (isFinite(v)) P[k] = v;
});
if (isV2) {
if (isV2 || isV3) {
P.synthOn = !!arr[baseLen];
P.modeB = Math.min(Math.max(Math.round(arr[baseLen + 1]) || 0, 0), MODES.length - 1);
P.mixOp = Math.min(Math.max(Math.round(arr[baseLen + 2]) || 0, 0), 4);
@@ -248,6 +268,16 @@ function decodeDesign(code) {
} else {
P.synthOn = false;
}
if (isV3) {
P.genomeOn = !!arr[baseLen + 4];
P.genes = [];
for (var gi = 0; gi < 12; gi++) {
var gv = Number(arr[baseLen + 5 + gi]);
P.genes.push(isFinite(gv) ? gv : 0);
}
} else {
P.genomeOn = false;
}
activePreset = -1;
setPresetActive(-1);
refreshAll();
@@ -295,18 +325,18 @@ function buildRail() {
P.mode = Math.floor(rnd() * MODES.length);
refreshAll();
});
reg(UI.modeGrid(sStyle, MODES, function () { return P.synthOn ? -1 : P.mode; },
function (v) { P.mode = v; P.synthOn = false; refreshAll(); }));
reg(UI.modeGrid(sStyle, MODES, function () { return (P.synthOn || P.genomeOn) ? -1 : P.mode; },
function (v) { P.mode = v; P.synthOn = false; P.genomeOn = false; refreshAll(); }));
/* synth: generated styles */
/* genome: brand-new generated styles */
var synthRow = UI.el("div", "share-row synth-row", sStyle);
var btnSynth = UI.el("button", "mini-btn", synthRow);
btnSynth.innerHTML = '<svg viewBox="0 0 16 16"><path d="M2 8 C4 3 7 13 9 8 C11 3 13 11 14 8" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="8" cy="2.5" r="1.2" fill="currentColor"/></svg>New synth 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 () {
generateSynthStyle();
generateGenomeStyle();
newSeed();
refreshAll();
UI.toast("Generated " + synthName());
UI.toast("New style discovered: " + genomeName());
});
var btnSave = UI.el("button", "mini-btn", synthRow);
btnSave.innerHTML = '<svg viewBox="0 0 16 16"><path d="M3 2 H11 L14 5 V14 H3 Z" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="5.5" y="9" width="5" height="4" fill="currentColor"/></svg>Save style';
@@ -446,8 +476,7 @@ function buildRail() {
/* ---------------- stage / meta ---------------- */
function updateMeta() {
document.getElementById("meta-mode").textContent =
P.synthOn ? synthName() : MODES[P.mode].full;
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();
@@ -517,18 +546,43 @@ document.addEventListener("DOMContentLoaded", function () {
}
}
/* intro: elements pop in big and settle into place, one after another */
/* 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(".topbar > *").forEach(function (n) { items.push(n); });
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 (n) { items.push(n); });
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) n.style.setProperty("--intro-d", (i * 65) + "ms");
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.length * 65 + 1100);
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) {
+1 -1
View File
@@ -250,8 +250,8 @@ var Modals = (function () {
a.download = "lumen-set-" + MODES[P.mode].key + ".zip";
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 4000);
UI.toast("Set saved: " + entries.length + " PNGs (" + w + "\u00d7" + h + ")");
close();
FX.celebrate("Set saved: " + entries.length + " PNGs (" + w + "\u00d7" + h + ")");
});
await build();
+137 -6
View File
@@ -29,12 +29,18 @@ uniform float u_light, u_gloss, u_lightAngle, u_irid, u_glow;
uniform float u_grain, u_cell, u_lines, u_ca, u_vig, u_soft;
uniform float u_travel;
/* synth: procedural style combinator */
/* synth: procedural style combinator (legacy share codes) */
uniform int u_synth; // 0 = single mode, 1 = blend two modes
uniform int u_modeB;
uniform int u_mixOp; // 0 noise mask, 1 screen, 2 multiply, 3 radial, 4 diagonal
uniform float u_blend;
/* genome: parametric style synthesizer, every gene set is its own style */
uniform int u_genome; // 1 = render the genome style
uniform vec4 u_g1; // field type, domain op, warp amount, fold count
uniform vec4 u_g2; // color map, shading, overlay, overlay scale
uniform vec4 u_g3; // ridge sharpness, poster steps, field scale, rotation
out vec4 fragColor;
#define TAU 6.28318530718
@@ -408,6 +414,131 @@ vec3 sceneMosaic(vec2 uv){
return col;
}
/* =========================================================
GENOME, the parametric style synthesizer.
12 genes select field, domain geometry, color mapping,
shading and overlay. Each combination is a distinct style.
========================================================= */
float gnVoro(vec2 p){
vec2 i = floor(p), f = fract(p);
float d = 8.0;
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++){
vec2 g = vec2(float(x), float(y));
vec2 o = hash22(i + g + floor(u_seed));
d = min(d, length(g + o - f));
}
return d;
}
float gnField(int ft, vec2 p){
vec2 so = SO(), lt = LT();
if (ft == 0) return fbm(p + so + lt);
if (ft == 1) { /* ridged */
float v = 1.0 - abs(2.0*fbm(p + so + lt) - 1.0);
return pow(v, 1.0 + u_g3.x*4.0);
}
if (ft == 2) { /* wave interference */
float a = sin(p.x*2.1 + fbm(p*0.7 + so + lt)*6.0);
float b = sin(p.y*1.7 + fbm(p.yx*0.8 - so - lt)*6.0);
return a*b*0.25 + 0.5;
}
if (ft == 3) { /* warped rings */
float d = length(p) + (fbm(p*0.9 + so + lt) - 0.5)*1.2;
return fract(d*(1.0 + u_g3.x*2.0));
}
if (ft == 4) { /* cellular */
float v = gnVoro(p*1.4 + lt*0.8);
return pow(clamp(v, 0.0, 1.0), 0.8 + u_g3.x*2.0);
}
/* flow: fbm fed through itself */
float f1 = fbm(p + so + lt);
return fbm(p + 2.4*vec2(f1, fbm(p + so*1.3 - lt)) + so);
}
vec2 gnDomain(int dop, vec2 p){
p = rot(u_g3.w*TAU) * p;
if (dop == 1) { /* polar */
return vec2(atan(p.y, p.x)*(1.0 + floor(u_g1.w*0.5)), length(p)*1.6);
}
if (dop == 2) { /* kaleidoscope */
float n = 2.0 + floor(u_g1.w);
float a = atan(p.y, p.x);
float seg = TAU/n;
a = abs(mod(a, seg) - seg*0.5);
return vec2(cos(a), sin(a))*length(p);
}
if (dop == 3) return abs(p); /* mirror */
if (dop == 4) { /* soft grid repeat */
return (fract(p*0.5) - 0.5)*2.6;
}
return p;
}
vec3 gnColor(int cm, float t, vec2 p){
t = clamp(t, 0.0, 1.0);
if (cm == 1) return paletteCyc(t*1.4);
if (cm == 2) { /* posterized */
float steps = 3.0 + floor(u_g3.y*5.0);
return palette(floor(t*steps)/(steps - 1.0));
}
if (cm == 3) { /* duotone + highlight pop */
vec3 c = mix(u_bg, u_c1, smoothstep(0.15, 0.75, t));
return mix(c, u_c3, smoothstep(0.82, 0.98, t));
}
return palette(t);
}
vec3 sceneGenome(vec2 uv){
vec2 p0 = toP(uv);
int ft = int(u_g1.x);
int dop = int(u_g1.y);
int cm = int(u_g2.x);
int sh = int(u_g2.y);
int ov = int(u_g2.z);
vec2 p = gnDomain(dop, p0*(0.6 + u_g3.z*1.4));
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;
float f = gnField(ft, p);
vec3 col = gnColor(cm, f*1.15 - 0.05, p);
if (sh == 1) { /* embossed light */
float e = 0.05;
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;
vec3 L = normalize(vec3(cos(la), sin(la), 0.6));
float diff = max(dot(n, L), 0.0);
float spec = pow(max(dot(n, normalize(L + vec3(0,0,1))), 0.0), u_gloss);
col *= 0.35 + 0.8*diff;
col += vec3(1.0)*spec*u_light*0.7;
} else if (sh == 2) { /* glowing edges */
float e = 0.04;
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 += 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 */
col *= smoothstep(0.0, 0.55, f)*1.25;
col = mix(u_bg, col, smoothstep(0.08, 0.35, f));
}
if (ov == 1) { /* stripes */
float s = sin(p0.x*u_g2.w*40.0 + f*6.0);
col *= 0.82 + 0.18*smoothstep(-0.2, 0.4, s);
} else if (ov == 2) { /* dot lattice */
vec2 g = fract(p0*u_g2.w*16.0) - 0.5;
col *= 0.78 + 0.22*smoothstep(0.42, 0.30, length(g));
} else if (ov == 3) { /* scanlines */
col *= 0.86 + 0.14*sin(uv.y*u_res.y*0.7 + f*3.0);
}
return col;
}
/* ---------------- dispatch + post ---------------- */
vec3 sceneFor(int m, vec2 uv){
@@ -423,6 +554,7 @@ vec3 sceneFor(int m, vec2 uv){
}
vec3 scene(vec2 uv){
if (u_genome == 1) return sceneGenome(uv);
vec3 a = sceneFor(u_mode, uv);
if (u_synth == 0) return a;
@@ -455,12 +587,11 @@ void main(){
vec2 uv = gl_FragCoord.xy/u_res;
vec3 col;
/* CA re-renders the scene per channel; with synth blending that would
inline the full mode dispatch 6 times and break the D3D linker, so
aberration only applies to single-mode renders */
if (u_ca > 0.004 && u_synth == 0){
/* CA re-renders per channel. It calls the single-mode renderer directly
to keep the D3D linker's static inlining budget low. */
if (u_ca > 0.004 && u_synth == 0 && u_genome == 0){
vec2 off = (uv-0.5)*u_ca*0.016;
col = vec3(scene(uv-off).r, scene(uv).g, scene(uv+off).b);
col = vec3(sceneFor(u_mode, uv-off).r, sceneFor(u_mode, uv).g, sceneFor(u_mode, uv+off).b);
} else {
col = scene(uv);
}
+51 -9
View File
@@ -28,6 +28,8 @@
* { box-sizing: border-box; margin: 0; padding: 0; }
:root { color-scheme: dark; }
html, body { height: 100%; }
body {
@@ -819,18 +821,58 @@ select { cursor: pointer; appearance: none; padding-right: 22px;
/* ---------- intro stagger animation ---------- */
@keyframes intro-pop {
0% { opacity: 0; transform: scale(1.5) translateY(-6px); filter: blur(5px); }
60% { opacity: 1; filter: blur(0); }
100% { opacity: 1; transform: scale(1) translateY(0); filter: blur(0); }
0% { opacity: 0; transform: scale(1.45) translateY(-4px); }
62% { opacity: 1; }
100% { opacity: 1; transform: scale(1) translateY(0); }
}
.intro .topbar > *,
.intro .rail-section,
.intro .stage-meta,
.intro .canvas-frame {
animation: intro-pop 640ms cubic-bezier(.22,1.25,.36,1) both;
.intro .stagger-item {
animation: intro-pop 560ms cubic-bezier(.22,1.3,.36,1) both;
animation-delay: var(--intro-d, 0ms);
will-change: transform, opacity;
}
.intro .canvas-frame { animation-duration: 800ms; }
.intro .canvas-frame.stagger-item { animation-duration: 780ms; }
/* ---------- download success effect ---------- */
.fx-layer {
position: fixed;
inset: 0;
z-index: 80;
pointer-events: none;
}
@keyframes fx-ring {
0% { transform: translate(-50%,-50%) scale(0.3); opacity: 0.9; }
100% { transform: translate(-50%,-50%) scale(2.4); opacity: 0; }
}
.fx-ring {
position: fixed;
width: 180px; height: 180px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.8);
z-index: 80;
pointer-events: none;
animation: fx-ring 700ms cubic-bezier(.2,.8,.3,1) forwards;
}
@keyframes toast-success-pop {
0% { opacity: 0; transform: translateX(-50%) scale(1.4); }
55% { opacity: 1; transform: translateX(-50%) scale(0.97); }
100% { opacity: 1; transform: translateX(-50%) scale(1); }
}
.toast.success {
display: inline-flex;
align-items: center;
gap: 9px;
border-color: rgba(140, 240, 170, 0.35);
box-shadow: 0 10px 38px -8px rgba(40, 200, 110, 0.25);
animation: toast-success-pop 420ms cubic-bezier(.22,1.4,.36,1);
}
.toast.success svg { width: 14px; height: 14px; flex: none; }
.toast.success svg path {
stroke-dasharray: 20;
stroke-dashoffset: 20;
animation: check-draw 360ms 150ms ease-out forwards;
}
@keyframes check-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }