Skip to content

Commit 5172c66

Browse files
RuitingMaclaude
andcommitted
Cell: non-linear flare fade + path-driven pendulum kick
The flare alpha curve switches from linear to pow(1 - age/fade, 0.5): the lit cell stays near peak through most of fadeMs and snaps off at the end rather than dropping at constant rate — visually a sustained pulse with a clean cutoff. Each cell's current lit alpha now also feeds an angular acceleration into the pendulum integrator (PATH_LIT_KICK = 2.0 rad/s² per unit alpha, in the cell's natural rotation direction). The kick combines with the existing momentum→alpha boost to create a positive-feedback loop while the flare passes: lit cell spins up → momentum boosts its alpha further → which sustains the kick. After the flare fades the cell relaxes back to its omega target. Reorganized so the per-cell lit-alpha lookup happens in the visible loop (before physics) and is stored on V; the bbox loop reuses it. v.key dropped — no longer needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fad7050 commit 5172c66

1 file changed

Lines changed: 36 additions & 17 deletions

File tree

src/components/CellSketch.astro

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,13 @@
121121
const PATH_SPAWN_INTERVAL_MS = 5000; // mean ms between spawns (Poisson)
122122
const PATH_STEP_MS = 110; // ms between successive cell ignitions along the line
123123
const PATH_FADE_MS = 3500; // each cell's fade duration
124+
const PATH_FADE_GAMMA = 0.5; // <1 sustains brightness, falls fast at end (sqrt curve)
124125
const PATH_PEAK_ALPHA = 0.70; // peak alpha at the line core (full opacity ≡ 1)
125126
const PATH_MIN_LENGTH = 7; // Chebyshev cells; below this we skip & retry
126127
const PATH_MAX_SPREAD = 3; // perpendicular cells the flare bleeds into
127128
const PATH_SPREAD_DECAY = 0.60; // peak alpha ratio per cell of spread distance
128129
const PATH_SPREAD_STEP_MS = 200; // ms delay per cell of spread distance
130+
const PATH_LIT_KICK = 2.0; // angular accel (rad/s²) per unit lit alpha — drives the cell's pendulum
129131

130132
// ----- Pre-rendered orbital point sprite (true exp decay) -----
131133
const DOT_HALF = 11;
@@ -489,13 +491,33 @@
489491
const flatNeeded = 2 * cellCount;
490492
if (flatBuf.length < flatNeeded) flatBuf = new Float64Array(flatNeeded * 2);
491493

492-
type V = { cx: number; cy: number; r: number; px: number; py: number; c: Cell; key: string; fill: string; fillAlpha: number };
494+
type V = { cx: number; cy: number; r: number; px: number; py: number; c: Cell; lit: number; fill: string; fillAlpha: number };
493495
const visible: V[] = [];
494496
let n = 0;
495497

496498
for (let j = jMin; j <= jMax; j++) {
497499
for (let i = iMin; i <= iMax; i++) {
498500
const c = ensureCell(i, j);
501+
502+
// Path-lit alpha for this cell at `now`. Computed here (before
503+
// physics integration) so it can both drive the pendulum kick
504+
// and be stored on V for the render pass — single lookup
505+
// instead of two. Non-linear fade: pow(1 - age/fade, gamma)
506+
// with gamma < 1 sustains brightness through most of fadeMs
507+
// and snaps off at the end.
508+
let curLit = 0;
509+
if (activePaths.length > 0) {
510+
const key = cellKey(i, j);
511+
for (let pi = 0; pi < activePaths.length; pi++) {
512+
const entry = activePaths[pi].cells.get(key);
513+
if (!entry) continue;
514+
const age = now - entry.litAt;
515+
if (age < 0 || age > PATH_FADE_MS) continue;
516+
const a = entry.peak * Math.pow(1 - age / PATH_FADE_MS, PATH_FADE_GAMMA);
517+
if (a > curLit) curLit = a;
518+
}
519+
}
520+
499521
const cwx = (i + 0.5) * CELL_SIZE;
500522
const cwy = (j + 0.5) * CELL_SIZE;
501523
const cx = cwx - camX + halfW;
@@ -513,6 +535,14 @@
513535
const tc = Math.atan2(dy, dx);
514536
dd += -k * Math.sin(c.theta - tc);
515537
}
538+
// Path-lit kick: angular accel proportional to current lit
539+
// alpha, in the cell's natural rotation direction. As the
540+
// flare advances, lit cells spin up; combined with the
541+
// existing momentum→alpha boost in the bbox loop, a lit
542+
// cell visibly rolls and stays bright via positive feedback.
543+
if (curLit > 0) {
544+
dd += Math.sign(c.omegaBase) * curLit * PATH_LIT_KICK;
545+
}
516546
c.thetaDot += dd * stepDt;
517547
c.theta += c.thetaDot * stepDt;
518548
}
@@ -523,7 +553,7 @@
523553
flatBuf[n * 2 + 1] = py;
524554
// fill / fillAlpha are filled in below, after Voronoi build,
525555
// when each cell's polygon-canvas overlap is known.
526-
visible.push({ cx, cy, r, px, py, c, key: cellKey(i, j), fill: '', fillAlpha: 0 });
556+
visible.push({ cx, cy, r, px, py, c, lit: curLit, fill: '', fillAlpha: 0 });
527557
n++;
528558
}
529559
}
@@ -613,21 +643,10 @@
613643
// fully decayed cells light up under the cursor.
614644
const m = Math.min(1, Math.abs(c.thetaDot) / MOMENTUM_REF);
615645
const boost = MOMENTUM_BOOST * m * m;
616-
// Path-light: max-blend the largest active path contribution
617-
// for this cell. Each cell on a path carries its own peak
618-
// (higher near the line core, lower in the spread halo); fade
619-
// is linear from peak → 0 across PATH_FADE_MS, starting at the
620-
// cell's own ignition time.
621-
let lit = 0;
622-
for (let pi = 0; pi < activePaths.length; pi++) {
623-
const entry = activePaths[pi].cells.get(v.key);
624-
if (!entry) continue;
625-
const age = now - entry.litAt;
626-
if (age < 0 || age > PATH_FADE_MS) continue;
627-
const a = entry.peak * (1 - age / PATH_FADE_MS);
628-
if (a > lit) lit = a;
629-
}
630-
v.fillAlpha = Math.max(Math.min(1, baseAlpha + boost), lit);
646+
// Path-light contribution was already computed in the visible
647+
// loop (so the same value drove the pendulum kick); reuse it
648+
// here as a max-blend over the veil + momentum base.
649+
v.fillAlpha = Math.max(Math.min(1, baseAlpha + boost), v.lit);
631650
v.fill = `rgba(${ACCENT[0]},${ACCENT[1]},${ACCENT[2]},${v.fillAlpha})`;
632651

633652
bboxesBuf[k * 4] = Math.max(0, Math.floor(bbMinX));

0 commit comments

Comments
 (0)