Translating pixels in canvas on sine wave - javascript

I am trying to create an image distortion effect on my canvas, but nothing appears to be happening. Here is my code:
self.drawScreen = function (abilityAnimator, elapsed) {
if (!self.initialized) {
self.initialized = true;
self.rawData = abilityAnimator.context.getImageData(self.targetX, self.targetY, self.width, self.height);
self.initialImgData = self.rawData.data;
}
abilityAnimator.drawBackground();
self.rawData = abilityAnimator.context.getImageData(self.targetX, self.targetY, self.width, self.height);
var imgData = self.rawData.data, rootIndex, translationIndex, newX;
for (var y = 0; y < self.height; y++) {
for (var x = 0; x < self.width; x++) {
rootIndex = (y * self.height + x) * 4;
newX = Math.ceil(self.amplitude * Math.sin(self.frequency * (y + elapsed)));
translationIndex = (y * self.width + newX) * 4;
imgData[translationIndex + 0] = self.initialImgData[rootIndex + 0];
imgData[translationIndex + 1] = self.initialImgData[rootIndex + 1];
imgData[translationIndex + 2] = self.initialImgData[rootIndex + 2];
imgData[translationIndex + 3] = self.initialImgData[rootIndex + 3];
}
}
abilityAnimator.context.putImageData(self.rawData, self.targetX, self.targetY);
};
abilityAnimator is a wrapper for my canvas object:
abilityAnimator.context = //canvas.context
abilityAnimator.drawBackground = function(){
this.canvas.width = this.canvas.width;
}
elapsed is simply the number of milliseconds since the animation began (elapsed is always <= 2000)
My member variables have the following values:
self.width = 125;
self.height = 125;
self.frequency = 0.5;
self.amplitude = self.width / 4;
self.targetX = //arbitrary value within canvas
self.targetY = //arbitrary value within canvas
I can translate the image to the right very easily so long as there is no sine function, however, introducing these lines:
newX = Math.ceil(self.amplitude * Math.sin(self.frequency * (y + elapsed)));
translationIndex = (y * self.width + newX) * 4;
Causes nothing to render at all. The translation indexes don't appear to be very strange, and the nature of the sinusoidal function should guarantee that the offset is no greater than 125 / 4 pixels.

Your formula using sin is wrong, the frequency will be so high it will be seen as noise.
The typical formula to build a sinusoid is :
res = sin ( 2 * PI * frequency * time ) ;
where frequency is in Hz and time in s.
So in js that would translate to :
res = Math.sin ( 2 * Math.PI * f * time_ms * 1e-3 ) ;
you can obviously compute just once the constant factor :
self.frequency = 0.5 * ( 2 * Math.PI * 1e-3 );
// then use
res = Math.sin ( self.frequency * time_ms ) ;
So you see you were 1000 times too fast.
Second issue :
Now that you have your time frequency ok, let's fix your spatial frequency : when multiplying time frequency by y, you're quite adding apples and cats.
To build the formula, think that you want to cross n time 2*PI during the height of the canvas.
So :
spatialFrequency = ( n ) * 2 * Math.PI / canvasHeight ;
and your formula becomes :
res = Math.sin ( self.frequency * time_ms + spatialFrequency * y ) ;
You can play with various values with this jsbin i made so you can visualize the effect :
http://jsbin.com/ludizubo/1/edit?js,output

Related

creating twinkling effect using javascript

