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; }