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:
@@ -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 |
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 + ")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user