
Created 2025-09-14
/**
 * ============================================================================
 * =                    Computer Graphics: Text-Based Shaders                 =
 * ============================================================================
 *
 * This tutorial introduces fundamental concepts of computer graphics through
 * text-based shader implementations in Recho.
 *
 * I'm taking Prof. Ken Perlin's Computer Graphics course this semester. In the
 * second lecture, it's fascinating to see him creating impressive visual
 * effects with GPU-based fragment shaders.
 *
 * However, I find it challenging for beginners to set up WebGL and write GLSL
 * code. Therefore, I'm creating text-based shader implementations in Recho to
 * make computer graphics more accessible to beginners.
 *
 * > You don't have to be familiar with Canvas, WebGL, or GLSL. Only basic
 * > JavaScript knowledge and some basic vector operations are required.
 *
 * After reading this, you will be able to create the following rotating sphere
 * with diffuse reflection model and ambient light!
 */
//➜
//➜             +=~~~---::::....:
//➜          *+==~~---::::..........
//➜       #**++==~~---::::.....      ..
//➜      %#*++===~~---::::.....        .
//➜    %%##*+++==~~~---::::......       ..
//➜   %%%##**++===~~----::::.......     ..:
//➜  %%%%##**+++==~~~~---:::::.............:
//➜  %%%%%##**+++===~~~----:::::...........:
//➜  %%%%%%##**+++===~~~-----::::::......:::
//➜  %%%%%%%##***++====~~~-----::::::::::::-
//➜  %%%%%%%%##***+++====~~~~-------:::::---
//➜  %%%%%%%%%###***+++====~~~~~----------~~
//➜  %%%%%%%%%%%###***++++====~~~~~~~~~~~~~=
//➜   %%%%%%%%%%%%###****++++=============+
//➜    %%%%%%%%%%%%%####****+++++++===+++*
//➜      %%%%%%%%%%%%%%####*******+++***
//➜       %%%%%%%%%%%%%%%%%############
//➜          %%%%%%%%%%%%%%%%%%%%%%%
//➜             %%%%%%%%%%%%%%%%%
//➜
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));
  }),
);
/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                                Shaders
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Computer graphics involves rendering pixels on the screen. Shaders are
 * programs that control how pixels are rendered. Here we focus on fragment
 * shaders, which are programs that execute once per pixel on the screen,
 * determining the color of each pixel.
 *
 * Since Recho only supports text-based output, our fragment shaders are
 * functions that execute once per cell in the output string, determining the
 * character displayed in each cell.
 *
 * Below is our implementation. The `shader` function takes a `callback`
 * function and an optional `dimension` argument, returning the output string.
 * The `callback` function is evaluated for each cell in the output string,
 * receiving the cell's position as a `Vector2` object. The callback's
 * return value determines the character displayed in that cell. For each cell,
 * the position is normalized to the range of [-1, 1] from left to right, and
 * the range of [1, -1] from top to bottom.
 */
function shader(callback, [width, height] = [41, 21]) {
  let output = "";
  for (let i = 0; i < height; i++) {
    const y = map(i, 0, height - 1, 1, -1);
    for (let j = 0; j < width; j++) {
      const x = map(j, 0, width - 1, -1, 1);
      const vPos = new Vector2(x, y);
      output += callback(vPos);
    }
    output += i === height - 1 ? "" : "\n";
  }
  return output;
}
function map(x, d0, d1, r0, r1) {
  return r0 + ((r1 - r0) * (x - d0)) / (d1 - d0);
}
/**
 * Since JavaScript doesn't support vector operations, we need to use a
 * library to help us. Here we use the vector module in Three.js. We specify
 * the version to 0.160.0, because it's the last version that can be imported
 * by Recho.
 */
const {Vector2, Vector3, Vector4} = await recho.require("three@0.160.0/build/three.min.js");
/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                              Hello Circle!
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Let's draw a circle to test the shader function. The circle is defined by
 * the equation `x^2 + y^2 = 1`. For each cell, we check if the distance from
 * the cell to the center of the circle is less than or equal to 1. If it is,
 * we draw a `+` character; otherwise, we draw a space character. Here is the
 * result:
 */
