├── 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 |
28 |
29 |
Controls
30 |
31 |
Left Arrow: left
32 |
Right Arrow: right
33 |
Space Bar: release ball
34 |
N: toggle 'floor is lava' (i.e. death upon hitting floor)
35 |
I: toggle 'blind mode'
36 |
P: toggle pause
37 |
O: step to next tick when paused
38 |
39 |
A list of Cool Things®
40 |
Not necessarily exhaustive, nor in order of coolness
41 |
42 |
Three states: idle, playing, victory (Game says 'Huzzah!' upon winning)
43 |
Side collision on both paddle and bricks
44 |
Paddle moves with inertia
45 |
Paddle bounces off walls
46 |
When ball bounces off paddle, it pushes the paddle in the opposite direction
47 |
Ball is a light source and other entities cast shadows. This also affects gameplay,
48 | since the paddle is partially obscured when the ball is at the top and the bricks
49 | inbetween them.
50 |
Light around ball pulsates very slightly
51 |
Ball leaves a particle trail
52 |
Screen flashes when the ball hits bricks or the floor
53 |
'Blind Mode' where you can neither see the bricks nor the paddle, only their shadows. (Toggle with "I")
54 |
Bricks come apart upon impact and scatter particles. (Yes, I know the fragments are circular)
55 |
Level editor
56 |
65 |
66 |
67 |
Bricks can have up to 6 lives and can be indestructible (use -1 for indestructible bricks)
68 |
Code adheres to the Revealing Module Pattern; objects have private and public members, implemented via closures
69 |
70 |
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 | })();
--------------------------------------------------------------------------------