;(function(exports) {
├── .gitignore ├── LICENSE ├── README.md ├── circles-bouncing-off-lines ├── circles-bouncing-off-lines.js ├── docs │ ├── circles-bouncing-off-lines.html │ ├── docco.css │ └── public │ │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff │ │ └── stylesheets │ │ └── normalize.css ├── index.html └── screenshot.gif ├── gitlet └── index.html ├── index.html ├── main.css ├── package.json └── space-invaders ├── docs ├── docco.css ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff │ └── stylesheets │ │ └── normalize.css └── space-invaders.html ├── index.html ├── screenshot.gif ├── shoot.mp3 └── space-invaders.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mary Rose Cook 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Annotated code 2 | 3 | A series of short, heavily annotated JavaScript programs. 4 | 5 | * By Mary Rose Cook 6 | * http://maryrosecook.com 7 | * maryrosecook@maryrosecook.com 8 | 9 | ## Space Invaders 10 | 11 | Demonstrates an animation loop, collision detection, keyboard input, drawing to the canvas and playing sounds. 12 | 13 |  14 | 15 | To read the annotated source, open `/space-invaders/docs/space-invaders.html` in your browser. 16 | 17 | ## Circles bouncing off lines 18 | 19 | Demonstrates some simple trig that calculates the movement of circles as they bounce off lines. 20 | 21 |  22 | 23 | To read the annotated source, open `/circles-bouncing-off-lines/docs/circles-bouncing-off-lines.html` in your browser. 24 | -------------------------------------------------------------------------------- /circles-bouncing-off-lines/circles-bouncing-off-lines.js: -------------------------------------------------------------------------------- 1 | ;(function(exports) { 2 | 3 | // The top-level functions that run the simulation 4 | // ----------------------------------------------- 5 | 6 | // **start()** creates the lines and circles and starts the simulation. 7 | function start() { 8 | 9 | // In index.html, there is a canvas tag that the game will be drawn in. 10 | // Grab that canvas out of the DOM. From it, get the drawing 11 | // context, an object that contains functions that allow drawing to the canvas. 12 | var screen = document.getElementById('circles-bouncing-off-lines').getContext('2d'); 13 | 14 | // `world` holds the current state of the world. 15 | var world = { 16 | circles: [], 17 | 18 | // Set up the five lines. 19 | lines: [ 20 | makeLine({ x: 100, y: 100 }), 21 | makeLine({ x: 200, y: 100 }), 22 | makeLine({ x: 150, y: 150 }), 23 | makeLine({ x: 100, y: 200 }), 24 | makeLine({ x: 220, y: 200 }), 25 | ], 26 | 27 | dimensions: { x: screen.canvas.width, y: screen.canvas.height }, 28 | 29 | // `timeLastCircleMade` is used in the periodic creation of new circles. 30 | timeLastCircleMade: 0 31 | }; 32 | 33 | // **tick()** is the main simulation tick function. It loops forever, running 60ish times a second. 34 | function tick() { 35 | 36 | // Update state of circles and lines. 37 | update(world); 38 | 39 | // Draw circles and lines. 40 | draw(world, screen); 41 | 42 | // Queue up the next call to tick with the browser. 43 | requestAnimationFrame(tick); 44 | }; 45 | 46 | // Run the first game tick. All future calls will be scheduled by 47 | // `tick()` itself. 48 | tick(); 49 | }; 50 | 51 | // Export `start()` so it can be run by index.html 52 | exports.start = start; 53 | 54 | // **update()** updates the state of the lines and circles. 55 | function update(world) { 56 | 57 | // Move and bounce the circles. 58 | updateCircles(world) 59 | 60 | // Create new circle if one hasn't been created for a while. 61 | createNewCircleIfDue(world); 62 | 63 | // Rotate the lines. 64 | updateLines(world); 65 | }; 66 | 67 | // **updateCircles()** moves and bounces the circles. 68 | function updateCircles(world) { 69 | for (var i = world.circles.length - 1; i >= 0; i--) { 70 | var circle = world.circles[i]; 71 | 72 | // Run through all lines. 73 | for (var j = 0; j < world.lines.length; j++) { 74 | var line = world.lines[j]; 75 | 76 | // If `line` is intersecting `circle`, bounce circle off line. 77 | if (trig.isLineIntersectingCircle(circle, line)) { 78 | physics.bounceCircle(circle, line); 79 | } 80 | } 81 | 82 | // Apply gravity to the velocity of `circle`. 83 | physics.applyGravity(circle); 84 | 85 | // Move `circle` according to its velocity. 86 | physics.moveCircle(circle); 87 | 88 | // Remove circles that are off screen. 89 | if (!isCircleInWorld(circle, world.dimensions)) { 90 | world.circles.splice(i, 1); 91 | } 92 | } 93 | }; 94 | 95 | // **createNewCircleIfDue()** creates a new circle every so often. 96 | function createNewCircleIfDue(world) { 97 | var now = new Date().getTime(); 98 | if (now - world.timeLastCircleMade > 400) { 99 | world.circles.push(makeCircle({ x: world.dimensions.x / 2, y: -5 })); 100 | 101 | // Update last circle creation time. 102 | world.timeLastCircleMade = now; 103 | } 104 | }; 105 | 106 | // **updateLines()** rotates the lines. 107 | function updateLines(world) { 108 | for (var i = 0; i < world.lines.length; i++) { 109 | world.lines[i].angle += world.lines[i].rotateSpeed; 110 | } 111 | }; 112 | 113 | // **draw()** draws the all the circles and lines in the simulation. 114 | function draw(world, screen) { 115 | // Clear away the drawing from the previous tick. 116 | screen.clearRect(0, 0, world.dimensions.x, world.dimensions.y); 117 | 118 | var bodies = world.circles.concat(world.lines); 119 | for (var i = 0; i < bodies.length; i++) { 120 | bodies[i].draw(screen); 121 | } 122 | }; 123 | 124 | // **makeCircle()** creates a circle that has the passed `center`. 125 | function makeCircle(center) { 126 | return { 127 | center: center, 128 | velocity: { x: 0, y: 0 }, 129 | radius: 5, 130 | 131 | // The circle has its own built-in `draw()` function. This allows 132 | // the main `draw()` to just polymorphicly call `draw()` on circles and lines. 133 | draw: function(screen) { 134 | screen.beginPath(); 135 | screen.arc(this.center.x, this.center.y, this.radius, 0, Math.PI * 2, true); 136 | screen.closePath(); 137 | screen.fillStyle = "black"; 138 | screen.fill(); 139 | } 140 | }; 141 | }; 142 | 143 | // **makeLine()** creates a line that has the passed `center`. 144 | function makeLine(center) { 145 | return { 146 | center: center, 147 | len: 70, 148 | 149 | // Angle of the line in degrees. 150 | angle: 0, 151 | 152 | rotateSpeed: 0.5, 153 | 154 | // The line has its own built-in `draw()` function. This allows 155 | // the main `draw()` to just polymorphicly call `draw()` on circles and lines. 156 | draw: function(screen) { 157 | var end1 = trig.lineEndPoints(this)[0]; 158 | var end2 = trig.lineEndPoints(this)[1]; 159 | 160 | screen.beginPath(); 161 | screen.lineWidth = 1.5; 162 | screen.moveTo(end1.x, end1.y); 163 | screen.lineTo(end2.x, end2.y); 164 | screen.closePath(); 165 | 166 | screen.strokeStyle = "black"; 167 | screen.stroke(); 168 | } 169 | }; 170 | }; 171 | 172 | // **isCircleInWorld()** returns true if `circle` is on screen. 173 | function isCircleInWorld(circle, worldDimensions) { 174 | return circle.center.x > -circle.radius && 175 | circle.center.x < worldDimensions.x + circle.radius && 176 | circle.center.y > -circle.radius && 177 | circle.center.y < worldDimensions.y + circle.radius; 178 | }; 179 | 180 | // Trigonometry functions to help with calculating circle movement 181 | // ------------------------------------------------------------- 182 | 183 | var trig = { 184 | 185 | // **distance()** returns the distance between `point1` and `point2` 186 | // as the crow flies. Uses Pythagoras's theorem. 187 | distance: function(point1, point2) { 188 | var x = point1.x - point2.x; 189 | var y = point1.y - point2.y; 190 | return Math.sqrt(x * x + y * y); 191 | }, 192 | 193 | // **magnitude()** returns the magnitude of the passed vector. 194 | // Sort of like the vector's speed. A vector with a larger x or y 195 | // will have a larger magnitude. 196 | magnitude: function(vector) { 197 | return Math.sqrt(vector.x * vector.x + vector.y * vector.y); 198 | }, 199 | 200 | // **unitVector()** returns the unit vector for `vector`. 201 | // A unit vector points in the same direction as the original, 202 | // but has a magnitude of 1. It's like a direction with a 203 | // speed that is the same as all other unit vectors. 204 | unitVector: function(vector) { 205 | return { 206 | x: vector.x / trig.magnitude(vector), 207 | y: vector.y / trig.magnitude(vector) 208 | }; 209 | }, 210 | 211 | // **dotProduct()** returns the dot product of `vector1` and 212 | // `vector2`. A dot product represents the amount one vector goes 213 | // in the direction of the other. Imagine `vector2` runs along 214 | // the ground and `vector1` represents a ball fired from a 215 | // cannon. If `vector2` is multiplied by the dot product of the 216 | // two vectors, it produces a vector that represents the amount 217 | // of ground covered by the ball. 218 | dotProduct: function(vector1, vector2) { 219 | return vector1.x * vector2.x + vector1.y * vector2.y; 220 | }, 221 | 222 | // **vectorBetween()** returns the vector that runs between `startPoint` 223 | // and `endPoint`. 224 | vectorBetween: function(startPoint, endPoint) { 225 | return { 226 | x: endPoint.x - startPoint.x, 227 | y: endPoint.y - startPoint.y 228 | }; 229 | }, 230 | 231 | // **lineEndPoints()** returns an array containing the points 232 | // at each end of `line`. 233 | lineEndPoints: function(line) { 234 | var angleRadians = line.angle * 0.01745; 235 | 236 | // Create a unit vector that represents the heading of 237 | // `line`. 238 | var lineUnitVector = trig.unitVector({ 239 | x: Math.cos(angleRadians), 240 | y: Math.sin(angleRadians) 241 | }); 242 | 243 | // Multiply the unit vector by half the line length. This 244 | // produces a vector that represents the offset of one of the 245 | // ends of the line from the center. 246 | var endOffsetFromCenterVector = { 247 | x: lineUnitVector.x * line.len / 2, 248 | y: lineUnitVector.y * line.len / 2 249 | }; 250 | 251 | // Return an array that contains the points at the two `line` ends. 252 | return [ 253 | 254 | // Add the end offset to the center to get one end of 'line'. 255 | { 256 | x: line.center.x + endOffsetFromCenterVector.x, 257 | y: line.center.y + endOffsetFromCenterVector.y 258 | }, 259 | 260 | // Subtract the end offset from the center to get the other 261 | // end of `line`. 262 | { 263 | x: line.center.x - endOffsetFromCenterVector.x, 264 | y: line.center.y - endOffsetFromCenterVector.y 265 | } 266 | ]; 267 | }, 268 | 269 | // **pointOnLineClosestToCircle()** returns the point on `line` 270 | // closest to `circle`. 271 | pointOnLineClosestToCircle: function(circle, line) { 272 | 273 | // Get the points at each end of `line`. 274 | var lineEndPoint1 = trig.lineEndPoints(line)[0]; 275 | var lineEndPoint2 = trig.lineEndPoints(line)[1]; 276 | 277 | // Create a vector that represents the line 278 | var lineUnitVector = trig.unitVector( 279 | trig.vectorBetween(lineEndPoint1, lineEndPoint2)); 280 | 281 | // Pick a line end and create a vector that represents the 282 | // imaginary line between the end and the circle. 283 | var lineEndToCircleVector = trig.vectorBetween(lineEndPoint1, circle.center); 284 | 285 | // Get a dot product of the vector between the line end and circle, and 286 | // the line vector. (See the `dotProduct()` function for a 287 | // fuller explanation.) This projects the line end and circle 288 | // vector along the line vector. Thus, it represents how far 289 | // along the line to go from the end to get to the point on the 290 | // line that is closest to the circle. 291 | var projection = trig.dotProduct(lineEndToCircleVector, lineUnitVector); 292 | 293 | // If `projection` is less than or equal to 0, the closest point 294 | // is at or past `lineEndPoint1`. So, return `lineEndPoint1`. 295 | if (projection <= 0) { 296 | return lineEndPoint1; 297 | 298 | // If `projection` is greater than or equal to the length of the 299 | // line, the closest point is at or past `lineEndPoint2`. So, 300 | // return `lineEndPoint2`. 301 | } else if (projection >= line.len) { 302 | return lineEndPoint2; 303 | 304 | // The projection indicates a point part way along the line. 305 | // Return that point. 306 | } else { 307 | return { 308 | x: lineEndPoint1.x + lineUnitVector.x * projection, 309 | y: lineEndPoint1.y + lineUnitVector.y * projection 310 | }; 311 | } 312 | }, 313 | 314 | // **isLineIntersectingCircle()** returns true if `line` is 315 | // intersecting `circle`. 316 | isLineIntersectingCircle: function(circle, line) { 317 | 318 | // Get point on line closest to circle. 319 | var closest = trig.pointOnLineClosestToCircle(circle, line); 320 | 321 | // Get the distance between the closest point and the center of 322 | // the circle. 323 | var circleToLineDistance = trig.distance(circle.center, closest); 324 | 325 | // Return true if distance is less than the radius. 326 | return circleToLineDistance < circle.radius; 327 | } 328 | } 329 | 330 | // Physics functions for calculating circle movement 331 | // ----------------------------------------------- 332 | 333 | var physics = { 334 | 335 | // **applyGravity()** adds gravity to the velocity of `circle`. 336 | applyGravity: function(circle) { 337 | circle.velocity.y += 0.06; 338 | }, 339 | 340 | // **moveCircle()** adds the velocity of the circle to its center. 341 | moveCircle: function(circle) { 342 | circle.center.x += circle.velocity.x; 343 | circle.center.y += circle.velocity.y; 344 | }, 345 | 346 | // **bounceCircle()** assumes `line` is intersecting `circle` and 347 | // bounces `circle` off `line`. 348 | bounceCircle: function(circle, line) { 349 | 350 | // Get the vector that points out from the surface the circle is 351 | // bouncing on. 352 | var bounceLineNormal = physics.bounceLineNormal(circle, line); 353 | 354 | // Set the new circle velocity by reflecting the old velocity in 355 | // `bounceLineNormal`. 356 | var dot = trig.dotProduct(circle.velocity, bounceLineNormal); 357 | circle.velocity.x -= 2 * dot * bounceLineNormal.x; 358 | circle.velocity.y -= 2 * dot * bounceLineNormal.y; 359 | 360 | // Move the circle until it has cleared the line. This stops 361 | // the circle getting stuck in the line. 362 | while (trig.isLineIntersectingCircle(circle, line)) { 363 | physics.moveCircle(circle); 364 | } 365 | }, 366 | 367 | // **bounceLineNormal()** assumes `line` intersects `circle`. It 368 | // returns the normal to the side of the line that the `circle` is 369 | // hitting. 370 | bounceLineNormal: function(circle, line) { 371 | 372 | // Get vector that starts at the closest point on 373 | // the line and ends at the circle. If the circle is hitting 374 | // the flat of the line, this vector will point perpenticular to 375 | // the line. If the circle is hitting the end of the line, the 376 | // vector will point from the end to the center of the circle. 377 | var circleToClosestPointOnLineVector = 378 | trig.vectorBetween( 379 | trig.pointOnLineClosestToCircle(circle, line), 380 | circle.center); 381 | 382 | // Make the normal a unit vector and return it. 383 | return trig.unitVector(circleToClosestPointOnLineVector); 384 | } 385 | }; 386 | 387 | // Start 388 | // ----- 389 | 390 | // When the DOM is ready, start the simulation. 391 | window.addEventListener('load', start); 392 | })(this); 393 | -------------------------------------------------------------------------------- /circles-bouncing-off-lines/docs/circles-bouncing-off-lines.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |;(function(exports) {
start() creates the lines and circles and starts the simulation.
70 | 71 | function start() {
In index.html, there is a canvas tag that the game will be drawn in. 85 | Grab that canvas out of the DOM. From it, get the drawing 86 | context, an object that contains functions that allow drawing to the canvas.
87 | 88 | var screen = document.getElementById('circles-bouncing-off-lines').getContext('2d');
world
holds the current state of the world.
var world = {
106 | circles: [],
Set up the five lines.
118 | 119 | lines: [
122 | makeLine({ x: 100, y: 100 }),
123 | makeLine({ x: 200, y: 100 }),
124 | makeLine({ x: 150, y: 150 }),
125 | makeLine({ x: 100, y: 200 }),
126 | makeLine({ x: 220, y: 200 }),
127 | ],
128 |
129 | dimensions: { x: screen.canvas.width, y: screen.canvas.height },
timeLastCircleMade
is used in the periodic creation of new circles.
timeLastCircleMade: 0
145 | };
tick() is the main simulation tick function. It loops forever, running 60ish times a second.
157 | 158 | function tick() {
Update state of circles and lines.
172 | 173 | update(world);
Draw circles and lines.
187 | 188 | draw(world, screen);
Queue up the next call to tick with the browser.
202 | 203 | requestAnimationFrame(tick);
206 | };
Run the first game tick. All future calls will be scheduled by
218 | tick()
itself.
tick();
223 | };
Export start()
so it can be run by index.html
exports.start = start;
update() updates the state of the lines and circles.
250 | 251 | function update(world) {
Move and bounce the circles.
265 | 266 | updateCircles(world)
Create new circle if one hasn’t been created for a while.
280 | 281 | createNewCircleIfDue(world);
Rotate the lines.
295 | 296 | updateLines(world);
299 | };
updateCircles() moves and bounces the circles.
311 | 312 | function updateCircles(world) {
315 | for (var i = world.circles.length - 1; i >= 0; i--) {
316 | var circle = world.circles[i];
Run through all lines.
328 | 329 | for (var j = 0; j < world.lines.length; j++) {
332 | var line = world.lines[j];
If line
is intersecting circle
, bounce circle off line.
if (trig.isLineIntersectingCircle(circle, line)) {
348 | physics.bounceCircle(circle, line);
349 | }
350 | }
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.
392 | 393 | if (!isCircleInWorld(circle, world.dimensions)) {
396 | world.circles.splice(i, 1);
397 | }
398 | }
399 | };
createNewCircleIfDue() creates a new circle every so often.
411 | 412 | function createNewCircleIfDue(world) {
415 | var now = new Date().getTime();
416 | if (now - world.timeLastCircleMade > 400) {
417 | world.circles.push(makeCircle({ x: world.dimensions.x / 2, y: -5 }));
Update last circle creation time.
429 | 430 | world.timeLastCircleMade = now;
433 | }
434 | };
updateLines() rotates the lines.
446 | 447 | function updateLines(world) {
450 | for (var i = 0; i < world.lines.length; i++) {
451 | world.lines[i].angle += world.lines[i].rotateSpeed;
452 | }
453 | };
draw() draws the all the circles and lines in the simulation.
465 | 466 | function draw(world, screen) {
Clear away the drawing from the previous tick.
480 | 481 | screen.clearRect(0, 0, world.dimensions.x, world.dimensions.y);
484 |
485 | var bodies = world.circles.concat(world.lines);
486 | for (var i = 0; i < bodies.length; i++) {
487 | bodies[i].draw(screen);
488 | }
489 | };
makeCircle() creates a circle that has the passed center
.
function makeCircle(center) {
505 | return {
506 | center: center,
507 | velocity: { x: 0, y: 0 },
508 | radius: 5,
The circle has its own built-in draw()
function. This allows
520 | the main draw()
to just polymorphicly call draw()
on circles and lines.
draw: function(screen) {
525 | screen.beginPath();
526 | screen.arc(this.center.x, this.center.y, this.radius, 0, Math.PI * 2, true);
527 | screen.closePath();
528 | screen.fillStyle = "black";
529 | screen.fill();
530 | }
531 | };
532 | };
makeLine() creates a line that has the passed center
.
function makeLine(center) {
548 | return {
549 | center: center,
550 | len: 70,
Angle of the line in degrees.
562 | 563 | angle: 0,
566 |
567 | rotateSpeed: 0.5,
The line has its own built-in draw()
function. This allows
579 | the main draw()
to just polymorphicly call draw()
on circles and lines.
draw: function(screen) {
584 | var end1 = trig.lineEndPoints(this)[0];
585 | var end2 = trig.lineEndPoints(this)[1];
586 |
587 | screen.beginPath();
588 | screen.lineWidth = 1.5;
589 | screen.moveTo(end1.x, end1.y);
590 | screen.lineTo(end2.x, end2.y);
591 | screen.closePath();
592 |
593 | screen.strokeStyle = "black";
594 | screen.stroke();
595 | }
596 | };
597 | };
isCircleInWorld() returns true if circle
is on screen.
function isCircleInWorld(circle, worldDimensions) {
613 | return circle.center.x > -circle.radius &&
614 | circle.center.x < worldDimensions.x + circle.radius &&
615 | circle.center.y > -circle.radius &&
616 | circle.center.y < worldDimensions.y + circle.radius;
617 | };
645 | var trig = {
distance() returns the distance between point1
and point2
657 | as the crow flies. Uses Pythagoras’s theorem.
distance: function(point1, point2) {
662 | var x = point1.x - point2.x;
663 | var y = point1.y - point2.y;
664 | return Math.sqrt(x * x + y * y);
665 | },
magnitude() returns the magnitude of the passed vector. 677 | Sort of like the vector’s speed. A vector with a larger x or y 678 | will have a larger magnitude.
679 | 680 | magnitude: function(vector) {
683 | return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
684 | },
unitVector() returns the unit vector for vector
.
696 | A unit vector points in the same direction as the original,
697 | but has a magnitude of 1. It’s like a direction with a
698 | speed that is the same as all other unit vectors.
unitVector: function(vector) {
703 | return {
704 | x: vector.x / trig.magnitude(vector),
705 | y: vector.y / trig.magnitude(vector)
706 | };
707 | },
dotProduct() returns the dot product of vector1
and
719 | vector2
. A dot product represents the amount one vector goes
720 | in the direction of the other. Imagine vector2
runs along
721 | the ground and vector1
represents a ball fired from a
722 | cannon. If vector2
is multiplied by the dot product of the
723 | two vectors, it produces a vector that represents the amount
724 | of ground covered by the ball.
dotProduct: function(vector1, vector2) {
729 | return vector1.x * vector2.x + vector1.y * vector2.y;
730 | },
vectorBetween() returns the vector that runs between startPoint
742 | and endPoint
.
vectorBetween: function(startPoint, endPoint) {
747 | return {
748 | x: endPoint.x - startPoint.x,
749 | y: endPoint.y - startPoint.y
750 | };
751 | },
lineEndPoints() returns an array containing the points
763 | at each end of line
.
lineEndPoints: function(line) {
768 | var angleRadians = line.angle * 0.01745;
Create a unit vector that represents the heading of
780 | line
.
var lineUnitVector = trig.unitVector({
785 | x: Math.cos(angleRadians),
786 | y: Math.sin(angleRadians)
787 | });
Multiply the unit vector by half the line length. This 799 | produces a vector that represents the offset of one of the 800 | ends of the line from the center.
801 | 802 | var endOffsetFromCenterVector = {
805 | x: lineUnitVector.x * line.len / 2,
806 | y: lineUnitVector.y * line.len / 2
807 | };
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’.
834 | 835 | {
838 | x: line.center.x + endOffsetFromCenterVector.x,
839 | y: line.center.y + endOffsetFromCenterVector.y
840 | },
Subtract the end offset from the center to get the other
852 | end of line
.
{
857 | x: line.center.x - endOffsetFromCenterVector.x,
858 | y: line.center.y - endOffsetFromCenterVector.y
859 | }
860 | ];
861 | },
pointOnLineClosestToCircle() returns the point on line
873 | closest to circle
.
pointOnLineClosestToCircle: function(circle, line) {
Get the points at each end of line
.
var lineEndPoint1 = trig.lineEndPoints(line)[0];
893 | var lineEndPoint2 = trig.lineEndPoints(line)[1];
Create a vector that represents the line
905 | 906 | var lineUnitVector = trig.unitVector(
909 | trig.vectorBetween(lineEndPoint1, lineEndPoint2));
Pick a line end and create a vector that represents the 921 | imaginary line between the end and the circle.
922 | 923 | var lineEndToCircleVector = trig.vectorBetween(lineEndPoint1, circle.center);
Get a dot product of the vector between the line end and circle, and
937 | the line vector. (See the dotProduct()
function for a
938 | fuller explanation.) This projects the line end and circle
939 | vector along the line vector. Thus, it represents how far
940 | along the line to go from the end to get to the point on the
941 | 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
957 | is at or past lineEndPoint1
. So, return lineEndPoint1
.
if (projection <= 0) {
962 | return lineEndPoint1;
If projection
is greater than or equal to the length of the
974 | line, the closest point is at or past lineEndPoint2
. So,
975 | return lineEndPoint2
.
} else if (projection >= line.len) {
980 | return lineEndPoint2;
The projection indicates a point part way along the line. 992 | Return that point.
993 | 994 | } else {
997 | return {
998 | x: lineEndPoint1.x + lineUnitVector.x * projection,
999 | y: lineEndPoint1.y + lineUnitVector.y * projection
1000 | };
1001 | }
1002 | },
isLineIntersectingCircle() returns true if line
is
1014 | intersecting circle
.
isLineIntersectingCircle: function(circle, line) {
Get point on line closest to circle.
1030 | 1031 | var closest = trig.pointOnLineClosestToCircle(circle, line);
Get the distance between the closest point and the center of 1045 | the circle.
1046 | 1047 | var circleToLineDistance = trig.distance(circle.center, closest);
Return true if distance is less than the radius.
1061 | 1062 | return circleToLineDistance < circle.radius;
1065 | }
1066 | }
1094 | var physics = {
applyGravity() adds gravity to the velocity of circle
.
applyGravity: function(circle) {
1110 | circle.velocity.y += 0.06;
1111 | },
moveCircle() adds the velocity of the circle to its center.
1123 | 1124 | moveCircle: function(circle) {
1127 | circle.center.x += circle.velocity.x;
1128 | circle.center.y += circle.velocity.y;
1129 | },
bounceCircle() assumes line
is intersecting circle
and
1141 | bounces circle
off line
.
bounceCircle: function(circle, line) {
Get the vector that points out from the surface the circle is 1157 | bouncing on.
1158 | 1159 | var bounceLineNormal = physics.bounceLineNormal(circle, line);
Set the new circle velocity by reflecting the old velocity in
1173 | bounceLineNormal
.
var dot = trig.dotProduct(circle.velocity, bounceLineNormal);
1178 | circle.velocity.x -= 2 * dot * bounceLineNormal.x;
1179 | circle.velocity.y -= 2 * dot * bounceLineNormal.y;
Move the circle until it has cleared the line. This stops 1191 | the circle getting stuck in the line.
1192 | 1193 | while (trig.isLineIntersectingCircle(circle, line)) {
1196 | physics.moveCircle(circle);
1197 | }
1198 | },
bounceLineNormal() assumes line
intersects circle
. It
1210 | returns the normal to the side of the line that the circle
is
1211 | hitting.
bounceLineNormal: function(circle, line) {
Get vector that starts at the closest point on 1227 | the line and ends at the circle. If the circle is hitting 1228 | the flat of the line, this vector will point perpenticular to 1229 | the line. If the circle is hitting the end of the line, the 1230 | vector will point from the end to the center of the circle.
1231 | 1232 | var circleToClosestPointOnLineVector =
1235 | trig.vectorBetween(
1236 | trig.pointOnLineClosestToCircle(circle, line),
1237 | circle.center);
Make the normal a unit vector and return it.
1249 | 1250 | return trig.unitVector(circleToClosestPointOnLineVector);
1253 | }
1254 | };
When the DOM is ready, start the simulation.
1291 | 1292 | window.addEventListener('load', start);
1295 | })(this);
35 | annotated code 36 | raw code 37 |
38 |42 | Uses some simple trigonometry to calculate the movement of 43 | circles bouncing off lines. 44 |
45 | 46 |47 | When I first tried to write this program, I had no idea how to bounce a circle off a line. 48 | And I couldn't find any good explanations on the web. Some resources were prose descriptions that did not 49 | derive the maths. Some derived the maths, but didn't explain what was going on. Some resouces showed only code. 50 | Some resources showed no code. 51 |
52 | 53 |54 | My approach for the code is to make it proceed in small, pedantic steps. 55 | My approach for the comments is to explain all the maths spatially, rather than algebraicly. Hopefully, this means 56 | that the explanations are intuitive. 57 |
58 |An implemention of Git in JavaScript. See the main Gitlet website for more information.
40 | 41 |Over the last six years, I've become better at using Git for version control. But my conceptions of the index, the working copy, the object graph and remotes have just grown fuzzier.
42 | 43 |Sometimes, I can only understand something by implementing it. So, I wrote Gitlet, my own version of Git. I pored over tutorials. I read articles about internals. I tried to understand how API commands work by reading the docs, then gave up and ran hundreds of experiments on repositories and rummaged throught the .git directory to figure out the results.
44 | 45 |I discovered that, if approached from the inside out, Git is easy to understand. It is the product of simple ideas that, when combined, produce something very deep and beautiful.
46 | 47 |If you would like to understand what happens when you run the basic Git commands, you can read Git in six hundred words.
48 | 49 |Afterwards, read the heavily annotated Gitlet source. 1000 lines of code sounds intimidating. But it's OK. The annotations explain both Git and the code in great detail. The code mirrors the terminology of the Git command line interface, so it should be approachable. And the implementation of the main Git commands is just 350 lines.
50 | 51 |I wrote an article, Introducing Gitlet, about the process of writing the code.
52 |;(function() {
new Game() Creates the game object with the game state and logic.
70 | 71 | var Game = function() {
In index.html, there is a canvas tag that the game will be drawn in. 85 | Grab that canvas out of the DOM.
86 | 87 | var canvas = document.getElementById("space-invaders");
Get the drawing context. This contains functions that let you draw to the canvas.
101 | 102 | var screen = canvas.getContext('2d');
Note down the dimensions of the canvas. These are used to 116 | place game bodies.
117 | 118 | var gameSize = { x: canvas.width, y: canvas.height };
Create the bodies array to hold the player, invaders and bullets.
132 | 133 | this.bodies = [];
Add the invaders to the bodies array.
147 | 148 | this.bodies = this.bodies.concat(createInvaders(this));
Add the player to the bodies array.
162 | 163 | this.bodies = this.bodies.concat(new Player(this, gameSize));
In index.html, there is an audio tag that loads the shooting sound. 177 | Get the shoot sound from the DOM and store it on the game object.
178 | 179 | this.shootSound = document.getElementById('shoot-sound');
182 |
183 | var self = this;
Main game tick function. Loops forever, running 60ish times a second.
195 | 196 | var tick = function() {
Update game state.
210 | 211 | self.update();
Draw game bodies.
225 | 226 | self.draw(screen, gameSize);
Queue up the next call to tick with the browser.
240 | 241 | requestAnimationFrame(tick);
244 | };
Run the first game tick. All future calls will be scheduled by 256 | the tick() function itself.
257 | 258 | tick();
261 | };
262 |
263 | Game.prototype = {
update() runs the main game logic.
275 | 276 | update: function() {
279 | var self = this;
notCollidingWithAnything
returns true if passed body
291 | is not colliding with anything.
var notCollidingWithAnything = function(b1) {
296 | return self.bodies.filter(function(b2) { return colliding(b1, b2); }).length === 0;
297 | };
Throw away bodies that are colliding with something. They 309 | will never be updated or draw again.
310 | 311 | this.bodies = this.bodies.filter(notCollidingWithAnything);
Call update on every body.
325 | 326 | for (var i = 0; i < this.bodies.length; i++) {
329 | this.bodies[i].update();
330 | }
331 | },
draw() draws the game.
343 | 344 | draw: function(screen, gameSize) {
Clear away the drawing from the previous tick.
358 | 359 | screen.clearRect(0, 0, gameSize.x, gameSize.y);
Draw each body as a rectangle.
373 | 374 | for (var i = 0; i < this.bodies.length; i++) {
377 | drawRect(screen, this.bodies[i]);
378 | }
379 | },
invadersBelow() returns true if invader
is directly
391 | above at least one other invader.
invadersBelow: function(invader) {
If filtered array is not empty, there are invaders below.
407 | 408 | return this.bodies.filter(function(b) {
Keep b
if it is an invader, if it is in the same column
422 | as invader
, and if it is somewhere below invader
.
return b instanceof Invader &&
427 | Math.abs(invader.center.x - b.center.x) < b.size.x &&
428 | b.center.y > invader.center.y;
429 | }).length > 0;
430 | },
addBody() adds a body to the bodies array.
442 | 443 | addBody: function(body) {
446 | this.bodies.push(body);
447 | }
448 | };
new Invader() creates an invader.
485 | 486 | var Invader = function(game, center) {
489 | this.game = game;
490 | this.center = center;
491 | this.size = { x: 15, y: 15 };
Invaders patrol from left to right and back again.
503 | this.patrolX
records the current (relative) position of the
504 | invader in their patrol. It starts at 0, increases to 40, then
505 | decreases to 0, and so forth.
this.patrolX = 0;
The x speed of the invader. A positive value moves the invader 521 | right. A negative value moves it left.
522 | 523 | this.speedX = 0.3;
526 | };
527 |
528 | Invader.prototype = {
update() updates the state of the invader for a single tick.
540 | 541 | update: function() {
If the invader is outside the bounds of their patrol…
555 | 556 | if (this.patrolX < 0 || this.patrolX > 30) {
… reverse direction of movement.
570 | 571 | this.speedX = -this.speedX;
574 | }
If coin flip comes up and no friends below in this 586 | invader’s column…
587 | 588 | if (Math.random() > 0.995 &&
591 | !this.game.invadersBelow(this)) {
… create a bullet just below the invader that will move 603 | downward…
604 | 605 | var bullet = new Bullet({ x: this.center.x, y: this.center.y + this.size.y / 2 },
608 | { x: Math.random() - 0.5, y: 2 });
… and add the bullet to the game.
620 | 621 | this.game.addBody(bullet);
624 | }
Move according to current x speed.
636 | 637 | this.center.x += this.speedX;
Update variable that keeps track of current position in patrol.
651 | 652 | this.patrolX += this.speedX;
655 | }
656 | };
createInvaders() returns an array of twenty-four invaders.
668 | 669 | var createInvaders = function(game) {
672 | var invaders = [];
673 | for (var i = 0; i < 24; i++) {
Place invaders in eight columns.
685 | 686 | var x = 30 + (i % 8) * 30;
Place invaders in three rows.
700 | 701 | var y = 30 + (i % 3) * 30;
Create invader.
715 | 716 | invaders.push(new Invader(game, { x: x, y: y}));
719 | }
720 |
721 | return invaders;
722 | };
new Player() creates a player.
759 | 760 | var Player = function(game, gameSize) {
763 | this.game = game;
764 | this.size = { x: 15, y: 15 };
765 | this.center = { x: gameSize.x / 2, y: gameSize.y - this.size.y * 2 };
Create a keyboard object to track button presses.
777 | 778 | this.keyboarder = new Keyboarder();
781 | };
782 |
783 | Player.prototype = {
update() updates the state of the player for a single tick.
795 | 796 | update: function() {
If left cursor key is down…
810 | 811 | if (this.keyboarder.isDown(this.keyboarder.KEYS.LEFT)) {
… move left.
825 | 826 | this.center.x -= 2;
829 |
830 | } else if (this.keyboarder.isDown(this.keyboarder.KEYS.RIGHT)) {
831 | this.center.x += 2;
832 | }
If S key is down…
844 | 845 | if (this.keyboarder.isDown(this.keyboarder.KEYS.S)) {
… create a bullet just above the player that will move upwards…
859 | 860 | var bullet = new Bullet({ x: this.center.x, y: this.center.y - this.size.y - 10 },
863 | { x: 0, y: -7 });
… add the bullet to the game…
875 | 876 | this.game.addBody(bullet);
… rewind the shoot sound…
890 | 891 | this.game.shootSound.load();
… and play the shoot sound.
905 | 906 | this.game.shootSound.play();
909 | }
910 | }
911 | };
new Bullet() creates a new bullet.
948 | 949 | var Bullet = function(center, velocity) {
952 | this.center = center;
953 | this.size = { x: 3, y: 3 };
954 | this.velocity = velocity;
955 | };
956 |
957 | Bullet.prototype = {
update() updates the state of the bullet for a single tick.
969 | 970 | update: function() {
Add velocity to center to move bullet.
984 | 985 | this.center.x += this.velocity.x;
988 | this.center.y += this.velocity.y;
989 | }
990 | };
new Keyboarder() creates a new keyboard input tracking object.
1027 | 1028 | var Keyboarder = function() {
Records up/down state of each key that has ever been pressed.
1042 | 1043 | var keyState = {};
When key goes down, record that it is down.
1057 | 1058 | window.addEventListener('keydown', function(e) {
1061 | keyState[e.keyCode] = true;
1062 | });
When key goes up, record that it is up.
1074 | 1075 | window.addEventListener('keyup', function(e) {
1078 | keyState[e.keyCode] = false;
1079 | });
Returns true if passed key is currently down. keyCode
is a
1091 | unique number that represents a particular key on the keyboard.
this.isDown = function(keyCode) {
1096 | return keyState[keyCode] === true;
1097 | };
Handy constants that give keyCodes human-readable names.
1109 | 1110 | this.KEYS = { LEFT: 37, RIGHT: 39, S: 83 };
1113 | };
drawRect() draws passed body as a rectangle to screen
, the drawing context.
var drawRect = function(screen, body) {
1154 | screen.fillRect(body.center.x - body.size.x / 2, body.center.y - body.size.y / 2,
1155 | body.size.x, body.size.y);
1156 | };
colliding() returns true if two passed bodies are colliding. 1168 | The approach is to test for five situations. If any are true, 1169 | the bodies are definitely not colliding. If none of them 1170 | are true, the bodies are colliding.
1171 |b1
is to the left of the left of b2
.b1
is above the top of b2
.b1
is to the right of the right of b2
.b1
is below the bottom of b2
. var colliding = function(b1, b2) {
1182 | return !(
1183 | b1 === b2 ||
1184 | b1.center.x + b1.size.x / 2 < b2.center.x - b2.size.x / 2 ||
1185 | b1.center.y + b1.size.y / 2 < b2.center.y - b2.size.y / 2 ||
1186 | b1.center.x - b1.size.x / 2 > b2.center.x + b2.size.x / 2 ||
1187 | b1.center.y - b1.size.y / 2 > b2.center.y + b2.size.y / 2
1188 | );
1189 | };
When the DOM is ready, create (and start) the game.
1226 | 1227 | window.addEventListener('load', function() {
1230 | new Game();
1231 | });
1232 | })();
37 | annotated code 38 | raw code 39 |
40 |44 | Move with the arrow keys. Shoot with the S key. Refresh to play again. 45 |
46 | 47 |48 | Demonstrates a game loop, collision detection, keyboard input, 49 | drawing to the canvas and playing sounds. 50 |
51 |