I'm taking the following approach to animate a star field across the screen, but I'm stuck for the next part.
JS
var c = document.getElementById('stars'),
ctx = c.getContext("2d"),
t = 0; // time
c.width = 300;
c.height = 300;
var w = c.width,
h = c.height,
z = c.height,
v = Math.PI; // angle of vision
(function animate() {
Math.seedrandom('bg');
ctx.globalAlpha = 1;
for (var i = 0; i <= 100; i++) {
var x = Math.floor(Math.random() * w), // pos x
y = Math.floor(Math.random() * h), // pos y
r = Math.random()*2 + 1, // radius
a = Math.random()*0.5 + 0.5, // alpha
// linear
d = (r*a), // depth
p = t*d; // pixels per t
x = x - p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
(function draw(x,y) {
var gradient = ctx.createRadialGradient(x, y, 0, x + r, y + r, r * 2);
gradient.addColorStop(0, 'rgba(255, 255, 255, ' + a + ')');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.beginPath();
ctx.arc(x, y, r, 0, 2*Math.PI);
ctx.fillStyle = gradient;
ctx.fill();
return draw;
})(x, y);
}
ctx.restore();
t += 1;
requestAnimationFrame(function() {
ctx.clearRect(0, 0, c.width, c.height);
animate();
});
})();
HTML
<canvas id="stars"></canvas>
CSS
canvas {
background: black;
}
JSFiddle
What it does right now is animate each star with a delta X that considers the opacity and size of the star, so the smallest ones appear to move slower.
Use p = t; to have all the stars moving at the same speed.
QUESTION
I'm looking for a clearly defined model where the velocities give the illusion of the stars rotating around the expectator, defined in terms of the center of the rotation cX, cY, and the angle of vision v which is what fraction of 2π can be seen (if the center of the circle is not the center of the screen, the radius should be at least the largest portion). I'm struggling to find a way that applies this cosine to the speed of star movements, even for a centered circle with a rotation of π.
These diagrams might further explain what I'm after:
Centered circle:
Non-centered:
Different angle of vision:
I'm really lost as to how to move forwards. I already stretched myself a bit to get here. Can you please help me with some first steps?
Thanks
UPDATE
I have made some progress with this code:
// linear
d = (r*a)*z, // depth
v = (2*Math.PI)/w,
p = Math.floor( d * Math.cos( t * v ) ); // pixels per t
x = x + p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
JSFiddle
Where p is the x coordinate of a particle in uniform circular motion and v is the angular velocity, but this generates a pendulum effect. I am not sure how to change these equations to create the illusion that the observer is turning instead.
UPDATE 2:
Almost there. One user at the ##Math freenode channel was kind enough to suggest the following calculation:
// linear
d = (r*a), // depth
p = t*d; // pixels per t
x = x - p; // movement
x = x - w * Math.floor(x / w); // go around when x < 0
x = (x / w) - 0.5;
y = (y / h) - 0.5;
y /= Math.cos(x);
x = (x + 0.5) * w;
y = (y + 0.5) * h;
JSFiddle
This achieves the effect visually, but does not follow a clearly defined model in terms of the variables (it just "hacks" the effect) so I cannot see a straightforward way to do different implementations (change the center, angle of vision). The real model might be very similar to this one.
UPDATE 3
Following from Iftah's response, I was able to use Sylvester to apply a rotation matrix to the stars, which need to be saved in an array first. Also each star's z coordinate is now determined and the radius r and opacity a are derived from it instead. The code is substantially different and lenghthier so I am not posting it, but it might be a step in the right direction. I cannot get this to rotate continuously yet. Using matrix operations on each frame seems costly in terms of performance.
JSFiddle
Here's some pseudocode that does what you're talking about.
Make a bunch of stars not too far but not too close (via rejection sampling)
Set up a projection matrix (defines the camera frustum)
Each frame
Compute our camera rotation angle
Make a "view" matrix (repositions the stars to be relative to our view)
Compose the view and projection matrix into the view-projection matrix
For each star
Apply the view-projection matrix to give screen star coordinates
If the star is behind the camera skip it
Do some math to give the star a nice seeming 'size'
Scale the star coordinate to the canvas
Draw the star with its canvas coordinate and size
I've made an implementation of the above. It uses the gl-matrix Javascript library to handle some of the matrix math. It's good stuff. (Fiddle for this is here, or see below.)
var c = document.getElementById('c');
var n = c.getContext('2d');
// View matrix, defines where you're looking
var viewMtx = mat4.create();
// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();
// Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
// We'll assume input parameters are sane.
field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
var frustum_depth = far_dist - near_dist;
var one_over_depth = 1 / frustum_depth;
var e11 = 1.0 / Math.tan(0.5 * field_of_view);
var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
var e22 = far_dist * one_over_depth;
var e32 = (-far_dist * near_dist) * one_over_depth;
return [
e00, 0, 0, 0,
0, e11, 0, 0,
0, 0, e22, e32,
0, 0, 1, 0
];
}
// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
angle = angle * Math.PI / 180.0; // Convert degrees to radians
return [
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, 0, 1
];
}
projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);
var angle = 0;
var viewProjMtx = mat4.create();
var minDist = 100;
var maxDist = 1000;
function Star() {
var d = 0;
do {
// Create random points in a cube.. but not too close.
this.x = Math.random() * maxDist - (maxDist / 2);
this.y = Math.random() * maxDist - (maxDist / 2);
this.z = Math.random() * maxDist - (maxDist / 2);
var d = this.x * this.x +
this.y * this.y +
this.z * this.z;
} while (
d > maxDist * maxDist / 4 || d < minDist * minDist
);
this.dist = Math.sqrt(d);
}
Star.prototype.AsVector = function() {
return [this.x, this.y, this.z, 1];
}
var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());
var lastLoop = Date.now();
function loop() {
var now = Date.now();
var dt = (now - lastLoop) / 1000.0;
lastLoop = now;
angle += 30.0 * dt;
viewMtx = ComputeViewMtx(angle);
//console.log('---');
//console.log(projMtx);
//console.log(viewMtx);
mat4.multiply(viewProjMtx, projMtx, viewMtx);
//console.log(viewProjMtx);
n.beginPath();
n.rect(0, 0, c.width, c.height);
n.closePath();
n.fillStyle = '#000';
n.fill();
n.fillStyle = '#fff';
var v = vec4.create();
for (var i = 0; i < stars.length; i++) {
var star = stars[i];
vec4.transformMat4(v, star.AsVector(), viewProjMtx);
v[0] /= v[3];
v[1] /= v[3];
v[2] /= v[3];
//v[3] /= v[3];
if (v[3] < 0) continue;
var x = (v[0] * 0.5 + 0.5) * c.width;
var y = (v[1] * 0.5 + 0.5) * c.height;
// Compute a visual size...
// This assumes all stars are the same size.
// It also doesn't scale with canvas size well -- we'd have to take more into account.
var s = 300 / star.dist;
n.beginPath();
n.arc(x, y, s, 0, Math.PI * 2);
//n.rect(x, y, s, s);
n.closePath();
n.fill();
}
window.requestAnimationFrame(loop);
}
loop();
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<canvas id="c" width="500" height="500"></canvas>
Some links:
More on projection matrices
gl-matrix
Using view/projection matrices
Update
Here's another version that has keyboard controls. Kinda fun. You can see the difference between rotating and parallax from strafing. Works best full page. (Fiddle for this is here or see below.)
var c = document.getElementById('c');
var n = c.getContext('2d');
// View matrix, defines where you're looking
var viewMtx = mat4.create();
// Projection matrix, defines how the view maps onto the screen
var projMtx = mat4.create();
// Adapted from http://stackoverflow.com/questions/18404890/how-to-build-perspective-projection-matrix-no-api
function ComputeProjMtx(field_of_view, aspect_ratio, near_dist, far_dist, left_handed) {
// We'll assume input parameters are sane.
field_of_view = field_of_view * Math.PI / 180.0; // Convert degrees to radians
var frustum_depth = far_dist - near_dist;
var one_over_depth = 1 / frustum_depth;
var e11 = 1.0 / Math.tan(0.5 * field_of_view);
var e00 = (left_handed ? 1 : -1) * e11 / aspect_ratio;
var e22 = far_dist * one_over_depth;
var e32 = (-far_dist * near_dist) * one_over_depth;
return [
e00, 0, 0, 0,
0, e11, 0, 0,
0, 0, e22, e32,
0, 0, 1, 0
];
}
// Make a view matrix with a simple rotation about the Y axis (up-down axis)
function ComputeViewMtx(angle) {
angle = angle * Math.PI / 180.0; // Convert degrees to radians
return [
Math.cos(angle), 0, Math.sin(angle), 0,
0, 1, 0, 0,
-Math.sin(angle), 0, Math.cos(angle), 0,
0, 0, -250, 1
];
}
projMtx = ComputeProjMtx(70, c.width / c.height, 1, 200, true);
var angle = 0;
var viewProjMtx = mat4.create();
var minDist = 100;
var maxDist = 1000;
function Star() {
var d = 0;
do {
// Create random points in a cube.. but not too close.
this.x = Math.random() * maxDist - (maxDist / 2);
this.y = Math.random() * maxDist - (maxDist / 2);
this.z = Math.random() * maxDist - (maxDist / 2);
var d = this.x * this.x +
this.y * this.y +
this.z * this.z;
} while (
d > maxDist * maxDist / 4 || d < minDist * minDist
);
this.dist = 100;
}
Star.prototype.AsVector = function() {
return [this.x, this.y, this.z, 1];
}
var stars = [];
for (var i = 0; i < 5000; i++) stars.push(new Star());
var lastLoop = Date.now();
var dir = {
up: 0,
down: 1,
left: 2,
right: 3
};
var dirStates = [false, false, false, false];
var shiftKey = false;
var moveSpeed = 100.0;
var turnSpeed = 1.0;
function loop() {
var now = Date.now();
var dt = (now - lastLoop) / 1000.0;
lastLoop = now;
angle += 30.0 * dt;
//viewMtx = ComputeViewMtx(angle);
var tf = mat4.create();
if (dirStates[dir.up]) mat4.translate(tf, tf, [0, 0, moveSpeed * dt]);
if (dirStates[dir.down]) mat4.translate(tf, tf, [0, 0, -moveSpeed * dt]);
if (dirStates[dir.left])
if (shiftKey) mat4.rotate(tf, tf, -turnSpeed * dt, [0, 1, 0]);
else mat4.translate(tf, tf, [moveSpeed * dt, 0, 0]);
if (dirStates[dir.right])
if (shiftKey) mat4.rotate(tf, tf, turnSpeed * dt, [0, 1, 0]);
else mat4.translate(tf, tf, [-moveSpeed * dt, 0, 0]);
mat4.multiply(viewMtx, tf, viewMtx);
//console.log('---');
//console.log(projMtx);
//console.log(viewMtx);
mat4.multiply(viewProjMtx, projMtx, viewMtx);
//console.log(viewProjMtx);
n.beginPath();
n.rect(0, 0, c.width, c.height);
n.closePath();
n.fillStyle = '#000';
n.fill();
n.fillStyle = '#fff';
var v = vec4.create();
for (var i = 0; i < stars.length; i++) {
var star = stars[i];
vec4.transformMat4(v, star.AsVector(), viewProjMtx);
if (v[3] < 0) continue;
var d = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
v[0] /= v[3];
v[1] /= v[3];
v[2] /= v[3];
//v[3] /= v[3];
var x = (v[0] * 0.5 + 0.5) * c.width;
var y = (v[1] * 0.5 + 0.5) * c.height;
// Compute a visual size...
// This assumes all stars are the same size.
// It also doesn't scale with canvas size well -- we'd have to take more into account.
var s = 300 / d;
n.beginPath();
n.arc(x, y, s, 0, Math.PI * 2);
//n.rect(x, y, s, s);
n.closePath();
n.fill();
}
window.requestAnimationFrame(loop);
}
loop();
function keyToDir(evt) {
var d = -1;
if (evt.keyCode === 38) d = dir.up
else if (evt.keyCode === 37) d = dir.left;
else if (evt.keyCode === 39) d = dir.right;
else if (evt.keyCode === 40) d = dir.down;
return d;
}
window.onkeydown = function(evt) {
var d = keyToDir(evt);
if (d >= 0) dirStates[d] = true;
if (evt.keyCode === 16) shiftKey = true;
}
window.onkeyup = function(evt) {
var d = keyToDir(evt);
if (d >= 0) dirStates[d] = false;
if (evt.keyCode === 16) shiftKey = false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.3.1/gl-matrix-min.js"></script>
<div>Click in this pane. Use up/down/left/right, hold shift + left/right to rotate.</div>
<canvas id="c" width="500" height="500"></canvas>
Update 2
Alain Jacomet Forte asked:
What is your recommended method of creating general purpose 3d and if you would recommend working at the matrices level or not, specifically perhaps to this particular scenario.
Regarding matrices: If you're writing an engine from scratch on any platform, then you're unavoidably going to end up working with matrices since they help generalize the basic 3D mathematics. Even if you use OpenGL/WebGL or Direct3D you're still going to end up making a view and projection matrix and additional matrices for more sophisticated purposes. (Handling normal maps, aligning world objects, skinning, etc...)
Regarding a method of creating general purpose 3d... Don't. It will run slow, and it won't be performant without a lot of work. Rely on a hardware-accelerated library to do the heavy lifting. Creating limited 3D engines for specific projects is fun and instructive (e.g. I want a cool animation on my webpage), but when it comes to putting the pixels on the screen for anything serious, you want hardware to handle that as much as you can for performance purposes.
Sadly, the web has no great standard for that yet, but it is coming in WebGL -- learn WebGL, use WebGL. It runs great and works well when it's supported. (You can, however, get away with an awful lot just using CSS 3D transforms and Javascript.)
If you're doing desktop programming, I highly recommend OpenGL via SDL (I'm not sold on SFML yet) -- it's cross-platform and well supported.
If you're programming mobile phones, OpenGL ES is pretty much your only choice (other than a dog-slow software renderer).
If you want to get stuff done rather than writing your own engine from scratch, the defacto for the web is Three.js (which I find effective but mediocre). If you want a full game engine, there's some free options these days, the main commercial ones being Unity and Unreal. Irrlicht has been around a long time -- never had a chance to use it, though, but I hear it's good.
But if you want to make all the 3D stuff from scratch... I always found how the software renderer in Quake was made a pretty good case study. Some of that can be found here.
You are resetting the stars 2d position each frame, then moving the stars (depending on how much time and speed of each star) - this is a bad way to achieve your goal. As you discovered, it gets very complex when you try to extend this solution to more scenarios.
A better way would be to set the stars 3d location only once (at initialization) then move a "camera" each frame (depending on time). When you want to render the 2d image you then calculate the stars location on screen. The location on screen depends on the stars 3d location and the current camera location.
This will allow you to move the camera (in any direction), rotate the camera (to any angle) and render the correct stars position AND keep your sanity.
Related
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
let last = 1
let start = 1
let i = 0
let origin = [250, 250]
for (let i2 = 0; i2 < 20; i2++) {
ctx.ellipse(...origin, start, start, Math.PI / 2 * i, 0, Math.PI / 2);
i++
i %= 4
if (i == 1) origin[1] -= last
else if (i == 2) origin[0] += last
else if (i == 3) origin[1] += last
else if (i == 0) origin[0] -= last;
[last, start] = [start, start + last]
}
ctx.stroke();
ctx.beginPath()
ctx.lineCap = 'round'
ctx.lineWidth = 7
ctx.strokeStyle = "red";
ctx.lineTo(400, 400)
ctx.stroke()
<canvas width="500" height="500" style="border:1px solid #000000;"></canvas>
What is the simplest way to make the spiral line go through an arbitrary point in the canvas? For example 400x 400y. I think adjusting the initial start and last values based on some calculation could work. The only difference between the first code snippet and the second one is the initial last and start variables. Other solutions that rewrite the entire thing are welcome too.
const canvas = document.querySelector( 'canvas' );
const ctx = canvas.getContext( '2d' );
ctx.strokeStyle = "black";
ctx.lineWidth = 1;
ctx.beginPath();
let last = 0.643
let start = 0.643
let i = 0
let origin = [250,250]
for (let i2=0; i2<20; i2++) {
ctx.ellipse(...origin, start, start, Math.PI/2 *i , 0, Math.PI /2);
i++
i%=4
if (i==1) origin[1] -= last
if (i==2) origin[0] += last
if (i==3) origin[1] += last
if (i==0) origin[0] -= last
;[last, start] = [start, start + last]
}
ctx.stroke();
ctx.beginPath()
ctx.lineCap = 'round'
ctx.lineWidth = 7
ctx.strokeStyle = "red";
ctx.lineTo(400, 400)
ctx.stroke()
<canvas width="500" height="500" style="border:1px solid #000000;"></canvas>
I am not sure how you want the spiral to intercept the point. There are twp options.
Rotate the spiral to intercept point
Scale the spiral to intercept point
This answer solves using method 1. Method 2 has some problems as the number of turns can grow exponentially making the rendering very slow if we don't set limits to where the point of intercept can be.
Not a spiral
The code you provided does not draw a spiral in the mathematical definition but rather is just a set of connected ellipsoids.
This means that there is more than one function that defines a point on these connected curves. To solve for a point will require some complexity as each possible curve must be solved and the solution then vetted to locate the correct curve. On top of that ellipsoids I find to result in some very ugly math.
A spiral function
If we define the curve as just one function where the spiral radius is defined by the angle, it is then very easy to solve.
The function for the radius can be a simplified polynomial in the form Ax^P+C where x is the angle, P is the spiralness (for want of a better term), A is the scale (again for want of a better term) and C is the start angle
C is there if you want to make the step angle of the spiral be a set length eg 1 px would be angle += 1 / (Ax^P+C) If C is 0 then 1/0 would result in an infinite loop.
Drawing the spiral
As defined above there are many types of spirals that can be rendered so there should be one that is close to the spiral you have.
Any point on the spiral is found as follows
x = cos(angle) * f(angle) + origin.x
y = sin(angle) * f(angle) + origin.y
where f is the poly f(x) = Ax^P+C
The following function draws a basic linear spiral f(x) = 1*x^1+0.1
function drawSpiral(origin) {
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
let i = 0;
while (i < 5) {
const r = i + 0.1; // f(x) = 1*x^1+0.1
ctx.lineTo(
Math.cos(i) * r + origin.x,
Math.sin(i) * r + origin.y
);
i += 0.1
}
ctx.stroke();
}
Solve to pass though point
To solve for a point we convert the point to a polar coordinate relative to the origin. See functions pointDist , pointAngle. We then solve for Ax^P+C = dist in terms of x (the angle) and dist the distance from the origin. Then subtract the angle to the point to get the spirals orientation. (NOTE ^ means to power of, rest of answer uses JavaScripts **)
To solve an arbitrary polynomial can become rather complex that is why I used the simplified version.
The function A * x ** P + C = pointDist(point) needs to be rearranged in terms of pointDist(point).
This gives x = ((pointDist(point) - C) / A) ** (1 / P)
And then subtract the polar angle x = ((pointDist(point)- C) / A) ** (1 / P) - pointAngle(point) and we have the angle offset so that the spiral will intercept the point.
Example
A working example in case the above was TLDR or had too much math like jargon.
The example defines a spiral via the coefficients of the radius function A, C, and P.
There are 3 example spirals Black, Blue, and Green.
A spiral is drawn until its radius is greater than the diagonal distance to the canvas corner. The origin is the center of the canvas.
The point to intercept is set by the mouse position over the page.
The spirals are only rendered when the mouse position changes.
The solution for the simplified polynomial is shown in steps in the function startAngle.
While I wrote the code I seam to have lost the orientation and thus needed to add 180 deg to the start angle (Math.PI) or the point ends up midway between spiral arms.
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0}, mouseOld = {x : undefined, y : undefined};
document.addEventListener("mousemove", (e) => { mouse.x = e.pageX; mouse.y = e.pageY });
requestAnimationFrame(loop);
const TURNS = 4 * Math.PI * 2;
let origin = {x: canvas.width / 2, y: canvas.height / 2};
scrollTo(0, origin.y - innerHeight / 2);
const maxRadius = (origin.x ** 2 + origin.y ** 2) ** 0.5; // dist from origin to corner
const pointDist = (p1, p2) => Math.hypot(p1.x - p2.x, p1.y - p2.y);
const pointAngle = (p1, p2) => Math.atan2(p1.y - p2.y, p1.x - p2.x);
const radius = (x, spiral) => spiral.A * x ** spiral.P + spiral.C;
const startAngle = (origin, point, spiral) => {
const dist = pointDist(origin, point);
const ang = pointAngle(origin, point);
// Da math
// from radius function A * x ** P + C
// where x is ang
// A * x ** P + C = dist
// A * x ** P = dist - C
// x ** P = (dist - C) / A
// x = ((dist - C) / A) ** (1 / p)
return ((dist - spiral.C) / spiral.A) ** (1 / spiral.P) - ang;
}
// F for Fibonacci
const startAngleF = (origin, point, spiral) => {
const dist = pointDist(origin, point);
const ang = pointAngle(origin, point);
return (1 / spiral.P) * Math.log(dist / spiral.A) - ang;
}
const radiusF = (x, spiral) => spiral.A * Math.E ** (spiral.P * x);
const spiral = (P, A, C, rFc = radius, aFc = startAngle) => ({P, A, C, rFc, aFc});
const spirals = [
spiral(2, 1, 0.1),
spiral(3, 0.25, 0.1),
spiral(0.3063489,0.2972713047, null, radiusF, startAngleF),
spiral(0.8,4, null, radiusF, startAngleF),
];
function drawSpiral(origin, point, spiral, col) {
const start = spiral.aFc(origin, point, spiral);
ctx.strokeStyle = col;
ctx.beginPath();
let i = 0;
while (i < TURNS) {
const r = spiral.rFc(i, spiral);
const ang = i - start - Math.PI;
ctx.lineTo(
Math.cos(ang) * r + origin.x,
Math.sin(ang) * r + origin.y
);
if (r > maxRadius) { break }
i += 0.1
}
ctx.stroke();
}
loop()
function loop() {
if (mouse.x !== mouseOld.x || mouse.y !== mouseOld.y) {
ctx.clearRect(0, 0, 500, 500);
ctx.lineWidth = 1;
drawSpiral(origin, mouse, spirals[0], "#FFF");
drawSpiral(origin, mouse, spirals[1], "#0FF");
ctx.lineWidth = 4;
drawSpiral(origin, mouse, spirals[2], "#FF0");
drawSpiral(origin, mouse, spirals[3], "#AF0");
ctx.beginPath();
ctx.lineCap = "round";
ctx.lineWidth = 7;
ctx.strokeStyle = "red";
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
Object.assign(mouseOld, mouse);
}
requestAnimationFrame(loop);
}
canvas { position : absolute; top : 0px; left : 0px; background: black }
<canvas id="canvas" width = "500" height = "500"></canvas>
UPDATE
As requested in the comments
I have added the Fibonacci spiral to the example
The radius function is radiusF
The function to find the start angle to intercept a point is startAngleF
The two new Fibonacci spirals are colored limeGreen and Yellow
To use the Fibonacci spiral you must include the functions radiusF and startAngleF when defining the spiral eg spiral(1, 1, 0, radiusF, startAngleF)
Note the 3rd argument is not used and is zero in the eg above. as I don't think you will need it
I have an object I want to make orbit a star. I've managed to make the object move towards the star, but now I need to set up lateral movement as well.
Obviously this isn't as easy as just adjusting X, as when it moves round to the side of the star I'll have to adjust Y as well. I'm wondering how I could use some math to figure out how much I need to adjust X and Y by as the object moves around the star.
Here's my code so far:
var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
c.width = window.innerWidth;
c.height = window.innerHeight;
var star = {
x: c.width / 2,
y: c.height / 2,
r: 100,
g: 2,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'orange';
ctx.fill();
ctx.closePath();
}
};
var node = {
x: c.width / 2,
y: 100,
r: 20,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.closePath();
}
};
//GAME LOOP
function gameLoop()
{
update();
render();
window.requestAnimationFrame(gameLoop);
}
function update()
{
//Move towards star
var dx = star.x - node.x;
var dy = star.y - node.y;
var angle = Math.atan2(dy, dx);
node.x += Math.cos(angle);
node.y += Math.sin(angle);
//Lateral movement
node.x += 2;
}
function render()
{
ctx.clearRect(0, 0, c.width, c.height);
star.draw();
node.draw();
}
window.requestAnimationFrame(gameLoop);
<html>
<head>
<style>
body
{
margin: 0;
padding: 0;
overflow: hidden;
}
#canvas
{
background-color: #001319;
}
</style>
</head>
<body>
<canvas id="canvas">
</canvas>
<script src="Orbit.js"></script>
</body>
</html>
Newton's and Kepler's clockwork universe
When Newton worked out the maths for calculating orbits he noted something that prompted him to coin the term "clockwork universe". In a two body simulation the orbital paths of both objects repeat precisely. This mean that both objects will at the same time be in the exact same position at the exact same speed they were in the last orbit, there will be no precession.
Gravity, force, mass, and distance.
For a more accurate model of gravity you can use the laws of gravity as discovered by Newton. F = G * (m1*m2)/(r*r) where F is force, G is the Gravitational constant (and for simulations it is just a scaling factor) m1,m2 are the mass of each body and r is the distance between the bodies.
Mass of a sphere
We give both the star and planet some mass. Let's say that in the computer 1 pixel cubed is equal to 1 unit mass. Thus the mass of a sphere of radius R is 4/3 * R3 * PI.
Force, mass, and acceleration
The force is always applied along the line between the bodies and is called acceleration.
When a force is applied to an object we use another of Newton's discovered laws, F=ma where a is acceleration. We have the F (force) and m (mass) so now all we need is a. Rearrange F=ma to get a = f/m.
If we look at both formulas in terms of a (acceleration) a = (G * (m1*m2)/(r*r)) / m1 we can see that the mass of the object we are apply force to is cancelled out a = G * (m2)/(r*r). Now we can calculate the acceleration due to gravity. Acceleration is just change in velocity over time, and we know that that change is in the direction of the other body. So we get the vector between the bodies (o1,o2 for object 1 and 2) dx = o2.x-o1.x, dy = o2.y-o1.y Then find the length of that vector (which is the r in the gravity formula) dist = Math.sqrt(dx* dx + dy * dy). Then we normalise the vector (make its length = one) by dividing by its length. dx /= dist, dy /= dist. Calculate the a (acceleration) and multiply the normalised vector between the object by a then add that to the object's velocity and that is it. Perfect Newtonian clockwork orbits (for two bodies that is).
Clean up with Kepler.
All that math is good but it does not make for a nice simulation. When the math is done both objects start moving and if the starting velocities are not in balance then the whole system will slowly drift of the canvas.
We could just make the display relative to one of the bodies, this would eliminate any drift in the system, but we still have the problem of getting an orbit. If one object is moving to fast it will fly off and never come back. If it is going too slow then it will fall in very close to the centerpoint of the other object. If this happens the change in velocity will approch infinity, something computers are not that good at handling.
So to get nice circular orbits we need one last bit of math.
Using Kepler's second law modified to fit into Newton's math we get a formula that will give the approximate (It is an approximate as the actual calculations involve an infinite series and I can not be bothered writing that out.) orbital velocity v = sqrt(G*(m1 + m2)/r). It looks similar to Newton's gravity law but in this the masses are summed not multiplied, and the distance is not squared.
So we use this to calculate the tangential speed of both bodies to give them near circular orbits. It is important that each object go in the opposite direction to each other.
I created a setup function that sets up the correct orbits for both the sun and the planet. But the value of G (Gravitational constant) is likely way to large. To get a better value I scale G (via kludge math) so that the sun's orbit speed is close to a desired ideal sunV (pixels per frame) To make the whole sim run quicker increase this value
As I have set up the code to have more than two bodies the calculation of starting velocity will only work if each object is significantly more massive than the next. I have added a moon (you need to un-comment to see) to the planet, but it is too big and it's starting velocity is a little too low. It gets pulled (gravity sling shot) by the Earth into a higher orbit,. but this also pulls the earth into a lower orbit making its orbit more eccentric
NOTE After all that I find that something is not quite right and there is still a tiny bit of drift in the system. As I am out of time I have just fixed the sun position to keep the system on the canvas.
var c = document.getElementById('canvas');
c.width = innerWidth;
c.height = innerHeight;
var ctx = c.getContext('2d');
const STAR_RADIUS = 100;
const PLANET_RADIUS = 10;
const MOON_RADIUS = 4.5;
var G = 1; // gravitational constant is not so constant as need to
// scale it to find best value for the system.
// for that I will scale it so that the suns orbital speed around the
// planet is approx 0.1 pixels per frame
const sunV = 0.1; // the sun's orbital desired speed. THis is used to tune G
const DRAW = function () {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = this.col;
ctx.fill();
ctx.closePath();
}
var star = {
x: c.width / 2,
y: c.height / 2,
vx : 0,
vy : 0,
r: STAR_RADIUS,
mass : (4/3) * Math.pow(STAR_RADIUS,3) * Math.PI,
col : 'orange',
draw : DRAW,
};
// kludge to fix drift
const sunStartX = star.x;
const sunStartY = star.y;
var node = {
x: c.width / 2 - STAR_RADIUS - PLANET_RADIUS * 5,
y: c.height / 2,
r: PLANET_RADIUS,
mass : (4/3) * Math.pow(PLANET_RADIUS,3) * Math.PI,
col : "blue",
draw : DRAW,
vx: -1,
vy: 0,
};
var moon = {
x: c.width / 2- STAR_RADIUS - PLANET_RADIUS * 7 ,
y: c.height / 2,
r: MOON_RADIUS,
mass : (4/3) * Math.pow(PLANET_RADIUS,3) * Math.PI,
col : "#888",
draw : DRAW,
vx: -1,
vy: 0,
};
const objects = [star, node];//, moon];
function setup(){
var dist,dx,dy,o1,o2,v,c,dv;
o1 = objects[0];
o1.vx = 0;
o1.vy = 0;
for(var j = 0; j < objects.length; j ++){
if(j !== 0){ // object can not apply force to them selves
o2 = objects[j];
dx = o2.x - o1.x;
dy = o2.y - o1.y;
dist = Math.sqrt(dx * dx + dy * dy);
dx /= dist;
dy /= dist;
// Find value og G
if(j === 1){ // is this not sun
v = Math.sqrt(G * ( o2.mass ) / dist);
dv = sunV - v;
while(Math.abs(dv) > sunV * sunV){
if(dv < 0){ // sun too fast
G *= 0.75;
}else{
G += G * 0.1;
}
v = Math.sqrt(G * ( o2.mass ) / dist);
dv = sunV - v;
}
}
v = Math.sqrt(G * ( o2.mass ) / dist);
o1.vx -= v * dy; // along the tangent
o1.vy += v * dx;
}
}
for(var i = 1; i < objects.length; i ++){
o1 = objects[i];
o1.vx = 0;
o1.vy = 0;
for(var j = 0; j <objects.length; j ++){
if(j !== i){
o2 = objects[j];
dx = o2.x - o1.x;
dy = o2.y - o1.y;
dist = Math.sqrt(dx * dx + dy * dy);
dx /= dist;
dy /= dist;
v = Math.sqrt(G * ( o2.mass ) / dist);
o1.vx += v * dy; // along the tangent
o1.vy -= v * dx;
}
}
}
}
//GAME LOOP
function gameLoop(){
update();
render();
requestAnimationFrame(gameLoop);
}
// every object exerts a force on every other object
function update(){
var dist,dx,dy,o1,o2,a;
// find force of acceleration each object applies to each object
for(var i = 0; i < objects.length; i ++){
o1 = objects[i];
for(var j = 0; j < objects.length; j ++){
if(i !== j){ // object can not apply force to them selves
o2 = objects[j];
dx = o2.x - o1.x;
dy = o2.y - o1.y;
dist = Math.sqrt(dx * dx + dy * dy);
dx /= dist; // normalise the line between the objects (makes the vector 1 unit long)
dy /= dist;
// get force
a = (G * o2.mass ) / (dist * dist);
o1.vx += a * dx;
o1.vy += a * dy;
}
}
}
// once all the forces have been found update objects positions
for(var i = 0; i < objects.length; i ++){
o1 = objects[i];
o1.x += o1.vx;
o1.y += o1.vy;
}
}
function render(){
ctx.clearRect(0, 0, c.width, c.height);
// kludge to fix drift
var offsetX = objects[0].x - sunStartX;
var offsetY = objects[0].y - sunStartY;
ctx.setTransform(1,0,0,1,-offsetX,-offsetY);
for(var i = 0; i < objects.length; i ++){
objects[i].draw();
}
ctx.setTransform(1,0,0,1,0,0);
}
setup();
requestAnimationFrame(gameLoop);
<canvas id='canvas'></canvas>
Okay, I've answered my own question.
Rather than making the star's gravity directly affect the x and y coordinates, I have a vx and vy of the object, and I cause the gravity to affect that value, and then just adjust x and y by vx and vy on each update.
Here's the code:
var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
c.width = window.innerWidth;
c.height = window.innerHeight;
var star = {
x: c.width / 2,
y: c.height / 2,
r: 100,
g: 0.5,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'orange';
ctx.fill();
ctx.closePath();
}
};
var node = {
x: c.width / 2,
y: 50,
r: 20,
vx: 15,
vy: 0,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.closePath();
}
};
//GAME LOOP
function gameLoop()
{
update();
render();
window.requestAnimationFrame(gameLoop);
}
function update()
{
node.x += node.vx;
node.y += node.vy;
//Move towards star
var dx = star.x - node.x;
var dy = star.y - node.y;
var angle = Math.atan2(dy, dx);
node.vx += (Math.cos(angle) * star.g);
node.vy += (Math.sin(angle) * star.g);
}
function render()
{
ctx.clearRect(0, 0, c.width, c.height);
star.draw();
node.draw();
}
window.requestAnimationFrame(gameLoop);
<canvas id='canvas'></canvas>
I created an algorithm to move a particle diagonally and it works fine using an angle. Basically, this is what I do:
this.x += this.speed * Math.cos(this.angle * Math.PI / 180);
this.y += this.speed * Math.sin(this.angle * Math.PI / 180);
this.draw();
How can I combine this with a zigzag movement?
I recommend calculating the lateral deviation from the normal path or amplitude which is given by
// Triangle wave at position t with period p:
function amplitude(t, p) {
t %= p;
return t > p * 0.25 ? t < p * 0.75 ? p * 0.5 - t : t - p : t;
}
where t will be set to the length of the traveled path, and p is the period of the 'zigzag' triangle wave pattern.
Given the amplitude and the previous position, we can now easily compute the next position by moving ahead as described by your original code and then adding the lateral deviation to our position:
var amplitude = amplitude(distance, p) - this.amplitude(previous_distance, p);
this.x += amplitude * Math.sin(this.angle * Math.PI/180);
this.y -= amplitude * Math.cos(this.angle * Math.PI/180);
A complete example with two movable objects, one moving 'normally' and one following a 'zigzag' pattern:
function Movable(x, y, speed, angle, period) {
this.x = x;
this.y = y;
this.speed = speed;
this.angle = angle;
this.period = period;
this.distance = 0;
}
Movable.prototype.moveDiagonal = function() {
this.distance += this.speed;
this.x += this.speed * Math.cos(this.angle * Math.PI / 180);
this.y += this.speed * Math.sin(this.angle * Math.PI / 180);
}
Movable.prototype.amplitudeZigZag = function() {
var p = this.period, d = this.distance % p;
return d > p * 0.25 ? d < p * 0.75 ? p * 0.5 - d : d - p : d;
}
Movable.prototype.moveZigZag = function() {
var amplitude1 = this.amplitudeZigZag();
this.moveDiagonal();
var amplitude2 = this.amplitudeZigZag();
var amplitude = amplitude2 - amplitude1;
this.x -= amplitude * Math.sin(this.angle * Math.PI/180);
this.y += amplitude * Math.cos(this.angle * Math.PI/180);
}
Movable.prototype.draw = function(context) {
context.beginPath();
context.arc(this.x, this.y, 1, 0, 2 * Math.PI);
context.stroke();
}
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var m1 = new Movable(0, 0, 2, 0, 50);
var m2 = new Movable(0, 0, 2, 0, 50);
for (var i = 0; i < 1000; ++i) {
m1.angle += Math.cos(i * Math.PI/180);
m2.angle += Math.cos(i * Math.PI/180);
m1.moveDiagonal();
m2.moveZigZag();
m1.draw(context);
m2.draw(context);
}
<canvas id="canvas" width="600" height="200"></canvas>
So let's say that you're moving in the direction this.angle and you want to zig-zag in that direction moving side to side ±45° from that direction. All you need to do is have a variable like var zigzag = 45; and add zigzag to this.angle when calculating the new positions. And to make it zig-zag, you need to negate it every so often like this zigzag *= -1;. If you want to stop zig-zagging, set zigzag = 0;.
The trick is knowing when to alternate between ±45°. Maybe you can have the switch timed and keep a reference to the last time you switched using Date.now();. You can check the difference between the current time and the recorded time, then negate zigzag once you've surpassed a certain number of milliseconds. Just remember to record the new time of the last switch. You could also keep track of distance travelled and use the same approach, whatever works for you.
I'm trying to draw 2 unit vectors and then draw an arc between them. I'm not looking for any solution, rather I want to know why my specific solution is not working.
First I pick 2 unit vectors at random.
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min;
}
var points = [{},{}];
points[0].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
points[1].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
Note: the math here is in 3D but I'm using a 2d example by just keeping the vectors in the XY plane
I can draw those 2 unit vectors in a canvas
// move to center of canvas
var scale = ctx.canvas.width / 2 * 0.9;
ctx.transform(ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.scale(scale, scale); // expand the unit fill the canvas
// draw a line for each unit vector
points.forEach(function(point) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(point.direction[0], point.direction[1]);
ctx.strokeStyle = point.color;
ctx.stroke();
});
That works.
Next I want to make a matrix that puts the XY plane with its Y axis aligned with the first unit vector and in the same plane as the plane described by the 2 unit vectors
var zAxis = normalize(cross(points[0].direction, points[1].direction));
var xAxis = normalize(cross(zAxis, points[0].direction));
var yAxis = points[0].direction;
I then draw a unit grid using that matrix
ctx.setTransform(
xAxis[0] * scale, xAxis[1] * scale,
yAxis[0] * scale, yAxis[1] * scale,
ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.beginPath();
for (var y = 0; y < 20; ++y) {
var v0 = (y + 0) / 20;
var v1 = (y + 1) / 20;
for (var x = 0; x < 20; ++x) {
var u0 = (x + 0) / 20;
var u1 = (x + 1) / 20;
ctx.moveTo(u0, v0);
ctx.lineTo(u1, v0);
ctx.moveTo(u0, v0);
ctx.lineTo(u0, v1);
}
}
ctx.stroke();
That works too. Run the sample below and see the pink unit grid is always aligned with the green unit vector and facing in the direction of the red unit vector.
Finally using the data for the unit grid I want to bend it the correct amount to fill the space between the 2 unit vectors. Given it's a unit grid it seems like I should be able to do this
var cosineOfAngleBetween = dot(points[0].direction, points[1].direction);
var expand = (1 + -cosineOfAngleBetween) / 2 * Math.PI;
var angle = x * expand; // x goes from 0 to 1
var newX = sin(angle) * y; // y goes from 0 to 1
var newY = cos(angle) * y;
And if I plot newX and newY for every grid point it seems like I should get the correct arc between the 2 unit vectors.
Taking the dot product of the two unit vectors should give me the cosine of the angle between them which goes from 1 if they are coincident to -1 if they are opposite. In my case I need expand to go from 0 to PI so (1 + -dot(p0, p1)) / 2 * PI seems like it should work.
But it doesn't. See the blue arc which is the unit grid points as input to the code above.
Some things I checked. I checked zAxis is correct. It's always either [0,0,1] or [0,0,-1] which is correct. I checked xAxis and yAxis are unit vectors. They are. I checked manually setting expand to PI * .5, PI, PI * 2 and it does exactly what I expect. PI * .5 gets a 90 degree arc, 1/4th of the way around from the blue unit vector. PI gets a half circle exactly as I expect. PI * 2 gets a full circle.
That makes it seem like dot(p0,p1) is wrong but looking at the dot function it seems correct and if test it with various easy vectors it returns what I expect dot([1,0,0], [1,0,0]) returns 1. dot([-1,0,0],[1,0,0]) returns -1. dot([1,0,0],[0,1,0]) returns 0. dot([1,0,0],normalize([1,1,0])) returns 0.707...
What am I missing?
Here's the code live
function cross(a, b) {
var dst = []
dst[0] = a[1] * b[2] - a[2] * b[1];
dst[1] = a[2] * b[0] - a[0] * b[2];
dst[2] = a[0] * b[1] - a[1] * b[0];
return dst;
}
function normalize(a) {
var dst = [];
var lenSq = a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
var len = Math.sqrt(lenSq);
if (len > 0.00001) {
dst[0] = a[0] / len;
dst[1] = a[1] / len;
dst[2] = a[2] / len;
} else {
dst[0] = 0;
dst[1] = 0;
dst[2] = 0;
}
return dst;
}
function dot(a, b) {
return (a[0] * b[0]) + (a[1] * b[1]) + (a[2] * b[2]);
}
var canvas = document.querySelector("canvas");
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext("2d");
function rand(min, max) {
if (max === undefined) {
max = min;
min = 0;
}
return Math.random() * (max - min) + min;
}
var points = [
{
direction: [0,0,0],
color: "green",
},
{
direction: [0,0,0],
color: "red",
},
];
var expand = 1;
var scale = ctx.canvas.width / 2 * 0.8;
function pickPoints() {
points[0].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
points[1].direction = normalize([rand(-1, 1), rand(-1, 1), 0]);
expand = (1 + -dot(points[0].direction, points[1].direction)) / 2 * Math.PI;
console.log("expand:", expand);
render();
}
pickPoints();
function render() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.scale(scale, scale);
ctx.lineWidth = 3 / scale;
points.forEach(function(point) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(point.direction[0], point.direction[1]);
ctx.strokeStyle = point.color;
ctx.stroke();
});
var zAxis = normalize(cross(points[0].direction, points[1].direction));
var xAxis = normalize(cross(zAxis, points[0].direction));
var yAxis = points[0].direction;
ctx.setTransform(
xAxis[0] * scale, xAxis[1] * scale,
yAxis[0] * scale, yAxis[1] * scale,
ctx.canvas.width / 2, ctx.canvas.height / 2);
ctx.lineWidth = 0.5 / scale;
ctx.strokeStyle = "pink";
drawPatch(false);
ctx.strokeStyle = "blue";
drawPatch(true);
function drawPatch(curved) {
ctx.beginPath();
for (var y = 0; y < 20; ++y) {
var v0 = (y + 0) / 20;
var v1 = (y + 1) / 20;
for (var x = 0; x < 20; ++x) {
var u0 = (x + 0) / 20;
var u1 = (x + 1) / 20;
if (curved) {
var a0 = u0 * expand;
var x0 = Math.sin(a0) * v0;
var y0 = Math.cos(a0) * v0;
var a1 = u1 * expand;
var x1 = Math.sin(a1) * v0;
var y1 = Math.cos(a1) * v0;
var a2 = u0 * expand;
var x2 = Math.sin(a0) * v1;
var y2 = Math.cos(a0) * v1;
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.moveTo(x0, y0);
ctx.lineTo(x2, y2);
} else {
ctx.moveTo(u0, v0);
ctx.lineTo(u1, v0);
ctx.moveTo(u0, v0);
ctx.lineTo(u0, v1);
}
}
}
ctx.stroke();
}
ctx.restore();
}
window.addEventListener('click', pickPoints);
canvas {
border: 1px solid black;
}
div {
display: flex;
}
<div><canvas></canvas><p> Click for new points</p></div>
There's nothing wrong with your dot product function. It's the way you're using it:
expand = (1 + -dot(points[0].direction, points[1].direction)) / 2 * Math.PI;
should be:
expand = Math.acos(dot(points[0].direction, points[1].direction));
The expand variable, as you use it, is an angle (in radians). The dot product gives you the cosine of the angle, but not the angle itself. While the cosine of an angle varies between 1 and -1 for input [0,pi], that value does not map linearly back to the angle itself.
In other words, it doesn't work because the cosine of an angle cannot be transformed into the angle itself simply by scaling it. That's what arcsine is for.
Note that in general, you can often get by using your original formula (or any simple formula that maps that [-1,1] domain to a range of [0,pi]) if all you need is an approximation, but it will never give an exact angle except at the extremes.
This can be seen visually by plotting the two functions on top of each other:
I used the following code based on this
ballA.vx = (u1x * (m1 - m2) + 2 * m2 * u2x) / (m1 + m2);
ballA.vy = (u1y * (m1 - m2) + 2 * m2 * u2y) / (m1 + m2);
ballB.vx = (u2x * (m2 - m1) + 2 * m1 * u1x) / (m1 + m2);
ballB.vy = (u2y * (m2 - m1) + 2 * m1 * u1y) / (m1 + m2);
but it obviously doesn't well as the formula is designed for one-dimensional collisions.
So I tried to use the below formula from this section.
But the problem is that I don't know what the angle of deflection is and how to calculate it. Also, how to take into account the bouncing coefficient in this formula?
Edit: I may have not been clear. The above code does work, although it may not be the expected behavior, as the original formula is designed for 1D collisions. The issues I'm trying therefore are:
What is the 2D equivalent?
How to take the bouncing coefficient into account?
How to calculate the direction (which is expressed with vx and vy) of the two balls following the collision?
I should start by saying: I created a new answer because I feel the old one has value for its simplicity
as promised here is a much more complex physics engine, yet I still feel it's simple enough to follow (hopefully! or I just wasted my time... lol), (url: http://jsbin.com/otipiv/edit#javascript,live)
function Vector(x, y) {
this.x = x;
this.y = y;
}
Vector.prototype.dot = function (v) {
return this.x * v.x + this.y * v.y;
};
Vector.prototype.length = function() {
return Math.sqrt(this.x * this.x + this.y * this.y);
};
Vector.prototype.normalize = function() {
var s = 1 / this.length();
this.x *= s;
this.y *= s;
return this;
};
Vector.prototype.multiply = function(s) {
return new Vector(this.x * s, this.y * s);
};
Vector.prototype.tx = function(v) {
this.x += v.x;
this.y += v.y;
return this;
};
function BallObject(elasticity, vx, vy) {
this.v = new Vector(vx || 0, vy || 0); // velocity: m/s^2
this.m = 10; // mass: kg
this.r = 15; // radius of obj
this.p = new Vector(0, 0); // position
this.cr = elasticity; // elasticity
}
BallObject.prototype.draw = function(ctx) {
ctx.beginPath();
ctx.arc(this.p.x, this.p.y, this.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
};
BallObject.prototype.update = function(g, dt, ppm) {
this.v.y += g * dt;
this.p.x += this.v.x * dt * ppm;
this.p.y += this.v.y * dt * ppm;
};
BallObject.prototype.collide = function(obj) {
var dt, mT, v1, v2, cr, sm,
dn = new Vector(this.p.x - obj.p.x, this.p.y - obj.p.y),
sr = this.r + obj.r, // sum of radii
dx = dn.length(); // pre-normalized magnitude
if (dx > sr) {
return; // no collision
}
// sum the masses, normalize the collision vector and get its tangential
sm = this.m + obj.m;
dn.normalize();
dt = new Vector(dn.y, -dn.x);
// avoid double collisions by "un-deforming" balls (larger mass == less tx)
// this is susceptible to rounding errors, "jiggle" behavior and anti-gravity
// suspension of the object get into a strange state
mT = dn.multiply(this.r + obj.r - dx);
this.p.tx(mT.multiply(obj.m / sm));
obj.p.tx(mT.multiply(-this.m / sm));
// this interaction is strange, as the CR describes more than just
// the ball's bounce properties, it describes the level of conservation
// observed in a collision and to be "true" needs to describe, rigidity,
// elasticity, level of energy lost to deformation or adhesion, and crazy
// values (such as cr > 1 or cr < 0) for stange edge cases obviously not
// handled here (see: http://en.wikipedia.org/wiki/Coefficient_of_restitution)
// for now assume the ball with the least amount of elasticity describes the
// collision as a whole:
cr = Math.min(this.cr, obj.cr);
// cache the magnitude of the applicable component of the relevant velocity
v1 = dn.multiply(this.v.dot(dn)).length();
v2 = dn.multiply(obj.v.dot(dn)).length();
// maintain the unapplicatble component of the relevant velocity
// then apply the formula for inelastic collisions
this.v = dt.multiply(this.v.dot(dt));
this.v.tx(dn.multiply((cr * obj.m * (v2 - v1) + this.m * v1 + obj.m * v2) / sm));
// do this once for each object, since we are assuming collide will be called
// only once per "frame" and its also more effiecient for calculation cacheing
// purposes
obj.v = dt.multiply(obj.v.dot(dt));
obj.v.tx(dn.multiply((cr * this.m * (v1 - v2) + obj.m * v2 + this.m * v1) / sm));
};
function FloorObject(floor) {
var py;
this.v = new Vector(0, 0);
this.m = 5.9722 * Math.pow(10, 24);
this.r = 10000000;
this.p = new Vector(0, py = this.r + floor);
this.update = function() {
this.v.x = 0;
this.v.y = 0;
this.p.x = 0;
this.p.y = py;
};
// custom to minimize unnecessary filling:
this.draw = function(ctx) {
var c = ctx.canvas, s = ctx.scale;
ctx.fillRect(c.width / -2 / s, floor, ctx.canvas.width / s, (ctx.canvas.height / s) - floor);
};
}
FloorObject.prototype = new BallObject(1);
function createCanvasWithControls(objs) {
var addBall = function() { objs.unshift(new BallObject(els.value / 100, (Math.random() * 10) - 5, -20)); },
d = document,
c = d.createElement('canvas'),
b = d.createElement('button'),
els = d.createElement('input'),
clr = d.createElement('input'),
cnt = d.createElement('input'),
clrl = d.createElement('label'),
cntl = d.createElement('label');
b.innerHTML = 'add ball with elasticity: <span>0.70</span>';
b.onclick = addBall;
els.type = 'range';
els.min = 0;
els.max = 100;
els.step = 1;
els.value = 70;
els.style.display = 'block';
els.onchange = function() {
b.getElementsByTagName('span')[0].innerHTML = (this.value / 100).toFixed(2);
};
clr.type = cnt.type = 'checkbox';
clr.checked = cnt.checked = true;
clrl.style.display = cntl.style.display = 'block';
clrl.appendChild(clr);
clrl.appendChild(d.createTextNode('clear each frame'));
cntl.appendChild(cnt);
cntl.appendChild(d.createTextNode('continuous shower!'));
c.style.border = 'solid 1px #3369ff';
c.style.display = 'block';
c.width = 700;
c.height = 550;
c.shouldClear = function() { return clr.checked; };
d.body.appendChild(c);
d.body.appendChild(els);
d.body.appendChild(b);
d.body.appendChild(clrl);
d.body.appendChild(cntl);
setInterval(function() {
if (cnt.checked) {
addBall();
}
}, 333);
return c;
}
// start:
var objs = [],
c = createCanvasWithControls(objs),
ctx = c.getContext('2d'),
fps = 30, // target frames per second
ppm = 20, // pixels per meter
g = 9.8, // m/s^2 - acceleration due to gravity
t = new Date().getTime();
// add the floor:
objs.push(new FloorObject(c.height - 10));
// as expando so its accessible in draw [this overides .scale(x,y)]
ctx.scale = 0.5;
ctx.fillStyle = 'rgb(100,200,255)';
ctx.strokeStyle = 'rgb(33,69,233)';
ctx.transform(ctx.scale, 0, 0, ctx.scale, c.width / 2, c.height / 2);
setInterval(function() {
var i, j,
nw = c.width / ctx.scale,
nh = c.height / ctx.scale,
nt = new Date().getTime(),
dt = (nt - t) / 1000;
if (c.shouldClear()) {
ctx.clearRect(nw / -2, nh / -2, nw, nh);
}
for (i = 0; i < objs.length; i++) {
// if a ball > viewport width away from center remove it
while (objs[i].p.x < -nw || objs[i].p.x > nw) {
objs.splice(i, 1);
}
objs[i].update(g, dt, ppm, objs, i);
for (j = i + 1; j < objs.length; j++) {
objs[j].collide(objs[i]);
}
objs[i].draw(ctx);
}
t = nt;
}, 1000 / fps);
the real "meat" and the origin for this discussion is the obj.collide(obj) method.
if we dive in (I commented it this time as it is much more complex than the "last"), you'll see that this equation: , is still the only one being used in this line: this.v.tx(dn.multiply((cr * obj.m * (v2 - v1) + this.m * v1 + obj.m * v2) / sm)); now I'm sure you're still saying: "zomg wtf! that's the same single dimension equation!" but when you stop and think about it a "collision" only ever happens in a single dimension. Which is why we use vector equations to extract the applicable components and apply the collisions only to those specific parts leaving the others untouched to go on their merry way (ignoring friction and simplifying the collision to not account for dynamic energy transforming forces as described in the comments for CR). This concept obviously gets more complicated as the object complexity grows and number of scene data points increases to account for things like deformity, rotational inertia, uneven mass distribution and points of friction... but that's so far beyond the scope of this it's almost not worth mentioning..
Basically, the concepts you really need to "grasp" for this to feel intuitive to you are the basics of Vector equations (all located in the Vector prototype), how they interact with each (what it actually means to normalize, or take a dot/scalar product, eg. reading/talking to someone knowledgeable) and a basic understanding of how collisions act on properties of an object (mass, speed, etc... again, read/talk to someone knowledgeable)
I hope this helps, good luck! -ck
here is a demo of an inelastic collision equation in action, custom made for you:
function BallObject(elasticity) {
this.v = { x: 1, y: 20 }; // velocity: m/s^2
this.m = 10; // mass: kg
this.p = { x: 40, y: 0}; // position
this.r = 15; // radius of obj
this.cr = elasticity; // elasticity
}
function draw(obj) {
ctx.beginPath();
ctx.arc(obj.p.x, obj.p.y, obj.r, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fill();
}
function collide(obj) {
obj.v.y = (obj.cr * floor.m * -obj.v.y + obj.m * obj.v.y) / (obj.m + floor.m);
}
function update(obj, dt) {
// over-simplified collision detection
// only consider the floor for simplicity
if ((obj.p.y + obj.r) > c.height) {
obj.p.y = c.height - obj.r;
collide(obj);
}
obj.v.y += g * dt;
obj.p.x += obj.v.x * dt * ppm;
obj.p.y += obj.v.y * dt * ppm;
}
var d = document,
c = d.createElement('canvas'),
b = d.createElement('button'),
els = d.createElement('input'),
clr = d.createElement('input'),
clrl = d.createElement('label'),
ctx = c.getContext('2d'),
fps = 30, // target frames per second
ppm = 20, // pixels per meter
g = 9.8, // m/s^2 - acceleration due to gravity
objs = [],
floor = {
v: { x: 0, y: 0 }, // floor is immobile
m: 5.9722 * Math.pow(10, 24) // mass of earth (probably could be smaller)
},
t = new Date().getTime();
b.innerHTML = 'add ball with elasticity: <span>0.70</span>';
b.onclick = function() { objs.push(new BallObject(els.value / 100)); };
els.type = 'range';
els.min = 0;
els.max = 100;
els.step = 1;
els.value = 70;
els.style.display = 'block';
els.onchange = function() {
b.getElementsByTagName('span')[0].innerHTML = (this.value / 100).toFixed(2);
};
clr.type = 'checkbox';
clr.checked = true;
clrl.appendChild(clr);
clrl.appendChild(d.createTextNode('clear each frame'));
c.style.border = 'solid 1px #3369ff';
c.style.borderRadius = '10px';
c.style.display = 'block';
c.width = 400;
c.height = 400;
ctx.fillStyle = 'rgb(100,200,255)';
ctx.strokeStyle = 'rgb(33,69,233)';
d.body.appendChild(c);
d.body.appendChild(els);
d.body.appendChild(b);
d.body.appendChild(clrl);
setInterval(function() {
var nt = new Date().getTime(),
dt = (nt - t) / 1000;
if (clr.checked) {
ctx.clearRect(0, 0, c.width, c.height);
}
for (var i = 0; i < objs.length; i++) {
update(objs[i], dt);
draw(objs[i]);
}
t = nt;
}, 1000 / fps);
to see it in action yourself, just go here: http://jsbin.com/iwuxol/edit#javascript,live
This utilizes this equation:
and since your "floor" doesn't move you only have to consider the influence on the ball's y velocity. mind you there are quite a few shortcuts and oversights here so this is a very primitive physics engine, and is mainly meant to illustrate this one equation...
hope this helps -ck
I strongly recommend you familiarize yourself with the center of momentum frame. It makes collisions much easier to understand. (And without that understanding you're just manipulating cryptic equations and you'll never know why things go wrong.)
Anyway, to determine the angle, you can use the impact parameter, basically how far "off center" one ball hits the other. The two balls are approaching each other in opposite directions (in the center-of-momentum frame), and the distance between their centers perpendicular to those velocities is the impact parameter h. Then the angle of deflection is 2 acos(h/(r1+r2)).
Once you get that working perfectly, you can worry about inelastic collisions and the coefficient of restitution.