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.
This commit is contained in:
parent
3a4e21b701
commit
2b47158b01
2 changed files with 34 additions and 24 deletions
|
|
@ -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<RenderedNode[]>(() => {
|
|||
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)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -116,7 +117,6 @@ function popDelay(nodeIndex: number): string {
|
|||
<circle
|
||||
v-else-if="node.educated"
|
||||
:class="{ pop: node.justReached }"
|
||||
:style="node.justReached ? { animationDelay: popDelay(nodeIndex) } : undefined"
|
||||
:cx="node.x"
|
||||
:cy="node.y"
|
||||
r="3.8"
|
||||
|
|
@ -127,7 +127,6 @@ function popDelay(nodeIndex: number): string {
|
|||
<circle
|
||||
v-else-if="node.reached"
|
||||
:class="{ pop: node.justReached }"
|
||||
:style="node.justReached ? { animationDelay: popDelay(nodeIndex) } : undefined"
|
||||
:cx="node.x"
|
||||
:cy="node.y"
|
||||
r="4"
|
||||
|
|
@ -186,11 +185,11 @@ function popDelay(nodeIndex: number): string {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Nodes reached this round fade and scale in across the full round
|
||||
interval (driven by --pop-ms), so playback reads as continuous motion.
|
||||
/* A node reached this round fades and scales in the moment its branch
|
||||
flips (no delay; the trickle timing lives in nodeDisplayedAsReached).
|
||||
The global reduced-motion rule collapses this to a discrete swap. */
|
||||
.pop {
|
||||
animation: pop var(--pop-ms, 650ms) ease-out backwards;
|
||||
animation: pop var(--pop-ms, 300ms) ease-out backwards;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,23 @@ export function appearanceJitter(nodeIndex: number): number {
|
|||
return (mixed >>> 0) / 4294967296
|
||||
}
|
||||
|
||||
// Reach as displayed mid-transition: every earlier round fully, plus the
|
||||
// current round's nodes whose appearance moment has passed. With
|
||||
// roundProgress = 1 this equals reachedCountAtRound.
|
||||
// Whether one node is displayed as reached mid-transition: every earlier
|
||||
// round fully, plus the current round's nodes whose appearance moment has
|
||||
// passed. The network rendering and the live counters share this exact
|
||||
// predicate, so a node keeps its unreached dot until the very frame its
|
||||
// rose dot starts fading in (no cohort blink at the round switch).
|
||||
export function nodeDisplayedAsReached(
|
||||
reachedAt: number,
|
||||
nodeIndex: number,
|
||||
round: number,
|
||||
roundProgress: number,
|
||||
): boolean {
|
||||
if (reachedAt < 0 || reachedAt > round) return false
|
||||
return reachedAt < round || roundProgress >= appearanceJitter(nodeIndex) * APPEAR_WINDOW
|
||||
}
|
||||
|
||||
// Reach as displayed mid-transition; with roundProgress = 1 this equals
|
||||
// reachedCountAtRound.
|
||||
export function reachedCountDisplayed(
|
||||
reachedAtRound: number[],
|
||||
round: number,
|
||||
|
|
@ -35,10 +49,7 @@ export function reachedCountDisplayed(
|
|||
): number {
|
||||
let displayed = 0
|
||||
reachedAtRound.forEach((reachedAt, nodeIndex) => {
|
||||
if (reachedAt < 0 || reachedAt > round) return
|
||||
if (reachedAt < round || roundProgress >= appearanceJitter(nodeIndex) * APPEAR_WINDOW) {
|
||||
displayed += 1
|
||||
}
|
||||
if (nodeDisplayedAsReached(reachedAt, nodeIndex, round, roundProgress)) displayed += 1
|
||||
})
|
||||
return displayed
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue