DEV Community

It's Just Nifty
It's Just Nifty

Posted on • Originally published at niftylittleme.com on

How To Create A Basic Infinity Canvas For Your Next.Js Project

Note: In this tutorial, I will use the app router. If you are using typescript, change some of the code to work properly.

Turns out Johnny was right: if I want something done I need to do it myself. So, I got my hands dirty and put more skeletons in my closet. Thanks to that, Johnny is no longer with us. I’m joking, obviously. Instead of doing whatever you think I did, I got started creating a solution for infinity canvases. And if you don’t know who Johnny is, I’ll get to that after this introduction. Skip to the Previously On The Nifty Little Me Blog section to find out if you’re that curious.

In this article, we will create a basic infinity canvas with no libraries or tools because there are still none out there. Actually, there is one, but let’s not talk about it again. Turns out, creating an infinity canvas wasn’t that hard if you know what you’re doing. I have succeeded in creating one with the basics. So, why not share what I have so far? Let’s move away from the introduction and start creating!

Unsplash Image by Christopher Gower

Previously On The Nifty Little Me Blog…

An infinite canvas is a canvas that never ends.

Basically, you know that article about how there are zero libraries or tools to help you easily set up an infinite canvas in your React projects? Yes, that's the one! What’s unfortunate is that there are a lot of tools out there that have infinite canvas functionality, which means that even though there is a solution, it’s not shared.

Johnny, the guy who said that one thing in the previous article, suggested that I should create a solution instead of ranting about how there isn’t one.

There is one solution that has documentation available for adding similar functionality to your React projects. Read about that in my last article.

Creating A Basic Toolbar

Let’s skip the getting started section because you should already know how to create a Next.js project. Plus, there is nothing to install inside your project.

Create a components folder in your src/app/ directory. Inside the components folder, create a file named ‘toolbar.jsx’. Inside it would be the buttons of all the tools you want to add:

'use client';
import React from 'react';

const Toolbar = ({ addItem }) => {
    return (
        <div className="relative top-4 left-10 bg-white p-4 rounded shadow z-10">
            <div className="gap-4 flex flex-row">
                <button onClick={() => addItem('rectangle')}>Add Rectangle</button>
                <button onClick={() => addItem('circle')}>Add Circle</button>
            </div>
        </div>
    );
};

export default Toolbar;
Enter fullscreen mode Exit fullscreen mode

Creating A Canvas

In your components folder, create a new file called canvas.jsx. In this file, we are going to do a couple of things:

  • Import toolbar

  • Add panning

  • Add zooming

  • Create grid

  • Show zoom percentage

We can accomplish all of that with this code:

'use client';
import { useRef, useEffect, useState } from 'react';
import Toolbar from './toolbar';

const InfiniteCanvas = () => {
    const canvasRef = useRef(null);
    const [scale, setScale] = useState(1);
    const [translateX, setTranslateX] = useState(0);
    const [translateY, setTranslateY] = useState(0);
    const [isPanning, setIsPanning] = useState(false);
    const [startX, setStartX] = useState(0);
    const [startY, setStartY] = useState(0);
    const [items, setItems] = useState([]);

    useEffect(() => {
        const canvas = canvasRef.current;
        const ctx = canvas.getContext('2d');

        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        const draw = () => {
            ctx.save();
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.translate(translateX, translateY);
            ctx.scale(scale, scale);

            // Example drawing: An infinite grid
            const gridSize = 50;
            const width = canvas.width;
            const height = canvas.height;

            // Calculate the top-left corner of the grid to start drawing
            const startX = Math.floor(-translateX / scale / gridSize) * gridSize;
            const startY = Math.floor(-translateY / scale / gridSize) * gridSize;

            for (let x = startX; x < width / scale - translateX / scale; x += gridSize) {
                for (let y = startY; y < height / scale - translateY / scale; y += gridSize) {
                    ctx.strokeRect(x, y, gridSize, gridSize);
                }
            }

            // Draw added items
            items.forEach(item => {
                if (item.type === 'rectangle') {
                    ctx.fillStyle = 'blue';
                    ctx.fillRect(item.x, item.y, 100, 50);
                } else if (item.type === 'circle') {
                    ctx.fillStyle = 'red';
                    ctx.beginPath();
                    ctx.arc(item.x, item.y, 50, 0, 2 * Math.PI);
                    ctx.fill();
                }
            });

            ctx.restore();
        };

        draw();

        const handleResize = () => {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            draw();
        };

        window.addEventListener('resize', handleResize);

        return () => {
            window.removeEventListener('resize', handleResize);
        };
    }, [scale, translateX, translateY, items]);

    const handleWheel = (event) => {
        event.preventDefault();
        const zoomIntensity = 0.1;
        const mouseX = event.clientX - canvasRef.current.offsetLeft;
        const mouseY = event.clientY - canvasRef.current.offsetTop;
        const zoomFactor = 1 + event.deltaY * -zoomIntensity;

        const newScale = Math.min(Math.max(0.5, scale * zoomFactor), 5); // Limit zoom scale

        setTranslateX(translateX - mouseX / scale * (newScale - scale));
        setTranslateY(translateY - mouseY / scale * (newScale - scale));
        setScale(newScale);
    };

    const handleMouseDown = (event) => {
        setIsPanning(true);
        setStartX(event.clientX - translateX);
        setStartY(event.clientY - translateY);
    };

    const handleMouseMove = (event) => {
        if (!isPanning) return;
        setTranslateX(event.clientX - startX);
        setTranslateY(event.clientY - startY);
    };

    const handleMouseUp = () => {
        setIsPanning(false);
    };

    const handleMouseLeave = () => {
        setIsPanning(false);
    };

    const addItem = (type) => {
        const newItem = {
            type,
            x: (canvasRef.current.width / 2 - translateX) / scale,
            y: (canvasRef.current.height / 2 - translateY) / scale,
        };
        setItems([...items, newItem]);
    };

    return (
        <div>
            <Toolbar addItem={addItem} />
            <div style={{ position: 'relative', width: '100%', height: '100%' }}>
                <canvas
                    ref={canvasRef}
                    onWheel={handleWheel}
                    onMouseDown={handleMouseDown}
                    onMouseMove={handleMouseMove}
                    onMouseUp={handleMouseUp}
                    onMouseLeave={handleMouseLeave}
                    style={{ width: '100%', height: '100%', display: 'block' }}
                />
                <div className="text-center fixed bottom-0 left-4 right-4 z-10 bg-gray-100 p-2 rounded shadow">
                    Zoom: {(scale * 100).toFixed(0)}%
                </div>
            </div>
        </div>
    );
};

export default InfiniteCanvas;
Enter fullscreen mode Exit fullscreen mode

Displaying The Infinite Canvas

Now, let’s display everything by adding a few lines to our page.tsx file code:

import InfiniteCanvas from './components/canvas';

export default function Home() {
    return (
        <div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}>
            <InfiniteCanvas />
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

That wraps up this article on creating a simple infinity canvas in Next.js. Of course, there are more things you would want to add—more things I want to add, but that’s the thing about basic; it only does the bare minimum.

If you liked this article, follow me on Medium and subscribe to my newsletter—that way you never miss me or any new articles.

Happy Coding Folks!

Top comments (0)