Logo

dev-resources.site

for different kinds of informations.

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

Published at
9/16/2024
Categories
nextjs
approuter
react
typescript
Author
nifty-little-me
Categories
4 categories in total
nextjs
open
approuter
open
react
open
typescript
open
Author
15 person written this
nifty-little-me
open
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!

approuter Article's
23 articles in total
Favicon
Show a loading screen when changing pages in Next.js App router
Favicon
Learning Next.js 13 App Router: A Comprehensive Guide 🚀
Favicon
Guide to build a modern Web App using Next.js 14 (App Router), Fully authentication (NextAuth), Theming and i18n
Favicon
A practical Guide - Migrating to Next.js App Router
Favicon
Adding Chat Functionality To Your Next.Js Project With Firebase
Favicon
Spicing Up Your Next.Js Projects With 3D: What Are Your Options?
Favicon
No More Pages
Favicon
How To Create A Basic Infinity Canvas For Your Next.Js Project
Favicon
How To Implement Text-To-Speech Functionality For BlockNote In Next.Js
Favicon
How To Add Drag-And-Drop Functionality With Editable Draggable Items In Next.js
Favicon
Adding Drag And Drop Functionality In Your Next.Js Project Without A Library
Favicon
Creating NPM Packages in Next.Js for Next.Js
Favicon
Using Firebase To Store Folders and BlockNote Documents In Next.Js
Favicon
An Alternative To Editor.js: BlockNote For React
Favicon
How To Add Editor.Js To Your Next.Js Code
Favicon
How To Use The Quill Rich Text Editor in Your Next.Js App Router Project
Favicon
Simple NextJS GraphQL client
Favicon
Implementing Internationalization (i18n) in Next.js 14 using App Router
Favicon
Web Streams API in Action: Delivering 6000+ Log Lines Concurrently Across 20 Tabs
Favicon
How to use React-Toastify with Next.js App router
Favicon
NextAuth - Implementando "Custom Login Pages" com "Credentials Sign in" utilizando App Router
Favicon
The origin of App Router - A Next.Js Rewind
Favicon
How to add multiple routers in a node application without using app.use() for each router ?

Featured ones: