DEV Community

Yar
Yar

Posted on • Edited on

Animation with Canvas and requestAnimationFrame() in React

Hi! So I spent a couple of days trying to figure out how to use <canvas> in React.

Canvas lets you output graphics on the screen
You may use it for games like Pong or data visualization

As a result I put together a very basic app to use it as a reference in case I need it in a project. Let me go through the code briefly!

What you see on the screen

Basically the app is supposed to do three things

  1. Output graphics on the screen
  2. Define the movement of the box
  3. Let you pause / resume the animation with a button

canvas_screenshot_1 grey

What you see in the code

Here is the source code. And the working app.

Essentially the application has three parts.

const CanvasPractice = () => {

    // get canvas
    const canvasRef = useRef()

    // set frame counter
    const [counter, setCounter] = useState(0)
    const [shouldStop, setShouldStop] = useState(true)

    // box position and speed
    const [positionX, setPositionX] = useState(165)
    const [positionY, setPositionY] = useState(165)

    const [dx, setDx] = useState(2)
    const [dy, setDy] = useState(1.5)

    const [motionType, setMotionType] = useState('Circle')

    // update the counter
    useLayoutEffect(() => {
        if (!shouldStop) {
            let timerId

            const animate = () => {
                setCounter(c => c + 1)
                timerId = requestAnimationFrame(animate)
            }
            timerId = requestAnimationFrame(animate)
            return () => cancelAnimationFrame(timerId)
        }
    }, [shouldStop])

    // output graphics
    useEffect(() => {

        const canvas = canvasRef.current
        const context = canvas.getContext('2d')

        context.clearRect(0, 0, 350, 350)

        // some code to calculate position 

        context.fillStyle = '#555555'
        context.fillRect(positionX, positionY, 20, 20)

    }, [counter])

    const changeMotionType = () => {
        if (motionType === 'Circle') {
            setMotionType('Bounce')
        } else {
            setMotionType('Circle')
        }
    }

    return (
        <div className='container'>
            <canvas ref={canvasRef} 
                width="350px" height="350px" 
                onClick={changeMotionType} 
            />
            <h3>Frame count: {counter}</h3>
            <p>Motion type is {motionType}</p>
            <button 
                onClick={() => setShouldStop(!shouldStop)}>
                { shouldStop ? 'Start' : 'Stop' }
            </button>
        </div>
    )
}

export default CanvasPractice
Enter fullscreen mode Exit fullscreen mode

1. The Engine

useLayoutEffect() section serves as an engine. requestAnimationFrame() function refreshes itself roughly 60 times a second and increased the counter value. It's the main pulse of the app.

You feed the counter as a dependency to the useEffect() section causing it to refresh and update the graphics on the screen.

Further reading

Here is a great article explaining how requestAnimationFrame() function works

Using requestAnimationFrame with React Hooks

And here you may find superuseful notes on why you might prefer useLayoutEffect() over useEffect() for this application

requestAnimationFrame and useEffect vs useLayoutEffect

2. Output

In the useEffect() section you initialize the canvas.

Calculate box position for the current frame, updating positionX and positionY state.

And output it using the context.fillRect(positionX, positionY, 20, 20) method.

3. In control

The button lets you pause / resume the animation.

Then there are two modes in which the box moves. It runs in circles or bounces off the sides of the board, like it would do in a video game. You can switch the mode by clicking anywhere on the board.

So

Did you find this exploration interesting?
If you have any suggestions, let me know!

Update

And here is the actual game built on the same foundation
poung.ptifur.digital

Top comments (0)