├── .gitignore ├── README.md └── canvaslife.js /.gitignore: -------------------------------------------------------------------------------- 1 | canvaslife.html 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | canvaslife 2 | ========== 3 | 4 | [Conway's Game of Life](http://en.wikipedia.org/wiki/Conway's_Game_of_Life) implemented in HTML5 canvas. `canvaslife.js` takes care of displaying and calculating the various states of the Game of Life universe, and allows you to interact with it through JavaScript. 5 | 6 | Demo 7 | ---- 8 | 9 | See a full demo at , including over 1200 interesting patterns. 10 | 11 | Setup 12 | ----- 13 | 14 | To start with, you need to include [jQuery](http://jquery.com/), and `canvaslife.js`, like so: 15 | 16 | 17 | … 18 | 19 | 20 | … 21 | 22 | 23 | You also need a `` element which canvaslife.js will use to display the grid of cells: 24 | 25 | 26 | … 27 | 28 | … 29 | 30 | 31 | Finally you need to initialize the Game of Life Universe and tell it which canvas element to use, like so: 32 | 33 | 38 | 39 | Usage 40 | ----- 41 | 42 | You can populate the grid by click and dragging your mouse to revive/kill cells. 43 | 44 | You can also interact with the Game of Life Univese through JavaScript, via the `life` and `graphics` objects. 45 | 46 | life 47 | ---- 48 | 49 | ### 'life' object properties 50 | 51 | `life.xCells` - Number of cells per row. 52 | 53 | `life.yCells` - Number of cells per column. 54 | 55 | `life.speed` - An integer representing the speed at which the universe is evolving. `1000` being the slowest, and `0` being the fastest. 56 | 57 | `life.universe` - A two dimensional array of booleans, representing the current alive/dead state of all cells in the universe. When making changes to this array ensure that `life.isAlive()` returns `false` so that your changes are not overriden by the next generation being calculated. 58 | 59 | 60 | ### 'life' object methods 61 | 62 | `life.initUniverse(canvasSelector)` - Initializes a Game of Life universe. `canvasSelector` should be a [jQuery selector](http://api.jquery.com/category/selectors/) that matches to the desired canvas element. 63 | 64 | `life.loadPattern(url)` - Loads a [Run Length Encoded](http://psoup.math.wisc.edu/mcell/ca_files_formats.html#RLE) format Game of Life pattern file from the specified `url` into the universe. 65 | 66 | `life.toggleLife()` - Toggle whether the universe is in an envolving or static state. 67 | 68 | `life.isAlive()` - Returns `true` if the universe is evolving, and `false` if it is static. 69 | 70 | `life.nextGen()` - Evolve the universe one generation forward. 71 | 72 | `life.clear()` - Kills all cells in the universe. 73 | 74 | `life.changeSpeed(faster)` - Increases the speed of the universe if `faster` is `true`, and decreases it if `faster` is `false`. 75 | 76 | graphics 77 | -------- 78 | 79 | ### 'graphics' object methods 80 | 81 | `graphics.paint()` - Paint the current state of the universe. 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /canvaslife.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2012 Julian Pulgarin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /*jslint plusplus:true, sloppy:true */ 18 | /*globals $, life, setInterval, clearInterval */ 19 | 20 | var Point = function (x, y) { 21 | this.x = x; 22 | this.y = y; 23 | }; 24 | 25 | var graphics = (function () { 26 | var canvas, 27 | ctx, 28 | canvasSelector, 29 | cellSize = 10, // pixels 30 | onColour = 'rgb(0, 0, 0)', 31 | offColour = 'rgb(255, 255, 255)', 32 | gridColour = 'rgb(50, 50, 50)'; 33 | 34 | function initCanvas(canvasSelector) { 35 | graphics.canvas = $(canvasSelector).get(0); 36 | graphics.ctx = graphics.canvas.getContext('2d'); 37 | graphics.canvasSelector = canvasSelector; 38 | } 39 | 40 | function drawCell(x, y, alive) { 41 | graphics.ctx.fillStyle = (alive) ? onColour : offColour; 42 | graphics.ctx.fillRect(x * cellSize + 1, y * cellSize + 1, cellSize - 1, cellSize - 1); 43 | } 44 | 45 | 46 | function handleMouse(e) { 47 | var that = this, 48 | state; 49 | 50 | function getCellPointUnderMouse(e) { 51 | return new Point(Math.floor((e.pageX - that.offsetLeft) / graphics.cellSize), Math.floor(((e.pageY - that.offsetTop) / graphics.cellSize))); 52 | } 53 | 54 | function processCell(cell) { 55 | var x = cell.x, 56 | y = cell.y; 57 | if (x > life.xCells - 1 || y > life.yCells - 1) { 58 | return; 59 | } 60 | if (typeof state === 'undefined') { 61 | state = !life.prev[x][y]; 62 | } 63 | life.prev[x][y] = state; 64 | drawCell(x, y, state); 65 | } 66 | 67 | processCell(getCellPointUnderMouse(e)); 68 | 69 | $(graphics.canvasSelector).mousemove(function (e) { 70 | processCell(getCellPointUnderMouse(e)); 71 | }); 72 | } 73 | 74 | function smartPaint() { 75 | var x, 76 | y; 77 | 78 | for (x = 0; x < life.xCells; x++) { 79 | for (y = 0; y < life.yCells; y++) { 80 | if (life.prev[x][y] !== life.next[x][y]) { 81 | graphics.drawCell(x, y, life.next[x][y]); 82 | } 83 | } 84 | } 85 | } 86 | 87 | function paint() { 88 | var x, 89 | y; 90 | 91 | for (x = 0; x < life.xCells; x++) { 92 | for (y = 0; y < life.yCells; y++) { 93 | graphics.drawCell(x, y, life.prev[x][y]); 94 | } 95 | } 96 | } 97 | 98 | return { 99 | canvas: canvas, 100 | ctx: ctx, 101 | canvasSelector: canvasSelector, 102 | cellSize: cellSize, 103 | onColour: onColour, 104 | offColour: offColour, 105 | gridColour: gridColour, 106 | initCanvas: initCanvas, 107 | drawCell: drawCell, 108 | handleMouse: handleMouse, 109 | paint: paint, 110 | smartPaint: smartPaint 111 | }; 112 | }()); 113 | 114 | var life = (function () { 115 | var yCells, 116 | xCells, 117 | prev = [], // previous generation 118 | next = [], // next generation 119 | speed = 100, 120 | timeout, 121 | alive = false, 122 | x, 123 | y; 124 | 125 | 126 | function initUniverse(canvasSelector) { 127 | var l = life, 128 | g = graphics; 129 | 130 | graphics.initCanvas(canvasSelector); 131 | life.xCells = Math.floor((graphics.canvas.width - 1) / graphics.cellSize); 132 | life.yCells = Math.floor((graphics.canvas.height - 1) / graphics.cellSize); 133 | graphics.ctx.fillStyle = graphics.offColour; 134 | graphics.ctx.fillRect(0, 0, life.xCells * graphics.cellSize, life.yCells * graphics.cellSize); 135 | graphics.ctx.fillStyle = graphics.gridColour; 136 | 137 | for (x = 0; x < life.xCells; x++) { 138 | life.prev[x] = []; 139 | life.next[x] = []; 140 | graphics.ctx.fillRect(x * graphics.cellSize, 0, 1, life.yCells * graphics.cellSize); 141 | for (y = 0; y < life.yCells; y++) { 142 | life.prev[x][y] = false; 143 | } 144 | } 145 | graphics.ctx.fillRect(life.xCells * graphics.cellSize, 0, 1, life.yCells * graphics.cellSize); 146 | for (y = 0; y < life.yCells; y++) { 147 | graphics.ctx.fillRect(0, y * graphics.cellSize, life.xCells * graphics.cellSize, 1); 148 | } 149 | graphics.ctx.fillRect(0, life.yCells * graphics.cellSize, life.xCells * graphics.cellSize, 1); 150 | $(canvasSelector).mousedown(graphics.handleMouse); 151 | $('body').mouseup(function (e) { 152 | $(graphics.canvasSelector).unbind('mousemove'); 153 | }); 154 | } 155 | 156 | function neighbourCount(x, y) { 157 | var l = life, 158 | count = 0, 159 | i, 160 | neighbours = [ 161 | life.prev[x][(y - 1 + life.yCells) % life.yCells], 162 | life.prev[(x + 1 + life.xCells) % life.xCells][(y - 1 + life.yCells) % life.yCells], 163 | life.prev[(x + 1 + life.xCells) % life.xCells][y], 164 | life.prev[(x + 1 + life.xCells) % life.xCells][(y + 1 + life.yCells) % life.yCells], 165 | life.prev[x][(y + 1 + life.yCells) % life.yCells], 166 | life.prev[(x - 1 + life.xCells) % life.xCells][(y + 1 + life.yCells) % life.yCells], 167 | life.prev[(x - 1 + life.xCells) % life.xCells][y], 168 | life.prev[(x - 1 + life.xCells) % life.xCells][(y - 1 + life.yCells) % life.yCells] 169 | ]; 170 | 171 | for (i = 0; i < neighbours.length; i++) { 172 | if (neighbours[i]) { 173 | count++; 174 | } 175 | } 176 | 177 | return count; 178 | } 179 | 180 | function nextGen() { 181 | var l = life, 182 | g = graphics, 183 | count; 184 | 185 | for (x = 0; x < life.xCells; x++) { 186 | for (y = 0; y < life.yCells; y++) { 187 | life.next[x][y] = life.prev[x][y]; 188 | } 189 | } 190 | 191 | for (x = 0; x < life.xCells; x++) { 192 | for (y = 0; y < life.yCells; y++) { 193 | count = neighbourCount(x, y); 194 | 195 | // Game of Life rules 196 | if (prev[x][y]) { 197 | if (count < 2 || count > 3) { 198 | next[x][y] = false; 199 | } 200 | } else if (count === 3) { 201 | next[x][y] = true; 202 | } 203 | } 204 | } 205 | 206 | graphics.smartPaint(); 207 | 208 | for (x = 0; x < life.xCells; x++) { 209 | for (y = 0; y < life.yCells; y++) { 210 | life.prev[x][y] = life.next[x][y]; 211 | } 212 | } 213 | } 214 | 215 | function toggleLife() { 216 | if (!alive) { 217 | alive = true; 218 | timeout = setInterval(life.nextGen, this.speed); 219 | } else { 220 | alive = false; 221 | clearInterval(timeout); 222 | } 223 | } 224 | 225 | // Parses files in Run Length Encoded Format 226 | // http://www.conwaylife.com/wiki/RLE 227 | function loadPattern(url) { 228 | var g = graphics, 229 | l = life, 230 | padding = 30; 231 | 232 | $.ajax({ 233 | url: url, 234 | success: function (data) { 235 | var match = data.match(/x\s=\s(\d*).*?y\s=\s(\d*).*\r([^]*)!/), 236 | x = parseInt(match[1], 10), 237 | pattern = match[3].replace(/\s+/g, ""), // remove whitespace 238 | lines = pattern.split('$'), 239 | offset = 0, 240 | i, 241 | line, 242 | length, 243 | j, 244 | y = padding - 1; 245 | 246 | $(graphics.canvasSelector).attr('height', graphics.cellSize * (y + 1 + (padding * 2))); 247 | $(graphics.canvasSelector).attr('width', graphics.cellSize * (x + 1 + (padding * 2))); 248 | $(graphics.canvasSelector).unbind('mousedown'); 249 | life.initUniverse(graphics.canvasSelector); 250 | 251 | 252 | for (i = 0; i < lines.length; i++) { 253 | y++; 254 | x = padding; 255 | line = lines[i]; 256 | while (line) { 257 | if (line.charAt(0) === 'o' || line.charAt(0) === 'b') { 258 | if (line.charAt(0) === 'o') { 259 | life.prev[x][y] = true; 260 | graphics.drawCell(x, y, true); 261 | } 262 | x++; 263 | line = line.substring(1); 264 | } else { 265 | length = line.match(/(\d*)/)[1]; 266 | line = line.substring(length.length); 267 | length = parseInt(length, 10); 268 | if (!line) { 269 | y += length - 1; 270 | break; 271 | } 272 | if (line.charAt(0) === 'o') { 273 | for (j = 0; j < length; j++) { 274 | life.prev[x + j][y] = true; 275 | graphics.drawCell(x + j, y, true); 276 | } 277 | } 278 | x += length; 279 | line = line.substring(1); 280 | } 281 | } 282 | } 283 | } 284 | }); 285 | } 286 | 287 | function isAlive() { 288 | return alive; 289 | } 290 | 291 | function changeSpeed(faster) { 292 | if (faster) { 293 | if (life.speed === 0) { 294 | return; 295 | } 296 | life.speed -= 10; 297 | 298 | } else { 299 | if (life.speed === 1000) { 300 | return; 301 | } 302 | life.speed += 10; 303 | } 304 | 305 | if (alive) { 306 | clearInterval(timeout); 307 | timeout = setInterval(life.nextGen, life.speed); 308 | } 309 | } 310 | 311 | function clear() { 312 | var x, 313 | y; 314 | 315 | for (x = 0; x < life.xCells; x++) { 316 | for (y = 0; y < life.yCells; y++) { 317 | life.next[x][y] = false; 318 | } 319 | } 320 | 321 | graphics.smartPaint(); 322 | 323 | for (x = 0; x < life.xCells; x++) { 324 | for (y = 0; y < life.yCells; y++) { 325 | life.prev[x][y] = false; 326 | } 327 | } 328 | } 329 | 330 | return { 331 | yCells: yCells, 332 | xCells: xCells, 333 | prev: prev, 334 | next: next, 335 | universe: prev, 336 | speed: speed, 337 | initUniverse: initUniverse, 338 | nextGen: nextGen, 339 | toggleLife: toggleLife, 340 | isAlive: isAlive, 341 | clear: clear, 342 | changeSpeed: changeSpeed, 343 | loadPattern: loadPattern 344 | }; 345 | }()); 346 | --------------------------------------------------------------------------------