← back to Damask Kaleidoscope
Damask Kaleidoscope: rebuild Gemma3 artifact into a true wedge-mirror kaleidoscope
15752e559305467c400c2a742c72b5c6e437d2dd · 2026-07-25 00:35:52 -0700 · Steve Abrams
Files touched
Diff
commit 15752e559305467c400c2a742c72b5c6e437d2dd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jul 25 00:35:52 2026 -0700
Damask Kaleidoscope: rebuild Gemma3 artifact into a true wedge-mirror kaleidoscope
---
.gitignore | 8 ++
index.html | 343 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 351 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f6f7feb
--- /dev/null
+++ b/index.html
@@ -0,0 +1,343 @@
+<!DOCTYPE html>
+<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 rgba(hex,al){ const a=hexToRgb(hex); return `rgba(${a[0]},${a[1]},${a[2]},${al})`; }
+ 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": an ogee vine sweeping outward with leaves, buds and gilt dots
+ function drawFlourish(g, rs, t, pal, amp, seed) {
+ const ph = seed * 1.7;
+ const sway = Math.sin(t*0.8 + ph) * (0.28 + amp*0.5);
+ const reach = 0.94;
+
+ // the sweeping ogee stem
+ const p0 = { x: 0, y: 0 };
+ const p1 = { x: rs*0.30*Math.cos(sway), y: -rs*0.30 };
+ const p2 = { x: rs*(0.58+0.05*Math.sin(t+ph)), y: -rs*0.34 - rs*0.18*sway };
+ const p3 = { x: rs*reach*Math.cos(0.32 + sway*0.4), y: -rs*reach*Math.sin(0.9 + sway*0.2) };
+
+ // gilt stem with soft outer glow then bright core
+ g.lineCap = "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.10); g.lineWidth = rs*0.055; }
+ else { g.strokeStyle = rgbaOf(pal.gold,0.55); g.lineWidth = rs*0.018; }
+ g.stroke();
+ }
+
+ // leaves / buds strung along the 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.16*(1-u*0.45)) * (0.85+0.3*Math.sin(t*1.3+ph+i));
+ drawLeaf(g, b.x, b.y, ang + side*(1.15+0.25*Math.sin(t+i)), scale, side, pal, amp);
+ }
+
+ // a blossom teardrop at the tip
+ drawLeaf(g, p3.x, p3.y, Math.atan2(p3.y,p3.x), rs*0.13*(0.9+0.2*Math.sin(t*1.6+ph)), 1, pal, amp, true);
+
+ // gilt punctuation dots
+ for (let i=0;i<4;i++){
+ const u = 0.2 + i*0.2;
+ const b = bez(p0,p1,p2,p3,u);
+ const dr = rs*0.012*(1.4+Math.sin(t*2+i+ph));
+ g.beginPath();
+ g.fillStyle = rgbaOf(pal.accent, 0.5);
+ 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>
(oldest)
·
back to Damask Kaleidoscope
·
Richer damask motif: baseline ogee curl + counter-scroll + f 05dd868 →