add looping recording playback to tutorial

RecordingPlayer gains autoPlay, loop, showControls props.
Tutorial shows the stick figure demo on loop above the tips.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
main
name 2026-04-12 09:44:41 -07:00
parent f032139b7c
commit c98a9e71e0
3 changed files with 57 additions and 24 deletions

View File

@ -35,14 +35,18 @@ interface RecordingPlayerProps {
frames: RecordedFrame[]
width?: number
height?: number
autoPlay?: boolean
loop?: boolean
showControls?: boolean
}
export function RecordingPlayer({ frames, width = 640, height = 480 }: RecordingPlayerProps) {
export function RecordingPlayer({ frames, width = 640, height = 480, autoPlay = false, loop = false, showControls = true }: RecordingPlayerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [playing, setPlaying] = useState(false)
const [playing, setPlaying] = useState(autoPlay)
const [frameIndex, setFrameIndex] = useState(0)
const playingRef = useRef(false)
const frameIndexRef = useRef(0)
const startTimeRef = useRef(0)
const totalDuration = frames.length > 0 ? frames[frames.length - 1].t : 0
@ -115,13 +119,13 @@ export function RecordingPlayer({ frames, width = 640, height = 480 }: Recording
playingRef.current = true
frameIndexRef.current = frameIndex
const startTime = performance.now()
startTimeRef.current = performance.now()
const startT = frames[frameIndexRef.current].t
let rafId: number
function tick() {
if (!playingRef.current) return
const elapsed = performance.now() - startTime
const elapsed = performance.now() - startTimeRef.current
const targetT = startT + elapsed
// Advance frame index
@ -133,6 +137,13 @@ export function RecordingPlayer({ frames, width = 640, height = 480 }: Recording
setFrameIndex(idx)
if (idx >= frames.length - 1) {
if (loop) {
frameIndexRef.current = 0
setFrameIndex(0)
startTimeRef.current = performance.now()
rafId = requestAnimationFrame(tick)
return
}
setPlaying(false)
return
}
@ -173,25 +184,27 @@ export function RecordingPlayer({ frames, width = 640, height = 480 }: Recording
height={height}
className={styles.canvas}
/>
<div className={styles.controls}>
<button className={styles.playBtn} onClick={togglePlay}>
{playing ? 'Pause' : 'Play'}
</button>
<input
type="range"
className={styles.scrubber}
min={0}
max={frames.length - 1}
value={frameIndex}
onChange={handleScrub}
/>
<span className={styles.time}>
{(frames[frameIndex].t / 1000).toFixed(1)}s / {(totalDuration / 1000).toFixed(1)}s
</span>
<span className={styles.frameInfo}>
{frameIndex + 1}/{frames.length}
</span>
</div>
{showControls && (
<div className={styles.controls}>
<button className={styles.playBtn} onClick={togglePlay}>
{playing ? 'Pause' : 'Play'}
</button>
<input
type="range"
className={styles.scrubber}
min={0}
max={frames.length - 1}
value={frameIndex}
onChange={handleScrub}
/>
<span className={styles.time}>
{(frames[frameIndex].t / 1000).toFixed(1)}s / {(totalDuration / 1000).toFixed(1)}s
</span>
<span className={styles.frameInfo}>
{frameIndex + 1}/{frames.length}
</span>
</div>
)}
</div>
)
}

View File

@ -22,10 +22,18 @@
font-size: 36px;
color: #d946ef;
text-align: center;
margin: 0 0 24px;
margin: 0 0 16px;
text-shadow: 0 0 30px rgba(217, 70, 239, 0.5);
}
.playerWrap {
display: flex;
justify-content: center;
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
}
.tips {
list-style: none;
counter-reset: tip;

View File

@ -1,5 +1,7 @@
import { useState } from 'react'
import { playSound, SFX_BUTTON_DEFAULT } from '../lib/sounds'
import { RecordingPlayer } from './RecordingPlayer'
import testRecording from '../data/test-recording.json'
import styles from './Tutorial.module.css'
const TUTORIAL_SEEN_KEY = 'tutorialSeen'
@ -20,6 +22,16 @@ export function Tutorial({ onDismiss }: { onDismiss: () => void }) {
<div className={styles.overlay}>
<div className={styles.card}>
<h2 className={`${styles.title} font-title`}>TIPS</h2>
<div className={styles.playerWrap}>
<RecordingPlayer
frames={testRecording}
width={280}
height={210}
autoPlay
loop
showControls={false}
/>
</div>
<ol className={styles.tips}>
<li>Bring your foot fully back to center after each step</li>
<li>Try to stay in one spot</li>