In early, because it looks like I’m going to be stuck working on a plumbing repair the rest of the day. Thanks @scruffynerf for running this show – I’m already looking forward to next week’s installment!
Today, you will see elegant, compact and efficient code for this job. Then, there’s this. @jeff put the idea in my head - the only way to get rid of it was to write the code. It uses… yes, the center column from a cellular automaton running Wolfram’s Rule 30 to generate a stream of random bits, with which I control all the blinking lights. Without, further ado, here it is:
/*
Rule 30 Flasher
https://en.wikipedia.org/wiki/Rule_30
Flashes a random pixel for a random length of time, with a random amount of twinkling.
Uses a 1D cellular automaton running Wolfram's rule 30 to generate a stream
of random bits, which we use to build various random numbers.
This pattern looks random, but for a given set of initial conditions, it's actually
completely deterministic -- it'll be the same every time.
*/
// arrays to hold the state of the current and previous generations of our
// automaton.
var ca_size = 32; // number of cells per generation
var ca_center = floor(ca_size / 2); // index center cell -- precalculates because we use it often
var g0 = array(ca_size);
var g1 = array(ca_size);
// pointers to data arrays, so we can swap arrays by just swapping pointers
// instead of moving data.
var currState = g0;
var nextState = g1;
var elapsedTime = 0; // accumulator for timing pixel displays
var lifetime = 500; // how long (ms) the current pixel will be displayed
var pix,hue,bri; // pixel index, color and current brightness
// Get random fraction of specified length from rule 30 generator
function getRandomFraction(n) {
return result = getRandomBits(n) >> n
}
// return the specified number of random bits into a 32-bit buffer
// (will discard oldest bits if n > 32)
function getRandomBits(n) {
var result = 0;
for (i = 0; i < n; i++) {
calcNextGen();
result = result << 1;
result |= currState[ca_center];
}
return result;
}
// set up the CA mechanics
function initialize() {
// set up pointers to our data array.
currState = g0;
nextState = g1;
// seed the cell at the center of the array. Bits in this
// column are reliably random, so it's where we'll get our
// random bits while running.
currState[ca_center] = 1;
}
// Use the current state and Wolfram's Rule 30 to calculate the CA's next
// generation. Rule 30 generates very chaotic patterns, and the
// center column of the pattern is known to be a reasonably good RNG.
function calcNextGen() {
var left,curr,right;
// set up 8-bit representation of current cell state. Note that "neighborhood"
// wraps around the ends of the array.
for (var i = 0; i < ca_size; i++) {
left = currState[(i > 0) ? i - 1 : ca_size - 1] << 2;
curr = currState[i] << 1;
right = currState[(i + 1) % ca_size];
// apply transformation rule
nextState[i] = (30 & (1 << (left | curr | right))) > 0;
}
// swap state arrays so the data for the generation we just calculated is in
// currState, which we will use to make random things happen.
var a = currState; currState = nextState; nextState = a;
}
// Main entry point - set up the CA before we start drawing pixels
initialize();
export function beforeRender(delta) {
elapsedTime += delta;
// display timer -- accumulate deltas, generate new pixel when
// total time has reached or exceeded this pixel's random
// lifetime
if (elapsedTime >= lifetime) {
// 8 bits will generate 256 distinct hues.
hue = getRandomFraction(8);
// pixels will live for from 100 to 1200 ms
lifetime = 100+(1100*getRandomFraction(8));
// pick a random pixel. 11 bits will generate 2k unique indices. If your
// strip has more than 2k LEDs, not all pixels will be hit, but the indices
// will still be distributed over the whole strip.
pix = floor(pixelCount * getRandomFraction(11));
// reset timer for the new pixel
elapsedTime = 0;
}
// calculate fadeout and twinkle, then adjust gamma.
// twinkle is controlled by grabbing a random bit from the CA on every frame
bri = 1-((elapsedTime / lifetime)+(0.1 * getRandomBits(1)));
bri = bri * bri;
}
// draw current pixel.Fadeout is a function of the random lifetime calculated
// in beforeRender, random twinkling is generated by varying saturation according to a bitstream
// from the CA
export function render(index) {
hsv(hue,1,(pix == index) * bri)
}