Skip to main content

AnimationCanvas

A simple animatable canvas with requestAnimationFrame()

Source

Import

import { AnimationCanvas } from '@tremolo-ui/react'

Features

  • Adapt devicePixelRatio.
  • Reduce flicking when resized.
  • Observe the parent element's dimensions.
  • Simple ticker and timer

API

ref: AnimationCanvasProps

Absolute Sizing vs Relative Sizing

Reactive Canvas

By default the canvas redraws on every animation frame. When the drawing is driven by state rather than by time, set animate={false}: the canvas then draws only when it has a reason to — when it mounts, when it is resized, and when the component re-renders.

That last one is what makes React.useState work as the source of the drawing. Pass the new draw and the canvas repaints; there is nothing to memoize and no redraw to trigger by hand.

See Reactive below for a working example.

tip

Writing draw inline is fine either way. The canvas is set up once and the handler is swapped in place, so re-rendering never restarts the animation, resets count and elapsedTime, or runs init again. Wrapping the component in React.memo, or holding draw in a useCallback, is not needed.

Examples

Basic

Loading...
function App() {
  return (
    <AnimationCanvas
      width={200}
      height={200}
      init={(ctx) => {
        ctx.font = '16px sans-serif'
      }}
      draw={(ctx, { width, height, count }) => {
        ctx.clearRect(0, 0, width, height)
        ctx.fillText(`frame: ${count}`, 0, 16)
        // draw sine wave
        const halfH = height / 2
        ctx.strokeStyle = '#29bbf0'
        ctx.beginPath()
        for (let i = 0; i < width; i++) {
          const y =
            halfH +
            halfH * 0.5 * Math.sin((4 * Math.PI * (i + count * 2)) / width)
          if (i == 0) ctx.moveTo(i, y)
          else ctx.lineTo(i, y)
        }
        ctx.stroke()
      }}
    />
  )
}

Reactive

A canvas driven by state instead of by time, with animate={false}. Moving the slider re-renders the component, and that is what repaints the canvas.

Loading...
function App() {
  const [hue, setHue] = useState(200)

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        gap: 8,
      }}
    >
      <AnimationCanvas
        animate={false}
        width={240}
        height={80}
        draw={(ctx, { width, height }) => {
          ctx.clearRect(0, 0, width, height)
          for (let x = 0; x < width; x += 12) {
            ctx.fillStyle = `hsl(${(hue + x) % 360} 90% 60%)`
            ctx.fillRect(x, 0, 10, height)
          }
        }}
      />
      <Slider.Root value={hue} min={0} max={360} onChange={(v) => setHue(v)}>
        <Slider.Track>
          <Slider.Thumb />
        </Slider.Track>
      </Slider.Root>
    </div>
  )
}

Save / Restore

Loading...