Skip to content

Commit e375295

Browse files
RuitingMaclaude
andcommitted
Cell: lifecycle keyed on polygon visibility, not grid coords
Previous logic gated the birth-clock on whether the cell's grid coordinate sat in [iMin+2, iMax-2] × [jMin+2, jMax-2]. But a cell's polygon can clip a strip on-canvas while its seed grid is already in the padding zone — at that transition the alpha jumped between "padding-zone full" and "on-canvas faded". Move the lifecycle decision to the bbox loop after Voronoi build, where each cell's polygon is in hand. The test is now `polygon bbox overlaps canvas rect`. Birth fires on first overlap; off-canvas (empty polygon or non-overlapping bbox) resets the clock — strict polygon-boundary semantics, no grid-discretization jumps. V keeps a Cell ref so the bbox loop can update fill/fillAlpha after the lifecycle check; the visible loop now leaves those slots empty and doesn't touch birthTime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6943246 commit e375295

1 file changed

Lines changed: 39 additions & 33 deletions

File tree

src/components/CellSketch.astro

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,17 @@
8787
// Per-cell tint as a translucent moss veil over BG. Each cell gets
8888
// a startAlpha sampled from a smooth 2-D noise (so adjacent cells
8989
// share regional means — opaque clusters and rare transparent
90-
// pockets, instead of uniform scatter). The cell sits at startAlpha
91-
// while it's in the padding zone; the moment its seed crosses into
92-
// the on-canvas window the birth-clock starts and alpha fades
93-
// linearly to 0 across TINT_LIFETIME_MS. Drifting back off-canvas
94-
// (or being pruned and re-entering) resets the clock — every
95-
// boundary crossing into view reads as a rebirth.
90+
// pockets, instead of uniform scatter). A cell sits at startAlpha
91+
// while its polygon is fully off-canvas; the moment the polygon's
92+
// bbox first overlaps the canvas the birth-clock starts and alpha
93+
// fades linearly to 0 across TINT_LIFETIME_MS. When the polygon
94+
// drifts back off-canvas (or the cell is pruned and re-enters) the
95+
// clock resets — every visual entry reads as a rebirth.
96+
//
97+
// The visibility test is on the polygon, not the seed's grid coord:
98+
// the polygon can clip a strip on-canvas while its seed sits in the
99+
// padding zone, and a grid-based test caused a visible alpha jump
100+
// at that boundary.
96101
const MAX_TINT = 0.55;
97102
const TINT_GROUP_SCALE = 4; // cells per noise unit (larger → bigger groups)
98103
const TINT_BIAS = 0.5; // <1 biases toward opaque (rare transparent)
@@ -262,7 +267,7 @@
262267
omegaAmp: number;
263268
omegaPhase: number;
264269
rFactor: number;
265-
birthTime: number; // ms — -1 until cell first enters the on-canvas window; then now
270+
birthTime: number; // ms — -1 while polygon is fully off-canvas; set to now on first overlap
266271
startAlpha: number; // peak fill alpha at birth, faded over time
267272
};
268273
const cells = new Map<string, Cell>();
@@ -357,32 +362,13 @@
357362
const flatNeeded = 2 * cellCount;
358363
if (flatBuf.length < flatNeeded) flatBuf = new Float64Array(flatNeeded * 2);
359364

360-
type V = { cx: number; cy: number; r: number; px: number; py: number; fill: string; fillAlpha: number };
365+
type V = { cx: number; cy: number; r: number; px: number; py: number; c: Cell; fill: string; fillAlpha: number };
361366
const visible: V[] = [];
362367
let n = 0;
363368

364-
// The on-canvas window is the visible range minus the 2-cell
365-
// padding on each side (cells in the padding zone have their seed
366-
// off-screen but contribute clipped polygon strips at the canvas
367-
// edge). Birth-clock starts when a cell first enters this window.
368-
const onCanvasIMin = iMin + 2, onCanvasIMax = iMax - 2;
369-
const onCanvasJMin = jMin + 2, onCanvasJMax = jMax - 2;
370-
371369
for (let j = jMin; j <= jMax; j++) {
372370
for (let i = iMin; i <= iMax; i++) {
373371
const c = ensureCell(i, j);
374-
// Re-set birth on every padding-zone frame: a cell that drifts
375-
// back off-canvas (without being pruned) gets a fresh clock the
376-
// next time it crosses in. Birth fires on the first on-canvas
377-
// frame after any padding-zone visit.
378-
const onCanvas =
379-
i >= onCanvasIMin && i <= onCanvasIMax &&
380-
j >= onCanvasJMin && j <= onCanvasJMax;
381-
if (onCanvas) {
382-
if (c.birthTime < 0) c.birthTime = now;
383-
} else {
384-
c.birthTime = -1;
385-
}
386372
const cwx = (i + 0.5) * CELL_SIZE;
387373
const cwy = (j + 0.5) * CELL_SIZE;
388374
const cx = cwx - camX + halfW;
@@ -408,12 +394,9 @@
408394
const py = cy + r * Math.sin(c.theta);
409395
flatBuf[n * 2] = px;
410396
flatBuf[n * 2 + 1] = py;
411-
const fade = c.birthTime < 0
412-
? 1 // still in padding zone — display at full startAlpha
413-
: Math.max(0, 1 - (now - c.birthTime) / TINT_LIFETIME_MS);
414-
const fillAlpha = c.startAlpha * fade;
415-
const fill = `rgba(${ACCENT[0]},${ACCENT[1]},${ACCENT[2]},${fillAlpha})`;
416-
visible.push({ cx, cy, r, px, py, fill, fillAlpha });
397+
// fill / fillAlpha are filled in below, after Voronoi build,
398+
// when each cell's polygon-canvas overlap is known.
399+
visible.push({ cx, cy, r, px, py, c, fill: '', fillAlpha: 0 });
417400
n++;
418401
}
419402
}
@@ -463,12 +446,18 @@
463446
const sdfW = sdfCanvas.width;
464447
const sdfH = sdfCanvas.height;
465448
for (let k = 0; k < n; k++) {
449+
const v = visible[k];
450+
const c = v.c;
466451
const polyRaw = voronoi.cellPolygon(k);
467452
if (!polyRaw) {
468453
bboxesBuf[k * 4] = 0;
469454
bboxesBuf[k * 4 + 1] = 0;
470455
bboxesBuf[k * 4 + 2] = 0;
471456
bboxesBuf[k * 4 + 3] = 0;
457+
// No polygon at all → fully off-canvas; reset birth so the
458+
// next time this cell's polygon does overlap we count it as a
459+
// fresh entry rather than continuing the stale clock.
460+
c.birthTime = -1;
472461
continue;
473462
}
474463
let bbMinX = Infinity, bbMinY = Infinity, bbMaxX = -Infinity, bbMaxY = -Infinity;
@@ -478,6 +467,23 @@
478467
if (p[1] < bbMinY) bbMinY = p[1];
479468
if (p[1] > bbMaxY) bbMaxY = p[1];
480469
}
470+
// Lifecycle test: does the polygon's bbox overlap the canvas?
471+
// Grid-coord based tests (i,j vs onCanvas window) miss the case
472+
// where the seed grid is in the padding zone but the polygon
473+
// still clips a strip on-canvas — caused a visible alpha jump
474+
// (full padding-zone alpha → faded on-canvas alpha) at the edge.
475+
const polyVisible = bbMaxX > 0 && bbMinX < w && bbMaxY > 0 && bbMinY < h;
476+
if (polyVisible) {
477+
if (c.birthTime < 0) c.birthTime = now;
478+
} else {
479+
c.birthTime = -1;
480+
}
481+
const fade = c.birthTime < 0
482+
? 1
483+
: Math.max(0, 1 - (now - c.birthTime) / TINT_LIFETIME_MS);
484+
v.fillAlpha = c.startAlpha * fade;
485+
v.fill = `rgba(${ACCENT[0]},${ACCENT[1]},${ACCENT[2]},${v.fillAlpha})`;
486+
481487
bboxesBuf[k * 4] = Math.max(0, Math.floor(bbMinX));
482488
bboxesBuf[k * 4 + 1] = Math.max(0, Math.floor(bbMinY));
483489
bboxesBuf[k * 4 + 2] = Math.min(sdfW, Math.ceil(bbMaxX));

0 commit comments

Comments
 (0)