useAnimationFrame
コンポーネントがマウントされている間、アニメーションフレームごとに関数を呼びます。
Sourcefunction App() { const [running, setRunning] = useState(true) const [elapsed, setElapsed] = useState(0) const [fps, setFps] = useState(0) useAnimationFrame( (_timestamp, deltaTime) => { // `deltaTime` is 0 on the first frame after a start, so a pause is not // counted as one long frame. setElapsed((elapsed) => elapsed + deltaTime) if (deltaTime > 0) setFps(Math.round(1000 / deltaTime)) }, { disabled: !running }, ) return ( <div style={{ userSelect: 'none' }}> <div style={{ height: 8, marginBottom: '0.5rem', borderRadius: 9999, background: 'rgba(127, 127, 127, 0.3)', overflow: 'hidden', }} > <div style={{ width: `${((elapsed % 2000) / 2000) * 100}%`, height: '100%', background: 'currentColor', }} /> </div> <div>elapsed: {(elapsed / 1000).toFixed(2)} s</div> <div>fps: {running ? fps : '-'}</div> <button type="button" onClick={() => setRunning((r) => !r)}> {running ? 'Pause' : 'Resume'} </button> </div> ) }
インポート
import { useAnimationFrame } from '@tremolo-ui/react'
コールバック
フレームのタイムスタンプと、前のフレームからの経過ミリ秒を受け取ります。
useAnimationFrame((timestamp, deltaTime) => {
position.current += speed * deltaTime
})
ループの最初のフレームでは deltaTime は 0 です。1 フレームごとに決まった量ではなく deltaTime に比例して動かせば、60 Hz と 120 Hz のディスプレイで速さが変わりません。
コールバックはフレームのたびに読み直されるので、インラインで書けて、最新の props と state が見えます。再レンダーしてもループは張り直されません。
一時停止
disabled でループを止めます。
useAnimationFrame(update, { disabled: !playing })
戻すと新しいループが始まり、その最初のフレームの deltaTime はまた 0 になります。止めていた時間が 1 つの長いフレームとして報告されないので、再開したときに先へ飛びません。
canvas に描く
<canvas> に描くなら、代わりに AnimationCanvas を使ってください。同じループを回したうえで、canvas の大きさを要素とデバイスピクセル比に合わせ続けます。ページの他の部分のために値を読むのも draw の中でできるので、ループを 2 本にせず 1 本で済みます。
Svelte と Vue
この hook は React だけのものです。どちらのフレームワークにも同じことをする手段が既にあるので、@tremolo-ui/svelte と @tremolo-ui/vue には置いていません。
- Svelte
- Vue
$effect の中でループを回し、cleanup で止めます。
$effect(() => {
if (paused) return
let previous: number | undefined
let frame = requestAnimationFrame(function loop(timestamp) {
frame = requestAnimationFrame(loop)
update(timestamp, previous === undefined ? 0 : timestamp - previous)
previous = timestamp
})
return () => cancelAnimationFrame(frame)
})
ループを張り直すのは、effect が最初のフレームより前に読んだもの(ここでは paused)だけです。update が読むものはフレームのコールバックの中で読まれ、effect は追跡しないので、state を自由に読めます。
VueUse に useRafFn があります。コールバックは { delta, timestamp } を受け取り、戻り値の pause() / resume() が disabled の代わりになります。
import { useRafFn } from '@vueuse/core'
const { pause, resume } = useRafFn(({ delta, timestamp }) =>
update(timestamp, delta),
)
オプション
コールバックは第 1 引数です。以下は第 2 引数のものです。
| Prop | 型 | 既定値 |
|---|---|---|
disabled | boolean | false |
すべての説明: UseAnimationFrameOptions