/* App state, parameter schema, randomizer recipes, and wiring. */ /* ---------------- modes ---------------- */ var MODES = [ { id: 0, key: "chrome", name: "\u94ec", full: "\u6db2\u6001\u94ec", icon: '' }, { id: 1, key: "silk", name: "\u4e1d\u7ef8", full: "\u4e1d\u5e26", icon: '' }, { id: 2, key: "bloom", name: "\u7efd\u653e", full: "\u67d4\u5149\u7efd\u653e", icon: '' }, { id: 3, key: "aura", name: "\u5149\u73af", full: "\u5149\u73af", icon: '' }, { id: 4, key: "rays", name: "\u5149\u7ebf", full: "\u5149\u7ebf", icon: '' }, { id: 5, key: "halftone", name: "\u534a\u8c03", full: "\u534a\u8c03", icon: '' }, { id: 6, key: "glyphs", name: "\u5b57\u5f62", full: "\u6570\u636e\u5b57\u5f62", icon: '' }, { id: 7, key: "reeded", name: "\u7f57\u7eb9", full: "\u7f57\u7eb9\u73bb\u7483", icon: '' }, { id: 8, key: "mosaic", name: "\u9a6c\u8d5b\u514b", full: "\u50cf\u7d20\u7efd\u653e", icon: '' } ]; var ASPECTS = { "16:9": 16 / 9, "3:2": 1.5, "1:1": 1, "4:5": 0.8, "21:9": 21 / 9 }; /* ---------------- state ---------------- */ /* startup: curated reeded-glass ember design (code LMN1.WzEsNyw5MDE1...) */ var P = { mode: 7, seed: 9015, c1: "#ff6a00", c2: "#ffb347", c3: "#a81c00", c4: "#3d0c02", bg: "#070403", hue: 23, sat: 0.55, exposure: 1.016, contrast: 0.957, scale: 0.803, complex: 5.603, warp: 1.073, flow: 0.233, stretch: -0.089, 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, 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, aspect: "16:9" }; /* randomizer ranges; missing keys fall back to DEF_RANGE */ var DEF_RANGE = { scale: [0.8, 1.8], complex: [3, 6], warp: [0.3, 1.5], flow: [0, 1], stretch: [-0.3, 0.5], light: [0.6, 1.6], gloss: [16, 80], lightAngle: [0, 360], irid: [0, 0.8], glow: [0, 0.5], contrast: [0.95, 1.2], grain: [0, 0.12], cell: [40, 140], lines: [20, 100], ca: [0, 0.4], vig: [0, 0.4], soft: [0.6, 1.4], sat: [0.95, 1.35], exposure: [0.95, 1.1], hue: [0, 0], travel: [0.3, 1.0], loop: [4, 8] }; var RECIPES = { chrome: { tone: ["dark"], scale: [1.2, 2.4], complex: [2.8, 5], warp: [0.7, 1.9], flow: [0.2, 1.0], stretch: [0.25, 0.7], light: [1.2, 2.0], gloss: [36, 100], irid: [0.2, 0.85], glow: [0.15, 0.55], contrast: [1.0, 1.3], grain: [0.02, 0.1], ca: [0, 0.45], vig: [0.15, 0.5], sat: [1.0, 1.4], travel: [0.4, 1.0], loop: [4, 8] }, silk: { tone: ["dark"], scale: [1.2, 2.2], warp: [0.3, 1.0], lines: [40, 90], gloss: [24, 80], light: [1.0, 1.9], stretch: [0, 0.4], irid: [0.2, 0.7], glow: [0.2, 0.6], grain: [0.02, 0.08], vig: [0.1, 0.4], sat: [1.0, 1.45], contrast: [1.0, 1.25], travel: [0.3, 0.8], loop: [4, 9] }, bloom: { tone: ["light", "light", "dark"], scale: [0.8, 1.6], warp: [0.3, 1.4], soft: [0.8, 1.5], light: [0, 0.6], glow: [0, 0.3], grain: [0, 0.07], ca: [0, 0.25], vig: [0, 0.25], contrast: [0.9, 1.05], sat: [0.9, 1.3], travel: [0.5, 1.2], loop: [5, 10] }, aura: { tone: ["light", "light", "dark"], scale: [0.9, 1.6], warp: [0.2, 1.0], soft: [0.9, 1.6], grain: [0, 0.06], contrast: [0.9, 1.05], sat: [0.85, 1.15], vig: [0, 0.2], glow: [0, 0.35], ca: [0, 0.08], travel: [0.3, 0.9], loop: [5, 10] }, rays: { tone: ["light", "light", "dark"], warp: [0.3, 1.2], lines: [20, 90], grain: [0, 0.1], glow: [0.1, 0.5], contrast: [0.95, 1.2], sat: [1.0, 1.4], vig: [0, 0.3], travel: [0.2, 0.7], loop: [5, 10], scale: [0.9, 1.5] }, halftone: { tone: ["light", "light", "dark"], cell: [60, 150], scale: [0.9, 1.8], warp: [0.4, 1.4], grain: [0, 0.05], contrast: [0.95, 1.15], sat: [0.95, 1.3], ca: [0, 0.2], vig: [0, 0.2], travel: [0.4, 1.0], loop: [4, 9] }, glyphs: { tone: ["dark"], cell: [70, 150], scale: [0.8, 1.6], warp: [0.5, 1.5], grain: [0.02, 0.12], glow: [0.3, 0.8], contrast: [1.0, 1.3], sat: [1.0, 1.5], ca: [0, 0.5], vig: [0.2, 0.6], travel: [0.4, 1.0], loop: [3, 7] }, reeded: { tone: ["light", "dark"], lines: [36, 90], warp: [0.6, 1.6], scale: [0.8, 1.5], grain: [0, 0.07], light: [0.4, 1.2], contrast: [0.95, 1.2], sat: [1.0, 1.35], vig: [0, 0.18], travel: [0.5, 1.1], loop: [5, 10] }, mosaic: { tone: ["light", "dark"], cell: [40, 120], warp: [0.3, 1.2], scale: [0.8, 1.5], grain: [0, 0.04], contrast: [0.95, 1.15], sat: [0.95, 1.3], vig: [0, 0.15], travel: [0.5, 1.2], loop: [5, 10], soft: [0.8, 1.4] }, genome: { tone: ["dark", "light", "dark"], scale: [0.9, 1.7], complex: [2.5, 5.5], warp: [0.35, 1.3], flow: [0.1, 0.8], stretch: [-0.2, 0.45], lines: [30, 100], cell: [50, 130], grain: [0, 0.08], ca: [0, 0.15], vig: [0.05, 0.25], soft: [0.75, 1.35], travel: [0.35, 0.95], loop: [4, 9] } }; var FIELD_NAMES = ["FLUX", "RIDGE", "WAVE", "RING", "CELL", "FLOW", "BAND", "GRID", "HEX", "ARC", "HATCH", "ORB"]; var FIELD_TYPES = FIELD_NAMES.length; var DOMAIN_TYPES = 7; var COLOR_MAPS = 6; var SHADE_TYPES = 5; var OVERLAY_TYPES = 5; var FORM_KEYS = ["scale", "complex", "warp", "flow", "stretch"]; var LIGHT_KEYS = ["light", "gloss", "lightAngle", "irid", "glow", "contrast"]; var TEXTURE_KEYS = ["grain", "cell", "lines", "ca", "vig", "soft"]; var MOTION_KEYS = ["loop", "travel"]; var GRADE_KEYS = ["sat", "exposure"]; /* ---------------- randomizer ---------------- */ function rnd() { return Math.random(); } function randIn(range) { return range[0] + rnd() * (range[1] - range[0]); } function recipeKey() { if (P.genomeOn) return "genome"; return MODES[P.mode].key; } function rangeFor(key) { var rec = RECIPES[recipeKey()] || {}; return rec[key] || DEF_RANGE[key]; } function pickTone() { var rec = RECIPES[recipeKey()] || { tone: ["dark", "light"] }; return rec.tone[Math.floor(rnd() * rec.tone.length)]; } var activePreset = 3; /* Ember — matches the startup design */ var setPresetActive = function () {}; function applyPalette(pal, presetIdx) { P.c1 = pal.colors[0]; P.c2 = pal.colors[1]; P.c3 = pal.colors[2]; P.c4 = pal.colors[3]; P.bg = pal.bg; activePreset = presetIdx; setPresetActive(presetIdx); } function randomizePalette(tone) { tone = tone || pickTone(); if (rnd() < 0.78) { var pool = []; PALETTES.forEach(function (p, i) { if (p.tone === tone) pool.push(i); }); if (!pool.length) pool = PALETTES.map(function (_, i) { return i; }); var idx = pool[Math.floor(rnd() * pool.length)]; applyPalette(PALETTES[idx], idx); } else { applyPalette(generateRandomPalette(rnd, tone), -1); } P.hue = 0; } /* tuned grading + lighting so random hits stay pleasant, not harsh */ function smartTune(tone) { var key = recipeKey(); var isLight = tone === "light"; var isGlass = key === "chrome" || key === "reeded" || key === "silk" || (P.genomeOn && Math.round(P.genes[5]) === 4); var isSoft = key === "bloom" || key === "aura" || key === "mosaic" || key === "rays" || key === "halftone"; P.exposure = isLight ? randIn([0.94, 1.06]) : randIn([0.90, 1.04]); P.contrast = isLight ? randIn([0.94, 1.10]) : randIn([0.92, 1.12]); P.sat = isLight ? randIn([0.82, 1.06]) : randIn([0.86, 1.16]); P.hue = randIn([-14, 14]); if (isGlass) { P.light = randIn([0.62, 1.32]); P.gloss = Math.round(randIn([26, 92])); P.lightAngle = Math.round(randIn([0, 360])); P.irid = randIn([0, 0.38]); P.glow = randIn([0, 0.2]); P.soft = randIn([0.82, 1.28]); } else if (isSoft) { P.light = randIn([0.12, 0.72]); P.gloss = Math.round(randIn([10, 44])); P.lightAngle = Math.round(randIn([0, 360])); P.irid = randIn([0, 0.22]); P.glow = randIn([0, 0.18]); } else { P.light = randIn([0.48, 1.28]); P.gloss = Math.round(randIn([14, 76])); P.lightAngle = Math.round(randIn([0, 360])); P.irid = randIn([0, 0.32]); P.glow = randIn([0, 0.28]); } P.ca = randIn([0, 0.16]); P.vig = randIn([0.05, 0.26]); P.grain = randIn([0, isLight ? 0.055 : 0.085]); } function randomizeKeys(keys) { keys.forEach(function (k) { var v = randIn(rangeFor(k)); if (k === "gloss" || k === "cell" || k === "lines" || k === "lightAngle") v = Math.round(v); if (k === "loop") v = Math.round(v * 2) / 2; P[k] = v; }); } function newSeed() { P.seed = Math.floor(rnd() * 10000); } /* 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 pickFieldType() { /* mix organic noise fields with clean geometric patterns */ if (rnd() < 0.48) return 6 + Math.floor(rnd() * (FIELD_TYPES - 6)); return Math.floor(rnd() * 6); } function generateGenomeStyle() { P.genes = [ pickFieldType(), // 0 field type Math.floor(rnd() * DOMAIN_TYPES), // 1 domain op 0.15 + rnd() * 1.0, // 2 extra warp 2 + Math.floor(rnd() * 6), // 3 fold count Math.floor(rnd() * COLOR_MAPS), // 4 color map Math.floor(rnd() * SHADE_TYPES),// 5 shading Math.floor(rnd() * OVERLAY_TYPES), // 6 overlay 0.25 + rnd() * 1.0, // 7 overlay scale rnd(), // 8 ridge sharpness rnd(), // 9 poster steps 0.25 + rnd() * 0.85, // 10 field scale rnd() // 11 rotation ]; P.genomeOn = true; P.synthOn = false; } 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 ft = Math.min(Math.max(Math.round(g[0]) || 0, 0), FIELD_TYPES - 1); return FIELD_NAMES[ft] + " " + 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.42 && Engine.hasGenome()) { generateGenomeStyle(); } else { P.synthOn = false; P.genomeOn = false; P.mode = Math.floor(rnd() * MODES.length); } } var tone = pickTone(); randomizePalette(tone); randomizeKeys(FORM_KEYS.concat(TEXTURE_KEYS, MOTION_KEYS)); smartTune(tone); 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 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); renderSavedStyles(); UI.toast("\u98ce\u683c\u5df2\u4fdd\u5b58\uff1a" + name); } var renderSavedStyles = function () {}; /* ---------------- design codes (share) ---------------- */ var SHARE_NUMS = [ "hue", "sat", "exposure", "contrast", "scale", "complex", "warp", "flow", "stretch", "light", "gloss", "lightAngle", "irid", "glow", "grain", "cell", "lines", "ca", "vig", "soft", "travel", "loop" ]; function encodeDesign() { 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; } function decodeDesign(code) { try { code = String(code).trim().replace(/^#/, ""); if (code.indexOf("LMN1.") !== 0) return false; var b64 = code.slice(5).replace(/-/g, "+").replace(/_/g, "/"); while (b64.length % 4) b64 += "="; var arr = JSON.parse(atob(b64)); 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; 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; P.mode = Math.min(Math.max(Math.round(arr[1]) || 0, 0), MODES.length - 1); P.seed = Math.min(Math.max(Math.round(arr[2]) || 0, 0), 9999); P.c1 = "#" + arr[3]; P.c2 = "#" + arr[4]; P.c3 = "#" + arr[5]; P.c4 = "#" + arr[6]; P.bg = "#" + arr[7]; if (ASPECTS[arr[8]]) P.aspect = arr[8]; SHARE_NUMS.forEach(function (k, i) { var v = Number(arr[9 + i]); if (isFinite(v)) P[k] = v; }); 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); var bl = Number(arr[baseLen + 3]); P.blend = isFinite(bl) ? Math.min(Math.max(bl, 0), 1) : 0.6; } 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(); if (typeof fitCanvas === "function") fitCanvas(); return true; } catch (e) { return false; } } function copyText(text, okMsg) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then( function () { UI.toast(okMsg); }, function () { UI.toast("\u65e0\u6cd5\u8bbf\u95ee\u526a\u8d34\u677f"); }); } else { UI.toast("\u6b64\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u526a\u8d34\u677f"); } } /* ---------------- UI wiring ---------------- */ var refreshers = []; function reg(fn) { refreshers.push(fn); return fn; } function refreshAll() { refreshers.forEach(function (f) { f(); }); updateMeta(); } function get(k) { return function () { return P[k]; }; } function set(k) { return function (v) { P[k] = v; updateMeta(); }; } function fmt2(v) { return (+v).toFixed(2); } function fmt1(v) { return (+v).toFixed(1); } function fmtInt(v) { return String(Math.round(v)); } function fmtDeg(v) { return Math.round(v) + "\u00b0"; } function fmtSec(v) { return (+v).toFixed(1) + "s"; } function buildRail() { var rail = document.getElementById("rail"); /* STYLE */ var sStyle = UI.section(rail, "\u98ce\u683c", function () { P.synthOn = false; P.mode = Math.floor(rnd() * MODES.length); 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(); })); /* 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 = '\u65b0\u98ce\u683c'; btnSynth.addEventListener("click", function () { if (!Engine.hasGenome()) { UI.toast("\u98ce\u683c\u5408\u6210\u5668\u6b63\u5728\u9884\u70ed\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5"); return; } generateGenomeStyle(); newSeed(); refreshAll(); UI.toast("\u53d1\u73b0\u65b0\u98ce\u683c\uff1a" + genomeName()); }); var btnSave = UI.el("button", "mini-btn", synthRow); btnSave.innerHTML = '\u4fdd\u5b58\u98ce\u683c'; btnSave.addEventListener("click", saveCurrentStyle); var synthCtl = UI.el("div", "synth-ctl", sStyle); reg(UI.slider(synthCtl, { label: "\u5408\u6210\u6df7\u5408", 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 = "×"; 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("\u5df2\u52a0\u8f7d " + st.name); }); }); }; renderSavedStyles(); reg(UI.lockRow(sStyle, { label: "\u968f\u673a\u5316\u65f6\u4fdd\u6301\u98ce\u683c", get: get("lockStyle"), set: set("lockStyle") })); /* COLOR */ var sColor = UI.section(rail, "\u8272\u5f69", function () { randomizePalette(); refreshAll(); }); setPresetActive = UI.presetChips(sColor, PALETTES, function (i) { applyPalette(PALETTES[i], i); refreshAll(); }); setPresetActive(activePreset); reg(UI.colorSwatches(sColor, [{ key: "c1", label: "A" }, { key: "c2", label: "B" }, { key: "c3", label: "C" }, { key: "c4", label: "D" }, { gap: true }, { key: "bg", label: "BG" }], function (k) { return P[k]; }, function (k, v) { P[k] = v; activePreset = -1; setPresetActive(-1); })); reg(UI.slider(sColor, { label: "\u8272\u76f8\u504f\u79fb", min: -180, max: 180, step: 1, fmt: fmtDeg, get: get("hue"), set: set("hue") })); reg(UI.slider(sColor, { label: "\u9971\u548c\u5ea6", min: 0, max: 2, step: 0.01, fmt: fmt2, get: get("sat"), set: set("sat") })); reg(UI.slider(sColor, { label: "\u66dd\u5149", min: 0.5, max: 1.6, step: 0.01, fmt: fmt2, get: get("exposure"), set: set("exposure") })); /* FORM */ var sForm = UI.section(rail, "\u5f62\u6001", function () { randomizeKeys(FORM_KEYS); newSeed(); refreshAll(); }); reg(UI.seedRow(sForm, { get: get("seed"), set: function (v) { P.seed = v; updateMeta(); refreshAll(); }, onDice: function () { newSeed(); refreshAll(); } })); reg(UI.slider(sForm, { label: "\u7f29\u653e", min: 0.5, max: 3, step: 0.01, fmt: fmt2, get: get("scale"), set: set("scale") })); reg(UI.slider(sForm, { label: "\u7ec6\u8282", min: 1, max: 8, step: 0.1, fmt: fmt1, get: get("complex"), set: set("complex") })); reg(UI.slider(sForm, { label: "\u626d\u66f2", min: 0, max: 2.5, step: 0.01, fmt: fmt2, get: get("warp"), set: set("warp") })); reg(UI.slider(sForm, { label: "\u6e4d\u6d41", min: 0, max: 2, step: 0.01, fmt: fmt2, get: get("flow"), set: set("flow") })); reg(UI.slider(sForm, { label: "\u62c9\u4f38", min: -1, max: 1, step: 0.01, fmt: fmt2, get: get("stretch"), set: set("stretch") })); /* LIGHTING */ var sLight = UI.section(rail, "\u5149\u7167", function () { randomizeKeys(LIGHT_KEYS); refreshAll(); }); reg(UI.slider(sLight, { label: "\u5f3a\u5ea6", min: 0, max: 2.2, step: 0.01, fmt: fmt2, get: get("light"), set: set("light") })); reg(UI.slider(sLight, { label: "\u5149\u6cfd", min: 4, max: 120, step: 1, fmt: fmtInt, get: get("gloss"), set: set("gloss") })); reg(UI.slider(sLight, { label: "\u89d2\u5ea6", min: 0, max: 360, step: 1, fmt: fmtDeg, get: get("lightAngle"), set: set("lightAngle") })); reg(UI.slider(sLight, { label: "\u8679\u5f69", min: 0, max: 1, step: 0.01, fmt: fmt2, get: get("irid"), set: set("irid") })); reg(UI.slider(sLight, { label: "\u8f89\u5149", min: 0, max: 1, step: 0.01, fmt: fmt2, get: get("glow"), set: set("glow") })); reg(UI.slider(sLight, { label: "\u5bf9\u6bd4\u5ea6", min: 0.6, max: 1.6, step: 0.01, fmt: fmt2, get: get("contrast"), set: set("contrast") })); /* TEXTURE */ var sTex = UI.section(rail, "\u7eb9\u7406", function () { randomizeKeys(TEXTURE_KEYS); refreshAll(); }); reg(UI.slider(sTex, { label: "\u9897\u7c92", min: 0, max: 0.4, step: 0.005, fmt: fmt2, get: get("grain"), set: set("grain") })); reg(UI.slider(sTex, { label: "\u5bc6\u5ea6", min: 14, max: 180, step: 1, fmt: fmtInt, get: get("cell"), set: set("cell") })); reg(UI.slider(sTex, { label: "\u810a\u7ebf", min: 8, max: 160, step: 1, fmt: fmtInt, get: get("lines"), set: set("lines") })); reg(UI.slider(sTex, { label: "\u8272\u5dee", min: 0, max: 1, step: 0.01, fmt: fmt2, get: get("ca"), set: set("ca") })); reg(UI.slider(sTex, { label: "\u6697\u89d2", min: 0, max: 1, step: 0.01, fmt: fmt2, get: get("vig"), set: set("vig") })); reg(UI.slider(sTex, { label: "\u67d4\u548c\u5ea6", min: 0.3, max: 1.6, step: 0.01, fmt: fmt2, get: get("soft"), set: set("soft") })); /* MOTION */ var sMotion = UI.section(rail, "\u8fd0\u52a8", function () { randomizeKeys(MOTION_KEYS); refreshAll(); }); reg(UI.slider(sMotion, { label: "\u5faa\u73af\u957f\u5ea6", min: 2, max: 12, step: 0.5, fmt: fmtSec, get: get("loop"), set: set("loop") })); reg(UI.slider(sMotion, { label: "\u884c\u7a0b", min: 0, max: 1.5, step: 0.01, fmt: fmt2, get: get("travel"), set: set("travel") })); /* SHARE */ var sShare = UI.section(rail, "\u5206\u4eab", null); var shareRow = UI.el("div", "share-row", sShare); var btnCopyCode = UI.el("button", "mini-btn", shareRow); btnCopyCode.textContent = "\u590d\u5236\u8bbe\u8ba1\u4ee3\u7801"; btnCopyCode.addEventListener("click", function () { copyText(encodeDesign(), "\u8bbe\u8ba1\u4ee3\u7801\u5df2\u590d\u5236\uff0c\u53ef\u7c98\u8d34\u5230\u4efb\u4f55\u5730\u65b9"); }); var btnCopyLink = UI.el("button", "mini-btn", shareRow); btnCopyLink.textContent = "\u590d\u5236\u94fe\u63a5"; btnCopyLink.addEventListener("click", function () { var url = location.origin + location.pathname + "#" + encodeDesign(); copyText(url, "\u5206\u4eab\u94fe\u63a5\u5df2\u590d\u5236"); }); var pasteInput = UI.el("input", "num-input mono share-input", sShare); pasteInput.type = "text"; pasteInput.placeholder = "\u7c98\u8d34\u8bbe\u8ba1\u4ee3\u7801\u2026"; pasteInput.spellcheck = false; function tryApplyPaste() { var v = pasteInput.value.trim(); if (!v) return; if (decodeDesign(v)) { UI.toast("\u8bbe\u8ba1\u5df2\u52a0\u8f7d"); pasteInput.value = ""; pasteInput.blur(); } else { UI.toast("\u65e0\u6548\u7684\u8bbe\u8ba1\u4ee3\u7801"); } } pasteInput.addEventListener("paste", function () { setTimeout(tryApplyPaste, 0); }); pasteInput.addEventListener("keydown", function (e) { if (e.key === "Enter") tryApplyPaste(); e.stopPropagation(); }); /* EXPORT: buttons open a dialog with preview and settings */ var sExp = UI.section(rail, "\u5bfc\u51fa", null); var grid = UI.el("div", "export-grid", sExp); UI.exportButton(grid, "\u56fe\u50cf", "PNG", '', function () { Modals.openExport("png"); }); UI.exportButton(grid, "\u89c6\u9891", "WEBM", '', function () { Modals.openExport("video"); }); UI.exportButton(grid, "\u5faa\u73af GIF", "GIF", '', function () { Modals.openExport("gif"); }); UI.exportButton(grid, "\u6e10\u53d8\u5957\u88c5", "ZIP", '', function () { Modals.openSetGenerator(); }); } /* ---------------- stage / meta ---------------- */ function updateMeta() { document.getElementById("meta-mode").textContent = styleName(); document.getElementById("meta-seed").textContent = "\u79cd\u5b50 " + String(Math.round(P.seed)).padStart(4, "0"); document.getElementById("meta-loop").textContent = P.loop.toFixed(1) + "s \u5faa\u73af"; if (Engine.isReady()) { var s = Engine.size(); document.getElementById("meta-res").textContent = s[0] + "\u00d7" + s[1]; } } var stage = { failed: false, resizeObs: null, resizeTimer: null, bufW: 0, bufH: 0 }; function fitCanvas(preview) { if (stage.failed) return; var frame = document.getElementById("canvas-frame"); if (!frame) return; var availW = frame.clientWidth - 80; var availH = frame.clientHeight - 52; if (availW <= 0 || availH <= 0) return; var ar = ASPECTS[P.aspect]; var w = availW, h = w / ar; if (h > availH) { h = availH; w = h * ar; } w = Math.round(w); h = Math.round(h); var canvasEl = document.getElementById("view"); if (!canvasEl || !canvasEl.isConnected) return; canvasEl.style.width = w + "px"; canvasEl.style.height = h + "px"; var boot = document.getElementById("canvas-boot"); if (boot) { boot.style.width = w + "px"; boot.style.height = h + "px"; } if (!Engine.isReady() || Engine.isLost()) return; var lowMem = (navigator.deviceMemory || 8) <= 4; var dprCap = preview ? 0.65 : (lowMem ? 0.75 : 0.85); var dpr = Math.min(window.devicePixelRatio || 1, dprCap); var bufW = 2 * Math.round(w * dpr / 2); var bufH = 2 * Math.round(h * dpr / 2); if (stage.bufW === bufW && stage.bufH === bufH) { updateMeta(); return; } stage.bufW = bufW; stage.bufH = bufH; Engine.setSize(bufW, bufH); updateMeta(); } function onStageResize() { clearTimeout(stage.resizeTimer); stage.resizeTimer = setTimeout(function () { fitCanvas(false); }, 250); } function detachStageResize() { if (stage.resizeObs) { stage.resizeObs.disconnect(); stage.resizeObs = null; } clearTimeout(stage.resizeTimer); } function setPlayingUI(v) { Engine.setPlaying(v); document.getElementById("icon-pause").style.display = v ? "" : "none"; document.getElementById("icon-play").style.display = v ? "none" : ""; } /* ---------------- boot ---------------- */ function wireControls() { var segRefresh = UI.segmented( document.getElementById("aspect-seg"), Object.keys(ASPECTS).map(function (k) { return [k, k]; }), get("aspect"), function (v) { P.aspect = v; fitCanvas(); } ); reg(segRefresh); Engine.onFps(function (fps) { document.getElementById("meta-fps").textContent = fps + " fps"; }); document.getElementById("btn-random").addEventListener("click", randomizeAll); document.getElementById("btn-play").addEventListener("click", function () { setPlayingUI(!Engine.isPlaying()); }); 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(); if (tag === "input" || tag === "select" || tag === "textarea") return; if (e.code === "Space") { e.preventDefault(); setPlayingUI(!Engine.isPlaying()); } else if (e.key === "r" || e.key === "R") { randomizeAll(); } else if (e.key === "s" || e.key === "S") { Exporter.exportPNG(P, ASPECTS[P.aspect]); } }); } function showStageError(msg) { var el = document.getElementById("stage-error"); if (!el) return; el.innerHTML = "\u9700\u8981 WebGL2\u3002\u8bf7\u4f7f\u7528\u6700\u65b0\u7248 Chrome\u3001Edge \u6216 Firefox\u3002
" + '' + String(msg).split("\n")[0] + ""; el.hidden = false; } function showBootError(msg) { stage.failed = true; detachStageResize(); Engine.suspend(); document.body.classList.remove("booting"); document.body.classList.remove("ready"); showStageError(msg); } function onContextLost() { stage.failed = true; detachStageResize(); Engine.suspend(); document.body.classList.remove("ready"); showStageError("WebGL \u4e0a\u4e0b\u6587\u4e22\u5931\u3002\u8bf7\u5237\u65b0\u9875\u9762\u3002"); UI.toast("WebGL \u4e0a\u4e0b\u6587\u4e22\u5931\u3002\u8bf7\u5237\u65b0\u9875\u9762\u3002"); } document.addEventListener("DOMContentLoaded", function () { var canvas = document.getElementById("view"); if (!canvas) return; var lowPower = (navigator.hardwareConcurrency || 8) <= 4 || ((navigator.deviceMemory || 8) <= 4); document.addEventListener("visibilitychange", function () { if (stage.failed) return; if (document.hidden) Engine.suspend(); else if (Engine.isReady() && !Engine.isLost()) Engine.resume(); }); /* show UI shell immediately; the shader compiles on a driver thread (KHR_parallel_shader_compile) and onReady fires when done, so the page never freezes during boot. */ fitCanvas(true); buildRail(); wireControls(); requestAnimationFrame(function () { Engine.init(canvas, function () { return P; }, { autostart: false, onContextLost: onContextLost, onError: showBootError, onReady: function () { Engine.setMaxFps(lowPower ? 30 : 45); var hash = decodeURIComponent(location.hash.slice(1) || ""); if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) { UI.toast("\u5df2\u52a0\u8f7d\u5206\u4eab\u7684\u8bbe\u8ba1"); } fitCanvas(false); stage.resizeObs = new ResizeObserver(onStageResize); stage.resizeObs.observe(document.getElementById("canvas-frame")); document.body.classList.remove("booting"); document.body.classList.add("ready"); Engine.start(); updateMeta(); } }); }); });