← back to Damask Kaleidoscope
gemma3-damask-kaleidoscope.html
360 lines
<!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>