blob: 77d95625bdf89073eba95ee513cbe893bfd7bfef [file] [log] [blame]
Serge Bazanski717aad42021-07-11 16:03:43 +00001// To add your own animation, extend 'Animation' and implement draw(), then add
2// your animation's class name to the list at the bottom of the script.
3
4class Animation {
5 // The constructor for Animation is called by the site rendering code when
6 // the site loads, so it should be fairly fast. Any delay causes the LED
7 // panel to take longer to load.
8 constructor(nx, ny) {
9 // LED array, indexed by x then y.
10 let leds = new Array(nx);
11 for (let x = 0; x < nx; x++) {
12 leds[x] = new Array(ny);
13 for (let y = 0; y < ny; y++) {
14 leds[x][y] = [0.0, 0.0, 0.0];
15 }
16 }
17 this.leds = leds;
18
19 // Number of LEDs, X and Y.
20 this.nx = nx;
21 this.ny = ny;
22 }
23
24 // Helper function that converts from HSV to RGB, can be used by your draw
25 // code.
26 // H, S and V values must be [0..1].
27 hsv2rgb(h, s, v) {
28 const i = Math.floor(h * 6);
29 const f = h * 6 - i;
30 const p = v * (1 - s);
31 const q = v * (1 - f * s);
32 const t = v * (1 - (1 - f) * s);
33
34 let r, g, b;
35 switch (i % 6) {
36 case 0: r = v, g = t, b = p; break;
37 case 1: r = q, g = v, b = p; break;
38 case 2: r = p, g = v, b = t; break;
39 case 3: r = p, g = q, b = v; break;
40 case 4: r = t, g = p, b = v; break;
41 case 5: r = v, g = p, b = q; break;
42 }
43 return [r, g, b];
44 }
45
46 draw(ts) {
47 // Implement your animation here.
48 // The 'ts' argument is a timestamp in seconds, floating point, of the
49 // frame being drawn.
50 //
51 // Your implementation should write to this.leds, which is two
52 // dimensional array containing [r,g,b] values. Colour values are [0..1].
53 //
54 // X coordinates are [0 .. this.nx), Y coordinates are [0 .. this.ny).
55 // The coordinate system is with X==Y==0 in the top-left part of the
56 // display.
57 //
58 // For example, for a 3x3 LED display the coordinates are as follors:
59 //
60 // (x:0 y:0) (x:1 y:0) (x:2 y:0)
61 // (x:0 y:1) (x:1 y:1) (x:2 y:1)
62 // (x:0 y:2) (x:1 y:2) (x:2 y:2)
63 //
64 // The LED array (this.leds) is indexed by X first and Y second.
65 //
66 // For example, to set the LED red at coordinates x:1 y:2:
67 //
68 // this.leds[1][2] = [1.0, 0.0, 0.0];
69 }
70}
71
72// 'Snake' chase animation, a simple RGB chase that goes around in a zigzag.
73// By q3k.
74class SnakeChase extends Animation {
75 draw(ts) {
76 const nx = this.nx;
77 const ny = this.ny;
78 // Iterate over all pixels column-wise.
79 for (let i = 0; i < (nx*ny); i++) {
80 let x = Math.floor(i / ny);
81 let y = i % ny;
82
83 // Flip every second row to get the 'snaking'/'zigzag' effect
84 // during iteration.
85 if (x % 2 == 0) {
86 y = ny - (y + 1);
87 }
88
89 // Pick a hue for every pixel.
90 let h = (i / (nx*ny) * 10) + (ts/2);
91 h = h % 1;
92
93 // Convert to RGB.
94 let c = this.hsv2rgb(h, 1, 1);
95
96 // Poke.
97 this.leds[x][y] = c;
98 }
99 }
100}
101
102// Game of life on a torus, with random state. If cycles or stalls are
103// detected, the simulation is restarted.
104// By q3k.
105class Life extends Animation {
106 draw(ts) {
107 // Generate state if needed.
108 if (this.state === undefined) {
109 this.generateState();
110 }
111
112 // Step simulation every so often.
113 if (this.nextStep === undefined || this.nextStep < ts) {
114 if (this.nextStep !== undefined) {
115 this.step();
116 this.recordState();
117 }
118 // 10 steps per second.
119 this.nextStep = ts + 1.0/10;
120 }
121
122 if (this.shouldRestart(ts)) {
123 this.generateState();
124 }
125
126 // Render state into LED matrix.
127 for (let x = 0; x < this.nx; x++) {
128 for (let y = 0; y < this.ny; y++) {
129 // Turn on and decay smoothly.
130 let [r, g, b] = this.leds[x][y];
131 if (this.state[x][y]) {
132 r += 0.5;
133 g += 0.5;
134 b += 0.5;
135 } else {
136 r -= 0.05;
137 g -= 0.05;
138 b -= 0.05;
139 }
140 r = Math.min(Math.max(r, 0.0), 1.0);
141 g = Math.min(Math.max(g, 0.0), 1.0);
142 b = Math.min(Math.max(b, 0.0), 1.0);
143 this.leds[x][y] = [r, g, b];
144 }
145 }
146 }
147
148 // recordState records the current state of the simulation within a
149 // 3-element FIFO. This data is used to detect 'stuck' simulations. Any
150 // time there is something repeating within the 3-element FIFO, it means
151 // we're in some boring loop or terminating step, and shouldRestart will
152 // then schedule a simulation restart.
153 recordState() {
154 if (this.recorded === undefined) {
155 this.recorded = [];
156 }
157 // Serialize state into string of 1 and 0.
158 const serialized = this.state.map((column) => {
159 return column.map((value) => value ? "1" : "0").join("");
160 }).join("");
161 this.recorded.push(serialized);
162
163 // Ensure there's not more then 3 recorded state;
164 while (this.recorded.length > 3) {
165 this.recorded.shift();
166 }
167 }
168
169 // shouldRestart looks at the recorded state of simulation frames, and
170 // ensures that there isn't anything repeated within the recorded data. If
171 // so, it schedules a restart of the simulation in 5 seconds.
172 shouldRestart(ts) {
173 // Nothing to do if we have no recorded data.
174 if (this.recorded === undefined) {
175 return false;
176 }
177
178 // If we have a deadline for restarting set already, just obey that and
179 // return true when it expires.
180 if (this.restartDeadline !== undefined) {
181 if (this.restartDeadline < ts) {
182 this.restartDeadline = undefined;
183 return true;
184 }
185 return false;
186 }
187
188 // Otherwise, look for repeat data in the recorded history. If anything
189 // is recorded, schedule a restart deadline in 5 seconds.
190 let s = new Set();
191
192 let restart = false;
193 for (let key of this.recorded) {
194 if (s.has(key)) {
195 restart = true;
196 break;
197 }
198 s.add(key);
199 }
200 if (restart) {
201 console.log("shouldRestart detected restart condition, scheduling restart...");
202 this.restartDeadline = ts + 2;
203 }
204 }
205
206 // generateState builds the initial randomized state of the simulation.
207 generateState() {
208 this.state = new Array();
209 for (let x = 0; x < this.nx; x++) {
210 this.state.push(new Array());
211 for (let y = 0; y < this.ny; y++) {
212 this.state[x][y] = Math.random() > 0.5;
213 }
214 }
215 this.recorded = [];
216 }
217
218 // step runs a simulation step for the game of life board.
219 step() {
220 let next = new Array();
221 for (let x = 0; x < this.nx; x++) {
222 next.push(new Array());
223 for (let y = 0; y < this.ny; y++) {
224 next[x][y] = this.nextFor(x, y);
225 }
226 }
227 this.state = next;
228 }
229
230 // nextFor runs a simulation step for a game of life cell at given
231 // coordinates.
232 nextFor(x, y) {
233 let current = this.state[x][y];
234 // Build coordinates of neighbors, wrapped around (effectively a
235 // torus).
236 let neighbors = [
237 [x-1, y-1], [x, y-1], [x+1, y-1],
238 [x-1, y ], [x+1, y ],
239 [x-1, y+1], [x, y+1], [x+1, y+1],
240 ].map(([x, y]) => {
241 x = x % this.nx;
242 y = y % this.ny;
243 if (x < 0) {
244 x += this.nx;
245 }
246 if (y < 0) {
247 y += this.ny;
248 }
249 return [x, y];
250 });
251 // Count number of live and dead neighbours.
252 const live = neighbors.filter(([x, y]) => { return this.state[x][y]; }).length;
253
254 if (current) {
255 if (live < 2 || live > 3) {
256 current = false;
257 }
258 } else {
259 if (live == 3) {
260 current = true;
261 }
262 }
263
264 return current;
265 }
266}
267
268// Add your animations here:
269export const animations = [
270 Life,
271 SnakeChase,
272];
273