Files
lumenshaders/js/main.js
T
svefnz d3471018b4 完整汉化:将所有用户可见文本翻译为简体中文
- index.html: 页面标题、meta 标签、按钮文字/提示、状态栏
- docs.html: 完整文档页面 9 个章节
- js/main.js: 模式名称、分区标题、滑块标签、toast 消息、按钮文字
- js/ui.js: 新种子按钮、随机化提示
- js/modals.js: 导出对话框标题/选项/标签、套装生成器文本
- js/exporter.js: 渲染进度文本、完成/错误 toast 消息
- js/palettes.js: 16 种调色板名称
- README.md: 项目说明全文
2026-06-16 19:58:21 +08:00

725 lines
34 KiB
JavaScript

/* App state, parameter schema, randomizer recipes, and wiring. */
/* ---------------- modes ---------------- */
var MODES = [
{ id: 0, key: "chrome", name: "\u94ec", full: "\u6db2\u6001\u94ec",
icon: '<svg viewBox="0 0 26 18"><path d="M1 12 C5 4 9 15 13 9 C17 3 21 13 25 7" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M1 15 C5 9 10 17 14 12 C18 8 22 15 25 11" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.45"/></svg>' },
{ id: 1, key: "silk", name: "\u4e1d\u7ef8", full: "\u4e1d\u5e26",
icon: '<svg viewBox="0 0 26 18"><path d="M1 13 C8 11 12 3 25 4" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M1 15.5 C8 13.5 12 5.5 25 6.5" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.65"/><path d="M1 18 C8 16 12 8 25 9" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.35"/></svg>' },
{ id: 2, key: "bloom", name: "\u7efd\u653e", full: "\u67d4\u5149\u7efd\u653e",
icon: '<svg viewBox="0 0 26 18"><circle cx="9" cy="8" r="5.5" fill="currentColor" opacity="0.35"/><circle cx="17" cy="11" r="4" fill="currentColor" opacity="0.6"/></svg>' },
{ id: 3, key: "aura", name: "\u5149\u73af", full: "\u5149\u73af",
icon: '<svg viewBox="0 0 26 18"><circle cx="13" cy="9" r="7" fill="none" stroke="currentColor" stroke-width="1.4" opacity="0.35"/><circle cx="13" cy="9" r="4.2" fill="none" stroke="currentColor" stroke-width="1.4" opacity="0.7"/><circle cx="13" cy="9" r="1.6" fill="currentColor"/></svg>' },
{ id: 4, key: "rays", name: "\u5149\u7ebf", full: "\u5149\u7ebf",
icon: '<svg viewBox="0 0 26 18"><path d="M13 1 L7 17 M13 1 L13 17 M13 1 L19 17 M13 1 L2 13 M13 1 L24 13" fill="none" stroke="currentColor" stroke-width="1.3" opacity="0.8"/></svg>' },
{ id: 5, key: "halftone", name: "\u534a\u8c03", full: "\u534a\u8c03",
icon: '<svg viewBox="0 0 26 18"><circle cx="4" cy="5" r="2.4" fill="currentColor"/><circle cx="11" cy="5" r="1.8" fill="currentColor"/><circle cx="18" cy="5" r="1.2" fill="currentColor"/><circle cx="24" cy="5" r="0.7" fill="currentColor"/><circle cx="4" cy="12" r="1.6" fill="currentColor"/><circle cx="11" cy="12" r="2.2" fill="currentColor"/><circle cx="18" cy="12" r="1.5" fill="currentColor"/><circle cx="24" cy="12" r="0.9" fill="currentColor"/></svg>' },
{ id: 6, key: "glyphs", name: "\u5b57\u5f62", full: "\u6570\u636e\u5b57\u5f62",
icon: '<svg viewBox="0 0 26 18"><g fill="currentColor"><rect x="2" y="2" width="2" height="3"/><rect x="7" y="2" width="2" height="3" opacity="0.5"/><rect x="12" y="2" width="2" height="3"/><rect x="17" y="2" width="2" height="3" opacity="0.3"/><rect x="22" y="2" width="2" height="3" opacity="0.7"/><rect x="2" y="8" width="2" height="3" opacity="0.4"/><rect x="7" y="8" width="2" height="3"/><rect x="12" y="8" width="2" height="3" opacity="0.6"/><rect x="17" y="8" width="2" height="3"/><rect x="22" y="8" width="2" height="3" opacity="0.4"/><rect x="2" y="14" width="2" height="3" opacity="0.7"/><rect x="7" y="14" width="2" height="3" opacity="0.3"/><rect x="12" y="14" width="2" height="3" opacity="0.8"/><rect x="17" y="14" width="2" height="3" opacity="0.5"/><rect x="22" y="14" width="2" height="3"/></g></svg>' },
{ id: 7, key: "reeded", name: "\u7f57\u7eb9", full: "\u7f57\u7eb9\u73bb\u7483",
icon: '<svg viewBox="0 0 26 18"><g stroke="currentColor" stroke-width="1.8" fill="none"><path d="M3 1 V17" opacity="0.9"/><path d="M8 1 V17" opacity="0.5"/><path d="M13 1 V17" opacity="0.9"/><path d="M18 1 V17" opacity="0.5"/><path d="M23 1 V17" opacity="0.9"/></g></svg>' },
{ id: 8, key: "mosaic", name: "\u9a6c\u8d5b\u514b", full: "\u50cf\u7d20\u7efd\u653e",
icon: '<svg viewBox="0 0 26 18"><g fill="currentColor"><rect x="2" y="2" width="6" height="6" opacity="0.9"/><rect x="9" y="2" width="6" height="6" opacity="0.4"/><rect x="16" y="2" width="6" height="6" opacity="0.7"/><rect x="2" y="9" width="6" height="6" opacity="0.3"/><rect x="9" y="9" width="6" height="6" opacity="0.8"/><rect x="16" y="9" width="6" height="6" opacity="0.5"/></g></svg>' }
];
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 = '<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>\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 = '<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>\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 = "&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("\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",
'<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 () { Modals.openExport("png"); });
UI.exportButton(grid, "\u89c6\u9891", "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 () { Modals.openExport("video"); });
UI.exportButton(grid, "\u5faa\u73af 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 () { Modals.openExport("gif"); });
UI.exportButton(grid, "\u6e10\u53d8\u5957\u88c5", "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 = 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<br>" +
'<span style="color:#5e5e68;font-size:11px">' + String(msg).split("\n")[0] + "</span>";
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();
}
});
});
});