メインコンテンツまでスキップ

useAnimationFrame

コンポーネントがマウントされている間、アニメーションフレームごとに関数を呼びます。

Source
Loading...
function 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 には置いていません。

$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 を自由に読めます。

オプション​

コールバックは第 1 引数です。以下は第 2 引数のものです。

Prop型既定値
disabledbooleanfalse

すべての説明: UseAnimationFrameOptions