-
-
Save dartlab-user/8be086652cebd7802498 to your computer and use it in GitHub Desktop.
Conway's Game of Life
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
{ | |
"origin": "dartlab.org", | |
"url": "http://dartlab.org/#:gistId", | |
"history": [] | |
} |
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
<h1 id="title">Conway's Game of Life</h1> | |
<p id="infoBar"> | |
Generation: <span id="generationValue"></span> | |
# Cells: <span id="cellsValue"></span> | |
<input id="clearButton" type="button" value="Clear" title="Kill everyone!" /> | |
<input id="randomButton" type="button" value="Random" title="Hold random button to create live cells" /> | |
</p> | |
<div id="toolbar"> | |
<p id="speedPanel"> | |
<label for="speed">Speed: </label> | |
<input id="speed" type="range" name="Speed" min="-1" max="10" step="1" value="2"> | |
<span id="speedValue">1x</span> | |
</p> | |
<p id="patternPanel"> | |
<!-- TODO: pattern --> | |
</p> | |
</div> | |
<div id="canvasPanel"> | |
<canvas id="canvas" width="256" height="256"></canvas> | |
</div> |
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
import 'dart:html'; | |
import 'dart:async'; | |
import 'dart:math'; | |
int lifeTime = 1000; | |
DivElement canvasPanel = querySelector("#canvasPanel"); | |
CanvasElement canvas = querySelector("#canvas"); | |
GridPresenter gridPresenter = new GridPresenter(new Engine(), new Grid(canvas)); | |
InputElement clearButton = querySelector("#clearButton"); | |
InputElement randomButton = querySelector("#randomButton"); | |
InputElement speed = querySelector("#speed"); | |
SpanElement speedValue = querySelector("#speedValue"); | |
SpanElement generationValue = querySelector("#generationValue"); | |
SpanElement cellsValue = querySelector("#cellsValue"); | |
Timer generationTimer = new Timer(Duration.ZERO, () {}); | |
void main() { | |
window.onLoad.listen(_autoResizeCanvas); | |
window.onResize.listen(_autoResizeCanvas); | |
window.onMouseWheel.listen(_zoom); | |
speed.onChange.listen(onSpeedChange); | |
clearButton.onClick.listen(_clear); | |
Timer _randomTimer = new Timer(Duration.ZERO, () {}); | |
randomButton.onMouseDown.listen((e) => _randomTimer = _randomStart()); | |
randomButton.onMouseUp.listen((e) => _randomTimer.cancel()); | |
randomButton.onMouseOut.listen((e) => _randomTimer.cancel()); | |
animate(); | |
} | |
animate() => window.requestAnimationFrame(_animate); | |
_animate(num time) { | |
generationTimer.cancel(); | |
if(lifeTime > 0) { | |
Duration duration = new Duration(milliseconds: max(0, lifeTime - (time.toInt() % lifeTime))); | |
generationTimer = new Timer(duration, () { | |
nextGeneration(); | |
animate(); | |
}); | |
} | |
} | |
nextGeneration() { | |
gridPresenter.nextGeneration(); | |
drawGeneration(); | |
} | |
drawGeneration() { | |
gridPresenter.drawGeneration(); | |
refreshInfo(); | |
} | |
refreshInfo() { | |
generationValue.text = gridPresenter.generationCount.toString(); | |
cellsValue.text = gridPresenter.generation.aliveCells.length.toString(); | |
} | |
onSpeedChange(e) { | |
int value = speed.valueAsNumber.toInt(); | |
num x = 1; | |
if(value < 0) { | |
x = -1; | |
speedValue.text = "Stop"; | |
} else if(value >= 10) { | |
x = 1000; | |
speedValue.text = "Max"; | |
} else { | |
x = 0.25 * pow(2, value); | |
speedValue.text = "${x}x"; | |
} | |
lifeTime = 1000 ~/ x; | |
animate(); | |
} | |
_clear(e) { | |
gridPresenter.clear(); | |
refreshInfo(); | |
} | |
_autoResizeCanvas(e) { | |
canvas.style.display = "none"; | |
canvas..width = canvasPanel.offsetWidth - 30 ..height = canvasPanel.offsetHeight - 30; | |
canvas.style.display = "block"; | |
gridPresenter.refresh(); | |
} | |
_zoom(WheelEvent e) { | |
int delta = (e.detail != 0 ? e.detail / 3 : e.deltaY / 120).toInt(); | |
gridPresenter.zoom(delta); | |
} | |
Timer _randomStart() => new Timer.periodic(const Duration(milliseconds: 100), (t) { | |
Random rand = new Random(new DateTime.now().millisecondsSinceEpoch); | |
int cellWidth = gridPresenter.grid.cellWidth; | |
int cellHeight = gridPresenter.grid.cellHeight; | |
num number = cellWidth * cellHeight * 0.01; | |
for(int i = 0; i < number; i++) { | |
Cell cell = new Cell(rand.nextInt(cellWidth), rand.nextInt(cellHeight)); | |
gridPresenter[cell] = true; | |
refreshInfo(); | |
} | |
}); | |
class Cell { | |
static const int _maxCache = 2048; | |
static Map<int, Map<int, Cell>> _cache = new Map(); | |
final int x, y; | |
factory Cell(int x, int y) { | |
if(x.abs() > _maxCache || y.abs() > _maxCache) { | |
return new Cell._internal(x, y); | |
} else if(!_cache.containsKey(x)) { | |
_cache[x] = new Map(); | |
return _cache[x][y] = new Cell._internal(x, y); | |
} else if(!_cache[x].containsKey(y)) { | |
return _cache[x][y] = new Cell._internal(x, y); | |
} else { | |
return _cache[x][y]; | |
} | |
} | |
const Cell._internal(this.x, this.y); | |
int get hashCode => x ^ y; | |
bool operator ==(other) => x == other.x && y == other.y; | |
String toString() => "[$x, $y]"; | |
List<Cell> neighbours() => [ | |
new Cell(x-1, y-1), new Cell(x, y-1), new Cell(x+1, y-1), | |
new Cell(x-1, y ), new Cell(x+1, y ), | |
new Cell(x-1, y+1), new Cell(x, y+1), new Cell(x+1, y+1), | |
]; | |
} | |
class Generation { | |
Set<Cell> _aliveCells = new Set(); | |
Set<Cell> _deadCells = new Set(); | |
Generation(); | |
Generation.from(Iterable<Cell> aliveCell) : this._aliveCells = new Set.from(aliveCell); | |
int aliveNeighbours(Cell cell) { | |
return cell.neighbours().where(_aliveCells.contains).length; | |
} | |
Set<Cell> aliveCellsAndNeighbours() { | |
Set cells = new Set(); | |
_aliveCells.forEach((cell) { | |
cells.add(cell); | |
cells.addAll(cell.neighbours()); | |
}); | |
return cells; | |
} | |
bool operator [](Cell cell) => _aliveCells.contains(cell); | |
void operator []=(Cell cell, bool value) { | |
if(value) { | |
_aliveCells.add(cell); | |
_deadCells.remove(cell); | |
} else { | |
_aliveCells.remove(cell); | |
_deadCells.add(cell); | |
} | |
} | |
Iterable<Cell> get aliveCells => _aliveCells; | |
Iterable<Cell> get deadCells => _deadCells; | |
String toString() => "Generation{aliveCell: $_aliveCells, deadCells: $_deadCells}"; | |
} | |
class Engine { | |
Cell min; | |
Cell max; | |
Engine({this.min, this.max}); | |
Generation nextGeneration(Generation current) { | |
Generation next = new Generation(); | |
current.aliveCellsAndNeighbours().forEach((cell) => processCell(current, next, cell)); | |
return next; | |
} | |
processCell(Generation current, Generation next, Cell cell) { | |
int aliveNeighbours = current.aliveNeighbours(cell); | |
next[cell] = aliveNeighbours == 3 || (aliveNeighbours == 2 && current[cell]); | |
} | |
} | |
typedef CellClick(Cell currentCell, {Cell startCell}); | |
class Grid { | |
CanvasElement _canvas; | |
CanvasRenderingContext2D _context; | |
num cellSize = 10; | |
num lineWidth = 1; | |
String backgroundColor = "white"; | |
String lineColor = "white"; | |
num _scale = 1; | |
Cell _startMouseDown; | |
Cell _currentMouseMove; | |
CellClick cellClick; | |
Grid(CanvasElement canvas) { | |
_canvas = canvas; | |
_canvas.onMouseDown.listen(_onMouseDown); | |
_canvas.onMouseUp.listen(_onMouseUp); | |
_canvas.onMouseMove.listen(_onMouseMove); | |
_context = canvas.getContext("2d"); | |
clear(); | |
} | |
void operator []=(Cell cell, bool value) => value ? drawCell(cell, "red") : clearCell(cell); | |
drawCell(Cell cell, [String color = "red"]) { | |
num lineWidth = this.lineWidth * scale; | |
_context..beginPath() | |
..rect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize) | |
..fillStyle = color | |
..fill() | |
..lineWidth = lineWidth | |
..strokeStyle = lineColor | |
..stroke() | |
..closePath(); | |
} | |
clear() { | |
_context..beginPath() | |
..rect(0, 0, width, height) | |
..fillStyle = backgroundColor | |
..closePath() | |
..fill(); | |
drawGrid(); | |
} | |
clearCell(Cell cell) => drawCell(cell, "white"); | |
drawGrid() { | |
if(lineColor != backgroundColor) { | |
for(num x = 0; x < width; x+= cellSize) { | |
drawLine(x, 0, x, height); | |
} | |
for(num y = 0; y < height; y+= cellSize) { | |
drawLine(0, y, width, y); | |
} | |
} | |
} | |
drawLine(num x1, num y1, num x2, num y2) { | |
_context..beginPath() | |
..strokeStyle = lineColor | |
..moveTo(x1, y1) | |
..lineTo(x2, y2) | |
..lineWidth = lineWidth * scale ..stroke(); | |
} | |
drawGeneration(Generation generation) { | |
generation.deadCells.forEach((cell) => clearCell(cell)); | |
generation.aliveCells.forEach((cell) => drawCell(cell, "red")); | |
} | |
_onMouseDown(MouseEvent event) =>_startMouseDown = _mouseEventToCell(event); | |
_onMouseUp(MouseEvent event) { | |
_startMouseDown = null; | |
_dispatchCellClick(_mouseEventToCell(event)); | |
_currentMouseMove = null; | |
} | |
_onMouseMove(MouseEvent event) { | |
// FIX: vérifier que la souris est bien DOWN | |
if(_startMouseDown != null) { | |
var cell = _mouseEventToCell(event); | |
if(_currentMouseMove != cell) { | |
_dispatchCellClick(cell); | |
} | |
} | |
} | |
_dispatchCellClick(Cell cell) { | |
if(cellClick != null && _currentMouseMove != cell) { | |
_currentMouseMove = cell; | |
if(_currentMouseMove == _startMouseDown) { | |
cellClick(_currentMouseMove); | |
} else { | |
cellClick(_currentMouseMove, startCell: _startMouseDown); | |
} | |
} | |
} | |
_mouseEventToCell(MouseEvent event) { | |
int x = event.offset.x ~/ (cellSize * scale); | |
int y = event.offset.y ~/ (cellSize * scale); | |
return new Cell(x, y); | |
} | |
num get width => _canvas.width / scale; | |
num get height => _canvas.height / scale; | |
int get cellWidth => (width / cellSize).ceil().toInt(); | |
int get cellHeight => (height / cellSize).ceil().toInt(); | |
num get scale => _scale; | |
set scale(num scale) { | |
print("Scale: $scale"); | |
_context.scale(scale / _scale, scale / _scale); | |
_scale = scale; | |
} | |
} | |
class GridPresenter { | |
Engine engine; | |
Grid grid; | |
Generation generation = new Generation(); | |
int generationCount = 0; | |
bool _clickState = true; | |
GridPresenter(this.engine, this.grid) { | |
grid.cellClick = _gridCellClick; | |
} | |
nextGeneration() { | |
generation = engine.nextGeneration(generation); | |
if(generation.aliveCells.length > 0) { | |
generationCount++; | |
} | |
} | |
drawGeneration() { | |
grid.drawGeneration(generation); | |
} | |
refresh() { | |
grid.clear(); | |
grid.drawGeneration(generation); | |
} | |
clear() { | |
generation = new Generation(); | |
generationCount = 0; | |
grid.clear(); | |
} | |
_gridCellClick(Cell current, {Cell startCell}) { | |
if(startCell == null) { | |
_clickState = generation[current] = !generation[current]; | |
} else { | |
generation[current] = _clickState; | |
} | |
grid[current] = generation[current]; | |
} | |
bool operator [](Cell cell) => generation[cell]; | |
void operator []=(Cell cell, bool value) { | |
generation[cell] = true; | |
if(value) { | |
grid.drawCell(cell); | |
} else { | |
grid.clearCell(cell); | |
} | |
} | |
zoom(int delta) { | |
grid.scale = max(1 / 8, min(grid.scale * (delta < 0 ? -0.5 : 2) * delta, 8)); | |
grid.clear(); | |
grid.drawGeneration(generation); | |
} | |
} |
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
html, body {height: 100%;} | |
body { | |
display: table; | |
position: relative; | |
width: 100%; | |
background-color: #F8F8F8; | |
font-family: 'Open Sans', sans-serif; | |
font-size: 14px; | |
font-weight: normal; | |
line-height: 1.2em; | |
margin: 15px; | |
overflow: hidden; | |
-webkit-user-select: none; | |
user-select: none; | |
} | |
canvas { | |
cursor:default; | |
} | |
p { | |
color: #333; | |
} | |
#title, #infoBar, #canvasPanel { | |
display: table-row; | |
position: relative; | |
} | |
#toolbar { | |
display: table; | |
position: relative; | |
width: 100%; | |
table-layout: fixed; | |
} | |
#title, #infoBar, #toolbar { | |
height: 30px; | |
white-space: nowrap; | |
} | |
#canvasPanel { | |
height: 100%; | |
} | |
#speedPanel, #patternPanel { | |
display: table-cell; | |
} | |
#speedPanel #speed { | |
vertical-align: middle; | |
width: 75%; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment