Recho

DocsExamplesSketches
Bairui Su
Bairui Su / Moon Sundial
Created 2025-09-12
//➜ 🌘🌕🌕🌕🌕🌕🌕🌖🌖🌖🌖🌕🌕🌕🌕🌕🌕🌘🌘🌘
//➜ 🌕🌕🌕🌕🌖🌖🌖🌖🌖🌖🌖🌖🌖🌖🌕🌕🌕🌕🌘🌘
//➜ 🌕🌕🌖🌖🌖🌖🌖🌖🌖🌖🌖🌖🌖🌖🌖🌕🌕🌕🌕🌘
//➜ 🌕🌖🌖🌖🌖🌗🌗🌗🌗🌗🌗🌗🌖🌖🌖🌖🌕🌕🌕🌘
//➜ 🌖🌖🌖🌖🌗🌗🌗🌗🌗🌗🌗🌗🌗🌗🌖🌖🌖🌕🌕🌕
//➜ 🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌗🌗🌗🌗🌖🌖🌖🌕🌕
//➜ 🌖🌖🌗🌗🌗🌘🌘🌘🌘🌘🌘🌘🌗🌗🌗🌖🌖🌖🌕🌕
//➜ 🌖🌗🌗🌗🌘🌘🌘🌘🌘🌘🌘🌘🌘🌗🌗🌗🌖🌖🌕🌕
//➜ 🌖🌗🌗🌗🌘🌘🌘🌕🌕🌕🌕🌘🌘🌗🌗🌗🌖🌖🌕🌕
//➜ 🌖🌗🌗🌘🌘🌘🌕🌕🌕🌕🌕🌘🌘🌗🌗🌗🌖🌖🌕🌕
//➜ 🌗🌗🌗🌘🌘🌕🌕🌕🌖🌖🌕🌘🌘🌗🌗🌗🌖🌖🌕🌕
//➜ 🌗🌗🌗🌘🌘🌕🌕🌕🌖🌖🌗🌗🌗🌗🌗🌖🌖🌖🌕🌕
//➜ 🌗🌗🌗🌘🌘🌕🌕🌕🌖🌖🌗🌗🌗🌗🌖🌖🌖🌕🌕🌕
//➜ 🌗🌗🌗🌘🌘🌕🌕🌕🌖🌖🌖🌖🌖🌖🌖🌖🌖🌕🌕🌕
//➜ 🌖🌗🌗🌘🌘🌘🌕🌕🌕🌖🌖🌖🌖🌖🌖🌖🌕🌕🌕🌘
//➜ 🌖🌗🌗🌘🌘🌘🌕🌕🌕🌕🌖🌖🌖🌖🌕🌕🌕🌕🌘🌘
//➜ 🌖🌗🌗🌗🌘🌘🌘🌕🌕🌕🌕🌕🌕🌕🌕🌕🌕🌘🌘🌘
//➜ 🌖🌖🌗🌗🌗🌘🌘🌘🌘🌕🌕🌕🌕🌕🌕🌕🌘🌘🌘🌘
//➜ 🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌘🌘🌘🌘🌘🌘🌘🌘🌘🌗
//➜ 🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌘🌘🌘🌘🌘🌘🌗🌗🌗
{
  let output = "";
  for (let i = 0; i < size; i++) {
    for (let j = 0; j < size; j++) {
      const [x, y] = [pos(j), pos(i)];
      const r = Math.hypot(x, y);
      const theta = Math.atan2(y, x) / (Math.PI * 2);
      const phase = now / 3000;
      const l = ((r + theta - phase) % 1) * -360;
      const index = ~~rotate(l);
      output += moons[index];
    }
    output += i == size - 1 ? "" : "\n";
  }
  echo(output);
}

const d3 = recho.require("d3");

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                             References
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * [1] https://observablehq.com/d/a741f9a27e8c0e73
 */
Luyu Cheng
Luyu Cheng / Maze
Created 2025-09-11
//➜ ──┬───────┬─────┬───────────────────┐
//➜   │       │     │                   │
//➜ │ │ ──┬─┐ │ ┌── ├─────┐ ┌────── ┌── │
//➜ │ │   │ │   │   │     │ │       │   │
//➜ │ │ │ │ └───┤ ┌─┤ ──┐ └─┘ ┌─────┤ ──┤
//➜ │ │ │ │     │ │ │   │     │     │   │
//➜ │ └─┘ │ │ ┌─┘ │ │ │ ├───┬─┘ ┌───┴── │
//➜ │     │ │ │   │   │ │   │   │       │
//➜ ├─────┤ │ │ ┌─┘ ┌─┤ │ │ │ │ │ ──┬───┤
//➜ │     │ │ │ │   │ │  ●│ │ │ │   │   │
//➜ │ ┌───┘ │ │ │ ──┘ ├───┘ │ │ └─┐ ├── │
//➜ │ │     │   │     │     │ │   │ │   │
//➜ │ │ ──┬─┴───┴───┐ │ ────┴─┤ ──┤ │ ──┤
//➜ │ │   │         │ │       │   │ │   │
//➜ │ └─┐ └─┐ ──┬─┐ └─┼────── │ │ │ └── │
//➜ │   │   │   │ │   │       │ │ │     │
//➜ │ ──┴─┐ ├── │ └─┐ │ ┌───┬─┴─┘ └─┬── │
//➜ │     │ │   │   │   │   │       │   │
//➜ │ ──┐ │ │ ┌─┘ ──┴───┘ ──┘ │ ────┘ ──┘
//➜ │   │     │               │
//➜ └───┴─────┴───────────────┴──────────
{
  frame;

  if (!maze.walk()) maze.regenerate();
  echo(maze.render());
}

const width = 18;
const height = 10;

const maze = new Maze(width, height);

const frame = recho.interval(100);

const cell = () => ({top: true, right: true, bottom: true, left: true});

class Maze {
  constructor(width, height) {
    this.width = width;
    this.height = height;
    this.initialize();
    this.generate();
  }

  initialize() {
    this.grid = _.times(this.height, () => _.times(this.width, cell));
    this.visited = _.times(this.height, () => _.times(this.width, _.constant(false)));
    this.path = null;
    this.step = 0;
  }

  regenerate() {
    this.initialize();
    this.generate();
  }

  generate() {
    this.carvePassage(0, 0);
    this.grid[0][0].left = false;
    this.grid[this.height - 1][this.width - 1].right = false;
  }

  walk() {
    if (this.path === null) {
      this.path = this.findPath();
      this.step = 0;
      return true;
    } else if (this.step + 1 < this.path.length) {
      this.step += 1;
      return true;
    } else {
      return false;
    }
  }

  carvePassage(x, y) {
    this.visited[y][x] = true;
    for (const neighbor of _.shuffle(this.getUnvisitedNeighbors(x, y))) {
      if (!this.visited[neighbor.y][neighbor.x]) {
        // Remove walls between current cell and neighbor
        this.removeWall(x, y, neighbor.x, neighbor.y);
        // Recursively visit neighbor
        this.carvePassage(neighbor.x, neighbor.y);
      }
    }
  }

  getUnvisitedNeighbors(x, y) {
    const neighbors = [];
    if (y > 0 && !this.visited[y - 1][x]) neighbors.push({x: x, y: y - 1}); // up
    if (x < this.width - 1 && !this.visited[y][x + 1]) neighbors.push({x: x + 1, y: y}); // right
    if (y < this.height - 1 && !this.visited[y + 1][x]) neighbors.push({x: x, y: y + 1}); // down
    if (x > 0 && !this.visited[y][x - 1]) neighbors.push({x: x - 1, y: y}); // left
    return neighbors;
  }

  removeWall(x1, y1, x2, y2) {
    const dx = x2 - x1;
    const dy = y2 - y1;
    if (dx === 1) {
      // Moving right
      this.grid[y1][x1].right = false;
      this.grid[y2][x2].left = false;
    } else if (dx === -1) {
      // Moving left
      this.grid[y1][x1].left = false;
      this.grid[y2][x2].right = false;
    } else if (dy === 1) {
      // Moving down
      this.grid[y1][x1].bottom = false;
      this.grid[y2][x2].top = false;
    } else if (dy === -1) {
      // Moving up
      this.grid[y1][x1].top = false;
      this.grid[y2][x2].bottom = false;
    }
  }

  findPath() {
    const queue = [[0, 0, [{x: 0, y: 0}]]];
    const visited = new Set([`${0},${0}`]);
    while (queue.length > 0) {
      const [x, y, path] = queue.shift();
      if (x === this.width - 1 && y === this.height - 1) return path;
      for (const {dx, dy, direction} of directions) {
        const nx = x + dx;
        const ny = y + dy;
        const key = `${nx},${ny}`;
        if (
          nx >= 0 &&
          nx < this.width &&
          ny >= 0 &&
          ny < this.height &&
          !visited.has(key) &&
          !this.grid[y][x][direction]
        ) {
          visited.add(key);
          queue.push([nx, ny, [...path, {x: nx, y: ny}]]);
        }
      }
    }
    return null;
  }

  render() {
    const pathSet = new Set();
    if (this.path !== null && this.step < this.path.length) {
      pathSet.add(`${this.path[this.step].x},${this.path[this.step].y}`);
    }

    // Create output grid (2x dimensions + 1 for walls)
    const outputHeight = this.height * 2 + 1;
    const outputWidth = this.width * 2 + 1;
    const output = _.times(outputHeight, () => _.times(outputWidth, _.constant(chars.space)));

    for (let y = 0; y < this.height; y++) {
      for (let x = 0; x < this.width; x++) {
        const cell = this.grid[y][x];
        const ox = x * 2;
        const oy = y * 2;
        if (cell.top) output[oy][ox + 1] = chars.horizontal;
        if (cell.bottom) output[oy + 2][ox + 1] = chars.horizontal;
        if (cell.left) output[oy + 1][ox] = chars.vertical;
        if (cell.right) output[oy + 1][ox + 2] = chars.vertical;
        if (pathSet.has(`${x},${y}`)) output[oy + 1][ox + 1] = chars.ghost;
      }
    }

    // Draw corners and junctions
    for (let y = 0; y < outputHeight; y += 2) {
      for (let x = 0; x < outputWidth; x += 2) {
        const top = y > 0 && output[y - 1][x] === chars.vertical;
        const bottom = y < outputHeight - 1 && output[y + 1][x] === chars.vertical;
        const left = x > 0 && output[y][x - 1] === chars.horizontal;
        const right = x < outputWidth - 1 && output[y][x + 1] === chars.horizontal;
        const connections = (top ? 1 : 0) + (right ? 1 : 0) + (bottom ? 1 : 0) + (left ? 1 : 0);
        if (connections === 0) {
          output[y][x] = chars.space;
        } else if (connections === 1) {
          if (top || bottom) output[y][x] = chars.vertical;
          else output[y][x] = chars.horizontal;
        } else if (connections === 2) {
          if (top && bottom) output[y][x] = chars.vertical;
          else if (left && right) output[y][x] = chars.horizontal;
          else if (top && right) output[y][x] = chars.bottomLeft;
          else if (top && left) output[y][x] = chars.bottomRight;
          else if (bottom && right) output[y][x] = chars.topLeft;
          else if (bottom && left) output[y][x] = chars.topRight;
        } else if (connections === 3) {
          if (!top) output[y][x] = chars.teeDown;
          else if (!right) output[y][x] = chars.teeLeft;
          else if (!bottom) output[y][x] = chars.teeUp;
          else if (!left) output[y][x] = chars.teeRight;
        } else {
          output[y][x] = chars.cross;
        }
      }
    }
    output[1][0] = chars.startMarker;
    output[outputHeight - 2][outputWidth - 1] = chars.endMarker;
    return output.map((row) => row.join("")).join("\n");
  }
}

const directions = [
  {dx: 0, dy: -1, direction: "top"},
  {dx: 1, dy: 0, direction: "right"},
  {dx: 0, dy: 1, direction: "bottom"},
  {dx: -1, dy: 0, direction: "left"},
];

const chars = {
  horizontal: "─",
  vertical: "│",
  topLeft: "┌",
  topRight: "┐",
  bottomLeft: "└",
  bottomRight: "┘",
  teeUp: "┴",
  teeDown: "┬",
  teeLeft: "┤",
  teeRight: "├",
  cross: "┼",
  space: " ",
  ghost: "●",
  startMarker: " ",
  endMarker: " ",
};

const _ = recho.require("lodash");
Bairui Su
Bairui Su / Running Race
Created 2025-09-09
//➜                                    🐌💨
//➜                                   🐢💨
//➜                              🚶‍♂️💨
//➜                     🚗💨
//➜           🚀💨
{
  const x = (count) => 40 - (count % 40);
  echo("🐌💨".padStart(x(snail)), {quote: false});
  echo("🐢💨".padStart(x(turtle)), {quote: false});
  echo("🚶‍♂️💨".padStart(x(human)), {quote: false});
  echo("🚗💨".padStart(x(car)), {quote: false});
  echo("🚀💨".padStart(x(rocket)), {quote: false});
}
Bairui Su
Created 2025-09-03
//➜
//➜             Great Britain
//➜     pigs -| 🐖
//➜   cattle -| 🐄 🐄 🐄 🐄
//➜    sheep -| 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑
//➜
//➜             United States
//➜     pigs -| 🐖 🐖 🐖 🐖 🐖 🐖
//➜   cattle -| 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄
//➜    sheep -| 🐑 🐑 🐑 🐑 🐑 🐑 🐑
//➜
//➜ Live stock (millions)
//➜
echo(output);

/**
 * Next, let's dive into how `output` is generated.
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                          Preparing the data
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * First, we need to prepare the data. We're going to use the following dataset
 * to create the chart. It's a tiny tubular dataset, with each row representing
 * a animal in a country and the count of the animal.
 */

const data = [
  {animal: "pigs", country: "Great Britain", count: 1354979},
  {animal: "cattle", country: "Great Britain", count: 3962921},
  {animal: "sheep", country: "Great Britain", count: 10931215},
  {animal: "pigs", country: "United States", count: 6281935},
  {animal: "cattle", country: "United States", count: 9917873},
  {animal: "sheep", country: "United States", count: 7084151},
];

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                            Importing D3
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Then we import D3 to help us with the data processing. In Recho, you can
 * typically use `recho.require(name)` to import an external library.
 *
 * > Ref. https://recho.dev/docs/libraries-imports
 * > Ref. https://d3js.org/
 */

const d3 = recho.require("d3");

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                          Generating the bars
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * We'll get started with generating the bars. There are three main tasks here:
 *
 * 1. Mapping animals types to their corresponding emojis.
 * 2. Mapping the counts to the number of emojis.
 * 3. Generating the bars based on the emojis and the number.
 *
 * Here is the implementation:
 */

//➜ [ 0, 1, 2, 3, 4, 5 ]
const I = echo(d3.range(data.length));

//➜ { cattle: "🐄", sheep: "🐑", pigs: "🐖" }
const emoji = echo({cattle: "🐄", sheep: "🐑", pigs: "🐖"});

//➜ [ "🐖", "🐄", "🐑", "🐖", "🐄", "🐑" ]
const E = echo(data.map((d) => emoji[d.animal]));

//➜ [ 1, 4, 11, 6, 10, 7 ]
const V = echo(data.map((d) => Math.round(d.count / 1e6)));

//➜ [ "🐖 ", "🐄 🐄 🐄 🐄 ", "🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 ", "🐖 🐖 🐖 🐖 🐖 🐖 ", "🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 ", "🐑 🐑 🐑 🐑 🐑 🐑 🐑 " ]
const bars = echo(I.map((i) => `${E[i]} `.repeat(V[i])));

/** This is the chart we got so far. */

//➜ 🐖
//➜ 🐄 🐄 🐄 🐄
//➜ 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑
//➜ 🐖 🐖 🐖 🐖 🐖 🐖
//➜ 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄
//➜ 🐑 🐑 🐑 🐑 🐑 🐑 🐑
echo(bars.join("\n"));

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                          Adding the labels
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Next step is to add the labels to the bars. We need to collect all the
 * animal types by a set, and compute a margin left to make sure the labels
 * are aligned. Then concatenate the labels to the bars with a separator: `-|`.
 */

//➜ [ "pigs", "cattle", "sheep" ]
const L = echo(Array.from(new Set(data.map((d) => d.animal))));

//➜ 6
const marginLeft = echo(d3.max(L, (d) => d.length));

//➜ [ "  pigs", "cattle", " sheep", "  pigs", "cattle", " sheep" ]
const labels = echo(data.map((d) => d.animal.padStart(marginLeft, " ")));

//➜ [ "    pigs -| 🐖 ", "  cattle -| 🐄 🐄 🐄 🐄 ", "   sheep -| 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 ", "    pigs -| 🐖 🐖 🐖 🐖 🐖 🐖 ", "  cattle -| 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 ", "   sheep -| 🐑 🐑 🐑 …
const rows = echo(I.map((i) => "  " + labels[i] + " -| " + bars[i]));

/** Now the chart looks like this. */

//➜     pigs -| 🐖
//➜   cattle -| 🐄 🐄 🐄 🐄
//➜    sheep -| 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑
//➜     pigs -| 🐖 🐖 🐖 🐖 🐖 🐖
//➜   cattle -| 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄
//➜    sheep -| 🐑 🐑 🐑 🐑 🐑 🐑 🐑
echo(rows.join("\n"));

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                         Generating the titles
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Technically speaking, the chart is a facet chart, which means it contains
 * multiple charts. The first one is for Great Britain, and the second one is
 * for United States. In order to differentiate the two charts, we need to add
 * the titles and some spacing.
 */

//➜ 45
const width = echo(d3.max(rows, (d) => d.length));

//➜ [ "Great Britain", "United States" ]
const T = echo(Array.from(new Set(data.map((d) => d.country))));

//➜ [ "            Great Britain", "            United States" ]
const titles = echo(T.map((t) => t.padStart(Math.ceil(width / 2 + 2), " ")));

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                             Final output
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Finally, we can concatenate the titles, the rows, and the live stock caption
 * to get the final output!
 */

//➜
//➜             Great Britain
//➜     pigs -| 🐖
//➜   cattle -| 🐄 🐄 🐄 🐄
//➜    sheep -| 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑 🐑
//➜
//➜             United States
//➜     pigs -| 🐖 🐖 🐖 🐖 🐖 🐖
//➜   cattle -| 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄 🐄
//➜    sheep -| 🐑 🐑 🐑 🐑 🐑 🐑 🐑
//➜
//➜ Live stock (millions)
//➜
const output = echo(
  [
    " ",
    titles[0], // Great Britain
    ...rows.slice(0, 3),
    " ",
    titles[1], // United States
    ...rows.slice(3),
    " ",
    "Live stock (millions)", // Add a caption
    " ",
  ].join("\n"),
);
Bairui Su
Created 2025-09-03
//➜                         燚
//➜   燚燚        燚燚  燚     燚燚  燚
//➜     燚燚       燚            燚
//➜       燚燚燚  燚燚 燚    燚燚燚燚   燚燚  燚燚 燚焱燚燚燚
//➜      燚焱    燚燚燚 燚燚   燚燚燚燚燚    燚燚  燚燚焱燚燚
//➜   焱燚   燚    燚燚燚燚燚  焱焱焱燚燚
//➜      焱焱焱燚燚燚燚燚燚焱焱焱燚燚燚 焱燚焱      燚焱焱
//➜   燚燚燚燚燚燚燚     燚燚燚焱焱燚
//➜   焱焱  焱燚燚燚燚燚燚焱焱燚焱焱焱    燚焱焱燚燚燚燚焱焱焱燚燚燚燚焱
//➜   燚燚炎炎燚焱焱炎焱燚燚燚燚燚  燚燚燚燚焱焱燚炎炎炎燚焱燚 燚燚 焱焱炎
//➜   燚燚燚 炎炎炎炎炎炎炎炎炎炎炎焱焱焱燚 炎炎 焱焱燚炎焱炎燚焱焱焱焱炎炎
//➜   焱焱焱焱焱焱焱焱炎炎炎炎焱焱焱焱焱火焱燚燚焱 焱燚焱燚焱炎炎炎炎燚炎焱焱
//➜   焱炎燚燚燚焱燚炎炎炎火火焱焱火火火火火火焱炎炎炎 燚炎炎  焱燚燚燚炎火
//➜   燚炎焱焱燚燚炎焱炎火焱炎炎焱焱燚焱炎炎焱炎火焱炎焱焱焱炎火焱炎炎焱焱焱炎
//➜   火炎焱焱火炎炎焱炎焱火炎炎炎炎炎炎炎炎焱炎焱火焱火燚焱焱炎炎焱焱焱焱炎火
{
  frame;

  for (let y = 0; y < rows - 1; y++) {
    for (let x = 0; x < cols; x++) {
      const decay = d3.randomInt(0, 3)();
      const spread = d3.randomInt(-1, 1)();
      const i = Math.min(Math.max(0, x - spread), cols - 1);
      const target = fire[index(i, y + 1)];
      fire[index(x, y)] = Math.max(0, target - decay);
    }
  }

  const noise = new Noise();
  const linear = d3.scaleLinear([0, 1], [0, rows]);
  const source = (i) => linear(noise.get(i, 0, 0));

  for (let x = 0; x < cols; x++) {
    fire[index(x, rows - 1)] = ~~source(x);
  }

  const quantile = d3.scaleQuantile(d3.extent(fire), chs);
  const ch = (d) => (d ? quantile(d) : " ");

  let output = "";
  for (let i = 0; i < rows; ++i) {
    for (let j = 0; j < cols; ++j) output += ch(fire[index(j, i)]);
    output += i === rows - 1 ? "" : "\n";
  }
  output = output.split("\n").map((d) => "  " + d).join("\n");

  echo(output);
}

const d3 = recho.require("d3");

/**
 * I like this example also because I found one importable noise library:
 * `perlin-noise-3d`:
 *
 * - https://www.npmjs.com/package/perlin-noise-3d
 */

const Noise = recho.require("perlin-noise-3d");

/**
 * Perlin noise is so useful in creative coding, so it's great that we can use
 * it in Recho before making it a built-in function.
 *
 * As you can see, the implementation replies on noise function to generate
 * natural source of fire, which is the key to create such effect!
 */
