← 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 →