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!
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;
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;
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>
);
}
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)