How to get alert Box with sound? - javascript

I am trying to get alert like pop-up with sound when the balls collide. When I use alert function it gets freeze and it will keep on showing alert. As far as now i tried to get alert like in text. But how to get alert with sound(eg.beep) ?
I have attached the code snippet. Can anyone help to get this.
Thanks in advance
const ctx = document.getElementById("Canvas").getContext("2d");
const containerR = 150;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx.globalAlpha = 0.8
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const balls = [
getBall(size / 2, size - 30, 1, 1, 8, "Green"),
getBall(size / 3, size - 50, 1, 1, 8, "Green"),
getBall(size / 4, size - 60, 1, 1, 8, "Green"),
getBall(size / 2, size / 5, 1, 1, 8, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
//console.clear(); // Clear console test messages
mydiv.textContent =" ";
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
if (a.collider) { // If ball has a collider:
mydiv.textContent = ("Alert");
//console.log(`${a.color[0]} → ← ${a.collider.color[0]}`);
}
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
engine();
<style>
canvas {
background: #eee;
margin: 0 auto;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
}
</style>
<html>
<canvas id="Canvas"></canvas>
<div id="mydiv"></div>
<div id="y"></div>
<div id="dx"></div>
<div id="dy"></div>
</html>

You can't use normal .alert() without halting the javascript execution so you have to stick with HTML alert/prompt (there is many libraries out there provide neat and customizable UI), for the beep sound, you can just use based64 data with data URI, this snippet here wont work because the SO snippets are sandbox, here is a working fiddle you can improve.
the beep sound example based on this answer here
Update: I added the custom alert and its based on this example from bluebird implementation, there is custom HTML and CSS, and Javascript PromptDialog(), basically this Javascript object implementation is asynchronous ans doesn't block the execution, please refer to the linked bluebird article for more information since its a guide on how to build it and it explains everything better than me.
function beep() {
var snd = new Audio("data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
snd.play();
}
var noop = function() {
return this;
};
function UserCanceledError() {
this.name = 'UserCanceledError';
this.message = 'User canceled dialog';
}
UserCanceledError.prototype = Object.create(Error.prototype);
function Dialog() {
this.setCallbacks(noop, noop);
}
Dialog.prototype.setCallbacks = function(okCallback, cancelCallback) {
this._okCallback = okCallback;
return this;
};
Dialog.prototype.waitForUser = function() {
var _this = this;
return new Promise(function(resolve, reject) {
_this.setCallbacks(resolve, reject);
});
};
Dialog.prototype.show = noop;
Dialog.prototype.hide = noop;
function PromptDialog() {
Dialog.call(this);
this.el = document.getElementById('dialog');
this.messageEl = this.el.querySelector('.message');
this.okButton = this.el.querySelector('button.ok');
this.attachDomEvents();
}
PromptDialog.prototype = Object.create(Dialog.prototype);
PromptDialog.prototype.attachDomEvents = function() {
var _this = this;
this.okButton.addEventListener('click', function() {
_this.hide();
console.log('Ok clicked!!');
});
};
PromptDialog.prototype.show = function(message) {
this.messageEl.innerHTML = '' + message;
this.el.className = '';
return this;
};
PromptDialog.prototype.hide = function() {
this.el.className = 'hidden';
return this;
};
const ctx = document.getElementById("Canvas").getContext("2d");
const containerR = 150;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx.globalAlpha = 0.8
var prompt = new PromptDialog();
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const balls = [
getBall(size / 2, size - 30, 1, 1, 8, "Green"),
getBall(size / 3, size - 50, 1, 1, 8, "Green"),
getBall(size / 4, size - 60, 1, 1, 8, "Green"),
getBall(size / 2, size / 5, 1, 1, 8, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
//console.clear(); // Clear console test messages
mydiv.textContent =" ";
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
if (a.collider) { // If ball has a collider:
mydiv.textContent = ("Alert");
beep();
prompt.show('collision detected')
.waitForUser()
.then(function(name) {
output.innerHTML = '' + name;
})
.catch(function(e) {
console.log('Unknown error', e);
})
.finally(function() {
prompt.hide();
});
//console.log(`${a.color[0]} → ← ${a.collider.color[0]}`);
}
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
engine();
canvas {
background: #eee;
margin: 0 auto;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
}
#dialog {
width: 200px;
margin: auto;
border: thin solid black;
padding: 10px;
background: lightgreen;
}
.hidden {
display: none;
}
<canvas id="Canvas"></canvas>
<div id="mydiv"></div>
<div id="y"></div>
<div id="dx"></div>
<div id="dy"></div>
<div id="dialog" class="hidden">
<div class="message"></div>
<div>
<button class="ok">Ok</button>
</div>
</div>

UPDATE: User interaction
you can add an audio tag and play it for sound
note: in newer browser chrome ^50, you need user interaction to audio tag work fine
const startBtn = document.getElementById("start-btn")
const sound = document.getElementById("beeb-player")
const ctx = document.getElementById("Canvas").getContext("2d");
const containerR = 150;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx.globalAlpha = 0.8
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const balls = [
getBall(size / 2, size - 30, 1, 1, 8, "Green"),
getBall(size / 3, size - 50, 1, 1, 8, "Green"),
getBall(size / 4, size - 60, 1, 1, 8, "Green"),
getBall(size / 2, size / 5, 1, 1, 8, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
//console.clear(); // Clear console test messages
mydiv.textContent =" ";
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
if (a.collider) { // If ball has a collider:
mydiv.textContent = ("Alert");
sound.play()
//console.log(`${a.color[0]} → ← ${a.collider.color[0]}`);
}
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
startBtn.addEventListener('click', engine, {once:true})
<style>
canvas {
background: #eee;
margin: 0 auto;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
}
</style>
<html>
<button id="start-btn">start</button>
<canvas id="Canvas"></canvas>
<div id="mydiv"></div>
<div id="y"></div>
<div id="dx"></div>
<div id="dy"></div>
<audio src="http://soundbible.com/grab.php?id=1815&type=mp3" id="beeb-player"></audio>
</html>
you can add an audio tag and play it when the balls collide
function alertWithSound() {
const el = document.getElementById("beeb-player")
el.play()
}
<audio src="http://soundbible.com/grab.php?id=1815&type=mp3" id="beeb-player"></audio>

Related

Circle / Lines collision problem in Vanilla Javascript

i'm working on a pseudo Physics Engine for a simple top down game where i need collisions to be handled against various shapes but i'm having problems with Circle / Line collisions.
For a Circle collided with a single Line it works as intended, but when the circle collides with two lines at the same time (Concave edges) there comes the problem, the circle overlaps them both if i keep pressed the movement key (In this example case D).
I just need to know why it is happening, and maybe how to fix. Thanks if you can help!
WASD to move the Circle: https://jsfiddle.net/bh840rua/1/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Script</title>
</head>
<body>
<canvas id="canvas" width="1600px" height="900px"></canvas>
<script>
const canvas = document.getElementById("canvas")
const context = canvas.getContext("2d")
const keys = []
document.addEventListener("keydown", (e) => {
keys[e.keyCode] = true
})
document.addEventListener("keyup", (e) => {
keys[e.keyCode] = false
})
class Circle {
constructor(x, y, radius) {
this.center = new Vector(x, y)
this.radius = radius
this.velocity = new Vector(0, 0)
}
move() {
let collision = this.collide(line)
if (collision.collided) {
let collisionLine = new Line(collision.intersection.x, collision.intersection.y,
this.center.x, this.center.y)
let newForce = new Vector(collisionLine.vector.x, collisionLine.vector.y)
newForce.normalize()
this.center.x = collision.intersection.x + newForce.x * this.radius
this.center.y = collision.intersection.y + newForce.y * this.radius
}
collision = this.collide(line2)
if (collision.collided) {
let collisionLine = new Line(collision.intersection.x, collision.intersection.y,
this.center.x, this.center.y)
let newForce = new Vector(collisionLine.vector.x, collisionLine.vector.y)
newForce.normalize()
this.center.x = collision.intersection.x + newForce.x * this.radius
this.center.y = collision.intersection.y + newForce.y * this.radius
}
this.center.x += this.velocity.x
this.center.y += this.velocity.y
}
checkVelocity() {
let direction = [0, 0, 0, 0]
if (keys[65]) {
direction[0] = -1
} else direction[0] = 1
if (keys[68]) {
direction[1] = 1
} else direction[1] = -1
if (keys[87]) {
direction[2] = -1
} else direction[2] = 1
if (keys[83]) {
direction[3] = 1
} else direction[3] = -1
this.velocity.x = direction[0] + direction[1]
this.velocity.y = direction[2] + direction[3]
this.velocity.normalize()
}
collide(object) {
let dx31 = this.center.x - object.start.x
let dx21 = object.end.x - object.start.x
let dy31 = this.center.y - object.start.y
let dy21 = object.end.y - object.start.y
let d = ((dx21 * dx21) + (dy21 * dy21))
if (d != 0) d = ((dx31 * dx21) + (dy31 * dy21)) / d
if (d < 0.0) d = 0
if (d > 1.0) d = 1
let dx = this.center.x - (object.start.x + (dx21 * d))
let dy = this.center.y - (object.start.y + (dy21 * d))
let magnitude = Math.sqrt((dx * dx) + (dy * dy))
if (circle.radius >= magnitude) {
return {
collided: true,
intersection: {
x: this.center.x - dx,
y: this.center.y - dy
}
}
} else return {
collided: false
}
}
render() {
context.strokeStyle = "black"
context.lineWidth = "1"
context.beginPath()
context.arc(this.center.x, this.center.y, this.radius, 0, Math.PI * 2)
context.stroke()
context.closePath()
}
}
class Line {
constructor(startX, startY, endX, endY) {
this.start = new Vector(startX, startY)
this.end = new Vector(endX, endY)
}
get vector() {
return new Vector(this.end.x - this.start.x, this.end.y - this.start.y)
}
get angleRad() {
return Math.atan(this.vector.y / this.vector.x)
}
get angleDeg() {
return Math.atan(this.vector.y / this.vector.x) * 180 / Math.PI
}
get magnitude() {
return Math.sqrt(Math.pow(this.vector.x, 2) + Math.pow(this.vector.y, 2))
}
render() {
context.strokeStyle = "black"
context.lineWidth = "1"
context.beginPath()
context.moveTo(this.start.x, this.start.y)
context.lineTo(this.end.x, this.end.y)
context.stroke()
context.closePath()
}
}
class Vector {
constructor(x, y) {
this.x = x
this.y = y
}
get angleRad() {
return Math.atan(this.y / this.x)
}
get angleDeg() {
return Math.atan(this.y / this.x) * 180 / Math.PI
}
get magnitude() {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2))
}
normalize() {
if (this.magnitude) {
let len = this.magnitude
this.x /= len
this.y /= len
}
}
}
const line = new Line(208, 200, 400, 200)
const line2 = new Line(208, 300, 400, 200)
const circle = new Circle(228, 150, 25);
let gameLoop = (timestamp) => {
context.clearRect(0, 0, canvas.width, canvas.height)
circle.checkVelocity()
circle.move()
line.render()
line2.render()
circle.render()
window.requestAnimationFrame(gameLoop)
}
window.requestAnimationFrame(gameLoop)
</script>
</body>
</html>

How do I rotate, scale and translate on Html5 Canvas?

I've tried for the last few days without too much success to rotate, scale and translate shapes on the canvas.
I've read everything I could find on internet about similar issues but still I cannot seem to be able to adapt it to my own problem.
If everything is drawn on the same scale, I can still drag and drop. If I rotate the shapes, then the mouseOver is messed up since the world coordinates don't correspond anymore with the shape coordinates.
If I scale, then it's impossible to select any shape.
I look at my code and do not understand what I'm doing wrong.
I read some really nice and detailed stackoverflow solutions to similar problems.
For example, user #blindman67 made a suggestion of using a setTransform helper and a getMouseLocal helper for getting the coordinates of the mouse relative to the transformed shape.
inverse transform matrix
I spent some time with that and could not fix my issue.
Here is an example of what I tried. Any suggestion is appreciated.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth - 40;
canvas.height = window.innerHeight - 60;
const canvasBounding = canvas.getBoundingClientRect();
const offsetX = canvasBounding.left;
const offsetY = canvasBounding.top;
let scale = 1;
let selectedShape = '';
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let mouseIsDown = false;
let mouseIsMovingShape = false;
let selectedTool = 'SELECT';
let shapes = {};
const selectButton = document.getElementById('select');
const rectangleButton = document.getElementById('rectangle');
canvas.addEventListener('mousedown', canvasMouseDown);
canvas.addEventListener('mouseup', canvasMouseUp);
canvas.addEventListener('mousemove', canvasMouseMove);
function canvasMouseDown(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
startX = mouseX;
startY = mouseY;
mouseIsDown = true;
selectedShape = '';
if (selectedTool === 'SELECT') {
for (const shapeId in shapes) {
const shape = shapes[shapeId];
if (shape.mouseIsOver(mouseX, mouseY)) {
selectedShape = shape.id;
shapes[shape.id].isSelected = true;
} else {
shapes[shape.id].isSelected = false;
}
}
}
draw();
}
function canvasMouseUp(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
endX = mouseX;
endY = mouseY;
mouseIsDown = false;
const tooSmallShape = Math.abs(endX) - startX < 1 || Math.abs(endY) - startY < 1;
if (tooSmallShape) {
return;
}
if (selectedTool === 'RECTANGLE') {
const newShape = new Shape(selectedTool.toLowerCase(), startX, startY, endX, endY);
shapes[newShape.id] = newShape;
selectedShape = '';
setActiveTool('SELECT');
}
draw();
}
function canvasMouseMove(e) {
e.preventDefault();
const mouseX = e.clientX - offsetX;
const mouseY = e.clientY - offsetY;
const dx = e.movementX;
const dy = e.movementY;
if (mouseIsDown) {
draw();
if (selectedTool === 'SELECT' && selectedShape !== '') {
const shape = shapes[selectedShape];
shape.x += dx;
shape.y += dy;
}
if (selectedTool === 'RECTANGLE') {
drawShapeGhost(mouseX, mouseY);
}
}
}
function draw() {
clear();
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.drawShape(ctx);
}
}
function clear() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.fillRect(0, 0, canvas.width, canvas.height)
}
function drawShapeGhost(x, y) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.strokeRect(startX, startY, x - startX, y - startY);
ctx.stroke();
}
function setActiveTool(tool) {
selectedTool = tool;
if (tool === 'RECTANGLE') {
rectangleButton.classList.add('active');
selectButton.classList.remove('active');
selectedTool = tool;
}
if (tool === 'SELECT') {
rectangleButton.classList.remove('active');
selectButton.classList.add('active');
selectedTool = tool;
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180;
};
class Shape {
constructor(shapeType, startX, startY, endX, endY, fill, stroke) {
this.id = shapeType + Date.now();
this.type = shapeType;
this.x = startX;
this.y = startY;
this.width = Math.abs(endX - startX);
this.height = Math.abs(endY - startY);
this.fill = fill || 'rgba(149, 160, 178, 0.8)';
this.stroke = stroke || 'rgba(0, 0, 0, 0.8)';
this.rotation = 0;
this.isSelected = false;
this.scale = 1;
}
drawShape(ctx) {
switch (this.type) {
case 'rectangle':
this._drawRectangle(ctx);
break;
}
}
_drawRectangle(ctx) {
ctx.save();
ctx.scale(this.scale, this.scale);
ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
ctx.rotate(degreesToRadians(this.rotation));
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
if (this.stroke !== null) {
ctx.strokeStyle = this.stroke;
ctx.strokeWidth = 1;
ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height);
ctx.stroke();
}
if (this.isSelected) {
ctx.strokeStyle = 'rgba(254, 0, 0, 1)';
ctx.strokeWidth = 1;
ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height)
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
mouseIsOver(mouseX, mouseY) {
if (this.type === 'rectangle') {
return (mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height);
}
}
}
const menu = document.getElementById('menu');
const rotation = document.getElementById('rotation');
const scaleSlider = document.getElementById('scale');
menu.addEventListener('click', onMenuClick);
rotation.addEventListener('input', onRotationChange);
scaleSlider.addEventListener('input', onScaleChange);
function onMenuClick(e) {
const tool = e.target.dataset.tool;
if (tool && tool === 'RECTANGLE') {
rectangleButton.classList.add('active');
selectButton.classList.remove('active');
selectedTool = tool;
}
if (tool && tool === 'SELECT') {
rectangleButton.classList.remove('active');
selectButton.classList.add('active');
selectedTool = tool;
}
}
function onRotationChange(e) {
if (selectedShape !== '') {
shapes[selectedShape].rotation = e.target.value;
draw();
}
}
function onScaleChange(e) {
scale = e.target.value;
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.scale = scale;
}
draw();
}
function setTransform(ctx, x, y, scaleX, scaleY, rotation) {
const xDx = Math.cos(rotation);
const xDy = Math.sin(rotation);
ctx.setTransform(xDx * scaleX, xDy * scaleX, -xDy * scaleY, xDx * scaleY, x, y);
}
function getMouseLocal(mouseX, mouseY, x, y, scaleX, scaleY, rotation) {
const xDx = Math.cos(rotation);
const xDy = Math.sin(rotation);
const cross = xDx * scaleX * xDx * scaleY - xDy * scaleX * (-xDy) * scaleY;
const ixDx = (xDx * scaleY) / cross;
const ixDy = (-xDy * scaleX) / cross;
const iyDx = (xDy * scaleY) / cross;
const iyDy = (xDx * scaleX) / cross;
mouseX -= x;
mouseY -= y;
const localMouseX = mouseX * ixDx + mouseY * iyDx;
const localMouseY = mouseX * ixDy + mouseY * iyDy;
return {
x: localMouseX,
y: localMouseY,
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180
};
function radiansToDegrees(radians) {
return radians * 180 / Math.PI
};
let timer;
function debounce(fn, ms) {
clearTimeout(timer);
timer = setTimeout(() => fn(), ms);
}
canvas {
margin-top: 1rem;
border: 1px solid black;
}
button {
border: 1px solid #adadad;
background-color: transparent;
}
.active {
background-color: lightblue;
}
.menu {
display: flex;
}
.d-flex {
display: flex;
margin-left: 2rem;
}
.rotation input {
margin-left: 1rem;
max-width: 50px;
}
<div id="menu" class="menu">
<button id="select" data-tool="SELECT">select</button>
<button id="rectangle" data-tool="RECTANGLE">rectangle</button>
<div class="d-flex">
<label for="rotation">rotation </label>
<input type="number" id="rotation" value="0">
</div>
<div class="d-flex">
<label for="scale">scale </label>
<input type="range" step="0.1" min="0.1" max="10" value="1" name="scale" id="scale">
</div>
</div>
<canvas id="canvas"></canvas>
If I have time tomorrow I will try to implement the following to your code but I can provide you with a working example of how to get mouse collision precision on a rotated rectangle. I had the same struggle with this and finally found a good explanation and code that I was able to get to work. Check out this website
Now for my own implementation I did not use the method on that website to get my vertices. As you'll see in my code I have a function called updateCorners() in my Square class. I also have objects called this.tl.x and this.tl.y (for each corner).
The formulas are what I use to get vertices of a translated and rotated rectangle and the corner objects are what are used to determine collision. From there I used the distance() function (Pythagorean theorem), the triangleArea() function, and then the clickHit() function which I renamed to collision() and changed some things.
Example in the snippet below
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let shapes = [];
let mouse = {
x: null,
y: null
}
canvas.addEventListener('mousemove', e => {
mouse.x = e.x - canvas.getBoundingClientRect().x;
mouse.y = e.y - canvas.getBoundingClientRect().y;
})
class Square {
constructor(x, y, w, h, c) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.c = c;
this.a = 0;
this.r = this.a * (Math.PI/180);
this.cx = this.x + this.w/2;
this.cy = this.y + this.h/2;
//used to track corners
this.tl = {x: 0, y: 0};
this.tr = {x: 0, y: 0};
this.br = {x: 0, y: 0};
this.bl = {x: 0, y: 0};
}
draw() {
ctx.save();
ctx.translate(this.x, this.y)
ctx.rotate(this.r);
ctx.fillStyle = this.c;
ctx.fillRect(-this.w/2,-this.h/2,this.w,this.h);
ctx.restore();
}
updateCorners() {
this.a += 0.1
this.r = this.a * (Math.PI/180);
let cos = Math.cos(this.r);
let sin = Math.sin(this.r)
//updates Top Left Corner
this.tl.x = (this.x-this.cx)*cos - (this.y-this.cy)*sin+(this.cx-this.w/2);
this.tl.y = (this.x-this.cx)*sin + (this.y-this.cy)*cos+(this.cy-this.h/2)
//updates Top Right Corner
this.tr.x = ((this.x+this.w)-this.cx)*cos - (this.y-this.cy)*sin+(this.cx-this.w/2)
this.tr.y = ((this.x+this.w)-this.cx)*sin + (this.y-this.cy)*cos+(this.cy-this.h/2)
//updates Bottom Right Corner
this.br.x = ((this.x+this.w)-this.cx)*cos - ((this.y+this.h)-this.cy)*sin+(this.cx-this.w/2)
this.br.y = ((this.x+this.w)-this.cx)*sin + ((this.y+this.h)-this.cy)*cos+(this.cy-this.h/2)
//updates Bottom Left Corner
this.bl.x = (this.x-this.cx)*cos - ((this.y+this.h)-this.cy)*sin+(this.cx-this.w/2)
this.bl.y = (this.x-this.cx)*sin + ((this.y+this.h)-this.cy)*cos+(this.cy-this.h/2)
}
}
let square1 = shapes.push(new Square(250, 70, 25, 25, 'red'));
let square2 = shapes.push(new Square(175,210, 100, 50, 'blue'));
let square3 = shapes.push(new Square(50,100, 30, 50, 'purple'));
let square4 = shapes.push(new Square(140,120, 120, 20, 'pink'));
//https://joshuawoehlke.com/detecting-clicks-rotated-rectangles/
//pythagorean theorm using built in javascript hypot
function distance(p1, p2) {
return Math.hypot(p1.x-p2.x, p1.y-p2.y);
}
//Heron's formula used to determine area of triangle
//in the collision() function we will break the rectangle into triangles
function triangleArea(d1, d2, d3) {
var s = (d1 + d2 + d3) / 2;
return Math.sqrt(s * (s - d1) * (s - d2) * (s - d3));
}
function collision(mouse, rect) {
//area of rectangle
var rectArea = Math.round(rect.w * rect.h);
// Create an array of the areas of the four triangles
var triArea = [
// mouse posit checked against tl-tr
triangleArea(
distance(mouse, rect.tl),
distance(rect.tl, rect.tr),
distance(rect.tr, mouse)
),
// mouse posit checked against tr-br
triangleArea(
distance(mouse, rect.tr),
distance(rect.tr, rect.br),
distance(rect.br, mouse)
),
// mouse posit checked against tr-bl
triangleArea(
distance(mouse, rect.br),
distance(rect.br, rect.bl),
distance(rect.bl, mouse)
),
// mouse posit checked against bl-tl
triangleArea(
distance(mouse, rect.bl),
distance(rect.bl, rect.tl),
distance(rect.tl, mouse)
)
];
let triArea2 = Math.round(triArea.reduce(function(a,b) { return a + b; }, 0));
if (triArea2 > rectArea) {
return false;
}
return true;
}
function animate() {
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = 'black';
ctx.fillText('x: '+mouse.x+',y: '+mouse.y, 50, 50);
for (let i=0; i< shapes.length; i++) {
shapes[i].draw();
shapes[i].updateCorners();
if (collision(mouse, shapes[i])) {
shapes[i].c = 'red';
} else {
shapes[i].c = 'green'
}
}
requestAnimationFrame(animate)
}
animate();
<canvas id="canvas"></canvas>
I'm sure there's many other ways to do this but this is what I was able to understand and get to work. I haven't really messed with scale so I can't help much there.
UPDATE:
Here is a snippet using the method you wanted. Now you can rotate, scale, and translate and still click inside the shape. Be aware I changed your mouse to a global mouse object vice making it a variable in every mouse... function.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth - 40;
canvas.height = window.innerHeight - 60;
const canvasBounding = canvas.getBoundingClientRect();
const offsetX = canvasBounding.left;
const offsetY = canvasBounding.top;
let scale = 1;
let selectedShape = "";
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let mouseIsDown = false;
let mouseIsMovingShape = false;
let selectedTool = "SELECT";
let localMouse = { x: null, y: null };
let mouse = { x: null, y: null };
let shapes = {};
const selectButton = document.getElementById("select");
const rectangleButton = document.getElementById("rectangle");
canvas.addEventListener("mousedown", canvasMouseDown);
canvas.addEventListener("mouseup", canvasMouseUp);
canvas.addEventListener("mousemove", canvasMouseMove);
function canvasMouseDown(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
startX = mouse.x;
startY = mouse.y;
mouseIsDown = true;
selectedShape = "";
if (selectedTool === "SELECT") {
for (const shapeId in shapes) {
const shape = shapes[shapeId];
if (shape.mouseIsOver()) {
selectedShape = shape.id;
shapes[shape.id].isSelected = true;
} else {
shapes[shape.id].isSelected = false;
}
}
}
draw();
}
function canvasMouseUp(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
endX = mouse.x;
endY = mouse.y;
mouseIsDown = false;
const tooSmallShape =
Math.abs(endX) - startX < 1 || Math.abs(endY) - startY < 1;
if (tooSmallShape) {
return;
}
if (selectedTool === "RECTANGLE") {
const newShape = new Shape(
selectedTool.toLowerCase(),
startX,
startY,
endX,
endY
);
shapes[newShape.id] = newShape;
selectedShape = "";
setActiveTool("SELECT");
}
draw();
}
function canvasMouseMove(e) {
e.preventDefault();
mouse.x = e.clientX - offsetX;
mouse.y = e.clientY - offsetY;
const dx = e.movementX;
const dy = e.movementY;
if (mouseIsDown) {
draw();
if (selectedTool === "SELECT" && selectedShape !== "") {
const shape = shapes[selectedShape];
shape.x += dx;
shape.y += dy;
}
if (selectedTool === "RECTANGLE") {
drawShapeGhost(mouse.x, mouse.y);
}
}
}
function draw() {
clear();
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.drawShape(ctx);
}
}
function clear() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawShapeGhost(x, y) {
ctx.strokeStyle = "rgba(0, 0, 0, 0.5)";
ctx.strokeRect(startX, startY, x - startX, y - startY);
ctx.stroke();
}
function setActiveTool(tool) {
selectedTool = tool;
if (tool === "RECTANGLE") {
rectangleButton.classList.add("active");
selectButton.classList.remove("active");
selectedTool = tool;
}
if (tool === "SELECT") {
rectangleButton.classList.remove("active");
selectButton.classList.add("active");
selectedTool = tool;
}
}
function degreesToRadians(degrees) {
return (Math.PI * degrees) / 180;
}
class Shape {
constructor(shapeType, startX, startY, endX, endY, fill, stroke) {
this.id = shapeType + Date.now();
this.type = shapeType;
this.x = startX;
this.y = startY;
this.width = Math.abs(endX - startX);
this.height = Math.abs(endY - startY);
this.fill = fill || "rgba(149, 160, 178, 0.8)";
this.stroke = stroke || "rgba(0, 0, 0, 0.8)";
this.rotation = 0;
this.isSelected = false;
this.scale = { x: 1, y: 1 };
}
drawShape(ctx) {
switch (this.type) {
case "rectangle":
this._drawRectangle(ctx);
break;
}
}
_drawRectangle(ctx) {
ctx.save();
setTransform(
this.x + this.width / 2,
this.y + this.height / 2,
this.scale.x,
this.scale.y,
degreesToRadians(this.rotation)
);
if (this.fill) {
ctx.fillStyle = this.fill;
ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height);
}
if (this.stroke !== null) {
ctx.strokeStyle = this.stroke;
ctx.strokeWidth = 1;
ctx.strokeRect(
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
ctx.stroke();
}
if (this.isSelected) {
ctx.strokeStyle = "rgba(254, 0, 0, 1)";
ctx.strokeWidth = 1;
ctx.strokeRect(
-this.width / 2,
-this.height / 2,
this.width,
this.height
);
ctx.stroke();
ctx.closePath();
}
ctx.restore();
}
mouseIsOver() {
localMouse = getMouseLocal(
mouse.x,
mouse.y,
this.x + this.width / 2,
this.y + this.height / 2,
this.scale.x,
this.scale.y,
degreesToRadians(this.rotation)
);
if (this.type === "rectangle") {
if (
localMouse.x > 0 - this.width / 2 &&
localMouse.x < 0 + this.width / 2 &&
localMouse.y < 0 + this.height / 2 &&
localMouse.y > 0 - this.height / 2
) {
return true;
}
}
}
}
const menu = document.getElementById("menu");
const rotation = document.getElementById("rotation");
const scaleSlider = document.getElementById("scale");
menu.addEventListener("click", onMenuClick);
rotation.addEventListener("input", onRotationChange);
scaleSlider.addEventListener("input", onScaleChange);
function onMenuClick(e) {
const tool = e.target.dataset.tool;
if (tool && tool === "RECTANGLE") {
rectangleButton.classList.add("active");
selectButton.classList.remove("active");
selectedTool = tool;
}
if (tool && tool === "SELECT") {
rectangleButton.classList.remove("active");
selectButton.classList.add("active");
selectedTool = tool;
}
}
function onRotationChange(e) {
if (selectedShape !== "") {
shapes[selectedShape].rotation = e.target.value;
draw();
}
}
function onScaleChange(e) {
scale = e.target.value;
for (const shapeId in shapes) {
const shape = shapes[shapeId];
shape.scale.x = scale;
shape.scale.y = scale;
}
draw();
}
function setTransform(x, y, sx, sy, rotate) {
var xdx = Math.cos(rotate); // create the x axis
var xdy = Math.sin(rotate);
ctx.setTransform(xdx * sx, xdy * sx, -xdy * sy, xdx * sy, x, y);
}
function getMouseLocal(mouseX, mouseY, x, y, sx, sy, rotate) {
var xdx = Math.cos(rotate); // create the x axis
var xdy = Math.sin(rotate);
var cross = xdx * sx * xdx * sy - xdy * sx * -xdy * sy;
var ixdx = (xdx * sy) / cross; // create inverted x axis
var ixdy = (-xdy * sx) / cross;
var iydx = (xdy * sy) / cross; // create inverted y axis
var iydy = (xdx * sx) / cross;
mouseX -= x;
mouseY -= y;
var localMouseX = mouseX * ixdx + mouseY * iydx;
var localMouseY = mouseX * ixdy + mouseY * iydy;
return { x: localMouseX, y: localMouseY };
}
function radiansToDegrees(radians) {
return (radians * 180) / Math.PI;
}
let timer;
function debounce(fn, ms) {
clearTimeout(timer);
timer = setTimeout(() => fn(), ms);
}
canvas {
margin-top: 1rem;
border: 1px solid black;
}
button {
border: 1px solid #adadad;
background-color: transparent;
}
.active {
background-color: lightblue;
}
.menu {
display: flex;
}
.d-flex {
display: flex;
margin-left: 2rem;
}
.rotation input {
margin-left: 1rem;
max-width: 50px;
}
<div id="menu" class="menu">
<button id="select" data-tool="SELECT">select</button>
<button id="rectangle" data-tool="RECTANGLE">rectangle</button>
<button id="triangle" data-tool="TRIANGLE">triangle</button>
<div class="d-flex">
<label for="rotation">rotation </label>
<input type="number" id="rotation" value="0">
</div>
<div class="d-flex">
<label for="scale">scale </label>
<input type="range" step="0.1" min="0.1" max="10" value="1" name="scale" id="scale">
</div>
</div>
<canvas id="canvas"></canvas>

How can I make my sine wave in javascript work properly?

I am trying to make a sine wave on a canvas that would be filled with a certain color. I was successful with creating the wave animation. However, i can't seem to get the fill right. You can see the way my code works in the snippet below
const canvas = document.querySelector('canvas')
const gui = new dat.GUI()
const c = canvas.getContext('2d')
canvas.width = innerWidth
canvas.height = innerHeight
const wave = {
y: canvas.height / 2,
length: 0.01,
amplitude: 150,
frequency: 0.01
}
const strokeColor = {
h: 200,
s: 50,
l: 50
}
const backgroundColor = {
r: 255,
g: 50,
b: 255,
a: 1
}
const waveFolder = gui.addFolder('wave')
waveFolder.add(wave, 'y', 0, canvas.height)
waveFolder.add(wave, 'length', -0.01, 0.01)
waveFolder.add(wave, 'amplitude', -300, 300)
waveFolder.add(wave, 'frequency', -0.01, 1)
const strokeFolder = gui.addFolder('stroke')
strokeFolder.add(strokeColor, 'h', 0, 255)
strokeFolder.add(strokeColor, 's', 0, 100)
strokeFolder.add(strokeColor, 'l', 0, 100)
const backgroundFolder = gui.addFolder('background')
backgroundFolder.add(backgroundColor, 'r', 0, 255)
backgroundFolder.add(backgroundColor, 'g', 0, 255)
backgroundFolder.add(backgroundColor, 'b', 0, 255)
backgroundFolder.add(backgroundColor, 'a', 0, 1)
let increment = 0
function drawSineWave() {
c.fillStyle = `rgba(${backgroundColor.r},${backgroundColor.g},${backgroundColor.b},${backgroundColor.a})`
c.clearRect(0, 0, canvas.width, canvas.height)
increment++
c.beginPath()
c.moveTo(0, canvas.height / 2)
c.lineTo(0, canvas.height / 2)
c.lineTo(0, canvas.height)
c.lineTo(canvas.width, canvas.height)
c.lineTo(canvas.width, canvas.height / 2)
c.moveTo(0, canvas.height / 2)
for (let i = 0; i < canvas.width; i++) {
c.lineTo(i, wave.y + Math.sin(increment / 50) * wave.amplitude * Math.sin(i * wave.length + wave.frequency))
}
c.lineTo(canvas.width, canvas.height / 2)
c.strokeStyle = `hsl(${strokeColor.h},${strokeColor.s}%,${strokeColor.l}%)`
c.fill()
c.stroke()
}
function animate() {
requestAnimationFrame(animate)
drawSineWave()
}
animate()
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
canvas {
width: 100%;
height: 100%;
}
<head>
<title>Waves</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<canvas></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<script src="index.js"></script>
</body>
The way I want the wave to look is this:
Can someone please help me achieve the desired result? Thanks!
You just need to draw the bottom half in the correct order
The result is
function drawSineWave() {
c.fillStyle = `rgba(${backgroundColor.r},${backgroundColor.g},${backgroundColor.b},${backgroundColor.a})`
c.clearRect(0, 0, canvas.width, canvas.height)
increment++
c.beginPath()
for (let i = 0; i < canvas.width; i++) {
c.lineTo(i, wave.y + Math.sin(increment / 50) * wave.amplitude * Math.sin(i * wave.length + wave.frequency))
}
c.strokeStyle = `hsl(${strokeColor.h},${strokeColor.s}%,${strokeColor.l}%)`
c.stroke(); // draw stroke along wave top only
c.lineTo(canvas.width, canvas.height) // bottom right
c.lineTo(0, canvas.height) // bottom left
c.fill()
}
Example
I played about with it a bit.
const canvas = document.querySelector('canvas')
const gui = new dat.GUI()
const c = canvas.getContext('2d')
canvas.width = innerWidth
canvas.height = innerHeight
const wave = {
offset: canvas.height/2,
speed: 1,
amplitude:50,
frequency: 1,
}
var disFreq = wave.frequency, disFreqChase = 0;
var disAmp = wave.amplitude, disAmpChase = 0;
var disY = wave.offset, disYChase = 0;
var disSp = wave.speed, disSpChase = 0;
const strokeColor = {width: 2, h:200,s:50, l:50 }
const backgroundColor = { r:255, g:50, b:255, a:1 }
const waveFolder = gui.addFolder('wave')
waveFolder.add(wave,'speed',-1,1); // peeks per second
waveFolder.add(wave,'offset',0,canvas.height);
waveFolder.add(wave,'amplitude',-100,100)
waveFolder.add(wave,'frequency',0.5,10); // frequency also defines wave length
// Value is fraction of canvas width
const strokeFolder = gui.addFolder('stroke')
strokeFolder.add(strokeColor,'width',0,18)
strokeFolder.add(strokeColor,'h',0,255)
strokeFolder.add(strokeColor,'s',0,100)
strokeFolder.add(strokeColor,'l',0,100)
const backgroundFolder = gui.addFolder('background')
backgroundFolder.add(backgroundColor,'r',0,255)
backgroundFolder.add(backgroundColor,'g',0,255)
backgroundFolder.add(backgroundColor,'b',0,255)
backgroundFolder.add(backgroundColor,'a',0,1)
let increment = 0
function drawSineWave() {
// smooth out changes
disFreq += disFreqChase = (disFreqChase += (wave.frequency - disFreq)*0.2)* 0.2;
disAmp += disAmpChase = (disAmpChase += (wave.amplitude - disAmp) * 0.2) * 0.2;
disY += disYChase = (disYChase += (wave.offset - disY) * 0.2) * 0.2;
disSp += disSpChase = (disSpChase += (wave.speed - disSp) * 0.2) * 0.2;
c.lineWidth = strokeColor.width;
c.fillStyle = `rgba(${backgroundColor.r},${backgroundColor.g},${backgroundColor.b},${backgroundColor.a})`
c.clearRect(0,0,canvas.width,canvas.height)
//increment++
increment += (disSp / 60);
c.beginPath()
const h = canvas.height;
for (let i=0;i<canvas.width;i++){
const a = (disAmp / 200) * h;
const x = ((i / canvas.width) * disFreq) % 1 + increment;
const p = x * Math.PI * 2;
c.lineTo(i, disY + Math.sin(p)*a);
}
c.strokeStyle = `hsl(${strokeColor.h},${strokeColor.s}%,${strokeColor.l}%)`
c.stroke()
c.lineTo(canvas.width,canvas.height)
c.lineTo(0, canvas.height)
c.fill()
}
function animate(){
requestAnimationFrame(animate)
drawSineWave()
}
animate()
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
canvas{
width: 100%;
height: 100%;
}
<canvas></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<script src="index.js"></script>
</body>

