-
Notifications
You must be signed in to change notification settings - Fork 0
/
automaton.js
323 lines (280 loc) · 10.2 KB
/
automaton.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
//noinspection ThisExpressionReferencesGlobalObjectJS
/**
* Created by Matthieu on 18/11/2015.
*/
this.Automaton = {};
(function automaton (automaton) {
/**
* Coming from "Javascript- The good parts"
* @param value The variable to test
* @returns {*|boolean} True if it is an array, false otherwise
*/
var is_array = function (value) {
return value && typeof value === 'object' &&
typeof value.length === 'number' &&
typeof value.splice === 'function' &&
!(value.propertyIsEnumerable('length'));
};
/**
* As describe in this article: https://en.wikipedia.org/wiki/Life-like_cellular_automaton
* Use the generic rules to create a lot of Automata
* @param {number[]} birth array containing all numbers of neighbour correct to emerge
* @param {number[]} survival array containing all numbers of neighbour needed to stay alive
* @returns {{execute: execute}} a simple object containing a matching execute function for the Automaton
*/
var lifeLike = function(birth, survival){
return {
execute: function(i, j, cell, auto){
var neigh = auto.getNeigh(i, j);
var alive = -cell.state;
neigh.forEach(function(e){
alive = alive + e.state;
});
cell.nextState = 0;
if(cell.state == 0){
if(birth.indexOf(alive) != -1){
cell.nextState = 1;
}
}else{
if(survival.indexOf(alive) != -1){
cell.nextState = 1;
}
}
},
/**
*
* @param {number} i line
* @param {number} j column
* @returns {{x: number, y: number, state: number, nextState: number}}
*/
init : function(i, j){
var s = Math.round(Math.random());
return {x: i, y: j, state: s, nextState: s};
},
/**
*
* @param x
* @param y
* @param cell
*/
update : function(x, y, cell) {
cell.state = cell.nextState;
}
}
};
automaton.lifeAutomatons = {
'conway': {name: 'Conway', birth: [3], survival: [2, 3]},
'seeds': {name: 'Seeds', birth: [2], survival: []},
'replicator': {name: 'Replicator', birth: [1, 3, 5, 7], survival: [1, 5, 7]},
'highlife': {name: 'Highlife', birth: [3, 6], survival: [2, 3]}
};
/**
* Given the key 'key' try to create the lifelike automaton associated in automaton.lifeAutomatons
* if it can't find one, create a 'conway' as default.
* @param {string} key
* @returns {Object}
*/
var getLifeAutomaton = function(key){
var auto = automaton.lifeAutomatons[key] || automaton.lifeAutomatons['conway'];
return Object.create(lifeLike(auto.birth, auto.survival));
};
/**
* Default number of cells on each axis
* @type {number}
*/
var NUMBER_CELLS = 10;
var LIFE_LIKE_KEY_WORD = 'lifelike';
/**
* Simple way to draw a square on a canvas
* @param surfaceId HTML ID of the canvas
* @returns {{}}
*/
automaton.drawableSurface = function(surfaceId){
var that = {};
var c = document.getElementById(surfaceId);
var context = c.getContext("2d");
/**
*
* @param x position on x-axis
* @param y position on y-axis
* @param width square's width
* @param height square's height
* @param color square's color
*/
that.drawSquare = function(x, y, width, height, color){
context.fillStyle = color;
context.fillRect(x, y, width, height);
};
/**
* Size of the canvas (width and height attributes in html/css)
* @returns {number[]}
*/
that.getSize = function(){
return [c.width, c.height];
};
return that;
};
/**
* Automaton. Main object that allows to execute a cellular automaton. For now, it only executes
* the well-know Conway Automaton
* @param {{surfaceId: string, freqUpdate: number, numberElements: number[], colors: string[],
* auto: string, circular: boolean}} params
* @returns {{}}
*/
automaton.automata = function(params){
var that = {};
var drawSurface = automaton.drawableSurface(params.surfaceId);
var surfaceSize = drawSurface.getSize();
var freqUpdate = params.freqUpdate || 1000;
var colors = params.colors || ['#D1D1D1', '#DD5856'];
var circular = params.circular || false;
// default values for number of cells
var w = Math.ceil(surfaceSize[0] / NUMBER_CELLS);
var wStep = 10;
var h = Math.ceil(surfaceSize[1] / NUMBER_CELLS);
var hStep = 10;
if(params.numberElements){
w = params.numberElements[0];
wStep = Math.ceil(surfaceSize[0] / w);
h = params.numberElements[1];
hStep = Math.ceil(surfaceSize[1] / h);
}
//
// Default automaton is always conway
// params are such that a custom lifeLike automaton is given
// try to create it, if it fails: conway wins
//
var cell = getLifeAutomaton('conway');
if(is_array(params.auto)){
if(params.auto[0] === LIFE_LIKE_KEY_WORD){
cell = Object.create(lifeLike(params.auto[1], params.auto[2]))|| cell;
}
}else{
cell = getLifeAutomaton(params.auto);
}
var cells = [];
var animating = false;
/**
* Explore the direct neighbourhood of the given cell and add all cell to the array
* This function is circular, meaning that a cell in (0,0) will see at (h-1,0) and (0,w-1)
* @param x x location on the automaton
* @param y y location on the automaton
* @returns {Array} where each entry correspond to a cell
*/
var getNeighCircular = function(x, y){
var neigh = [];
for(var i = x-1; i <= x+1; i++){
for(var j = y-1; j <= y+1; j++){
neigh.push(cells[(i+w)%w][(j+h)%h]);
}
}
return neigh;
};
/**
* Explore the direct neighbourhood of the given cell and add all cell to the array
* This function is NOT circular
* @param x x location on the automaton
* @param y y location on the automaton
* @returns {Array} where each entry correspond to a cell, may have less than 8 entries
*/
var getNeighNonCircular = function(x, y){
var neigh = [];
for(var i = Math.max(x-1, 0); i <= Math.min(x+1, w-1); i++){
for(var j = Math.max(y-1, 0); j <= Math.min(y+1, h-1); j++){
neigh.push(cells[i][j]);
}
}
return neigh;
};
that.getNeigh = circular && getNeighCircular || getNeighNonCircular;
/**
* Apply the given action to all cell. Call action with its position and the current cell
* @param action action(x, y, cell)
*/
var applyAll = function(action){
for(var i = 0; i < w; i++){
for(var j = 0; j < h; j++){
action(i, j, cells[i][j]);
}
}
};
/**
* Same as applyAll with the current automaton as the last parameter
* @param action takes x, y, cell and automaton
*/
var executeAll = function(action){
for(var i = 0; i < w; i++){
for(var j = 0; j < h; j++){
action(i, j, cells[i][j], that);
}
}
};
/**
* Allows to init all cells of the automaton
* @param actionInit must return an object to bet set on each cells
*/
var initAll = function(actionInit){
for(var i = 0; i < w; i++){
cells.push([]);
for(var j = 0; j < h; j++){
cells[i].push(actionInit(i, j));
}
}
};
var displayAll = function(){
applyAll(function(x, y, cell){
drawSurface.drawSquare( x * wStep, y * hStep,
wStep, hStep,
colors[cell.state]);
});
};
/**
* Allow to execute only one iteration of the automaton.
*/
that.nextIteration = function(){
executeAll(cell.execute);
applyAll(cell.update);
displayAll();
};
/**
* Make the animation, make one step each time it is called
* Stop when animating is set to false
*/
var next = function(){
setTimeout(function(){
if(animating){
that.nextIteration();
next();
}
}, freqUpdate);
};
/**
* Start the automaton. Do nothing if the automaton is already running
*/
that.start = function(){
if(!animating){
animating = true;
next();
}
};
/**
* Stop the automaton. Do nothing if the automaton is already running
*/
that.stop = function(){
animating = false;
};
/**
* Live update of the frequency
* @param newFreq, new
*/
that.updateFrequency = function(newFreq){
if(typeof newFreq === 'number'){
freqUpdate = newFreq;
}
};
// init and display all for the first time
initAll(cell.init);
displayAll();
return that;
}
})(Automaton);