# Optimise Canvas Drawing Of A Circle

## 25 May 2020 - 1 answer

I am new to HTML5 canvas and looking to make a few circles move in random directions for a fancy effect on my website.

I have noticed that when these circles move, the CPU usage is very high. When there is just a couple of circles moving it is often ok, but when there is around 5 or more it starts to be a problem.

Here is a screenshot of profiling this in Safari for a few seconds with 5 circles.

Here is the code I have so far for my Circle component:

``````export default function Circle({ color = null }) {
useEffect(() => {
if (!color) return

let requestId = null
let canvas = ref.current
let context = canvas.getContext("2d")

let ratio = getPixelRatio(context)
let canvasWidth = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2)
let canvasHeight = getComputedStyle(canvas).getPropertyValue("height").slice(0, -2)

canvas.width = canvasWidth * ratio
canvas.height = canvasHeight * ratio
canvas.style.width = "100%"
canvas.style.height = "100%"

let y = random(0, canvas.height)
let x = random(0, canvas.width)
const height = random(100, canvas.height * 0.6)

let directionX = random(0, 1) === 0 ? "left" : "right"
let directionY = random(0, 1) === 0 ? "up" : "down"

const speedX = 0.1
const speedY = 0.1

context.fillStyle = color

const render = () => {
//draw circle
context.clearRect(0, 0, canvas.width, canvas.height)
context.beginPath()
context.arc(x, y, height, 0, 2 * Math.PI)

//prevent circle from going outside of boundary
if (x < 0) directionX = "right"
if (x > canvas.width) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvas.height) directionY = "up"

//move circle
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY

//apply color
context.fill()

//animate
requestId = requestAnimationFrame(render)
}

render()

return () => {
cancelAnimationFrame(requestId)
}
}, [color])

let ref = useRef()
return <canvas ref={ref} />
}
``````

Is there a more performant way to draw and move circles using canvas?

When they do not move, the CPU usage starts off around ~3% then drops to less than 1%, and when I remove the circles from the DOM, the CPU usage is always less than 1%.

I understand it's often better to do these types of animations with CSS (as I believe it uses the GPU rather than the CPU), but I couldn't work out how to get it to work using the transition CSS property. I could only get the scale transformation to work.

My fancy effect only looks "cool" when there are many circles moving on the screen, hence looking for a more performant way to draw and move the circles.

Here is a sandbox for a demo: https://codesandbox.io/s/async-meadow-vx822 (view in chrome or safari for best results)

Here is a slightly different approach to combine circles and background to have only one canvas element to improve rendered dom.

This component uses the same colours and sizes with your randomization logic but stores all initial values in a `circles` array before rendering anything. `render` functions renders background colour and all circles together and calculates their move in each cycle.

``````export default function Circles() {
useEffect(() => {
const colorList = {
1: ["#247ba0", "#70c1b3", "#b2dbbf", "#f3ffbd", "#ff1654"],
2: ["#05668d", "#028090", "#00a896", "#02c39a", "#f0f3bd"]
};
const colors = colorList[random(1, Object.keys(colorList).length)];
const primary = colors[random(0, colors.length - 1)];
const circles = [];

let requestId = null;
let canvas = ref.current;
let context = canvas.getContext("2d");

let ratio = getPixelRatio(context);
let canvasWidth = getComputedStyle(canvas)
.getPropertyValue("width")
.slice(0, -2);
let canvasHeight = getComputedStyle(canvas)
.getPropertyValue("height")
.slice(0, -2);

canvas.width = canvasWidth * ratio;
canvas.height = canvasHeight * ratio;
canvas.style.width = "100%";
canvas.style.height = "100%";

[...colors, ...colors].forEach(color => {
let y = random(0, canvas.height);
let x = random(0, canvas.width);
const height = random(100, canvas.height * 0.6);

let directionX = random(0, 1) === 0 ? "left" : "right";
let directionY = random(0, 1) === 0 ? "up" : "down";

circles.push({
color: color,
y: y,
x: x,
height: height,
directionX: directionX,
directionY: directionY
});
});

const render = () => {
context.fillStyle = primary;
context.fillRect(0, 0, canvas.width, canvas.height);

circles.forEach(c => {
const speedX = 0.1;
const speedY = 0.1;

context.fillStyle = c.color;
context.beginPath();
context.arc(c.x, c.y, c.height, 0, 2 * Math.PI);
if (c.x < 0) c.directionX = "right";
if (c.x > canvas.width) c.directionX = "left";
if (c.y < 0) c.directionY = "down";
if (c.y > canvas.height) c.directionY = "up";
if (c.directionX === "left") c.x -= speedX;
else c.x += speedX;
if (c.directionY === "up") c.y -= speedY;
else c.y += speedY;
context.fill();
context.closePath();
});

requestId = requestAnimationFrame(render);
};

render();

return () => {
cancelAnimationFrame(requestId);
};
});

let ref = useRef();
return <canvas ref={ref} />;
}
``````

You can simply replace all bunch of circle elements and background style with this one component in your app component.

``````export default function App() {
return (
<>
<div className="absolute inset-0 overflow-hidden">
<Circles />
</div>
<div className="backdrop-filter-blur-90 absolute inset-0 bg-gray-900-opacity-20" />
</>
);
}
``````