[object Object]

← back to Damask Kaleidoscope

v2.1.0: density pass — filler ogee arms + inner acanthus collar + guilloché lattice + warm bronze floor kill the starburst read; fix mirror-breaking sample offset that caused alternating black pie-slices

b4ec0dc28c2d9764c0a6f3bac6ab74fcb7d76801 · 2026-07-25 08:45:32 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit b4ec0dc28c2d9764c0a6f3bac6ab74fcb7d76801
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 08:45:32 2026 -0700

    v2.1.0: density pass — filler ogee arms + inner acanthus collar + guilloché lattice + warm bronze floor kill the starburst read; fix mirror-breaking sample offset that caused alternating black pie-slices
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 README.md  |  12 ++++++--
 index.html | 102 +++++++++++++++++++++++++++++++++++++++++++++----------------
 2 files changed, 85 insertions(+), 29 deletions(-)

diff --git a/README.md b/README.md
index c86032a..4d37d6a 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,12 @@ Open the file in any modern browser — no build, no dependencies.
   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.
+- `v2.0.0` — merged 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.
+- `v2.1.0` — **density pass** to kill the "starburst" read: half-step filler ogee arms
+  fill the angular gaps at every radius; a tight inner acanthus collar + a faint
+  concentric guilloché lattice + a warm bronze floor lift the inter-motif voids from
+  dead black to gilt ground. (Also fixed a real bug: a large off-center kaleidoscope
+  sample broke the per-wedge mirror symmetry — odd wedges are `scale(1,-1)` but the
+  offset wasn't — producing alternating black pie-slices; the offset is now a whisper.)
diff --git a/index.html b/index.html
index d423301..504542f 100644
--- a/index.html
+++ b/index.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<!-- Designer Wallcoverings — Damask Kaleidoscope · v2.0.0 -->
+<!-- Designer Wallcoverings — Damask Kaleidoscope · v2.1.0 -->
 <html lang="en">
 <head>
 <meta charset="utf-8">
@@ -209,16 +209,49 @@
     g.fillStyle = bgGrad;
     g.fillRect(0,0,S,S);
 
+    // faint warm floor — lifts the inter-motif voids from dead black to deep
+    // bronze so the radial negative space reads as ornament ground, not sunburst
+    // gaps. The rim vignette re-darkens the edge, so the ground stays near-black.
+    const floor = g.createRadialGradient(rs,rs,rs*0.04, rs,rs, rs*0.9);
+    floor.addColorStop(0,   rgbaOf(pal.gold, 0.12));
+    floor.addColorStop(0.55,rgbaOf(pal.gold, 0.06));
+    floor.addColorStop(1,   rgbaOf(pal.gold, 0.0));
+    g.globalCompositeOperation = "lighter";
+    g.fillStyle = floor; g.fillRect(0,0,S,S);
+    g.globalCompositeOperation = "source-over";
+
     g.translate(rs, rs);
 
+    // faint guilloché ground — concentric gilt rings threaded across the whole
+    // field. Being rotationally symmetric they stay clean rings through the
+    // kaleidoscope and lace the inter-arm voids with filigree, so the mandala
+    // reads as gilt lattice rather than hard black pie-slices.
+    g.globalCompositeOperation = "lighter";
+    g.lineWidth = Math.max(rs*0.0035, 0.5);
+    for (let ri=0; ri<10; ri++){
+      const rad = rs*(0.13 + ri*0.088) * (1 + 0.02*Math.sin(t*0.6 + ri));
+      g.strokeStyle = rgbaOf(pal.gold, 0.16 - ri*0.006);
+      g.beginPath(); g.arc(0,0, rad, 0, Math.PI*2); g.stroke();
+    }
+    g.globalCompositeOperation = "source-over";
+
     // central bezier urn (foot of the damask), drawn first so foliage overlaps it
     drawUrn(g, rs, t, pal, amp);
 
     const ARMS = 6;
+    // main radiating ogee arms
     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);
+      drawFlourish(g, rs, t, pal, amp, k, 1);
+      g.restore();
+    }
+    // shorter filler arms offset by half a step — full ogee stems that occupy the
+    // angular gaps at every radius, so the mandala reads as a field, not spokes.
+    for (let k=0; k<ARMS; k++) {
+      g.save();
+      g.rotate(((k+0.5)/ARMS)*Math.PI*2 + swirl*0.35);
+      drawFlourish(g, rs, t, pal, amp, k+11, 0.66);
       g.restore();
     }
 
@@ -231,12 +264,13 @@
       const bud = rs*0.13*(0.85 + 0.2*Math.sin(t*1.5 + k));
       drawLeaf(g, Math.cos(a)*rr, Math.sin(a)*rr, a + Math.PI/2, bud, (k%2?1:-1), pal, amp, t, k*0.9, k%3===0);
     }
-    // a tighter inner bud ring for mid-radius density
-    const FILL2 = 8;
+    // a tight inner petal collar ringing the hub — kills the near-center black
+    // pie by wrapping the medallion in dense overlapping acanthus petals.
+    const FILL2 = 12;
     for (let k=0; k<FILL2; k++){
       const a  = (k/FILL2)*Math.PI*2 + swirl*0.18;
-      const rr = rs*0.34;
-      drawLeaf(g, Math.cos(a)*rr, Math.sin(a)*rr, a + Math.PI/2, rs*0.10, (k%2?-1:1), pal, amp, t, k*1.3+2, false);
+      const rr = rs*(0.22 + 0.02*Math.sin(t*1.6 + k));
+      drawLeaf(g, Math.cos(a)*rr, Math.sin(a)*rr, a + Math.PI/2, rs*0.13, (k%2?-1:1), pal, amp, t, k*1.3+2, k%2===0);
     }
 
     // central medallion bloom (gilt sheen at the very center) — additive
