;(function() { var rink_src ="https://upload.wikimedia.org/wikipedia/commons/c/c4/Icehockeylayout.svg"; // toggle debug drawing window.debugmode = true; var img = new Image(); img.src = rink_src; // size of the rink image var width = 750; var height = 348; // create and setup the DOM elements var screen = document.getElementById('screen'); var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.style.width = "100%"; // drawing context var ctx = canvas.getContext('2d'); screen.appendChild(canvas); // holds all the pucks var pucks = []; var circleRadius = 93; // the game board (ice rink) used mainly for collisions var board = { rect: new Rectangle({x: 98, y: 4}, width - 98 * 2, height - 8), // mid rect rect0: new Rectangle({x: 4, y: 98}, 94, height - 98 * 2), // left rect rect1: new Rectangle({x: width - 98, y: 98}, 94, height - 98 * 2), // right rect c0: new Circle({x: 98, y: 98}, circleRadius), // top left c1: new Circle({x: width - 98, y: 98}, circleRadius), // top right c2: new Circle({x: width - 98, y: height - 98}, circleRadius), // bot right c3: new Circle({x: 98, y: height - 98}, circleRadius), // bot left // render the board // (for debugging, sizing and placing the shapes on the rink image) render: function (ctx) { this.rect.render(ctx); this.rect0.render(ctx, "#CC1133"); this.rect1.render(ctx, "#CC1133"); this.c0.render(ctx); this.c1.render(ctx); this.c2.render(ctx); this.c3.render(ctx); }, // check if a puck is enclosed by the board // by calling all the attatched shapes' (that make up the board) // enclosing functions encloses: function (puck) { var obj = (this.rect.encloses(puck) || this.rect0.encloses(puck) || this.rect1.encloses(puck) || this.c0.encloses(puck) || this.c1.encloses(puck) || this.c2.encloses(puck) || this.c3.encloses(puck)); if (typeof obj === 'object') { // save the enclosing Shape on the puck object for later use puck.collidedLastWith = obj; } return obj; } }; // helper function (used in collision) to calculate // angle of speed and angle of collision against circle tangents in degrees function angle(x, y) { var a = Math.atan(x / y) * (180 / Math.PI); if (x > 0) { if (y > 0) return a + 270; if (y < 0) return a + 90; if (y == 0) return 0; } if (x < 0) { if (y > 0) return a + 270; if (y < 0) return a + 90; if (y == 0) return 180; } if (x == 0) { if (y < 0) return 90; if (y > 0) return 270; } return NaN; }; function Circle (point, rad, collideAgainst) { this.point = point; this.rad = rad; this.type = "Circle"; this.render = function (ctx, col) { ctx.fillStyle = col || "#1133CC"; ctx.globalAlpha = 0.2; ctx.beginPath(); ctx.arc(this.point.x, this.point.y, this.rad, 0, 2 * Math.PI); ctx.fill(); ctx.globalAlpha = 1; }; this.encloses = function (circle) { var xx = (this.point.x - circle.point.x); var yy = (this.point.y - circle.point.y); var distance = Math.sqrt(xx * xx + yy * yy); return (distance <= (this.rad + circle.rad * 0.2)) ? this : false; }; this.collideAgainst = collideAgainst || function (puck) { var cr = this.rad; var cx = this.point.x; var cy = this.point.y; // calculate velocity var vx = puck.spd.x; var vy = puck.spd.y; var vv = Math.sqrt(vx * vx + vy * vy); var va = angle(vx, vy); // calculate circle tangent var dx = puck.point.x - cx; var dy = puck.point.y - cy; var ca = angle(dx, dy); // circle angle to puck var ta = ca - 90; var ta1 = ca - 90; var ta2 = ca + 90; if (ta1 < 0) ta1 += 360; if (ta2 < 0) ta2 += 360; var delta = ((ta1 > (va - 90)) && (ta1 < va)); // choose appropriate tangent based on the // direction of the collision to the circumference if (ta1 > (va - 90) && ta1 < va) ta = ta1; else ta = ta2; //log("va: %s, ca: %s, ta: %s, delta: %s", va | 0, ca | 0, ta | 0, delta); // deg to radians (Math library uses radians) var aa = ca * (Math.PI / 180); // calculate new position (at circle circumference) var offset = 1; // very near for a smooth curve var nx = cx + cr * Math.cos(aa) * offset; var ny = cy + cr * -Math.sin(aa) * offset; // new speed based on the tangent vv *= 0.99; // dampen speed abit var ta_rad = ta * (Math.PI / 180); // to radians for Math var nvx = vv * Math.cos(ta_rad); var nvy = vv * -Math.sin(ta_rad); // set the new calculated stats puck.point.x = nx; puck.point.y = ny; puck.spd.x = nvx; puck.spd.y = nvy; }; }; function Rectangle (point, width, height, collideAgainst) { this.point = point; this.width = width; this.height = height; this.type = "Rectangle"; this.render = function (ctx, col) { ctx.fillStyle = col || "#11CC33"; ctx.globalAlpha = 0.2; ctx.fillRect(this.point.x, this.point.y, this.width, this.height); ctx.globalAlpha = 1; }; this.encloses = function (circle) { var p = circle.point; var h = circle.rad / 2; // basic AABB collision return (!(p.x + h < this.point.x || p.x - h > this.point.x + this.width || p.y + h < this.point.y || p.y - h > this.point.y + this.height)) ? this : false; }; this.collideAgainst = collideAgainst || function (puck) { var now = Date.now(); var spd = Math.sqrt(puck.spd.x * puck.spd.x + puck.spd.y * puck.spd.y); var delay = Math.min(Math.max(600, 2000 / spd), 50); if (now - puck.lastRectCollisionTime < delay) return; puck.lastRectCollisionTime = now; if (puck.point.y > height / 10 && puck.point.y < canvas.height - canvas.height / 10) { puck.spd.x = -puck.spd.x; } else { puck.spd.y = -puck.spd.y; } }; }; // load image and start simulation when done img.onload = function () { // kickstart the animation loop requestAnimationFrame(animate); console.log("App loaded"); var puck = new Puck({x: width / 2, y: height - height / 10}, 5); puck.spd = {x: 15, y: 1}; pucks.push( puck ); }; function Puck (point, rad) { this.rad = rad; this.point = point; this.spd = {x: 0, y: 0}; this.dead = false; var stillTicks = 0; this.tick = function () { this.point.x += this.spd.x; this.point.y += this.spd.y; // friction var f = 0.9875; this.spd.x *= f; this.spd.y *= f; if (Math.round(this.spd.x * 10) / 10 == 0 && Math.round(this.spd.y * 10) / 10 == 0) { stillTicks++; if (stillTicks > 10) { this.dead = true; } } }; this.render = function (ctx) { ctx.fillStyle = "#111"; ctx.globalAlpha = 1; ctx.beginPath(); ctx.arc(this.point.x, this.point.y, this.rad, 0, 2 * Math.PI); ctx.fill(); }; }; var lastTime = Date.now(); var TARGET_FPS = 60; var MS_PER_FRAME = 1000 / TARGET_FPS; var timebuffer = 0; // main game update function animate () { var now = Date.now(); var updateLength = (now - lastTime); lastTime = now; var delta = (updateLength / MS_PER_FRAME) + timebuffer; if (delta > 5) { delta = 5; } // max 1 update (no timebuffer catchup) -> less cpu, more battery life // delta = 1; // when in dev mode // keep updates at TARGET_FPS while (delta >= 1) { update(); delta -= 1.0; }; timebuffer = delta; render(); requestAnimationFrame(animate); }; var lastShot = Date.now(); function update () { var now = Date.now(); // update pucks for (var i = 0; i < pucks.length; i++) { var puck = pucks[i]; puck.tick(); if (puck.dead) { // remove the puck at this index and reduce the i index counter by 1 pucks.splice(i--, 1); } else { // check collision for puck against board if (!board.encloses( puck )) { // outside of rink, i.e., has collides with rink previous tick // puck should have variable name "collidedLastWith" set. // use it to determine type of collision if (puck.collidedLastWith) { console.log("Last collided with: " + puck.collidedLastWith.type); puck.collidedLastWith.collideAgainst( puck ); } } } }; if (now - lastShot > 1000) { lastShot = now; var puck = new Puck({x: width / 2, y: height - height / 10}, 5); //puck.spd = {x: Math.random() * 50 - 25, y: Math.random() * 30 - 15}; var xspd = 6 + Math.random() * 30; puck.spd = {x: Math.random() < 0.5 ? xspd: -xspd, y: Math.random() * 6 - 3}; pucks.push( puck ); } }; function render () { // draw rink ctx.clearRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); // draw board geometrics (debug info) if (window.debugmode) board.render(ctx); for (var i = 0; i < pucks.length; i++) { var puck = pucks[i]; puck.render(ctx); }; }; })();

!