fix(desktop): center memory-graph timeline stars, surface quiet buckets

Revert the helix coil to the constellation scatter, biased toward the
midline (triangular vertical), and stop a packed core ring from crushing
every quieter bucket into one invisible speck: sqrt-scale the per-bucket
star count, floor star size to 2px, and lift the dim baseline.
This commit is contained in:
Brooklyn Nicholson 2026-06-30 01:48:04 -05:00
parent 7548910ece
commit c0b308e1fe

View file

@ -35,10 +35,6 @@ const ACTIVE_MARKER_CLASS = 'opacity-100'
const INACTIVE_MARKER_CLASS = 'opacity-30'
// Busiest bucket gets this many stars; quieter ones scale down proportionally.
const MAX_STARS_PER_BUCKET = 7
// Full coils the constellation winds across the timeline's width.
const COIL_TURNS = 6
// Vertical swing (in % of track height) the coil arcs above/below the midline.
const COIL_AMPLITUDE = 36
// Deterministic PRNG (mulberry32) so a bucket's stars stay put across renders.
function rng(seed: number): () => number {
@ -54,11 +50,11 @@ function rng(seed: number): () => number {
}
}
// Wind each time bucket's activity into stars along a helix: count ∝ events,
// split between skill- and memory-coloured stars, ordered left→right and arced
// above/below the midline by a sine wave so the field reads as a coiling spiral
// rather than random scatter. Front-of-coil stars (cos→1) read brighter and
// larger for a sense of depth. A starmap timeline for a starmap.
// Scatter each time bucket's activity into stars: count ∝ events, split between
// skill- and memory-coloured stars, jittered within the bucket's horizontal slot
// and across the track height. Vertical placement is biased toward the midline
// (a triangular distribution) so stars cluster near the centre more often than
// the edges. A starmap timeline for a starmap.
function buildStars(axis: TimeAxis): Star[] {
const n = Math.max(1, axis.buckets.length)
const stars: Star[] = []
@ -69,28 +65,30 @@ function buildStars(axis: TimeAxis): Star[] {
}
const intensity = axis.maxTotal > 0 ? b.total / axis.maxTotal : 0
const count = Math.max(1, Math.round(intensity * MAX_STARS_PER_BUCKET))
// sqrt curve so a single co-timed burst (a packed core ring) doesn't crush
// every quieter bucket down to the 1-star floor and read as blank.
const count = Math.max(1, Math.round(Math.sqrt(intensity) * MAX_STARS_PER_BUCKET))
const skillCount = Math.round((b.skill / b.total) * count)
const r = rng(i * 9973 + 7)
const slot = 1 / n
const center = (i + 0.5) / n
for (let s = 0; s < count; s++) {
// Ordered position within the bucket's slot keeps the coil smooth.
const frac = (i + (s + 0.5) / count) / n
const angle = frac * COIL_TURNS * Math.PI * 2
// Depth: front of the coil (cos→1) is brighter/larger than the back.
const depth = (Math.cos(angle) + 1) / 2
const wobble = (r() - 0.5) * slot * 0.25
const top = 50 + Math.sin(angle) * COIL_AMPLITUDE + (r() - 0.5) * 5
const jitter = (r() - 0.5) * slot * 0.9
// Average of two uniforms → triangular peak at 0.5, pulling stars toward
// the midline more often while still reaching the edges occasionally.
const vertical = (r() + r()) / 2
stars.push({
delay: r() * 3,
duration: 2.4 + r() * 2.6,
kind: s < skillCount ? 'skill' : 'memory',
leftPct: Math.max(0, Math.min(1, frac + wobble)) * 100,
opacity: 0.45 + depth * 0.5,
size: 1 + Math.round(depth * 2.4),
topPct: Math.max(6, Math.min(94, top))
leftPct: Math.max(0, Math.min(1, center + jitter)) * 100,
// Brighter, slightly larger stars are rarer.
opacity: 0.5 + r() * 0.5,
// Floor at 2px so a lone star in a quiet bucket still reads on black.
size: 2 + Math.round(r() * r() * 2.2),
topPct: 12 + vertical * 76
})
}
})
@ -225,7 +223,7 @@ export const Timeline = memo(function Timeline({
backgroundColor: colorFor(star.kind),
height: star.size,
left: `${star.leftPct}%`,
opacity: 0.16,
opacity: 0.22,
top: `${star.topPct}%`,
width: star.size
}}