← back to Damask Kaleidoscope
v2.0.0: restore challenge spec compliance (56px top bar, Royal+carmine palette, Cormorant wordmark, 8-fold) atop the wedge-mirror engine; add bezier urn + acanthus leaves; README
abb6422127d72c499e85802999bc3d1307bf1fe7 · 2026-07-25 01:16:18 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A README.mdA gemma3-damask-kaleidoscope.htmlM index.html
Diff
commit abb6422127d72c499e85802999bc3d1307bf1fe7
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jul 25 01:16:18 2026 -0700
v2.0.0: restore challenge spec compliance (56px top bar, Royal+carmine palette, Cormorant wordmark, 8-fold) atop the wedge-mirror engine; add bezier urn + acanthus leaves; README
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
README.md | 26 +++
gemma3-damask-kaleidoscope.html | 359 ++++++++++++++++++++++++++++++++++++++++
index.html | 330 ++++++++++++++++++++++++------------
3 files changed, 608 insertions(+), 107 deletions(-)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c86032a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# Designer Wallcoverings — Damask Kaleidoscope
+
+A single self-contained `index.html`: a luxury **8-fold damask kaleidoscope** for the DW brand.
+Open the file in any modern browser — no build, no dependencies.
+
+## What it does
+- **True kaleidoscope reflection** — an evolving damask motif is drawn to an offscreen
+ source canvas, then a wedge is clipped and mirror-tiled 16× (8 mirrored pairs) for
+ seamless **8-fold dihedral** symmetry.
+- **Fixed 56px top bar** above the canvas: DW wordmark left (Cormorant Garamond, white,
+ uppercase, `.18em`), a `↻ Click to shift palette` hint, and a live **palette pill** on
+ the right whose dot tracks the palette's secondary bloom colour.
+- **Bezier damask motif** — the radial ornament is built from `bezierCurveTo` anchors:
+ ogee (S-curve) vines, lobed **acanthus leaf/petal** forms, and a central **urn**.
+ The kaleidoscope oscillation is applied to those bezier anchor points, so petals and
+ the urn breathe and warp.
+- **Mandatory Royal palette** (default): ground `#0d0b07`, champagne `#EDE0C4`, crisp gold
+ petals `#C8A951`, carmine secondary bloom `#B5335A` — petal-vs-ground hue contrast kept
+ 60°+ so it pops (no muddy warm beige). Sapphire / Emerald / Amethyst also included.
+- Mouse-warp symmetry, click (or tap) cycles palettes with a crossfade, center gold sheen,
+ vignette overlay, time-based 60fps loop, `prefers-reduced-motion` aware.
+
+## History
+`v2.0.0` merges the strong wedge-mirror engine from the earlier rebuild with full
+compliance to the original challenge spec (56px top bar, Royal + carmine palette,
+Cormorant Garamond wordmark, 8-fold), which the interim rebuild had dropped.
diff --git a/gemma3-damask-kaleidoscope.html b/gemma3-damask-kaleidoscope.html
new file mode 100644
index 0000000..e2b5ebe
--- /dev/null
+++ b/gemma3-damask-kaleidoscope.html
@@ -0,0 +1,359 @@
+<!DOCTYPE html>
+<!-- Designer Wallcoverings — Damask Kaleidoscope · v1.0.1 -->
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+<title>Designer Wallcoverings — Damask Kaleidoscope</title>
+<style>
+ :root { color-scheme: dark; }
+ html, body { margin: 0; height: 100%; overflow: hidden; background: #060505; }
+ canvas { display: block; position: fixed; inset: 0; cursor: crosshair; touch-action: none; }
+
+ #ui {
+ position: fixed; inset: 0; pointer-events: none;
+ font-family: "Optima", "Palatino Linotype", "Cormorant Garamond", Georgia, serif;
+ color: #f3e7cf; z-index: 10;
+ }
+ #title {
+ position: absolute; top: 5.2vh; left: 0; right: 0; text-align: center;
+ letter-spacing: .42em; text-transform: uppercase;
+ font-size: clamp(11px, 1.7vw, 17px); font-weight: 500;
+ color: rgba(243,231,207,.62); text-shadow: 0 1px 22px rgba(0,0,0,.85);
+ transition: opacity 1.2s ease;
+ }
+ #title small { display:block; letter-spacing:.24em; font-size:.62em; margin-top:.7em; color:rgba(243,231,207,.34); }
+
+ #palette {
+ position: absolute; bottom: 6.4vh; left: 0; right: 0; text-align: center;
+ letter-spacing: .34em; text-transform: uppercase;
+ font-size: clamp(10px, 1.35vw, 14px);
+ color: rgba(243,231,207,.55);
+ opacity: 0; transition: opacity .5s ease; text-shadow: 0 1px 18px rgba(0,0,0,.8);
+ }
+ #palette b { color: var(--accent, #e9c877); font-weight: 600; }
+
+ #hint {
+ position: absolute; bottom: 3.0vh; left: 0; right: 0; text-align: center;
+ letter-spacing: .26em; text-transform: uppercase;
+ font-size: clamp(8.5px, 1.05vw, 11px); color: rgba(243,231,207,.28);
+ transition: opacity 1.6s ease;
+ }
+ .faded { opacity: 0 !important; }
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<div id="ui">
+ <div id="title">Damask Kaleidoscope<small>Designer Wallcoverings</small></div>
+ <div id="palette">Palette · <b id="palName">Byzantine Gold</b></div>
+ <div id="hint">move to warp the symmetry · click to change palette</div>
+</div>
+
+<script>
+"use strict";
+(() => {
+ const canvas = document.getElementById("c");
+ const ctx = canvas.getContext("2d", { alpha: false });
+
+ // Offscreen "tube contents" — the evolving damask that the kaleidoscope mirrors.
+ const src = document.createElement("canvas");
+ const sctx = src.getContext("2d");
+
+ /* ---------- palettes (luxury gold / charcoal / blush families) ---------- */
+ const PALETTES = [
+ { name: "Byzantine Gold", deep:"#0c0a07", bg:"#1c150d", gold:"#d8b45a", blush:"#e5a79c", accent:"#f6e7c2" },
+ { name: "Charcoal & Blush", deep:"#0b0a0c", bg:"#221d22", gold:"#c9a24b", blush:"#dc8b98", accent:"#f3d9c5" },
+ { name: "Emerald Court", deep:"#050f0a", bg:"#0f1c15", gold:"#cfab61", blush:"#84c6a6", accent:"#eef2d6" },
+ { name: "Amethyst Royale", deep:"#0a0710", bg:"#1a1122", gold:"#caa64f", blush:"#c691d2", accent:"#f0e1f6" },
+ ];
+
+ let palIndex = 0;
+ let curPal = mixPalette(PALETTES[0], PALETTES[0], 0);
+ let fromPal = PALETTES[0], toPal = PALETTES[0], palT = 1; // crossfade progress
+
+ /* ---------- geometry / device ---------- */
+ let W = 0, H = 0, cx = 0, cy = 0, R = 0, dpr = 1;
+ const SIDES = 12; // kaleidoscope mirror segments (even -> seamless)
+ const SLICE = (Math.PI * 2) / SIDES;
+ let S = 0; // source (offscreen) size in px
+
+ function resize() {
+ dpr = Math.min(window.devicePixelRatio || 1, 2);
+ W = window.innerWidth; H = window.innerHeight;
+ canvas.width = Math.round(W * dpr);
+ canvas.height = Math.round(H * dpr);
+ canvas.style.width = W + "px";
+ canvas.style.height = H + "px";
+ cx = W / 2; cy = H / 2;
+ R = Math.hypot(W, H) / 2 + 4; // cover the whole screen
+ // source square, capped for performance; center at S/2
+ S = Math.min(Math.round(R * 1.15), 900);
+ src.width = S; src.height = S;
+ }
+ window.addEventListener("resize", resize);
+ resize();
+
+ /* ---------- pointer warp ---------- */
+ let mx = 0, my = 0; // normalized -1..1 from center (target)
+ let wx = 0, wy = 0; // smoothed
+ let warp = 0, warpTarget = 0; // 0..1 distance-driven warp strength
+ function pointer(px, py) {
+ mx = (px - cx) / (W / 2);
+ my = (py - cy) / (H / 2);
+ warpTarget = Math.min(Math.hypot(mx, my), 1.4);
+ fade(hint);
+ }
+ canvas.addEventListener("mousemove", e => pointer(e.clientX, e.clientY));
+ canvas.addEventListener("touchmove", e => {
+ if (e.touches[0]) pointer(e.touches[0].clientX, e.touches[0].clientY);
+ e.preventDefault();
+ }, { passive: false });
+
+ /* ---------- palette cycling ---------- */
+ function cyclePalette() {
+ fromPal = curPal;
+ palIndex = (palIndex + 1) % PALETTES.length;
+ toPal = PALETTES[palIndex];
+ palT = 0;
+ const el = document.getElementById("palName");
+ el.textContent = toPal.name;
+ const p = document.getElementById("palette");
+ document.getElementById("ui").style.setProperty("--accent", toPal.accent);
+ p.style.opacity = "1";
+ clearTimeout(cyclePalette._t);
+ cyclePalette._t = setTimeout(() => p.style.opacity = "0", 2200);
+ fade(document.getElementById("title"));
+ }
+ canvas.addEventListener("click", cyclePalette);
+ canvas.addEventListener("touchend", e => { cyclePalette(); e.preventDefault(); });
+
+ const hint = document.getElementById("hint");
+ function fade(el){ if(el && !el.classList.contains("faded")) el.classList.add("faded"); }
+
+ /* ---------- color helpers ---------- */
+ function hexToRgb(h){ const n = parseInt(h.slice(1),16); return [n>>16&255, n>>8&255, n&255]; }
+ function lerp(a,b,t){ return a+(b-a)*t; }
+ function mixHex(h1,h2,t){
+ const a=hexToRgb(h1), b=hexToRgb(h2);
+ return `rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;
+ }
+ function mixPalette(a,b,t){
+ return { name:b.name, deep:mixHex(a.deep,b.deep,t), bg:mixHex(a.bg,b.bg,t),
+ gold:mixHex(a.gold,b.gold,t), blush:mixHex(a.blush,b.blush,t), accent:mixHex(a.accent,b.accent,t) };
+ }
+ // parse an "rgb(...)" string back to [r,g,b] so we can build rgba() glow strings from mixed colors
+ function toRgb(str){ const m=str.match(/\d+/g); return m?[+m[0],+m[1],+m[2]]:[0,0,0]; }
+ function rgbaOf(str,al){ const a=toRgb(str); return `rgba(${a[0]},${a[1]},${a[2]},${al})`; }
+
+ /* ---------- the evolving damask motif (drawn into the source canvas) ----------
+ Rendered with 6-fold rotational symmetry so it already reads as ornament;
+ the 12-way kaleidoscope then mirrors it into a seamless mandala. */
+ function drawSource(t, pal, swirl, amp) {
+ const g = sctx, rs = S / 2;
+ // background: deep radial well
+ const bgGrad = g.createRadialGradient(rs, rs, rs*0.02, rs, rs, rs);
+ bgGrad.addColorStop(0, pal.bg);
+ bgGrad.addColorStop(0.55, pal.bg);
+ bgGrad.addColorStop(1, pal.deep);
+ g.setTransform(1,0,0,1,0,0);
+ g.fillStyle = bgGrad;
+ g.fillRect(0,0,S,S);
+
+ g.translate(rs, rs);
+ g.globalCompositeOperation = "lighter";
+
+ const ARMS = 6;
+ for (let k=0; k<ARMS; k++) {
+ g.save();
+ g.rotate((k/ARMS)*Math.PI*2 + swirl*0.35);
+ drawFlourish(g, rs, t, pal, amp, k);
+ g.restore();
+ }
+
+ // central medallion bloom
+ const mr = rs * (0.16 + 0.03*Math.sin(t*0.9));
+ const med = g.createRadialGradient(0,0,0,0,0,mr);
+ med.addColorStop(0, rgbaOf(pal.accent, 0.9));
+ med.addColorStop(0.4, rgbaOf(pal.gold, 0.55));
+ med.addColorStop(1, rgbaOf(pal.gold, 0));
+ g.fillStyle = med;
+ g.beginPath(); g.arc(0,0,mr,0,Math.PI*2); g.fill();
+
+ g.globalCompositeOperation = "source-over";
+ g.setTransform(1,0,0,1,0,0);
+ }
+
+ // one ornamental "arm": a baseline ogee (S-curve) vine + counter-scroll, strung
+ // with damask leaves, buds and gilt dots. Curvy even at rest so it always reads
+ // as ornament, not spokes; morphs & opens up with time + pointer warp (amp).
+ function drawFlourish(g, rs, t, pal, amp, seed) {
+ const ph = seed * 1.7;
+ const sway = Math.sin(t*0.8 + ph) * (0.18 + amp*0.5);
+ const breathe = 0.9 + 0.1*Math.sin(t*1.1 + ph);
+
+ // Baseline ogee: an intrinsic S-curl that sweeps out and hooks back — the
+ // signature damask ogee — independent of sway so it's ornate with no input.
+ const p0 = { x: 0, y: 0 };
+ const p1 = { x: rs*(0.34 + 0.06*sway), y: -rs*0.10 }; // curl out low
+ const p2 = { x: rs*(0.20 - 0.10*sway), y: -rs*(0.52*breathe) }; // sweep up & back in
+ const p3 = { x: rs*(0.60 + 0.08*Math.sin(t*0.7+ph)),
+ y: -rs*(0.90*breathe) }; // hook to the tip
+
+ // gilt stem: soft outer glow pass, then a bright core pass
+ g.lineCap = "round"; g.lineJoin = "round";
+ for (let pass=0; pass<2; pass++) {
+ g.beginPath();
+ g.moveTo(p0.x,p0.y);
+ g.bezierCurveTo(p1.x,p1.y,p2.x,p2.y,p3.x,p3.y);
+ if (pass===0){ g.strokeStyle = rgbaOf(pal.gold,0.14); g.lineWidth = rs*0.060; }
+ else { g.strokeStyle = rgbaOf(pal.gold,0.62); g.lineWidth = rs*0.020; }
+ g.stroke();
+ }
+
+ // counter-scroll: a smaller curl branching the other way for density
+ const c0 = bez(p0,p1,p2,p3,0.42);
+ const c1 = { x: c0.x - rs*(0.22+0.05*sway), y: c0.y - rs*0.06 };
+ const c2 = { x: c0.x - rs*0.10, y: c0.y - rs*0.30*breathe };
+ const c3 = { x: c0.x + rs*0.06, y: c0.y - rs*0.40*breathe };
+ g.beginPath();
+ g.moveTo(c0.x,c0.y);
+ g.bezierCurveTo(c1.x,c1.y,c2.x,c2.y,c3.x,c3.y);
+ g.strokeStyle = rgbaOf(pal.gold,0.4); g.lineWidth = rs*0.013; g.stroke();
+
+ // leaves / buds strung along the main stem
+ const LEAVES = 5;
+ for (let i=1;i<=LEAVES;i++){
+ const u = i/(LEAVES+0.5);
+ const b = bez(p0,p1,p2,p3,u);
+ const tang = bezTan(p0,p1,p2,p3,u);
+ const ang = Math.atan2(tang.y, tang.x);
+ const side = (i%2===0)?1:-1;
+ const scale = rs*(0.20*(1-u*0.4)) * (0.9+0.25*Math.sin(t*1.3+ph+i));
+ drawLeaf(g, b.x, b.y, ang + side*(1.2+0.2*Math.sin(t+i)), scale, side, pal, amp);
+ }
+ // a couple of leaves on the counter-scroll tip
+ drawLeaf(g, c3.x, c3.y, Math.atan2(c3.y-c2.y, c3.x-c2.x)-0.6, rs*0.14*breathe, -1, pal, amp);
+
+ // blossom teardrop at the tip
+ drawLeaf(g, p3.x, p3.y, Math.atan2(p3.y,p3.x), rs*0.15*(0.9+0.2*Math.sin(t*1.6+ph)), 1, pal, amp, true);
+
+ // gilt punctuation dots along the stem
+ for (let i=0;i<4;i++){
+ const u = 0.18 + i*0.2;
+ const b = bez(p0,p1,p2,p3,u);
+ const dr = rs*0.013*(1.4+Math.sin(t*2+i+ph));
+ g.beginPath();
+ g.fillStyle = rgbaOf(pal.accent, 0.55);
+ g.arc(b.x,b.y,Math.max(dr,0.5),0,Math.PI*2); g.fill();
+ }
+ }
+
+ // a filled damask leaf/petal (teardrop) with blush body + gilt rib
+ function drawLeaf(g, x, y, ang, size, side, pal, amp, bloom) {
+ g.save();
+ g.translate(x,y); g.rotate(ang);
+ const w = size*(bloom?0.85:0.55), l = size*(bloom?1.15:1.6);
+ g.beginPath();
+ g.moveTo(0,0);
+ g.quadraticCurveTo(w*side, -l*0.5, 0, -l);
+ g.quadraticCurveTo(-w*side*0.62, -l*0.5, 0, 0);
+ const lg = g.createLinearGradient(0,0,0,-l);
+ lg.addColorStop(0, rgbaOf(pal.blush, 0.34+amp*0.12));
+ lg.addColorStop(0.6, rgbaOf(pal.blush, 0.16));
+ lg.addColorStop(1, rgbaOf(pal.accent, 0.05));
+ g.fillStyle = lg; g.fill();
+ // gilt rib
+ g.beginPath(); g.moveTo(0,0); g.lineTo(0,-l*0.92);
+ g.strokeStyle = rgbaOf(pal.gold, 0.4); g.lineWidth = Math.max(size*0.03,0.4); g.stroke();
+ g.restore();
+ }
+
+ // cubic bezier point + tangent
+ function bez(p0,p1,p2,p3,u){ const m=1-u;
+ return { x: m*m*m*p0.x+3*m*m*u*p1.x+3*m*u*u*p2.x+u*u*u*p3.x,
+ y: m*m*m*p0.y+3*m*m*u*p1.y+3*m*u*u*p2.y+u*u*u*p3.y }; }
+ function bezTan(p0,p1,p2,p3,u){ const m=1-u;
+ return { x: 3*m*m*(p1.x-p0.x)+6*m*u*(p2.x-p1.x)+3*u*u*(p3.x-p2.x),
+ y: 3*m*m*(p1.y-p0.y)+6*m*u*(p2.y-p1.y)+3*u*u*(p3.y-p2.y) }; }
+
+ /* ---------- the kaleidoscope compositor ---------- */
+ function compose(rot) {
+ ctx.setTransform(dpr,0,0,dpr,0,0);
+ ctx.fillStyle = curPal.deep;
+ ctx.fillRect(0,0,W,H);
+
+ ctx.save();
+ ctx.translate(cx, cy);
+ ctx.rotate(rot);
+
+ const half = SLICE/2;
+ for (let i=0;i<SIDES;i++){
+ ctx.save();
+ ctx.rotate(i*SLICE);
+ if (i % 2 === 1) ctx.scale(1,-1); // mirror alternate wedges -> seamless
+ // clip a wedge spanning [0, SLICE]
+ ctx.beginPath();
+ ctx.moveTo(0,0);
+ ctx.arc(0,0,R, -0.001, SLICE+0.001);
+ ctx.closePath();
+ ctx.clip();
+ // draw the source square so its center sits at the kaleidoscope center
+ ctx.drawImage(src, -R, -R, R*2, R*2);
+ ctx.restore();
+ }
+ ctx.restore();
+
+ // gilt center hub
+ const hub = ctx.createRadialGradient(cx,cy,0,cx,cy,R*0.10);
+ hub.addColorStop(0, rgbaOf(curPal.accent,0.55));
+ hub.addColorStop(0.5, rgbaOf(curPal.gold,0.18));
+ hub.addColorStop(1, rgbaOf(curPal.gold,0));
+ ctx.fillStyle = hub;
+ ctx.beginPath(); ctx.arc(cx,cy,R*0.10,0,Math.PI*2); ctx.fill();
+
+ // vignette for depth
+ const vg = ctx.createRadialGradient(cx,cy,R*0.35,cx,cy,R*0.98);
+ vg.addColorStop(0,"rgba(0,0,0,0)");
+ vg.addColorStop(1,"rgba(0,0,0,0.55)");
+ ctx.fillStyle = vg;
+ ctx.fillRect(0,0,W,H);
+ }
+
+ /* ---------- animation loop (time-based, 60fps) ---------- */
+ let t = 0, last = performance.now(), spin = 0;
+ function frame(now) {
+ let dt = (now - last) / 1000;
+ last = now;
+ if (dt > 0.05) dt = 0.05; // clamp huge first/hidden-tab deltas
+ t += dt;
+
+ // smooth pointer + warp
+ wx += (mx - wx) * Math.min(dt*6,1);
+ wy += (my - wy) * Math.min(dt*6,1);
+ warp += (warpTarget - warp) * Math.min(dt*4,1);
+
+ // palette crossfade
+ if (palT < 1) { palT = Math.min(palT + dt/0.8, 1); }
+ curPal = mixPalette(fromPal, toPal, palT<1 ? palT*palT*(3-2*palT) : 1);
+
+ // master rotation: gentle idle drift + pointer-driven push
+ spin += dt * (0.05 + wx * 0.55);
+ const rot = spin + wy * 0.4;
+
+ // the source swirls & the motif amplitude opens up with pointer distance
+ const swirl = t*0.15 + wx*1.6;
+ const amp = warp * 0.9;
+
+ drawSource(t, curPal, swirl, amp);
+ compose(rot);
+
+ requestAnimationFrame(frame);
+ }
+
+ document.getElementById("ui").style.setProperty("--accent", PALETTES[0].accent);
+ requestAnimationFrame(frame);
+})();
+</script>
+</body>
+</html>
diff --git a/index.html b/index.html
index e2b5ebe..ea41c81 100644
--- a/index.html
+++ b/index.html
@@ -1,54 +1,91 @@
<!DOCTYPE html>
-<!-- Designer Wallcoverings — Damask Kaleidoscope · v1.0.1 -->
+<!-- Designer Wallcoverings — Damask Kaleidoscope · v2.0.0 -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Designer Wallcoverings — Damask Kaleidoscope</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&display=swap" rel="stylesheet">
<style>
- :root { color-scheme: dark; }
- html, body { margin: 0; height: 100%; overflow: hidden; background: #060505; }
- canvas { display: block; position: fixed; inset: 0; cursor: crosshair; touch-action: none; }
-
- #ui {
- position: fixed; inset: 0; pointer-events: none;
- font-family: "Optima", "Palatino Linotype", "Cormorant Garamond", Georgia, serif;
- color: #f3e7cf; z-index: 10;
+ :root {
+ color-scheme: dark;
+ --bar-h: 56px;
+ --champagne: #EDE0C4;
+ --accent: #B5335A; /* palette secondary — drives the pill dot */
}
- #title {
- position: absolute; top: 5.2vh; left: 0; right: 0; text-align: center;
- letter-spacing: .42em; text-transform: uppercase;
- font-size: clamp(11px, 1.7vw, 17px); font-weight: 500;
- color: rgba(243,231,207,.62); text-shadow: 0 1px 22px rgba(0,0,0,.85);
- transition: opacity 1.2s ease;
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ html, body { height: 100%; overflow: hidden; background: #0d0b07; }
+ #c { display: block; position: fixed; inset: 0; z-index: 1; cursor: pointer; touch-action: none; }
+
+ /* ---------- fixed 56px top bar, above the canvas ---------- */
+ #topbar {
+ position: fixed; top: 0; left: 0; right: 0; height: var(--bar-h);
+ z-index: 10;
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 0 24px;
+ background: linear-gradient(180deg, rgba(9,7,4,.90) 0%, rgba(9,7,4,.55) 62%, rgba(9,7,4,0) 100%);
+ -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
+ pointer-events: none; user-select: none;
}
- #title small { display:block; letter-spacing:.24em; font-size:.62em; margin-top:.7em; color:rgba(243,231,207,.34); }
-
- #palette {
- position: absolute; bottom: 6.4vh; left: 0; right: 0; text-align: center;
- letter-spacing: .34em; text-transform: uppercase;
- font-size: clamp(10px, 1.35vw, 14px);
- color: rgba(243,231,207,.55);
- opacity: 0; transition: opacity .5s ease; text-shadow: 0 1px 18px rgba(0,0,0,.8);
+ .wordmark {
+ font-family: "Cormorant Garamond", "Didot", "Bodoni MT", Georgia, serif;
+ color: #ffffff;
+ text-transform: uppercase;
+ letter-spacing: .18em;
+ font-weight: 600;
+ font-size: clamp(15px, 2vw, 22px);
+ line-height: 1;
+ white-space: nowrap;
+ text-shadow: 0 1px 14px rgba(0,0,0,.7);
}
- #palette b { color: var(--accent, #e9c877); font-weight: 600; }
-
- #hint {
- position: absolute; bottom: 3.0vh; left: 0; right: 0; text-align: center;
- letter-spacing: .26em; text-transform: uppercase;
- font-size: clamp(8.5px, 1.05vw, 11px); color: rgba(243,231,207,.28);
- transition: opacity 1.6s ease;
+ .wordmark .mono {
+ display: inline-block;
+ padding-right: .32em; margin-right: .18em;
+ font-size: 1.14em; font-weight: 700;
+ color: var(--champagne);
+ border-right: 1px solid rgba(237,224,196,.30);
+ }
+ .right { display: flex; align-items: center; gap: 16px; }
+ .hint {
+ font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ color: rgba(237,224,196,.50);
+ font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
+ white-space: nowrap;
+ transition: opacity 1.4s ease;
+ }
+ .hint::before { content: "↻ "; color: rgba(200,169,81,.8); }
+ .pill {
+ font-family: -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ font-size: 12px; letter-spacing: .15em; text-transform: uppercase;
+ color: #0d0b07;
+ background: linear-gradient(180deg, #EDE0C4, #C8A951);
+ padding: 6px 15px; border-radius: 999px;
+ box-shadow: 0 0 20px rgba(200,169,81,.28), inset 0 1px 0 rgba(255,255,255,.45);
+ display: flex; align-items: center; gap: 9px;
+ white-space: nowrap;
}
+ .pill .dot {
+ width: 8px; height: 8px; border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 7px var(--accent);
+ transition: background .5s ease, box-shadow .5s ease;
+ }
+ #palName { transition: opacity .35s ease; }
.faded { opacity: 0 !important; }
+ @media (max-width: 520px) { .hint { display: none; } }
</style>
</head>
<body>
<canvas id="c"></canvas>
-<div id="ui">
- <div id="title">Damask Kaleidoscope<small>Designer Wallcoverings</small></div>
- <div id="palette">Palette · <b id="palName">Byzantine Gold</b></div>
- <div id="hint">move to warp the symmetry · click to change palette</div>
-</div>
+<header id="topbar">
+ <div class="wordmark"><span class="mono">DW</span>Designer Wallcoverings</div>
+ <div class="right">
+ <span class="hint" id="hint">Click to shift palette</span>
+ <span class="pill"><span class="dot" id="pillDot"></span><span id="palName">Royal</span></span>
+ </div>
+</header>
<script>
"use strict";
@@ -60,12 +97,18 @@
const src = document.createElement("canvas");
const sctx = src.getContext("2d");
- /* ---------- palettes (luxury gold / charcoal / blush families) ---------- */
+ const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+ /* ---------- palettes ----------
+ Royal is MANDATORY + first: near-black ground, champagne highlight,
+ crisp gold petals, carmine secondary bloom (60°+ off gold so it pops).
+ Every palette keeps a gilt `gold` and a saturated `sec` bloom that is a
+ clear hue-jump from the gold — never a muddy warm beige. */
const PALETTES = [
- { name: "Byzantine Gold", deep:"#0c0a07", bg:"#1c150d", gold:"#d8b45a", blush:"#e5a79c", accent:"#f6e7c2" },
- { name: "Charcoal & Blush", deep:"#0b0a0c", bg:"#221d22", gold:"#c9a24b", blush:"#dc8b98", accent:"#f3d9c5" },
- { name: "Emerald Court", deep:"#050f0a", bg:"#0f1c15", gold:"#cfab61", blush:"#84c6a6", accent:"#eef2d6" },
- { name: "Amethyst Royale", deep:"#0a0710", bg:"#1a1122", gold:"#caa64f", blush:"#c691d2", accent:"#f0e1f6" },
+ { name: "Royal", deep:"#0d0b07", bg:"#191207", gold:"#C8A951", sec:"#B5335A", champ:"#EDE0C4" },
+ { name: "Sapphire", deep:"#060a14", bg:"#0d1626", gold:"#C8A951", sec:"#5E90CC", champ:"#DCE6F2" },
+ { name: "Emerald", deep:"#05100b", bg:"#0d1c14", gold:"#C8A951", sec:"#43A776", champ:"#E4EFE0" },
+ { name: "Amethyst", deep:"#0a0710", bg:"#170f22", gold:"#C8A951", sec:"#A277C6", champ:"#ECE3F2" },
];
let palIndex = 0;
@@ -74,7 +117,7 @@
/* ---------- geometry / device ---------- */
let W = 0, H = 0, cx = 0, cy = 0, R = 0, dpr = 1;
- const SIDES = 12; // kaleidoscope mirror segments (even -> seamless)
+ const SIDES = 16; // 16 wedges (8 mirrored pairs) -> true 8-fold dihedral symmetry
const SLICE = (Math.PI * 2) / SIDES;
let S = 0; // source (offscreen) size in px
@@ -87,8 +130,7 @@
canvas.style.height = H + "px";
cx = W / 2; cy = H / 2;
R = Math.hypot(W, H) / 2 + 4; // cover the whole screen
- // source square, capped for performance; center at S/2
- S = Math.min(Math.round(R * 1.15), 900);
+ S = Math.min(Math.round(R * 1.15), 900); // source square, capped for 60fps
src.width = S; src.height = S;
}
window.addEventListener("resize", resize);
@@ -97,7 +139,7 @@
/* ---------- pointer warp ---------- */
let mx = 0, my = 0; // normalized -1..1 from center (target)
let wx = 0, wy = 0; // smoothed
- let warp = 0, warpTarget = 0; // 0..1 distance-driven warp strength
+ let warp = 0, warpTarget = 0; // 0..1+ distance-driven warp strength
function pointer(px, py) {
mx = (px - cx) / (W / 2);
my = (py - cy) / (H / 2);
@@ -111,25 +153,29 @@
}, { passive: false });
/* ---------- palette cycling ---------- */
+ const palName = document.getElementById("palName");
+ const pillDot = document.getElementById("pillDot");
+ const hint = document.getElementById("hint");
+
+ function applyPillChrome(pal) {
+ document.documentElement.style.setProperty("--accent", pal.sec);
+ document.documentElement.style.setProperty("--champagne", pal.champ);
+ document.body.style.background = pal.deep;
+ }
function cyclePalette() {
fromPal = curPal;
palIndex = (palIndex + 1) % PALETTES.length;
toPal = PALETTES[palIndex];
palT = 0;
- const el = document.getElementById("palName");
- el.textContent = toPal.name;
- const p = document.getElementById("palette");
- document.getElementById("ui").style.setProperty("--accent", toPal.accent);
- p.style.opacity = "1";
- clearTimeout(cyclePalette._t);
- cyclePalette._t = setTimeout(() => p.style.opacity = "0", 2200);
- fade(document.getElementById("title"));
+ // swap the pill label with a tiny fade so it reads as a deliberate shift
+ palName.classList.add("faded");
+ setTimeout(() => { palName.textContent = toPal.name; palName.classList.remove("faded"); }, 180);
+ applyPillChrome(toPal);
}
canvas.addEventListener("click", cyclePalette);
canvas.addEventListener("touchend", e => { cyclePalette(); e.preventDefault(); });
- const hint = document.getElementById("hint");
- function fade(el){ if(el && !el.classList.contains("faded")) el.classList.add("faded"); }
+ function fade(el){ if (el && !el.classList.contains("faded")) el.classList.add("faded"); }
/* ---------- color helpers ---------- */
function hexToRgb(h){ const n = parseInt(h.slice(1),16); return [n>>16&255, n>>8&255, n&255]; }
@@ -139,29 +185,34 @@
return `rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;
}
function mixPalette(a,b,t){
- return { name:b.name, deep:mixHex(a.deep,b.deep,t), bg:mixHex(a.bg,b.bg,t),
- gold:mixHex(a.gold,b.gold,t), blush:mixHex(a.blush,b.blush,t), accent:mixHex(a.accent,b.accent,t) };
+ return { name:b.name,
+ deep:mixHex(a.deep,b.deep,t), bg:mixHex(a.bg,b.bg,t),
+ gold:mixHex(a.gold,b.gold,t), sec:mixHex(a.sec,b.sec,t), champ:mixHex(a.champ,b.champ,t) };
}
// parse an "rgb(...)" string back to [r,g,b] so we can build rgba() glow strings from mixed colors
function toRgb(str){ const m=str.match(/\d+/g); return m?[+m[0],+m[1],+m[2]]:[0,0,0]; }
function rgbaOf(str,al){ const a=toRgb(str); return `rgba(${a[0]},${a[1]},${a[2]},${al})`; }
/* ---------- the evolving damask motif (drawn into the source canvas) ----------
- Rendered with 6-fold rotational symmetry so it already reads as ornament;
- the 12-way kaleidoscope then mirrors it into a seamless mandala. */
+ 6-fold rotational armature of ogee vines + bezier leaves + a central bezier
+ urn; the 16-way kaleidoscope then mirrors a wedge into a seamless 8-fold
+ mandala. The kaleidoscope "oscillation" is applied to the bezier anchor
+ points (leaves, urn, ogee tip) so the motif breathes and warps. */
function drawSource(t, pal, swirl, amp) {
const g = sctx, rs = S / 2;
- // background: deep radial well
+ // background: deep radial well (near-black ground)
const bgGrad = g.createRadialGradient(rs, rs, rs*0.02, rs, rs, rs);
bgGrad.addColorStop(0, pal.bg);
- bgGrad.addColorStop(0.55, pal.bg);
+ bgGrad.addColorStop(0.5, pal.bg);
bgGrad.addColorStop(1, pal.deep);
g.setTransform(1,0,0,1,0,0);
g.fillStyle = bgGrad;
g.fillRect(0,0,S,S);
g.translate(rs, rs);
- g.globalCompositeOperation = "lighter";
+
+ // central bezier urn (foot of the damask), drawn first so foliage overlaps it
+ drawUrn(g, rs, t, pal, amp);
const ARMS = 6;
for (let k=0; k<ARMS; k++) {
@@ -171,10 +222,11 @@
g.restore();
}
- // central medallion bloom
- const mr = rs * (0.16 + 0.03*Math.sin(t*0.9));
+ // central medallion bloom (gilt sheen at the very center) — additive
+ g.globalCompositeOperation = "lighter";
+ const mr = rs * (0.15 + 0.03*Math.sin(t*0.9));
const med = g.createRadialGradient(0,0,0,0,0,mr);
- med.addColorStop(0, rgbaOf(pal.accent, 0.9));
+ med.addColorStop(0, rgbaOf(pal.champ, 0.9));
med.addColorStop(0.4, rgbaOf(pal.gold, 0.55));
med.addColorStop(1, rgbaOf(pal.gold, 0));
g.fillStyle = med;
@@ -184,6 +236,45 @@
g.setTransform(1,0,0,1,0,0);
}
+ // A central damask urn/vase silhouette built from bezierCurveTo. Its shoulder
+ // and lip anchors oscillate with time + pointer warp (the kaleidoscope osc).
+ function drawUrn(g, rs, t, pal, amp) {
+ const o = Math.sin(t*1.1) * (0.5 + amp); // anchor oscillator
+ const s = rs * 0.30; // urn scale
+ const shoulder = 1 + 0.10*o; // belly bulge
+ const lip = 1 + 0.14*o; // flared lip
+
+ g.save();
+ g.beginPath();
+ // start at the foot, sweep the right flank up through the belly to the flared lip
+ g.moveTo(0, s*0.10);
+ g.bezierCurveTo( s*0.34, s*0.06,
+ s*0.52*shoulder, -s*0.30,
+ s*0.30, -s*0.58);
+ g.bezierCurveTo( s*0.16, -s*0.74,
+ s*0.30*lip, -s*0.86,
+ s*0.20*lip, -s*0.98);
+ // mirror the left flank back down to the foot
+ g.bezierCurveTo(-s*0.30*lip, -s*0.86,
+ -s*0.16, -s*0.74,
+ -s*0.30, -s*0.58);
+ g.bezierCurveTo(-s*0.52*shoulder, -s*0.30,
+ -s*0.34, s*0.06,
+ 0, s*0.10);
+ g.closePath();
+
+ const ug = g.createLinearGradient(0, s*0.10, 0, -s*0.98);
+ ug.addColorStop(0, rgbaOf(pal.gold, 0.30));
+ ug.addColorStop(0.5, rgbaOf(pal.gold, 0.60));
+ ug.addColorStop(1, rgbaOf(pal.champ, 0.55));
+ g.fillStyle = ug; g.fill();
+ // champagne rim-light for crisp definition
+ g.strokeStyle = rgbaOf(pal.champ, 0.45);
+ g.lineWidth = Math.max(rs*0.006, 1);
+ g.stroke();
+ g.restore();
+ }
+
// one ornamental "arm": a baseline ogee (S-curve) vine + counter-scroll, strung
// with damask leaves, buds and gilt dots. Curvy even at rest so it always reads
// as ornament, not spokes; morphs & opens up with time + pointer warp (amp).
@@ -194,11 +285,11 @@
// Baseline ogee: an intrinsic S-curl that sweeps out and hooks back — the
// signature damask ogee — independent of sway so it's ornate with no input.
- const p0 = { x: 0, y: 0 };
- const p1 = { x: rs*(0.34 + 0.06*sway), y: -rs*0.10 }; // curl out low
- const p2 = { x: rs*(0.20 - 0.10*sway), y: -rs*(0.52*breathe) }; // sweep up & back in
- const p3 = { x: rs*(0.60 + 0.08*Math.sin(t*0.7+ph)),
- y: -rs*(0.90*breathe) }; // hook to the tip
+ const p0 = { x: 0, y: -rs*0.14 };
+ const p1 = { x: rs*(0.34 + 0.06*sway), y: -rs*0.24 }; // curl out low
+ const p2 = { x: rs*(0.20 - 0.10*sway), y: -rs*(0.58*breathe) }; // sweep up & back in
+ const p3 = { x: rs*(0.52 + 0.08*Math.sin(t*0.7+ph)),
+ y: -rs*(0.92*breathe) }; // hook to the tip
// gilt stem: soft outer glow pass, then a bright core pass
g.lineCap = "round"; g.lineJoin = "round";
@@ -206,8 +297,8 @@
g.beginPath();
g.moveTo(p0.x,p0.y);
g.bezierCurveTo(p1.x,p1.y,p2.x,p2.y,p3.x,p3.y);
- if (pass===0){ g.strokeStyle = rgbaOf(pal.gold,0.14); g.lineWidth = rs*0.060; }
- else { g.strokeStyle = rgbaOf(pal.gold,0.62); g.lineWidth = rs*0.020; }
+ if (pass===0){ g.strokeStyle = rgbaOf(pal.gold,0.16); g.lineWidth = rs*0.058; }
+ else { g.strokeStyle = rgbaOf(pal.gold,0.72); g.lineWidth = rs*0.020; }
g.stroke();
}
@@ -219,9 +310,9 @@
g.beginPath();
g.moveTo(c0.x,c0.y);
g.bezierCurveTo(c1.x,c1.y,c2.x,c2.y,c3.x,c3.y);
- g.strokeStyle = rgbaOf(pal.gold,0.4); g.lineWidth = rs*0.013; g.stroke();
+ g.strokeStyle = rgbaOf(pal.gold,0.45); g.lineWidth = rs*0.013; g.stroke();
- // leaves / buds strung along the main stem
+ // leaves / buds strung along the main stem (alternating gold + carmine bloom)
const LEAVES = 5;
for (let i=1;i<=LEAVES;i++){
const u = i/(LEAVES+0.5);
@@ -229,43 +320,66 @@
const tang = bezTan(p0,p1,p2,p3,u);
const ang = Math.atan2(tang.y, tang.x);
const side = (i%2===0)?1:-1;
- const scale = rs*(0.20*(1-u*0.4)) * (0.9+0.25*Math.sin(t*1.3+ph+i));
- drawLeaf(g, b.x, b.y, ang + side*(1.2+0.2*Math.sin(t+i)), scale, side, pal, amp);
+ const scale = rs*(0.22*(1-u*0.4)) * (0.9+0.25*Math.sin(t*1.3+ph+i));
+ const bloom = (i % 2 === 0); // even = carmine secondary bloom
+ drawLeaf(g, b.x, b.y, ang + side*(1.2+0.2*Math.sin(t+i)), scale, side, pal, amp, t, i+ph, bloom);
}
- // a couple of leaves on the counter-scroll tip
- drawLeaf(g, c3.x, c3.y, Math.atan2(c3.y-c2.y, c3.x-c2.x)-0.6, rs*0.14*breathe, -1, pal, amp);
-
- // blossom teardrop at the tip
- drawLeaf(g, p3.x, p3.y, Math.atan2(p3.y,p3.x), rs*0.15*(0.9+0.2*Math.sin(t*1.6+ph)), 1, pal, amp, true);
+ // a leaf on the counter-scroll tip
+ drawLeaf(g, c3.x, c3.y, Math.atan2(c3.y-c2.y, c3.x-c2.x)-0.6, rs*0.15*breathe, -1, pal, amp, t, ph, false);
+ // carmine blossom at the ogee tip
+ drawLeaf(g, p3.x, p3.y, Math.atan2(p3.y,p3.x), rs*0.16*(0.9+0.2*Math.sin(t*1.6+ph)), 1, pal, amp, t, ph, true);
// gilt punctuation dots along the stem
for (let i=0;i<4;i++){
- const u = 0.18 + i*0.2;
+ const u = 0.20 + i*0.2;
const b = bez(p0,p1,p2,p3,u);
const dr = rs*0.013*(1.4+Math.sin(t*2+i+ph));
g.beginPath();
- g.fillStyle = rgbaOf(pal.accent, 0.55);
+ g.fillStyle = rgbaOf(pal.champ, 0.55);
g.arc(b.x,b.y,Math.max(dr,0.5),0,Math.PI*2); g.fill();
}
}
- // a filled damask leaf/petal (teardrop) with blush body + gilt rib
- function drawLeaf(g, x, y, ang, size, side, pal, amp, bloom) {
+ // A filled damask leaf/petal built from bezierCurveTo, with a lobed acanthus
+ // profile. Its flank control anchors OSCILLATE (osc) so the petal breathes as
+ // the kaleidoscope warps. `bloom` -> carmine secondary petal, else gilt gold.
+ function drawLeaf(g, x, y, ang, size, side, pal, amp, t, phase, bloom) {
+ const osc = Math.sin(t*1.6 + phase) * (0.18 + amp*0.35); // anchor oscillation
+ const w = size*(bloom?0.72:0.60) * side; // half-width (mirrored by side)
+ const l = size*(bloom?1.15:1.65); // length outward (−y)
+ const belly = 1 + 0.28*osc; // widest-point bulge
+ const curl = w*0.22*osc; // tip sway
+
g.save();
g.translate(x,y); g.rotate(ang);
- const w = size*(bloom?0.85:0.55), l = size*(bloom?1.15:1.6);
g.beginPath();
g.moveTo(0,0);
- g.quadraticCurveTo(w*side, -l*0.5, 0, -l);
- g.quadraticCurveTo(-w*side*0.62, -l*0.5, 0, 0);
+ // right flank: base -> lobe -> pointed tip (acanthus ogee)
+ g.bezierCurveTo( w*0.55, -l*0.14,
+ w*1.02*belly, -l*0.46,
+ curl, -l);
+ // left flank: tip -> lobe -> base (mirror)
+ g.bezierCurveTo(-w*1.02*belly, -l*0.46,
+ -w*0.55, -l*0.14,
+ 0, 0);
+ g.closePath();
+
+ const body = bloom ? pal.sec : pal.gold;
const lg = g.createLinearGradient(0,0,0,-l);
- lg.addColorStop(0, rgbaOf(pal.blush, 0.34+amp*0.12));
- lg.addColorStop(0.6, rgbaOf(pal.blush, 0.16));
- lg.addColorStop(1, rgbaOf(pal.accent, 0.05));
+ lg.addColorStop(0, rgbaOf(body, bloom ? 0.55 : 0.42));
+ lg.addColorStop(0.55,rgbaOf(body, bloom ? 0.40 : 0.30));
+ lg.addColorStop(1, rgbaOf(pal.champ, 0.10));
g.fillStyle = lg; g.fill();
- // gilt rib
- g.beginPath(); g.moveTo(0,0); g.lineTo(0,-l*0.92);
- g.strokeStyle = rgbaOf(pal.gold, 0.4); g.lineWidth = Math.max(size*0.03,0.4); g.stroke();
+
+ // champagne crisp outline for petal-vs-ground definition (the "pop")
+ g.strokeStyle = rgbaOf(pal.champ, bloom ? 0.28 : 0.40);
+ g.lineWidth = Math.max(size*0.02, 0.5);
+ g.stroke();
+
+ // gilt central rib
+ g.beginPath(); g.moveTo(0,0); g.lineTo(curl*0.5,-l*0.9);
+ g.strokeStyle = rgbaOf(bloom ? pal.champ : pal.gold, 0.5);
+ g.lineWidth = Math.max(size*0.028, 0.4); g.stroke();
g.restore();
}
@@ -287,11 +401,10 @@
ctx.translate(cx, cy);
ctx.rotate(rot);
- const half = SLICE/2;
for (let i=0;i<SIDES;i++){
ctx.save();
ctx.rotate(i*SLICE);
- if (i % 2 === 1) ctx.scale(1,-1); // mirror alternate wedges -> seamless
+ if (i % 2 === 1) ctx.scale(1,-1); // mirror alternate wedges -> seamless 8-fold
// clip a wedge spanning [0, SLICE]
ctx.beginPath();
ctx.moveTo(0,0);
@@ -304,18 +417,20 @@
}
ctx.restore();
- // gilt center hub
- const hub = ctx.createRadialGradient(cx,cy,0,cx,cy,R*0.10);
- hub.addColorStop(0, rgbaOf(curPal.accent,0.55));
- hub.addColorStop(0.5, rgbaOf(curPal.gold,0.18));
+ // center gold sheen (hub)
+ const hub = ctx.createRadialGradient(cx,cy,0,cx,cy,R*0.11);
+ hub.addColorStop(0, rgbaOf(curPal.champ,0.60));
+ hub.addColorStop(0.45, rgbaOf(curPal.gold,0.20));
hub.addColorStop(1, rgbaOf(curPal.gold,0));
+ ctx.globalCompositeOperation = "lighter";
ctx.fillStyle = hub;
- ctx.beginPath(); ctx.arc(cx,cy,R*0.10,0,Math.PI*2); ctx.fill();
+ ctx.beginPath(); ctx.arc(cx,cy,R*0.11,0,Math.PI*2); ctx.fill();
+ ctx.globalCompositeOperation = "source-over";
- // vignette for depth
- const vg = ctx.createRadialGradient(cx,cy,R*0.35,cx,cy,R*0.98);
+ // vignette overlay for depth
+ const vg = ctx.createRadialGradient(cx,cy,R*0.34,cx,cy,R*0.98);
vg.addColorStop(0,"rgba(0,0,0,0)");
- vg.addColorStop(1,"rgba(0,0,0,0.55)");
+ vg.addColorStop(1,"rgba(0,0,0,0.60)");
ctx.fillStyle = vg;
ctx.fillRect(0,0,W,H);
}
@@ -326,19 +441,20 @@
let dt = (now - last) / 1000;
last = now;
if (dt > 0.05) dt = 0.05; // clamp huge first/hidden-tab deltas
- t += dt;
+ const rate = reduceMotion ? 0.25 : 1;
+ t += dt * rate;
// smooth pointer + warp
wx += (mx - wx) * Math.min(dt*6,1);
wy += (my - wy) * Math.min(dt*6,1);
warp += (warpTarget - warp) * Math.min(dt*4,1);
- // palette crossfade
+ // palette crossfade (smoothstep)
if (palT < 1) { palT = Math.min(palT + dt/0.8, 1); }
curPal = mixPalette(fromPal, toPal, palT<1 ? palT*palT*(3-2*palT) : 1);
// master rotation: gentle idle drift + pointer-driven push
- spin += dt * (0.05 + wx * 0.55);
+ spin += dt * rate * (0.05 + wx * 0.55);
const rot = spin + wy * 0.4;
// the source swirls & the motif amplitude opens up with pointer distance
@@ -351,7 +467,7 @@
requestAnimationFrame(frame);
}
- document.getElementById("ui").style.setProperty("--accent", PALETTES[0].accent);
+ applyPillChrome(PALETTES[0]);
requestAnimationFrame(frame);
})();
</script>
← ff920e1 chore: remove dead rgba() helper, stamp v1.0.1 (session clos
·
back to Damask Kaleidoscope
·
auto-save: 2026-07-25T08:35:01 (1 files) — index.html cdcbfd9 →