
Created 2025-08-22
/**
* ============================================================================
* = Matrix Rain =
* ============================================================================
*
* Matrix train is a classic effect in ASCII art and in the Matrix movie. So I
* want to use Recho to create a simplified version. This example also provides
* a way to render **shapes** in Recho.
*
* My implementation is basically a **particle system**. Each column of the
* matrix is a particle. Each particle has a lifespan, a initial y position,
* and a set of characters. The following rules are applied to each particle:
*
* - If a particle is dead (lifespan < 0), it will be reset,
* - otherwise, if the lifespan is less than the length of the characters, the
* column will fade out, say to change characters to spaces successively,
* - otherwise, the column will blink by replacing with new characters.
*
* Because there is no graphics API in Recho, we need to render the matrix
* manually by creating a buffer and updating it, instead of concatenating
* strings directly.
*
* Feel free to teak the `width` and `height` to see the changes!
*/
const width = 60;
const height = 25;
const columns = d3.range(width).map(() => createColumn(height));
//➜ w $ R | @ 2 !
//➜ j M & C m ,
//➜ _# D 9 z L ? Z=
//➜ !d ` [ * g g 3
//➜ t| [ - BS E k e u< J : o6 j
//➜ I* ! j R$ p7 p I&D k, j ?1 M
//➜ k j# W O D ( , > a s# XY 9' . XK y )
//➜ a $\ *n [B W W F 6[ w> R AOL p j _B U Z
//➜ X /1 NX9 K ~ 7 Z /u b_ o H ,:u=$ 6
//➜ Btb* rE a yD i n ^ ks , S! |? & ?$ l a
//➜ $ 3 @ y ro 9 b Oh ! I- ^7 x =t X
//➜ 8 n < F 8ZG - + 8i 2 .j j bx ,tf G
//➜ _ 8 t 3 GH& T u S K y M ! Ln 45Y K L
//➜ } d ca @ h 4 T 7 6 T ( !y_hx7 P r
//➜ u i / FH0 = K v P ` e <[ XjD <6 u
//➜ M% V { vg | V 4 q 7 [ R ; [@ =; \
//➜ R y [: } ) `v S [ % / g- u= 3
//➜ g $ UH g & rD @ b H = o 4+ R
//➜ 9 g 96 W c ~ t * mq
//➜ ; vI ) p \ M
//➜ ) wc A ~
//➜ d _ = D
//➜ ` ~ q #
//➜ | m e
//➜ e W ^
{
frame;
// Create a new buffer.
const buffer = d3.range(width * height).map(() => " ");
// Update all columns.
for (let i = columns.length - 1; i >= 0; --i) {
const column = columns[i];
const {lifespan, length, chars} = column;
const n = chars.length;
if (lifespan < 0) columns[i] = createColumn(height);
else if (lifespan <= n) chars[n - lifespan] = " ";
else {
for (let j = length - 1; j < n; ++j) chars[j] = randomChar();
chars.push(randomChar());
}
column.lifespan -= 1;
}
// Update the buffer.
for (let i = 0; i < columns.length; ++i) {
const column = columns[i];
const {y, chars} = column;
for (let j = 0; j < chars.length; ++j) buffer[(y + j) * width + i] = chars[j];
}
// Render the buffer.
let output = "";
for (let i = 0; i < height; ++i) {
for (let j = 0; j < width; ++j) output += buffer[i * width + j];
output += i === height - 1 ? "" : "\n";
}
output = output.split("\n").map((d) => " " + d).join("\n");
echo(output);
}
function createColumn(height) {
const lifespan = d3.randomInt(height)();
const length = d3.randomInt(lifespan)();
const chars = d3.range(length).map(randomChar);
const y = d3.randomInt(0, 10)();
return {lifespan, chars, y};
}
function randomChar() {
return String.fromCharCode(d3.randomInt(32, 127)());
}
const frame = recho.interval(1000 / 15);
const d3 = recho.require("d3");