I am making a snake game with plain javascript. But now I have come to the food part of the game. but I just cant get it to work properly. I have an food function that generates random cords within the gamefield and then draws the food. This is already quite wonky at the first place. but then I want to detect of the snake cords and foodcords match up with a margin of 20px. but I just cant get it to word smoothly together.
Can anyone maybe help me figure out what is going wrong and how I can fix this? Thanks!
let canvas = document.getElementById("canvas");
let movespeedX = 2;
let movespeedY = 0;
var canvasContext = canvas.getContext('2d');
//newest cord
let locationX = 20;
let locationY = 20;
//cords up to snakelength
let cords = [
{X: 5, Y: 5}
];
let snakeLength = 5;
//food location
let foodX;
let foodY;
let isfood = false;
//onload draw, move and set food
window.onload = function() {
setInterval(callField => {draw(); move(); food()}, 1000/60);
//keyboard controls
document.addEventListener('keydown', event => {
const key = event.key.toLowerCase();
if(key == "w" || key == "arrowup")
{
movespeedX = 0;
movespeedY = -2;
}
if(key == "s" || key == "arrowdown")
{
movespeedX = 0;
movespeedY = 2;
}
if(key == "a" || key == "arrowleft")
{
movespeedY = 0;
movespeedX = -2;
}
if(key == "d" || key == "arrowright")
{
movespeedY = 0;
movespeedX = 2;
}
});
}
function move()
{
//add movespeed to location to move all directions
locationX += movespeedX;
locationY += movespeedY;
//if a wall is hit restart
if(cords[0].X >= canvas.width || cords[0].X <= 0 || cords[0].Y >= canvas.height || cords[0].Y < 0)
{
restart();
}
//if food is hit with 20px margin (this is currently verry trippy and does not work)
if(foodY+20 > cords[0].X && foodY+20 > cords[0].Y)
{
isfood = false;
snakeLength += 5;
}
//update cords array with newest location
cords.unshift({X: locationX, Y: locationY});
if(cords.length > snakeLength)
{
delete cords[snakeLength];
}
}
function draw()
{
//draw canvas
drawRect(0,0,canvas.width,canvas.height,"black");
//draw food
drawCircle(foodX, foodY, 20, "red");
//draw snake
cords.forEach(element => {
drawCircle(element.X+20,element.Y,20,"white");
});
}
//reset to standard values
function restart()
{
locationX = 20;
locationY = 20;
movespeedX = 0;
movespeedY = 0;
snakeLength = 1;
cords = [
{X: 0, Y: 0}
];
}
//if the is no food, generate new cords and set food to true
function food()
{
if(isfood === false)
{
foodX = Math.floor(Math.random() * canvas.width) + 50;
foodY = Math.floor(Math.random() * canvas.height) + 50;
isfood = true;
}
}
function drawRect(leftX, topY, width, height, color)
{
canvasContext.fillStyle = color;
canvasContext.fillRect(leftX, topY, width, height);
}
function drawCircle(leftX,topY,radius,color)
{
canvasContext.fillStyle = color;
canvasContext.beginPath();
canvasContext.arc(leftX,topY,radius,0,Math.PI*2,true);
canvasContext.fill()
}```
Use Pythagorean theorem to find the distance of two objects then for circles account the radius plus any additional buffer you may want.
So something like
if (distance < objects.radius + other.objects.radius) {
return true
}
Here's your code
let canvas = document.getElementById("canvas");
let movespeedX = 2;
let movespeedY = 0;
var canvasContext = canvas.getContext("2d");
canvas.width = innerWidth;
canvas.height = innerHeight;
//newest cord
let locationX = 20;
let locationY = 20;
//cords up to snakelength
let cords = [{ X: 5, Y: 5 }];
let snakeLength = 5;
//food location
let foodX;
let foodY;
let isfood = false;
//onload draw, move and set food
window.onload = function () {
setInterval((callField) => {
draw();
move();
food();
}, 1000 / 60);
//keyboard controls
document.addEventListener("keydown", (event) => {
const key = event.key.toLowerCase();
if (key == "w" || key == "arrowup") {
movespeedX = 0;
movespeedY = -2;
}
if (key == "s" || key == "arrowdown") {
movespeedX = 0;
movespeedY = 2;
}
if (key == "a" || key == "arrowleft") {
movespeedY = 0;
movespeedX = -2;
}
if (key == "d" || key == "arrowright") {
movespeedY = 0;
movespeedX = 2;
}
});
};
function move() {
//add movespeed to location to move all directions
locationX += movespeedX;
locationY += movespeedY;
//if a wall is hit restart
if (
cords[0].X >= canvas.width ||
cords[0].X <= 0 ||
cords[0].Y >= canvas.height ||
cords[0].Y < 0
) {
restart();
}
//if food is hit with 20px margin (this is currently verry trippy and does not work)
let dx = foodX - cords[0].X;
let dy = foodY - cords[0].Y;
let dist = Math.hypot(dx, dy);
if (dist < 60) {
isfood = false;
snakeLength += 5;
}
//update cords array with newest location
cords.unshift({ X: locationX, Y: locationY });
if (cords.length > snakeLength) {
delete cords[snakeLength];
}
}
function draw() {
//draw canvas
drawRect(0, 0, canvas.width, canvas.height, "black");
//draw food
drawCircle(foodX, foodY, 20, "red");
//draw snake
cords.forEach((element) => {
drawCircle(element.X + 20, element.Y, 20, "white");
});
}
//reset to standard values
function restart() {
locationX = 20;
locationY = 20;
movespeedX = 0;
movespeedY = 0;
snakeLength = 1;
cords = [{ X: 0, Y: 0 }];
}
//if the is no food, generate new cords and set food to true
function food() {
if (isfood === false) {
foodX = Math.floor(Math.random() * canvas.width) + 50;
foodY = Math.floor(Math.random() * canvas.height) + 50;
isfood = true;
}
}
function drawRect(leftX, topY, width, height, color) {
canvasContext.fillStyle = color;
canvasContext.fillRect(leftX, topY, width, height);
}
function drawCircle(leftX, topY, radius, color) {
canvasContext.fillStyle = color;
canvasContext.beginPath();
canvasContext.arc(leftX, topY, radius, 0, Math.PI * 2, true);
canvasContext.fill();
}
<canvas id="canvas"></canvas>
I'm creating a game, and I need to draw) some elements at the top of the <canvas>, but the more elements appear, the more lag the page itself. I found this example where a lot of circles appear, but everything works fine - JSFiddle. Can someone tell me how to optimize my case?
"use strict";
/*Determing canvas*/
window.onload = () => {
const canvas = document.getElementById("canvas"),
ctx = canvas.getContext('2d'),
endTitle = document.getElementById('gameover');
let spawnRate = 300,
lastspawn = -1;
class Wall {
/*Getting values*/
constructor(x, y, width, height, speed) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speed = speed;
}
/*Draw rectangle*/
draw() {
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.fillRect(this.x, this.y, this.width, this.height)
}
}
/*Making walls*/
let walls = [];
/*Spawn walls endlessly*/
function spawnWalls() {
const wall_x = Math.floor(Math.random() * (canvas.width - 20)) + 10
const wall_y = 0
for (let i = 0; i < 200; i++) {
walls.push(new Wall(wall_x, wall_y, 10, 10, 10))
}
}
/*Update game*/
function refresh() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let time = Date.now()
if (time > (lastspawn + spawnRate)) {
lastspawn = time;
spawnWalls();
spawnRate -= 10;
}
walls.forEach(wall => wall.draw())
for (let j of walls) {
j.y += j.speed;
j.draw();
};
};
let interval = setInterval(refresh, 50);
Walls is too big.
It looks like you're never removing old walls, so they are continuing to be drawn well after they have been removed from the canvas. In your Refresh function, check if the wall has passed the canvas size and if so, remove that from the walls array.
EDIT:
I've added your 'remove' code from the comment.
Another easy win is to stop using new Date() because of who knows what, probably time zones and localization, dates are very expensive to instantiate. However modern browsers offer a performance API, and that can tell you the time since page load, which seems to have a substantial improvement on your existing performance.
"use strict";
/*Determing canvas*/
window.onload = () => {
const canvas = document.getElementById("canvas"),
ctx = canvas.getContext('2d'),
endTitle = document.getElementById('gameover');
let spawnRate = 300,
lastspawn = -1;
endTitle.style.display = "none";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
/*Classes*/
class Player {
/*Get player info*/
constructor(x, y, width, height, speed) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speed = speed;
}
/*Draw player*/
draw() {
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.fillRect(this.x, this.y, this.width, this.height);
}
/*Move player*/
move() {
}
};
class Wall {
/*Getting values*/
constructor(x, y, width, height, speed) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.speed = speed;
}
/*Draw rectangle*/
draw() {
ctx.beginPath();
ctx.fillStyle = "#000000";
ctx.fillRect(this.x, this.y, this.width, this.height)
}
}
/*Defining players*/
let player_01 = new Player(20, 70, 20, 20, 10);
let player_02 = new Player(50, 500, 20, 20, 10);
let players = [];
players.push(player_01);
/*Making walls*/
let walls = [];
/*Spawn Walls for infinity*/
function spawnWalls() {
const wall_x = Math.floor(Math.random() * (canvas.width - 20)) + 10
const wall_y = 0
for (let i = 0; i < 200; i++) {
walls.push(new Wall(wall_x, wall_y, 10, 10, 10))
}
}
/*Update game*/
function refresh() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let time = performance.now()
if (time > (lastspawn + spawnRate)) {
lastspawn = time;
spawnWalls();
spawnRate -= 10;
}
walls.forEach(wall => wall.draw())
outOfWindow()
for (let i of players) {
i.draw();
};
for (let j of walls) {
if (j.y > canvas.height) {
walls.shift(j)
}
j.y += j.speed;
j.draw();
if (player_01.height + player_01.y > j.y && j.height + j.y > player_01.y && player_01.width + player_01.x > j.x && j.width + j.x > player_01.x) {
clearInterval(interval);
endTitle.style.display = "flex";
};
};
};
let interval = setInterval(refresh, 50);
/*Move players on keypress*/
for (let i of players) {
window.addEventListener("keydown", (event) => {
let key = event.key.toLowerCase();
if (key == "w") i.y -= i.speed;
else if (key == "s") i.y += i.speed;
else if (key == "a") i.x -= i.speed;
else if (key == "d") i.x += i.speed;
})
}
/*Check if player out of the window*/
function outOfWindow() {
for (let i of players) {
if (i.x < 0) i.x = 0;
else if (i.x + i.width > canvas.width) i.x = canvas.width - i.width;
else if (i.y < 0) i.y = 0;
else if (i.y + i.height > canvas.height) i.y = canvas.height - i.height;
}
}
}
#gameover {
position: absolute;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: rgba(0, 0, 0, .5);
}
<div id="gameover">
<h2>The Game Is Over</h2>
<button onclick="restart()">Try again!</button>
</div>
<canvas id="canvas"></canvas>
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" />
</>
);
}
I tried to assemble your code as possible, it seems you have buffer overflow (blue js heap), you need to investigate here, these are the root cause.
The initial approach is to create circle just once, then animate the child from parent, by this way you avoid intensive memory and CPU computing.
Add how many circles by clicking on the canvas, canvas credit goes to Martin
Update
Following for alexander discussion it is possible to use setTimeout, or Timeinterval (Solution 2)
Soltion #1
App.js
import React from 'react';
import { useCircle } from './useCircle';
import './App.css';
const useAnimationFrame = callback => {
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const animate = time => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime)
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []); // Make sure the effect runs only once
}
function App() {
const [count, setCount] = React.useState(0)
const [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight, counts] = useCircle();
const speedX = 1 // tunne performance by changing this
const speedY = 1 // tunne performance by changing this
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const handleCanvasClick = (event) => {
// on each click get current mouse location
const currentCoord = { x: event.clientX, y: event.clientY ,directionX:"right",directionY:"down"};
// add the newest mouse location to an array in state
setCoordinates([...coordinates, currentCoord]);
// query.push(currentCoord)
//query.push(currentCoord)
};
const move = () => {
let q = [...coordinates]
q.map(coordinate => { return { x: coordinate.x + 10, y: coordinate.y + 10 } })
setCoordinates(q)
}
const handleClearCanvas = (event) => {
setCoordinates([]);
};
const animate = time => {
//if (time % 2===0){
setCount(time)
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
setCoordinates(coordinates => coordinates.map((coordinate)=> {
let x=coordinate.x;
let y=coordinate.y;
let directionX=coordinate.directionX
let directionY=coordinate.directionY
if (x < 0) directionX = "right"
if (x > canvasWidth) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvasHeight) directionY = "up"
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
return { x:x,y:y,directionX:directionX,directionY:directionX}
}))
// }
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []); // Make sure the effect runs only once
return (
<main className="App-main" >
<div>{Math.round(count)}</div>
<canvas
className="App-canvas"
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
onClick={handleCanvasClick}
/>
<div className="button" >
<button onClick={handleClearCanvas} > CLEAR </button>
</div>
</main>
);
};
export default App;
userCircle.js
import React, { useState, useEffect, useRef } from 'react';
var circle = new Path2D();
circle.arc(100, 100, 50, 0, 2 * Math.PI);
const SCALE = 1;
const OFFSET = 80;
export const canvasWidth = window.innerWidth * .5;
export const canvasHeight = window.innerHeight * .5;
export const counts=0;
export function draw(ctx, location) {
console.log("attempting to draw")
ctx.fillStyle = 'red';
ctx.shadowColor = 'blue';
ctx.shadowBlur = 15;
ctx.save();
ctx.scale(SCALE, SCALE);
ctx.translate(location.x / SCALE - OFFSET, location.y / SCALE - OFFSET);
ctx.rotate(225 * Math.PI / 180);
ctx.fill(circle);
ctx.restore();
};
export function useCircle() {
const canvasRef = useRef(null);
const [coordinates, setCoordinates] = useState([]);
useEffect(() => {
const canvasObj = canvasRef.current;
const ctx = canvasObj.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
coordinates.forEach((coordinate) => {
draw(ctx, coordinate)
}
);
});
return [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight,counts];
}
Soltion #2 Using Interval
IntervalExample.js (app) 9 sample circle
import React, { useState, useEffect } from 'react';
import Circlo from './Circlo'
const IntervalExample = () => {
const [seconds, setSeconds] = useState(0);
const [circules, setCircules] = useState([]);
let arr =[
{x:19,y:15, r:3,directionX:'left',directionY:'down'},
{x:30,y:10,r:4,directionX:'left',directionY:'down'},
{x:35,y:20,r:5,directionX:'left',directionY:'down'},
{x:0,y:15, r:3,directionX:'left',directionY:'down'},
{x:10,y:30,r:4,directionX:'left',directionY:'down'},
{x:20,y:50,r:5,directionX:'left',directionY:'down'},
{x:70,y:70, r:3,directionX:'left',directionY:'down'},
{x:80,y:80,r:4,directionX:'left',directionY:'down'},
{x:10,y:20,r:5,directionX:'left',directionY:'down'},
]
const reno =(arr)=>{
const table = Array.isArray(arr) && arr.map(item => <Circlo x={item.x} y={item.y} r={item.r} />);
return(table)
}
const speedX = 0.1 // tunne performance by changing this
const speedY = o.1 // tunne performance by changing this
const move = (canvasHeight,canvasWidth) => {
let xarr= arr.map(((coordinate)=> {
let x=coordinate.x;
let y=coordinate.y;
let directionX=coordinate.directionX
let directionY=coordinate.directionY
let r=coordinate.r
if (x < 0) directionX = "right"
if (x > canvasWidth) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvasHeight) directionY = "up"
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
return { x:x,y:y,directionX:directionX,directionY:directionY,r:r}
}))
return xarr;
}
useEffect(() => {
const interval = setInterval(() => {
arr =move(100,100)
setCircules( arr)
setSeconds(seconds => seconds + 1);
}, 10);
return () => clearInterval(interval);
}, []);
return (
<div className="App">
<p>
{seconds} seconds have elapsed since mounting.
</p>
<svg viewBox="0 0 100 100">
{ reno(circules)}
</svg>
</div>
);
};
export default IntervalExample;
Circlo.js
import React from 'react';
export default function Circlo(props) {
return (
<circle cx={props.x} cy={props.y} r={props.r} fill="red" />
)
}
First of all, nice effect!
Once said that, I read carefully your code and it seems fine. I'm afraid that the high CPU load is unavoidable with many canvas and transparencies...
To optimize your effect you could try two ways:
try to use only one canvas
try use only CSS, at the end you are using canvas only to draw a filled circle with color from a fixed set: you could use images with pre-drawn same circles and use more or less the same code to simply chage style properties of the images
Probably with a shader you'll be able to obtain the same effect with high CPU save, but unfortunately I'm not proficient on shaders so I can't give you any relevant hint.
Hope I given you some ideas.
Cool effect! I was really surprised that solution proposed by #Sam Erkiner did not perform that much better for me than your original. I would have expected single canvas to be way more efficient.
I decided to try this out with new animation API and pure DOM elements and see how well that works.
Here is my solution(Only changed Circle.js file):
import React, { useEffect, useRef, useMemo } from "react";
import { random } from "lodash";
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
export default function Circle({ color = null }) {
let ref = useRef();
useEffect(() => {
let y = random(0, HEIGHT);
let x = random(0, WIDTH);
let directionX = random(0, 1) === 0 ? "left" : "right";
let directionY = random(0, 1) === 0 ? "up" : "down";
const speed = 0.5;
const render = () => {
if (x <= 0) directionX = "right";
if (x >= WIDTH) directionX = "left";
if (y <= 0) directionY = "down";
if (y >= HEIGHT) directionY = "up";
let targetX = directionX === 'right' ? WIDTH : 0;
let targetY = directionY === 'down' ? HEIGHT : 0;
const minSideDistance = Math.min(Math.abs(targetX - x), Math.abs(targetY - y));
const duration = minSideDistance / speed;
targetX = directionX === 'left' ? x - minSideDistance : x + minSideDistance;
targetY = directionY === 'up' ? y - minSideDistance : y + minSideDistance;
ref.current.animate([
{ transform: `translate(${x}px, ${y}px)` },
{ transform: `translate(${targetX}px, ${targetY}px)` }
], {
duration: duration,
});
setTimeout(() => {
x = targetX;
y = targetY;
ref.current.style.transform = `translate(${targetX}px, ${targetY}px)`;
}, duration - 10);
setTimeout(() => {
render();
}, duration);
};
render();
}, [color]);
const diameter = useMemo(() => random(0, 0.6 * Math.min(WIDTH, HEIGHT)), []);
return <div style={{
background: color,
position: 'absolute',
width: `${diameter}px`,
height: `${diameter}px`,
top: 0,
left: 0
}} ref={ref} />;
}
Here are performance stats from Safari on my 6 year old Macbook:
Maybe with some additional tweaks could be pushed into green zone?
Your original solution was at the start of red zone, single canvas solution was at the end of yellow zone on Energy impact chart.
I highly recommend reading the article Optimizing the Canvas on the Mozilla Developer's Network website. Specifically, without getting into actual coding, it is not advisable to perform expensive rendering operations repeatedly in the canvas. Alternatively, you can create a virtual canvas inside your circle class and perform the drawing on there when you initially create the circle, then scale your Circle canvas and blit it the main canvas, or blit it and then scale it on the canvas you are blitting to. You can use CanvasRenderingContext2d.getImageData and .putImageData to copy from one canvas to another. How you implement it is up to you, but the idea is not to draw primitives repeatedly when drawing it once and copying the pixel data is pretty fast by comparison.
Update
I tried messing around with your example but I don't have any experience with react so I'm not exactly sure what's going on. Anyway, I cooked up a pure Javascript example without using virtual canvasses, but rather drawing to a canvas, adding it to the document, and animating the canvas itself inside the constraints of the original canvas. This seems to work the fastest and smoothest (Press c to add circles and d to remove circles):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Buffer Canvas</title>
<style>
body, html {
background-color: aquamarine;
padding: 0;
margin: 0;
}
canvas {
border: 1px solid black;
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
<script>
function randInt(min, max) {
return min + Math.floor(Math.random() * max);
}
class Circle {
constructor(x, y, r) {
this._canvas = document.createElement('canvas');
this.x = x;
this.y = y;
this.r = r;
this._canvas.width = 2*this.r;
this._canvas.height = 2*this.r;
this._canvas.style.width = this._canvas.width+'px';
this._canvas.style.height = this._canvas.height+'px';
this._canvas.style.border = '0px';
this._ctx = this._canvas.getContext('2d');
this._ctx.beginPath();
this._ctx.ellipse(this.r, this.r, this.r, this.r, 0, 0, Math.PI*2);
this._ctx.fill();
document.querySelector('body').appendChild(this._canvas);
const direction = [-1, 1];
this.vx = 2*direction[randInt(0, 2)];
this.vy = 2*direction[randInt(0, 2)];
this._canvas.style.position = "absolute";
this._canvas.style.left = this.x + 'px';
this._canvas.style.top = this.y + 'px';
this._relativeElem = document.querySelector('body').getBoundingClientRect();
}
relativeTo(elem) {
this._relativeElem = elem;
}
getImageData() {
return this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
}
right() {
return this._relativeElem.left + this.x + this.r;
}
left() {
return this._relativeElem.left + this.x - this.r;
}
top() {
return this._relativeElem.top + this.y - this.r
}
bottom() {
return this._relativeElem.top + this.y + this.r;
}
moveX() {
this.x += this.vx;
this._canvas.style.left = this.x - this.r + 'px';
}
moveY() {
this.y += this.vy;
this._canvas.style.top = this.y - this.r + 'px';
}
move() {
this.moveX();
this.moveY();
}
reverseX() {
this.vx = -this.vx;
}
reverseY() {
this.vy = -this.vy;
}
}
let canvas, ctx, width, height, c, canvasRect;
window.onload = preload;
let circles = [];
function preload() {
canvas = document.createElement('canvas');
canvas.style.backgroundColor = "antiquewhite";
ctx = canvas.getContext('2d');
width = canvas.width = 800;
height = canvas.height = 600;
document.querySelector('body').appendChild(canvas);
canvasRect = canvas.getBoundingClientRect();
document.addEventListener('keypress', function(e) {
if (e.key === 'c') {
let radius = randInt(10, 50);
let c = new Circle(canvasRect.left + canvasRect.width / 2 - radius, canvasRect.top + canvasRect.height / 2 - radius, radius);
c.relativeTo(canvasRect);
circles.push(c);
} else if (e.key === 'd') {
let c = circles.pop();
c._canvas.parentNode.removeChild(c._canvas);
}
});
render();
}
function render() {
// Draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach((c) => {
// Check position and change direction if we hit the edge
if (c.left() <= canvasRect.left || c.right() >= canvasRect.right) {
c.reverseX();
}
if (c.top() <= canvasRect.top || c.bottom() >= canvasRect.bottom) {
c.reverseY();
}
// Update position for next render
c.move();
});
requestAnimationFrame(render);
}
</script>
</head>
<body>
</body>
</html>
The attached code shows how to select (on click) a drawn object on canvas and then the object is moved with a double click to click position or deselected with a double click prior to/ post-movement.
I have tried for days but could not work out how to apply this function to all objects in a class or an array via looping (via class constructor + prototyping). I would like to be able to select or deselect any object on screen.
Help will be very much appreciated. Thank you.
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
canvas {
display: block;
margin: 0px;
}
body {
margin: 0px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<input id="click1" type ="button" value="x" style="position: fixed; top: 0px; left: 650px; position: absolute;"></input>
<input id="click2" type ="button" value="y" style="position: fixed; top: 0px; left: 750px; position: absolute;"></input>
<script>
window.onload = function(){
var canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
let strokeColor;
let color;
let mouse_x;
let mouse_y;
let x;
let y;
let w;
let h;
let selected = false;
x = 50;
y = 50;
w = 50;
h = 50;
color="green";
strokeColor = "green";
document.getElementById('canvas').addEventListener("mousemove",go);
document.getElementById('canvas').addEventListener("mouseup",mouseUp);
document.getElementById('canvas').addEventListener("dblclick",dblClick);
document.getElementById('canvas').addEventListener("dblclick",move);
function move(){
if(selected == true){
x = mouse_x;
y = mouse_y;
}
}
function mouseUp(){
if(mouse_x > x && mouse_x < x+w && mouse_y > y && mouse_y < y+w){
strokeColor = "black";
selected = true;
console.log(selected);
}
}
function dblClick(){
if(mouse_x > x && mouse_x < x+w && mouse_y > y && mouse_y < y+w){
color = "green";
strokeColor = color;
selected = false;
console.log(selected);
}
}
function go(e){
mouse_x = e.clientX;
mouse_y = e.clientY;
document.getElementById('click1').value = mouse_x;
document.getElementById('click2').value = mouse_y;
}
function draw(){
context.strokeStyle = strokeColor;;
context.fillStyle = color;
context.beginPath();
context.lineWidth = 3;
context.rect(x,y,w,h);
context.fill();
context.stroke();
}
function animate(){
context.clearRect(0,0,width,height);
context.save();
draw();
context.restore();
requestAnimationFrame(animate);
}
animate();
};
</script>
</body>
</html>
This should be easy to do, let's first talk about the logic that we need.
Have a list of items that we can draw.
Make those items have their own state.
Check if when we click we hit an item or not.
Move or toggle the selected item.
Handle Clear & Draw of the canvas.
Made this simple fiddle just to show you the idea behind it,
let context = $("canvas")[0].getContext("2d");
let elements = [
new element(20, 20, 20, 20, "green"),
new element(45, 30, 30, 30, "red"),
new element(80, 40, 50, 50, "blue"),
];
let mousePosition = {
x: 0,
y: 0,
};
let selected;
function element(x, y, height, width, color) {
this.x = x;
this.y = y;
this.height = height;
this.width = width;
this.color = color;
this.selected = false;
this.draw = function(context) {
context.strokeStyle = (this.selected ? "black" : "white");
context.fillStyle = this.color;
context.beginPath();
context.lineWidth = 2;
context.rect(this.x, this.y, this.height, this.width);
context.fill();
context.stroke();
}
this.move = function(x, y) {
this.x = x;
this.y = y;
}
}
//Select Function
function get_select(x, y) {
let found;
$.each(elements, (i, element) => {
if(x > element.x
&& x < element.x + element.width
&& y > element.y
&& y < element.y + element.height) {
found = element;
}
});
return (found);
}
// Handle selection & Movement.
$("canvas").click(function() {
let found = get_select(mousePosition.x, mousePosition.y);
Clear();
// Toggle Selection
if (found && !selected) {
found.selected = true;
selected = found;
} else if (found === selected) {
found.selected = false;
selected = null;
}
// Move
if (!found && selected) {
selected.move(mousePosition.x, mousePosition.y);
}
Draw();
});
// Record mouse position.
$("canvas").mousemove((event) => {
mousePosition.x = event.pageX;
mousePosition.y = event.pageY;
});
//Draw ALL elements.
function Draw() {
$.each(elements, (i, element) => {
element.draw(context);
});
}
function Clear() {
context.clearRect(0, 0, $("canvas")[0].width, $("canvas")[0].height);
}
// Start.
$(document).ready(() => {
Draw();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<body>
<canvas></canvas>
</body>
</html>
Hope it helps you out!
I am trying to create a simple snake game. My problem is, most of the times when the snake meets the food, the position of the snake is not how it should be.
For a better understanding, please look at this screenshot where the snake (white) meets the food (green):
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
x = 0,
y = 0,
speed = 3;
x_move = speed,
y_move = 0,
food_position_x = Math.floor(Math.random() * canvas.width) - 20;
food_position_y = Math.floor(Math.random() * canvas.height) - 20;
// Drawing
function draw() {
requestAnimationFrame(function() {
draw();
});
// Draw the snake
ctx.beginPath();
ctx.rect(x, y, 20, 20);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.closePath();
// Draw the food
ctx.beginPath();
ctx.rect(food_position_x, food_position_y, 20, 20);
ctx.fillStyle = "lightgreen";
ctx.fill();
ctx.closePath();
// Increase the value of x and y in order to animate
x = x + x_move;
y = y + y_move;
}
draw();
// Key Pressing
document.addEventListener('keydown', function(event) {
switch(event.keyCode) {
case 40: // Moving down
if (x_move != 0 && y_move != -1) {
x_move = 0;
y_move = speed;
}
break;
case 39: // Moving right
if (x_move != -1 && y_move != 0) {
x_move = speed;
y_move = 0;
}
break;
case 38: // Moving top
if (x_move != 0 && y_move != 1) {
x_move = 0;
y_move = -speed;
}
break;
case 37: // Moving left
if (x_move != 1 && y_move != 0) {
x_move = -speed;
y_move = 0;
}
break;
}
});
canvas { background-color:red }
<canvas id="canvas" width="600" height="400"></canvas>
So it seems like I would need a simple grid system but how would I go ahead on this to fix this issue?
I can't see the code for your collision. But I think you have a mistake in your origin. The origin of your squares is not in the middle, it is on the top left corner.