<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<h1>Circles Bouncing Off Lines</h1>
<canvas id="circles-bouncing-off-lines" height="300" width="300"></canvas>
<p><a href="http://annotated-code.maryrosecook.com/circles-bouncing-off-lines/index.html">More Info about the code here!</a></p>
</body>
</html>
;(function(exports) {
// The top-level functions that run the simulation
// -----------------------------------------------
// **start()** creates the lines and circles and starts the simulation.
function start() {
// In index.html, there is a canvas tag that the game will be drawn in.
// Grab that canvas out of the DOM. From it, get the drawing
// context, an object that contains functions that allow drawing to the canvas.
var screen = document.getElementById('circles-bouncing-off-lines').getContext('2d');
// `world` holds the current state of the world.
var world = {
circles: [],
// Set up the five lines.
lines: [
makeLine({ x: 100, y: 100 }),
makeLine({ x: 200, y: 100 }),
makeLine({ x: 150, y: 150 }),
makeLine({ x: 100, y: 200 }),
makeLine({ x: 220, y: 200 }),
],
dimensions: { x: screen.canvas.width, y: screen.canvas.height },
// `timeLastCircleMade` is used in the periodic creation of new circles.
timeLastCircleMade: 0
};
// **tick()** is the main simulation tick function. It loops forever, running 60ish times a second.
function tick() {
// Update state of circles and lines.
update(world);
// Draw circles and lines.
draw(world, screen);
// Queue up the next call to tick with the browser.
requestAnimationFrame(tick);
}
// Run the first game tick. All future calls will be scheduled by
// `tick()` itself.
tick();
}
// Export `start()` so it can be run by index.html
exports.start = start;
// **update()** updates the state of the lines and circles.
function update(world) {
// Move and bounce the circles.
updateCircles(world);
// Create new circle if one hasn't been created for a while.
createNewCircleIfDue(world);
// Rotate the lines.
updateLines(world);
}
// **updateCircles()** moves and bounces the circles.
function updateCircles(world) {
for (var i = world.circles.length - 1; i >= 0; i--) {
var circle = world.circles[i];
// Run through all lines.
for (var j = 0; j < world.lines.length; j++) {
var line = world.lines[j];
// If `line` is intersecting `circle`, bounce circle off line.
if (trig.isLineIntersectingCircle(circle, line)) {
physics.bounceCircle(circle, line);
}
}
// Apply gravity to the velocity of `circle`.
physics.applyGravity(circle);
// Move `circle` according to its velocity.
physics.moveCircle(circle);
// Remove circles that are off screen.
if (!isCircleInWorld(circle, world.dimensions)) {
world.circles.splice(i, 1);
}
}
}
// **createNewCircleIfDue()** creates a new circle every so often.
function createNewCircleIfDue(world) {
var now = new Date().getTime();
if (now - world.timeLastCircleMade > 400) {
world.circles.push(makeCircle({ x: world.dimensions.x / 2, y: -5 }));
// Update last circle creation time.
world.timeLastCircleMade = now;
}
}
// **updateLines()** rotates the lines.
function updateLines(world) {
for (var i = 0; i < world.lines.length; i++) {
world.lines[i].angle += world.lines[i].rotateSpeed;
}
}
// **draw()** draws the all the circles and lines in the simulation.
function draw(world, screen) {
// Clear away the drawing from the previous tick.
screen.clearRect(0, 0, world.dimensions.x, world.dimensions.y);
var bodies = world.circles.concat(world.lines);
for (var i = 0; i < bodies.length; i++) {
bodies[i].draw(screen);
}
}
// **makeCircle()** creates a circle that has the passed `center`.
function makeCircle(center) {
return {
center: center,
velocity: { x: 0, y: 0 },
radius: 5,
// The circle has its own built-in `draw()` function. This allows
// the main `draw()` to just polymorphicly call `draw()` on circles and lines.
draw: function(screen) {
screen.beginPath();
screen.arc(this.center.x, this.center.y, this.radius, 0, Math.PI * 2, true);
screen.closePath();
screen.fillStyle = "black";
screen.fill();
}
};
}
// **makeLine()** creates a line that has the passed `center`.
function makeLine(center) {
return {
center: center,
len: 70,
// Angle of the line in degrees.
angle: 0,
rotateSpeed: 0.5,
// The line has its own built-in `draw()` function. This allows
// the main `draw()` to just polymorphicly call `draw()` on circles and lines.
draw: function(screen) {
var end1 = trig.lineEndPoints(this)[0];
var end2 = trig.lineEndPoints(this)[1];
screen.beginPath();
screen.lineWidth = 1.5;
screen.moveTo(end1.x, end1.y);
screen.lineTo(end2.x, end2.y);
screen.closePath();
screen.strokeStyle = "black";
screen.stroke();
}
};
}
// **isCircleInWorld()** returns true if `circle` is on screen.
function isCircleInWorld(circle, worldDimensions) {
return circle.center.x > -circle.radius &&
circle.center.x < worldDimensions.x + circle.radius &&
circle.center.y > -circle.radius &&
circle.center.y < worldDimensions.y + circle.radius;
}
// Trigonometry functions to help with calculating circle movement
// -------------------------------------------------------------
var trig = {
// **distance()** returns the distance between `point1` and `point2`
// as the crow flies. Uses Pythagoras's theorem.
distance: function(point1, point2) {
var x = point1.x - point2.x;
var y = point1.y - point2.y;
return Math.sqrt(x * x + y * y);
},
// **magnitude()** returns the magnitude of the passed vector.
// Sort of like the vector's speed. A vector with a larger x or y
// will have a larger magnitude.
magnitude: function(vector) {
return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
},
// **unitVector()** returns the unit vector for `vector`.
// A unit vector points in the same direction as the original,
// but has a magnitude of 1. It's like a direction with a
// speed that is the same as all other unit vectors.
unitVector: function(vector) {
return {
x: vector.x / trig.magnitude(vector),
y: vector.y / trig.magnitude(vector)
};
},
// **dotProduct()** returns the dot product of `vector1` and
// `vector2`. A dot product represents the amount one vector goes
// in the direction of the other. Imagine `vector2` runs along
// the ground and `vector1` represents a ball fired from a
// cannon. If `vector2` is multiplied by the dot product of the
// two vectors, it produces a vector that represents the amount
// of ground covered by the ball.
dotProduct: function(vector1, vector2) {
return vector1.x * vector2.x + vector1.y * vector2.y;
},
// **vectorBetween()** returns the vector that runs between `startPoint`
// and `endPoint`.
vectorBetween: function(startPoint, endPoint) {
return {
x: endPoint.x - startPoint.x,
y: endPoint.y - startPoint.y
};
},
// **lineEndPoints()** returns an array containing the points
// at each end of `line`.
lineEndPoints: function(line) {
var angleRadians = line.angle * 0.01745;
// Create a unit vector that represents the heading of
// `line`.
var lineUnitVector = trig.unitVector({
x: Math.cos(angleRadians),
y: Math.sin(angleRadians)
});
// Multiply the unit vector by half the line length. This
// produces a vector that represents the offset of one of the
// ends of the line from the center.
var endOffsetFromCenterVector = {
x: lineUnitVector.x * line.len / 2,
y: lineUnitVector.y * line.len / 2
};
// Return an array that contains the points at the two `line` ends.
return [
// Add the end offset to the center to get one end of 'line'.
{
x: line.center.x + endOffsetFromCenterVector.x,
y: line.center.y + endOffsetFromCenterVector.y
},
// Subtract the end offset from the center to get the other
// end of `line`.
{
x: line.center.x - endOffsetFromCenterVector.x,
y: line.center.y - endOffsetFromCenterVector.y
}
];
},
// **pointOnLineClosestToCircle()** returns the point on `line`
// closest to `circle`.
pointOnLineClosestToCircle: function(circle, line) {
// Get the points at each end of `line`.
var lineEndPoint1 = trig.lineEndPoints(line)[0];
var lineEndPoint2 = trig.lineEndPoints(line)[1];
// Create a vector that represents the line
var lineUnitVector = trig.unitVector(
trig.vectorBetween(lineEndPoint1, lineEndPoint2));
// Pick a line end and create a vector that represents the
// imaginary line between the end and the circle.
var lineEndToCircleVector = trig.vectorBetween(lineEndPoint1, circle.center);
// Get a dot product of the vector between the line end and circle, and
// the line vector. (See the `dotProduct()` function for a
// fuller explanation.) This projects the line end and circle
// vector along the line vector. Thus, it represents how far
// along the line to go from the end to get to the point on the
// line that is closest to the circle.
var projection = trig.dotProduct(lineEndToCircleVector, lineUnitVector);
// If `projection` is less than or equal to 0, the closest point
// is at or past `lineEndPoint1`. So, return `lineEndPoint1`.
if (projection <= 0) {
return lineEndPoint1;
// If `projection` is greater than or equal to the length of the
// line, the closest point is at or past `lineEndPoint2`. So,
// return `lineEndPoint2`.
} else if (projection >= line.len) {
return lineEndPoint2;
// The projection indicates a point part way along the line.
// Return that point.
} else {
return {
x: lineEndPoint1.x + lineUnitVector.x * projection,
y: lineEndPoint1.y + lineUnitVector.y * projection
};
}
},
// **isLineIntersectingCircle()** returns true if `line` is
// intersecting `circle`.
isLineIntersectingCircle: function(circle, line) {
// Get point on line closest to circle.
var closest = trig.pointOnLineClosestToCircle(circle, line);
// Get the distance between the closest point and the center of
// the circle.
var circleToLineDistance = trig.distance(circle.center, closest);
// Return true if distance is less than the radius.
return circleToLineDistance < circle.radius;
}
};
// Physics functions for calculating circle movement
// -----------------------------------------------
var physics = {
// **applyGravity()** adds gravity to the velocity of `circle`.
applyGravity: function(circle) {
circle.velocity.y += 0.06;
},
// **moveCircle()** adds the velocity of the circle to its center.
moveCircle: function(circle) {
circle.center.x += circle.velocity.x;
circle.center.y += circle.velocity.y;
},
// **bounceCircle()** assumes `line` is intersecting `circle` and
// bounces `circle` off `line`.
bounceCircle: function(circle, line) {
// Get the vector that points out from the surface the circle is
// bouncing on.
var bounceLineNormal = physics.bounceLineNormal(circle, line);
// Set the new circle velocity by reflecting the old velocity in
// `bounceLineNormal`.
var dot = trig.dotProduct(circle.velocity, bounceLineNormal);
circle.velocity.x -= 2 * dot * bounceLineNormal.x;
circle.velocity.y -= 2 * dot * bounceLineNormal.y;
// Move the circle until it has cleared the line. This stops
// the circle getting stuck in the line.
while (trig.isLineIntersectingCircle(circle, line)) {
physics.moveCircle(circle);
}
},
// **bounceLineNormal()** assumes `line` intersects `circle`. It
// returns the normal to the side of the line that the `circle` is
// hitting.
bounceLineNormal: function(circle, line) {
// Get vector that starts at the closest point on
// the line and ends at the circle. If the circle is hitting
// the flat of the line, this vector will point perpenticular to
// the line. If the circle is hitting the end of the line, the
// vector will point from the end to the center of the circle.
var circleToClosestPointOnLineVector =
trig.vectorBetween(
trig.pointOnLineClosestToCircle(circle, line),
circle.center);
// Make the normal a unit vector and return it.
return trig.unitVector(circleToClosestPointOnLineVector);
}
};
// Start
// -----
// When the DOM is ready, start the simulation.
window.addEventListener('load', start);
})(this);
Output
300px
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |