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 { ROUND_MS } from '@/composables/usePlayback'
|
||||||
import { useSimStore } from '@/composables/useSimStore'
|
import { useSimStore } from '@/composables/useSimStore'
|
||||||
import { graphKey } from '@/lib/graph'
|
import { graphKey } from '@/lib/graph'
|
||||||
import { APPEAR_WINDOW, appearanceJitter } from '@/lib/reach'
|
import { nodeDisplayedAsReached } from '@/lib/reach'
|
||||||
import type { PanelSpec } from '@/presets/types'
|
import type { PanelSpec } from '@/presets/types'
|
||||||
|
|
||||||
// One panel's network picture (spec section 6). Shape carries meaning,
|
// 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)
|
const educatedNodes = new Set(panelResult.educated)
|
||||||
return layout.value.map((point, nodeIndex) => {
|
return layout.value.map((point, nodeIndex) => {
|
||||||
const reachedAtRound = panelResult.reachedAtRound[nodeIndex] ?? -1
|
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 {
|
return {
|
||||||
x: point.x,
|
x: point.x,
|
||||||
y: point.y,
|
y: point.y,
|
||||||
|
|
@ -70,16 +77,10 @@ function onNodeClick(nodeIndex: number, event: MouseEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dots of the current round appear at deterministic random moments spread
|
// Each dot's fade starts exactly when the shared appearance predicate
|
||||||
// across the round interval (the same jitter drives the live counter in
|
// flips it to reached (per-frame roundProgress gating), so no delay is
|
||||||
// the panel stats), then fade in over the rest of the interval; playback
|
// needed here; the fade itself takes a slice of the round interval.
|
||||||
// never shows a finished still frame between rounds.
|
const popDurationMs = computed(() => (ROUND_MS / store.state.speed) * 0.3)
|
||||||
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`
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -116,7 +117,6 @@ function popDelay(nodeIndex: number): string {
|
||||||
<circle
|
<circle
|
||||||
v-else-if="node.educated"
|
v-else-if="node.educated"
|
||||||
:class="{ pop: node.justReached }"
|
:class="{ pop: node.justReached }"
|
||||||
:style="node.justReached ? { animationDelay: popDelay(nodeIndex) } : undefined"
|
|
||||||
:cx="node.x"
|
:cx="node.x"
|
||||||
:cy="node.y"
|
:cy="node.y"
|
||||||
r="3.8"
|
r="3.8"
|
||||||
|
|
@ -127,7 +127,6 @@ function popDelay(nodeIndex: number): string {
|
||||||
<circle
|
<circle
|
||||||
v-else-if="node.reached"
|
v-else-if="node.reached"
|
||||||
:class="{ pop: node.justReached }"
|
:class="{ pop: node.justReached }"
|
||||||
:style="node.justReached ? { animationDelay: popDelay(nodeIndex) } : undefined"
|
|
||||||
:cx="node.x"
|
:cx="node.x"
|
||||||
:cy="node.y"
|
:cy="node.y"
|
||||||
r="4"
|
r="4"
|
||||||
|
|
@ -186,11 +185,11 @@ function popDelay(nodeIndex: number): string {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nodes reached this round fade and scale in across the full round
|
/* A node reached this round fades and scales in the moment its branch
|
||||||
interval (driven by --pop-ms), so playback reads as continuous motion.
|
flips (no delay; the trickle timing lives in nodeDisplayedAsReached).
|
||||||
The global reduced-motion rule collapses this to a discrete swap. */
|
The global reduced-motion rule collapses this to a discrete swap. */
|
||||||
.pop {
|
.pop {
|
||||||
animation: pop var(--pop-ms, 650ms) ease-out backwards;
|
animation: pop var(--pop-ms, 300ms) ease-out backwards;
|
||||||
transform-box: fill-box;
|
transform-box: fill-box;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,23 @@ export function appearanceJitter(nodeIndex: number): number {
|
||||||
return (mixed >>> 0) / 4294967296
|
return (mixed >>> 0) / 4294967296
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reach as displayed mid-transition: every earlier round fully, plus the
|
// Whether one node is displayed as reached mid-transition: every earlier
|
||||||
// current round's nodes whose appearance moment has passed. With
|
// round fully, plus the current round's nodes whose appearance moment has
|
||||||
// roundProgress = 1 this equals reachedCountAtRound.
|
// 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(
|
export function reachedCountDisplayed(
|
||||||
reachedAtRound: number[],
|
reachedAtRound: number[],
|
||||||
round: number,
|
round: number,
|
||||||
|
|
@ -35,10 +49,7 @@ export function reachedCountDisplayed(
|
||||||
): number {
|
): number {
|
||||||
let displayed = 0
|
let displayed = 0
|
||||||
reachedAtRound.forEach((reachedAt, nodeIndex) => {
|
reachedAtRound.forEach((reachedAt, nodeIndex) => {
|
||||||
if (reachedAt < 0 || reachedAt > round) return
|
if (nodeDisplayedAsReached(reachedAt, nodeIndex, round, roundProgress)) displayed += 1
|
||||||
if (reachedAt < round || roundProgress >= appearanceJitter(nodeIndex) * APPEAR_WINDOW) {
|
|
||||||
displayed += 1
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return displayed
|
return displayed
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue