Recho

DocsExamplesSketches
Bairui Su
Created 2025-09-14
//➜             *+===~~----::::::
//➜          #*++==~~~----:::::...::
//➜       @%#**++==~~~---:::::........:
//➜      @%##**++==~~~----::::.........:
//➜    @@%%##**++===~~~---:::::.........::
//➜   @@@@%##**+++==~~~~----:::::........::
//➜  @@@@@%%##**+++==~~~~----:::::::....:::-
//➜  @@@@@%%###**+++===~~~-----::::::::::::-
//➜  @@@@@@%%###**+++===~~~~------::::::::--
//➜  @@@@@@@%%###***+++===~~~~~------------~
//➜  @@@@@@@@@%%##***++++====~~~~~~------~~~
//➜  @@@@@@@@@@%%###***++++=====~~~~~~~~~~==
//➜  @@@@@@@@@@@@%%###****+++++============+
//➜   @@@@@@@@@@@@@%%%###****+++++++++++++*
//➜    @@@@@@@@@@@@@@%%%#####************#
//➜      @@@@@@@@@@@@@@@%%%%############
//➜       @@@@@@@@@@@@@@@@@@%%%%%%%%%%@
//➜          @@@@@@@@@@@@@@@@@@@@@@@
//➜             @@@@@@@@@@@@@@@@@
//➜
echo(
  shader((vPos) => {
    const z2 = 1 - vPos.dot(vPos);
    if (z2 > 0) {
      const dir = new Vector3(1, 1, 1);
      const p = new Vector3(vPos.x, vPos.y, Math.sqrt(z2));
      const intensity = 0.5 * Math.max(0, p.dot(dir));
      return gray2ASCII(new Vector4(intensity, intensity, intensity, 1));
    }
    return gray2ASCII(new Vector4(0, 0, 0, 0));
  }),
);

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                             Ambient Light
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * To make the sphere appear smoother, we can add ambient lighting to the
 * sphere. Ambient light is constant lighting that is not affected by the
 * direction of the light source.
 *
 * Here we simply add the ambient light to the diffuse light to obtain the
 * final lighting. The sphere appears brighter, and the transitions between
 * layers are more natural.
 */

//➜
//➜             +=~~~---::::....:
//➜          *+==~~---::::..........
//➜       #**++==~~---::::.....      ..
//➜      %#*++===~~---::::.....        .
//➜    %%##*+++==~~~---::::......       ..
//➜   %%%##**++===~~----::::.......     ..:
//➜  %%%%##**+++==~~~~---:::::.............:
//➜  %%%%%##**+++===~~~----:::::...........:
//➜  %%%%%%##**+++===~~~-----::::::......:::
//➜  %%%%%%%##***++====~~~-----::::::::::::-
//➜  %%%%%%%%##***+++====~~~~-------:::::---
//➜  %%%%%%%%%###***+++====~~~~~----------~~
//➜  %%%%%%%%%%%###***++++====~~~~~~~~~~~~~=
//➜   %%%%%%%%%%%%###****++++=============+
//➜    %%%%%%%%%%%%%####****+++++++===+++*
//➜      %%%%%%%%%%%%%%####*******+++***
//➜       %%%%%%%%%%%%%%%%%############
//➜          %%%%%%%%%%%%%%%%%%%%%%%
//➜             %%%%%%%%%%%%%%%%%
//➜
echo(
  shader((vPos) => {
    const z2 = 1 - vPos.dot(vPos);
    const skyColor = new Vector3(0.5, 0.85, 1.5);
    const ambient = new Vector3(0.2, 0.1, 0.05);
    if (z2 > 0) {
      const dir = new Vector3(1, 1, 1);
      const p = new Vector3(vPos.x, vPos.y, Math.sqrt(z2));
      const intensity = 0.5 * Math.max(0, p.dot(dir));
      const diffuse = skyColor.clone().multiplyScalar(intensity);
      const light = diffuse.add(ambient);
      return gray2ASCII(new Vector4(light.x, light.y, light.z, 1));
    }
    return gray2ASCII(new Vector4(0, 0, 0, 0));
  }),
);

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                            Rotating Sphere
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Next, we make the sphere rotate around the y-axis. We can use the time
 * variable to compute the direction of the light. The key is to change the `x`
 * and `z` components of the light direction.
 *
 * We define a time variable `uTime` using `recho.now()`, which is a generator
 * that yields the current time continuously. Every time the time variable
 * changes, the referencing block (shader) will be re-evaluated, resulting in
 * smooth animations.
 */

