I finally learned the matrix rain in the matrix

Time:2022-5-26

I believe everyone is very impressed by the matrix rain in the matrix movie, which is the following effect.

I finally learned the matrix rain in the matrix

The effect is very cool. I took a look at the code of the relevant implementation library. It is also very simple. The core is to use the control characters of the command line well. Let’s share it here.

staymatrix-rainThere are only two files in the source code of,ansi.jsandindex.js, very small.

Control character and control sequence

ansi.jsSome command line operation methods are defined in, that is, some method encapsulation is made for the control characters, and the code is as follows:

const ctlEsc = `\x1b[`;
const ansi = {
  reset: () => `${ctlEsc}c`,
  clearScreen: () => `${ctlEsc}2J`,
  cursorHome: () => `${ctlEsc}H`,
  cursorPos: (row, col) => `${ctlEsc}${row};${col}H`,
  cursorVisible: () => `${ctlEsc}?25h`,
  cursorInvisible: () => `${ctlEsc}?25l`,
  useAltBuffer: () => `${ctlEsc}?47h`,
  useNormalBuffer: () => `${ctlEsc}?47l`,
  underline: () => `${ctlEsc}4m`,
  off: () => `${ctlEsc}0m`,
  bold: () => `${ctlEsc}1m`,
  color: c => `${ctlEsc}${c};1m`,

  colors: {
    fgRgb: (r, g, b) => `${ctlEsc}38;2;${r};${g};${b}m`,
    bgRgb: (r, g, b) => `${ctlEsc}48;2;${r};${g};${b}m`,
    fgBlack: () => ansi.color(`30`),
    fgRed: () => ansi.color(`31`),
    fgGreen: () => ansi.color(`32`),
    fgYellow: () => ansi.color(`33`),
    fgBlue: () => ansi.color(`34`),
    fgMagenta: () => ansi.color(`35`),
    fgCyan: () => ansi.color(`36`),
    fgWhite: () => ansi.color(`37`),
    bgBlack: () => ansi.color(`40`),
    bgRed: () => ansi.color(`41`),
    bgGreen: () => ansi.color(`42`),
    bgYellow: () => ansi.color(`43`),
    bgBlue: () => ansi.color(`44`),
    bgMagenta: () => ansi.color(`45`),
    bgCyan: () => ansi.color(`46`),
    bgWhite: () => ansi.color(`47`),
  },
};

module.exports = ansi;

In hereansiEach method on the object is not explained too much. We can see that each method returns a strange string, which can change the display effect of the command line.

These strings are actually a control sequence composed of control characters. What are control characters? We should all know the ASC character set. In this character set, in addition to defining some visible characters, there are many invisible characters, namely control characters. These control characters can control the display and action of printers, command lines and other devices.

There are two control character sets, CO character set and C1 character set. C0 character set is0x00reach0x1FThese two hexadecimal numbers are characters in the range, while the C1 character set is0x80reach0x9FCharacters in the range of these two hexadecimal numbers. The characters and corresponding functions in C0 and C1 character sets can behereYes, we won’t describe it in detail.

