Optimise canvas drawing of a circle - javascript

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>

Related

Alternative to using an event in a setinterval

So I tried my code and I ain't getting the results I expect, especially with the function btnr. Basically what I'm trying to achieve is this: as the code is running I want the snake to change direction when the button is clicked.
var canvas = document.querySelector("canvas");
var right = document.querySelector("#right");
var left = document.querySelector("#left");
var up = document.querySelector("#up");
var down = document.querySelector("#down");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - 200;
var ctx = canvas.getContext("2d");
width = 20;
height = 20;
var snake = [{
x: 40,
y: 0
}, {
x: 20,
y: 0
}];
function move() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (var i = 0; i < snake.length; i++) {
console.log(i);
//lets just say i stands for index number
ctx.strokeStyle = "orange";
ctx.strokeRect(snake[i].x, snake[i].y, width, height);
}
snakex = snake[0].x;
snakey = snake[0].y;
snakex += 20;
function btnr() {
snakex -= 20;
}
right.addEventListener("click", btnr);
var newhead = {
x: snakex,
y: snakey
};
snake.pop();
snake.unshift(newhead);
}
setInterval(move, 800);
<canvas></canvas>
<button id="right">right</button>
<button id="down">down</button>
<button id="left">left</button>
<button id="up">up</button>
I've adjusted the code to make the buttons respond as you expect:
const canvas = document.querySelector("canvas");
const right = document.querySelector("#right");
const left = document.querySelector("#left");
const up = document.querySelector("#up");
const down = document.querySelector("#down");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - 200;
const ctx = canvas.getContext("2d");
const width = 20;
const height = 20;
const snake = [{
x: 40,
y: 0
}, {
x: 20,
y: 0
}];
// --- NEW --- //
let direction = 'right'
right.addEventListener("click", () => direction = 'right');
left.addEventListener("click", () => direction = 'left');
up.addEventListener("click", () => direction = 'up');
down.addEventListener("click", () => direction = 'down');
function move() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < snake.length; i++) {
//lets just say i stands for index number
ctx.strokeStyle = "orange";
ctx.strokeRect(snake[i].x, snake[i].y, width, height);
}
let snakex = snake[0].x;
let snakey = snake[0].y;
// --- NEW --- //
if (direction === 'right') {
snakex += 20;
} else if (direction === 'left') {
snakex -= 20;
} else if (direction === 'up') {
snakey -= 20;
} else if (direction === 'down') {
snakey += 20;
}
const newhead = {
x: snakex,
y: snakey
};
snake.pop();
snake.unshift(newhead);
}
setInterval(move, 800);
:root {
background-color: #242424;
}
<div id="app">
<canvas></canvas>
<button id="right">right</button>
<button id="down">down</button>
<button id="left">left</button>
<button id="up">up</button>
</div>
The main concept you were missing is that you need some way to to track what direction the snake is going. To do that, we can create a variable direction and set it to a string that represents the snake's direction.
After that, we can hook up the buttons to change the direction when each button is pressed by setting the direction variable to left, right, up, or down depending on what was pressed. I moved the logic to add these handles out of your event loop because it would add a new handler each time, which is unnecessary.
Finally in the event loop itself we have some logic to check which direction is pressed, and we adjust the snake's location accordingly.

Move an object through set of coordinates on HTML5 Canvas

I want to move a object (circle in this case) through array of coordinates (for example: {(300,400), (200,300), (300,200),(400,400)})on HTML5 Canvas. I could move the object to one coordinate as follows. The following code draws a circle at (100,100) and moves it to (300,400). I am stuck when trying to extend this so that circle moves through set of coordinates one after the other.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
//circle object
let circle ={
x:100,
y:100,
radius:10,
dx:1,
dy:1,
color:'blue'
}
//function to draw above circle on canvas
function drawCircle(){
ctx.beginPath();
ctx.arc(circle.x,circle.y,circle.radius,0,Math.PI*2);
ctx.fillStyle=circle.color;
ctx.fill();
ctx.closePath();
}
//Moving to a target coordinate (targetX,targetY)
function goTo(targetX,targetY){
if(Math.abs(circel.x-targetX)<circle.dx && Math.abs(circel.y-targetY)<circle.dy){
circle.dx=0;
circle.dy=0;
circel.x = targetX;
circle.y = targetY;
}
else{
const opp = targetY - circle.y;
const adj = targetX - circle.x;
const angle = Math.atan2(opp,adj)
circel.x += Math.cos(angle)*circle.dx
circle.y += Math.sin(angle)*circle.dy
}
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawCircle()
goTo(300,400)
requestAnimationFrame(update);
}
update()
Random access key frames
For the best control of animations you need to create way points (key frames) that can be accessed randomly by time. This means you can get any position in the animation just by setting the time.
You can then play and pause, set speed, reverse and seek to any position in the animation.
Example
The example below uses a set of points and adds data required to quickly locate the key frames at the requested time and interpolate the position.
The blue dot will move at a constant speed over the path in a time set by pathTime in this case 4 seconds.
The red dot's position is set by the slider. This is to illustrate the random access of the animation position.
const ctx = canvas.getContext('2d');
const pathTime = 4; // Total time to travel path from start to end in seconds
var startTime, animTime = 0, paused = false;
requestAnimationFrame(update);
const P2 = (x, y) => ({x, y, dx: 0,dy: 0,dist: 0, start: 0, end: 0});
const pathCoords = [
P2(20, 20), P2(100, 50),P2(180, 20), P2(150, 100), P2(180, 180),
P2(100, 150), P2(20, 180), P2(50, 100), P2(20, 20),
];
createAnimationPath(pathCoords);
const circle ={
draw(rad = 10, color = "blue") {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(this.x, this.y, rad, 0, Math.PI * 2);
ctx.fill();
}
};
function createAnimationPath(points) { // Set up path for easy random position lookup
const segment = (prev, next) => {
[prev.dx, prev.dy] = [next.x - prev.x, next.y - prev.y];
prev.dist = Math.hypot(prev.dx, prev.dy);
next.end = next.start = prev.end = prev.start + prev.dist;
}
var i = 1;
while (i < points.length) { segment(points[i - 1], points[i++]) }
}
function getPos(path, pos, res = {}) {
pos = (pos % 1) * path[path.length - 1].end; // loop & scale to total length
const pathSeg = path.find(p => pos >= p.start && pos <= p.end);
const unit = (pos - pathSeg.start) / pathSeg.dist; // unit distance on segment
res.x = pathSeg.x + pathSeg.dx * unit; // x, y position on segment
res.y = pathSeg.y + pathSeg.dy * unit;
return res;
}
function update(time){
// startTime ??= time; // Throws syntax on iOS
startTime = startTime ?? time; // Fix for above
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (paused) { startTime = time - animTime }
else { animTime = time - startTime }
getPos(pathCoords, (animTime / 1000) / pathTime, circle).draw();
getPos(pathCoords, timeSlide.value, circle).draw(5, "red");
requestAnimationFrame(update);
}
pause.addEventListener("click", ()=> { paused = true; pause.classList.add("pause") });
play.addEventListener("click", ()=> { paused = false; pause.classList.remove("pause") });
rewind.addEventListener("click", ()=> { startTime = undefined; animTime = 0 });
div {
position:absolute;
top: 5px;
left: 20px;
}
#timeSlide {width: 360px}
.pause {color:blue}
button {height: 30px}
<div><input id="timeSlide" type="range" min="0" max="1" step="0.001" value="0" width= "200"><button id="rewind">Start</button><button id="pause">Pause</button><button id="play">Play</button></div>
<canvas id="canvas" width="200" height="200"></canvas>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// array of path coords
const pathCoords = [
[200,100],
[300, 150],
[200,190],
[400,100],
[50,10],
[150,10],
[0, 50],
[500,90],
[20,190],
[10,180],
];
// current point
let currentTarget = pathCoords.shift();
//circle object
const circle ={
x:10,
y:10,
radius:10,
dx:2,
dy:2,
color:'blue'
}
//function to draw above circle on canvas
function drawCircle(){
ctx.beginPath();
ctx.arc(circle.x,circle.y,circle.radius,0,Math.PI*2);
ctx.fillStyle=circle.color;
ctx.fill();
ctx.closePath();
}
//Moving to a target coordinate (targetX,targetY)
function goTo(targetX, targetY){
if(Math.abs(circle.x-targetX)<circle.dx && Math.abs(circle.y-targetY)<circle.dy){
// dont stop...
//circle.dx = 0;
//circle.dy = 0;
circle.x = targetX;
circle.y = targetY;
// go to next point
if (pathCoords.length) {
currentTarget = pathCoords.shift();
} else {
console.log('Path end');
}
} else {
const opp = targetY - circle.y;
const adj = targetX - circle.x;
const angle = Math.atan2(opp,adj)
circle.x += Math.cos(angle)*circle.dx
circle.y += Math.sin(angle)*circle.dy
}
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawCircle();
goTo(...currentTarget);
requestAnimationFrame(update);
}
update();
<canvas id=canvas width = 500 height = 200></canvas>

JS canvas animation, my effect is accelerating and accumulating, but the speed of the effect is in the console same?

I tried to create a canvas effect with fireworks, but the more you click, the faster it gets and it seems to accumulate on itself. When I listed the speed it was similar and did not correspond to what was happening there. I also tried to cancel the draw if it got out of the canvas but it didn´t help.
Here is link https://dybcmwd8icxxdxiym4xkaw-on.drv.tw/canvasTest.html
var fireAr = [];
var expAr = [];
function Firework(x, y, maxY, maxX, cn, s, w, en) {
this.x = x;
this.y = y;
this.maxY = maxY;
this.maxX = maxX;
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
this.explosion = function() {
for (; this.i < this.en; this.i++) {
var ey = this.maxY;
var ex = this.maxX;
var ecn = Math.floor(Math.random() * color.length);
var esX = (Math.random() - 0.5) * 3;
var esY = (Math.random() - 0.5) * 3;
var ew = Math.random() * 10;
var t = true;
expAr.push(new Exp(ew, esX, esY, ex, ey, ecn, t));
}
for (var e = 0; e < expAr.length; e++) {
expAr[e].draw();
}
}
this.draw = function() {
if (this.y < this.maxY) {
this.explosion();
} else {
this.track();
this.y -= this.s;
}
}
}
function Exp(ew, esX, esY, ex, ey, ecn, t) {
this.ew = ew;
this.esX = esX;
this.esY = esY;
this.ex = ex;
this.ey = ey;
this.ecn = ecn;
this.t = t;
this.draw = function() {
if (this.t == true) {
c.beginPath();
c.shadowBlur = 20;
c.shadowColor = color[this.ecn];
c.rect(this.ex, this.ey, this.ew, this.ew);
c.fillStyle = color[this.ecn];
c.fill();
c.closePath();
this.ex += this.esX;
this.ey += this.esY;
}
}
}
window.addEventListener('click', function(event) {
var x = event.clientX;
var y = canvas.height;
mouse.clickX = event.clientX;
mouse.clickY = event.clientY;
var maxY = event.clientY;
var maxX = event.clientX;
var cn = Math.floor(Math.random() * color.length);
var s = Math.random() * 5 + 5;
var w = Math.random() * 20 + 2;
var en = Math.random() * 50 + 5;
fireAr.push(new Firework(x, y, maxY, maxX, cn, s, w, en));
});
function ani() {
requestAnimationFrame(ani);
c.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < fireAr.length; i++) {
fireAr[i].draw();
}
}
ani();
I deleted some unnecessary parts in my opinion but if I'm wrong and I missed something I'll try to fix it
Here are a few simple ways you can improve performance:
Commenting out shadowBlur gives a noticeable boost. If you need shadows, see this answer which illustrates pre-rendering.
Try using fillRect and ctx.rotate() instead of drawing a path. Saving/rotating/restoring the canvas might be prohibitive, so you could use non-rotated rectangles.
Consider using a smaller canvas which is quicker to repaint than one that may fill the entire window.
Another issue is more subtle: Fireworks and Exps are being created (making objects is expensive!) and pushed onto arrays. But these arrays are never trimmed and objects are never reused after they've left the visible canvas. Eventually, the rendering loop gets bogged down by all of the computation for updating and rendering every object in the fireAr and expAr arrays.
A naive solution is to check for objects exiting the canvas and splice them from the expAr. Here's pseudocode:
for (let i = expAr.length - 1; i >= 0; i--) {
if (!inBounds(expAr[i], canvas)) {
expAr.splice(i, 1);
}
}
Iterate backwards since this mutates the array's length. inBounds is a function that checks an Exp object's x and y properties along with its size and the canvas width and height to determine if it has passed an edge. More pseudocode:
function inBounds(obj, canvas) {
return obj.x >= 0 && obj.x <= canvas.width &&
obj.y >= 0 && obj.y <= canvas.height;
}
This check isn't exactly correct since the rectangles are rotated. You could check each corner of the rectangle with a pointInRect function to ensure that at least one is inside the canvas.
Fireworks can be spliced out when they "explode".
splice is an expensive function that walks up to the entire array to shift items forward to fill in the vacated element. Splicing multiple items in a loop gives quadratic performance. This can be made linear by putting surviving fireworks in a new list and replacing the previous generation on each frame. Dead firework objects can be saved in a pool for reuse.
Beyond that, I strongly recommend using clear variable names.
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
These names have little or no meaning to an outside reader and are unlikely to mean much to you if you take a couple months away from the code. Use full words like "size", "width", etc.
Another side point is that it's a good idea to debounce your window resize listener.
Here's a quick proof of concept that illustrates the impact of shadowBlur and pruning dead elements.
const rnd = n => ~~(Math.random() * n);
const mouse = {pressed: false, x: 0, y: 0};
let fireworks = [];
let shouldSplice = false;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
document.body.style.margin = 0;
canvas.style.background = "#111";
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
ctx.shadowBlur = 0;
const fireworksAmt = document.querySelector("#fireworks-amt");
document.querySelector("input[type=range]").addEventListener("change", e => {
ctx.shadowBlur = e.target.value;
document.querySelector("#shadow-amt").textContent = ctx.shadowBlur;
});
document.querySelector("input[type=checkbox]").addEventListener("change", e => {
shouldSplice = !shouldSplice;
});
const createFireworks = (x, y) => {
const color = `hsl(${rnd(360)}, 100%, 60%)`;
return Array(rnd(20) + 1).fill().map(_ => ({
x: x,
y: y,
vx: Math.random() * 6 - 3,
vy: Math.random() * 6 - 3,
size: rnd(4) + 2,
color: color
}));
}
(function render() {
if (mouse.pressed) {
fireworks.push(...createFireworks(mouse.x, mouse.y));
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const e of fireworks) {
e.x += e.vx;
e.y += e.vy;
e.vy += 0.03;
ctx.beginPath();
ctx.fillStyle = ctx.shadowColor = e.color;
ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2);
ctx.fill();
if (shouldSplice) {
e.size -= 0.03;
if (e.size < 1) {
e.dead = true;
}
}
}
fireworks = fireworks.filter(e => !e.dead);
fireworksAmt.textContent = "fireworks: " + fireworks.length;
requestAnimationFrame(render);
})();
let debounce;
addEventListener("resize", e => {
clearTimeout(debounce);
debounce = setTimeout(() => {
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
}, 100);
});
canvas.addEventListener("mousedown", e => {
mouse.pressed = true;
});
canvas.addEventListener("mouseup", e => {
mouse.pressed = false;
});
canvas.addEventListener("mousemove", e => {
mouse.x = e.offsetX;
mouse.y = e.offsetY;
});
* {
font-family: monospace;
user-select: none;
}
div > span, body > div {padding: 0.5em;}
canvas {display: block;}
<div>
<div id="fireworks-amt">fireworks: 0</div>
<div>
<label>splice? </label>
<input type="checkbox">
</div>
<div>
<label>shadowBlur (<span id="shadow-amt">0</span>): </label>
<input type="range" value=0>
</div>
</div>

