From 2b47158b0102911e55ca3e3d2c9dd41ed89642a5 Mon Sep 17 00:00:00 2001 From: Justin Visser Date: Wed, 10 Jun 2026 18:28:49 +0200 Subject: [PATCH] web: kill the round-switch flash, dots swap exactly at their moment The flash came from branch switching at the round tick: every node due that round dropped its gray unreached dot at once while its rose dot waited invisibly for an animation delay. The unreached/reached branch is now gated per frame by the same appearance predicate the live counter uses (nodeDisplayedAsReached), so each gray dot stays put until the very frame its rose dot starts fading in. Animation delays are gone; the fade starts on the branch flip. --- web/src/components/NetworkView.vue | 33 +++++++++++++++--------------- web/src/lib/reach.ts | 25 +++++++++++++++------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/web/src/components/NetworkView.vue b/web/src/components/NetworkView.vue index 5b6f853..e11b6e9 100644 --- a/web/src/components/NetworkView.vue +++ b/web/src/components/NetworkView.vue @@ -5,7 +5,7 @@ import { useNodeHover } from '@/composables/useNodeHover' import { ROUND_MS } from '@/composables/usePlayback' import { useSimStore } from '@/composables/useSimStore' import { graphKey } from '@/lib/graph' -import { APPEAR_WINDOW, appearanceJitter } from '@/lib/reach' +import { nodeDisplayedAsReached } from '@/lib/reach' import type { PanelSpec } from '@/presets/types' // One panel's network picture (spec section 6). Shape carries meaning, @@ -39,7 +39,14 @@ const nodes = computed(() => { const educatedNodes = new Set(panelResult.educated) return layout.value.map((point, nodeIndex) => { const reachedAtRound = panelResult.reachedAtRound[nodeIndex] ?? -1 - const reached = reachedAtRound >= 0 && reachedAtRound <= store.state.round + // A node stays an unreached dot until its own appearance moment inside + // the round transition; the swap to rose is when its fade-in starts. + const reached = nodeDisplayedAsReached( + reachedAtRound, + nodeIndex, + store.state.round, + store.state.roundProgress, + ) return { x: point.x, y: point.y, @@ -70,16 +77,10 @@ function onNodeClick(nodeIndex: number, event: MouseEvent) { } } -// Dots of the current round appear at deterministic random moments spread -// across the round interval (the same jitter drives the live counter in -// the panel stats), then fade in over the rest of the interval; playback -// never shows a finished still frame between rounds. -const roundIntervalMs = computed(() => ROUND_MS / store.state.speed) -const popDurationMs = computed(() => roundIntervalMs.value * 0.3) - -function popDelay(nodeIndex: number): string { - return `${appearanceJitter(nodeIndex) * APPEAR_WINDOW * roundIntervalMs.value}ms` -} +// Each dot's fade starts exactly when the shared appearance predicate +// flips it to reached (per-frame roundProgress gating), so no delay is +// needed here; the fade itself takes a slice of the round interval. +const popDurationMs = computed(() => (ROUND_MS / store.state.speed) * 0.3)