| // To add your own animation, extend 'Animation' and implement draw(), then add |
| // your animation's class name to the list at the bottom of the script. |
| |
| class Animation { |
| // The constructor for Animation is called by the site rendering code when |
| // the site loads, so it should be fairly fast. Any delay causes the LED |
| // panel to take longer to load. |
| constructor(nx, ny) { |
| // LED array, indexed by x then y. |
| let leds = new Array(nx); |
| for (let x = 0; x < nx; x++) { |
| leds[x] = new Array(ny); |
| for (let y = 0; y < ny; y++) { |
| leds[x][y] = [0.0, 0.0, 0.0]; |
| } |
| } |
| this.leds = leds; |
| |
| // Number of LEDs, X and Y. |
| this.nx = nx; |
| this.ny = ny; |
| } |
| |
| // Helper function that converts from HSV to RGB, can be used by your draw |
| // code. |
| // H, S and V values must be [0..1]. |
| hsv2rgb(h, s, v) { |
| const i = Math.floor(h * 6); |
| const f = h * 6 - i; |
| const p = v * (1 - s); |
| const q = v * (1 - f * s); |
| const t = v * (1 - (1 - f) * s); |
| |
| let r, g, b; |
| switch (i % 6) { |
| case 0: r = v, g = t, b = p; break; |
| case 1: r = q, g = v, b = p; break; |
| case 2: r = p, g = v, b = t; break; |
| case 3: r = p, g = q, b = v; break; |
| case 4: r = t, g = p, b = v; break; |
| case 5: r = v, g = p, b = q; break; |
| } |
| return [r, g, b]; |
| } |
| |
| draw(ts) { |
| // Implement your animation here. |
| // The 'ts' argument is a timestamp in seconds, floating point, of the |
| // frame being drawn. |
| // |
| // Your implementation should write to this.leds, which is two |
| // dimensional array containing [r,g,b] values. Colour values are [0..1]. |
| // |
| // X coordinates are [0 .. this.nx), Y coordinates are [0 .. this.ny). |
| // The coordinate system is with X==Y==0 in the top-left part of the |
| // display. |
| // |
| // For example, for a 3x3 LED display the coordinates are as follors: |
| // |
| // (x:0 y:0) (x:1 y:0) (x:2 y:0) |
| // (x:0 y:1) (x:1 y:1) (x:2 y:1) |
| // (x:0 y:2) (x:1 y:2) (x:2 y:2) |
| // |
| // The LED array (this.leds) is indexed by X first and Y second. |
| // |
| // For example, to set the LED red at coordinates x:1 y:2: |
| // |
| // this.leds[1][2] = [1.0, 0.0, 0.0]; |
| } |
| } |
| |
| // 'Snake' chase animation, a simple RGB chase that goes around in a zigzag. |
| // By q3k. |
| class SnakeChase extends Animation { |
| draw(ts) { |
| const nx = this.nx; |
| const ny = this.ny; |
| // Iterate over all pixels column-wise. |
| for (let i = 0; i < (nx*ny); i++) { |
| let x = Math.floor(i / ny); |
| let y = i % ny; |
| |
| // Flip every second row to get the 'snaking'/'zigzag' effect |
| // during iteration. |
| if (x % 2 == 0) { |
| y = ny - (y + 1); |
| } |
| |
| // Pick a hue for every pixel. |
| let h = (i / (nx*ny) * 10) + (ts/2); |
| h = h % 1; |
| |
| // Convert to RGB. |
| let c = this.hsv2rgb(h, 1, 1); |
| |
| // Poke. |
| this.leds[x][y] = c; |
| } |
| } |
| } |
| |
| // Game of life on a torus, with random state. If cycles or stalls are |
| // detected, the simulation is restarted. |
| // By q3k. |
| class Life extends Animation { |
| draw(ts) { |
| // Generate state if needed. |
| if (this.state === undefined) { |
| this.generateState(); |
| } |
| |
| // Step simulation every so often. |
| if (this.nextStep === undefined || this.nextStep < ts) { |
| if (this.nextStep !== undefined) { |
| this.step(); |
| this.recordState(); |
| } |
| // 10 steps per second. |
| this.nextStep = ts + 1.0/10; |
| } |
| |
| if (this.shouldRestart(ts)) { |
| this.generateState(); |
| } |
| |
| // Render state into LED matrix. |
| for (let x = 0; x < this.nx; x++) { |
| for (let y = 0; y < this.ny; y++) { |
| // Turn on and decay smoothly. |
| let [r, g, b] = this.leds[x][y]; |
| if (this.state[x][y]) { |
| r += 0.5; |
| g += 0.5; |
| b += 0.5; |
| } else { |
| r -= 0.05; |
| g -= 0.05; |
| b -= 0.05; |
| } |
| r = Math.min(Math.max(r, 0.0), 1.0); |
| g = Math.min(Math.max(g, 0.0), 1.0); |
| b = Math.min(Math.max(b, 0.0), 1.0); |
| this.leds[x][y] = [r, g, b]; |
| } |
| } |
| } |
| |
| // recordState records the current state of the simulation within a |
| // 3-element FIFO. This data is used to detect 'stuck' simulations. Any |
| // time there is something repeating within the 3-element FIFO, it means |
| // we're in some boring loop or terminating step, and shouldRestart will |
| // then schedule a simulation restart. |
| recordState() { |
| if (this.recorded === undefined) { |
| this.recorded = []; |
| } |
| // Serialize state into string of 1 and 0. |
| const serialized = this.state.map((column) => { |
| return column.map((value) => value ? "1" : "0").join(""); |
| }).join(""); |
| this.recorded.push(serialized); |
| |
| // Ensure there's not more then 3 recorded state; |
| while (this.recorded.length > 3) { |
| this.recorded.shift(); |
| } |
| } |
| |
| // shouldRestart looks at the recorded state of simulation frames, and |
| // ensures that there isn't anything repeated within the recorded data. If |
| // so, it schedules a restart of the simulation in 5 seconds. |
| shouldRestart(ts) { |
| // Nothing to do if we have no recorded data. |
| if (this.recorded === undefined) { |
| return false; |
| } |
| |
| // If we have a deadline for restarting set already, just obey that and |
| // return true when it expires. |
| if (this.restartDeadline !== undefined) { |
| if (this.restartDeadline < ts) { |
| this.restartDeadline = undefined; |
| return true; |
| } |
| return false; |
| } |
| |
| // Otherwise, look for repeat data in the recorded history. If anything |
| // is recorded, schedule a restart deadline in 5 seconds. |
| let s = new Set(); |
| |
| let restart = false; |
| for (let key of this.recorded) { |
| if (s.has(key)) { |
| restart = true; |
| break; |
| } |
| s.add(key); |
| } |
| if (restart) { |
| console.log("shouldRestart detected restart condition, scheduling restart..."); |
| this.restartDeadline = ts + 2; |
| } |
| } |
| |
| // generateState builds the initial randomized state of the simulation. |
| generateState() { |
| this.state = new Array(); |
| for (let x = 0; x < this.nx; x++) { |
| this.state.push(new Array()); |
| for (let y = 0; y < this.ny; y++) { |
| this.state[x][y] = Math.random() > 0.5; |
| } |
| } |
| this.recorded = []; |
| } |
| |
| // step runs a simulation step for the game of life board. |
| step() { |
| let next = new Array(); |
| for (let x = 0; x < this.nx; x++) { |
| next.push(new Array()); |
| for (let y = 0; y < this.ny; y++) { |
| next[x][y] = this.nextFor(x, y); |
| } |
| } |
| this.state = next; |
| } |
| |
| // nextFor runs a simulation step for a game of life cell at given |
| // coordinates. |
| nextFor(x, y) { |
| let current = this.state[x][y]; |
| // Build coordinates of neighbors, wrapped around (effectively a |
| // torus). |
| let neighbors = [ |
| [x-1, y-1], [x, y-1], [x+1, y-1], |
| [x-1, y ], [x+1, y ], |
| [x-1, y+1], [x, y+1], [x+1, y+1], |
| ].map(([x, y]) => { |
| x = x % this.nx; |
| y = y % this.ny; |
| if (x < 0) { |
| x += this.nx; |
| } |
| if (y < 0) { |
| y += this.ny; |
| } |
| return [x, y]; |
| }); |
| // Count number of live and dead neighbours. |
| const live = neighbors.filter(([x, y]) => { return this.state[x][y]; }).length; |
| |
| if (current) { |
| if (live < 2 || live > 3) { |
| current = false; |
| } |
| } else { |
| if (live == 3) { |
| current = true; |
| } |
| } |
| |
| return current; |
| } |
| } |
| |
| // Add your animations here: |
| export const animations = [ |
| Life, |
| SnakeChase, |
| ]; |
| |