web: playback, slice 4 of M3
usePlayback drives one global round on a rAF timer: 700 ms per round at 1x, speed pill cycling 0.5/1/2, play-once autoplay after the first load, no looping; at the final round the play button becomes replay. Under prefers-reduced-motion there is no autoplay, the page settles on the final round, and the cadence slows to 1000 ms. The PlayerBar is the mockup's centered pill with a native range scrubber (one tick per round, aria-valuetext, seeking pauses) and the global keyboard map: Space toggles, arrows step, Home/End jump; text inputs and native control handling are left alone. A single polite live region announces run updates, pauses, scrub releases, and the final state. Nodes reached in the current round pop in (scale 0.6 to 1, 250 ms ease-out) with a small deterministic stagger so each round reads as a wave. Per Justin's feedback the panel percentage and reached count now follow the playhead instead of sitting on the final outcome; the hero keeps the final numbers, since its sentence claims outcomes.
This commit is contained in:
parent
71baf10e73
commit
0e3a14a0b3
7 changed files with 736 additions and 13 deletions
|
|
@ -2,7 +2,7 @@
|
|||
import { computed } from 'vue'
|
||||
import NetworkView from './NetworkView.vue'
|
||||
import { useSimStore } from '@/composables/useSimStore'
|
||||
import { formatPct, roundPct } from '@/lib/format'
|
||||
import { roundPct } from '@/lib/format'
|
||||
import type { PanelSpec } from '@/presets/types'
|
||||
|
||||
const props = defineProps<{ panel: PanelSpec; accent: string }>()
|
||||
|
|
@ -11,10 +11,19 @@ const store = useSimStore()
|
|||
|
||||
const result = computed(() => store.state.resultsByPanelId[props.panel.id])
|
||||
const numStudents = computed(() => store.effectiveConfig(props.panel).numStudents)
|
||||
|
||||
// The numbers follow the playhead: they count what the network picture
|
||||
// shows at the current round, reaching the final outcome on the last one.
|
||||
const reachedCount = computed(() => {
|
||||
const panelResult = result.value
|
||||
if (!panelResult) return 0
|
||||
return panelResult.reachedAtRound.filter(
|
||||
(reachedAt) => reachedAt >= 0 && reachedAt <= store.state.round,
|
||||
).length
|
||||
})
|
||||
const reachedPctNow = computed(() => (reachedCount.value / numStudents.value) * 100)
|
||||
const tone = computed(() =>
|
||||
result.value && roundPct(result.value.reachedPct) <= store.preset.toneThresholdPct
|
||||
? 'good'
|
||||
: 'bad',
|
||||
roundPct(reachedPctNow.value) <= store.preset.toneThresholdPct ? 'good' : 'bad',
|
||||
)
|
||||
|
||||
interface PanelChip {
|
||||
|
|
@ -48,12 +57,9 @@ const dimmed = computed(() => store.state.runState === 'running' && result.value
|
|||
</button>
|
||||
</div>
|
||||
<template v-if="result">
|
||||
<div class="pct" :class="tone">
|
||||
{{ roundPct(result.reachedPct) }}<small>%</small>
|
||||
<span class="visually-hidden">{{ formatPct(result.reachedPct) }} reached</span>
|
||||
</div>
|
||||
<div class="pct" :class="tone">{{ roundPct(reachedPctNow) }}<small>%</small></div>
|
||||
<div class="meta">
|
||||
<span>{{ result.numReached }} of {{ numStudents }} reached</span>
|
||||
<span>{{ reachedCount }} of {{ numStudents }} reached</span>
|
||||
<span class="sep" aria-hidden="true">•</span>
|
||||
<span>{{ result.numRounds }} rounds</span>
|
||||
<span class="sep" aria-hidden="true">•</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue