Summary:
In attempt to make a simple collision detection system between a moving rect and a falling circle, I would like to make it more realistic.
Main question:
-The main thing I would like to solve is detecting when the circle Object is hitting the corner of the rect and in return having the circle bounce based off of that angle.
The Code:
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle();
}
function draw() {
background(75);
obstacle.display();
obstacle.update();
for (var i = 0; i < balls.length; i++) {
balls[i].display();
if (!RectCircleColliding(balls[i], obstacle)){
balls[i].update();
balls[i].edges();
}
//console.log(RectCircleColliding(balls[i], obstacle));
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY));
}
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
}
this.edges = function() {
if (this.y >= height - this.r) {
this.y = height - this.r;
this.velocity = this.velocity * -1;
this.gravity = this.gravity * 1.1;
}
}
}
function Obstacle() {
this.x = width - width;
this.y = height / 2;
this.w = 200;
this.h = 25;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
function RectCircleColliding(Ball, Obstacle) {
// define obstacle borders
var oRight = Obstacle.x + Obstacle.w;
var oLeft = Obstacle.x;
var oTop = Obstacle.y;
var oBottom = Obstacle.y + Obstacle.h;
//compare ball's position (acounting for radius) with the obstacle's border
if (Ball.x + Ball.r > oLeft) {
if (Ball.x - Ball.r < oRight) {
if (Ball.y + Ball.r > oTop) {
if (Ball.y - Ball.r < oBottom) {
let oldY = Ball.y;
Ball.y = oTop - Ball.r;
Ball.velocity = Ball.velocity * -1;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return (true);
}
}
}
}
return false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>
Expected output:
I would like to see the falling circles bounce off the rectangle with respect to where they are hitting on the rectangle.
If the circles hit the corners they should bounce differently versus hitting dead center.
Prerequisite
The ball's velocity must be a vector (XY components), not just a single number.
1. Determine if the circle might hit a side or a corner
Obtain the components of the vector from the center of the rectangle to the circle, and check it against the rectangle's dimensions:
// Useful temporary variables for later use
var hx = 0.5 * obstacle.w;
var hy = 0.5 * obstacle.h;
var rx = obstacle.x + hx;
var ry = obstacle.y + hy;
// displacement vector
var dx = ball.x - rx;
var dy = ball.y - ry;
// signs
var sx = dx < -hx ? -1 : (dx > hx ? 1 : 0);
var sy = dy < -hy ? -1 : (dy > hy ? 1 : 0);
If both sx, sy are non-zero, the ball might hit a corner, otherwise it might hit a side.
2. Determine whether the circle collides
Multiply each sign by the respective half-dimension:
// displacement vector from the nearest point on the rectangle
var tx = sx * (Math.abs(dx) - hx);
var ty = sy * (Math.abs(dy) - hy);
// distance from p to the center of the circle
var dc = Math.hypot(tx, ty);
if (dc <= ball.r) {
/* they do collide */
}
3. Determine the collision normal vector
(tx, ty) are the components of the normal vector, but only if the ball's center is outside the rectangle:
// epsilon to account for numerical imprecision
const EPSILON = 1e-6;
var nx = 0, ny = 0, nl = 0;
if (sx == 0 && sy == 0) { // center is inside
nx = dx > 0 ? 1 : -1;
ny = dy > 0 ? 1 : -1;
nl = Math.hypot(nx, ny);
} else { // outside
nx = tx;
ny = ty;
nl = dc;
}
nx /= nl;
ny /= nl;
4. Resolve any "penetration"
(No immature jokes please)
This ensures that the ball will never penetrate into the surface of the rectangle, which improves the visual quality of the collisions:
ball.x += (ball.r - dc) * nx;
ball.y += (ball.r - dc) * ny;
5. Resolve the collision
If the circle is travelling in the direction of the normal, don't resolve the collision as the ball might stick to the surface:
// dot-product of velocity with normal
var dv = ball.vx * nx + ball.vy * ny;
if (dv >= 0.0) {
/* exit and don't do anything else */
}
// reflect the ball's velocity in direction of the normal
ball.vx -= 2.0 * dv * nx;
ball.vy -= 2.0 * dv * ny;
Working JS snippet
const GRAVITY = 250.0;
function Ball(x, y, r) {
this.x = x;
this.y = y;
this.r = r;
this.vx = 0;
this.vy = 0;
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.collidePage = function(b) {
if (this.vy > 0 && this.y + this.r >= b) {
this.y = b - this.r;
this.vy = -this.vy;
}
}
this.updatePosition = function(dt) {
this.x += this.vx * dt;
this.y += this.vy * dt;
}
this.updateVelocity = function(dt) {
this.vy += GRAVITY * dt;
}
}
function Obstacle(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle(0, height / 2, 200, 25);
}
const DT = 0.05;
function draw() {
background(75);
obstacle.update();
obstacle.display();
for (var i = 0; i < balls.length; i++) {
balls[i].updatePosition(DT);
balls[i].collidePage(height);
ResolveRectCircleCollision(balls[i], obstacle);
balls[i].updateVelocity(DT);
balls[i].display();
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY, 15));
}
const EPSILON = 1e-6;
function ResolveRectCircleCollision(ball, obstacle) {
var hx = 0.5 * obstacle.w, hy = 0.5 * obstacle.h;
var rx = obstacle.x + hx, ry = obstacle.y + hy;
var dx = ball.x - rx, dy = ball.y - ry;
var sx = dx < -hx ? -1 : (dx > hx ? 1 : 0);
var sy = dy < -hy ? -1 : (dy > hy ? 1 : 0);
var tx = sx * (Math.abs(dx) - hx);
var ty = sy * (Math.abs(dy) - hy);
var dc = Math.hypot(tx, ty);
if (dc > ball.r)
return false;
var nx = 0, ny = 0, nl = 0;
if (sx == 0 && sy == 0) {
nx = dx > 0 ? 1 : -1; ny = dy > 0 ? 1 : -1;
nl = Math.hypot(nx, ny);
} else {
nx = tx; ny = ty;
nl = dc;
}
nx /= nl; ny /= nl;
ball.x += (ball.r - dc) * nx; ball.y += (ball.r - dc) * ny;
var dv = ball.vx * nx + ball.vy * ny;
if (dv >= 0.0)
return false;
ball.vx -= 2.0 * dv * nx; ball.vy -= 2.0 * dv * ny;
return true;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
I'll try to present a solution, which keeps as may from the original code as possible. The solution intends to be a evolution of the code presented in the question.
Add a sideward movement (selv.sideV) to the Ball object, which is initialized by 0:
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.sideV = 0
// ...
}
Move the ball to the side in update, by the sideward movement and reduce the sideward movment:
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
this.x += this.sideV;
this.sideV *= 0.98;
}
Create function for an intersection test of 2 boxes:
function IsectRectRect(l1, r1, t1, b1, l2, r2, t2, b2) {
return l1 < r2 && l2 < r1 && t1 < b2 && t2 < b1;
}
And a function which can calculate the refection vector R of an incident vector V to a the normal vector of surface N (the reflection like a billiard ball):
function reflect( V, N ) {
R = V.copy().sub(N.copy().mult(2.0 * V.dot(N)));
return R;
}
When the Ball collides with the Obstacle, then you've to handle 3 situations.
The Ball fully hits the top of the Obstacle: IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB)
The Ball hits the left edge of the Obstacle: IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB)
The Ball hits the right edge of the Obstacle: IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB)
In each case the normal vector for the reflection has to be calculated. This is the vector from either the top or the edge of the Obstacle to the center of the Ball.
Use the function reflect to bounce the Ball on the Obstacle:
function RectCircleColliding(Ball, Obstacle) {
let oL = Obstacle.x;
let oR = Obstacle.x + Obstacle.w;
let oT = Obstacle.y;
let oB = Obstacle.y + Obstacle.h;
let bL = Ball.x - Ball.r;
let bR = Ball.x + Ball.r;
let bT = Ball.y - Ball.r;
let bB = Ball.y + Ball.r;
let isect = false;
let hitDir = createVector(0, 1);
if ( IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB) ) {
isect = true;
} else if ( IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oL, oT))
isect = hitDir.mag() < Ball.r;
} else if ( IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oR, oT))
isect = hitDir.mag() < Ball.r;
}
if ( isect ) {
let dir = createVector(Ball.sideV, Ball.velocity);
R = reflect(dir, hitDir.normalize());
Ball.velocity = R.y;
Ball.sideV = R.x;
let oldY = Ball.y;
Ball.y = oT - Ball.r;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return true;
}
return false;
}
See the example, where I applied the changes to your original code:
var balls = [];
var obstacle;
function setup() {
createCanvas(400, 400);
obstacle = new Obstacle();
}
function draw() {
background(75);
obstacle.display();
obstacle.update();
for (var i = 0; i < balls.length; i++) {
balls[i].display();
if (!RectCircleColliding(balls[i], obstacle)){
balls[i].update();
balls[i].edges();
}
//console.log(RectCircleColliding(balls[i], obstacle));
}
}
function mousePressed() {
balls.push(new Ball(mouseX, mouseY));
}
function Ball(x, y) {
this.x = x;
this.y = y;
this.r = 15;
this.gravity = 0.5;
this.velocity = 0;
this.sideV = 0
this.display = function() {
fill(255, 0, 100);
stroke(255);
ellipse(this.x, this.y, this.r * 2);
}
this.update = function() {
this.velocity += this.gravity;
this.y += this.velocity;
this.x += this.sideV;
this.sideV *= 0.98;
}
this.edges = function() {
if (this.y >= height - this.r) {
this.y = height - this.r;
this.velocity = this.velocity * -1;
this.gravity = this.gravity * 1.1;
}
}
}
function Obstacle() {
this.x = width - width;
this.y = height / 2;
this.w = 200;
this.h = 25;
this.display = function() {
fill(0);
stroke(255);
rect(this.x, this.y, this.w, this.h);
}
this.update = function() {
this.x++;
if (this.x > width + this.w /2) {
this.x = -this.w;
}
}
}
function IsectRectRect(l1, r1, t1, b1, l2, r2, t2, b2) {
return l1 < r2 && l2 < r1 && t1 < b2 && t2 < b1;
}
function reflect( V, N ) {
R = V.copy().sub(N.copy().mult(2.0 * V.dot(N)));
return R;
}
function RectCircleColliding(Ball, Obstacle) {
let oL = Obstacle.x;
let oR = Obstacle.x + Obstacle.w;
let oT = Obstacle.y;
let oB = Obstacle.y + Obstacle.h;
let bL = Ball.x - Ball.r;
let bR = Ball.x + Ball.r;
let bT = Ball.y - Ball.r;
let bB = Ball.y + Ball.r;
let isect = false;
let hitDir = createVector(0, 1);
if ( IsectRectRect(oL, oR, oT, oB, Ball.x, Ball.x, bT, bB) ) {
isect = true;
} else if ( IsectRectRect(oL, oL, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oL, oT))
isect = hitDir.mag() < Ball.r;
} else if ( IsectRectRect(oR, oR, oT, oB, bL, bR, bT, bB) ) {
hitDir = createVector(Ball.x, Ball.y).sub(createVector(oR, oT))
isect = hitDir.mag() < Ball.r;
}
if ( isect ) {
let dir = createVector(Ball.sideV, Ball.velocity);
R = reflect(dir, hitDir.normalize());
Ball.velocity = R.y;
Ball.sideV = R.x;
let oldY = Ball.y;
Ball.y = oT - Ball.r;
if (Ball.gravity < 2.0){
Ball.gravity = Ball.gravity * 1.1;
} else {
Ball.velocity = 0;
Ball.y = oldY;
}
return true;
}
return false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.js"></script>
Related
There is a falling balls code https://jsfiddle.net/d1e8x7wk/,
which is generated using canvas
Code also added to this editor
window.addEventListener('load', () => {
//---------------------------------------
// Set up ball options
//---------------------------------------
const imgBalls = [
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png',
'https://cdn.iconscout.com/icon/premium/png-256-thumb/basketball-2500972-2093649.png'
]
let ballCount = imgBalls.length, // How many balls
DAMPING = 0.4, // Damping
GRAVITY = 0.01, // Gravity strength
SPEED = 5, // Ball speed
ballAdditionTime = 100, // How fast are balls added
ballSrc = imgBalls, // Ball image source
topOffset = 400, // Adjust this for initial ball spawn point
xOffset = 0, // left offset
yOffset = 0, // bottom offset
ballDensity = 20, // How dense are the balls
ball_1_size = 200, // Ball 1 size
ball_2_size = 180, // Ball 2 size
ball_3_size = 62, // Ball 6 size
canvasWidth = 1500, // Canvas width
canvasHeight = 1000, // Canvas height
stackBall = true, // Stack the balls (or false is overlap)
ballsLoaded = 0,
stopAnimation = false;
//---------------------------------------
// Canvas globals
//---------------------------------------
let canvas,
ctx,
TWO_PI = Math.PI * 2,
balls = [],
vel_x,
vel_y;
let rect = {
x: 0,
y: 0,
w: canvasWidth,
h: canvasHeight
};
//---------------------------------------
// do the animation
//---------------------------------------
window.requestAnimFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, ballAdditionTime);
};
//---------------------------------------
// set up the ball
//---------------------------------------
const Ball = function(x, y, radius, num) {
this.x = x;
this.y = y;
this.px = x;
this.py = y;
this.fx = 0;
this.fy = 0;
this.radius = radius;
this.num = num;
this.angle = 0;
// Different ball sizes
let random = Math.round(Math.random() * imgBalls.length)
if (random === 0) {
this.width = ball_1_size;
this.height = ball_1_size;
if (stackBall) {
this.radius = ball_1_size / 2;
}
} else if (random === 1 || random === 2) {
this.width = ball_2_size;
this.height = ball_2_size;
if (stackBall) {
this.radius = ball_2_size / 2;
}
} else {
this.width = ball_3_size;
this.height = ball_3_size;
if (stackBall) {
this.radius = ball_3_size / 2;
}
}
ctx.rotate(this.angle * Math.PI / 180);
};
//---------------------------------------
// Apply the physics
//---------------------------------------
Ball.prototype.apply_force = function(delta) {
delta *= delta;
this.fy += GRAVITY;
this.x += this.fx * delta;
this.y += this.fy * delta;
this.fx = this.fy = 0;
};
//---------------------------------------
// Newtonian motion algorithm
//---------------------------------------
Ball.prototype.velocity = function() {
var nx = this.x * 2 - this.px;
var ny = this.y * 2 - this.py;
this.px = this.x;
this.py = this.y;
this.x = nx;
this.y = ny;
};
//---------------------------------------
// Ball prototype
//---------------------------------------
Ball.prototype.draw = function(ctx) {
img = new Image();
img.src = imgBalls[this.num];
if (stackBall) {
ctx.drawImage(
img,
this.x - this.radius - xOffset,
this.y - this.radius - xOffset,
this.width,
this.height
);
} else {
ctx.drawImage(
img,
this.x - xOffset,
this.y - yOffset,
this.width,
this.height
);
}
};
//---------------------------------------
// resolve collisions (ball on ball)
//---------------------------------------
let resolve_collisions = function(ip) {
let i = balls.length;
while (i--) {
let ball_1 = balls[i];
let n = balls.length;
while (n--) {
if (n == i) continue;
let ball_2 = balls[n];
let diff_x = ball_1.x - ball_2.x;
let diff_y = ball_1.y - ball_2.y;
let length = diff_x * diff_x + diff_y * diff_y;
let dist = Math.sqrt(length);
let real_dist = dist - (ball_1.radius + ball_2.radius);
if (real_dist < 0) {
let vel_x1 = ball_1.x - ball_1.px;
let vel_y1 = ball_1.y - ball_1.py;
let vel_x2 = ball_2.x - ball_2.px;
let vel_y2 = ball_2.y - ball_2.py;
let depth_x = diff_x * (real_dist / dist);
let depth_y = diff_y * (real_dist / dist);
ball_1.x -= depth_x * 0.5;
ball_1.y -= depth_y * 0.5;
ball_2.x += depth_x * 0.5;
ball_2.y += depth_y * 0.5;
if (ip) {
let pr1 = (DAMPING * (diff_x * vel_x1 + diff_y * vel_y1)) / length;
let pr2 = (DAMPING * (diff_x * vel_x2 + diff_y * vel_y2)) / length;
vel_x1 += pr2 * diff_x - pr1 * diff_x;
vel_x2 += pr1 * diff_x - pr2 * diff_x;
vel_y1 += pr2 * diff_y - pr1 * diff_y;
vel_y2 += pr1 * diff_y - pr2 * diff_y;
ball_1.px = ball_1.x - vel_x1;
ball_1.py = ball_1.y - vel_y1;
ball_2.px = ball_2.x - vel_x2;
ball_2.py = ball_2.y - vel_y2;
}
}
}
}
};
//---------------------------------------
// Bounce off the walls
//---------------------------------------
let check_walls = function() {
let i = balls.length;
while (i--) {
let ball = balls[i];
if (ball.x < ball.radius) {
let vel_x = ball.px - ball.x;
ball.x = ball.radius;
ball.px = ball.x - vel_x * DAMPING;
} else if (ball.x + ball.radius > canvas.width) {
vel_x = ball.px - ball.x;
ball.x = canvas.width - ball.radius;
ball.px = ball.x - vel_x * DAMPING;
}
// Ball is new. So don't do collision detection until it hits the canvas. (with an offset to stop it snapping)
if (ball.y > 100) {
if (ball.y < ball.radius) {
let vel_y = ball.py - ball.y;
ball.y = ball.radius;
ball.py = ball.y - vel_y * DAMPING;
} else if (ball.y + ball.radius > canvas.height) {
vel_y = ball.py - ball.y;
ball.y = canvas.height - ball.radius;
ball.py = ball.y - vel_y * DAMPING;
}
}
}
};
//---------------------------------------
// Add a ball to the canvas
//---------------------------------------
let add_ball = function(num) {
let x = Math.random() * canvas.width;
let y = Math.random() * canvas.height;
let r = 30 + Math.random() * ballDensity;
let diff_x = x;
let diff_y = y;
let dist = Math.sqrt(diff_x * diff_x + diff_y * diff_y);
balls.push(new Ball(x, y, r, num));
};
//---------------------------------------
// iterate balls
//---------------------------------------
let update = function() {
let iter = 1;
let delta = SPEED / iter;
while (iter--) {
let i = balls.length;
while (i--) {
balls[i].apply_force(delta);
balls[i].velocity();
}
resolve_collisions();
check_walls();
i = balls.length;
while (i--) {
balls[i].velocity();
let ball = balls[i];
}
resolve_collisions();
check_walls();
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
i = balls.length;
while (i--) {
balls[i].draw(ctx);
}
requestAnimFrame(update);
};
//---------------------------------------
// Set up the canvas object
//---------------------------------------
function doBalls() {
stopAnimation = false;
canvas = document.getElementById("balls");
ctx = canvas.getContext("2d");
let $canvasDiv = document.querySelector(".section");
function respondCanvas() {
canvas.height = $canvasDiv.clientHeight;
canvas.width = $canvasDiv.clientWidth;
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
respondCanvas();
ballAdd();
}
function ballAdd() {
let count = 0;
let timer = setInterval(function() {
addBallTimer();
}, 100);
let addBallTimer = function() {
add_ball(count % ballCount);
count++;
if (count === ballCount) {
stopTimer();
}
};
let stopTimer = function() {
clearInterval(timer);
};
update();
}
doBalls();
});
.section {
position: relative;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000;
}
<div class="section">
<canvas id="balls"></canvas>
</div>
I'm trying to make a rotation the balls
const Ball = function(x, y, radius, num) {
this.x = x;
this.y = y;
this.px = x;
this.py = y;
this.fx = 0;
this.fy = 0;
this.radius = radius;
this.num = num;
this.angle = 0;
// Different ball sizes
let random = Math.round(Math.random() * imgBalls.length)
if (random === 0) {
this.width = ball_1_size;
this.height = ball_1_size;
if (stackBall) {
this.radius = ball_1_size / 2;
}
} else if (random === 1 || random === 2) {
this.width = ball_2_size;
this.height = ball_2_size;
if (stackBall) {
this.radius = ball_2_size / 2;
}
} else {
this.width = ball_3_size;
this.height = ball_3_size;
if (stackBall) {
this.radius = ball_3_size / 2;
}
}
ctx.rotate(this.angle * Math.PI / 180);
};
After reading the information,
I wrote this code, but it does not work for me
ctx.rotate(this.angle * Math.PI / 180);
I must have misunderstood how to do this, tell me how to properly rotate the balls
Thanks in advance
You will need to
increase the angle at some point in your code
apply a rotation transform to rotate around each ball center before each drawImage call
ctx.save();
ctx.translate(+this.x, +this.y);
ctx.rotate(this.angle++ * Math.PI / 180);
ctx.translate(-this.x, -this.y);
ctx.drawImage
ctx.restore()
ctx.save() and ctx.restore() is called to restore the canvas transform after each draw call.
Please read
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/rotate#rotating_a_shape_around_its_center
for details on applying transforms to Canvas.
i have a simple physics engine to detect collisions between circles and paddles, right now i can set the paddle to be controlled by the mouse or not. but i want to be able to control one of the circles by mouse instead. at the bottom where bat[i].update() will control the specified paddle by that index. but i want to be able to control the circle by the mouse instead to further expand this engine. is there any way i can do this?
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false }
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 0;
var balls = []
var bats = []
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx, style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) };
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function distance(x1, y1, x2, y2) {
const xDist = x2 - x1;
const yDist = y2 - y1;
return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
}
// the ball
class ball {
constructor() {
this.r = 25
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 15
this.dy = 15
this.mass = 1
this.maxSpeed = 15
this.style = {
lineWidth: 12,
strokeStyle: "green"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if (y > canvas.height - this.r) {
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if (y < this.r) {
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if (x > canvas.width - this.r) {
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if (x < this.r) {
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if (speed > this.maxSpeed) { // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed - this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
class player {
constructor() {
this.r = 50
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 0.2
this.dy = 0.2
this.mass = 1
this.maxSpeed = 1000
this.style = {
lineWidth: 12,
strokeStyle: "blue"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
const ballShadow = {
r: 50,
x: 50,
y: 50,
dx: 0.2,
dy: 0.2,
}
//bat
class bat {
constructor(x, y, w, h) {
this.x = x
this.y = y
this.dx = 0
this.dy = 0
this.width = w
this.height = h
this.style = {
lineWidth: 2,
strokeStyle: "black",
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.strokeRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
function doBatBall(bat, ball) {
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if (s.x < 0) {
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if (s.y < 0) {
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if (s.dx > 0 && s.dy > 0) { return } // ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy) // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x + (s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y + (s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if (s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR) { // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if (!hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB) { // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if (!hit) { // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if (dist > radSqr) { return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x, cpy - s.y);
// is the distance greater than the ball speed then its a miss
if (d > ballSpeed) { return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed
s.dy *= ballSpeed
hit = true;
}
// if the ball hit the bat restore absolute position
if (hit) {
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) + velocity.y * Math.sin(angle),
y: -velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.dx - otherParticle.dx;
const yVelocityDiff = particle.dy - otherParticle.dy;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
const m1 = particle.mass;
const m2 = otherParticle.mass;
const u1 = rotate({ x: particle.dx, y: particle.dy }, angle);
const u2 = rotate({ x: otherParticle.dx, y: otherParticle.dy }, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
};
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
};
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
particle.dx = vFinal1.x;
particle.dy = vFinal1.y;
otherParticle.dx = vFinal2.x;
otherParticle.dy = vFinal2.y;
}
}
for (let i = 0; i < 10; i++) {
balls.push(new ball())
}
balls.push(new player())
//x,y,w,h
bats.push(new bat((window.innerWidth / 2) - 150, window.innerHeight / 2, 10, 100))
bats.push(new bat((window.innerWidth / 2) + 150, window.innerHeight / 2, 10, 100))
// main update function
function update(timer) {
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
// move bat and ball
for (var i = 0; i < balls.length; i++) {
for (var j = 0; j < bats.length; j++) {
doBatBall(bats[j], balls[i])
}
balls[i].update()
balls[i].draw(ctx)
}
bats.forEach(bat => {
//bat.update();
bat.draw(ctx);
})
// check for bal bat contact and change ball position and trajectory if needed
// draw ball and bat
requestAnimationFrame(update);
}
requestAnimationFrame(update);
<body>
<canvas></canvas>
</body>
The balls cannot have 0 speed, but player can. Because of that, it fails at dividing by zero:
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
Simply add || 1 to the end of the line should fix it.
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false }
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 0;
var balls = []
var bats = []
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx, style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) };
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function distance(x1, y1, x2, y2) {
const xDist = x2 - x1;
const yDist = y2 - y1;
return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
}
// the ball
class ball {
constructor() {
this.r = 25
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 15
this.dy = 15
this.mass = 1
this.maxSpeed = 15
this.style = {
lineWidth: 12,
strokeStyle: "green"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if (y > canvas.height - this.r) {
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if (y < this.r) {
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if (x > canvas.width - this.r) {
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if (x < this.r) {
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if (speed > this.maxSpeed) { // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed - this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
class player {
constructor() {
this.r = 50
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 0.2
this.dy = 0.2
this.mass = 1
this.maxSpeed = 1000
this.width = this.r
this.height = this.r
this.style = {
lineWidth: 12,
strokeStyle: "blue"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
/* change */
/*
x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);
*/
x < this.width && (x = this.width);
y < this.height && (y = this.height);
x > canvas.width - this.width && (x = canvas.width - this.width);
y > canvas.height - this.height && (y = canvas.height - this.height);
/* end change */
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
const ballShadow = {
r: 50,
x: 50,
y: 50,
dx: 0.2,
dy: 0.2,
}
//bat
class bat {
constructor(x, y, w, h) {
this.x = x
this.y = y
this.dx = 0
this.dy = 0
this.width = w
this.height = h
this.style = {
lineWidth: 2,
strokeStyle: "black",
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.strokeRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
function doBatBall(bat, ball) {
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if (s.x < 0) {
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if (s.y < 0) {
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if (s.dx > 0 && s.dy > 0) { return } // ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy) // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x + (s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y + (s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if (s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR) { // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if (!hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB) { // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if (!hit) { // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
/* change */
// const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed) || 1;
/* end change */
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if (dist > radSqr) { return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x, cpy - s.y);
// is the distance greater than the ball speed then its a miss
if (d > ballSpeed) { return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed
s.dy *= ballSpeed
hit = true;
}
// if the ball hit the bat restore absolute position
if (hit) {
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) + velocity.y * Math.sin(angle),
y: -velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.dx - otherParticle.dx;
const yVelocityDiff = particle.dy - otherParticle.dy;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
const m1 = particle.mass;
const m2 = otherParticle.mass;
const u1 = rotate({ x: particle.dx, y: particle.dy }, angle);
const u2 = rotate({ x: otherParticle.dx, y: otherParticle.dy }, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
};
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
};
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
particle.dx = vFinal1.x;
particle.dy = vFinal1.y;
otherParticle.dx = vFinal2.x;
otherParticle.dy = vFinal2.y;
}
}
for (let i = 0; i < 10; i++) {
balls.push(new ball())
}
balls.push(new player())
//x,y,w,h
bats.push(new bat((window.innerWidth / 2) - 150, window.innerHeight / 2, 10, 100))
bats.push(new bat((window.innerWidth / 2) + 150, window.innerHeight / 2, 10, 100))
// main update function
function update(timer) {
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
// move bat and ball
for (var i = 0; i < balls.length; i++) {
for (var j = 0; j < bats.length; j++) {
doBatBall(bats[j], balls[i])
}
balls[i].update()
balls[i].draw(ctx)
}
bats.forEach(bat => {
//bat.update();
bat.draw(ctx);
})
// check for bal bat contact and change ball position and trajectory if needed
// draw ball and bat
requestAnimationFrame(update);
}
requestAnimationFrame(update);
<body>
<canvas></canvas>
</body>
Adding your player to the array of balls is not what you want to do. Keep that object separate. Create your player as a variable and in your loop just draw and update the player
let player = new Player();
function update(timer) {
...
player.update();
player.draw(ctx);
...
}
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext("2d");
const mouse = { x: 0, y: 0, button: false }
function mouseEvents(e) {
mouse.x = e.x;
mouse.y = e.y;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 0;
var balls = []
var bats = []
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx, style) => { Object.keys(style).forEach(key => ctx[key] = style[key]) };
function random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function distance(x1, y1, x2, y2) {
const xDist = x2 - x1;
const yDist = y2 - y1;
return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
}
// the ball
class ball {
constructor() {
this.r = 25
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 15
this.dy = 15
this.mass = 1
this.maxSpeed = 15
this.style = {
lineWidth: 12,
strokeStyle: "green"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if (y > canvas.height - this.r) {
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if (y < this.r) {
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if (x > canvas.width - this.r) {
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if (x < this.r) {
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if (speed > this.maxSpeed) { // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed - this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
for (var i = 0; i < balls.length; i++) {
if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
class Player {
constructor() {
this.r = 50
this.x = random(50, 1500)
this.y = random(50, 1500)
this.dx = 0.2
this.dy = 0.2
this.mass = 1
this.maxSpeed = 1000
this.style = {
lineWidth: 12,
strokeStyle: "blue"
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.beginPath();
ctx.arc(this.x, this.y, this.r - this.style.lineWidth * 0.45, 0, PI2);
ctx.stroke();
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
/*x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);*/
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
for (var i = 0; i < balls.length; i++) {
//if (this === balls[i]) continue
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r * 2) {
resolveCollision(this, balls[i])
}
}
}
}
const ballShadow = {
r: 50,
x: 50,
y: 50,
dx: 0.2,
dy: 0.2,
}
//bat
class bat {
constructor(x, y, w, h) {
this.x = x
this.y = y
this.dx = 0
this.dy = 0
this.width = w
this.height = h
this.style = {
lineWidth: 2,
strokeStyle: "black",
}
}
draw(ctx) {
setStyle(ctx, this.style);
ctx.strokeRect(this.x - this.width / 2, this.y - this.height / 2, this.width, this.height);
}
update() {
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
function doBatBall(bat, ball) {
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if (s.x < 0) {
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if (s.y < 0) {
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if (s.dx > 0 && s.dy > 0) { return } // ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy) // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x + (s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y + (s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if (s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR) { // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if (!hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB) { // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if (!hit) { // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy) / (ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if (dist > radSqr) { return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x, cpy - s.y);
// is the distance greater than the ball speed then its a miss
if (d > ballSpeed) { return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed
s.dy *= ballSpeed
hit = true;
}
// if the ball hit the bat restore absolute position
if (hit) {
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
function rotate(velocity, angle) {
const rotatedVelocities = {
x: velocity.x * Math.cos(angle) + velocity.y * Math.sin(angle),
y: -velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
};
return rotatedVelocities;
}
function resolveCollision(particle, otherParticle) {
const xVelocityDiff = particle.dx - otherParticle.dx;
const yVelocityDiff = particle.dy - otherParticle.dy;
const xDist = otherParticle.x - particle.x;
const yDist = otherParticle.y - particle.y;
if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
const angle = Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
const m1 = particle.mass;
const m2 = otherParticle.mass;
const u1 = rotate({ x: particle.dx, y: particle.dy }, angle);
const u2 = rotate({ x: otherParticle.dx, y: otherParticle.dy }, angle);
const v1 = {
x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2),
y: u1.y
};
const v2 = {
x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2),
y: u2.y
};
const vFinal1 = rotate(v1, -angle);
const vFinal2 = rotate(v2, -angle);
particle.dx = vFinal1.x;
particle.dy = vFinal1.y;
otherParticle.dx = vFinal2.x;
otherParticle.dy = vFinal2.y;
}
}
for (let i = 0; i < 10; i++) {
balls.push(new ball())
}
let player = new Player();
//x,y,w,h
bats.push(new bat((window.innerWidth / 2) - 150, window.innerHeight / 2, 10, 100))
bats.push(new bat((window.innerWidth / 2) + 150, window.innerHeight / 2, 10, 100))
// main update function
function update(timer) {
if (w !== innerWidth || h !== innerHeight) {
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
// move bat and ball
for (var i = 1; i < balls.length; i++) {
for (var j = 0; j < bats.length; j++) {
doBatBall(bats[j], balls[i])
}
balls[i].update()
balls[i].draw(ctx)
}
player.update();
player.draw(ctx);
bats.forEach(bat => {
//bat.update();
bat.draw(ctx);
})
// check for bal bat contact and change ball position and trajectory if needed
// draw ball and bat
requestAnimationFrame(update);
}
update();
<canvas></canvas>
Keep in mind the mouse is not exactly in the center of the circle because you are not accounting for the canvas position in the mousemove function. Also you have something limiting your player objects ability to move over the whole canvas.
EDIT:
This is what was causing your player to not be able to move over the whole canvas so comment it out.
/* x < this.width / 2 && (x / 2);
y < this.height / 2 && (y / 2);
x > canvas.width / 2 && (x = canvas.width / 2);
y > canvas.height / 2 && (y = canvas.height / 2);*/
And your collision is slightly off because in you player class you are checking distance against this.r * 2. I would change it to
for (var i = 0; i < balls.length; i++) {
//if (this === balls[i]) continue //not really needed
if (distance(this.x, this.y, balls[i].x, balls[i].y) < this.r + balls[i].r) {
resolveCollision(this, balls[i])
}
I am trying to write aa small physics demo using Javascript. I have multiple balls that bounce off each other just fine, but things go wrong when I try to add gravity.
I am trying to conserve the momentum once they hit, but when I add constant gravity to each one, the physics start to break down.
Here is what I have in terms of code:
class Ball {
constructor ({
x,
y,
vx,
vy,
radius,
color = 'red',
}) {
this.x = x
this.y = y
this.vx = vx
this.vy = vy
this.radius = radius
this.color = color
this.mass = 1
}
render (ctx) {
ctx.save()
ctx.fillStyle = this.color
ctx.strokeStyle = this.color
ctx.translate(this.x, this.y)
ctx.strokeRect(-this.radius, -this.radius, this.radius * 2, this.radius * 2)
ctx.beginPath()
ctx.arc(0, 0, this.radius, Math.PI * 2, false)
ctx.closePath()
ctx.fill()
ctx.restore()
return this
}
getBounds () {
return {
x: this.x - this.radius,
y: this.y - this.radius,
width: this.radius * 2,
height: this.radius * 2
}
}
}
const intersects = (rectA, rectB) => {
return !(rectA.x + rectA.width < rectB.x ||
rectB.x + rectB.width < rectA.x ||
rectA.y + rectA.height < rectB.y ||
rectB.y + rectB.height < rectA.y)
}
const checkWall = (ball) => {
const bounceFactor = 0.5
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius
ball.vx *= -bounceFactor
}
if (ball.x - ball.radius < 0) {
ball.x = ball.radius
ball.vx *= -bounceFactor
}
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius
ball.vy *= -1
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius
ball.vy *= -bounceFactor
}
}
const rotate = (x, y, sin, cos, reverse) => {
return {
x: reverse ? x * cos + y * sin : x * cos - y * sin,
y: reverse ? y * cos - x * sin : y * cos + x * sin
}
}
const checkCollision = (ball0, ball1, dt) => {
const dx = ball1.x - ball0.x
const dy = ball1.y - ball0.y
const dist = Math.sqrt(dx * dx + dy * dy)
const minDist = ball0.radius + ball1.radius
if (dist < minDist) {
//calculate angle, sine, and cosine
const angle = Math.atan2(dy, dx)
const sin = Math.sin(angle)
const cos = Math.cos(angle)
//rotate ball0's position
const pos0 = {x: 0, y: 0}
//rotate ball1's position
const pos1 = rotate(dx, dy, sin, cos, true)
//rotate ball0's velocity
const vel0 = rotate(ball0.vx, ball0.vy, sin, cos, true)
//rotate ball1's velocity
const vel1 = rotate(ball1.vx, ball1.vy, sin, cos, true)
//collision reaction
const vxTotal = (vel0.x - vel1.x)
vel0.x = ((ball0.mass - ball1.mass) * vel0.x + 2 * ball1.mass * vel1.x) /
(ball0.mass + ball1.mass)
vel1.x = vxTotal + vel0.x
const absV = Math.abs(vel0.x) + Math.abs(vel1.x)
const overlap = (ball0.radius + ball1.radius) - Math.abs(pos0.x - pos1.x)
pos0.x += vel0.x / absV * overlap
pos1.x += vel1.x / absV * overlap
//rotate positions back
const pos0F = rotate(pos0.x, pos0.y, sin, cos, false)
const pos1F = rotate(pos1.x, pos1.y, sin, cos, false)
//adjust positions to actual screen positions
ball1.x = ball0.x + pos1F.x
ball1.y = ball0.y + pos1F.y
ball0.x = ball0.x + pos0F.x
ball0.y = ball0.y + pos0F.y
//rotate velocities back
const vel0F = rotate(vel0.x, vel0.y, sin, cos, false)
const vel1F = rotate(vel1.x, vel1.y, sin, cos, false)
ball0.vx = vel0F.x
ball0.vy = vel0F.y
ball1.vx = vel1F.x
ball1.vy = vel1F.y
}
}
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let oldTime = 0
canvas.width = innerWidth
canvas.height = innerHeight
document.body.appendChild(canvas)
const log = document.getElementById('log')
const balls = new Array(36).fill(null).map(_ => new Ball({
x: Math.random() * innerWidth,
y: Math.random() * innerHeight,
vx: (Math.random() * 2 - 1) * 5,
vy: (Math.random() * 2 - 1) * 5,
radius: 20,
}))
requestAnimationFrame(updateFrame)
function updateFrame (ts) {
const dt = ts - oldTime
oldTime = ts
ctx.clearRect(0, 0, innerWidth, innerHeight)
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// ADD GRAVITY HERE
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// CHECK FOR COLLISIONS HERE
checkCollision(ball0, ball1, dt)
}
}
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
// const dist = ball2.x - ball1.x
// if (Math.abs(dist) < ball1.radius + ball2.radius) {
// const vxTotal = ball1.vx - ball2.vx
// ball1.vx = ((ball1.mass - ball2.mass) * ball1.vx + 2 * ball2.mass * ball2.vx) / (ball1.mass + ball2.mass)
// ball2.vx = vxTotal + ball1.vx
// ball1.x += ball1.vx
// ball2.x += ball2.vx
// }
// ball.vy += 0.5
// ball.x += ball.vx
// ball.y += ball.vy
//
// ball.render(ctx)
requestAnimationFrame(updateFrame)
}
* { margin: 0; padding: 0; }
As you can see, I have checkCollision helper method, which calculates the kinetic energy and new velocities of a ball once it has collided with another ball. My update loop looks like this:
// add velocities to balls position
// check if its hitting any wall and bounce it back
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// Add constant gravity to the vertical velocity
// When balls stack up on each other at the bottom, the gravity is still applied and my
// "checkCollision" method freaks out and the physics start to explode
ball.vy += 0.8
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// Check collisions between two balls
checkCollision(ball0, ball1, dt)
}
}
// Finally render the ball on-screen
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
How do I calculate aa gravity, while preventing the physics from exploding when the balls start stacking on top of each other?
It seems that the gravity force is colliding with the "checkCollision" method. The checkCollision method tries to move them back in place, but the constant gravity overwrites it and continues pulling them down.
EDIT: After some reading I understand some Verlet integration is in order, but am having difficulties with wrapping my head around it.
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// This line needs to be converted to verlet motion?
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
Balls do not overlap
There is a fundamental flaw in the collision testing due to the fact that the collisions are calculated only when 2 balls overlap. In the real world this never happens.
The result of "collide on overlap" when many balls are interacting, will result in behavior that does not conserve the total energy of the system.
Resolve by order of collision
You can resolve collisions such that balls never overlap however the amount of processing is indeterminate per frame, growing exponentially as the density of balls increases.
The approach is to locate the first collision between balls in the time between frames. Resolve that collision and then with the new position of that collision find the next collision closest in time forward from the last. Do that until there are no pending collisions for that frame. (There is more to it than that) The result is that the simulation will never be in the impossible state where balls overlap.
Check out my Pool simulator on CodePen that uses this method to simulate pool balls. The balls can have any speed and always resolve correctly.
Verlet integration.
However you can reduce the noise using the overlapping collisions by using verlet integration which will keep the total energy of the balls at a more stable level.
To do that we introduce 2 new properties of the ball, px, py that hold the previous position of the ball.
Each frame we calculate the balls velocity as the difference between the current position and the new position. That velocity is used to do the calculations for the frame.
When a ball changes direction (hits wall or another ball) we also need to change the balls previous position to match where it would have been on the new trajectory.
Use constant time steps.
Using time steps based on time since last frame will also introduce noise and should not be used in the overlap collision method.
Reduce time, increase iteration
To further combat the noise you need to slow the overall speed of the balls to reduce the amount they overlay and thus more closely behave as if they collided at the point ballA.radius + ballB.radius apart. Also you should test every ball against every other ball, not just ball against balls above it in the balls array.
To keep the animation speed up you solve the ball V ball V wall collisions a few times per frame. The example does 5. The best value depends on the total energy of the balls, the level of noise that is acceptable, and the CPU power of the device its running on.
Accuracy matters
Your collision function is also way out there. I had a quick look and it did not look right. I added an alternative in the example.
When a ball hits a wall it does so at some time between the frames. You must move the ball away from the wall by the correct distance. Not doing so is like simulating a ball that sticks to a wall a tiny bit each time it hits, further diverging from what really happens.
Example
This is a rewrite of your original code. Click canvas to add some energy.
const ctx = canvas.getContext("2d");
const BOUNCE = 0.75;
const resolveSteps = 5;
var oldTime = 0;
const $setOf = (count, fn = (i) => i) => {var a = [], i = 0; while (i < count) { a.push(fn(i++)) } return a };
const $rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const $randP = (min = 1, max = min + (min = 0), p = 2) => Math.random() ** p * (max - min) + min;
var W = canvas.width, H = canvas.height;
const BALL_COUNT = 80;
const BALL_RADIUS = 15, BALL_MIN_RADIUS = 6;
const GRAV = 0.5 / resolveSteps;
requestAnimationFrame(updateFrame);
canvas.addEventListener("click", () => {
balls.forEach(b => {
b.px = b.x + (Math.random() * 18 - 9);
b.py = b.y + (Math.random() * -18);
})
});
class Ball {
constructor({x, y, vx, vy, radius}) {
this.x = x;
this.y = y;
this.px = x - vx;
this.py = y - vy;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.mass = radius * radius * Math.PI * (4 / 3); // use sphere volume as mass
}
render(ctx) {
ctx.moveTo(this.x + this.radius, this.y);
ctx.arc(this.x, this.y, this.radius, Math.PI * 2, false);
}
move() {
this.vx = this.x - this.px;
this.vy = this.y - this.py;
this.vy += GRAV;
this.px = this.x;
this.py = this.y;
this.x += this.vx;
this.y += this.vy;
this.checkWall();
}
checkWall() {
const ball = this;
const top = ball.radius;
const left = ball.radius;
const bottom = H - ball.radius;
const right = W - ball.radius;
if (ball.x > right) {
const away = (ball.x - right) * BOUNCE;
ball.x = right - away;
ball.vx = -Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
} else if (ball.x < left) {
const away = (ball.x - left) * BOUNCE;
ball.x = left + away;
ball.vx = Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
}
if (ball.y > bottom) {
const away = (ball.y - bottom) * BOUNCE;
ball.y = bottom - away;
ball.vy = -Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
} else if (ball.y < top) {
const away = (ball.y - top) * BOUNCE;
ball.y = top + away;
ball.vy = Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
}
}
collisions() {
var b, dx, dy, nx, ny, cpx, cpy, p, d, i = 0;
var {x, y, vx, vy, px, py, radius: r, mass: m} = this;
while (i < balls.length) {
b = balls[i++];
if (this !== b) {
const rr = r + b.radius;
if (x + rr > b.x && x < b.x + rr && y + rr > b.y && y < b.y + rr) {
dx = x - b.x;
dy = y - b.y;
d = (dx * dx + dy * dy) ** 0.5;
if (d < rr) {
nx = (b.x - x) / d;
ny = (b.y - y) / d;
p = 2 * (vx * nx + vy * ny - b.vx * nx - b.vy * ny) / (m + b.mass);
cpx = (x * b.radius + b.x * r) / rr;
cpy = (y * b.radius + b.y * r) / rr;
x = cpx + r * (x - b.x) / d;
y = cpy + r * (y - b.y) / d;
b.x = cpx + b.radius * (b.x - x) / d;
b.y = cpy + b.radius * (b.y - y) / d;
px = x - (vx -= p * b.mass * nx);
py = y - (vy -= p * b.mass * ny);
b.px = b.x - (b.vx += p * m * nx);
b.py = b.y - (b.vy += p * m * ny);
}
}
}
}
this.x = x;
this.y = y;
this.px = px;
this.py = py;
this.vx = vx;
this.vy = vy;
this.checkWall();
}
}
const balls = (() => {
return $setOf(BALL_COUNT, () => new Ball({
x: $rand(BALL_RADIUS, W - BALL_RADIUS),
y: $rand(BALL_RADIUS, H - BALL_RADIUS),
vx: $rand(-2, 2),
vy: $rand(-2, 2),
radius: $randP(BALL_MIN_RADIUS, BALL_RADIUS, 4),
}));
})();
function updateFrame(ts) {
var i = 0, j = resolveSteps;
ctx.clearRect(0, 0, W, H);
while (i < balls.length) { balls[i++].move() }
while (j--) {
i = 0;
while (i < balls.length) { balls[i++].collisions(balls) }
}
ctx.fillStyle = "#0F0";
ctx.beginPath();
i = 0;
while (i < balls.length) { balls[i++].render(ctx) }
ctx.fill();
requestAnimationFrame(updateFrame)
}
<canvas id="canvas" width="400" height="180" style="border:1px solid black;"></canvas>
<div style="position: absolute; top: 10px; left: 10px;">Click to stir</div>
I'm working on a game that uses projectiles and a shielding system. The player would hold down 'Space' to use the shield. My plan is to get the projectiles to bounce of of the enemies shields (I've implemented speed so I already know how to do that). The problem I am having is with the collision, since the player rotates to follow the mouse I struggled with finding the best way to create the shield but I eventually settled on an arc, I used some trigonometry to get the left, leftHalf, mid, rightHalf, and right point of the arc/shield. The Player with Shield. The issue is I can't get the collision to work from just 5, x/y coordinates (the arc is just being drawn for show I'm only sending the points to the server). This is what I have for my collision so far:
p: Player object
self: bullet object
bot: a variable based on the direction the character is facing (bottom: true or false)
shieldLeft, sheildRight, etc: an array containing x and y coordinate 0 for x, 1 for y
if (self.getDistance(p) < 32 && self.parent !== p.id)
{
if (p.isShielding == true)
{
switch(self.bot)
{
case true:
if (self.x >= p.shieldRight[0] && self.x <= p.shieldLeft[0])
{
console.log("BOT X");
if ((self.y >= p.shieldLeft[1] || self.y >= p.shieldRight[1]) && self.y <= p.shieldMid[1])
{
console.log("BOT Y");
self.spdX = -self.spdX;
self.spdY = -self.spdY;
}
}
break;
case false:
if (self.x <= p.shieldRight[0] && self.x >= p.shieldLeft[0])
{
console.log("TOP X");
if ((self.y <= p.shieldLeft[1] || self.y <= p.shieldRight[1]) && self.y >= p.shieldMid[1])
{
console.log("TOP Y");
self.spdX = -self.spdX;
self.spdY = -self.spdY;
}
}
break;
}
}
}
I would really appreciate any help, I can't continue with the game features until there actually is a game. Thank you!
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: black;
}
canvas {
position: absolute;
margin: auto;
left: 0;
right: 0;
border: solid 1px white;
border-radius: 10px;
cursor: crosshair;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="application/javascript">
// Anonymous closure
(function() {
// Enforce strict rules for JS code
"use strict";
// App variables
var canvasWidth = 180;
var canvasHeight = 160;
var canvas = null;
var bounds = null;
var ctx = null;
var player = null;
var projectiles = [];
projectiles.length = 5;
// Classes
// Constructor function
function Player(x,y) {
this.x = x;
this.y = y;
this.dx = 0.0;
this.dy = 0.0;
this.rotation = 0.0;
this.targetX = 0.0;
this.targetY = 0.0;
this.isShieldUp = false;
this.isShieldRecharging = false;
this.shieldPower = this.shieldPowerMax;
this.left = false;
this.right = false;
this.up = false;
this.down = false;
window.addEventListener("keydown",this.onkeydown.bind(this));
window.addEventListener("keyup",this.onkeyup.bind(this));
window.addEventListener("mousemove",this.onmousemove.bind(this));
}
// shared properties/functions across all instances
Player.prototype = {
width: 10,
height: 10,
shieldRadius: 15.0,
shieldArcSize: 3.0, // In Radians
shieldPowerMax: 50.0,
shieldPowerCharge: 0.5,
shieldPowerDrain: 0.75,
onkeydown: function(e) {
switch(e.key) {
case " ": this.isShieldUp = true && !this.isShieldRecharging; break;
case "w": this.up = true; break;
case "s": this.down = true; break;
case "a": this.left = true; break;
case "d": this.right = true; break;
}
},
onkeyup: function(e) {
switch(e.key) {
case " ": this.isShieldUp = false; break;
case "w": this.up = false; break;
case "s": this.down = false; break;
case "a": this.left = false; break;
case "d": this.right = false; break;
}
},
onmousemove: function(e) {
this.targetX = e.clientX - bounds.left;
this.targetY = e.clientY - bounds.top;
},
tick: function() {
var x = (this.targetX - this.x);
var y = (this.targetY - this.y);
var l = Math.sqrt(x * x + y * y);
x = x / l;
y = y / l;
this.rotation = Math.acos(x) * (y < 0.0 ? -1.0 : 1.0);
if (this.isShieldUp) {
this.shieldPower = this.shieldPower - this.shieldPowerDrain;
if (this.shieldPower < 0.0) {
this.shieldPower = 0.0;
this.isShieldUp = false;
this.isShieldRecharging = true;
}
} else {
this.shieldPower = this.shieldPower + this.shieldPowerCharge;
if (this.shieldPower > this.shieldPowerMax) {
this.shieldPower = this.shieldPowerMax;
this.isShieldRecharging = false;
}
}
if (this.up) { --this.y; this.dy = -1; } else
if (this.down) { ++this.y; this.dy = 1; } else { this.dy = 0; }
if (this.left) { --this.x; this.dx = -1; } else
if (this.right) { ++this.x; this.dx = 1; } else { this.dx = 0; }
},
render: function() {
ctx.fillStyle = "darkred";
ctx.strokeStyle = "black";
ctx.translate(this.x,this.y);
ctx.rotate(this.rotation);
ctx.beginPath();
ctx.moveTo(0.5 * this.height,0.0);
ctx.lineTo(-0.5 * this.height,0.5 * this.width);
ctx.lineTo(-0.5 * this.height,-0.5 * this.width);
ctx.lineTo(0.5 * this.height,0.0);
ctx.fill();
ctx.stroke();
if (this.isShieldUp) {
ctx.strokeStyle = "cyan";
ctx.beginPath();
ctx.arc(0.0,0.0,this.shieldRadius,this.shieldArcSize * -0.5,this.shieldArcSize * 0.5,false);
ctx.stroke();
}
ctx.rotate(-this.rotation);
ctx.translate(-this.x,-this.y);
ctx.fillStyle = "black";
ctx.fillRect(canvasWidth - 80,canvasHeight - 20,75,15);
ctx.fillStyle = this.isShieldRecharging ? "red" : "cyan";
ctx.fillRect(canvasWidth - 75,canvasHeight - 15,65 * (this.shieldPower / this.shieldPowerMax),5);
}
};
function Projectile(x,y,dx,dy) {
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
}
Projectile.prototype = {
radius: 2.5,
tick: function(player) {
this.x = this.x + this.dx;
this.y = this.y + this.dy;
if (this.x + this.radius < 0.0) { this.x = canvasWidth + this.radius; }
if (this.x - this.radius > canvasWidth) { this.x = -this.radius; }
if (this.y + this.radius < 0.0) { this.y = canvasHeight + this.radius; }
if (this.y - this.radius > canvasHeight) { this.y = -this.radius; }
if (player.isShieldUp) {
var px = (player.x - this.x);
var py = (player.y - this.y);
var pl = Math.sqrt(px * px + py * py);
var ml = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var mx = this.dx / ml;
var my = this.dy / ml;
px = px / pl;
py = py / pl;
if (Math.acos(px * mx + py * my) < player.shieldArcSize * 0.5 && pl < player.shieldRadius) {
px = -px;
py = -py;
this.dx = this.dx - 2.0 * px * (this.dx * px + this.dy * py) + player.dx;
this.dy = this.dy - 2.0 * py * (this.dx * px + this.dy * py) + player.dy;
}
}
},
render: function() {
ctx.moveTo(this.x + this.radius,this.y);
ctx.arc(this.x,this.y,this.radius,0.0,2.0 * Math.PI,false);
}
}
// Game loop
function loop() {
// Tick
player.tick();
for (var i = 0; i < projectiles.length; ++i) {
projectiles[i].tick(player);
}
// Render
ctx.fillStyle = "#555555";
ctx.fillRect(0,0,canvasWidth,canvasHeight);
player.render();
ctx.fillStyle = "white";
ctx.strokeStyle = "black";
ctx.beginPath();
for (var i = 0; i < projectiles.length; ++i) {
projectiles[i].render();
}
ctx.fill();
ctx.stroke();
//
requestAnimationFrame(loop); // Runs the loop at 60hz
}
// "Main Method", executes after the page loads
window.onload = function() {
canvas = document.getElementById("canvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
bounds = canvas.getBoundingClientRect();
ctx = canvas.getContext("2d");
player = new Player(90.0,80.0);
for (var i = 0; i < projectiles.length; ++i) {
projectiles[i] = new Projectile(
Math.random() * canvasWidth,
Math.random() * canvasHeight,
Math.random() * 2.0 - 1.0,
Math.random() * 2.0 - 1.0
);
}
loop();
}
})();
</script>
</body>
</html>
I found this amazing plugin which simulates the physics of fabric: http://codepen.io/anon/pen/mxjKC
document.getElementById('close').onmousedown = function(e) {
e.preventDefault();
document.getElementById('info').style.display = 'none';
return false;
};
// settings
var physics_accuracy = 3,
mouse_influence = 20,
mouse_cut = 5,
gravity = 1200,
cloth_height = 30,
cloth_width = 50,
start_y = 20,
spacing = 7,
tear_distance = 60;
window.requestAnimFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
var canvas,
ctx,
cloth,
boundsx,
boundsy,
mouse = {
down: false,
button: 1,
x: 0,
y: 0,
px: 0,
py: 0
};
var Point = function (x, y) {
this.x = x;
this.y = y;
this.px = x;
this.py = y;
this.vx = 0;
this.vy = 0;
this.pin_x = null;
this.pin_y = null;
this.constraints = [];
};
Point.prototype.update = function (delta) {
if (mouse.down) {
var diff_x = this.x - mouse.x,
diff_y = this.y - mouse.y,
dist = Math.sqrt(diff_x * diff_x + diff_y * diff_y);
if (mouse.button == 1) {
if (dist < mouse_influence) {
this.px = this.x - (mouse.x - mouse.px) * 1.8;
this.py = this.y - (mouse.y - mouse.py) * 1.8;
}
} else if (dist < mouse_cut) this.constraints = [];
}
this.add_force(0, gravity);
delta *= delta;
nx = this.x + ((this.x - this.px) * .99) + ((this.vx / 2) * delta);
ny = this.y + ((this.y - this.py) * .99) + ((this.vy / 2) * delta);
this.px = this.x;
this.py = this.y;
this.x = nx;
this.y = ny;
this.vy = this.vx = 0
};
Point.prototype.draw = function () {
if (!this.constraints.length) return;
var i = this.constraints.length;
while (i--) this.constraints[i].draw();
};
Point.prototype.resolve_constraints = function () {
if (this.pin_x != null && this.pin_y != null) {
this.x = this.pin_x;
this.y = this.pin_y;
return;
}
var i = this.constraints.length;
while (i--) this.constraints[i].resolve();
this.x > boundsx ? this.x = 2 * boundsx - this.x : 1 > this.x && (this.x = 2 - this.x);
this.y < 1 ? this.y = 2 - this.y : this.y > boundsy && (this.y = 2 * boundsy - this.y);
};
Point.prototype.attach = function (point) {
this.constraints.push(
new Constraint(this, point)
);
};
Point.prototype.remove_constraint = function (constraint) {
this.constraints.splice(this.constraints.indexOf(constraint), 1);
};
Point.prototype.add_force = function (x, y) {
this.vx += x;
this.vy += y;
};
Point.prototype.pin = function (pinx, piny) {
this.pin_x = pinx;
this.pin_y = piny;
};
var Constraint = function (p1, p2) {
this.p1 = p1;
this.p2 = p2;
this.length = spacing;
};
Constraint.prototype.resolve = function () {
var diff_x = this.p1.x - this.p2.x,
diff_y = this.p1.y - this.p2.y,
dist = Math.sqrt(diff_x * diff_x + diff_y * diff_y),
diff = (this.length - dist) / dist;
if (dist > tear_distance) this.p1.remove_constraint(this);
var px = diff_x * diff * 0.5;
var py = diff_y * diff * 0.5;
this.p1.x += px;
this.p1.y += py;
this.p2.x -= px;
this.p2.y -= py;
};
Constraint.prototype.draw = function () {
ctx.moveTo(this.p1.x, this.p1.y);
ctx.lineTo(this.p2.x, this.p2.y);
};
var Cloth = function () {
this.points = [];
var start_x = canvas.width / 3 - cloth_width * spacing / 2;
for (var y = 0; y <= cloth_height; y++) {
for (var x = 0; x <= cloth_width; x++) {
var p = new Point(start_x + x * spacing, start_y + y * spacing);
x != 0 && p.attach(this.points[this.points.length - 1]);
y == 0 && p.pin(p.x, p.y);
y != 0 && p.attach(this.points[x + (y - 1) * (cloth_width + 1)])
this.points.push(p);
}
}
};
Cloth.prototype.update = function () {
var i = physics_accuracy;
while (i--) {
var p = this.points.length;
while (p--) this.points[p].resolve_constraints();
}
i = this.points.length;
while (i--) this.points[i].update(.016);
};
Cloth.prototype.draw = function () {
ctx.beginPath();
var i = cloth.points.length;
while (i--) cloth.points[i].draw();
ctx.stroke();
};
function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
cloth.update();
cloth.draw();
requestAnimFrame(update);
}
function start() {
canvas.onmousedown = function (e) {
mouse.button = e.which;
mouse.px = mouse.x;
mouse.py = mouse.y;
var rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left,
mouse.y = e.clientY - rect.top,
mouse.down = true;
e.preventDefault();
};
canvas.onmouseup = function (e) {
mouse.down = false;
e.preventDefault();
};
canvas.onmousemove = function (e) {
mouse.px = mouse.x;
mouse.py = mouse.y;
var rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left,
mouse.y = e.clientY - rect.top,
e.preventDefault();
};
canvas.oncontextmenu = function (e) {
e.preventDefault();
};
boundsx = canvas.width - 1;
boundsy = canvas.height - 1;
ctx.strokeStyle = 'red';
cloth = new Cloth();
update();
}
window.onload = function () {
canvas = document.getElementById('c');
ctx = canvas.getContext('2d');
canvas.width = 1120;
canvas.height = 700;
canvas.style.width = '560px';
canvas.style.height = '350px';
ctx.scale(2,2);
start();
};
<div id=info>
<input type="button" value="close" id="close"></input>
<canvas id="c"></canvas>
</div>
I would love to know how to use it for an image instead of the net but I don't know where to start at all.
What is the theory behind that - where could I place the image?
You can not put an image on that mesh because the resulting squares are not 2d. This answer gives a little detail as to why.
To do what you want you need to use webGL. It is a straightforward conversion, the cloth is a mesh (vertices and polygons connecting the vertices) where each vertex will hold a texture coordinate and you let the sim run as is. There should be plenty of examples of how to map a image onto a mesh in stackoverflow. You might consider using three.js if you are new to 3D and coding.
Here is an example using three.js and a verlet cloth with texture mapped onto it.