Align circles and Balls are not bouncing

I am trying to place a big circle in the center of the screen and four other circles on the right side. How can I achieve this? Also, there should be balls bouncing inside the small circles on the right, and only one ball is inside each of them instead of several. An alert should be triggered by the big main circle. From the small circles, there should be no alert. How can I solve this problem? I am thankful for any help. Thanks in advance.
var noop = function() {
return this;
};
function UserCanceledError() {
this.name = 'UserCanceledError';
this.message = 'User canceled dialog';
}
UserCanceledError.prototype = Object.create(Error.prototype);
function Dialog() {
this.setCallbacks(noop, noop);
}
Dialog.prototype.setCallbacks = function(okCallback, cancelCallback) {
this._okCallback = okCallback;
return this;
};
Dialog.prototype.waitForUser = function() {
var _this = this;
return new Promise(function(resolve, reject) {
_this.setCallbacks(resolve, reject);
});
};
Dialog.prototype.show = noop;
Dialog.prototype.hide = noop;
function PromptDialog() {
Dialog.call(this);
this.el = document.getElementById('dialog');
this.messageEl = this.el.querySelector('.message');
this.okButton = this.el.querySelector('button.ok');
this.attachDomEvents();
}
PromptDialog.prototype = Object.create(Dialog.prototype);
PromptDialog.prototype.attachDomEvents = function() {
var _this = this;
this.okButton.addEventListener('click', function() {
_this.hide();
console.log('Ok clicked!!');
});
};
PromptDialog.prototype.show = function(message) {
this.messageEl.innerHTML = '' + message;
this.el.className = '';
return this;
};
PromptDialog.prototype.hide = function() {
this.el.className = 'hidden';
return this;
};
const ctx = document.getElementById("Canvas").getContext("2d");
const containerR = 150;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx.globalAlpha = 0.8;
//Adding fourcircles to the right
const ctx1 = document.getElementById("Canvas1").getContext("2d");
const ctx2 = document.getElementById("Canvas2").getContext("2d");
const ctx3 = document.getElementById("Canvas3").getContext("2d");
const ctx4 = document.getElementById("Canvas4").getContext("2d");
const containerR2 = 80;
const size2 = containerR2 * 2
ctx1.canvas.width = ctx1.canvas.height = size2;
ctx2.canvas.width = ctx2.canvas.height = size2;
ctx3.canvas.width = ctx3.canvas.height = size2;
ctx4.canvas.width = ctx4.canvas.height = size2;
ctx1.globalAlpha = 0.8;
ctx2.globalAlpha = 0.8;
ctx3.globalAlpha = 0.8;
ctx4.globalAlpha = 0.8;
var prompt = new PromptDialog();
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const balls = [
getBall(size / 2, size - 30, 0.1, 0.1, 8, "Green"),
getBall(size / 3, size - 50, 0.1, 0.1, 8, "Green"),
getBall(size / 4, size - 60, 0.1, 0.1, 8, "Green"),
getBall(size / 2, size / 5, 0.1, 0.1, 8, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
ctx1.beginPath();
ctx1.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx1.fillStyle = ball.collider ? "red" : ball.color;
ctx1.fill();
ctx1.closePath();
ctx2.beginPath();
ctx2.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx2.fillStyle = ball.collider ? "red" : ball.color;
ctx2.fill();
ctx2.closePath();
ctx3.beginPath();
ctx3.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx3.fillStyle = ball.collider ? "red" : ball.color;
ctx3.fill();
ctx3.closePath();
ctx4.beginPath();
ctx4.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx4.fillStyle = ball.collider ? "red" : ball.color;
ctx4.fill();
ctx4.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
//console.clear(); // Clear console test messages
mydiv.textContent =" ";
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx1.clearRect(0, 0, ctx1.canvas.width, ctx1.canvas.height);
ctx2.clearRect(0, 0, ctx2.canvas.width, ctx2.canvas.height);
ctx3.clearRect(0, 0, ctx3.canvas.width, ctx3.canvas.height);
ctx4.clearRect(0, 0, ctx4.canvas.width, ctx4.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
if (a.collider) { // If ball has a collider:
//mydiv.textContent = ("Alert");
beep();
prompt.show('ALERT!!!!! Not Maintaining Distance')
.waitForUser()
.then(function(name) {
output.innerHTML = '' + name;
})
.catch(function(e) {
console.log('Unknown error', e);
})
.finally(function() {
prompt.hide();
});
//console.log(`${a.color[0]} → ← ${a.collider.color[0]}`);
}
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
engine();
canvas {
background: #eee;
margin: 0 auto;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
}
.row {
display: flex;
}
.container1 {
display: flex;
flex-direction: column;
margin-left: 70%;
margin-top: 8%;
float: right;
}
#Canvas1, #Canvas2, #Canvas3, #Canvas4 {
background: #eee;
border-radius: 50%;
border: solid 1px #000;
margin: 4px;
}
<div class="column">
<canvas id="Canvas"></canvas>
</div>
<div class="container1">
<div class="row">
<canvas id="Canvas1"></canvas>
<div><p>abc</p></div>
<canvas id="Canvas2"></canvas>
<div><p>def</p></div>
</div>
<div class="row">
<canvas id="Canvas3"></canvas>
<div><p>hij</p></div>
<canvas id="Canvas4"></canvas>
<div><p>klm</p></div>
</div>
</div>
<div id="mydiv"></div>
<div id="y"></div>
<div id="dx"></div>
<div id="dy"></div>
<div id="dialog" class="hidden">
<div class="message"></div>
<div>
<button class="ok">OK</button>
</div>
</div>
There are a lot of errors in the provided code:
beep() fonction is not defined, it prevents the alert system to work.
there is only one list of the same 4 balls for the 5 canvas, so you see the same 4 balls in all canvas.
updatePos uses containerR that is a global variable (radius of the big circle), calculations can only be done for the big circle.
drawBall draws the same single ball in the 5 contexts
Fixed code, with random initial positions for balls :
function PromptDialog(dialog) {
this.el = document.getElementById(dialog);
this.messageEl = this.el.querySelector('.message');
this.okButton = this.el.querySelector('button.ok');
this.attachDomEvents();
}
PromptDialog.prototype.attachDomEvents = function() {
this.okButton.addEventListener('click', () => {
this.hide();
//console.log('Ok clicked!!');
});
};
PromptDialog.prototype.show = function(message) {
this.messageEl.innerHTML = '' + message;
this.el.className = '';
};
PromptDialog.prototype.hide = function() {
this.el.className = 'hidden';
};
var prompt = new PromptDialog('dialog');
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const drawBall = (ball, ctx) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
}
const updatePos = (ball, containerR) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
function makeArea(domid, radius, ballsNumber, alerts) {
const ctx = document.getElementById(domid).getContext("2d");
const containerR = radius;
const size = radius * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx.globalAlpha = 0.8;
const balls = [];
const speed = 0.1;
for(var i=0 ; i<ballsNumber ; ++i) {
const r = Math.random()*radius*0.5;
const t = Math.random()*Math.PI*2;
const t2 = Math.random()*Math.PI*2;
balls.push(
getBall(
radius + Math.cos(t)*r,
radius + Math.sin(t)*r,
Math.cos(t2)*speed,
Math.sin(t2)*speed,
8,
"Green")
);
}
return {
ctx: ctx,
radius: radius,
balls: balls,
alerts:alerts
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
const areas = [
makeArea("Canvas", 150, 4, true),
makeArea("Canvas1", 80, 4, false),
makeArea("Canvas2", 80, 4, false),
makeArea("Canvas3", 80, 4, false),
makeArea("Canvas4", 80, 4, false)
];
function engine() {
//console.clear(); // Clear console test messages
areas.forEach((area) =>{
const ctx = area.ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
area.balls.forEach((a, ai) => {
a.collider = undefined;
area.balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
if (a.collider && area.alerts) { // If ball has a collider:
//beep();
prompt.show('ALERT!!!!! Not Maintaining Distance');
//console.log(`${a.color[0]} → ← ${a.collider.color[0]}`);
}
updatePos(a, area.radius);
drawBall(a, ctx);
});
});
requestAnimationFrame(engine);
}
engine();
canvas {
background: #eee;
margin: 0 auto;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
}
.row {
display: flex;
}
.hidden {
display:none;
}
#Canvas1, #Canvas2, #Canvas3, #Canvas4 {
background: #eee;
border-radius: 50%;
border: solid 1px #000;
margin: 4px;
}
<div class="row">
<div>
<canvas id="Canvas"></canvas>
</div>
<div>
<div class="row">
<canvas id="Canvas1"></canvas>
<div><p>abc</p></div>
<canvas id="Canvas2"></canvas>
<div><p>def</p></div>
</div>
<div class="row">
<canvas id="Canvas3"></canvas>
<div><p>hij</p></div>
<canvas id="Canvas4"></canvas>
<div><p>klm</p></div>
</div>
</div>
</div>
<div id="dialog" class="hidden">
<div class="message"></div>
<div>
<button class="ok">OK</button>
</div>
</div>

How to align 4 circles

I am trying to align these 4 circles ( top 2 circle below that 2 circle). I tried changing the margin style it fails and it is creating 4 circles vertically. I need 2 circles at the top below that 2 circle should be there at the right side of the webpage. Can anyone help how to align these 4 circle horizontally 2*2.
const ctx = document.getElementById("Canvas").getContext("2d");
const ctx2 = document.getElementById("Canvas2").getContext("2d");
const ctx3 = document.getElementById("Canvas3").getContext("2d");
const ctx4 = document.getElementById("Canvas4").getContext("2d");
const containerR = 80;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx2.canvas.width = ctx2.canvas.height = size;
ctx3.canvas.width = ctx3.canvas.height = size;
ctx4.canvas.width = ctx4.canvas.height = size;
ctx.globalAlpha = 0.6;
ctx2.globalAlpha = 0.8;
ctx3.globalAlpha = 0.8;
ctx4.globalAlpha = 0.8;
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const balls = [
getBall(size / 2, size - 30, -0.1, -0.1, 4, "Green"),
getBall(size / 3, size - 50, 0.1, 0.1, 4, "Green"),
getBall(size / 4, size - 60, -0.1, 0.1, 4, "Green"),
getBall(size / 2, size / 5, 0.1, 0.1, 4, "Green"),
getBall(size / 2, size / 5, 0.1, -0.1, 4, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
ctx2.beginPath();
ctx2.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx2.fillStyle = ball.collider ? "red" : ball.color;
ctx2.fill();
ctx2.closePath();
ctx3.beginPath();
ctx3.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx3.fillStyle = ball.collider ? "red" : ball.color;
ctx3.fill();
ctx3.closePath();
ctx4.beginPath();
ctx4.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx4.fillStyle = ball.collider ? "red" : ball.color;
ctx4.fill();
ctx4.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
//console.clear(); // Clear console test messages
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx2.clearRect(0, 0, ctx2.canvas.width, ctx2.canvas.height);
ctx3.clearRect(0, 0, ctx3.canvas.width, ctx3.canvas.height);
ctx4.clearRect(0, 0, ctx4.canvas.width, ctx4.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
engine();
<canvas id="Canvas"
style = "background: #eee;
margin-top: 5%;
margin-left: 60%;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
align-content: right;"></canvas>
<canvas id ="Canvas2"
style = "background: #eee;
margin-top: 8%;
margin-left: 60%;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
align-content: right;"></canvas>
<canvas id ="Canvas3"
style = "background: #eee;
margin-top: 6%;
margin-left: 75%;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
align-content: right;"></canvas>
<canvas id ="Canvas4"
style = "background: #eee;
margin-top: 6%;
margin-left: 75%;
margin-bottom: 50%;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
align-content: right;"></canvas>
To align the layout I used some CSS flexbox features, within these features it's possible to align them on all possible ways.
Learn more about flexbox here: https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Flexbox and see my example for a quick start.
const ctx = document.getElementById("Canvas").getContext("2d");
const ctx2 = document.getElementById("Canvas2").getContext("2d");
const ctx3 = document.getElementById("Canvas3").getContext("2d");
const ctx4 = document.getElementById("Canvas4").getContext("2d");
const containerR = 80;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx2.canvas.width = ctx2.canvas.height = size;
ctx3.canvas.width = ctx3.canvas.height = size;
ctx4.canvas.width = ctx4.canvas.height = size;
ctx.globalAlpha = 0.6;
ctx2.globalAlpha = 0.8;
ctx3.globalAlpha = 0.8;
ctx4.globalAlpha = 0.8;
const getBall = (x, y, dx, dy, r, color) => ({
x,
y,
dx,
dy,
r,
color
});
const balls = [
getBall(size / 2, size - 30, -0.1, -0.1, 4, "Green"),
getBall(size / 3, size - 50, 0.1, 0.1, 4, "Green"),
getBall(size / 4, size - 60, -0.1, 0.1, 4, "Green"),
getBall(size / 2, size / 5, 0.1, 0.1, 4, "Green"),
getBall(size / 2, size / 5, 0.1, -0.1, 4, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
ctx2.beginPath();
ctx2.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx2.fillStyle = ball.collider ? "red" : ball.color;
ctx2.fill();
ctx2.closePath();
ctx3.beginPath();
ctx3.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx3.fillStyle = ball.collider ? "red" : ball.color;
ctx3.fill();
ctx3.closePath();
ctx4.beginPath();
ctx4.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx4.fillStyle = ball.collider ? "red" : ball.color;
ctx4.fill();
ctx4.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
//console.clear(); // Clear console test messages
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx2.clearRect(0, 0, ctx2.canvas.width, ctx2.canvas.height);
ctx3.clearRect(0, 0, ctx3.canvas.width, ctx3.canvas.height);
ctx4.clearRect(0, 0, ctx4.canvas.width, ctx4.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
engine();
.row {
display: flex;
}
.container {
display: flex;
flex-direction: column;
}
#Canvas, #Canvas2, #Canvas3, #Canvas4 {
background: #eee;
border-radius: 50%;
border: solid 4px #000;
margin: 10px;
}
.canvas-holder {
display: flex;
flex-direction: column;
align-items: center;
}
<div class="container">
<div class="row">
<div class="canvas-holder">
<canvas id="Canvas"></canvas>
<span>Circle one</span>
</div>
<div class="canvas-holder">
<canvas id="Canvas2"></canvas>
<span>Circle two</span>
</div>
</div>
<div class="row">
<div class="canvas-holder">
<canvas id="Canvas3"></canvas>
<span>Circle tree</span>
</div>
<div class="canvas-holder">
<canvas id="Canvas4"></canvas>
<span>Circle four</span>
</div>
</div>
</div>
I suggest moving your styles to CSS to reduce repeated styles.
For example:
const ctx = document.getElementById("Canvas").getContext("2d");
const ctx2 = document.getElementById("Canvas2").getContext("2d");
const ctx3 = document.getElementById("Canvas3").getContext("2d");
const ctx4 = document.getElementById("Canvas4").getContext("2d");
const containerR = 80;
const size = containerR * 2
ctx.canvas.width = ctx.canvas.height = size;
ctx2.canvas.width = ctx2.canvas.height = size;
ctx3.canvas.width = ctx3.canvas.height = size;
ctx4.canvas.width = ctx4.canvas.height = size;
ctx.globalAlpha = 0.6;
ctx2.globalAlpha = 0.8;
ctx3.globalAlpha = 0.8;
ctx4.globalAlpha = 0.8;
const getBall = (x, y, dx, dy, r, color) => ({x, y, dx, dy, r, color});
const balls = [
getBall(size / 2, size - 30, -0.1, -0.1, 4, "Green"),
getBall(size / 3, size - 50, 0.1, 0.1, 4, "Green"),
getBall(size / 4, size - 60, -0.1, 0.1, 4, "Green"),
getBall(size / 2, size / 5, 0.1, 0.1, 4, "Green"),
getBall(size / 2, size / 5, 0.1, -0.1, 4, "Green"),
];
const drawBall = (ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx.fillStyle = ball.collider ? "red" : ball.color;
ctx.fill();
ctx.closePath();
ctx2.beginPath();
ctx2.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx2.fillStyle = ball.collider ? "red" : ball.color;
ctx2.fill();
ctx2.closePath();
ctx3.beginPath();
ctx3.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx3.fillStyle = ball.collider ? "red" : ball.color;
ctx3.fill();
ctx3.closePath();
ctx4.beginPath();
ctx4.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2, false);
ctx4.fillStyle = ball.collider ? "red" : ball.color;
ctx4.fill();
ctx4.closePath();
}
const updatePos = (ball) => {
ball.x += ball.dx;
ball.y += ball.dy;
const dx = ball.x - containerR;
const dy = ball.y - containerR;
if (Math.sqrt(dx * dx + dy * dy) >= containerR - ball.r) {
const v = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy);
const angleToCollisionPoint = Math.atan2(-dy, dx);
const oldAngle = Math.atan2(-ball.dy, ball.dx);
const newAngle = 2 * angleToCollisionPoint - oldAngle;
ball.dx = -v * Math.cos(newAngle);
ball.dy = v * Math.sin(newAngle);
}
}
const collides = (a, b) => (Math.hypot(Math.abs(a.x - b.x), Math.abs(a.y - b.y)) < (a.r + b.r));
function engine() {
//console.clear(); // Clear console test messages
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx2.clearRect(0, 0, ctx2.canvas.width, ctx2.canvas.height);
ctx3.clearRect(0, 0, ctx3.canvas.width, ctx3.canvas.height);
ctx4.clearRect(0, 0, ctx4.canvas.width, ctx4.canvas.height);
balls.forEach((a, ai) => {
a.collider = undefined;
balls.forEach((b, bi) => {
if (bi === ai) return; // Don't look at self
if (collides(a, b)) a.collider = b; // Store the colliding B ball
});
updatePos(a);
drawBall(a);
});
requestAnimationFrame(engine);
}
engine();
.canvas-wrapper {
display: flex;
flex-wrap: wrap;
}
.canvas-wrapper div {
width: 50%;
margin-bottom: 10px;
}
.canvas-wrapper canvas {
background: #eee;
border-radius: 50%;
box-shadow: 0 0 0 4px #000;
max-width: 80%;
box-sizing: border-box;
}
<div class="canvas-wrapper">
<div><canvas id="Canvas"></canvas></div>
<div><canvas id ="Canvas2"></canvas></div>
<div><canvas id ="Canvas3"></canvas></div>
<div><canvas id ="Canvas4"></canvas></div>
</div>

Categories