How would you calculate the velocities for particles bouncing off eachother? - javascript

I have made this piece of code using canvas to portray some particles that fight over their designated color. Currently I have made the particle "put" their colors on the other particles, if they collide and I've also made them "suck" the other particles' mass, if within a certain radius. I want to take it to the next level by also, when a ball have hit another ball, make them bounce away from each other, by somehow negating the velocity in a way that has something to do with the relative angle to the other particle. How would one implement this? I can't quite wrap my head around this, although I study science in high school.
Codepen: http://codepen.io/dremp/pen/fzxvK?editors=001
This is the snippet that detects if particles have hit eachother.
function update() {
paintCanvas();
for(var i = 0; i < Particles.length; i++) {
p = Particles[i];
p.x += p.vx;
p.y += p.vy;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0 * Math.PI, 2 * Math.PI, false);
var drawMethod = (fillParticles === true) ? 'fill':'stroke';
ctx[drawMethod]();
ctx[drawMethod+"Style"] = p.color;
if (p.x + p.radius > W || p.x - p.radius < 0) p.vx = p.vx * - 1 + 0.05;
if (p.y + p.radius > H || p.y - p.radius < 0) p.vy = p.vy * - 1 + 0.05;
if (p.x + p.radius > W) p.vx -= 0.05;
else if (p.x - p.radius < 0) p.vx += 0.05;
if (p.y + p.radius > H) p.vy -= 0.05;
else if (p.y - p.radius < 0) p.vy += 0.05;
for(var j = i + 1; j < Particles.length; j++) {
p2 = Particles[j];
var disance,
distanceX = p.x - p2.x,
distanceY = p.y - p2.y,
color;
distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
if(distance <= p.radius + p2.radius) {
p2.color = p.color;
color = p.color;
var ax = distanceX / 1000,
ay = distanceY / 1000;
p.x += ax;
p.y += ay;
}
else if(distance <= p.radius * 2 + 50) {
ctx.lineWidth = 2;
ctx.strokeStyle = p.color;
ctx.beginPath();
ctx.moveTo(p.x,p.y);
ctx.lineTo(p2.x,p2.y);
ctx.stroke();
ctx.beginPath();
p.radius += 0.005;
p2.radius -= 0.005;
if(p.radius > 50) p.radius -= 0.01;
if(p2.radius < 5) p2.radius += 0.01;
}
}
}
requestAnimationFrame(update);
}

You need to calculate impulse:
// Find normal from other to me
normX = p.x - p2.x;
normY = p.y - p2.y;
// make into unit vector
normLength = Math.sqrt( normX * normX + normY * normY);
normX = normX / normLength;
normY = normY / normLength;
// Get projection of movement vectors onto normal
// (Dot prod each with norm)
myProj = (p.vx * normX) + (p.vy * normY);
otherProj = (p2.vx * normX) + (p2.vy * normY);
// Now, factor in impulse, derived from
// Conservation of Energy / Conservation of Momentum
impulse = ( 2 * (myProj - otherProj) );
p.mass = 1; // Replace with "mass" calculation (based on area?)
p2.mass = 1;
impulse = impulse / (p.mass + p2.mass);
p.vx = p.vx - (impulse * p2.mass * normX);
p.vy = p.vy - (impulse * p2.mass * normY);
p2.vx = p2.vx + (impulse * p.mass * normX);
p2.vy = p2.vy + (impulse * p.mass * normY);
Demo: http://codepen.io/gengel/pen/pFHGa
If you have different balls of different sizes flying around, you should change those p.mass lines to reflect that (area would probably work - (pi * r)^2 )
(edited to replace "P" with "impulse" since P was taken)

Related

How to bounce an object within circle bounds?

I have a basic circle bouncing off the walls of a rectangle canvas (that I adapted from an example).
https://jsfiddle.net/n5stvv52/1/
The code to check for this kind of collision is somewhat crude, like so, but it works:
if (p.x > canvasWidth - p.rad) {
p.x = canvasWidth - p.rad
p.velX *= -1
}
if (p.x < p.rad) {
p.x = p.rad
p.velX *= -1
}
if (p.y > canvasHeight - p.rad) {
p.y = canvasHeight - p.rad
p.velY *= -1
}
if (p.y < p.rad) {
p.y = p.rad
p.velY *= -1
}
Where p is the item moving around.
However, the bounds of my canvas now need to be a circle, so I check collision with the following:
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad
if (collision) {
console.log('Out of circle bounds!')
}
When my ball hits the edges of the circle, the if (collision) statement executes as true and I see the log. So I can get it detected, but I'm unable to know how to calculate the direction it should then go after that.
Obviously comparing x to the canvas width isn't what I need because that's the rectangle and a circle is cut at the corners.
Any idea how I can update my if statements to account for this newly detected circle?
I'm absolutely terrible with basic trigonometry it seems, so please bear with me! Thank you.
You can use the polar coordinates to normalize the vector:
var theta = Math.atan2(dy, dx)
var R = canvasRadius - p.rad
p.x = canvasRadius + R * Math.cos(theta)
p.y = canvasRadius + R * Math.sin(theta)
p.velX *= -1
p.velY *= -1
https://jsfiddle.net/d3k5pd94/1/
Update: The movement can be more natural if we add randomness to acceleration:
p.velX *= Math.random() > 0.5 ? 1 : -1
p.velY *= Math.random() > 0.5 ? 1 : -1
https://jsfiddle.net/1g9h9jvq/
So in order to do this you will indeed need some good ol' trig. The basic ingredients you'll need are:
The vector that points from the center of the circle to the collision point.
The velocity vector of the ball
Then, since things bounce with roughly an "equal and opposite angle", you'll need to find the angle difference between that velocity vector and the radius vector, which you can get by using a dot product.
Then do some trig to get a new vector that is that much off from the radius vector, in the other direction (this is your equal and opposite). Set that to be the new velocity vector, and you're good to go.
I know that's a bit dense, especially if you're rusty with your trig / vector math, so here's the code to get it going. This code could probably be simplified but it demonstrates the essential steps at least:
function canvasApp (selector) {
const canvas = document.querySelector(selector)
const context = canvas.getContext('2d')
const canvasWidth = canvas.width
const canvasHeight = canvas.height
const canvasRadius = canvasWidth / 2
const particleList = {}
const numParticles = 1
const initVelMax = 1.5
const maxVelComp = 2.5
const randAccel = 0.3
const fadeColor = 'rgba(255,255,255,0.1)'
let p
context.fillStyle = '#050505'
context.fillRect(0, 0, canvasWidth, canvasHeight)
createParticles()
draw()
function createParticles () {
const minRGB = 16
const maxRGB = 255
const alpha = 1
for (let i = 0; i < numParticles; i++) {
const vAngle = Math.random() * 2 * Math.PI
const vMag = initVelMax * (0.6 + 0.4 * Math.random())
const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const color = `rgba(${r},${g},${b},${alpha})`
const newParticle = {
x: Math.random() * canvasWidth,
y: Math.random() * canvasHeight,
velX: vMag * Math.cos(vAngle),
velY: vMag * Math.sin(vAngle),
rad: 15,
color
}
if (i > 0) {
newParticle.next = particleList.first
}
particleList.first = newParticle
}
}
function draw () {
context.fillStyle = fadeColor
context.fillRect(0, 0, canvasWidth, canvasHeight)
p = particleList.first
// random accleration
p.velX += (1 - 2 * Math.random()) * randAccel
p.velY += (1 - 2 * Math.random()) * randAccel
// don't let velocity get too large
if (p.velX > maxVelComp) {
p.velX = maxVelComp
} else if (p.velX < -maxVelComp) {
p.velX = -maxVelComp
}
if (p.velY > maxVelComp) {
p.velY = maxVelComp
} else if (p.velY < -maxVelComp) {
p.velY = -maxVelComp
}
p.x += p.velX
p.y += p.velY
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad
if (collision) {
console.log('Out of circle bounds!')
// Center of circle.
const center = [Math.floor(canvasWidth/2), Math.floor(canvasHeight/2)];
// Vector that points from center to collision point (radius vector):
const radvec = [p.x, p.y].map((c, i) => c - center[i]);
// Inverse vector, this vector is one that is TANGENT to the circle at the collision point.
const invvec = [-p.y, p.x];
// Direction vector, this is the velocity vector of the ball.
const dirvec = [p.velX, p.velY];
// This is the angle in radians to the radius vector (center to collision point).
// Time to rememeber some of your trig.
const radangle = Math.atan2(radvec[1], radvec[0]);
// This is the "direction angle", eg, the DIFFERENCE in angle between the radius vector
// and the velocity vector. This is calculated using the dot product.
const dirangle = Math.acos((radvec[0]*dirvec[0] + radvec[1]*dirvec[1]) / (Math.hypot(...radvec)*Math.hypot(...dirvec)));
// This is the reflected angle, an angle that is "equal and opposite" to the velocity vec.
const refangle = radangle - dirangle;
// Turn that back into a set of coordinates (again, remember your trig):
const refvec = [Math.cos(refangle), Math.sin(refangle)].map(x => x*Math.hypot(...dirvec));
// And invert that, so that it points back to the inside of the circle:
p.velX = -refvec[0];
p.velY = -refvec[1];
// Easy peasy lemon squeezy!
}
context.fillStyle = p.color
context.beginPath()
context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
context.closePath()
context.fill()
p = p.next
window.requestAnimationFrame(draw)
}
}
canvasApp('#canvas')
<canvas id="canvas" width="500" height="500" style="border: 1px solid red; border-radius: 50%;"></canvas>
DISCLAIMER: Since your initial position is random, this doens't work very well with the ball starts already outside of the circle. So make sure the initial point is within the bounds.
You don't need trigonometry at all. All you need is the surface normal, which is the vector from the point of impact to the center. Normalize it (divide both coordinates by the length), and you get the new velocity using
v' = v - 2 * (v • n) * n
Where v • n is the dot product:
v • n = v.x * n.x + v.y * n.y
Translated to your code example, that's
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad
if (collision) {
// the normal at the point of collision is -dx, -dy normalized
var nx = -dx / nl
var ny = -dy / nl
// calculate new velocity: v' = v - 2 * dot(d, v) * n
const dot = p.velX * nx + p.velY * ny
p.velX = p.velX - 2 * dot * nx
p.velY = p.velY - 2 * dot * ny
}
function canvasApp(selector) {
const canvas = document.querySelector(selector)
const context = canvas.getContext('2d')
const canvasWidth = canvas.width
const canvasHeight = canvas.height
const canvasRadius = canvasWidth / 2
const particleList = {}
const numParticles = 1
const initVelMax = 1.5
const maxVelComp = 2.5
const randAccel = 0.3
const fadeColor = 'rgba(255,255,255,0.1)'
let p
context.fillStyle = '#050505'
context.fillRect(0, 0, canvasWidth, canvasHeight)
createParticles()
draw()
function createParticles() {
const minRGB = 16
const maxRGB = 255
const alpha = 1
for (let i = 0; i < numParticles; i++) {
const vAngle = Math.random() * 2 * Math.PI
const vMag = initVelMax * (0.6 + 0.4 * Math.random())
const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const color = `rgba(${r},${g},${b},${alpha})`
const newParticle = {
// start inside circle
x: canvasWidth / 4 + Math.random() * canvasWidth / 2,
y: canvasHeight / 4 + Math.random() * canvasHeight / 2,
velX: vMag * Math.cos(vAngle),
velY: vMag * Math.sin(vAngle),
rad: 15,
color
}
if (i > 0) {
newParticle.next = particleList.first
}
particleList.first = newParticle
}
}
function draw() {
context.fillStyle = fadeColor
context.fillRect(0, 0, canvasWidth, canvasHeight)
// draw circle bounds
context.fillStyle = "black"
context.beginPath()
context.arc(canvasRadius, canvasRadius, canvasRadius, 0, 2 * Math.PI, false)
context.closePath()
context.stroke()
p = particleList.first
// random accleration
p.velX += (1 - 2 * Math.random()) * randAccel
p.velY += (1 - 2 * Math.random()) * randAccel
// don't let velocity get too large
if (p.velX > maxVelComp) {
p.velX = maxVelComp
} else if (p.velX < -maxVelComp) {
p.velX = -maxVelComp
}
if (p.velY > maxVelComp) {
p.velY = maxVelComp
} else if (p.velY < -maxVelComp) {
p.velY = -maxVelComp
}
p.x += p.velX
p.y += p.velY
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad
if (collision) {
// the normal at the point of collision is -dx, -dy normalized
var nx = -dx / nl
var ny = -dy / nl
// calculate new velocity: v' = v - 2 * dot(d, v) * n
const dot = p.velX * nx + p.velY * ny
p.velX = p.velX - 2 * dot * nx
p.velY = p.velY - 2 * dot * ny
}
context.fillStyle = p.color
context.beginPath()
context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
context.closePath()
context.fill()
p = p.next
window.requestAnimationFrame(draw)
}
}
canvasApp('#canvas')
<canvas id="canvas" width="176" height="176"></canvas>

