Major update: synth styles, gradient sets, export dialogs, glass UI, docs

- Synth styles: blend any two renderers with five mix operations into
  brand-new generated looks, savable to localStorage as style chips
- Share codes bumped to v2 (synth fields), v1 codes still decode
- Gradient set generator: 4-12 seed variations with preview grid and
  ZIP batch download via a dependency-free store-only ZIP writer
- Export buttons now open dialogs with live preview and settings
- UI redesign: glassy translucent surfaces, rounder geometry, staggered
  intro animation, hover micro-interactions, refined scrollbars
- Docs page, generated favicon/touch icons and OG image, meta tags
- Skip chromatic aberration during synth blends to keep the D3D shader
  linker under its inlining limit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonxlnx
2026-06-10 16:13:52 +02:00
parent dcd29a97d5
commit 6d8c96d1db
14 changed files with 1041 additions and 93 deletions
+7 -1
View File
@@ -17,7 +17,8 @@ var Engine = (function () {
"u_scale", "u_complex", "u_warp", "u_flow", "u_stretch",
"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_travel",
"u_synth", "u_modeB", "u_mixOp", "u_blend"
];
function compile(type, src) {
@@ -106,6 +107,11 @@ var Engine = (function () {
gl.uniform1f(uniforms.u_soft, P.soft);
gl.uniform1f(uniforms.u_travel, P.travel);
gl.uniform1i(uniforms.u_synth, P.synthOn ? 1 : 0);
gl.uniform1i(uniforms.u_modeB, P.modeB | 0);
gl.uniform1i(uniforms.u_mixOp, P.mixOp | 0);
gl.uniform1f(uniforms.u_blend, P.blend);
}
function renderAt(phase) {
+1 -1
View File
@@ -91,7 +91,7 @@ var Exporter = (function () {
async function exportVideo(P, aspect) {
if (busy) return;
if (typeof VideoEncoder === "undefined" || typeof VideoFrame === "undefined") {
UI.toast("This browser has no WebCodecs support \u2014 use a current Chrome, Edge or Firefox");
UI.toast("This browser has no WebCodecs support, use a current Chrome, Edge or Firefox");
return;
}
busy = true;
+146 -28
View File
@@ -36,6 +36,7 @@ var P = {
light: 1.175, gloss: 44, lightAngle: 235, irid: 0.012, glow: 0.471,
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,
lockStyle: false,
imgRes: "2160", vidRes: "1080", vidFps: "30", vidLen: "l2",
gifW: "640", gifFps: 25, gifDither: true, gifLoop: true,
@@ -134,14 +135,65 @@ 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;
}
function synthName() {
return "SYN " + MODES[P.mode].name.slice(0, 3).toUpperCase() + "+" +
MODES[P.modeB].name.slice(0, 3).toUpperCase();
}
function randomizeAll() {
if (!P.lockStyle) P.mode = Math.floor(rnd() * MODES.length);
if (!P.lockStyle) {
if (rnd() < 0.38) {
generateSynthStyle();
} else {
P.synthOn = false;
P.mode = Math.floor(rnd() * MODES.length);
}
}
randomizePalette();
randomizeKeys(FORM_KEYS.concat(LIGHT_KEYS, TEXTURE_KEYS, MOTION_KEYS));
newSeed();
refreshAll();
}
/* ---------------- saved styles (localStorage) ---------------- */
var STYLES_KEY = "lumen-styles-v1";
function loadSavedStyles() {
try { return JSON.parse(localStorage.getItem(STYLES_KEY)) || []; }
catch (e) { return []; }
}
function persistSavedStyles(list) {
try { localStorage.setItem(STYLES_KEY, JSON.stringify(list)); } catch (e) {}
}
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");
list.unshift({ name: name, code: encodeDesign(), ts: Date.now() });
if (list.length > 24) list.length = 24;
persistSavedStyles(list);
renderSavedStyles();
UI.toast("Style saved: " + name);
}
var renderSavedStyles = function () {};
/* ---------------- design codes (share) ---------------- */
var SHARE_NUMS = [
@@ -153,10 +205,11 @@ var SHARE_NUMS = [
];
function encodeDesign() {
var arr = [1, P.mode, Math.round(P.seed),
var arr = [2, 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);
var b64 = btoa(JSON.stringify(arr))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return "LMN1." + b64;
@@ -169,7 +222,10 @@ function decodeDesign(code) {
var b64 = code.slice(5).replace(/-/g, "+").replace(/_/g, "/");
while (b64.length % 4) b64 += "=";
var arr = JSON.parse(atob(b64));
if (!Array.isArray(arr) || arr[0] !== 1 || arr.length !== 9 + SHARE_NUMS.length) return false;
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 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;
@@ -183,6 +239,15 @@ function decodeDesign(code) {
var v = Number(arr[9 + i]);
if (isFinite(v)) P[k] = v;
});
if (isV2) {
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);
var bl = Number(arr[baseLen + 3]);
P.blend = isFinite(bl) ? Math.min(Math.max(bl, 0), 1) : 0.6;
} else {
P.synthOn = false;
}
activePreset = -1;
setPresetActive(-1);
refreshAll();
@@ -226,10 +291,56 @@ function buildRail() {
/* STYLE */
var sStyle = UI.section(rail, "Style", function () {
P.synthOn = false;
P.mode = Math.floor(rnd() * MODES.length);
refreshAll();
});
reg(UI.modeGrid(sStyle, MODES, get("mode"), function (v) { P.mode = v; updateMeta(); }));
reg(UI.modeGrid(sStyle, MODES, function () { return P.synthOn ? -1 : P.mode; },
function (v) { P.mode = v; P.synthOn = false; refreshAll(); }));
/* synth: 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.addEventListener("click", function () {
generateSynthStyle();
newSeed();
refreshAll();
UI.toast("Generated " + synthName());
});
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';
btnSave.addEventListener("click", saveCurrentStyle);
var synthCtl = UI.el("div", "synth-ctl", sStyle);
reg(UI.slider(synthCtl, { label: "Synth blend", min: 0, max: 1, step: 0.01, fmt: fmt2,
get: get("blend"), set: function (v) { P.blend = v; updateMeta(); } }));
reg(function () { synthCtl.style.display = P.synthOn ? "" : "none"; });
var savedWrap = UI.el("div", "saved-styles", sStyle);
renderSavedStyles = function () {
savedWrap.innerHTML = "";
var list = loadSavedStyles();
list.forEach(function (st, i) {
var chip = UI.el("button", "saved-chip", savedWrap);
var label = UI.el("span", null, chip);
label.textContent = st.name;
var del = UI.el("span", "saved-del", chip);
del.innerHTML = "&times;";
del.addEventListener("click", function (ev) {
ev.stopPropagation();
var l = loadSavedStyles();
l.splice(i, 1);
persistSavedStyles(l);
renderSavedStyles();
});
chip.addEventListener("click", function () {
if (decodeDesign(st.code)) UI.toast("Loaded " + st.name);
});
});
};
renderSavedStyles();
reg(UI.lockRow(sStyle, { label: "Keep style when randomizing", get: get("lockStyle"), set: set("lockStyle") }));
/* COLOR */
@@ -286,7 +397,7 @@ function buildRail() {
var btnCopyCode = UI.el("button", "mini-btn", shareRow);
btnCopyCode.textContent = "Copy design code";
btnCopyCode.addEventListener("click", function () {
copyText(encodeDesign(), "Design code copied \u2014 paste it anywhere");
copyText(encodeDesign(), "Design code copied, paste it anywhere");
});
var btnCopyLink = UI.el("button", "mini-btn", shareRow);
btnCopyLink.textContent = "Copy link";
@@ -315,36 +426,28 @@ function buildRail() {
e.stopPropagation();
});
/* EXPORT */
/* EXPORT: buttons open a dialog with preview and settings */
var sExp = UI.section(rail, "Export", null);
reg(UI.selectRow(sExp, { label: "Image size", options: [["1080", "1920 \u00d7 1080"], ["1440", "2560 \u00d7 1440"], ["2160", "3840 \u00d7 2160"]], get: get("imgRes"), set: set("imgRes") }));
reg(UI.selectRow(sExp, { label: "Video size", options: [["720", "720p"], ["1080", "1080p"], ["1440", "1440p"]], get: get("vidRes"), set: set("vidRes") }));
reg(UI.selectRow(sExp, { label: "Video fps", options: [["24", "24 fps"], ["30", "30 fps"], ["60", "60 fps"]], get: get("vidFps"), set: set("vidFps") }));
reg(UI.selectRow(sExp, { label: "Video length", options: [
["l1", "1 loop"], ["l2", "2 loops"], ["l3", "3 loops"], ["l4", "4 loops"], ["l6", "6 loops"], ["l8", "8 loops"],
["s5", "5 seconds"], ["s10", "10 seconds"], ["s15", "15 seconds"], ["s30", "30 seconds"], ["s60", "60 seconds"]
], get: get("vidLen"), set: set("vidLen") }));
reg(UI.selectRow(sExp, { label: "GIF width", options: [["360", "360 px"], ["480", "480 px"], ["640", "640 px"], ["800", "800 px"]], get: get("gifW"), set: set("gifW") }));
reg(UI.selectRow(sExp, { label: "GIF fps", options: [["15", "15 fps"], ["20", "20 fps"], ["25", "25 fps"], ["30", "30 fps"]], get: function () { return String(P.gifFps); }, set: function (v) { P.gifFps = parseInt(v, 10); } }));
reg(UI.toggleRow(sExp, { label: "GIF dithering", get: get("gifDither"), set: set("gifDither") }));
reg(UI.toggleRow(sExp, { label: "GIF loop forever", get: get("gifLoop"), set: set("gifLoop") }));
var grid = UI.el("div", "export-grid", sExp);
UI.exportButton(grid, "Save image", "PNG",
UI.exportButton(grid, "Image", "PNG",
'<svg viewBox="0 0 16 16"><rect x="1.5" y="1.5" width="13" height="13" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="5.5" cy="5.5" r="1.5" fill="currentColor"/><path d="M2 12 L6 8 L9 11 L11.5 8.5 L14 11" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>',
function () { Exporter.exportPNG(P, ASPECTS[P.aspect]); });
UI.exportButton(grid, "Record video", "WEBM",
function () { Modals.openExport("png"); });
UI.exportButton(grid, "Video", "WEBM",
'<svg viewBox="0 0 16 16"><rect x="1.5" y="3.5" width="9" height="9" rx="2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.5 7 L14.5 4.5 V11.5 L10.5 9" fill="currentColor"/></svg>',
function () { Exporter.exportVideo(P, ASPECTS[P.aspect]); });
UI.exportButton(grid, "Render loop GIF", "GIF",
function () { Modals.openExport("video"); });
UI.exportButton(grid, "Looping GIF", "GIF",
'<svg viewBox="0 0 16 16"><path d="M13.5 8 a5.5 5.5 0 1 1 -1.6 -3.9" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M13.8 1.6 V4.4 H11" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>',
function () { Exporter.exportGIF(P, ASPECTS[P.aspect]); });
function () { Modals.openExport("gif"); });
UI.exportButton(grid, "Gradient set", "ZIP",
'<svg viewBox="0 0 16 16"><rect x="1.5" y="1.5" width="5.5" height="5.5" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="1.5" width="5.5" height="5.5" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><rect x="1.5" y="9" width="5.5" height="5.5" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="9" width="5.5" height="5.5" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
function () { Modals.openSetGenerator(); });
}
/* ---------------- stage / meta ---------------- */
function updateMeta() {
document.getElementById("meta-mode").textContent = MODES[P.mode].full;
document.getElementById("meta-mode").textContent =
P.synthOn ? synthName() : MODES[P.mode].full;
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();
@@ -414,6 +517,20 @@ document.addEventListener("DOMContentLoaded", function () {
}
}
/* intro: elements pop in big and settle into place, one after another */
(function intro() {
var items = [];
document.querySelectorAll(".topbar > *").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); });
items.forEach(function (n, i) {
if (n) n.style.setProperty("--intro-d", (i * 65) + "ms");
});
document.body.classList.add("intro");
setTimeout(function () { document.body.classList.remove("intro"); }, items.length * 65 + 1100);
})();
Engine.onFps(function (fps) {
document.getElementById("meta-fps").textContent = fps + " fps";
});
@@ -422,9 +539,10 @@ document.addEventListener("DOMContentLoaded", function () {
document.getElementById("btn-play").addEventListener("click", function () {
setPlayingUI(!Engine.isPlaying());
});
document.getElementById("btn-export-png").addEventListener("click", function () { Exporter.exportPNG(P, ASPECTS[P.aspect]); });
document.getElementById("btn-export-video").addEventListener("click", function () { Exporter.exportVideo(P, ASPECTS[P.aspect]); });
document.getElementById("btn-export-gif").addEventListener("click", function () { Exporter.exportGIF(P, ASPECTS[P.aspect]); });
document.getElementById("btn-export-png").addEventListener("click", function () { Modals.openExport("png"); });
document.getElementById("btn-export-video").addEventListener("click", function () { Modals.openExport("video"); });
document.getElementById("btn-export-gif").addEventListener("click", function () { Modals.openExport("gif"); });
document.getElementById("btn-set").addEventListener("click", function () { Modals.openSetGenerator(); });
document.addEventListener("keydown", function (e) {
var tag = (e.target.tagName || "").toLowerCase();
+261
View File
@@ -0,0 +1,261 @@
/* Modal system: export dialog with preview and settings, and the set
generator that produces N consistent variations of the current design. */
var Modals = (function () {
function el(tag, cls, parent) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (parent) parent.appendChild(e);
return e;
}
var root = null;
function open(title, subtitle) {
close();
root = el("div", "modal-backdrop", document.body);
var card = el("div", "modal-card", root);
var head = el("div", "modal-head", card);
var tWrap = el("div", null, head);
var t = el("div", "modal-title", tWrap);
t.textContent = title;
if (subtitle) {
var s = el("div", "modal-sub mono", tWrap);
s.textContent = subtitle;
}
var x = el("button", "modal-close", head);
x.innerHTML = '<svg viewBox="0 0 12 12"><path d="M2 2 L10 10 M10 2 L2 10" stroke="currentColor" stroke-width="1.6" fill="none"/></svg>';
x.addEventListener("click", close);
root.addEventListener("click", function (e) { if (e.target === root) close(); });
var body = el("div", "modal-body", card);
return body;
}
function close() {
if (root) { root.remove(); root = null; }
}
function snapshotInto(canvas2d, aspect) {
var src = Engine.canvas();
var ctx = canvas2d.getContext("2d");
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas2d.width, canvas2d.height);
ctx.drawImage(src, 0, 0, canvas2d.width, canvas2d.height);
}
/* ---------- export dialog ---------- */
function selectField(parent, label, options, value, onChange) {
var row = el("div", "field-row", parent);
var lab = el("span", "ctl-label", row);
lab.textContent = label;
var sel = el("select", null, row);
options.forEach(function (o) {
var op = el("option", null, sel);
op.value = o[0]; op.textContent = o[1];
});
sel.value = value;
sel.addEventListener("change", function () { onChange(sel.value); });
return sel;
}
function toggleField(parent, label, value, onChange) {
var row = el("div", "toggle-row", parent);
var lab = el("span", "ctl-label", row);
lab.textContent = label;
var tg = el("button", "toggle" + (value ? " on" : ""), row);
tg.addEventListener("click", function () {
var on = !tg.classList.contains("on");
tg.classList.toggle("on", on);
onChange(on);
});
}
function openExport(kind) {
var titles = { png: "Export image", video: "Export video", gif: "Export GIF" };
var body = open(titles[kind], MODES[P.mode].full + " \u00b7 seed " + Math.round(P.seed));
/* live preview */
var prevWrap = el("div", "modal-preview", body);
var pv = el("canvas", null, prevWrap);
var ar = ASPECTS[P.aspect];
pv.width = 480; pv.height = Math.round(480 / ar);
snapshotInto(pv, ar);
var pvTimer = setInterval(function () {
if (!document.body.contains(pv)) { clearInterval(pvTimer); return; }
snapshotInto(pv, ar);
}, 120);
var pvMeta = el("div", "modal-preview-meta mono", prevWrap);
var form = el("div", "modal-form", body);
function metaText() {
if (kind === "png") {
var h = parseInt(P.imgRes, 10);
return Math.round(h * ar) + " \u00d7 " + h + " px";
}
if (kind === "video") {
var vh = parseInt(P.vidRes, 10);
var fps = parseInt(P.vidFps, 10);
var sec = (String(P.vidLen).charAt(0) === "s")
? parseInt(String(P.vidLen).slice(1), 10)
: P.loop * parseInt(String(P.vidLen).slice(1), 10);
return Math.round(vh * ar) + " \u00d7 " + vh + " \u00b7 " + fps + " fps \u00b7 " +
sec.toFixed(1) + "s \u00b7 " + Math.round(sec * fps) + " frames";
}
var gw = parseInt(P.gifW, 10);
return gw + " \u00d7 " + Math.round(gw / ar) + " \u00b7 " + P.gifFps + " fps \u00b7 " +
Math.round(P.loop * P.gifFps) + " frames \u00b7 " + P.loop.toFixed(1) + "s loop";
}
function refreshMeta() { pvMeta.textContent = metaText(); }
refreshMeta();
if (kind === "png") {
selectField(form, "Resolution", [["1080", "1920 \u00d7 1080"], ["1440", "2560 \u00d7 1440"], ["2160", "3840 \u00d7 2160"]],
P.imgRes, function (v) { P.imgRes = v; refreshMeta(); });
} else if (kind === "video") {
selectField(form, "Resolution", [["720", "720p"], ["1080", "1080p"], ["1440", "1440p"]],
P.vidRes, function (v) { P.vidRes = v; refreshMeta(); });
selectField(form, "Frame rate", [["24", "24 fps"], ["30", "30 fps"], ["60", "60 fps"]],
P.vidFps, function (v) { P.vidFps = v; refreshMeta(); });
selectField(form, "Length", [
["l1", "1 loop"], ["l2", "2 loops"], ["l3", "3 loops"], ["l4", "4 loops"], ["l6", "6 loops"], ["l8", "8 loops"],
["s5", "5 seconds"], ["s10", "10 seconds"], ["s15", "15 seconds"], ["s30", "30 seconds"], ["s60", "60 seconds"]
], P.vidLen, function (v) { P.vidLen = v; refreshMeta(); });
} else {
selectField(form, "Width", [["360", "360 px"], ["480", "480 px"], ["640", "640 px"], ["800", "800 px"]],
P.gifW, function (v) { P.gifW = v; refreshMeta(); });
selectField(form, "Frame rate", [["15", "15 fps"], ["20", "20 fps"], ["25", "25 fps"], ["30", "30 fps"]],
String(P.gifFps), function (v) { P.gifFps = parseInt(v, 10); refreshMeta(); });
toggleField(form, "Dithering", P.gifDither, function (v) { P.gifDither = v; });
toggleField(form, "Loop forever", P.gifLoop, function (v) { P.gifLoop = v; });
}
var actions = el("div", "modal-actions", body);
var dl = el("button", "btn btn-primary modal-dl", actions);
dl.innerHTML = '<svg viewBox="0 0 16 16"><path d="M8 2 V10 M4.5 7 L8 10.5 L11.5 7" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M3 13.5 H13" stroke="currentColor" stroke-width="1.6"/></svg>Download ' +
(kind === "png" ? "PNG" : kind === "video" ? "WebM" : "GIF");
dl.addEventListener("click", function () {
close();
var a = ASPECTS[P.aspect];
if (kind === "png") Exporter.exportPNG(P, a);
else if (kind === "video") Exporter.exportVideo(P, a);
else Exporter.exportGIF(P, a);
});
}
/* ---------- set generator ---------- */
function setSeeds(base, n) {
var seeds = [];
for (var i = 0; i < n; i++) seeds.push((base + 73 + i * 911) % 10000);
return seeds;
}
async function openSetGenerator() {
var body = open("Gradient set", "consistent variations of the current design");
var info = el("div", "modal-note", body);
info.textContent = "Same style, palette and settings with different seeds. Use a set for hero, cards and section backgrounds that visually belong together.";
var form = el("div", "modal-form", body);
var state = { count: 6, res: "1080" };
selectField(form, "Variations", [["4", "4"], ["6", "6"], ["8", "8"], ["12", "12"]],
"6", function (v) { state.count = parseInt(v, 10); build(); });
selectField(form, "PNG size", [["720", "1280 \u00d7 720"], ["1080", "1920 \u00d7 1080"], ["2160", "3840 \u00d7 2160"]],
"1080", function (v) { state.res = v; });
var grid = el("div", "set-grid", body);
var actions = el("div", "modal-actions", body);
var dl = el("button", "btn btn-primary modal-dl", actions);
dl.innerHTML = '<svg viewBox="0 0 16 16"><path d="M8 2 V10 M4.5 7 L8 10.5 L11.5 7" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M3 13.5 H13" stroke="currentColor" stroke-width="1.6"/></svg>Download set as ZIP';
var seeds = [];
async function build() {
grid.innerHTML = "";
seeds = setSeeds(Math.round(P.seed), state.count);
var ar = ASPECTS[P.aspect];
var origSeed = P.seed;
var prev = Engine.size();
var wasPlaying = Engine.isPlaying();
Engine.suspend();
Engine.setPlaying(false);
Engine.setSize(320, 2 * Math.round(320 / ar / 2));
for (var i = 0; i < seeds.length; i++) {
P.seed = seeds[i];
Engine.renderAt(0.3);
var tile = el("button", "set-tile", grid);
var c = el("canvas", null, tile);
c.width = 320; c.height = Math.round(320 / ar);
c.getContext("2d").drawImage(Engine.canvas(), 0, 0, c.width, c.height);
var lab = el("span", "set-tile-label mono", tile);
lab.textContent = "#" + String(seeds[i]).padStart(4, "0");
(function (sd) {
tile.addEventListener("click", function () {
P.seed = sd;
refreshAll();
UI.toast("Applied seed " + sd);
close();
});
})(seeds[i]);
await new Promise(function (r) { setTimeout(r, 0); });
}
P.seed = origSeed;
Engine.setSize(prev[0], prev[1]);
Engine.setPlaying(wasPlaying);
Engine.resume();
}
dl.addEventListener("click", async function () {
dl.disabled = true;
dl.textContent = "Rendering\u2026";
var ar = ASPECTS[P.aspect];
var h = parseInt(state.res, 10);
var w = 2 * Math.round(h * ar / 2);
var origSeed = P.seed;
var prev = Engine.size();
var wasPlaying = Engine.isPlaying();
Engine.suspend();
Engine.setPlaying(false);
Engine.setSize(w, h);
var entries = [];
for (var i = 0; i < seeds.length; i++) {
P.seed = seeds[i];
Engine.renderAt(0.3);
var blob = await new Promise(function (res) { Engine.canvas().toBlob(res, "image/png"); });
if (blob) {
entries.push({
name: "lumen-set-" + String(i + 1).padStart(2, "0") + "-seed" + String(seeds[i]).padStart(4, "0") + ".png",
data: new Uint8Array(await blob.arrayBuffer())
});
}
dl.textContent = "Rendering " + (i + 1) + "/" + seeds.length + "\u2026";
await new Promise(function (r) { setTimeout(r, 0); });
}
P.seed = origSeed;
Engine.setSize(prev[0], prev[1]);
Engine.setPlaying(wasPlaying);
Engine.resume();
var zip = ZipWriter.build(entries);
var a = document.createElement("a");
a.href = URL.createObjectURL(zip);
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();
});
await build();
}
return { openExport: openExport, openSetGenerator: openSetGenerator, close: close };
})();
+48 -10
View File
@@ -29,6 +29,12 @@ 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 */
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;
out vec4 fragColor;
#define TAU 6.28318530718
@@ -404,23 +410,55 @@ vec3 sceneMosaic(vec2 uv){
/* ---------------- dispatch + post ---------------- */
vec3 scene(vec2 uv){
if (u_mode == 0) return sceneChrome(uv);
if (u_mode == 1) return sceneSilk(uv);
if (u_mode == 2) return sceneBloom(uv);
if (u_mode == 3) return sceneAura(uv);
if (u_mode == 4) return sceneRays(uv);
if (u_mode == 5) return sceneHalftone(uv);
if (u_mode == 6) return sceneGlyphs(uv);
if (u_mode == 7) return sceneReeded(uv);
vec3 sceneFor(int m, vec2 uv){
if (m == 0) return sceneChrome(uv);
if (m == 1) return sceneSilk(uv);
if (m == 2) return sceneBloom(uv);
if (m == 3) return sceneAura(uv);
if (m == 4) return sceneRays(uv);
if (m == 5) return sceneHalftone(uv);
if (m == 6) return sceneGlyphs(uv);
if (m == 7) return sceneReeded(uv);
return sceneMosaic(uv);
}
vec3 scene(vec2 uv){
vec3 a = sceneFor(u_mode, uv);
if (u_synth == 0) return a;
vec3 b = sceneFor(u_modeB, uv);
float asp = u_res.x/u_res.y;
vec2 c = (uv - 0.5)*vec2(asp, 1.0);
if (u_mixOp == 1) { /* screen: layered light */
return mix(a, 1.0 - (1.0 - a)*(1.0 - b), u_blend);
}
if (u_mixOp == 2) { /* multiply with lift */
return mix(a, a*b*1.6 + a*0.12, u_blend);
}
if (u_mixOp == 3) { /* radial: B grows from center */
float m = smoothstep(0.15, 0.85, length(c)*1.15);
return mix(b, a, mix(1.0, m, u_blend));
}
if (u_mixOp == 4) { /* soft diagonal split */
float ang = TAU*hash11(u_seed*0.091 + 5.0);
float m = smoothstep(-0.45, 0.45, cos(ang)*c.x + sin(ang)*c.y);
return mix(a, b, m*u_blend);
}
/* default: organic noise mask */
float m = fbm(c*1.6 + SO()*0.7 + LT()*0.5);
m = smoothstep(0.32, 0.68, m);
return mix(a, b, m*u_blend);
}
void main(){
vec2 uv = gl_FragCoord.xy/u_res;
vec3 col;
if (u_ca > 0.004){
/* 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){
vec2 off = (uv-0.5)*u_ca*0.016;
col = vec3(scene(uv-off).r, scene(uv).g, scene(uv+off).b);
} else {
+91
View File
@@ -0,0 +1,91 @@
/* Minimal store-only (no compression) ZIP writer for batch PNG export. */
var ZipWriter = (function () {
var CRC_TABLE = (function () {
var t = new Uint32Array(256);
for (var n = 0; n < 256; n++) {
var c = n;
for (var k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
t[n] = c >>> 0;
}
return t;
})();
function crc32(buf) {
var c = 0xFFFFFFFF;
for (var i = 0; i < buf.length; i++) {
c = CRC_TABLE[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
}
return (c ^ 0xFFFFFFFF) >>> 0;
}
function dosDateTime() {
var d = new Date();
var time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
var date = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
return { time: time, date: date };
}
/* entries: [{ name: string, data: Uint8Array }] -> Blob */
function build(entries) {
var parts = [];
var central = [];
var offset = 0;
var dt = dosDateTime();
entries.forEach(function (e) {
var nameBytes = new TextEncoder().encode(e.name);
var crc = crc32(e.data);
var local = new Uint8Array(30 + nameBytes.length);
var v = new DataView(local.buffer);
v.setUint32(0, 0x04034b50, true);
v.setUint16(4, 20, true); // version needed
v.setUint16(6, 0x0800, true); // utf8 flag
v.setUint16(8, 0, true); // store
v.setUint16(10, dt.time, true);
v.setUint16(12, dt.date, true);
v.setUint32(14, crc, true);
v.setUint32(18, e.data.length, true);
v.setUint32(22, e.data.length, true);
v.setUint16(26, nameBytes.length, true);
v.setUint16(28, 0, true);
local.set(nameBytes, 30);
parts.push(local, e.data);
var cd = new Uint8Array(46 + nameBytes.length);
var c = new DataView(cd.buffer);
c.setUint32(0, 0x02014b50, true);
c.setUint16(4, 20, true);
c.setUint16(6, 20, true);
c.setUint16(8, 0x0800, true);
c.setUint16(10, 0, true);
c.setUint16(12, dt.time, true);
c.setUint16(14, dt.date, true);
c.setUint32(16, crc, true);
c.setUint32(20, e.data.length, true);
c.setUint32(24, e.data.length, true);
c.setUint16(28, nameBytes.length, true);
c.setUint32(42, offset, true);
cd.set(nameBytes, 46);
central.push(cd);
offset += local.length + e.data.length;
});
var cdSize = 0;
central.forEach(function (c) { cdSize += c.length; });
var end = new Uint8Array(22);
var ev = new DataView(end.buffer);
ev.setUint32(0, 0x06054b50, true);
ev.setUint16(8, entries.length, true);
ev.setUint16(10, entries.length, true);
ev.setUint32(12, cdSize, true);
ev.setUint32(16, offset, true);
return new Blob(parts.concat(central, [end]), { type: "application/zip" });
}
return { build: build };
})();