I have implemented a recursive maze generation algorithm in Javascript, and am rendering using HTML canvas.
The problem I have, is that I am unable to render to the canvas while the recursive function is in progress. The render only appears after recursive calls have finished.
Here is my (rough) functioning recursive function :
function start(canvas_context)
{
//do some stuff here...
render(canvas_context);
if()...{
start(canvas_context);
}
else if(...)
{
if(...)
{
start(canvas_context);
}
}
}
Here is my rendering function:
function render(canvas_context)
{
canvas_context.fillStyle = 'black';
canvas_context.fillRect(0,0,canvas.width, canvas.height);
for(..)
{
for(...)
{
canvas_context.strokeStyle = 'red';
canvas_context.beginPath();
canvas_context.moveTo(start.x, start.y);
canvas_context.lineTo(end.x,end.y);
canvas_context.stroke();
}
}
}
Here are the full functions if they are needed:
function start(canvas_context)
{
//Vector2.print(game_config["current_grid_pos"]);
var current_block = game_config["grid_space"][game_config["current_grid_pos"].y][game_config["current_grid_pos"].x];
var random_neighbor_grid_pos = random_neighbor(current_block);
//render(canvas_context);
if(random_neighbor_grid_pos.x != -1 && random_neighbor_grid_pos.y != -1)
{
var next_block = game_config["grid_space"][random_neighbor_grid_pos.y][random_neighbor_grid_pos.x];
next_block.visited = true;
game_config["visited_stack"].push(random_neighbor_grid_pos);
remove_walls(current_block, next_block);
game_config["current_grid_pos"] = random_neighbor_grid_pos;
start(canvas_context);
}
else if (game_config["visited_stack"].length > 0)
{
current_block.visited = true;
game_config["visited_stack"].pop();
if(game_config["visited_stack"].length != 0)
{
game_config["current_grid_pos"] = game_config["visited_stack"][game_config["visited_stack"].length - 1];
start(canvas_context);
}
}
}
function render(canvas_context)
{
//Initialise the canvas background.
canvas_context.fillStyle = 'black';
canvas_context.fillRect(0,0,canvas.width, canvas.height);
for(var y = 0; y < game_config["grid_space"].length; y++)
{
for(var x = 0; x < game_config["grid_space"][0].length; x++)
{
var block = game_config["grid_space"][y][x];
var walls = block.get_walls();
for(var key in walls)
{
var wall = walls[key];
render_line(canvas_context, wall.start, wall.end);
}
}
}
}
function render_line(canvas_context, start, end)
{
canvas_context.strokeStyle = 'red';
canvas_context.beginPath();
canvas_context.moveTo(start.x, start.y);
canvas_context.lineTo(end.x,end.y);
canvas_context.stroke();
}
Is it even possible to render during recursive calls with HTML canvas?
I think you can do it without a recursion but I show both ways:
const cellsise = 10;
class block {
constructor (x, y){
this.x = x;
this. y = y;
this.delta = {x: x * cellsise , y: y * cellsise};
this.walls = {
left: {
start: {
x: 0 + this.delta.x,
y: 0 + this.delta.y
},
end: {
x: 0 + this.delta.x,
y: cellsise + this.delta.y
}
},
top: {
start: {
x: 0 + this.delta.x,
y: 0 + this.delta.y
},
end: {
x: cellsise + this.delta.x,
y: 0 + this.delta.y
}
},
right: {
start: {
x: cellsise + this.delta.x,
y: 0 + this.delta.y
},
end: {
x: cellsise + this.delta.x,
y: cellsise + this.delta.y
}
},
bottom: {
start: {
x: 0 + this.delta.x,
y: cellsise + this.delta.y
},
end: {
x: cellsise + this.delta.x,
y: cellsise + this.delta.y
}
}
};
}
get_walls(){
return this.walls;
}
}
const game_config = {
grid_space:[],
height: 10,
width: 10,
}
function generateGridSpace() {
game_config.grid_space=[];
for(y = 0 ; y < game_config.height; ++y){
game_config.grid_space.push([]);
for(x = 0 ; x < game_config.width; ++x){
game_config.grid_space[y].push(new block(x, y))
}
}
}
function remove_walls(current_block){
let left = Math.random() > 0.5;
let top = !left //Math.random() > 0.5;
let right = Math.random() > 0.5;
let bottom = !right //false// Math.random() > 0.5;
if (left && current_block.x - 1 > 0) {
delete current_block.walls.left;
delete game_config.grid_space[current_block.x-1][current_block.y].walls.right
}
if (top && current_block.y - 1 > 0) {
delete current_block.walls.top;
delete game_config.grid_space[current_block.x][current_block.y-1].walls.bottom;
}
if (right && current_block.x+1 < game_config.width -1) {
delete current_block.walls.right;
delete game_config.grid_space[current_block.x + 1][current_block.y].walls.left;
}
if (bottom && current_block.y+1 < game_config.height -1) {
delete current_block.walls.bottom;
delete game_config.grid_space[current_block.x][current_block.y + 1].walls.top;
}
}
function start_with_recursion(canvas_context, mix=null) {
if (!mix) {
mix = game_config.grid_space.reduce((acc, e) => [...acc,...e], [])
.sort(() => Math.random() - 0.5)
} else if (mix.length === 0) {
return;
}
const block = mix.pop();
const i = game_config.width * game_config.height - mix.length
setTimeout(() => {
remove_walls(block);
render_block(canvas_context, block);
}, i * 50)
start_with_recursion(canvas_context, mix)
}
function start_without_recursion(canvas_context) {
const mix = game_config.grid_space.reduce((acc, e) => [...acc,...e], [])
.sort(() => Math.random() - 0.5)
for(let i = 0; i < mix.length; ++i){
setTimeout(() => {
remove_walls(mix[i]);
render_block(canvas_context, mix[i]);
}, i * 50)
}
}
function render_block(canvas_context, block) {
var walls = block.get_walls();
for(var key in walls)
{
var wall = walls[key];
render_line(canvas_context, wall.start, wall.end);
}
}
function render_line(canvas_context, start, end) {
canvas_context.strokeStyle = 'red';
canvas_context.beginPath();
canvas_context.moveTo(
5 + start.x + .5,
5 + start.y + .5
);
canvas_context.lineTo(
5 + end.x + .5,
5 + end.y + .5
);
canvas_context.stroke();
}
canvas = document.querySelector('canvas');
canvas.height = game_config.height * cellsise + 5 * 2;
canvas.width = game_config.width * cellsise + 5 * 2;
ctx = canvas.getContext('2d');
ctx.fillStyle = 'black';
ctx.fillRect(0,0,canvas.width, canvas.height);
// ctx.lineWidth = 2;
generateGridSpace();
// start_without_recursion(ctx);
start_with_recursion(ctx);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<canvas></canvas>
</body>
</html>
also for get a random neighbor:
function random_neighbor(current_block) {
const {height, width} = game_config;
const neighbors = [
{x: current_block.x, y: current_block.y - 1}, // from top
{x: current_block.x - 1, y: current_block.y}, // from left
{x: current_block.x + 1, y: current_block.y}, // from right
{x: current_block.x, y: current_block.y + 1}, // from bottom
].filter(e => e.x >= 0 && e.x < width && e.y >= 0 && e.y < height)
const rnd = neighbors[Math.floor(Math.random() * neighbors.length)]
return rnd; // {x, y}
}
You also need to take into account that the drawn wall can be removed later, which will not correspond to reality. Therefore, it is better to render (even with a delay) after the maze is fully generated.
Suggest converting your recursive function into an iterator function, and then introducing a second function that makes use of setInterval to iteratively draw your maze, and once complete, then performs a clearInterval.
Here's a quick sample that I whipped up exemplifying the concept, which simply draws increasingly larger rectangles on the canvas every 50ms, with the iterator function returning the size of the next rectangle to be drawn. In your case, you'll likely want to yield an object defining the portion of the maze to be drawn...
var ctx = document.getElementById('canvas').getContext('2d');
function* recurse( n, max ) {
yield n;
if ( n < max ) {
yield* recurse( n + 1, max );
}
}
function drawRectangles() {
function drawIteratively() {
let nd = iterator.next();
if ( nd.done ) {
clearInterval( interval );
} else {
ctx.lineWidth = 1;
ctx.strokeStyle = '#FF0000';
ctx.beginPath();
ctx.rect( nd.value, nd.value, nd.value * 2, nd.value * 2 );
ctx.stroke();
}
}
let iterator = recurse( 25, 100 );
let interval = setInterval( drawIteratively, 50 );
}
drawRectangles();
<canvas id='canvas' width="300" height="300"></canvas>
Note that the iterator function is a very basic recursive function, but recursive nonetheless, showing the critical syntax involved in a recursive iterator function...
Related
In my 2D (top-down) game, I am attempting to add a dash function.
Using an if (keyWentDown("e")) {} condition inside a function named dash. I have a thing set up to face the player's direction, being: plrvector.rotation = 90, and I want my character to move about 50px smoothly when E is pressed.
Problem is, I have no clue how to use vectors. Any tips or directions? I have attempted to use various techniques; however, I could not find any tutorials or anything that could help me.
I am not sure you want to move the player exactly 50px. You would just temporarily increase the velocity by a factor. In the example below, if the E key is pressed, a "turbo" mode is activate increasing the speed by a factor of 3.
You would have to do something along the lines of:
function update(progress) {
const { player } = state;
const turbo = state.pressedKeys.turbo ? 3 : 1; // x3 speed (E)
if (state.pressedKeys.left) {
if (player.speed.x > 0) player.speed.x *= -1;
player.position.x += progress * player.speed.x * turbo;
}
}
Note: This can be modified to only allow turbo for a short duration. For instance, the player may have a turbo meter that they need to fill in order to use this mode.
The following example is adapted from: "Quick Tip: How to Make a Game Loop in JavaScript". I did not utilize vectors pre se, but I store the position and velocity (speed) in a vector-like object. If you want to convert this to vectors, you can check out victor.js. It can easily add/multiply/normalize vectors for you.
// Canvas context reference
const ctx = document.querySelector('#game').getContext('2d');
const hud = document.querySelector('#hud');
// Set the canvas size
Object.assign(ctx.canvas, { width: 600, height: 160 });
// Game state
const state = {
player: {
dimensions: { depth: 10, height: 10, width: 10 },
position: { x: Math.floor(ctx.canvas.width / 2), y: Math.floor(ctx.canvas.height / 2) },
speed: { x: 0.25, y: 0.25 }
},
pressedKeys: {
left: false,
right: false,
turbo: false,
up: false,
down: false
}
};
// Track keys
const keyMap = new Map(Object.entries({
ArrowDown: 'down',
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowUp: 'up',
a: 'left',
d: 'right',
e: 'turbo',
s: 'down',
w: 'up',
}));
// Game Loop ~ Update
function update(progress) {
const { player } = state;
hud.innerText = Object.entries(state.pressedKeys)
.filter(([k, v]) => v)
.map(([k]) => `<${k}>`)
.sort()
.join(' ');
const turbo = state.pressedKeys.turbo ? 3 : 1; // x3 speed (E)
// Check pressed keys to update position
if (state.pressedKeys.left) {
if (player.speed.x > 0) player.speed.x *= -1;
player.position.x += progress * player.speed.x * turbo;
}
if (state.pressedKeys.right) {
if (player.speed.x < 0) player.speed.x *= -1;
player.position.x += progress * player.speed.x * turbo;
}
if (state.pressedKeys.up) {
if (player.speed.y > 0) player.speed.y *= -1;
player.position.y += progress * player.speed.y * turbo;
}
if (state.pressedKeys.down) {
if (player.speed.y < 0) player.speed.y *= -1;
player.position.y += progress * player.speed.y * turbo;
}
// Check bounds
const threshold = player.dimensions.width;
if (player.position.x > ctx.canvas.width - threshold) {
player.position.x = ctx.canvas.width - threshold;
} else if (player.position.x < threshold) {
player.position.x = threshold;
}
if (player.position.y > ctx.canvas.height - threshold) {
player.position.y = ctx.canvas.height - threshold;
} else if (player.position.y < threshold) {
player.position.y = threshold;
}
}
// Game Loop ~ Draw
function draw() {
const { player, pressedKeys } = state;
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.closePath();
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(player.position.x, player.position.y, player.dimensions.width, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
}
// Game Loop ~ Main
function loop(timestamp) {
const progress = timestamp - lastRender;
update(progress);
draw();
lastRender = timestamp;
window.requestAnimationFrame(loop);
}
// Begin game
let lastRender = 0;
window.requestAnimationFrame(loop);
// Register keyboard events
window.addEventListener('keydown', onKeyDown, false);
window.addEventListener('keyup', onKeyUp, false);
// Event handlers
function onKeyDown({ key }) {
toggleKey(key, true);
}
function onKeyUp({ key }) {
toggleKey(key, false);
}
// Convenience
function toggleKey(key, value) {
const index = keyMap.get(key);
if (index) {
const currentValue = state.pressedKeys[index];
state.pressedKeys[index] = value ?? !currentValue;
}
}
*, *::before,*::after { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
body {
display: flex; flex-direction: column;
align-items: center; justify-content: flex-start; gap: 0.25em;
padding: 0.25em;
}
#hud { white-space: pre; text-transform: uppercase; }
<!-- See: https://www.sitepoint.com/quick-tip-game-loop-in-javascript/ -->
<canvas id="game"></canvas>
<div id="hud"></div>
Vector math
Here is an example of adding/multiplying vectors:
Add t and u to get { x: 4, y: 7 }
Multiply the result above by t to get { x: 4, y: 14 }
const vectorAdd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const vectorMultiply = (a, b) => ({ x: a.x * b.x, y: a.y * b.y });
let t = { x: 1, y: 2 }, u = { x: 3, y: 5 }, v = vectorMultiply(vectorAdd(t, u), t);
console.log(v); // { x: 4, y: 14 }
If you want to chain these calls, you can try creating a wrapper:
const vectorAdd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const vectorMultiply = (a, b) => ({ x: a.x * b.x, y: a.y * b.y });
const vector = function({ x, y }) {
[this.x, this.y] = [x, y];
this.add = (other) => {
const { x, y } = vectorAdd(this, other);
[this.x, this.y] = [x, y];
return this;
};
this.multiply = (other) => {
const { x, y } = vectorMultiply(this, other);
[this.x, this.y] = [x, y];
return this;
};
this.value = () => ({ x: this.x, y: this.y });
return this;
}
let t = { x: 1, y: 2 }, u = { x: 3, y: 5 }, v = vector(t).add(u).multiply(t).value();
console.log(v); // { x: 4, y: 14 }
I'm trying to make a simple interactive game where there are circles of different colours moving in the canvas and when the user clicks on the blue circles, it logs the number of clicks on the screen. When clicking circles with any other colour, the animation stops.
I'm very new to javascript but this is what I have for now. I've made a function with random coloured circles and a function with blue circles moving but I'm totally stuck on how to stop the animation when clicking on the function with a random coloured circles and logging the amount of clicks on the blue circles. If someone could help me move forward with it in any way (doesn't have to be the full thing), that would be awesome, thanks.
JS
var canvas;
var ctx;
var w = 1000;
var h = 600;
var colours = ["red", "blue"];
var allCircles = [];
for(var i=0; i<1; i++){
setTimeout(function(){console.log(i)},1000);
}
document.querySelector("#myCanvas").onclick = click;
createData(2);
createDataTwo(20);
setUpCanvas();
animationLoop();
function animationLoop(){
clear();
for(var i = 0; i<allCircles.length; i++){
circle(allCircles[i]);
forward(allCircles[i], 5)
turn(allCircles[i], randn(30));
collisionTestArray(allCircles[i],allCircles)
bounce(allCircles[i]);
}
requestAnimationFrame(animationLoop);
}
function collisionTestArray(o, a){
for(var i=0; i<a.length; i++){
if(o !=a[i]){
collision(o,a[i]);
}
}
}
function collision(o1,o2){
if(o1 && o2){
var differencex = Math.abs(o1.x-o2.x);
var differencey = Math.abs(o1.y-o2.y);
var hdif = Math.sqrt(differencex*differencex+differencey*differencey);
if(hdif<o1.r+o2.r){
if(differencex < differencey){
turn(o1, 180-2*o1.angle);
turn(o2, 180-2*o2.angle);
}else{
turn(o1, 360-2*o1.angle);
turn(o2, 360-2*o2.angle);
}
turn(o1, 180);
turn(o2, 180);
console.log("collision");
};
}
}
function click(event){
clear()
}
function bounce (o){
if(o.x > w || o.x < 0){
turn(o, 180-2*o.angle);
};
if(o.y > h || o.y < 0){
turn(o, 360-2*o.angle);
}
}
function clear(){
ctx.clearRect(0,0,w,h);
}
function stop (){
o1.changex = 0;
o1.changey = 0;
o2.changex = 0;
o2.changey = 0;
}
function circle (o){
var x = o.x;
var y = o.y;
var a = o.angle;
var d = o.d;
ctx.beginPath();
ctx.arc(o.x,o.y,o.r,0,2*Math.PI);
ctx.fillStyle = "hsla("+o.c+",100%,50%, "+o.a+")";
ctx.fill();
o.x = x;
o.y = y;
o.angle = a;
o.d = d;
}
function createData(num){
for(var i=0; i<num; i++){
allCircles.push({
"x": rand(w),
"changex": rand(10),
"y":rand(h),
"changex": rand(10),
"w": randn(w),
"h": randn(h),
"d": 1,
"a": 1,
"angle": 0,
"changle":15,
"c":216,
"r": 50
}
)
}
}
function createDataTwo(num){
for(var i=0; i<num; i++){
allCircles.push({
"x": rand(w),
"changex": rand(10),
"y":rand(h),
"changex": rand(10),
"w": randn(w),
"h": randn(h),
"d": 1,
"a": 1,
"angle": 0,
"changle":15,
"c":rand(90),
"r": 50
}
)
}
}
function turn(o,angle){
if(angle != undefined){
o.changle=angle;
};
o.angle+=o.changle;
}
function forward(o,d){
var changeX;
var changeY;
var oneDegree = Math.PI/180;
if(d != undefined){
o.d = d;
};
changeX= o.d*Math.cos(o.angle*oneDegree);
changeY = o.d*Math.sin(o.angle*oneDegree);
o.x+=changeX;
o.y+=changeY;
}
function randn(r){
var result = Math.random()*r - r/2
return result
}
function randi(r) {
var result = Math.floor(Math.random()*r);
return result
}
function rand(r){
return Math.random()*r
}
function setUpCanvas(){
canvas = document.querySelector("#myCanvas");
ctx = canvas.getContext("2d");
canvas.width = w;
canvas.height = h;
canvas.style.border = "5px solid orange"
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, w, h);
}
console.log("assi4")
HTML
<html>
<head>
<link rel="stylesheet" type="text/css" href="../modules.css">
</head>
<body>
<div id="container">
<h1>Click the Blue Circles Only</h1>
<canvas id = "myCanvas"></canvas>
</div>
<script src="assi5.js"></script>
</body>
</html>
CSS
#container {
margin: auto;
width: 75%;
text-align: center;
}
You can use cancelAnimationFrame to stop the animation when a non-blue circle is clicked
You need to pass it a reference to the frame ID returned from requestAnimationFrame for it to work.
In order to tell if a circle was clicked, you need to check the coordinates of each circle against the coordinates of the click.
I have an example below if you had your blue circles in an array "blue", and other circles in array "other", the ID returned by requestAnimationFrame as "frame".
The check function returns the number of blue circles hit (the points scored) and if any other circles were hit, it stops the animation.
getCoords returns the coordinates of the click on the canvas from the click event.
canvas.addEventListener('click', event=>{
points += check(getCoords(event), blue, other, frame);
document.getElementById('points').textContent = points;
})
function check({x, y}, blue, other, frame) {
other.filter(circle=>circle.isWithin(x, y))
.length && cancelAnimationFrame(frame); // This is where animation stops
return blue.filter(circle=>circle.isWithin(x, y)).length;
}
function getCoords(event) {
const canvas = event.target;
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return { x, y };
}
I have an example that works below where I changed the circles to the result of a function rather than an inline object, and moved the functions you use on them into their own class. You don't have to do this, but I find it a lot easier to understand.
function main() {
const canvas = document.getElementById('canvas');
const context = canvas.getContext("2d");
const clear = () => context.clearRect(0, 0, canvas.width, canvas.height);
const blue = new Array(2).fill().map(() => new Circle(context, 216));
const other = new Array(10).fill().map(() => new Circle(context));
let circles = [...blue, ...other];
let frame = 0;
let points = 0;
// Move the circle a bit and check if it needs to bounce
function update(circle) {
circle.forward(1)
circle.turn(30, true)
circle.collisionTestArray(circles)
circle.bounce();
}
// Main game loop, clear canvas, update circle positions, draw circles
function loop() {
clear();
circles.filter(circle => circle.free).forEach(update);
circles.forEach(circle => circle.draw());
frame = requestAnimationFrame(loop);
}
loop();
canvas.addEventListener('click', event => {
points += check(getCoords(event), blue, other, frame, circles);
document.getElementById('points').textContent = points;
})
}
function check({ x, y }, blue, other, frame) {
other.filter(circle => circle.isWithin(x, y))
// .map(circle=>circle.toggle())
.length && cancelAnimationFrame(frame); // This is where animation stops
return blue.filter(circle => circle.isWithin(x, y)).length;
}
function getCoords(event) {
const canvas = event.target;
const rect = canvas.getBoundingClientRect()
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return { x, y };
}
main();
function Circle(context, c) {
const randn = r => rand(r) - r / 2;
const randi = r => Math.floor(randi(r));
const rand = r => Math.random() * r;
// These are for easily stopping and starting a circle;
this.free = true;
this.stop = () => this.free = false;
this.release = () => this.free = true;
this.toggle = () => this.free = !this.free;
const {
width,
height
} = context.canvas;
// These are the same properties you were using in your code
this.x = rand(width);
this.changex = rand(10);
this.y = rand(height);
this.changey = rand(10);
this.w = randn(width);
this.h = randn(height);
this.d = 1;
this.a = 1;
this.angle = 0;
this.changle = 15;
this.c = c || rand(90); // This is the only difference between blue and other circles
this.r = 50;
// These next functions you had in your code, I just moved them into the circle definition
this.draw = () => {
const { x, y, r, c } = this;
context.beginPath();
context.arc(x, y, r, 0, 2 * Math.PI);
context.fillStyle = "hsla(" + c + ",100%,50%, 1)";
context.fill();
}
this.bounce = () => {
const { x, y, angle } = this;
if (x > width || x < 0) {
this.turn(180 - 2 * angle);
}
if (y > height || y < 0) {
this.turn(360 - 2 * angle);
}
}
this.turn = (angle, random = false) => {
this.changle = random ? randn(angle) : angle;
this.angle += this.changle;
}
this.forward = d => {
this.d = d;
this.x += this.d * Math.cos(this.angle * Math.PI / 180);
this.y += this.d * Math.sin(this.angle * Math.PI / 180);
}
this.collisionTestArray = a => a
.filter(circle => circle != this)
.forEach(circle => this.collision(circle));
this.collision = circle => {
var differencex = Math.abs(this.x - circle.x);
var differencey = Math.abs(this.y - circle.y);
var hdif = Math.sqrt(differencex ** 2 + differencey ** 2);
if (hdif < this.r + circle.r) {
if (differencex < differencey) {
this.turn(180 - 2 * this.angle);
circle.turn(180 - 2 * circle.angle);
} else {
this.turn(360 - 2 * this.angle);
circle.turn(360 - 2 * circle.angle);
}
this.turn(180);
circle.turn(180);
}
}
// These 2 functions I added to check if the circle was clicked
this.distanceFrom = (x, y) => Math.sqrt((this.x - x) ** 2 + (this.y - y) ** 2);
this.isWithin = (x, y) => this.r > this.distanceFrom(x, y);
}
#canvas {
border: 5px solid orange;
}
#container {
margin: auto;
width: 75%;
text-align: center;
}
<div id="container">
<h1>Click the Blue Circles Only</h1>
<canvas id="canvas" width="1000" height="600"></canvas>
<p>
Points: <span id="points">0</span>
</p>
</div>
using OOP is better in this situation and will save you a lot of time
I have written the OOP version of your game, I wrote it in harry so you may find some bugs but it is good as a starting point
const canvas = document.querySelector("canvas")
const ctx = canvas.getContext("2d")
let h = canvas.height = 600
let w = canvas.width = 800
const numberOfCircles = 20
let circles = []
// running gameover
let gameStatus = "running"
let score = 0
canvas.addEventListener("click", (e) => {
if(gameStatus === "gameOver") {
document.location.reload()
return;
}
const mouse = {x: e.offsetX, y: e.offsetY}
for(let circle of circles) {
if(distance(mouse, circle) <= circle.radius) {
if(circle.color == "blue") {
gameStatus = "running"
score += 1
} else {
gameStatus = "gameOver"
}
}
}
})
class Circle {
constructor(x, y, color, angle) {
this.x = x
this.y = y
this.color = color
this.radius = 15
this.angle = angle
this.speed = 3
}
draw(ctx) {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fill();
}
move(circles) {
this.check(circles)
this.x += Math.cos(this.angle) * this.speed
this.y += Math.sin(this.angle) * this.speed
}
check(circles) {
if(this.x + this.radius > w || this.x - this.radius < 0) this.angle += Math.PI
if(this.y + this.radius > h || this.y - this.radius < 0) this.angle += Math.PI
for(let circle of circles) {
if(circle === this) continue
if(distance(this, circle) <= this.radius + circle.radius) {
// invert angles or any other effect
// there are much better soultion for resolving colliusions
circle.angle += Math.PI / 2
this.angle += Math.PI / 2
}
}
}
}
}
setUp()
gameLoop()
function gameLoop() {
ctx.clearRect(0,0,w,h)
if(gameStatus === "gameOver") {
ctx.font = "30px Comic"
ctx.fillText("Game Over", w/2 - 150, h/2 - 100)
ctx.fillText("you have scored : " + score, w/2 - 150, h/2)
return;
}
ctx.font = "30px Comic"
ctx.fillText("score : " + score, 20, 30)
for (let i = 0; i < circles.length; i++) {
const cirlce = circles[i]
cirlce.draw(ctx)
cirlce.move(circles)
}
requestAnimationFrame(gameLoop)
}
function random(to, from = 0) {
return Math.floor(Math.random() * (to - from) + from)
}
function setUp() {
gameStatus = "running"
score = 0
circles = []
for (var i = 0; i < numberOfCircles; i++) {
const randomAngle = random(360) * Math.PI / 180
circles.push(new Circle(random(w, 20), random(h, 20), randomColor(), randomAngle))
}
}
function randomColor() {
const factor = random(10)
if(factor < 3) return "blue"
return `rgb(${random(255)}, ${random(255)}, ${random(100)})`
}
function distance(obj1, obj2) {
const xDiff = obj1.x - obj2.x
const yDiff = obj1.y - obj2.y
return Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2))
}
I'm building an animation with multiple drawn circles going left to right. However, the animation starts at the same time for all circles, and I need them to have a timed interval (for example 1 second) between each other. Any idea on how to manage this?
I've tried setInterval without success.
The drawing of circles is as follows:
function isPrime(num) {
for(var i = 2; i < num; i++)
if(num % i === 0) return false;
return num > 1;
}
const settings = {
width: 300,
height: 930,
radius: 13,
gap: 5,
circles: 30,
};
const canvas = document.getElementById("canvas");
canvas.width = settings.width;
canvas.height = settings.height;
const ctx = canvas.getContext("2d");
var randomNumber = [];
const circles = [...Array(settings.circles).keys()].map((i) => ({
number: randomNumber[i],
x: settings.radius,
y: settings.radius + (settings.radius * 2 + settings.gap) * i,
radius: settings.radius,
dx: 100, // This is the speed in pixels per second
dy: 0,
isPrime: isPrime(randomNumber[i]),
}));
function drawCircle(circle) {
i = 0;
if (circle.number > 0 && circle.number <= 10) {
ctx.strokeStyle = "#0b0bf1";
} else if (circle.number > 10 && circle.number <= 20) {
ctx.strokeStyle = "#f10b0b";
} else if (circle.number > 20 && circle.number <= 30) {
ctx.strokeStyle = "#0bf163";
} else if (circle.number > 30 && circle.number <= 40) {
ctx.strokeStyle = "#f1da0b";
} else if (circle.number > 40 && circle.number <= 50) {
ctx.strokeStyle = "#950bf1";
} else if (circle.number > 50 && circle.number <= 60) {
ctx.strokeStyle = "#0bf1e5";
}
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(circle.number, circle.x - 5, circle.y + 3);
}
function updateCircle(circle, dt) {
circle.x = clamp(
circle.x + circle.dx * dt,
circle.radius + 1,
settings.width - circle.radius - 1
);
circle.y = clamp(
circle.y + circle.dy * dt,
circle.radius + 1,
settings.height - circle.radius - 1
);
}
function animate() {
ctx.clearRect(0, 0, settings.width, settings.height);
circles.forEach(drawCircle);
requestAnimationFrame(animate);
}
animate();
update((dt) => circles.forEach((circle) => updateCircle(circle, dt)), 50);
I think you want to do like
let i = 0, l = circles.length;
let intv = setInterval(()=>{
drawCircle(circles[i++]);
if(i === l){
clearInterval(intv); intv = undefined;
}
}, 100); // change interval as desired
instead of circles.forEach(drawCircle);.
That requestAnimationFrame looks pretty useless like that too.
In my code I am detecting collisions between a box and the balls, but as soon as the first collision occurs the console shows the following error:
"message": "Uncaught TypeError: Cannot read property 'x' of undefined"
What is the error in my code? What is the meaning of the displayed error?
// Creating code for multiple balls as well as record the movement of the player
var canvas, cxt, h, w, n = 10,
i, mousePos;
var ball = []; // Empty array
var p = {
x: 0,
y: 0,
length: 30,
breath: 30,
color: 'black'
}
function init() {
canvas = document.querySelector('#style');
cxt = canvas.getContext('2d');
h = canvas.height;
w = canvas.width;
ballType(n);
// Add a mousemove event listener to the canvas
canvas.addEventListener('mousemove', mouseMoved);
main();
}
function mouseMoved(evt) {
mousePos = getMousePos(canvas, evt);
}
function getMousePos(canvas, evt) {
// Necessary work in the canvas coordinate system
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
function movePlayerWithMouse() {
if (mousePos !== undefined) {
p.x = mousePos.x;
p.y = mousePos.y;
}
}
function ballType(n) {
var cl = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink', 'cyan', 'grey', 'pink'];
for (i = 0; i < n; i++) {
var e = Math.floor(10 * Math.random());
f = {
x: 100,
y: 100,
radius: 5 + (30 * Math.random()), // Radius will be between 5 and 35
a: -5 + (10 * Math.random()), // Value of a will be between -5 and 5
b: -5 + (10 * Math.random()), //value of b will be between -5 and 5
color: cl[e]
}
ball.push(f);
}
}
function main() {
cxt.clearRect(0, 0, w, h);
player(p);
for (i = 0; i < n; i++) {
draw(ball[i]);
}
var l = 0
for (i = 0; i < n; i++) {
move(ball[i], l);
l++;
}
movePlayerWithMouse();
count();
requestAnimationFrame(main);
}
function draw(d) {
cxt.save();
cxt.translate(0, 0);
cxt.fillStyle = d.color;
cxt.beginPath();
cxt.arc(d.x, d.y, d.radius, 0, 2 * Math.PI);
cxt.fill();
cxt.restore();
}
function move(m, index) {
m.x += m.a;
m.y += m.b;
check(m);
testCollision(m, index);
}
function check(m) {
if ((m.x + m.radius) > w) { // Collision with the right wall
m.a = -m.a;
m.x = w - m.radius;
} else if ((m.x - m.radius) < 0) { // Collision with the left wall
m.a = -m.a;
m.x = m.radius;
}
if ((m.y + m.radius) > h) { // Collision with the top wall
m.b = -m.b;
m.y = h - m.radius;
} else if ((m.y - m.radius) < 0) { // Collision with the bottom surface
m.b = -m.b;
m.y = m.radius;
}
}
function player(p) {
cxt.save();
cxt.translate(0, 0);
cxt.fillStyle = p.color;
cxt.fillRect(p.x, p.y, p.length, p.breath);
cxt.restore();
}
// For testing the collision
function test(rx, ry, rw, rh, cx, cy, cr) {
var x0 = cx;
var y0 = cy;
if (x0 < rx) {
x0 = rx;
}
if (x0 > (rx + rw)) {
x0 = rx + rw;
}
if (y0 < ry) {
y0 = ry;
}
if (y0 > (ry + rh)) {
y0 = ry + rh;
}
return (((cx - x0) * (cx - x0) + (cy - y0) * (cy - y0)) < (cr * cr));
}
function testCollision(v, index) {
if (test(p.x, p.y, p.breath, p.length, v.x, v.y, v.radius)) {
ball.splice(index, 1); // Splice starts deleting the elements of array from the index given in the first parameter
// and the second parameter accepts the number of array elements to be deleted
}
}
function count() {
cxt.save();
if (ball.length == 0) {
cxt.fillText("You win", 20, 20);
} else {
cxt.fillText(ball.length, 20, 20);
}
cxt.restore();
}
#style {
border: 2px dotted black;
}
<body onload='init();'>
<div>
<canvas id='style' height='400' width='400'>
Your browser does not support canvas...
</canvas>
</div>
</body>
There is an error in your code as you are removing the object from array, but not reducing the n so just update the following:
function testCollision(v, index) {
if (test(p.x, p.y, p.breath, p.length, v.x, v.y, v.radius)) {
ball.splice(index, 1);
n--;
// Splice starts deleting the elements of array from the index given in the first parameter
// and the second parameter accepts the no. of array elements to be deleted
}
}
For the last step of this project, I want the growing circle to stop when it collides with another circle. The isOnCircle function already checks for this successfully when creating a new circle. However, when adding the condition !isOnCircle to my grow() function (line 61) it prevents any new circles from being added.
function grow() {
var a = circles[circles.length - 1];
if (!isOnCircle(a)){
a.radius += 1;
}}
Perhaps the circle is being created first, then in the check for collision, it's colliding with itself. Where else could I put the !isOnCircle check so that it gets checked at every radius increase and stops the grow function then?
check this
//set up canvas
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var circles = [];
//create circle
function create(location) {
circles.push({
x: location.x,
y: location.y,
radius: 10,
color: '#' + Math.floor(Math.random() * 16777215).toString(16)
});
}
//figure out mouse position
var rect = document.getElementById("canvas").getBoundingClientRect();
// Get canvas offset on page
var offset = {
x: rect.left,
y: rect.top
};
function isOnCanvas(a) {
if ((a.x >= 0 && a.x <= rect.width) && (a.y >= 0 && a.y <= rect.height)) {
return true;
}
return false;
}
function isOnCircle(a) {
var i = 0,
l = circles.length,
x, y, d, c;
for (; i < l; ++i) {
c = circles[i];
x = a.x - c.x;
y = a.y - c.y;
d = (a.radius || 10) + c.radius;
if (x * x + y * y <= d * d) {
return true;
}
}
return false;
}
// draw all circles
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < circles.length; i++) {
var p = circles[i];
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI);
ctx.fillStyle = p.color;
ctx.fill();
}
}
//make last drawn circle 1px bigger
function grow() {
var a = circles[circles.length - 1];
a.radius += 1;
}
//find percentage of canvas filled in
var totalSpace = canvas.width * canvas.height;
var totalFilled = function () {
total = 0;
for (var i = 0; i < circles.length; i++) {
var p = circles[i];
total += Math.PI * Math.pow(p.radius, 2);
}
return total;
console.log(total);
}
function findPercentage() {
return (totalFilled() / totalSpace) * 100;
}
function updateInfo() {
percentage = findPercentage();
document.getElementById("percentage").innerHTML = "You've filled in " + percentage.toFixed(1) + "%";
}
//do all the stuff
var animate = function () {
grow();
draw();
updateInfo();
}
//put this outside function so we can stop it later
var growLoop;
window.onmousedown = function (e) {
// get event location on page offset by canvas location
var location = {
x: e.pageX - offset.x,
y: e.pageY - offset.y
};
if (isOnCanvas(location) && !isOnCircle(location)) {
create(location);
draw();
updateInfo();
growLoop = setInterval(animate, 100);
}
};
window.onmouseup = function () {
clearInterval(growLoop);
}
window.onmouseout = function () {
clearInterval(growLoop);
}
it's colliding with itself.
Probably. You definitely should avoid that in the collision detection:
function isOnCircle(a) {
var l = circles.length,
x, y, d, c;
for (var i = 0; i < l; ++i) {
c = circles[i];
if (a == c) // add these
continue; // two lines!
x = a.x - c.x;
y = a.y - c.y;
d = (a.radius || 10) + c.radius;
if (x * x + y * y <= d * d) {
return true;
}
}
return false;
}
It is colliding with itself. Since you know the current circle will always be the last one in circles you can modify isOnCircle like this:
l = circles.length - 1,
so that it won't check against the current circle.
http://jsfiddle.net/SeAGU/91/