var stars = function() {
this.x = Math.floor(Math.random()* 1000) ;
this.y = Math.floor(Math.random()* 900) ;
this.radius = 2 ;
this.starColour = "gold";
}
var starNum = 20;
var starry = new Array(starNum);
for(var s = 0 ; s < 100 ; s++){
starry[s] = new stars()
}
var starDraw = function() {
var starCanvas = document.getElementById("stars");
var starCtx = starCanvas.getContext("2d");
starCtx.clearRect(0, 0, 1000, 900);
for(i = 0; i < 100 ; i++){
var star = starry[i];
starCtx.fillStyle= "white";
starCtx.shadowBlur = 5;
starCtx.shadowColor = "white";
starCtx.beginPath();
// draw it
starCtx.arc(star.x, star.y, star.radius, Math.PI * 2, false);
starCtx.stroke();
starCtx.fill();
}
}
function starLoop(){
starDraw();
requestAnimationFrame(starLoop);
}
requestAnimationFrame(starLoop);
So I am trying to create a twinkling effect for the stars using only javascript and I can't figure out how to do it.
I have searched around and found no real answers up to now so I would appreciate if I could get an answer here. I am very new to coding so please take it easy on me.
A random star field. A little exaggerated, but easy to tone down (or up) if needed.
The important part is to avoid direct random values as most things in nature are not random but tend to fall close to a fixed point. This is call a gaussian distribution. There are several ways to generate such random values.
// gRandom is far more likely to be near 0.5 than 1 or zero
var gRandom = (Math.random()+Math.random()+Math.random()+Math.random()) / 4;
// or
// gRandom is more likely to be near zero than near 1
var gRandom = Math.random() * Math.random();
I use these method to set the sizes of stars (far more small stars than big) and create the colour and movement.
To try and get a more realistic effect I also move the stars by less than a pixel. This has the effect of changing the brightness but not look like movement.
Code has plenty of comments
const ctx = canvas.getContext("2d");
// function calls a callback count times. Saves typing out for loops all the time
const doFor = (count, callback) => {
var i = 0;
while (i < count) {
callback(i++)
}
};
// creates a random integer between min and max. If min only given the between 0 and the value
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
// same as above but as floats.
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
// creates a 2d point at x,y. If only x is a point than set to that point
const point = (x = 0, y) => {
if (x.x && y === undefined) {return { x: x.x,y: x.y} }
return {x,y: y === undefined ? 0 : y }
};
function ease (time, amount = 2) { return Math.pow(time % 1,amount) };
const clamp = (v, min = 1,max = min + (min = 0)) => v < min ? min : v > max ? max : v;
// stuff for stars
const skyColour = [10,30,50];
const density = 1000; // number of star per every density pixels
const colourChangeRate = 16; // Time in frames to change a colour
const stars = [];
const star = { // define a star
draw() {
this.count += 1; // integer counter used to triger color change every 16 frames
if (this.count % colourChangeRate === 0) { // change colour ?
// colour is a gaussian distrabution (NOT random) centered at #888
var c = (Math.random() + Math.random() + Math.random() + Math.random()) * 4;
var str = "#";
str += Math.floor(c * this.red).toString(16); // change color
str += Math.floor(c * this.green).toString(16); // change color
str += Math.floor(c * this.blue).toString(16); // change color
this.col = str;
}
ctx.fillStyle = this.col;
// move star around a pixel. Again its not random
// but a gaussian distrabution. The movement is sub pixel and will only
// make the stars brightness vary not look like its moving
var ox = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
var oy = (Math.random() + Math.random() + Math.random() + Math.random()) / 4;
ctx.fillRect(this.pos.x + ox, this.pos.y + oy, this.size, this.size);
}
}
// create a random star
// the size is caculated to produce many more smaller stars than big
function createStar(pos) {
stars.push(Object.assign({}, star, {
pos,
col: "#ccc",
count: randI(colourChangeRate),
size: rand(1) * rand(1) * 2 + 0.5,
red: 1-(rand(1) * rand(1) *rand(1)), // reduces colour channels
green: 1-(rand(1) * rand(1) *rand(1)), // but only by a very small amount
blue: 1-(rand(1) * rand(1) *rand(1)), // most of the time but occasional
// star will have a distinct colour
}));
}
var starCount;
var skyGrad;
// render the stars
function mainLoop(time) {
// resize canva if page size changes
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
// create a new set of stars
stars.length = 0;
// density is number of pixels one the canvas that has one star
starCount = Math.floor((canvas.width * canvas.height) / density);
// create the random stars;
doFor(starCount, () => createStar(point(randI(canvas.width), randI(canvas.height))));
skyGrad = ctx.createLinearGradient(0,0,0,canvas.height);
skyGrad.addColorStop(0,"black");
doFor(100,(i)=>{
var pos = clamp(i/100,0,1);
var col = ease(pos);
skyGrad.addColorStop(
pos,
"rgb(" +
Math.floor(skyColour[0] * col) + "," +
Math.floor(skyColour[1] * col) + "," +
Math.floor(skyColour[2] * col) + ")"
);
});
// floating point error can cause problems if we dont set the top
// at 1
skyGrad.addColorStop(1,"rgb("+skyColour[0]+","+skyColour[1]+","+skyColour[2]+")");
}
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
doFor(starCount, (i) => stars[i].draw());
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

How to "push out "XYZ coordinates forming a 3D orbit with an offset in the middle

I have a orbit of length 200. But it is centered around a sun of radius 0 (length 0). Now I want to expand the sun to have a radius of 1 and "push" out the outer orbits as well.
The XYZ coordinates look like this:
[-6.76, 5.75, -1.06],
[-6.95, 5.54, -0.91],
[-7.13, 5.33, -0.75],
[-7.31, 5.11, -0.58]
... followed by 196 more coordinates
I tried tried a lot of things to make the circle bigger * radius and / someNumbers. To at least try to do it myself.
But i lost it when i made an if like this:
If(the x coordination > 0)
the x coordination += 1;
}
Else{
the x coordination += 1;
}
And also for Y and Z but when they came close to the 1 and -1 position of that axis they skipped to the other side.
Creating a line (with the width of 1 on both sides) of emptiness along the axis.
Result of MBo's awnser(view from above):
// arrayIndex is a number to remember at which point it is in the orbit array
satellites.forEach(function (element) {
if (element.arrayIndex>= element.satellite.coordinates.length) {
element.arrayIndex= 0;
}
var posX = element.satellite.coordinates[element.arrayIndex][0];
var posY = element.satellite.coordinates[element.arrayIndex][1];
var posZ = element.satellite.coordinates[element.arrayIndex][2];
R = Math.sqrt(posX^2 + posY^2 + posZ^2);
cf = (R + earthRadius) / R;
xnew = posX * cf;
ynew = posY * cf;
znew = posZ * cf;
// var posX = earthRadius * (element.satellite.coordinates[element.test][0] / (200 * earthRadius) * earthRadius);
// var posY = earthRadius * (element.satellite.coordinates[element.test][1] / (200 * earthRadius) * earthRadius);
// var posZ = earthRadius * (element.satellite.coordinates[element.test][2] / (200 * earthRadius) * earthRadius);
// divide by 100 to scale it down some more
element.position.x = xnew / 100;
element.position.y = ynew / 100;
element.position.z = znew / 100;
element.arrayIndex= element.arrayIndex+ 1;
});
You have orbit radius
/////////R = Sqrt(x^2 + y^2 + z^2)
Edit to avoid confusion:
R = Sqrt(x * x + y * y + z * z)
You need to modify coordinates to make orbit radius R+r. To preserve orbit form, for every point find it's R, and multiply all components by coefficient (R+r)/R
R = Sqrt(x^2 + y^2 + z^2)
cf = (R + r) / R
xnew = x * cf
ynew = y * cf
znew = z * cf

How to curve a unit mesh between 2 unit vectors

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:

How do I draw x number of circles around a central circle, starting at the top of the center circle?

I'm trying to create a UI that has a lot of items in circles. Sometimes these circles will have related circles that should be displayed around them.
I was able to cobble together something that works, here.
The problem is that the outer circles start near 0 degrees, and I'd like them to start at an angle supplied by the consumer of the function/library. I was never a star at trigonometry, or geometry, so I could use a little help.
As you can see in the consuming code, there is a setting: startingDegree: 270 that the function getPosition should honor, but I haven't been able to figure out how.
Update 04/02/2014:
as I mentioned in my comment to Salix alba, I wasn't clear above, but what I needed was to be able to specify the radius of the satellite circles, and to have them go only partly all the way around. Salix gave a solution that calculates the size the satellites need to be to fit around the center circle uniformly.
Using some of the hints in Salix's answer, I was able to achieve the desired result... and have an extra "mode," thanks to Salix, in the future.
The working, though still rough, solution is here: http://jsfiddle.net/RD4RZ/11/. Here is the entire code (just so it's all on SO):
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<script type="text/javascript" src="//code.jquery.com/jquery-1.10.1.js"></script>
<style type="text/css">
.circle
{
position: absolute;
width: 100px;
height: 100px;
background-repeat: no-repeat;background-position: center center;
border: 80px solid #a19084;
border-radius: 50%;
-moz-border-radius: 50%;
}
.sm
{
border: 2px solid #a19084;
}
</style>
<script type="text/javascript">//<![CDATA[
$(function () {
function sind(x) {
return Math.sin(x * Math.PI / 180);
}
/*the law of cosines:
cc = aa + bb - 2ab cos(C), where c is the satellite diameter a and b are the legs
solving for cos C, cos C = ( aa + bb - cc ) / 2ab
Math.acos((a * a + b * b - c * c) / (2 * a * b)) = C
*/
function solveAngle(a, b, c) { // Returns angle C using law of cosines
var temp = (a * a + b * b - c * c) / (2 * a * b);
if (temp >= -1 && temp <= 1)
return radToDeg(Math.acos(temp));
else
throw "No solution";
}
function radToDeg(x) {
return x / Math.PI * 180;
}
function degToRad(x) {
return x * (Math.PI / 180);
}
var satellite = {
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getPosition: function (settings) {
//backwards compat
settings.centerPadding = settings.centerPadding || settings.itemPadding;
settings.noOverLap = typeof settings.noOverLap == 'undefined' ? true : settings.noOverLap;
settings.startingDegree = settings.startingDegree || 270;
settings.startSatellitesOnEdge = typeof settings.startSatellitesOnEdge == 'undefined' ? true : settings.startSatellitesOnEdge;
var itemIndex = $.inArray(settings.item, settings.collection);
var itemCnt = settings.collection.length;
var satelliteSide = settings.itemDiameter + (settings.itemSeparation || 0) + (settings.itemPadding || 0);
var evenDistribution = typeof settings.evenDistribution == 'undefined' ? true : settings.evenDistribution;
var degreeOfSeparation = (360 / itemCnt);
/*
we know all three sides:
one side is the diameter of the satellite itself (plus any padding). the other two
are the parent radius + the radius of the satellite itself (plus any padding).
given that, we need to find the angle of separation using the law of cosines (solveAngle)
*/
//if (!evenDistribution) {
var side1 = ((satelliteSide / 2)) + ((settings.minCenterDiameter + (2 * settings.centerPadding)) / 2);
var side2 = satelliteSide;;
var degreeOfSeparationBasedOnSatellite = solveAngle(side1, side1, side2); //Math.acos(((((side1 * side1) + (side2 * side2)) - (side2 * side2)) / (side2 * side2 * 2)) / 180 * Math.PI) * Math.PI;
degreeOfSeparation = evenDistribution? degreeOfSeparation: settings.noOverLap ? Math.min(degreeOfSeparation, degreeOfSeparationBasedOnSatellite) : degreeOfSeparationBasedOnSatellite;
//}
//angle-angle-side
//a-A-B
var a = satelliteSide;
var A = degreeOfSeparation;
/*
the three angles of any triangle add up to 180. We know one angle (degreeOfSeparation)
and we know the other two are equivalent to each other, so...
*/
var B = (180 - A) / 2;
//b is length necessary to fit all satellites, might be too short to be outside of base circle
var b = a * sind(B) / sind(A);
var offset = (settings.itemDiameter / 2) + (settings.itemPadding || 0); // 1; //
var onBaseCircleLegLength = ((settings.minCenterDiameter / 2) + settings.centerPadding) + offset;
var offBase = false;
if (b > onBaseCircleLegLength) {
offBase = true;
}
b = settings.noOverLap ? Math.max(b, onBaseCircleLegLength) : onBaseCircleLegLength;
var radianDegree = degToRad(degreeOfSeparation);
//log('b=' + b);
//log('settings.center.x=' + settings.center.x);
//log('settings.center.y=' + settings.center.y);
var degreeOffset = settings.startingDegree;
if (settings.startSatellitesOnEdge) {
degreeOffset += ((offBase ? degreeOfSeparation : degreeOfSeparationBasedOnSatellite) / 2);
}
var i = ((Math.PI * degreeOffset) / 180) + (radianDegree * itemIndex);// + (degToRad(degreeOfSeparationBasedOnSatellite) / 2); //(radianDegree) * (itemIndex);
var x = (Math.cos(i) * b) + (settings.center.x - offset);
var y = (Math.sin(i) * b) + (settings.center.y - offset);
return { 'x': Math.round(x), 'y': Math.round(y) };
}
,
/* if we ever want to size satellite by how many need to fit tight around the base circle:
x: function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1 - s);
console.log(angle);
console.log(s);
console.log(r);
console.log(startAngle);
console.log(startAngle / (Math.PI * 2));
for (var i = 0; i < n; ++i) {
var phi = ((Math.PI * startAngle) / 180) + (angle * i * 2);
var cx = 150 + (baseRadius + r) * Math.cos(phi);
var cy = 150 + (baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx, cy, r));
}
},
*/
//settings must have: collection (array), itemDiameter (number), minCenterDiameter (number), center (json with x, y numbers)
//optional: itemPadding (number), evenDistribution (boolean), centerPadding (boolean), noOverLap (boolean)
getAllPositions: function (settings) {
var point;
var points = [];
var collection = settings.collection;
for (var i = 0; i < collection.length; i++) {
settings.item = collection[i]
points.push(satellite.getPosition(settings));
}
return points;
}
};
var el = $("#center"), cnt = 10, arr = [], itemDiameter= 100;
for (var c = 0; c < cnt; c++) {
arr.push(c);
}
var settings = {
collection: arr,
itemDiameter: itemDiameter,
minCenterDiameter: el.width(),
center: { x: el.width() / 2, y: el.width() / 2 },
itemPadding: 2,
evenDistribution: false,
centerPadding: parseInt(el.css("border-width")),
noOverLap: false,
startingDegree: 270
};
var points = satellite.getAllPositions(settings);
for (var i = 0; i < points.length; i++) {
var $newdiv1 = $("<div></div>");
var div = el.append($newdiv1);
$newdiv1.addClass("circle").addClass("sm");
$newdiv1.text(i);
$newdiv1.css({ left: points[i].x, top: points[i].y, width: itemDiameter +'px', height: itemDiameter +'px' });
}
});//]]>
</script>
</head>
<body>
<div id="center" class="circle" style="left:250px;top:250px" >
</div>
</body>
</html>
The central bit you need to work out is radius of the small circles. If you have R for radius of the central circle and you want to fit n smaller circles around it. Let the as yet unknown radius of the small circle be r. We can construct a right angle triangle with one corner in the center of the big circle one in the center of the small circle and one which is where a line from the center is tangent to the small circle. This will be a right angle. The angle at the center is a the hypotenuse has length R+r the opposite is r and we don't need the adjacent. Using trig
sin(a) = op / hyp = r / (R + r)
rearrange
(R+r) sin(a) = r
R sin(a) + r sin(a) = r
R sin(a) = r - r sin(a)
R sin(a) = (1 - sin(a)) r
r = R sin(a) / ( 1 - sin(a))
once we have r we are pretty much done.
You can see this as a fiddle http://jsfiddle.net/SalixAlba/7mAAS/
// canvas and mousedown related variables
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
// save canvas size to vars b/ they're used often
var canvasWidth = canvas.width;
var canvasHeight = canvas.height;
var baseRadius = 50;
var baseCircle = new Circle(150,150,50);
var nCircles = 7;
var startAngle = 15.0;
function Circle(x,y,r) {
this.x = x;
this.y = y;
this.r = r;
}
Circle.prototype.draw = function() {
ctx.beginPath();
ctx.arc(this.x,this.y,this.r, 0, 2 * Math.PI, false);
ctx.stroke();
}
var circles = new Array();
function calcCircles(n) {
circles.splice(0); // clear out old circles
var angle = Math.PI / n;
var s = Math.sin(angle);
var r = baseRadius * s / (1-s);
console.log(angle);
console.log(s);
console.log(r);
for(var i=0;i<n;++i) {
var phi = startAngle + angle * i * 2;
var cx = 150+(baseRadius + r) * Math.cos(phi);
var cy = 150+(baseRadius + r) * Math.sin(phi);
circles.push(new Circle(cx,cy,r));
}
}
function draw() {
baseCircle.draw();
circles.forEach(function(ele){ele.draw()});
}
calcCircles(7);
draw();

Calculate velocity and direction of a ball to ball collision based on mass and bouncing coefficient

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.

Categories

Resources