Serge Bazanski | 717aad4 | 2021-07-11 16:03:43 +0000 | [diff] [blame] | 1 | // 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 | |
| 4 | class 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. |
| 74 | class 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. |
| 105 | class 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: |
| 269 | export const animations = [ |
| 270 | Life, |
| 271 | SnakeChase, |
| 272 | ]; |
| 273 | |