Skip to main content

AnimationCanvas

A simple animatable canvas with requestAnimationFrame()

Source
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()
      }}
    />
  )
}

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 Reference​

AnimationCanvas renders a <canvas> and takes its attributes, such as className and style. A ref reaches the <canvas>.

In Svelte and Vue the props are the same, written their way — class rather than className, bind:value or v-model, snippets or slots for content. See Svelte and Vue.

PropTypeDefault
draw*CanvasDrawFunction—
initCanvasInitFunction—
animatebooleantrue
optionsCanvasRenderingContext2DSettings—
reduceFlickeringbooleantrue

Full reference: AnimationCanvasCommonProps

Fixed vs Resizable​

A canvas is fixed by default: width and height set its size in CSS pixels. With resizable it follows the size of its parent element instead, so the parent needs a size of its own. The two are exclusive, and passing width or height together with resizable is a type error.

Fixed​

PropTypeDefault
resizablefalse—
widthnumber100
heightnumber100

Full reference: AnimationCanvasFixedProps

Resizable​

PropTypeDefault
resizable*true—
widthundefined—
heightundefined—

Full reference: AnimationCanvasResizableProps

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​

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
        className={sliderTheme.root}
        value={hue}
        min={0}
        max={360}
        onChange={(v) => setHue(v)}
      >
        <Slider.Track className={sliderTheme.track}>
          <Slider.Thumb className={sliderTheme.thumb} aria-label="Hue" />
        </Slider.Track>
      </Slider.Root>
    </div>
  )
}

Save / Restore​

Loading...