@@ -295,18 +329,20 @@
   // 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) {
+  function drawFlourish(g, rs, t, pal, amp, seed, reach) {
+    reach = reach || 1;                          // 1 = full main arm, <1 = shorter filler arm
     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);
+    const rr = rs * reach;                        // radial reach of THIS arm
 
     // 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: -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
+    const p0 = { x: 0, y: -rr*0.14 };
+    const p1 = { x: rr*(0.34 + 0.06*sway),  y: -rr*0.24 };            // curl out low
+    const p2 = { x: rr*(0.20 - 0.10*sway),  y: -rr*(0.58*breathe) };  // sweep up & back in
+    const p3 = { x: rr*(0.52 + 0.08*Math.sin(t*0.7+ph)),
+                 y: -rr*(0.92*breathe) };                             // hook to the tip
 
     // gilt stem: soft outer glow pass, then a bright core pass
     g.lineCap = "round"; g.lineJoin = "round";
@@ -314,20 +350,20 @@
       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.16); g.lineWidth = rs*0.058; }
-      else         { g.strokeStyle = rgbaOf(pal.gold,0.72); g.lineWidth = rs*0.020; }
+      if (pass===0){ g.strokeStyle = rgbaOf(pal.gold,0.16); g.lineWidth = rs*0.058*(0.6+0.4*reach); }
+      else         { g.strokeStyle = rgbaOf(pal.gold,0.72); g.lineWidth = rs*0.022*(0.6+0.4*reach); }
       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 };
+    const c1 = { x: c0.x - rr*(0.22+0.05*sway), y: c0.y - rr*0.06 };
+    const c2 = { x: c0.x - rr*0.10,             y: c0.y - rr*0.30*breathe };
+    const c3 = { x: c0.x + rr*0.06,             y: c0.y - rr*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.45); g.lineWidth = rs*0.013; g.stroke();
+    g.strokeStyle = rgbaOf(pal.gold,0.45); g.lineWidth = rs*0.013*reach; g.stroke();
 
     // leaves / buds strung along the main stem (alternating gold + carmine bloom)
     const LEAVES = 5;
@@ -337,14 +373,14 @@
       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.22*(1-u*0.4)) * (0.9+0.25*Math.sin(t*1.3+ph+i));
+      const scale = rr*(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 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);
+    drawLeaf(g, c3.x, c3.y, Math.atan2(c3.y-c2.y, c3.x-c2.x)-0.6, rr*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);
+    drawLeaf(g, p3.x, p3.y, Math.atan2(p3.y,p3.x), rr*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++){
@@ -409,7 +445,7 @@
              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) {
+  function compose(rot, ox, oy) {
     ctx.setTransform(dpr,0,0,dpr,0,0);
     ctx.fillStyle = curPal.deep;
     ctx.fillRect(0,0,W,H);
@@ -418,6 +454,10 @@
     ctx.translate(cx, cy);
     ctx.rotate(rot);
 
+    // Sample the source OFF-CENTER (ox,oy) and slowly drifting — this is what
+    // turns radial "spokes" into interlocking concentric mandala rings, i.e. a
+    // real kaleidoscope read instead of a starburst. Draw the source oversized
+    // (R*2.4) so the shifted sample still covers the full wedge radius.
     for (let i=0;i<SIDES;i++){
       ctx.save();
       ctx.rotate(i*SLICE);
@@ -428,8 +468,7 @@
       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.drawImage(src, -R*1.2 + ox, -R*1.2 + oy, R*2.4, R*2.4);
       ctx.restore();
     }
     ctx.restore();
@@ -447,7 +486,7 @@
     // 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.60)");
+    vg.addColorStop(1,"rgba(0,0,0,0.50)");
     ctx.fillStyle = vg;
     ctx.fillRect(0,0,W,H);
   }
@@ -478,8 +517,19 @@
     const swirl = t*0.15 + wx*1.6;
     const amp = warp * 0.9;
 
+    // off-center kaleidoscope sample point: a slow orbital drift, nudged by the
+    // pointer, kept within ±0.2R so the oversized source still covers the wedge.
+    // NB: a large sample offset breaks the per-wedge mirror symmetry (odd wedges
+    // are scale(1,-1)'d but the offset is not), which produces alternating black
+    // pie-slices. Keep it a whisper — just enough to animate, not enough to split
+    // the mirror. Density comes from the filler arms + warm floor, not the offset.
+    const offR = 0.02 * R;
+    const offA = t*0.05;
+    const ox = Math.cos(offA)*offR;
+    const oy = Math.sin(offA)*offR;
+
     drawSource(t, curPal, swirl, amp);
-    compose(rot);
+    compose(rot, ox, oy);
 
     requestAnimationFrame(frame);
   }

← cdcbfd9 auto-save: 2026-07-25T08:35:01 (1 files) — index.html  ·  back to Damask Kaleidoscope  ·  chore: remove dead pillDot ref, stamp v2.1.1 (session close) d2883c4 →