//➜                     +
//➜             +++++++++++++++++
//➜         +++++++++++++++++++++++++
//➜       +++++++++++++++++++++++++++++
//➜     +++++++++++++++++++++++++++++++++
//➜    +++++++++++++++++++++++++++++++++++
//➜   +++++++++++++++++++++++++++++++++++++
//➜  +++++++++++++++++++++++++++++++++++++++
//➜  +++++++++++++++++++++++++++++++++++++++
//➜  +++++++++++++++++++++++++++++++++++++++
//➜ +++++++++++++++++++++++++++++++++++++++++
//➜  +++++++++++++++++++++++++++++++++++++++
//➜  +++++++++++++++++++++++++++++++++++++++
//➜  +++++++++++++++++++++++++++++++++++++++
//➜   +++++++++++++++++++++++++++++++++++++
//➜    +++++++++++++++++++++++++++++++++++
//➜     +++++++++++++++++++++++++++++++++
//➜       +++++++++++++++++++++++++++++
//➜         +++++++++++++++++++++++++
//➜             +++++++++++++++++
//➜                     +
echo(shader((vPos) => (vPos.length() <= 1 ? "+" : " ")));
/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                          Colors to Characters
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * As mentioned above, actual fragment shaders primarily work with colors.
 * Therefore, it's better to focus on colors and convert them to characters
 * using simple rules in our text-based shader. A color is typically
 * represented by a `Vector4` object, containing the red, green, blue, and
 * alpha components. The alpha component is optional and is usually 1 for
 * opaque colors.
 *
 * Let's first implement a rule that converts the grayscale value of colors to
 * a sequence of ASCII characters.
 */
function gray2ASCII(color) {
  // If the color is transparent, return a space character.
  if (color.w !== undefined && color.w === 0) return " ";
  const chs = ["@", "%", "#", "*", "+", "=", "~", "-", ":", ".", " "];
  const gray = (color.x + color.y + color.z) / 3;
  const i = Math.floor(gray * chs.length);
  return chs[i];
}
/**
 * Then we can use the `gray2ASCII` function as shown below. This example maps
 * the x and y components of the position to the red and green components of
 * the color respectively, resulting in a gradient.
 */
//➜ ======+++++++++++***********###########%%
//➜ ========+++++++++++***********###########
//➜ ==========+++++++++++***********#########
//➜ ~===========+++++++++++***********#######
//➜ ~~~===========+++++++++++***********#####
//➜ ~~~~~===========+++++++++++***********###
//➜ ~~~~~~~===========+++++++++++***********#
//➜ ~~~~~~~~~===========+++++++++++**********
//➜ ~~~~~~~~~~~===========+++++++++++********
//➜ --~~~~~~~~~~~===========+++++++++++******
//➜ ----~~~~~~~~~~~===========+++++++++++****
//➜ ------~~~~~~~~~~~===========+++++++++++**
//➜ --------~~~~~~~~~~~===========+++++++++++
//➜ ----------~~~~~~~~~~~===========+++++++++
//➜ :-----------~~~~~~~~~~~===========+++++++
//➜ :::-----------~~~~~~~~~~~===========+++++
//➜ :::::-----------~~~~~~~~~~~===========+++
//➜ :::::::-----------~~~~~~~~~~~===========+
//➜ :::::::::-----------~~~~~~~~~~~==========
//➜ :::::::::::-----------~~~~~~~~~~~========
//➜ ..:::::::::::-----------~~~~~~~~~~~======
echo(
  shader((vPos) => {
    const rgb = new Vector3(vPos.x, vPos.y, 0).multiplyScalar(-0.5).addScalar(0.5);
    return gray2ASCII(rgb);
  }),
);
/**
 * In addition to grayscale, we can also convert the RGB components of colors
 * to a sequence of emoji color blocks.
 */
function rgb2emoji(color) {
  if (color.w !== undefined && color.w === 0) return " ";
  const normalize = (x) => x.map((d) => d / 255);
  const emojis = [
    {char: "🟥", rgb: normalize([196, 58, 38])},
    {char: "🟧", rgb: normalize([240, 144, 54])},
    {char: "🟨", rgb: normalize([234, 188, 64])},
    {char: "🟩", rgb: normalize([81, 177, 52])},
    {char: "🟦", rgb: normalize([40, 92, 233])},
    {char: "🟪", rgb: normalize([173, 66, 246])},
    {char: "⬛", rgb: normalize([3, 3, 3])},
    {char: "⬜", rgb: normalize([217, 217, 217])},
  ];
  let best = emojis[0];
  let bestDist = Infinity;
  for (const e of emojis) {
    const [r, g, b] = e.rgb;
    const {x, y, z} = color;
    const dist = (x - r) ** 2 + (y - g) ** 2 + (z - b) ** 2;
    if (dist < bestDist) {
      best = e;
      bestDist = dist;
    }
  }
  return best.char;
}
/**
 * However, the result is not optimal. Therefore, we'll use the grayscale rule
 * for the following examples.
 */
//➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦⬛⬛⬛⬛⬛⬛⬛⬛
//➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦⬛⬛⬛⬛
//➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦⬛
//➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
//➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
//➜ 🟧🟧🟧🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦
//➜ 🟧🟧🟧🟧🟧🟧🟥🟥🟥🟥🟥🟥🟥🟩🟩🟩🟩🟦🟦🟦🟦🟦🟦🟦🟦
//➜ 🟧🟧🟧🟧🟧🟧🟧🟧🟧🟥🟥🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟦🟦🟦🟦
//➜ 🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟦
//➜ 🟧🟧🟧🟧🟧🟧🟧🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
//➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
//➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
//➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
//➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
//➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
//➜ 🟨🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
//➜ 🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜⬜🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
echo(
  shader(
    (vPos) => {
      const rgb = new Vector3(vPos.x, vPos.y, 0).multiplyScalar(-0.5).addScalar(0.5);
      return rgb2emoji(rgb);
    },
    [25, 17],
  ),
);
/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                              First Sphere
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Now let's draw a sphere. The sphere is defined by the equation `x^2 + y^2 +
 * z^2 = 1`. First, for each vPos (x, y), we compute the z component using
 * `1 - x^2 - y^2`, which is equivalent to `1 - vPos.dot(vPos)`. If z is
 * valid (z > 0), we compute the color of that cell; otherwise, we set the
 * color to transparent.
 *
 * We assign the z component to the red and green components of the color.
 * The larger the z value (closer to the center), the brighter the color
 * appears. This simulates a beam of light shining perpendicularly onto the
 * screen from outside.
 */
//➜
//➜             %%###*******###%%
//➜          %##****+++++++++****##%
//➜       %##**+++++=========+++++**##%
//➜      ##**+++=================+++**##
//➜    %#**+++=====~~~~~~~~~~~=====+++**#%
//➜   %#**++====~~~~~~~~~~~~~~~~~====++**#%
//➜  @#**++====~~~~~~~~~~~~~~~~~~~====++**#@
//➜  %#*++====~~~~~~---------~~~~~~====++*#%
//➜  #**++===~~~~~~-----------~~~~~~===++**#
//➜  #**++===~~~~~~-----------~~~~~~===++**#
//➜  #**++===~~~~~~-----------~~~~~~===++**#
//➜  %#*++====~~~~~~---------~~~~~~====++*#%
//➜  @#**++====~~~~~~~~~~~~~~~~~~~====++**#@
//➜   %#**++====~~~~~~~~~~~~~~~~~====++**#%
//➜    %#**+++=====~~~~~~~~~~~=====+++**#%
//➜      ##**+++=================+++**##
//➜       %##**+++++=========+++++**##%
//➜          %##****+++++++++****##%
//➜             %%###*******###%%
//➜
echo(
  shader((vPos) => {
    const z2 = 1 - vPos.dot(vPos);
    if (z2 > 0) {
      const z = Math.sqrt(z2);
      return gray2ASCII(new Vector4(z, 0, z, 1));
    }
    return gray2ASCII(new Vector4(0, 0, 0, 0));
  }),
);
/**
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                           Diffuse Reflection Model
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *
 * Next, we use the diffuse reflection model to make the sphere more realistic.
 * Given a directional light source, the intensity of light on a surface is
 * proportional to the cosine of the angle between the light direction and the
 * surface normal. For a sphere, the surface normal at point (x, y, z) is the
 * normalized vector `(x, y, z)`.
 *
 * Here we define a light direction `dir` (1, 1, 1). For each point `p`, we use
 * the dot product between `p` and `dir` to compute the light intensity.
 */
//➜
//➜             *+===~~----::::::
//➜          #*++==~~~----:::::...::
//➜       @%#**++==~~~---:::::........:
//➜      @%##**++==~~~----::::.........:
//➜    @@%%##**++===~~~---:::::.........::
//➜   @@@@%##**+++==~~~~----:::::........::
//➜  @@@@@%%##**+++==~~~~----:::::::....:::-
//➜  @@@@@%%###**+++===~~~-----::::::::::::-
//➜  @@@@@@%%###**+++===~~~~------::::::::--
//➜  @@@@@@@%%###***+++===~~~~~------------~
//➜  @@@@@@@@@%%##***++++====~~~~~~------~~~
//➜  @@@@@@@@@@%%###***++++=====~~~~~~~~~~==
//➜  @@@@@@@@@@@@%%###****+++++============+
//➜   @@@@@@@@@@@@@%%%###****+++++++++++++*
//➜    @@@@@@@@@@@@@@%%%#####************#
//➜      @@@@@@@@@@@@@@@%%%%############
//➜       @@@@@@@@@@@@@@@@@@%%%%%%%%%%@
//➜          @@@@@@@@@@@@@@@@@@@@@@@
//➜             @@@@@@@@@@@@@@@@@
//➜
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.
 */