AnimationCanvas
A simple animatable canvas with requestAnimationFrame()
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.
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
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.
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> ) }