const uTime = recho.now();

//➜
//➜             ####*****+++==~~-
//➜          %%%%%%%%%####***++==~~-
//➜       %%%%%%%%%%%%%%%%###**+++==~-:
//➜      %%%%%%%%%%%%%%%%%%%%##***++==~-
//➜    %%%%%%%%%%%%%%%%%%%%%%%%##***++=~~:
//➜   %%%%%%%%%%%%%%%%%%%%%%%%%%%##**++==~-
//➜  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**+==~-
//➜  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**++=~
//➜  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**+==
//➜  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#**+=
//➜  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**+
//➜  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##*+
//➜  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#*+
//➜   %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#*
//➜    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#
//➜      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
//➜       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%
//➜          %%%%%%%%%%%%%%%%%%%%%%%
//➜             %%%%%%%%%%%%%%%%%
//➜
echo(
  shader((vPos) => {
    const theta = uTime / 400;
    const sqrt2 = Math.sqrt(2);
    const dx = Math.cos(theta) * sqrt2;
    const dz = Math.sin(theta) * sqrt2;
    const z2 = 1 - vPos.dot(vPos);
    const skyColor = new Vector3(0.5, 0.85, 1.5);
    const ambient = new Vector3(0.2, 0.1, 0.05);
    if (z2 > 0) {
      const dir = new Vector3(dx, 1, dz);
      const p = new Vector3(vPos.x, vPos.y, Math.sqrt(z2));
      const intensity = 0.5 * Math.max(0, p.dot(dir));
      const diffuse = skyColor.clone().multiplyScalar(intensity);
      const light = diffuse.add(ambient);
      return gray2ASCII(new Vector4(light.x, light.y, light.z, 1));
    }
    return gray2ASCII(new Vector4(0, 0, 0, 0));
  }),
);

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                            Moving Sphere
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Let's not only rotate the sphere, but also move it. This can be achieved by
 * updating the position and radius of the sphere.
 */

//➜
//➜
//➜
//➜
//➜
//➜
//➜
//➜
//➜
//➜
//➜
//➜                                       #***
//➜                                  %%%%%%%####***
//➜                               %%%%%%%%%%%%%###***
//➜                              %%%%%%%%%%%%%%%%###**+
//➜                             %%%%%%%%%%%%%%%%%%%###**
//➜                            %%%%%%%%%%%%%%%%%%%%%%##**
//➜                            %%%%%%%%%%%%%%%%%%%%%%%##*
//➜                            %%%%%%%%%%%%%%%%%%%%%%%%#*
//➜                            %%%%%%%%%%%%%%%%%%%%%%%%#
//➜                             %%%%%%%%%%%%%%%%%%%%%%%#
//➜                              %%%%%%%%%%%%%%%%%%%%%%
//➜                                %%%%%%%%%%%%%%%%%%
//➜                                   %%%%%%%%%%%%
//➜
//➜
//➜
//➜
//➜
//➜
//➜
echo(
  shader(
    (vPos) => {
      const now = uTime / 2000;
      const theta = uTime / 400;
      const sqrt2 = Math.sqrt(2);
      const dx = Math.cos(theta) * sqrt2;
      const dz = Math.sin(theta) * sqrt2;
      const r = 0.3 + 0.2 * Math.sin(4 * now);
      const x = vPos.x + 0.5 * Math.sin(5 * now);
      const y = vPos.y + 0.5 * Math.sin(7 * now);
      const z2 = r * r - (x * x + y * y);
      const skyColor = new Vector3(0.5, 0.85, 1.5);
      const ambient = new Vector3(0.2, 0.1, 0.05);
      if (z2 > 0) {
        const dir = new Vector3(dx, 1, dz);
        const p = new Vector3(x, y, Math.sqrt(z2));
        const intensity = 0.5 * Math.max(0, p.dot(dir));
        const diffuse = skyColor.clone().multiplyScalar(intensity);
        const light = diffuse.add(ambient);
        return gray2ASCII(new Vector4(light.x, light.y, light.z, 1));
      }
      return gray2ASCII(new Vector4(0, 0, 0, 0));
    },
    [61, 31],
  ),
);

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                        More Spheres with more Lights
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Our final example is more complex, featuring multiple spheres and multiple
 * lights. However, the concept remains the same as the previous examples.
 * The only difference is that for each cell, we need to compute the color for
 * each sphere, then select the one closest to the screen as the final color.
 */