Js - calculate distance from point mapped to colour forms square?

I am working on a project where I visualise the effect of a magnetic dipole, on a range of vectors, I'm just testing with one pole at the moment and something doesn't work but I don't know why.
the force that a vector receives is mapped onto a color to check if I did it right, these are my results:
so this is the canvas I'm working with
and when I lower the size of each vector and increase the density you can see this forms diamonds rather than a circular pattern.
Does anybody know why this is or what could be causing it?
code below here:
function calcForce(magnet, vector){
return 1/distance(magnet.x,magnet.y,vector.centerx,vector.centery) * magnet.force;
}
function distance(cx, cy, ex, ey){
var dy = Math.abs(ey - cy);
var dx = Math.abs(ex - cx);
return Math.sqrt((dx^2) + (dy^2));
}
function mapRainbow(value) {
return 'hsl(' + value + ',100%,50%)';
}
function map_range(value, low1, high1, low2, high2) {
return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
}
function mapForce(force){
return map_range(force,10,1000,20,40);
}
function drawStroke(stroke){
ctx.beginPath();
ctx.moveTo(stroke.x1,stroke.y1);
ctx.lineTo(stroke.x2,stroke.y2);
stroke.color = mapRainbow(stroke.force);
ctx.strokeStyle = stroke.color;
ctx.stroke();
ctx.closePath();
}
*this is not all the code by far but I think this is enough, need to see more? just ask.
Use distance to generate gradient:
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
function distance(x1, y1, x2, y2) {
return Math.sqrt((x2 -= x1) * x2 + (y2 -= y1) * y2);
}
function angleBetweenPoints(x1, y1, x2, y2) {
return (Math.atan2(x2 - x1, y2 - y1) + 2 * Math.PI);
}
var center = { x: 250, y: 250 };
var vectorLength = 15;
function max(v, m) {
if (v > m) {
return m;
}
return v;
}
function draw() {
if (Math.random() > 0.5) {
center.x += (Math.random() - 0.5) * 10;
}
else {
center.y += (Math.random() - 0.5) * 10;
}
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (var xIndex = 0; xIndex < canvas.width; xIndex += vectorLength) {
for (var yIndex = 0; yIndex < canvas.height; yIndex += vectorLength) {
var x = xIndex - (Math.random() * vectorLength * 0.0);
var y = yIndex - (Math.random() * vectorLength * 0.0);
var angle = angleBetweenPoints(center.x, center.y, x, y);
var dist = distance(x, y, center.x, center.y);
ctx.fillStyle = "rgb(" + Math.floor(max(dist, 255)) + "," + Math.floor((255 - max(dist, 255))) + ",0)";
ctx.translate((x + vectorLength * 0.5), (y + vectorLength * 0.5));
ctx.rotate(-angle);
ctx.fillRect(0 - vectorLength * 0.5, 0 - vectorLength * 0.5, vectorLength * 0.25, vectorLength * 0.75);
ctx.rotate(angle);
ctx.translate(0 - (x + vectorLength * 0.5), 0 - (y + vectorLength * 0.5));
}
}
ctx.fillRect(center.x + vectorLength / 2, center.y + vectorLength / 2, vectorLength, vectorLength);
requestAnimationFrame(function () {
setTimeout(draw, 1000 / 60);
});
}
draw();

Canvas animation (network nodes and edges): how to improve its performances, particularly on FireFox?

Having to give the idea of an ever changing network of nodes (each with different impact and possibly more than one color) connecting each other to create something.
I wanted to give it depth perception, so I ended up using two canvases around the title: one in the foreground, even over the words, and the other in background, with slightly larger and blurred elements.
Demo here, full JavaScript code at the moment:
// min and max radius, radius threshold and percentage of filled circles
var radMin = 5,
radMax = 125,
filledCircle = 60, //percentage of filled circles
concentricCircle = 30, //percentage of concentric circles
radThreshold = 25; //IFF special, over this radius concentric, otherwise filled
//min and max speed to move
var speedMin = 0.3,
speedMax = 2.5;
//max reachable opacity for every circle and blur effect
var maxOpacity = 0.6;
//default palette choice
var colors = ['52,168,83', '117,95,147', '199,108,23', '194,62,55', '0,172,212', '120,120,120'],
bgColors = ['52,168,83', '117,95,147', '199,108,23', '194,62,55', '0,172,212', '120,120,120'],
circleBorder = 10,
backgroundLine = bgColors[0];
var backgroundMlt = 0.85;
//min distance for links
var linkDist = Math.min(canvas.width, canvas.height) / 2.4,
lineBorder = 2.5;
//most importantly: number of overall circles and arrays containing them
var maxCircles = 12,
points = [],
pointsBack = [];
//populating the screen
for (var i = 0; i < maxCircles * 2; i++) points.push(new Circle());
for (var i = 0; i < maxCircles; i++) pointsBack.push(new Circle(true));
//experimental vars
var circleExp = 1,
circleExpMax = 1.003,
circleExpMin = 0.997,
circleExpSp = 0.00004,
circlePulse = false;
//circle class
function Circle(background) {
//if background, it has different rules
this.background = (background || false);
this.x = randRange(-canvas.width / 2, canvas.width / 2);
this.y = randRange(-canvas.height / 2, canvas.height / 2);
this.radius = background ? hyperRange(radMin, radMax) * backgroundMlt : hyperRange(radMin, radMax);
this.filled = this.radius < radThreshold ? (randint(0, 100) > filledCircle ? false : 'full') : (randint(0, 100) > concentricCircle ? false : 'concentric');
this.color = background ? bgColors[randint(0, bgColors.length - 1)] : colors[randint(0, colors.length - 1)];
this.borderColor = background ? bgColors[randint(0, bgColors.length - 1)] : colors[randint(0, colors.length - 1)];
this.opacity = 0.05;
this.speed = (background ? randRange(speedMin, speedMax) / backgroundMlt : randRange(speedMin, speedMax)); // * (radMin / this.radius);
this.speedAngle = Math.random() * 2 * Math.PI;
this.speedx = Math.cos(this.speedAngle) * this.speed;
this.speedy = Math.sin(this.speedAngle) * this.speed;
var spacex = Math.abs((this.x - (this.speedx < 0 ? -1 : 1) * (canvas.width / 2 + this.radius)) / this.speedx),
spacey = Math.abs((this.y - (this.speedy < 0 ? -1 : 1) * (canvas.height / 2 + this.radius)) / this.speedy);
this.ttl = Math.min(spacex, spacey);
};
Circle.prototype.init = function() {
Circle.call(this, this.background);
}
//support functions
//generate random int a<=x<=b
function randint(a, b) {
return Math.floor(Math.random() * (b - a + 1) + a);
}
//generate random float
function randRange(a, b) {
return Math.random() * (b - a) + a;
}
//generate random float more likely to be close to a
function hyperRange(a, b) {
return Math.random() * Math.random() * Math.random() * (b - a) + a;
}
//rendering function
function drawCircle(ctx, circle) {
//circle.radius *= circleExp;
var radius = circle.background ? circle.radius *= circleExp : circle.radius /= circleExp;
ctx.beginPath();
ctx.arc(circle.x, circle.y, radius * circleExp, 0, 2 * Math.PI, false);
ctx.lineWidth = Math.max(1, circleBorder * (radMin - circle.radius) / (radMin - radMax));
ctx.strokeStyle = ['rgba(', circle.borderColor, ',', circle.opacity, ')'].join('');
if (circle.filled == 'full') {
ctx.fillStyle = ['rgba(', circle.borderColor, ',', circle.background ? circle.opacity * 0.8 : circle.opacity, ')'].join('');
ctx.fill();
ctx.lineWidth=0;
ctx.strokeStyle = ['rgba(', circle.borderColor, ',', 0, ')'].join('');
}
ctx.stroke();
if (circle.filled == 'concentric') {
ctx.beginPath();
ctx.arc(circle.x, circle.y, radius / 2, 0, 2 * Math.PI, false);
ctx.lineWidth = Math.max(1, circleBorder * (radMin - circle.radius) / (radMin - radMax));
ctx.strokeStyle = ['rgba(', circle.color, ',', circle.opacity, ')'].join('');
ctx.stroke();
}
circle.x += circle.speedx;
circle.y += circle.speedy;
if (circle.opacity < (circle.background ? maxOpacity : 1)) circle.opacity += 0.01;
circle.ttl--;
}
//initializing function
function init() {
window.requestAnimationFrame(draw);
}
//rendering function
function draw() {
if (circlePulse) {
if (circleExp < circleExpMin || circleExp > circleExpMax) circleExpSp *= -1;
circleExp += circleExpSp;
}
var ctxfr = document.getElementById('canvas').getContext('2d');
var ctxbg = document.getElementById('canvasbg').getContext('2d');
ctxfr.globalCompositeOperation = 'destination-over';
ctxfr.clearRect(0, 0, canvas.width, canvas.height); // clear canvas
ctxbg.globalCompositeOperation = 'destination-over';
ctxbg.clearRect(0, 0, canvas.width, canvas.height); // clear canvas
ctxfr.save();
ctxfr.translate(canvas.width / 2, canvas.height / 2);
ctxbg.save();
ctxbg.translate(canvas.width / 2, canvas.height / 2);
//function to render each single circle, its connections and to manage its out of boundaries replacement
function renderPoints(ctx, arr) {
for (var i = 0; i < arr.length; i++) {
var circle = arr[i];
//checking if out of boundaries
if (circle.ttl<0) {}
var xEscape = canvas.width / 2 + circle.radius,
yEscape = canvas.height / 2 + circle.radius;
if (circle.ttl < -20) arr[i].init(arr[i].background);
//if (Math.abs(circle.y) > yEscape || Math.abs(circle.x) > xEscape) arr[i].init(arr[i].background);
drawCircle(ctx, circle);
}
for (var i = 0; i < arr.length - 1; i++) {
for (var j = i + 1; j < arr.length; j++) {
var deltax = arr[i].x - arr[j].x;
var deltay = arr[i].y - arr[j].y;
var dist = Math.pow(Math.pow(deltax, 2) + Math.pow(deltay, 2), 0.5);
//if the circles are overlapping, no laser connecting them
if (dist <= arr[i].radius + arr[j].radius) continue;
//otherwise we connect them only if the dist is < linkDist
if (dist < linkDist) {
var xi = (arr[i].x < arr[j].x ? 1 : -1) * Math.abs(arr[i].radius * deltax / dist);
var yi = (arr[i].y < arr[j].y ? 1 : -1) * Math.abs(arr[i].radius * deltay / dist);
var xj = (arr[i].x < arr[j].x ? -1 : 1) * Math.abs(arr[j].radius * deltax / dist);
var yj = (arr[i].y < arr[j].y ? -1 : 1) * Math.abs(arr[j].radius * deltay / dist);
ctx.beginPath();
ctx.moveTo(arr[i].x + xi, arr[i].y + yi);
ctx.lineTo(arr[j].x + xj, arr[j].y + yj);
var samecolor = arr[i].color == arr[j].color;
ctx.strokeStyle = ["rgba(", arr[i].borderColor, ",", Math.min(arr[i].opacity, arr[j].opacity) * ((linkDist - dist) / linkDist), ")"].join("");
ctx.lineWidth = (arr[i].background ? lineBorder * backgroundMlt : lineBorder) * ((linkDist - dist) / linkDist); //*((linkDist-dist)/linkDist);
ctx.stroke();
}
}
}
}
var startTime = Date.now();
renderPoints(ctxfr, points);
renderPoints(ctxbg, pointsBack);
deltaT = Date.now() - startTime;
ctxfr.restore();
ctxbg.restore();
window.requestAnimationFrame(draw);
}
init();
I asked around and ctx.save() and ctx.restore() are in the top list of suspects, but I wouldn't know how to do this without them.
This is my first animation with canvas, which AFAIK should have been the best option in terms of cross-browser support and (decent) performances, but any advice on this side is still welcome; also, seems to slow down significantly on FF, but just on some machines where hardware acceleration does not work properly (or at all).
From what I read here (and basically everywhere else), FF seems to have serious issues dealing with canvas, but maybe I can optimize things a bit more.
Should I use something other than canvas to do the animation? But also consider that other options (like using SVG) seem to have less support, not to mention it would mean redoing most of the work.
Notes: The first part with the general variables might not be the best practice, but it worked to let a non-technical staff member (UI designer) play on the variables to see different results.