How to calculate jump based on delta time?

I'm trying to make a little game with JavaScript (no engine) and I want to get rid of frame-based animation.
I successfully added delta time for horizontal movements (work fine with 60 or 144fps).
But I can't make it work with the jump, height (or the strength) isn't always the same, and I don't know why.
I already tried (And still had the exact same problem):
Passing Delta Time at the end of update(): x += Math.round(dx * dt)
Changing Date.now() to performance.now()
Not rounding DeltaY
Locking Jump Height
I made a simplified example with 2 jump type, height locked jump and a normal jump (IDK what to call it). Both have the same problem.
const canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
canvas2 = document.getElementById('canvas2'),
ctx2 = canvas2.getContext('2d');
// CLASS PLAYER ------------------------
class Actor {
constructor(color, ctx, j) {
this.c = ctx
this.w = 20
this.h = 40
this.x = canvas.width /2 - this.w/2
this.y = canvas.height/2 - this.h/2
this.color = color
// Delta
this.dy = 0
// Movement
this.gravity = 25/1000
this.maxSpeed = 600/1000
// Jump Height lock
this.jumpType = (j) ? 'capedJump' : 'normalJump'
this.jumpHeight = -50
// Booleans
this.isOnFloor = false
}
// Normal jump
normalJump(max) {
if(!this.isOnFloor) return
this.dy = -max
this.isOnFloor = false
}
// Jump lock (locked max height)
capedJump(max) {
const jh = this.jumpHeight;
if(jh >= 0) return
this.dy += -max/15
if(jh - this.dy >= 0) {
this.dy = (jh - this.dy) + jh
this.jumpHeight = 0
} else {
this.jumpHeight += -this.dy
}
}
update(dt) {
const max = this.maxSpeed*dt,
gravity = this.gravity*dt;
// JUMP
this[this.jumpType](max)
// GRAVITY
this.dy += gravity
// TOP AND DOWN COLLISION (CANVAS BORDERS)
const y = this.y + this.dy,
h = y + this.h;
if (y <= 0) this.y = this.dy = 0
else if (h >= canvas.height) {
this.y = canvas.height - this.h
this.dy = 0
this.isOnFloor = true
this.jumpHeight = -50
}
// Update Y
this.y += Math.round(this.dy)
}
draw() {
const ctx = this.c
ctx.fillStyle = this.color
ctx.fillRect(this.x, this.y, this.w, this.h)
}
}
const Player = new Actor('brown', ctx, false)
const Player2 = new Actor('blue', ctx2, true)
// ANIMATE -----------------------------
let lastRender = 0
let currRender = Date.now()
function animate() {
// Set Delta Time
lastRender = currRender
currRender = Date.now()
let dt = currRender - lastRender
// CANVAS #1 (LEFT)
ctx.clearRect(0, 0, canvas.width, canvas.height)
background(ctx)
Player.update(dt)
Player.draw()
// CANVAS #2 (RIGHT)
ctx2.clearRect(0, 0, canvas2.width, canvas2.height)
background(ctx2)
Player2.update(dt)
Player2.draw()
window.requestAnimationFrame(animate)
}
animate()
// EVENT LISTENERS -----------------------
window.addEventListener('keydown', (e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = true
})
window.addEventListener('keyup', (e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code)) Player.keys[e.code] = false
})
// Just a function to draw Background nothing to see here
function background(c) {
const lineNumber = Math.floor(canvas.height/10)
c.fillStyle = 'gray'
for(let i = 0; i < lineNumber; i++) {
c.fillRect(0, lineNumber*i, canvas.width, 1)
}
}
div {
display: inline-block;
font-family: Arial;
}
canvas {
border: 1px solid black;
}
span {
display: block;
color: gray;
text-align: center;
}
<div>
<canvas width="100" height="160" id="canvas"></canvas>
<span>Normal</span>
</div>
<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>
Here's how I would refactor the code:
Don't use dy for both speed and position (which you seem to be doing). Rename it vy and use it purely as the vertical velocity.
Move isOnFloor to a function so that we can always check for collisions with the floor.
Decouple the jump functions from actual movement updates. Just make them set the vertical velocity if the player is on the floor.
Perform top / bottom collision checking separately depending on the direction of movement.
Don't round DeltaY - it'll mess up small movements.
With these changes in place, the movement behavior is correct and stable:
const canvas1 = document.getElementById('canvas1'),
ctx1 = canvas1.getContext('2d'),
canvas2 = document.getElementById('canvas2'),
ctx2 = canvas2.getContext('2d');
// Global physics variables
const GRAVITY = 0.0015;
const MAXSPEED = 0.6;
const MAXHEIGHT = 95;
// CLASS PLAYER ------------------------
class Actor {
constructor(C, W, H, J) {
// World size
this.worldW = W;
this.worldH = H;
// Size & color
this.w = 20;
this.h = 40;
this.color = C;
// Speed
this.vy = 0;
// Position
this.x = W/2 - this.w/2;
this.y = H/2 - this.h/2;
// Jump Height lock
this.jumpCapped = J;
this.jumpHeight = 0;
}
// move isOnFloor() to a function
isOnFloor() {
return this.y >= this.worldH - this.h;
}
// Normal jump
normalJump() {
if(!this.isOnFloor()) return;
this.vy = -MAXSPEED;
}
// Jump lock (locked max height)
cappedJump(max) {
if(!this.isOnFloor()) return;
this.vy = -MAXSPEED;
this.jumpHeight = max;
}
update(dt) {
// JUMP
if (this.jumpCapped)
this.cappedJump(MAXHEIGHT);
else
this.normalJump();
// GRAVITY
this.vy += GRAVITY * dt;
this.y += this.vy * dt;
// Bottom collision
if (this.vy > 0) {
if (this.isOnFloor()) {
this.y = this.worldH - this.h;
this.vy = 0;
}
}
else
// Top collision
if (this.vy < 0) {
const maxh = (this.jumpCapped) ? (this.worldH - this.jumpHeight) : 0;
if (this.y < maxh) {
this.y = maxh;
this.vy = 0;
}
}
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
}
}
const Player1 = new Actor('brown', canvas1.width, canvas1.height, false);
const Player2 = new Actor('blue', canvas2.width, canvas2.height, true);
// ANIMATE -----------------------------
let lastUpdate = 0;
let randomDT = 0;
function animate() {
// Compute delta time
let currUpdate = Date.now();
let dt = currUpdate - lastUpdate;
// Randomize the update time interval
// to test the physics' stability
if (dt > randomDT) {
randomDT = 35 * Math.random() + 5;
Player1.update(dt);
Player2.update(dt);
lastUpdate = currUpdate;
}
// CANVAS #1 (LEFT)
ctx1.clearRect(0, 0, canvas1.width, canvas1.height);
background(ctx1);
Player1.draw(ctx1);
// CANVAS #2 (RIGHT)
ctx2.clearRect(0, 0, canvas2.width, canvas2.height);
background(ctx2);
Player2.draw(ctx2);
window.requestAnimationFrame(animate);
}
animate();
// EVENT LISTENERS -----------------------
window.addEventListener('keydown',
(e) => {
e.preventDefault();
if (Player.keys.hasOwnProperty(e.code))
Player.keys[e.code] = true;
}
)
window.addEventListener('keyup',
(e) => {
e.preventDefault()
if (Player.keys.hasOwnProperty(e.code))
Player.keys[e.code] = false;
}
)
// Just a function to draw Background nothing to see here
function background(c) {
const lineNumber = Math.floor(canvas1.height/10)
c.fillStyle = 'gray'
for(let i = 0; i < lineNumber; i++) {
c.fillRect(0, lineNumber*i, canvas1.width, 1)
}
}
div {
display: inline-block;
font-family: Arial;
}
canvas {
border: 1px solid black;
}
span {
display: block;
color: gray;
text-align: center;
}
<div>
<canvas width="100" height="160" id="canvas1"></canvas>
<span>Normal</span>
</div>
<div>
<canvas width="100" height="160" id="canvas2"></canvas>
<span>Locked</span>
</div>

