blob: 77d95625bdf89073eba95ee513cbe893bfd7bfef [file] [log] [blame]
// 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,
];