Last active
December 24, 2015 02:19
-
-
Save ne-sachirou/6730101 to your computer and use it in GitHub Desktop.
Life game, sync and async. See http://c4se.hatenablog.com/entry/2013/09/28/004018
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<!-- lisence: Public Domain --> | |
<meta charset="UTF-8"> | |
<title>ASync Life game</title> | |
<style> | |
.container_20130928_1 { | |
display: -ms-flexbox; | |
display: flex; | |
-ms-flex-direction: row; | |
flex-direction: row; | |
-ms-flex-wrap: wrap; | |
flex-wrap: wrap; | |
-ms-flex-pack: center; | |
justify-content: center; | |
} | |
.container_20130928_1 > div { | |
box-shadow: 0 0 1px black; | |
margin: 0.1em; | |
padding: 0.5em; | |
} | |
.container_20130928_1 .canvas { | |
height: 400px; | |
width: 400px; | |
} | |
.container_20130928_1 .game { | |
display: block; | |
position: relative; | |
-webkit-transform-origin: 0 0; | |
transform-origin: 0 0; | |
-webkit-transform: scale(2, 2); | |
transform: scale(2, 2); | |
} | |
</style> | |
<div class="container_20130928_1"> | |
<button id="create_20130928_1">Create life games</button> | |
<p>It may be too heavy!</p> | |
</div> | |
<template id="game_template_20130928_1"> | |
<div id="" class="holder"> | |
<div> | |
<input type="number" class="ratio" value="1"/>% | |
<button class="spawn">spawn</button> | |
<button class="kill">kill</button> | |
</div> | |
<div class="canvas"> | |
<canvas class="game" width="200" height="200"></canvas> | |
</div> | |
</div> | |
</template> | |
<script src="lifegame.js"></script> | |
<script> | |
(function(global) { | |
var view_sync, view_async; | |
if (!('content' in document.createElement('template'))) { | |
(function() { | |
var style = document.createElement('style'); | |
style.textContent = 'template { display: none; }'; | |
document.body.appendChild(style); | |
}()); | |
Object.defineProperty(HTMLUnknownElement.prototype, 'content', { | |
/** | |
* @return {DocumentFragment|undefined} | |
*/ | |
get: function() { | |
var fragment; | |
if (this.tagName !== 'TEMPLATE') | |
return; | |
fragment = document.createDocumentFragment(); | |
Array.prototype.slice.call(this.childNodes).forEach(function(node) { | |
fragment.appendChild(node.cloneNode(true)); | |
}); | |
return fragment; | |
} | |
}); | |
} | |
window.addEventListener('DOMContentLoaded', function(evt) { | |
document.getElementById('create_20130928_1').addEventListener('click', function(evt) { | |
document.getElementsByClassName('container_20130928_1')[0].innerHTML = ''; | |
createCanvas('game_sync_20130928_1'); | |
view_sync = runGame('game_sync_20130928_1', false, { | |
color: 'rgb(255, 100, 200)' | |
}); | |
createCanvas('game_async_20130928_1'); | |
view_async = runGame('game_async_20130928_1', true, { | |
color: 'rgb(100, 200, 255)' | |
}); | |
}); | |
}); | |
/** | |
* @param {string} holder_id | |
*/ | |
function createCanvas(holder_id) { | |
var holder = document.getElementById('game_template_20130928_1').content.cloneNode(true); | |
holder.querySelector('.holder').id = holder_id; | |
document.getElementsByClassName('container_20130928_1')[0].appendChild(holder); | |
} | |
/** | |
* @param {string} holder_id | |
* @param {boolean} is_async | |
* @param {Object.<string,Object>} options | |
* @return {ViewLifegame} | |
*/ | |
function runGame(holder_id, is_async, options) { | |
var view = new ViewLifegame_20130928_1, | |
holder = document.getElementById(holder_id); | |
function getRatio() { | |
return parseFloat(holder.getElementsByClassName('ratio')[0].value) * 0.01; | |
} | |
function spawn() { view.cells.spawn(getRatio()); } | |
function kill() { view.cells.kill(getRatio()); } | |
holder.getElementsByClassName('spawn')[0].addEventListener('click', spawn); | |
holder.getElementsByClassName('kill')[0].addEventListener('click', kill); | |
view.init(holder.getElementsByClassName('game')[0], options); | |
view.cells.spawn(0.1); | |
view.run(is_async); | |
return view; | |
} | |
global.views_20130928_1 = [view_sync, view_async]; | |
}(this.self || global)); | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @license Public Domain | |
*/ | |
(function(global) { | |
'use strict'; | |
if (! global.requestAnimationFrame) { | |
if (global.mozRequestAnimationFrame) | |
global.requestAnimationFrame = global.mozRequestAnimationFrame; | |
} | |
// {{{ ViewLifegame | |
/** | |
* @constructor | |
*/ | |
function ViewLifegame() { | |
/** @type {HTMLCanvasElement} */ | |
this.canvas = null; | |
/** @params {Object.<string,Object>} */ | |
this.options = {}; | |
/** @type {CanvasRenderingContext2D} */ | |
this.context = null; | |
/** @type {Cells} */ | |
this.cells = null; | |
} | |
ViewLifegame.prototype = { | |
/** | |
* @param {HTMLCanvasElement} canvas | |
* @params {Object.<string,Object>} options | |
* { color: color of cells } | |
* @return {ViewLifegame} | |
*/ | |
init: function(canvas, options) { | |
this.canvas = canvas; | |
this.options = options; | |
this.context = canvas.getContext('2d'); | |
this.cells = new Cells(canvas.width, canvas.height); | |
return this; | |
}, | |
/** | |
* @param {boolean=} is_async false is default. | |
* @return {ViewLifegame} | |
*/ | |
run: function(is_async) { | |
var animation = new Animation; | |
animation.add(function() { | |
this.draw(); | |
if (! is_async) | |
this.cells.doNext(); | |
}, this); | |
this.draw(); | |
if (is_async) | |
this.cells.forEach(function(cell) { cell.run(); }); | |
animation.start(); | |
return this; | |
}, | |
/** | |
* @private | |
* @return {ViewLifegame} | |
*/ | |
draw: function() { | |
this.context.fillStyle = 'black'; | |
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
this.context.fillStyle = this.options.color; | |
this.cells.forEach(function(cell, x, y) { | |
if (cell.is_alive) | |
this.context.fillRect(x, y, 1, 1); | |
}, this); | |
return this; | |
} | |
}; | |
// }}} | |
// {{{ Cells | |
/** | |
* @param {number} width | |
* @param {number} height | |
*/ | |
function Cells(width, height) { | |
var x = 0, y = 0; | |
/** @type {number} */ | |
this.width = width; | |
/** @type {number} */ | |
this.height = height; | |
/** @type {Array.<Array.<number>>} */ | |
this.field = [ ]; | |
for (x = 0; x < width; ++x) { | |
this.field[x] = [ ]; | |
for (y = 0; y < height; ++y) | |
this.field[x][y] = new Cell(this, x, y, false); | |
} | |
} | |
Cells.prototype = { | |
/** | |
* @param {number} ratio | |
* @return {Cells} | |
*/ | |
spawn: function(ratio) { | |
var i = 0, | |
number = Math.floor(this.width * this.height * ratio); | |
for (i = 0; i < number; ++i) | |
this.at(random(0, this.width), random(0, this.height)).is_alive = true; | |
return this; | |
}, | |
/** | |
* @param {number} ratio | |
* @return {Cells} | |
*/ | |
kill: function(ratio) { | |
var i = 0, | |
number = Math.floor(this.width * this.height * ratio); | |
for (i = 0; i < number; ++i) | |
this.at(random(0, this.width), random(0, this.height)).is_alive = false; | |
return this; | |
}, | |
/** | |
* @return {Cells} | |
*/ | |
doNext: function() { | |
var requests = []; | |
this.forEach(function(cell, x, y) { | |
var next = cell.next(); | |
if (cell.is_alive !== next) | |
requests.push([x, y, next]); | |
}, this); | |
requests.forEach(function(request) { | |
this.at(request[0], request[1]).is_alive = request[2]; | |
}, this); | |
return this; | |
}, | |
/** | |
* @param {function(this:Object,Cell,x:number,y:number,Cells):(boolean|undefined)} fn | |
* @param {Object=} obj | |
* @return {Cells} | |
*/ | |
forEach: function(fn, obj) { | |
var x = 0, y = 0, | |
width = this.width, height = this.height, | |
result; | |
if (obj === void 0) | |
obj = this; | |
loop: for (x = 0; x < width; ++x) { | |
for (y = 0; y < height; ++y) { | |
result = fn.call(obj, this.field[x][y], x, y, this); | |
if (result === false) | |
break loop; | |
} | |
} | |
return this; | |
}, | |
/** | |
* @private | |
* @param {number} x | |
* @param {number} y | |
* @return {?Cell} | |
*/ | |
at: function(x, y) { | |
var column; | |
x = (x + this.width) % this.width; | |
y = (y + this.height) % this.height; | |
column = this.field[x]; | |
if (! column) | |
return null; | |
return column[y]; | |
} | |
}; | |
// }}} | |
// {{{ Cell | |
/** | |
* @param {Cells} cells | |
* @param {number} x | |
* @param {number} y | |
* @param {boolean} is_alive | |
*/ | |
function Cell(cells, x, y, is_alive) { | |
this.cells = cells; | |
this.x = x; | |
this.y = y; | |
this.is_alive = is_alive; | |
} | |
Cell.prototype = { | |
/** | |
* @return {Cell} | |
*/ | |
run: function() { | |
var animation = new Animation; | |
this.doNext(); | |
animation.add(function() { | |
if (random(0, 10) === 0) | |
this.doNext(); | |
}, this); | |
animation.start(); | |
// setTimeout(this.run.bind(this), random(33, 333)); | |
return this; | |
}, | |
/** | |
* @return {Cell} | |
*/ | |
doNext: function() { | |
this.is_alive = this.next(); | |
return this; | |
}, | |
/** | |
* @return {boolean} | |
*/ | |
next: function() { | |
var adjacent_count = this.adjacent_count(); | |
if (this.is_alive) { | |
switch (adjacent_count) { | |
case 0: case 1: | |
case 4: case 5: case 6: case 7: case 8: | |
return false; | |
case 2: case 3: | |
return true; | |
default: | |
throw new Error('Adjacent count is ' + adjacent_count + '.'); | |
} | |
} | |
if (adjacent_count === 3) | |
return true; | |
return false; | |
}, | |
/** | |
* @return {number} | |
*/ | |
adjacent_count: function() { | |
return this.adjacent().reduce(function(sum, cell) { | |
if (cell.is_alive) | |
sum += 1; | |
return sum; | |
}, 0); | |
}, | |
/** | |
* @return {Array.<Cell>} | |
* [ 0, 1, 2, | |
* 3, v, 4, | |
* 5, 6, 7 ] | |
*/ | |
adjacent: function() { | |
var cells = this.cells, x = this.x, y = this.y; | |
return [ | |
cells.at(x - 1, y - 1), cells.at(x, y - 1), cells.at(x + 1, y - 1), | |
cells.at(x - 1, y), cells.at(x + 1, y), | |
cells.at(x - 1, y + 1), cells.at(x, y + 1), cells.at(x + 1, y + 1) ]; | |
} | |
}; | |
// }}} | |
// {{{ Animation | |
function Animation() { | |
var instance; | |
instance = this; | |
Animation = function() { return instance; }; | |
/** @type {boolean} */ | |
this.is_started = false; | |
/** @type {Array.<{fn:function(),obj:Object}>} */ | |
this.fns = []; | |
} | |
Animation.prototype = { | |
/** | |
* @return {Animation} | |
*/ | |
start: function() { | |
function doNext() { | |
this.fns.forEach(function(fn) { | |
fn.fn.call(fn.obj); | |
}); | |
requestAnimationFrame(doNext.bind(this)); | |
} | |
if (this.is_started) | |
return; | |
this.is_started = true; | |
requestAnimationFrame(doNext.bind(this)); | |
return this; | |
}, | |
/** | |
* @param {function(this:Object)} fn | |
* @param {Object} obj | |
* @return {Animation} | |
*/ | |
add: function(fn, obj) { | |
this.fns.push({fn: fn, obj: obj}); | |
return this; | |
} | |
}; | |
// }}} | |
/** | |
* @param {number} min | |
* @param {number} max | |
* @return {number} | |
*/ | |
function random(min, max) { | |
return Math.floor(Math.random() * (max - min)) + min; | |
} | |
global.ViewLifegame_20130928_1 = ViewLifegame; | |
}(this.self || global)); | |
// vim:set foldmethod=marker: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See my blog post 非同期的 (async) life game (JavaScript) - c4se記:さっちゃんですよ☆