|
87 | 87 | // Per-cell tint as a translucent moss veil over BG. Each cell gets |
88 | 88 | // a startAlpha sampled from a smooth 2-D noise (so adjacent cells |
89 | 89 | // 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. |
96 | 101 | const MAX_TINT = 0.55; |
97 | 102 | const TINT_GROUP_SCALE = 4; // cells per noise unit (larger → bigger groups) |
98 | 103 | const TINT_BIAS = 0.5; // <1 biases toward opaque (rare transparent) |
|
262 | 267 | omegaAmp: number; |
263 | 268 | omegaPhase: number; |
264 | 269 | 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 |
266 | 271 | startAlpha: number; // peak fill alpha at birth, faded over time |
267 | 272 | }; |
268 | 273 | const cells = new Map<string, Cell>(); |
|
357 | 362 | const flatNeeded = 2 * cellCount; |
358 | 363 | if (flatBuf.length < flatNeeded) flatBuf = new Float64Array(flatNeeded * 2); |
359 | 364 |
|
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 }; |
361 | 366 | const visible: V[] = []; |
362 | 367 | let n = 0; |
363 | 368 |
|
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 | | - |
371 | 369 | for (let j = jMin; j <= jMax; j++) { |
372 | 370 | for (let i = iMin; i <= iMax; i++) { |
373 | 371 | 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 | | - } |
386 | 372 | const cwx = (i + 0.5) * CELL_SIZE; |
387 | 373 | const cwy = (j + 0.5) * CELL_SIZE; |
388 | 374 | const cx = cwx - camX + halfW; |
|
408 | 394 | const py = cy + r * Math.sin(c.theta); |
409 | 395 | flatBuf[n * 2] = px; |
410 | 396 | 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 }); |
417 | 400 | n++; |
418 | 401 | } |
419 | 402 | } |
|
463 | 446 | const sdfW = sdfCanvas.width; |
464 | 447 | const sdfH = sdfCanvas.height; |
465 | 448 | for (let k = 0; k < n; k++) { |
| 449 | + const v = visible[k]; |
| 450 | + const c = v.c; |
466 | 451 | const polyRaw = voronoi.cellPolygon(k); |
467 | 452 | if (!polyRaw) { |
468 | 453 | bboxesBuf[k * 4] = 0; |
469 | 454 | bboxesBuf[k * 4 + 1] = 0; |
470 | 455 | bboxesBuf[k * 4 + 2] = 0; |
471 | 456 | 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; |
472 | 461 | continue; |
473 | 462 | } |
474 | 463 | let bbMinX = Infinity, bbMinY = Infinity, bbMaxX = -Infinity, bbMaxY = -Infinity; |
|
478 | 467 | if (p[1] < bbMinY) bbMinY = p[1]; |
479 | 468 | if (p[1] > bbMaxY) bbMaxY = p[1]; |
480 | 469 | } |
| 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 | + |
481 | 487 | bboxesBuf[k * 4] = Math.max(0, Math.floor(bbMinX)); |
482 | 488 | bboxesBuf[k * 4 + 1] = Math.max(0, Math.floor(bbMinY)); |
483 | 489 | bboxesBuf[k * 4 + 2] = Math.min(sdfW, Math.ceil(bbMaxX)); |
|
0 commit comments