Bairui Su
Bairui Su / Matrix Rain
Created 2025-08-22
//➜              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");
Bairui Su
Bairui Su / Random Histogram
Created 2025-08-22
//➜          █
//➜       █  █          █  █                    ██
//➜       █  █  █ █     █  █          █         ██
//➜   █ █ ██ █  █ ██    █  █  ███     █       █ ██
//➜  ██ ██████  █ ██    ██ █ ████    ██  ████ █ ██  ██
//➜ ██████████  ████████████ ████    ██ ██████████ ███
//➜ ██████████ ████████████████████ ██████████████ ███
//➜ ██████████████████████████████████████████████████
{
  let output = "";
  for (let i = 0; i < height; i++) {
    for (let j = 0; j < width; j++) {
      const bin = bins[j];
      const h = bin ? (bin * height) / d3.max(bins) : 0;
      output += h >= height - i ? "█" : " ";
    }
    output += i === height - 1 ? "" : "\n";
  }
  echo(output);
}
Bairui Su
Bairui Su / Mandelbrot Set
Created 2025-08-21
//➜ ·········································*OoO@@@@@@@@@@@@@@@@@@@*···············
//➜ ···········································@@@@@@@@@@@@@@@@@@@@@@···············
//➜ ·········································@@@@@@@@@@@@@@@@@@@@@@@@@@·············
//➜ ·······························@·O@**····*@@@@@@@@@@@@@@@@@@@@@@@@O·············
//➜ ······························*@@@@@@@@··@@@@@@@@@@@@@@@@@@@@@@@@@@·············
//➜ ·····························*@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@o·············
//➜ ··························*o*@@@@@@@@@@@o@@@@@@@@@@@@@@@@@@@@@@@@o··············
//➜ ············@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*················
//➜ ··························*o*@@@@@@@@@@@o@@@@@@@@@@@@@@@@@@@@@@@@o··············
//➜ ·····························*@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@@@@@@o·············
//➜ ······························*@@@@@@@@··@@@@@@@@@@@@@@@@@@@@@@@@@@·············
//➜ ·······························@·O@**····*@@@@@@@@@@@@@@@@@@@@@@@@O·············
//➜ ·········································@@@@@@@@@@@@@@@@@@@@@@@@@@·············
//➜ ···········································@@@@@@@@@@@@@@@@@@@@@@···············
//➜ ·········································*OoO@@@@@@@@@@@@@@@@@@@*···············
//➜ ·············································@@@@@@@@@@@@@@@@@@@@···············
//➜ ·············································@@·*@@@@@@@@@@@@···················
//➜ ·············································@·······@@@*·······················
//➜ ····················································O@@@@*······················
//➜ ····················································o@@@@*······················
//➜ ······················································O@························
//➜ ················································································
{
  let output = "";
  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      const re = map(x, 0, cols, -2.5, 1);
      const im = map(y, 0, rows, -1, 1);
      let [a, b, i] = [0, 0, 0];
      while (i < maxIter) {
        [a, b] = [a * a - b * b + re, 2 * a * b + im];
        if (a * a + b * b > 4) break;
        i++;
      }
      const index = ~~((i / maxIter) * (colors.length - 1));
      output += colors[index];
    }
    output += y === rows - 1 ? "" : "\n";
  }
  echo(output);
}

function map(x, d0, d1, r0, r1) {
  return r0 + ((r1 - r0) * (x - d0)) / (d1 - d0);
}

/**
 * Again, you don't need to completely understand the code above for now. If
 * you find textual outputs can be interesting and creative by this example,
 * that's the point!
 *
 * If you're really curious about Mandelbrot set, here are some examples that
 * I made with Charming.js[2] you may find interesting:
 *
 * - Multibrot Set: https://observablehq.com/d/fc2cfd9ae9e7524c
 * - Multibrot Set Table: https://observablehq.com/d/3028c0d5655345e3
 * - Multibrot Set Transition: https://observablehq.com/d/c040d3db33c0033e
 * - Zoomable Mandelbrot Set (Canvas): https://observablehq.com/d/2e5bdd2365236c2d
 * - Zoomable Mandelbrot Set (WebGL): https://observablehq.com/d/cfe263c1213334e3
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                              References
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * [1] https://en.wikipedia.org/wiki/Mandelbrot_set
 * [2] https://charmingjs.org/
 */