How do I increase the score value of my game?

I am making a game in which the player catches falling objects. When an object is caught, the score should increase by one, but it doesn't. I have a collision function already in which the objects disappear when the player catches them. The score is already displayed on the screen. Here is my code:
(() => {
// In general, don't touch anything except for sections explicitly marked as such.
// Look for the exclamations points (!!!!!) for such markers.
let canvas = document.getElementById("game");
let game = canvas.getContext("2d");
let lastTimestamp = 0;
let playerScore = 0;
const FRAME_RATE = 60;
const FRAME_DURATION = 1000 / FRAME_RATE;
// !!!!! Change/add to this as needed. What other objects or variables will you need for your game idea?
// A score? Different kinds of fallers? Player statistics? It's all up to you!
let fallers = [];
// Check out that cool ES6 feature: default parameter values!
const DEFAULT_DESCENT = 0.0002; // This is per millisecond.
let Faller = function (x, y, width, height, dx = 0, dy = 0, ax = 0, ay = DEFAULT_DESCENT) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
// Velocity.
this.dx = dx;
this.dy = dy;
// Acceleration.
this.ax = ax;
this.ay = ay;
};
Faller.prototype.draw = function () {
game.fillStyle = "blue";
game.fillRect(this.x, this.y, this.width, this.height);
};
Faller.prototype.move = function (millisecondsElapsed) {
//Good old Newtonian physics.
this.x += this.dx * millisecondsElapsed;
this.y += this.dy * millisecondsElapsed;
this.dx += this.ax * millisecondsElapsed;
this.dy += this.ay * millisecondsElapsed;
};
const DEFAULT_PLAYER_WIDTH = 45;
const DEFAULT_PLAYER_HEIGHT = 20;
const DEFAULT_PLAYER_Y = canvas.height - DEFAULT_PLAYER_HEIGHT;
let Player = function (x, y = DEFAULT_PLAYER_Y, width = DEFAULT_PLAYER_WIDTH, height = DEFAULT_PLAYER_HEIGHT) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
};
Player.prototype.draw = function () {
game.fillStyle = "aqua";
game.beginPath();
game.moveTo(this.x - this.width / 2, this.y + this.height);
game.lineTo(this.x - 2, this.y);
game.lineTo(this.x, this.y - 20);
game.lineTo(this.x + 2, this.y);
game.lineTo(this.x + this.width / 2, this.y + this.height);
game.closePath();
game.fill();
};
let player = new Player(canvas.width / 2);
// !!!!! You can treat this function like Khan Academy’s `draw`---just precede all
// drawing instructions with `game.`
let draw = (millisecondsElapsed) => {
game.clearRect(0, 0, canvas.width, canvas.height);
fallers.forEach((faller) => {
faller.draw();
//COLLISION FUNCTION!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
let playerScore = document.getElementById("playerScore");
if (player.x < faller.x + faller.width && player.x + player.width > faller.x &&
player.y < faller.y + faller.height && player.y + player.height > faller.y) {
game.clearRect(0, 0, canvas.width, canvas.height);
};
playerScore += 1;
faller.move(millisecondsElapsed);
});
player.draw();
// Remove fallers that have hit the ground. You might have other reasons to remove fallers.
fallers = fallers.filter((faller) => {
return faller.y < canvas.height;
});
};
// !!!!! This section is modifiable to a degree. It is responsible for generating falling objects at random.
// You don't want to completely eliminate this code, but you may want to revise it to modify the rate/range
// of objects that get generated.
const MIN_WIDTH = 10;
const WIDTH_RANGE = 20;
const MIN_HEIGHT = 10;
const HEIGHT_RANGE = 20;
const MILLISECONDS_BETWEEN_FALLERS = 800;
let fallerGenerator;
let startFallerGenerator = () => {
fallerGenerator = setInterval(() => {
// !!!!! This code looks really repetitive! Hmmmm, what to do...
let fallerWidth = Math.floor(Math.random() * WIDTH_RANGE) + MIN_WIDTH;
fallers.push(new Faller(
Math.floor(Math.random() * (canvas.width - fallerWidth)), 0,
fallerWidth, Math.floor(Math.random() * HEIGHT_RANGE) + MIN_HEIGHT
));
}, MILLISECONDS_BETWEEN_FALLERS);
};
let stopFallerGenerator = () => clearInterval(fallerGenerator);
// !!!!! This section is also modifiable to a degree: it is responsible for moving the "player" around based on
// mouse movement.
let setPlayerPositionBasedOnMouse = (event) => {
player.x = event.clientX / document.body.clientWidth * canvas.width;
};
document.body.addEventListener("mouseenter", setPlayerPositionBasedOnMouse);
document.body.addEventListener("mousemove", setPlayerPositionBasedOnMouse);
// OK, back to the no-touch zone (unless you _really_ know what you’re doing).
let running = false;
let nextFrame = (timestamp) => {
if (!lastTimestamp) {
lastTimestamp = timestamp;
}
if (timestamp - lastTimestamp < FRAME_DURATION) {
if (running) {
window.requestAnimationFrame(nextFrame);
}
return;
}
draw(timestamp - lastTimestamp);
lastTimestamp = timestamp;
if (running) {
window.requestAnimationFrame(nextFrame);
}
};
document.getElementById("start-button").addEventListener("click", () => {
running = true;
lastTimestamp = 0;
startFallerGenerator();
window.requestAnimationFrame(nextFrame);
});
document.getElementById("stop-button").addEventListener("click", () => {
stopFallerGenerator();
running = false;
});
})();
canvas {
border: solid 1px gray;
display: block;
margin-left: auto;
margin-right: auto;
width: 1024px;
}
p {
text-align: center;
}
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Undefined</title>
<link rel="stylesheet" href="falling.css">
</head>
<body>
<h1>Not finished</h1>
<p>
<b>Score:</b> <span id="playerScore">0</span>
<!-- This starter code only contains the area in which the game graphics get drawn.
Feel free to add more elements for displays like scores, lives left, player
statistics, instructions, etc. -->
<canvas id="game" width="1024" height="536">
Sorry, but you need a web browser that supports the
<code>canvas</code> element.
</canvas>
<p>
<button id="start-button">Start</button>
<button id="stop-button">Stop</button>
</p>
<script src="falling.js"></script>
</body>
</html>
You start the game with:
let playerScore = 0;
But in your collision code you have:
let playerScore = document.getElementById("playerScore");
and
playerScore += 1;
Aside from having two variables with the same name (albeit in different scopes), you end up calling += 1 on the HTML element, and never actually write the value of the score to the document for display.

Categories

Resources