From e39ee3d9edf3107f808f671a30102a894f8141f0 Mon Sep 17 00:00:00 2001 From: Leonxlnx Date: Wed, 10 Jun 2026 01:10:39 +0200 Subject: [PATCH] Add shareable design codes and a cleaner curated startup design Designs serialize to compact LMN1. codes (base64url) that restore the exact style, palette, seed and all parameters when pasted into the new Share section or opened as a #hash link. Startup now shows a calm Aura on the Halo palette with a fixed seed instead of a random chrome. Co-authored-by: Cursor --- README.md | 1 + js/main.js | 126 +++++++++++++++++++++++++++++++++++++++++++++++++---- styles.css | 21 +++++++++ 3 files changed, 140 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fa26344..7f547f2 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Requires a browser with WebGL2 (Chrome, Edge, Firefox). - **Lighting**: intensity, gloss, light angle, iridescence, glow, contrast. - **Texture**: film grain, halftone/glyph density, ridge count, chromatic aberration, vignette, softness. - **Motion**: loop length and travel distance. Loops are seamless by construction (the noise field is sampled along a closed circle). +- **Share**: every design serializes to a compact code (`LMN1.…`). Copy it, send it to anyone, and pasting it into the Share box recreates the exact design — style, colors, seed, all parameters. "Copy link" produces a URL that loads the design directly. - `Space` pauses, `S` saves a PNG. ## Export diff --git a/js/main.js b/js/main.js index 74cf679..2407797 100644 --- a/js/main.js +++ b/js/main.js @@ -27,14 +27,15 @@ var ASPECTS = { "16:9": 16 / 9, "3:2": 1.5, "1:1": 1, "4:5": 0.8, "21:9": 21 / 9 /* ---------------- state ---------------- */ +/* startup: a calm, clean aura on the Halo palette with a fixed seed */ var P = { - mode: 0, seed: Math.floor(Math.random() * 10000), - c1: "#e0220a", c2: "#ff5a1f", c3: "#1f8cff", c4: "#bfe7ff", bg: "#050507", - hue: 0, sat: 1.2, exposure: 1.0, contrast: 1.15, - scale: 1.7, complex: 3.8, warp: 1.1, flow: 0.5, stretch: 0.45, - light: 1.6, gloss: 64, lightAngle: 130, irid: 0.55, glow: 0.35, - grain: 0.05, cell: 90, lines: 56, ca: 0.25, vig: 0.35, soft: 0.95, - travel: 0.7, loop: 5, + mode: 3, seed: 1207, + c1: "#4a30e0", c2: "#7a8cff", c3: "#e89ab8", c4: "#c2d4ff", bg: "#f4f6ff", + hue: 0, sat: 1.0, exposure: 1.0, contrast: 1.0, + scale: 1.2, complex: 3.8, warp: 0.5, flow: 0.5, stretch: 0, + light: 1.0, gloss: 64, lightAngle: 130, irid: 0.3, glow: 0.15, + grain: 0.03, cell: 90, lines: 56, ca: 0, vig: 0, soft: 1.35, + travel: 0.6, loop: 6, lockStyle: false, imgRes: "2160", vidRes: "1080", vidFps: "30", vidLen: "l2", gifW: "640", gifFps: 25, gifDither: true, gifLoop: true, @@ -96,7 +97,7 @@ function rangeFor(key) { return rec[key] || DEF_RANGE[key]; } -var activePreset = 0; +var activePreset = 11; /* Halo — matches the startup design */ var setPresetActive = function () {}; function applyPalette(pal, presetIdx) { @@ -141,6 +142,67 @@ function randomizeAll() { refreshAll(); } +/* ---------------- 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 = [1, 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); }); + 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)); + if (!Array.isArray(arr) || arr[0] !== 1 || arr.length !== 9 + SHARE_NUMS.length) 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; + }); + 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("Could not access clipboard"); }); + } else { + UI.toast("Clipboard not available in this browser"); + } +} + /* ---------------- UI wiring ---------------- */ var refreshers = []; @@ -218,6 +280,41 @@ function buildRail() { reg(UI.slider(sMotion, { label: "Loop length", min: 2, max: 12, step: 0.5, fmt: fmtSec, get: get("loop"), set: set("loop") })); reg(UI.slider(sMotion, { label: "Travel", min: 0, max: 1.5, step: 0.01, fmt: fmt2, get: get("travel"), set: set("travel") })); + /* SHARE */ + var sShare = UI.section(rail, "Share", null); + var shareRow = UI.el("div", "share-row", sShare); + 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"); + }); + var btnCopyLink = UI.el("button", "mini-btn", shareRow); + btnCopyLink.textContent = "Copy link"; + btnCopyLink.addEventListener("click", function () { + var url = location.origin + location.pathname + "#" + encodeDesign(); + copyText(url, "Share link copied"); + }); + var pasteInput = UI.el("input", "num-input mono share-input", sShare); + pasteInput.type = "text"; + pasteInput.placeholder = "Paste a design code\u2026"; + pasteInput.spellcheck = false; + function tryApplyPaste() { + var v = pasteInput.value.trim(); + if (!v) return; + if (decodeDesign(v)) { + UI.toast("Design loaded"); + pasteInput.value = ""; + pasteInput.blur(); + } else { + UI.toast("Invalid design code"); + } + } + pasteInput.addEventListener("paste", function () { setTimeout(tryApplyPaste, 0); }); + pasteInput.addEventListener("keydown", function (e) { + if (e.key === "Enter") tryApplyPaste(); + e.stopPropagation(); + }); + /* EXPORT */ 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") })); @@ -301,9 +398,22 @@ document.addEventListener("DOMContentLoaded", function () { ); reg(segRefresh); + /* load a shared design from the URL hash, e.g. .../#LMN1.xxxx */ + var hash = decodeURIComponent(location.hash.slice(1) || ""); + if (hash.indexOf("LMN1.") === 0 && decodeDesign(hash)) { + UI.toast("Shared design loaded"); + } + fitCanvas(); new ResizeObserver(fitCanvas).observe(document.getElementById("canvas-frame")); + /* load a shared design from the URL hash, e.g. site/#LMN1.xxxx */ + if (location.hash.length > 6) { + if (decodeDesign(decodeURIComponent(location.hash.slice(1)))) { + UI.toast("Shared design loaded"); + } + } + Engine.onFps(function (fps) { document.getElementById("meta-fps").textContent = fps + " fps"; }); diff --git a/styles.css b/styles.css index 8ba2ec1..e7ccc51 100644 --- a/styles.css +++ b/styles.css @@ -465,6 +465,27 @@ select { cursor: pointer; appearance: none; padding-right: 22px; .toggle.on { background: var(--text); } .toggle.on::after { transform: translateX(13px); background: #0a0a0c; } +/* ---------- share section ---------- */ + +.share-row { display: flex; gap: 8px; margin-bottom: 10px; } +.share-row .mini-btn { flex: 1; justify-content: center; } +.share-input { + width: 100%; + text-align: left; + color: var(--text-mid); +} +.share-input::placeholder { color: var(--text-dim); } + +/* ---------- share section ---------- */ + +.share-row { display: flex; gap: 8px; margin-bottom: 10px; } +.share-row .mini-btn { flex: 1; justify-content: center; } +.share-input { + width: 100%; + text-align: left; +} +.share-input::placeholder { color: var(--text-dim); } + /* ---------- export section ---------- */ .export-grid { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }