diff --git a/README.md b/README.md
index 0936160..75c1e67 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/assets/apple-touch-icon.png b/assets/apple-touch-icon.png
index c1d0052..980b98b 100644
Binary files a/assets/apple-touch-icon.png and b/assets/apple-touch-icon.png differ
diff --git a/assets/favicon.png b/assets/favicon.png
index f66bdf1..d5fa01c 100644
Binary files a/assets/favicon.png and b/assets/favicon.png differ
diff --git a/assets/icon-512.png b/assets/icon-512.png
index dd9265f..4d93d42 100644
Binary files a/assets/icon-512.png and b/assets/icon-512.png differ
diff --git a/docs.html b/docs.html
index 2911d90..f5ef103 100644
--- a/docs.html
+++ b/docs.html
@@ -101,7 +101,7 @@
Styles
Pick one of the 9 base styles in the Style section: Chrome, Silk, Bloom, Aura, Rays, Halftone, Glyphs, Reeded and Mosaic. Each one is its own renderer with its own character.
-
Synth styles go beyond the base set: New synth style blends two renderers with a random mix operation (organic mask, screen, multiply, radial or diagonal split) into a completely new look. The Synth blend slider controls how strongly the second layer shows. The big Randomize button also produces synth styles some of the time.
+
Generated styles go far beyond the base set: New style 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 Randomize button also discovers new styles some of the time.
Found something you love? Save style stores it in your browser. Saved styles appear as chips you can reload with one click, and they survive page reloads.
diff --git a/index.html b/index.html
index 93f6884..5fba21a 100644
--- a/index.html
+++ b/index.html
@@ -110,6 +110,7 @@
+
diff --git a/js/engine.js b/js/engine.js
index 38bef3d..326b353 100644
--- a/js/engine.js
+++ b/js/engine.js
@@ -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) {
diff --git a/js/exporter.js b/js/exporter.js
index 8046f8a..08e40f2 100644
--- a/js/exporter.js
+++ b/js/exporter.js
@@ -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 + ")");
}
}
diff --git a/js/fx.js b/js/fx.js
new file mode 100644
index 0000000..b630d5e
--- /dev/null
+++ b/js/fx.js
@@ -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 = '' + message + "";
+ 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 };
+})();
diff --git a/js/main.js b/js/main.js
index 698c34d..ca3c679 100644
--- a/js/main.js
+++ b/js/main.js
@@ -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 = 'New synth style';
+ btnSynth.innerHTML = '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 = '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) {
diff --git a/js/modals.js b/js/modals.js
index dfe8851..3d10aa8 100644
--- a/js/modals.js
+++ b/js/modals.js
@@ -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();
diff --git a/js/shaders.js b/js/shaders.js
index 21f64cf..327ab54 100644
--- a/js/shaders.js
+++ b/js/shaders.js
@@ -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);
}
diff --git a/styles.css b/styles.css
index 3f2c647..a828e63 100644
--- a/styles.css
+++ b/styles.css
@@ -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; }