In the code above,\x1b[It’s actually a combination,\x1bDefinedESCKey, heel[Indicates that this is aControl sequence importer (CSI)。 stay\x1b[All subsequent characters will be parsed into control characters by the command line.

Common control sequences are:

sequence function
CSI n A Move up n (default 1) cells
CSI n A Move down n (default 1) cells
CSI n C Move n (default 1) cells forward
CSI n D Move back n (default 1) cells
CSI n E Move the cursor to the beginning of the next line of line n (default: 1)
CSI n F Move the cursor to the beginning of the previous line of line n (1 by default)
CSI n G Move the cursor to column n (default: 1) of the current row
CSI n ; m H Move the cursor to the specified position, row n, column M. N and M default to 1, i.e. CSI; 5h and CSI 1; 5h equivalent.
CSI n J Clear the screen. If n is 0 (or not specified), clear from the cursor position to the end of the screen; If n is 1, clear from the cursor position to the beginning of the screen; If n is 2, the entire screen is cleared; If n is 3, not only the entire screen is emptied, but also the scroll cache is emptied.
CSI n K Clear the line. If n is 0 (or not specified), clear from the cursor position to the end of the line; If n is 1, it is cleared from the cursor position to the beginning of the line; If n is 2, the whole line is cleared and the cursor position remains unchanged.
CSI n S Scroll up n (default = 1) rows
CSI n T Scroll down n (default = 1) rows
CSI n ; m f AndCSI n ; m HSame function
CSI n m Set the display effect, such asCSI 1 mIndicates that bold is set,CSI 4 mUnderline.

We can passCSI n mControl sequence to control the display effect. After setting a display, subsequent characters will continue to use this effect until we change the display effect. Can passCSI 0 mTo clearly show the effect. Common display effects can be displayed inSGR (Select Graphic Rendition) parametersI found that due to space constraints, I won’t repeat it here.

In the above code, some colors are also defined. We can see that the definitions of colors are all numbers. In fact, each number corresponds to a color. Here are some common colors.

Foreground Background color name Foreground Background color name
30 40 black 90 100 Bright black
31 41 gules 91 101 Bright red
32 42 green 92 102 Bright green
33 43 yellow 93 103 Bright yellow
34 44 blue 94 104 Bright blue
35 45 Magenta 95 105 Bright magenta
36 46 Cyan 96 106 Bright cyan
37 47 white 97 107 Bright white

In the above code, theCSI n;1mIn fact, there are two effects to define color in the form of, one is the specific color value, and the other is bold. Some command line implementations will use bold effect to define bright color. For example, if you directly defineCSI 32 mMaybe the final display is dark green. Let’s change it toCSI 32;1mA bright green is displayed.

Colors support multiple formats. The above is3-bit and 4-bitFormat, and8-bitand24-bit。 There are also use examples in the code, which will not be repeated here.

Matrix rendering

In the matrix rain code,index.jsThe core function of the isMatrixRainThis class:

class MatrixRain {
  constructor(opts) {
    this.transpose = opts.direction === `h`;
    this.color = opts.color;
    this.charRange = opts.charRange;
    this.maxSpeed = 20;
    this.colDroplets = [];
    this.numCols = 0;
    this.numRows = 0;

    // handle reading from file
    if (opts.filePath) {
      if (!fs.existsSync(opts.filePath)) {
        throw new Error(`${opts.filePath} doesn't exist`);
      }
      this.fileChars = fs.readFileSync(opts.filePath, `utf-8`).trim().split(``);
      this.filePos = 0;
      this.charRange = `file`;
    }
  }

  generateChars(len, charRange) {
    // by default charRange == ascii
    let chars = new Array(len);

    if (charRange === `ascii`) {
      for (let i = 0; i < len; i++) {
        chars[i] = String.fromCharCode(rand(0x21, 0x7E));
      }
    } else if (charRange === `braille`) {
      for (let i = 0; i < len; i++) {
        chars[i] = String.fromCharCode(rand(0x2840, 0x28ff));
      }
    } else if (charRange === `katakana`) {
      for (let i = 0; i < len; i++) {
        chars[i] = String.fromCharCode(rand(0x30a0, 0x30ff));
      }
    } else if (charRange === `emoji`) {
      // emojis are two character widths, so use a prefix
      const emojiPrefix = String.fromCharCode(0xd83d);
      for (let i = 0; i < len; i++) {
        chars[i] = emojiPrefix + String.fromCharCode(rand(0xde01, 0xde4a));
      }
    } else if (charRange === `file`) {
      for (let i = 0; i < len; i++, this.filePos++) {
        this.filePos = this.filePos < this.fileChars.length ? this.filePos : 0;
        chars[i] = this.fileChars[this.filePos];
      }
    }

    return chars;
  }

  makeDroplet(col) {
    return {
      col,
      alive: 0,
      curRow: rand(0, this.numRows),
      height: rand(this.numRows / 2, this.numRows),
      speed: rand(1, this.maxSpeed),
      chars: this.generateChars(this.numRows, this.charRange),
    };
  }

  resizeDroplets() {
    [this.numCols, this.numRows] = process.stdout.getWindowSize();

    // transpose for direction
    if (this.transpose) {
      [this.numCols, this.numRows] = [this.numRows, this.numCols];
    }

    // Create droplets per column
    // add/remove droplets to match column size
    if (this.numCols > this.colDroplets.length) {
      for (let col = this.colDroplets.length; col < this.numCols; ++col) {
        // make two droplets per row that start in random positions
        this.colDroplets.push([this.makeDroplet(col), this.makeDroplet(col)]);
      }
    } else {
      this.colDroplets.splice(this.numCols, this.colDroplets.length - this.numCols);
    }
  }

  writeAt(row, col, str, color) {
    // Only output if in viewport
    if (row >=0 && row < this.numRows && col >=0 && col < this.numCols) {
      const pos = this.transpose ? ansi.cursorPos(col, row) : ansi.cursorPos(row, col);
      write(`${pos}${color || ``}${str || ``}`);
    }
  }

  renderFrame() {
    const ansiColor = ansi.colors[`fg${this.color.charAt(0).toUpperCase()}${this.color.substr(1)}`]();

    for (const droplets of this.colDroplets) {
      for (const droplet of droplets) {
        const {curRow, col: curCol, height} = droplet;
        droplet.alive++;

        if (droplet.alive % droplet.speed === 0) {
          this.writeAt(curRow - 1, curCol, droplet.chars[curRow - 1], ansiColor);
          this.writeAt(curRow, curCol, droplet.chars[curRow], ansi.colors.fgWhite());
          this.writeAt(curRow - height, curCol, ` `);
          droplet.curRow++;
        }

        if (curRow - height > this.numRows) {
          // reset droplet
          Object.assign(droplet, this.makeDroplet(droplet.col), {curRow: 0});
        }
      }
    }

    flush();
  }
}

There are also several tools and methods:

// Simple string stream buffer + stdout flush at once
let outBuffer = [];
function write(chars) {
  return outBuffer.push(chars);
}

function flush() {
  process.stdout.write(outBuffer.join(``));
  return outBuffer = [];
}

function rand(start, end) {
  return start + Math.floor(Math.random() * (end - start));
}

The startup code of matrix rain is as follows:

const args = argParser.parseArgs();
const matrixRain = new MatrixRain(args);

function start() {
  if (!process.stdout.isTTY) {
    console.error(`Error: Output is not a text terminal`);
    process.exit(1);
  }

  // clear terminal and use alt buffer
  process.stdin.setRawMode(true);
  write(ansi.useAltBuffer());
  write(ansi.cursorInvisible());
  write(ansi.colors.bgBlack());
  write(ansi.colors.fgBlack());
  write(ansi.clearScreen());
  flush();
  matrixRain.resizeDroplets();
}

function stop() {
  write(ansi.cursorVisible());
  write(ansi.clearScreen());
  write(ansi.cursorHome());
  write(ansi.useNormalBuffer());
  flush();
  process.exit();
}

process.on(`SIGINT`, () => stop());
process.stdin.on(`data`, () => stop());
process.stdout.on(`resize`, () => matrixRain.resizeDroplets());
setInterval(() => matrixRain.renderFrame(), 16); // 60FPS

start();

First initialize aMatrixRainClass, and then callstartmethod.startMethod passedMatrixRainofresizeDropletsMethod to initialize the content to be displayed.

MatrixRainClass instance manages acolDropletsArray to save the raindrops in each column. stayresizeDropletsWe can see that there are two raindrops in each column.

In the startup code, we can also see that it is called every 16 millisecondsrenderFrameMethod to draw the page. andrenderFrameMethod will traverse eachcolDropletEvery raindrop in the. Since the initial position and velocity of each raindrop are random, throughdroplet.aliveanddroplet.speedTo determine whether to update the raindrop position during each rendering, so as to achieve the effect of uneven falling of each raindrop. When the raindrop has moved out of the visual range of the screen, it will be reset.

Every rendering is throughwriteFunction to write data to the global cache, and thenflushFunction one update.

Common interview knowledge points, technical solutions and tutorials can be obtained by scanning the code and following the official account “Zhongli Qianxun”, or come herehttps://everfind.github.io

I finally learned the matrix rain in the matrix

Let’s grow together~