Why are some of my angles incorrect when changing?

I have programed a program with JavaScript and the DOM that shows a ball bouncing in a box; it is a canvas element. The only things that do not work correctly are the angles. This is not noticeable until the angles are small, when it is clear the ball did not bounce back correctly from the box. The angle may be coming to a box gradually, and then bounce very steeply back. This seems like perhaps instead of the angle-in=angle-out, the angle that was being headed from was what was output angle. This would be equivalent of an angle in and its compliment out. The problem seems to happen with only half the types of bounces: it might not happen on one wall coming in a direction, but would on another wall coming in a direction.
: http://i.stack.imgur.com/WviEd.gif
I have posted all the code for ability for the test of the code, and a gradual angle is used, so the problem can be seen, but the angles that are the problem are in the checkAngle function.
<!doctype html>
<script src="code/chapter/15_game.js"></script>
<script src="code/game_levels.js"></script>
<script src="code/chapter/16_canvas.js"></script>
<canvas width="400" height="400"></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var lastTime = null;
function frame(time) {
if (lastTime != null)
updateAnimation(Math.min(100, time - lastTime) / 1000);
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
var x = y = 200//, angle = 2 * Math.PI * Math.random();
var angle = 2 * Math.PI / 40 + Math.PI;
function checkAngle(angle) {
if(x + 10 >= 400) {
if(angle <= Math.PI)
return angle = (Math.PI/2) + ((Math.PI / 2) - reduceAngle(angle));
else if(angle > Math.PI)
return angle = (3 * Math.PI / 2) - ((Math.PI / 2) - reduceAngle(angle));
}else if(x - 10 <= 0) {
if(angle <= Math.PI)
return angle = Math.PI/2 - reduceAngle(angle);
else if(angle > Math.PI)
return angle = 3* Math.PI/2 + (Math.PI/2 - reduceAngle(angle));
}else if(y - 10 <= 0) {
if(angle >= 3 * Math.PI /2)
return angle = Math.PI/2 - reduceAngle(angle);
else if(angle < 3 * Math.PI/2)
return angle = Math.PI - (Math.PI / 2 - reduceAngle(angle));
}else if(y + 10 >= 400) {
if(angle <= Math.PI/2)
return angle = 2*Math.PI - (Math.PI / 2 - reduceAngle(angle));
else if(angle > Math.PI/2)
return angle = Math.PI + (Math.PI / 2 - reduceAngle(angle));
}else
return angle;
}
function reduceAngle(angle) {
if(angle < Math.PI / 2) {
return angle;
}else{
angle = angle - (Math.PI / 2);
return reduceAngle (angle);
}
}
function updateAnimation(step) {
cx.clearRect(0, 0, 400, 400);
cx.lineWidth = 4;
cx.strokeRect(0, 0, 400, 400);
angle = checkAngle(angle);
x += Math.cos(angle) * step * 200;
y += Math.sin(angle) * step * 200;
cx.lineWidth = 2;
cx.beginPath();
cx.arc(x, y, 20, 0, 7);
cx.stroke();
}
</script>
When dealing with reflections in a non-rotated box you don't really need to deal with angles when reflecting. Just define an initial vector based on angle.
var vector = {
x: speed * Math.cos(angle),
y: speed * Math.sin(angle)
};
Then you simply check bounds for x and y separately and inverse the slope-value for each axis:
if (x - radius <= 0 || x + radius>= 400) {
vector.x = -vector.x; // reflect x
}
if (y - radius<= 0 || y + radius> 400) {
vector.y = -vector.y; // reflect y
}
You can always adjust the angle on the fly by adding another vector with the delta-angle.
If your bounce box would not be 0° rotated, then check out this answer for vector-reflection.
For example
Using your code as a basis, this would be implemented like this:
var cx = document.querySelector("canvas").getContext("2d");
var lastTime = null;
var x, y;
// calculate an initial vector
var angle = 20 / 180 * Math.PI; // angle -> radians
var speed = 5;
var vector = {
x: speed * Math.cos(angle),
y: speed * Math.sin(angle)
};
x = y = 200;
function frame(time) {
if (lastTime != null)
updateAnimation(Math.min(100, time - lastTime) / 1000);
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
function checkAngle() {
var dlt = 10 + 2 + 4; //include radius and line-widths;
if (x - dlt <= 0 || x + dlt >= 400) {
vector.x = -vector.x; // reflect x
}
if (y - dlt <= 0 || y + dlt > 400) {
vector.y = -vector.y; // reflect y
}
}
function updateAnimation(step) {
cx.clearRect(0, 0, 400, 400);
cx.lineWidth = 4;
cx.strokeRect(0, 0, 400, 400);
x += vector.x; // use our vector
y += vector.y;
checkAngle(); // test for hits
cx.lineWidth = 2;
cx.beginPath();
cx.arc(x, y, 20, 0, 7);
cx.stroke();
}
<canvas width="400" height="400"></canvas>
Judging by what I've seen in your drawings and demonstrations, the angles are being taken off the wrong axis. If approaching at 20 degrees, you'd expect the ball to leave at 180-20 degrees. Instead what it's doing is leaving at 90+20 degrees.
While I can't find the precise place in your code that makes this error, I felt I had to point this out in the hopes that someone can improve upon it.

Calculate if line crosses circle, weird behavior at certain angles

Okay, I want to be able to calculate whether a line crosses a circle(at least a part of the line inside the circle). I found several answers to this, but I thought they were too complicated so I came up with this. I'm no math guy, so I'm kinda stuck now. When the line is aligned vertically the "radius >= Math.sqrt(len * len + len * len - o);" becomes true( with 45° angles it becomes 0). I have no clue why this happens. Thanks :)
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
cx -= sx; x -= sx; //sx is the first point's x position
cy -= sy; y -= sy;//sy is the first point's y position
len = Math.sqrt((cy * cy) + (cx * cx))//hypotenuse of circle (cy, cx) to (0, 0) (with offset)
atanx = Math.atan(y / x); //angle of (0, 0) to (x, y) in radians
atany = atanx - Math.atan(cy / cx); //to center
var o = 2 * len * len * Math.cos(atany);
var o = o < 0 ? -o:o//Had to do this, at some point the value can become inverted
return radius >= Math.sqrt(len * len + len * len - o);
}
Edit:
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
cx -= sx; x -= sx; //sx is the first point's x position
cy -= sy; y -= sy;//sy is the first point's y position
ctp = Math.sin(Math.atan(y / x) - Math.atan(cy / cx)) * Math.sqrt((cy * cy) + (cx * cx));
return radius >= ctp && ctp >= -radius;
}
Works pretty much the same but is faster. The problem is that it calculates an infinite line. How would I fix that?
Edit 2:
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
cx -= sx; x -= sx;
cy -= sy; y -= sy;
var h = Math.sqrt(cy * cy + cx * cx)
ctp = Math.sin(Math.atan(y / x) - Math.atan(cy / cx)) * h;
sideb = Math.sqrt(h * h - ctp * ctp);
line = Math.sqrt(x * x + y * y)
if (sideb - radius > line) {return false}
return radius >= ctp && ctp >= -radius;
}
Partial fix, doesn't go on to infinity for one direction from the line(line end)
Edit 3:
A bit longer but more than twice as fast, back to square one
function lineInCircle2(sx, sy, x, y, cx, cy, radius) {
var ysy = y - sy
var xsx = x - sx
var k = ((y-sy) * (cx-sx) - (x-sx) * (cy-sy)) / (ysy * ysy + xsx * xsx)
var ncx = cx - k * (y-sy)
var ncy = cy + k * (x-sx)
ncx -= cx
ncy -= cy
var ctp = Math.sqrt(ncx * ncx + ncy * ncy)
return radius >= ctp && ctp >= -radius;
}
Edit 4:
Success!
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
if (sx > cx + radius && x > cx + radius || x < cx - radius && sx < cx - radius) {return false;}
if (sy > cy + radius && y > cy + radius || y < cy - radius && sy < cy - radius) {return false;}
var k = ((y - sy) * (cx - sx) - (x - sx) * (cy - sy)) / ((y - sy) * (y - sy) + (x - sx) * (x - sx))
var ncx = k * (y - sy)
var ncy = k * (x - sx)
return radius >= Math.sqrt(ncx * ncx + ncy * ncy);
}
Does exactly what I want, I optimized it down to 4.5 - 4.6 seconds for 100000000 iterations compared for 10+ secs for the first version and still is much more accurate(meaning no more weird behavior in certain angles). I'm satisfied :D
Too much work. Find the normal that passes through the center, and see if the intersection is closer than the radius.
function lineInCircle(sx, sy, x, y, cx, cy, radius) {
if (sx > cx + radius && x > cx + radius || x < cx - radius && sx < cx - radius) {return false;}
if (sy > cy + radius && y > cy + radius || y < cy - radius && sy < cy - radius) {return false;}
var k = ((y - sy) * (cx - sx) - (x - sx) * (cy - sy)) / ((y - sy) * (y - sy) + (x - sx) * (x - sx))
var ncx = k * (y - sy)
var ncy = k * (x - sx)
return radius >= Math.sqrt(ncx * ncx + ncy * ncy);
}
Takes about 4.5 - 4.6 seconds for 100000000 iterations to finish on my machine.

Categories

Resources