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
parent
f032139b7c
commit
c98a9e71e0
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue