├── README.md ├── examples ├── .DS_Store └── brkout │ ├── index.html │ └── scripts │ ├── ball.js │ ├── breakout.js │ ├── bricks.js │ ├── g_keys.js │ ├── g_main.js │ ├── particles.js │ ├── player.js │ ├── render.js │ ├── update.js │ └── utils.js └── shadow.js /README.md: -------------------------------------------------------------------------------- 1 | shadow.js 2 | ========= 3 | 4 | A simple script for casting shadows from a dynamic point lightsource 5 | 6 | 7 | #### Example 8 | var origin = {x: 100, y: 200}; 9 | var rect = { 10 | lx: 300, 11 | ty: 350, 12 | w: 50, 13 | h: 200 14 | }; 15 | 16 | // Cast a shadow of a rectangle from origin 17 | Shadow.castFromRectangle( 18 | ctx, 19 | origin, 20 | rect.lx, 21 | rect.ty, 22 | rect.w, 23 | rect.h 24 | ); 25 | 26 | 27 | #### TODO 28 | * Allow gradients in shadows, i.e. let them fade out instead of just exceeding the edge of the canvas. 29 | * Implement multisampling of shadows, make the edges softer. 30 | * Implement shadow-casting for curved forms via bezier curves. 31 | * Implement shadow-casting for circles. 32 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kreldjarn/shadowjs/7ff6939c064be5b917015aba2ec77f3d05eaa795/examples/.DS_Store -------------------------------------------------------------------------------- /examples/brkout/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Breakout! 5 | 21 | 22 | 23 | 24 | 25 | https://chrome.google.com 26 | 27 | 28 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/brkout/scripts/ball.js: -------------------------------------------------------------------------------- 1 | // ========== 2 | // BALL STUFF 3 | // ========== 4 | var g_ball = (function() 5 | { 6 | // Private 7 | // ======= 8 | var loc = g_player.getLoc(); 9 | var cx = loc.x, 10 | cy = loc.y, 11 | radius = 10, 12 | vel = 7, 13 | angle = -0.5 * Math.PI, 14 | trail = halo(cx, cy, '255, 255, 255'), 15 | isIdle = true, 16 | justCollided = false; 17 | RELEASE_KEY = ' '.charCodeAt(0); 18 | 19 | var handleCollisions = function(prevX, prevY, nextX, nextY) 20 | { 21 | // Bounce off the paddle 22 | var collision = g_player.collidesWith(prevX, prevY, nextX, nextY, radius); 23 | if (!collision.miss) 24 | { 25 | angle = collision.angle; 26 | } 27 | 28 | // Hit roof 29 | if (nextY < 0) 30 | { 31 | angle *= -1; 32 | } 33 | // Hit bottom 34 | if (nextY > g_canvas.height) 35 | { 36 | bg.flash(); 37 | // If 'floor is lava' is turned on we reset the ball and flash the screen 38 | if (g_death) setIdle(); 39 | // If not, we bounce of the bottom 40 | else angle *= -1; 41 | } 42 | 43 | // Hit sides 44 | if (nextX < 0 || 45 | nextX > g_canvas.width) { 46 | angle = Math.PI - angle; 47 | } 48 | 49 | // Collision with bricks 50 | // If we've just collided we are definitely hitting the *same* brick on 51 | // two consecutive ticks. 52 | if (justCollided) 53 | { 54 | justCollided = false; 55 | return; 56 | } 57 | var potential = g_level.getBricksAt(cx - radius, 58 | cy - radius, 59 | 2 * radius, 60 | 2 * radius); 61 | for (var i = 0; i < potential.length; ++i) 62 | { 63 | justCollided = handleCollision(potential[i], prevX, prevY, nextX, nextY); 64 | if (justCollided) break; // Only handle one collision per tick 65 | } 66 | }; 67 | 68 | var handleCollision = function(pot, prevX, prevY, nextX, nextY) 69 | { 70 | var ind = pot.getInd(); 71 | 72 | var x = g_level.getX(ind.j), 73 | y = g_level.getY(ind.i), 74 | w = g_level.getWidth(), 75 | h = g_level.getHeight(); 76 | 77 | var r = radius; 78 | var prev, next; 79 | // East/west collisions 80 | if (prevX < nextX) 81 | { 82 | next = nextX + r >= x && 83 | nextY - r <= y + h && 84 | nextY + r >= y; 85 | prev = prevX + r >= x; 86 | } 87 | else 88 | { 89 | next = nextX - r <= x + w && 90 | nextY - r <= y + h && 91 | nextY + r >= y; 92 | prev = prevX + r <= x + w; 93 | 94 | } 95 | if (next && !prev) 96 | { 97 | pot.decreaseLife(); 98 | angle = Math.PI - angle; 99 | return true; 100 | } 101 | 102 | // North/south collisions 103 | if (prevY < nextY) 104 | { 105 | next = nextY - r <= y && 106 | nextX + r >= x && 107 | nextX - r <= x + w; 108 | prev = prevY + r <= y; 109 | } 110 | else 111 | { 112 | next = nextY + r >= y + h && 113 | nextX + r >= x && 114 | nextX - r <= x + w; 115 | prev = prevY - r >= y + h; 116 | } 117 | if (next && !prev) 118 | { 119 | pot.decreaseLife(); 120 | angle *= -1; 121 | return true; 122 | } 123 | }; 124 | 125 | // Public 126 | // ====== 127 | var updateDispatcher = function(du) 128 | { 129 | if (g_keys.eatKey(RELEASE_KEY) && getIdle() && !g_level.hasWon()) 130 | { 131 | angle = -0.75*Math.PI + 0.5*Math.random() * Math.PI; 132 | setActive(); 133 | } 134 | if (!isIdle) 135 | update(du); 136 | else 137 | idle(du); 138 | }; 139 | var update = function(du) 140 | { 141 | // Remember my previous position 142 | var prevX = cx; 143 | var prevY = cy; 144 | 145 | // Compute my provisional new position (barring collisions) 146 | var nextX = prevX + Math.cos(angle) * vel * du; 147 | var nextY = prevY + Math.sin(angle) * vel * du; 148 | 149 | handleCollisions(prevX, prevY, nextX, nextY); 150 | 151 | // *Actually* update my position 152 | // ...using whatever velocity I've ended up with 153 | // 154 | cx += Math.cos(angle) * vel * du; 155 | cy += Math.sin(angle) * vel * du; 156 | trail.update(cx, cy); 157 | }; 158 | 159 | var idle = function() 160 | { 161 | loc = g_player.getLoc(); 162 | cx = loc.x; 163 | cy = loc.y - radius; 164 | trail.update(cx, cy); 165 | }; 166 | 167 | 168 | var render = function (ctx) 169 | { 170 | fillCircle(ctx, cx, cy, radius, '#FFF'); 171 | trail.render(ctx); 172 | }; 173 | 174 | var getCenter = function() 175 | { 176 | return {x: cx, y: cy}; 177 | }; 178 | 179 | var getVel = function() 180 | { 181 | return {x: vel * Math.cos(angle), y: vel * Math.sin(angle)}; 182 | }; 183 | 184 | var setIdle = function() 185 | { 186 | isIdle = true; 187 | }; 188 | 189 | var getIdle = function() 190 | { 191 | return isIdle; 192 | }; 193 | 194 | var setActive = function() 195 | { 196 | isIdle = false; 197 | }; 198 | 199 | var getAngle = function () 200 | { 201 | return angle; 202 | }; 203 | 204 | return { 205 | update : updateDispatcher, 206 | render : render, 207 | getCenter : getCenter, 208 | getVel : getVel, 209 | setIdle : setIdle, 210 | getIdle : getIdle, 211 | setActive : setActive, 212 | getAngle : getAngle 213 | }; 214 | })(); -------------------------------------------------------------------------------- /examples/brkout/scripts/breakout.js: -------------------------------------------------------------------------------- 1 | // "Showoff Breakout" 2 | 3 | "use strict"; 4 | 5 | /* jshint browser: true, devel: true, globalstrict: true */ 6 | 7 | var g_canvas = document.getElementById("workspace"); 8 | var g_ctx = g_canvas.getContext("2d"); 9 | 10 | 11 | 12 | // LEVEL 13 | 14 | var level = [ 15 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 16 | [1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1], 17 | [1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 5, 1], 18 | [0, 0, 0, 3, 3, 4, 4, 3, 3, 0, 0, 0], 19 | [1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 5, 1], 20 | [1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1], 21 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 22 | ]; 23 | 24 | 25 | var g_level = setUpLevel(level); 26 | var TOGGLE_DEATH_KEY = 'N'.charCodeAt(0); 27 | var g_death = true; 28 | 29 | 30 | document.getElementById('start-level').onclick = function(e) 31 | { 32 | level = document.getElementById('level-editor').value; 33 | level = '[' + level + ']'; 34 | g_level = setUpLevel(JSON.parse(level)); 35 | } 36 | 37 | 38 | // UPDATE 39 | 40 | function updateSimulation(du) { 41 | if (g_keys.eatKey(TOGGLE_DEATH_KEY)) g_death = !g_death; 42 | 43 | if (g_level.hasWon()) 44 | { 45 | g_ball.setIdle(); 46 | } 47 | 48 | g_ball.update(du); 49 | 50 | g_player.update(du); 51 | 52 | 53 | for (var i = 0; i < g_explosions.length;) 54 | { 55 | if (!g_explosions[i].isOver()) 56 | { 57 | g_explosions[i++].update(); 58 | } 59 | else 60 | { 61 | g_explosions.splice(i, 1); 62 | } 63 | } 64 | } 65 | 66 | 67 | // RENDER 68 | 69 | function renderSimulation(ctx) { 70 | g_player.render(ctx); 71 | g_level.render(ctx); 72 | g_ball.render(ctx); 73 | for (var i = 0; i < g_explosions.length; ++i) 74 | { 75 | g_explosions[i].render(ctx); 76 | } 77 | } 78 | 79 | // Kick it off 80 | g_main.init(); -------------------------------------------------------------------------------- /examples/brkout/scripts/bricks.js: -------------------------------------------------------------------------------- 1 | function setUpLevel(levelTemplate) 2 | { 3 | var MAX_BRICKS_ROW = 12, 4 | MAX_BRICKS_COL = 7, 5 | OFFSET = 60; 6 | 7 | var BRICK_MARGIN = 15, 8 | BRICK_WIDTH = ((g_canvas.width - (2 * OFFSET)) / MAX_BRICKS_ROW) - BRICK_MARGIN, 9 | BRICK_HEIGHT = BRICK_WIDTH; 10 | 11 | var colour = ['255, 55, 100', 12 | '255, 55, 255', 13 | '55, 255, 255', 14 | '255, 155, 55', 15 | '55, 255, 155', 16 | '55, 155, 255']; 17 | 18 | var indestructible_color = '255, 255, 255' 19 | 20 | g_ball.setIdle(); 21 | 22 | var level = (function() 23 | { 24 | // Private 25 | // ======= 26 | var bricks = []; 27 | // We keep score of the total life for the victory conditions, rather 28 | // than checking all bricks, each tick. 29 | var totalLife = 0; 30 | for (var i = 0; i < levelTemplate.length; ++i, bricks.push([])); 31 | 32 | // Public 33 | // ====== 34 | var getX = function(j) 35 | { 36 | return OFFSET + ((BRICK_HEIGHT + BRICK_MARGIN) * j); 37 | }; 38 | var getY = function(i) 39 | { 40 | return OFFSET + ((BRICK_WIDTH + BRICK_MARGIN) * i); 41 | }; 42 | var getWidth = function(i, j) 43 | { 44 | return BRICK_WIDTH; 45 | }; 46 | var getHeight = function(i, j) 47 | { 48 | return BRICK_HEIGHT; 49 | }; 50 | 51 | var setBrick = function(i, j, brick) 52 | { 53 | bricks[i][j] = brick; 54 | }; 55 | var getBrick = function(i, j) 56 | { 57 | var tmp = bricks[i]; 58 | if (!tmp) return null; 59 | tmp = tmp[j]; 60 | if (tmp && tmp.getLife()) return tmp; 61 | return null; 62 | }; 63 | // Takes a rectangle as an argument and returns an array of bricks that 64 | // are close to it. 65 | var getBricksAt = function(x, y, w, h) 66 | { 67 | // We presume that the input rectangle is smaller than or equal to 68 | // this size of a brick. Therefore, we can at most return 4 bricks 69 | // that overlap the rectangle. 70 | var nj = Math.floor((x + w/2 - OFFSET) / (BRICK_WIDTH + BRICK_MARGIN)), 71 | ni = Math.floor((y + h/2 - OFFSET) / (BRICK_HEIGHT + BRICK_MARGIN)); 72 | 73 | var potential = [ 74 | getBrick(ni, nj), 75 | getBrick(ni + 1, nj), 76 | getBrick(ni, nj + 1), 77 | getBrick(ni + 1, nj + 1) 78 | ]; 79 | for (var i = 0; i < potential.length; ) 80 | { 81 | if (potential[i]) ++i; 82 | else potential.splice(i, 1); 83 | } 84 | 85 | return potential; 86 | }; 87 | 88 | var addToLife = function(i) 89 | { 90 | totalLife += i; 91 | }; 92 | 93 | var hasWon = function() 94 | { 95 | return totalLife === 0; 96 | } 97 | 98 | var render = function(ctx) 99 | { 100 | if (!g_blindMode) 101 | { 102 | // Render bricks 103 | for (var i = 0; i < bricks.length; ++i) 104 | { 105 | var yOffset = getY(i); 106 | for (var j = 0; j < bricks[i].length; ++j) 107 | { 108 | var xOffset = getX(j); 109 | bricks[i][j].render(ctx, xOffset, yOffset); 110 | } 111 | } 112 | } 113 | // Render shadows on top of the bricks 114 | for (var i = 0; i < bricks.length; ++i) 115 | { 116 | var yOffset = getY(i); 117 | for (var j = 0; j < bricks[i].length; ++j) 118 | { 119 | var xOffset = getX(j); 120 | if (bricks[i][j].getLife()) 121 | { 122 | Shadow.castFromRectangle( 123 | ctx, 124 | g_ball.getCenter(), 125 | xOffset, 126 | yOffset, 127 | BRICK_WIDTH, 128 | BRICK_HEIGHT 129 | ); 130 | } 131 | } 132 | } 133 | }; 134 | 135 | return { 136 | setBrick : setBrick, 137 | getBrick : getBrick, 138 | getBricksAt : getBricksAt, 139 | getX : getX, 140 | getY : getY, 141 | getWidth : getWidth, 142 | getHeight : getHeight, 143 | render : render, 144 | addToLife : addToLife, 145 | hasWon : hasWon 146 | }; 147 | })(); 148 | 149 | // We populate the wall with bricks as denoted by levelTemplate 150 | for (var i = 0; i < levelTemplate.length && i < MAX_BRICKS_COL; ++i) 151 | { 152 | for (var j = 0; j < levelTemplate[i].length && j < MAX_BRICKS_ROW; ++j) 153 | { 154 | level.setBrick(i, j, (function() 155 | { 156 | // Bricks don't have an absolute position. The wall handles those 157 | 158 | // Private 159 | // ======= 160 | var type = levelTemplate[i][j], 161 | life = (type < colour.length) ? type : -1, // Indestructible 162 | indestructible = life === -1, 163 | _i = i, 164 | _j = j; 165 | if (!indestructible) 166 | level.addToLife(life); 167 | 168 | // Public 169 | // ====== 170 | var render = function(ctx, x, y) 171 | { 172 | //console.log('x: ' + x + ', y: ' + y); 173 | if (life) 174 | { 175 | if (!indestructible) 176 | { 177 | var c_index = (life >= colour.length) ? 178 | colour.length - 1 : life - 1; 179 | var c = colour[c_index]; 180 | } 181 | else 182 | { 183 | var c = indestructible_color; 184 | } 185 | fillBox(ctx, x, y, BRICK_WIDTH, BRICK_HEIGHT, 186 | 'rgba(' + c + ',255)'); 187 | } 188 | 189 | }; 190 | var decreaseLife = function() 191 | { 192 | if (life && !indestructible) 193 | { 194 | var col = colour[life - 1].split(', '); 195 | bg.flash(col[0], col[1], col[2]); 196 | life--; 197 | level.addToLife(-1); 198 | var magnitude = 50; 199 | var i = getInd(); 200 | var vel = g_ball.getVel(); 201 | g_explosions.push(brickSplosion(g_level.getX(i.j), 202 | g_level.getY(i.i), 203 | BRICK_WIDTH, 204 | BRICK_HEIGHT, 205 | vel.x, 206 | vel.y, 207 | magnitude, 208 | colour[life])); 209 | } 210 | 211 | }; 212 | var getLife = function() 213 | { 214 | return life; 215 | }; 216 | var getInd = function() 217 | { 218 | return {i: _i, j: _j}; 219 | }; 220 | 221 | return { 222 | render : render, 223 | decreaseLife : decreaseLife, 224 | getLife : getLife, 225 | getInd : getInd 226 | }; 227 | })()); 228 | } 229 | } 230 | return level; 231 | 232 | } -------------------------------------------------------------------------------- /examples/brkout/scripts/g_keys.js: -------------------------------------------------------------------------------- 1 | // ================= 2 | // KEYBOARD HANDLING 3 | // ================= 4 | 5 | var g_keys = (function() 6 | { 7 | // Private 8 | // ======= 9 | var state = {}; 10 | var handleKeydown = function(e) 11 | { 12 | state[e.keyCode] = true; 13 | }; 14 | var handleKeyup = function(e) 15 | { 16 | state[e.keyCode] = false; 17 | }; 18 | window.addEventListener("keydown", handleKeydown); 19 | window.addEventListener("keyup", handleKeyup); 20 | 21 | 22 | // Public 23 | // ====== 24 | var eatKey = function(keyCode) 25 | { 26 | var isDown = state[keyCode]; 27 | state[keyCode] = false; 28 | return isDown; 29 | }; 30 | var getState = function(keyCode) 31 | { 32 | return state[keyCode]; 33 | }; 34 | 35 | return { 36 | getState : getState, 37 | eatKey : eatKey 38 | }; 39 | })(); -------------------------------------------------------------------------------- /examples/brkout/scripts/g_main.js: -------------------------------------------------------------------------------- 1 | // MAINLOOP 2 | 3 | var g_main = { 4 | // "Frame Time" is a (potentially high-precision) frame-clock for animations 5 | _frameTime_ms : null, 6 | _frameTimeDelta_ms : null, 7 | 8 | }; 9 | 10 | // Perform one iteration of the mainloop 11 | g_main.iter = function (frameTime) { 12 | 13 | // Use the given frameTime to update all of our game-clocks 14 | this._updateClocks(frameTime); 15 | 16 | // Perform the iteration core to do all the "real" work 17 | this._iterCore(this._frameTimeDelta_ms); 18 | 19 | // Diagnostics, such as showing current timer values etc. 20 | this._debugRender(g_ctx); 21 | 22 | // Request the next iteration if needed 23 | if (!this._isGameOver) this._requestNextIteration(); 24 | }; 25 | 26 | g_main._updateClocks = function (frameTime) { 27 | 28 | // First-time initialisation 29 | if (this._frameTime_ms === null) this._frameTime_ms = frameTime; 30 | 31 | // Track frameTime and its delta 32 | this._frameTimeDelta_ms = frameTime - this._frameTime_ms; 33 | this._frameTime_ms = frameTime; 34 | }; 35 | 36 | g_main._iterCore = function (dt) { 37 | 38 | // Handle QUIT 39 | if (requestedQuit()) { 40 | this.gameOver(); 41 | return; 42 | } 43 | 44 | update(dt); 45 | render(g_ctx); 46 | }; 47 | 48 | g_main._isGameOver = false; 49 | 50 | g_main.gameOver = function () { 51 | //this._isGameOver = true; 52 | //console.log("gameOver: quitting..."); 53 | }; 54 | 55 | // Simple voluntary quit mechanism 56 | // 57 | var KEY_QUIT = 'Q'.charCodeAt(0); 58 | function requestedQuit() { 59 | return false; 60 | // This is really annoying: 61 | return g_keys.getState(KEY_QUIT); 62 | } 63 | 64 | // Annoying shim for cross-browser compat 65 | window.requestAnimationFrame = 66 | window.requestAnimationFrame || 67 | window.mozRequestAnimationFrame || 68 | window.webkitRequestAnimationFrame || 69 | window.msRequestAnimationFrame; 70 | 71 | // For the "window" APIs to callback to: 72 | function mainIterFrame(frameTime) { 73 | g_main.iter(frameTime); 74 | } 75 | 76 | g_main._requestNextIteration = function () { 77 | window.requestAnimationFrame(mainIterFrame); 78 | }; 79 | 80 | // Mainloop-level debug-rendering 81 | 82 | var TOGGLE_TIMER_SHOW = 'T'.charCodeAt(0); 83 | 84 | g_main._doTimerShow = false; 85 | 86 | g_main._debugRender = function (ctx) { 87 | 88 | if (g_keys.eatKey(TOGGLE_TIMER_SHOW)) this._doTimerShow = !this._doTimerShow; 89 | 90 | if (!this._doTimerShow) return; 91 | 92 | var y = 350; 93 | ctx.fillText('FT ' + this._frameTime_ms, 50, y+10); 94 | ctx.fillText('FD ' + this._frameTimeDelta_ms, 50, y+20); 95 | ctx.fillText('UU ' + g_prevUpdateDu, 50, y+30); 96 | ctx.fillText('FrameSync ON', 50, y+40); 97 | }; 98 | 99 | g_main.init = function () { 100 | this._requestNextIteration(); 101 | }; -------------------------------------------------------------------------------- /examples/brkout/scripts/particles.js: -------------------------------------------------------------------------------- 1 | var g_explosions = []; 2 | function particle(_x, _y, _r, _dX, _dY, _colour) 3 | { 4 | // Private 5 | // ======= 6 | var x = _x, 7 | y = _y, 8 | r = _r, 9 | // dX and dY are X and Y velocity, respectively 10 | dX = _dX, 11 | dY = _dY, 12 | colour = _colour, 13 | alpha = 0.3 + Math.random() * 0.5; 14 | // degrade is the magnitude by which alpha and size changes 15 | // each tick 16 | var degrade = -0.015; 17 | 18 | // Public 19 | // ====== 20 | var res = {}; 21 | res.update = function(growth) 22 | { 23 | x += (dX + Math.random()/10); 24 | y += (dY + Math.random()/10); 25 | alpha += degrade; 26 | 27 | // Calculate new radius 28 | if (growth) 29 | var nr = r - (degrade * 2); 30 | else 31 | { 32 | var nr = r + (degrade * 2); 33 | // Add gravity 34 | dY -= degrade * 10; 35 | } 36 | 37 | if (nr > 0) r = nr; 38 | }; 39 | res.render = function(ctx) 40 | { 41 | ctx.beginPath(); 42 | ctx.arc(x, y, r, 0, Math.PI*2, false); 43 | ctx.fillStyle = 'rgba('+ colour +', ' + alpha + ')'; 44 | ctx.fill(); 45 | }; 46 | res.getAlpha = function() 47 | { 48 | return alpha; 49 | }; 50 | return res; 51 | } 52 | 53 | 54 | function halo(_x, _y, _colour) 55 | { 56 | // Private 57 | // ======= 58 | var x = _x, 59 | y = _y, 60 | colour = _colour, 61 | r = 200, 62 | pulse = 0.1, 63 | particles = []; 64 | 65 | // Public 66 | // ====== 67 | res = {}; 68 | res.spawnParticle = function() 69 | { 70 | // We randomize the velocity vector of the particles to make 71 | // them look more natural. 72 | velX = (Math.random() < 0.5) ? Math.random() : - Math.random(); 73 | velY = (Math.random() < 0.5) ? Math.random() : - Math.random(); 74 | particles.push(particle(x, y, Math.random()*10, velX, velY, colour)); 75 | }; 76 | res.update = function(_x, _y) 77 | { 78 | this.spawnParticle(); 79 | this.spawnParticle(); 80 | 81 | x = _x; 82 | y = _y; 83 | 84 | if (pulse > 2.5) 85 | pulse = 0; 86 | else 87 | pulse += 0.3; 88 | 89 | for (var i = 0; i < particles.length; ) 90 | { 91 | particles[i].update(true); 92 | // We allow particles to be garbage collected when they have 93 | // faded almost completely 94 | if (particles[i].getAlpha() < 0.05) 95 | particles.splice(i, 1); 96 | else 97 | ++i; 98 | } 99 | }; 100 | res.render = function(ctx) 101 | { 102 | var deltaR1 = (r + pulse) / 400; 103 | var deltaR2 = deltaR1 / 2; 104 | ctx.beginPath(); 105 | var rad = ctx.createRadialGradient(x, y, pulse, x, y, r); 106 | rad.addColorStop(0, 'rgba(155, 200, 255, 0.0)'); 107 | rad.addColorStop(0.1, 'rgba(255, 255, 255,' + deltaR1 + ')'); 108 | rad.addColorStop(0.1, 'rgba(155, 200, 255,' + deltaR2 + ')'); 109 | rad.addColorStop(0.8, 'rgba(155, 200, 255, 0)'); 110 | ctx.fillStyle = rad; 111 | ctx.arc(x, y, r, 0, Math.PI*2, false); 112 | ctx.fill(); 113 | for (var i = 0; i < particles.length; ++i) 114 | { 115 | particles[i].render(ctx); 116 | } 117 | }; 118 | return res; 119 | } 120 | 121 | function brickSplosion(x, y, w, h, vX, vY, magnitude, colour) 122 | { 123 | // Private 124 | // ======= 125 | var particles = []; 126 | for (var i = 0; i < magnitude; ++i) 127 | { 128 | var cx = x + Math.random() * w, 129 | cy = y + Math.random() * h, 130 | r = 2 + Math.random() * 3, 131 | dX = cx - (x + w/2) + vX, 132 | dY = cy - (y + h/2) + vY; 133 | // dX, dY => box *slowly* comes apart 134 | particles.push(particle(cx, cy, r, dX/10, dY/10, colour)); 135 | } 136 | 137 | // Public 138 | // ====== 139 | var res = {}; 140 | res.update = function() 141 | { 142 | for (var i = 0; i < particles.length; ) 143 | { 144 | particles[i].update(false); 145 | if (particles[i].getAlpha() < 0.05) 146 | particles.splice(i, 1); 147 | else 148 | ++i; 149 | } 150 | }; 151 | res.render = function(ctx) 152 | { 153 | for (var i = 0; i < particles.length; ++i) 154 | { 155 | particles[i].render(ctx); 156 | } 157 | }; 158 | res.isOver = function() 159 | { 160 | return particles.length === 0; 161 | }; 162 | return res; 163 | } -------------------------------------------------------------------------------- /examples/brkout/scripts/player.js: -------------------------------------------------------------------------------- 1 | var KEY_A = 'A'.charCodeAt(0); 2 | var KEY_D = 'D'.charCodeAt(0); 3 | var KEY_LT = 37 4 | var KEY_RT = 39 5 | var g_player = player(40, 570, KEY_LT, KEY_RT); 6 | 7 | function player(_cx, _cy, _GO_LEFT, _GO_RIGHT) 8 | { 9 | // Private 10 | // ======= 11 | var cx = _cx, 12 | cy = _cy, 13 | GO_LEFT = _GO_LEFT, 14 | GO_RIGHT = _GO_RIGHT; 15 | 16 | var halfWidth = 40, 17 | halfHeight = 5, 18 | easeModifier = 15, 19 | targetX = cx; 20 | 21 | // Public 22 | // ====== 23 | var res = {}; 24 | res.update = function (du) { 25 | cx += (targetX - cx) / easeModifier * du; 26 | 27 | // Bounce off the sides 28 | if (cx - halfWidth < 0) 29 | { 30 | if (targetX < halfWidth) 31 | targetX = halfWidth + Math.abs(targetX); 32 | } 33 | else if (cx + halfWidth > g_canvas.width) 34 | { 35 | 36 | if (targetX + halfWidth > g_canvas.width) 37 | targetX = 2 * g_canvas.width - 2 * halfWidth - targetX; 38 | } 39 | 40 | 41 | if (g_keys.getState(GO_LEFT)) 42 | targetX -= 11 * du; 43 | if (g_keys.getState(GO_RIGHT)) 44 | targetX += 11 * du; 45 | }; 46 | 47 | res.render = function (ctx) { 48 | Shadow.castFromRectangle( 49 | ctx, 50 | g_ball.getCenter(), 51 | cx - halfWidth, 52 | cy - halfHeight, 53 | halfWidth * 2, 54 | halfHeight * 2 55 | ); 56 | if (!g_blindMode) 57 | { 58 | ctx.save(); 59 | ctx.fillStyle = '#FFF'; 60 | ctx.fillRect(cx - halfWidth, 61 | cy - halfHeight, 62 | halfWidth * 2, 63 | halfHeight * 2); 64 | ctx.restore(); 65 | } 66 | }; 67 | 68 | res.getLoc = function() 69 | { 70 | return {x: cx, y: cy}; 71 | } 72 | 73 | res.collidesWith = function(prevX, prevY, 74 | nextX, nextY, 75 | r) 76 | { 77 | // We check for collisions with the top and bottom of the paddle: 78 | var right = cx + halfWidth, 79 | left = cx - halfWidth; 80 | var playerEdge = cy; 81 | // Check Y coords 82 | if ((nextY - r < playerEdge && prevY - r >= playerEdge) || 83 | (nextY + r > playerEdge && prevY + r <= playerEdge)) { 84 | // Check X coords 85 | if (nextX + r >= left && 86 | nextX - r <= right) { 87 | // New angle of the ball depends on its orientation wrt the player 88 | var deltaX = nextX - cx; 89 | var deltaY = nextY - cy; 90 | // Collisions with the ball push the Player object in the opposite 91 | // direction to that of the ball. The ball is twice as heavy as 92 | // the paddle 93 | targetX -= deltaX * 2; 94 | return {angle: Math.atan2(deltaY, deltaX)}; 95 | } 96 | } 97 | // We check for collisions with the sides of the paddle: 98 | if (nextX - r < right && prevX - r >= right || 99 | nextX + r > left && prevX + r <= left) 100 | { 101 | if (nextY + r >= cy - halfHeight && 102 | nextY - r <= cy - halfHeight) 103 | { 104 | // Extra kick if we're hitting the paddle on the side 105 | targetX += g_ball.getVel().x * 20; 106 | // If side collision, we send the ball in the direction 107 | // from whence it came 108 | return {angle: Math.PI + g_ball.getAngle()}; 109 | } 110 | } 111 | // It's a miss! 112 | return {miss: true}; 113 | }; 114 | 115 | return res; 116 | } -------------------------------------------------------------------------------- /examples/brkout/scripts/render.js: -------------------------------------------------------------------------------- 1 | // GENERIC RENDERING 2 | 3 | var g_doClear = true; 4 | var g_doBox = false; 5 | var g_undoBox = false; 6 | var g_doFlipFlop = false; 7 | var g_doRender = true; 8 | 9 | var g_blindMode = false; 10 | 11 | var g_frameCounter = 1; 12 | 13 | var TOGGLE_CLEAR = 'C'.charCodeAt(0); 14 | var TOGGLE_BLIND_MODE = 'I'.charCodeAt(0); 15 | 16 | function renderVictoryMessage(ctx) 17 | { 18 | ctx.save(); 19 | ctx.fillStyle = '#FFF'; 20 | ctx.textAlign = 'center'; 21 | ctx.font = '40px Arial'; 22 | ctx.fillText("Huzzah!", g_canvas.width/2, g_canvas.height/2); 23 | ctx.restore(); 24 | } 25 | 26 | 27 | function render(ctx) { 28 | 29 | // Process various option toggles 30 | // 31 | if (g_keys.eatKey(TOGGLE_CLEAR)) g_doClear = !g_doClear; 32 | 33 | if (g_keys.eatKey(TOGGLE_BLIND_MODE)) g_blindMode = !g_blindMode; 34 | 35 | if (g_doClear) 36 | fillBox(ctx, 0, 0, g_canvas.width, g_canvas.height, 37 | 'rgb(' + bg.r + ',' + bg.g + ',' + bg.b + ')'); 38 | 39 | 40 | if (g_level.hasWon()) 41 | { 42 | renderVictoryMessage(ctx); 43 | } 44 | 45 | if (g_doRender) renderSimulation(ctx); 46 | 47 | ++g_frameCounter; 48 | } -------------------------------------------------------------------------------- /examples/brkout/scripts/update.js: -------------------------------------------------------------------------------- 1 | // GENERIC UPDATE LOGIC 2 | 3 | // The "nominal interval" is the one that all of our time-based units are 4 | // calibrated to e.g. a velocity unit is "pixels per nominal interval" 5 | var NOMINAL_UPDATE_INTERVAL = 16.666; 6 | 7 | // Dt is in units of the timer-system (i.e. milliseconds) 8 | var g_prevUpdateDt = null; 9 | 10 | // Du, u represents time in multiples of our nominal interval 11 | var g_prevUpdateDu = null; 12 | 13 | var g_isUpdateOdd = false; 14 | 15 | 16 | // bg controls the background color 17 | var bg = { 18 | r: 80, 19 | g: 80, 20 | b: 80, 21 | easeColour: function(du) 22 | { 23 | this.r = Math.floor(this.r + (80 - this.r) / 15 * du); 24 | this.g = Math.floor(this.g + (80 - this.g) / 15 * du); 25 | this.b = Math.floor(this.b + (80 - this.b) / 15 * du); 26 | }, 27 | flash: function(r, g, b) 28 | { 29 | r = Number(r) || 255; 30 | g = Number(g) || 255; 31 | b = Number(b) || 255; 32 | this.r = Math.max(r, 80); 33 | this.g = Math.max(g, 80); 34 | this.b = Math.max(b, 80); 35 | } 36 | }; 37 | 38 | function update(dt) { 39 | 40 | // Get out if skipping (e.g. due to pause-mode) 41 | if (shouldSkipUpdate()) return; 42 | 43 | // Remember this for later 44 | var original_dt = dt; 45 | 46 | // Warn about very large dt values -- they may lead to error 47 | if (dt > 200) { 48 | console.log("Big dt =", dt, ": CLAMPING TO NOMINAL"); 49 | dt = NOMINAL_UPDATE_INTERVAL; 50 | } 51 | 52 | // If using variable time, divide the actual delta by the "nominal" rate, 53 | // giving us a conveniently scaled "du" to work with. 54 | var du = (dt / NOMINAL_UPDATE_INTERVAL); 55 | bg.easeColour(du); 56 | updateSimulation(du); 57 | 58 | g_prevUpdateDt = original_dt; 59 | g_prevUpdateDu = du; 60 | 61 | g_isUpdateOdd = !g_isUpdateOdd; 62 | } 63 | 64 | // Togglable Pause Mode 65 | // 66 | var KEY_PAUSE = 'P'.charCodeAt(0); 67 | var KEY_STEP = 'O'.charCodeAt(0); 68 | 69 | var g_isUpdatePaused = false; 70 | 71 | function shouldSkipUpdate() { 72 | if (g_keys.eatKey(KEY_PAUSE)) { 73 | g_isUpdatePaused = !g_isUpdatePaused; 74 | } 75 | return g_isUpdatePaused && !g_keys.eatKey(KEY_STEP); 76 | } -------------------------------------------------------------------------------- /examples/brkout/scripts/utils.js: -------------------------------------------------------------------------------- 1 | // ===== 2 | // UTILS 3 | // ===== 4 | 5 | function clearCanvas(ctx) { 6 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 7 | } 8 | 9 | function fillCircle(ctx, x, y, r, style) { 10 | ctx.save(); 11 | ctx.beginPath(); 12 | ctx.fillStyle = style; 13 | ctx.arc(x, y, r, 0, Math.PI * 2); 14 | ctx.fill(); 15 | ctx.restore(); 16 | } 17 | 18 | function fillBox(ctx, x, y, w, h, style) { 19 | ctx.save(); 20 | ctx.fillStyle = style; 21 | ctx.fillRect(x, y, w, h); 22 | ctx.restore(); 23 | } -------------------------------------------------------------------------------- /shadow.js: -------------------------------------------------------------------------------- 1 | // ========= 2 | // shadow.js 3 | // ========= 4 | // Author: Kristján Eldjárn, kristjan@eldjarn.net 5 | 6 | 7 | var Shadow = (function() { 8 | // ======= 9 | // PRIVATE 10 | // ======= 11 | var _defaultScale = window.innerWidth; 12 | 13 | // Vector operations 14 | var _vec2 = function(a, b) { 15 | return { 16 | x: b.x - a.x, 17 | y: b.y - a.y 18 | }; 19 | }; 20 | 21 | var _normal = function(a, b) { 22 | return { 23 | x: b.y - a.y, 24 | y: -(b.x - a.x) 25 | }; 26 | }; 27 | 28 | var _dot = function(a, b) { 29 | return a.x * b.x + a.y * b.y; 30 | }; 31 | 32 | 33 | // ====== 34 | // PUBLIC 35 | // ====== 36 | var castFromRectangle = function(ctx, origin, x, y, w, h, scale) { 37 | // Casts a shadow of a rectangular object from a point light source 38 | // located at origin 39 | 40 | // Create an array that traverses the segments of the rectangle 41 | // in a counter-clockwise order. 42 | var points = [ 43 | {x: x, y: y}, 44 | {x: x, y: y + h}, 45 | {x: x + w, y: y + h}, 46 | {x: x + w, y: y} 47 | ]; 48 | cast(ctx, origin, points, scale); 49 | }; 50 | 51 | 52 | var cast = function(ctx, origin, points, scale) { 53 | // Casts a shadow of the (convex) form described by the points array 54 | // w.r.t. a point light source located at origin 55 | 56 | // ctx 57 | // === 58 | // The HTML5 canvas context onto which the shadow is to be cast. 59 | 60 | // origin 61 | // ====== 62 | // An {x, y}-object containing the coordinates of the light source. 63 | 64 | // points 65 | // ====== 66 | // The array of points that make up the (convex!) form that casts the 67 | // shadow. The points should be in counter-clockwise order, or else the 68 | // shadows will be inversed (i.e. edges that are visible to the point 69 | // light source will cast a shadow while those invisible to the light 70 | // source will not). 71 | 72 | // scale 73 | // ===== 74 | // Scales the length of the cast shadow. 75 | // Defaults to the width of the browser window real estate. This is 76 | // probably too large for most applications, and it might increase 77 | // performance to use a smaller scalar. If you have a square-ish canvas 78 | // and want the shadow to definitely exceed the canvas (i.e. not see the 79 | // end of the shadow) I recommend using scale = canvas.width * 1.5 80 | scale = scale || _defaultScale; 81 | 82 | // Check if edge is invisible from the perspective of origin 83 | var a = points[points.length - 1]; 84 | for (var i = 0; i < points.length; ++i, a = b) 85 | { 86 | var b = points[i]; 87 | 88 | var originToA = _vec2(origin, a); 89 | var normalAtoB = _normal(a, b); 90 | var normalDotOriginToA = _dot(normalAtoB, originToA); 91 | 92 | // If the edge is invisible from the perspective of origin it casts 93 | // a shadow. 94 | if (normalDotOriginToA < 0) 95 | { 96 | // dot(a, b) == cos(phi) * |a| * |b| 97 | // thus, dot(a, b) < 0 => cos(phi) < 0 => 90° < phi < 270° 98 | 99 | var originToB = _vec2(origin, b); 100 | 101 | // We draw the form of the shade so that it definitely exceeds 102 | // the canvas. This is probably cheaper than projecting the 103 | // points onto the edges of the canvas. 104 | ctx.beginPath() 105 | ctx.moveTo(a.x, a.y); 106 | ctx.lineTo(a.x + scale * originToA.x, 107 | a.y + scale * originToA.y); 108 | ctx.lineTo(b.x + scale * originToB.x, 109 | b.y + scale * originToB.y); 110 | ctx.lineTo(b.x, b.y); 111 | ctx.closePath(); 112 | // ==== 113 | // TODO 114 | // ==== 115 | // Create an option to have the fillStyle be a gradient, i.e. 116 | // letting the shadow fade to transparency. 117 | ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; 118 | ctx.fill(); 119 | } 120 | } 121 | }; 122 | 123 | // If (for some reason) your points array traverses the segments of the 124 | // form in clockwise order, use castInverse. 125 | var castInverse = function(ctx, origin, points, scale) { 126 | // Copy points and reverse in place 127 | var pointsReversed = points.slice(0).reverse(); 128 | cast(ctx, origin, pointsReversed, scale); 129 | }; 130 | 131 | return { 132 | castFromRectangle : castFromRectangle, 133 | cast : cast, 134 | castInverse : castInverse 135 | }; 136 | })(); --------------------------------------------------------------------------------