//➜
//➜                                                   ####***++
//➜           **######                               ########***
//➜          ###########                            ############
//➜         #############                            ###########
//➜         #############                             #########
//➜         #############                                 #
//➜           #########
//➜
//➜         ####****+
//➜        ########***
//➜        ###########*
//➜        ###########
//➜         #########
//➜            ###
//➜                           %%*****+++=
//➜                          %%********+++
//➜                         %%%***********+
//➜                         %%%***********
//➜                          %%%*********
//➜                             %%%***          ##***++
//➜                                           %%###****++
//➜                                          %%#######****
//➜                                          %%##########**
//➜   *#****+++                               %###########
//➜  ######****+                               %%#######
//➜  #########***
//➜  ###########
//➜   ##########
//➜     #####
//➜
echo(
  shader(
    (vPos) => {
      const scale = 5;
      const now = uTime / 1000;
      const ambient = new Vector3(0.2, 0.1, 0.05);
      let fragColor = new Vector4(0, 0, 0, 0);
      let zMax = -1000;
      const L1 = new Vector3(Math.sin(now * 2), 1, 0).normalize();
      const L2 = new Vector3(-1, -0.1, 0).normalize();
      for (let i = 0; i < 10; i++) {
        const x = scale * vPos.x + 4 * Math.sin(11.8 * i + 100.3 + 0.3 * now);
        const y = scale * vPos.y + 4 * Math.sin(10.3 * i + 200.6 + 0.3 * now);
        const z = scale * vPos.y + 4 * Math.cos(10.3 * i + 200.6 + 0.3 * now);
        const r = 1 * 1 - (x * x + y * y);
        const z1 = z + r / (scale * scale);
        if (r > 0 && z1 > zMax) {
          zMax = z1;
          const p = new Vector3(x, y, Math.sqrt(r));
          let D1 = p.clone().dot(L1);
          let D2 = p.clone().dot(L2);
          D1 = 0.4 * Math.max(0, D1 * Math.abs(D1));
          D2 = 0.4 * Math.max(0, D1 * Math.abs(D2));
          const d1 = new Vector3(0.2, 0.5, 1).multiplyScalar(D1);
          const d2 = new Vector3(0.2, 0.5, 1).multiplyScalar(D2);
          const diffuse = d1.add(d2);
          const color = new Vector3(0.5 + 0.5 * Math.sin(i), 0.5 + 0.5 * Math.sin(4 * i), 0.5 + 0.5 * Math.sin(5 * i));
          const light = color.multiply(diffuse.add(ambient));
          return gray2ASCII(new Vector4(Math.sqrt(light.x), Math.sqrt(light.y), Math.sqrt(light.z), 1));
        }
      }
      return gray2ASCII(fragColor);
    },
    [61, 31],
  ),
);

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                               Summary
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * In this tutorial, we have learned how to use shaders to create basic
 * computer graphics effects. We have learned how to use the diffuse reflection
 * model to compute lighting, how to use ambient lighting to make the sphere
 * appear smoother, how to use the time variable to create animations, and how
 * to use multiple spheres and lights to create more complex effects.
 *
 * I hope you have enjoyed this tutorial. If you have any questions, please
 * feel free to comment on https://github.com/recho-dev/recho/issues/98.
 */
Bairui Su
Bairui Su / Word Count
Created 2025-09-13
//➜ 🟩🟩🟩🟩🟩 dog
//➜ 🟩🟩🟩🟩 cat
//➜ 🟩🟩🟩 mouse
//➜ 🟩 playing
//➜ 🟩 yard
//➜ 🟩 barked
//➜ 🟩 loudly
//➜ 🟩 ran
//➜ 🟩 quickly
//➜ 🟩 hid
//➜ 🟩 under
//➜ 🟩 bench
//➜ 🟩 kept
//➜ 🟩 looking
//➜ 🟩 jumped
//➜ 🟩 small
//➜ 🟩 fence
//➜ 🟩 followed
//➜ 🟩 bird
//➜ 🟩 watched
//➜ 🟩 silently
//➜ 🟩 moved
echo(entries.map(([word, count]) => "🟩".repeat(count) + " " + word).join("\n"));

/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                              References
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Here are the links to the JavaScript APIs we used:
 *
 * - `string.toLowerCase()`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase
 * - `string.replace(pattern, replacement)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
 * - `string.split(splitter)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split
 * - `array.filter(callback)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
 * - `array.includes(value)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
 * - `array.reduce(callback, initialValue)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
 * - `Object.entries(object)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
 * - `string.repeat(value)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
 * - `array.join(separator)`: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join
 */
Bairui Su
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 / Phases of The Moon
Created 2025-09-12
//➜ ╏           ╎ ⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌⓇⒻⓈⓊⓂⓉⓌ ╏
//➜ ╏   January ╎    🌑🌑🌒🌒🌒🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌖🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑    ╏
//➜ ╏  February ╎       🌒🌒🌒🌒🌓🌓🌓🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑    ╏
//➜ ╏     March ╎       🌑🌑🌒🌒🌒🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑 ╏
//➜ ╏     April ╎   🌒🌒🌒🌓🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌑🌑🌑🌒      ╏
//➜ ╏       May ╎     🌒🌒🌒🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌑🌑🌑🌑🌒🌒   ╏
//➜ ╏      June ╎ 🌒🌓🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌒🌒🌒        ╏
//➜ ╏      July ╎   🌒🌓🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌒🌒🌒🌒🌓     ╏
//➜ ╏    August ╎      🌓🌓🌓🌔🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓  ╏
//➜ ╏ September ╎  🌓🌓🌔🌔🌔🌔🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓       ╏
//➜ ╏   October ╎    🌓🌓🌔🌔🌔🌕🌕🌕🌕🌖🌖🌖🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓🌓🌓    ╏
//➜ ╏  November ╎       🌔🌔🌔🌔🌕🌕🌕🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓🌓🌓  ╏
//➜ ╏  December ╎  🌔🌔🌔🌕🌕🌕🌖🌖🌖🌖🌗🌗🌗🌗🌘🌘🌘🌘🌑🌑🌑🌑🌒🌒🌒🌒🌓🌓🌓🌔🌔      ╏
{
  const matrix = _.times(12, () => _.times(6 * 7, () => SPACE));
  for (const day of days) {
    const row = day.getMonth();
    const column = day.getDate() + months[row].getDay();
    matrix[row][column] = getMoonEmoji(day);
  }
  // Remove the leading and trailing spaces from the header and each row.
  const head = Math.min(...matrix.map((xs) => xs.findIndex((x) => x !== SPACE)));
  const tail = Math.max(...matrix.map((xs) => xs.findLastIndex((x) => x !== SPACE))) + 1;
  echo(line(" ".repeat(longestLength), header.slice(head, tail)), {quote: false});
  echo(matrix.map((x, i) => line(alignedMonthNames[i], x.slice(head, tail).join(""))).join("\n"));
}

function line(...items) {
  return "╏ " + items.join(" ╎ ") + " ╏";
}

const header = "ⓂⓉⓌⓇⒻⓈⓊ".repeat(7);
const months = d3.timeMonths(theFirstDay, theLastDay);
const longestLength = monthNames.reduce((x, y) => Math.max(x, y.length), 0);
const monthNames = months.map(d3.timeFormat("%B"));
const alignedMonthNames = monthNames.map((n) => n.padStart(longestLength, " "));

const theFirstDay = d3.timeYear(new Date(year, 0, 1));
const theLastDay = d3.timeYear.offset(theFirstDay, 1);
const days = d3.timeDays(theFirstDay, theLastDay);

const SPACE = "\u3000"; // Full-width space

function getMoonEmoji(date) {
  const index = Math.round(suncalc.getMoonIllumination(date).phase * 8);
  return String.fromCodePoint(0x1f311 + (index === 8 ? 0 : index));
}

const suncalc = recho.require("suncalc");
const d3 = recho.require("d3");
const _ = recho.require("lodash");
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/
 */