我终于学会了黑客帝国里的矩阵雨

这篇博客介绍了如何使用ANSI控制序列在命令行中创建类似《黑客帝国》中的矩阵雨效果。文章详细讲解了控制字符、控制序列及其功能,包括移动光标、清空屏幕和设置显示效果。还展示了如何生成和渲染不同的字符,以及颜色的设定。最后,博主分享了一个名为MatrixRain的类,用于创建矩阵雨动画。
摘要由CSDN通过智能技术生成

bgBlue: () => ansi.color(44),

bgMagenta: () => ansi.color(45),

bgCyan: () => ansi.color(46),

bgWhite: () => ansi.color(47),

},

};

module.exports = ansi;

这里面 ansi 对象上的每一个方法不做过多解释了。我们看到,每个方法都是返回一个奇怪的字符串,通过这些字符串可以改变命令行的显示效果。

这些字符串其实是一个个控制字符组成的控制序列。那什么是控制字符呢?我们应该都知道 ASC 字符集,这个字符集里面除了定义了一些可见字符以外,还有很多不可见的字符,就是控制字符。这些控制字符可以控制打印机、命令行等设备的显示和动作。

有两个控制字符集,分别是 CO 字符集和 C1 字符集。C0 字符集是 0x00 到 0x1F 这两个十六进制数范围内的字符,而 C1 字符集是 0x80 到 0x9F 这两个十六进制数范围内的字符。C0 和 C1 字符集内的字符和对应的功能可以在[这里](()查到,我们不做详细描述了。

上面代码中,\x1b[ 其实是一个组合,\x1b 定义了 ESC 键,后跟 [ 表示这是一个[控制序列导入器(Control Sequence Introducer,CSI)](()。在 \x1b[ 后面的所有字符都会被命令行解析为控制字符。

常用的控制序列有这些:

| 序列 | 功能 |

| — | — |

| CSI n A | 向上移动 n(默认为 1) 个单元 |

| CSI n A | 向下移动 n(默认为 1) 个单元 |

| CSI n C | 向前移动 n(默认为 1) 个单元 |

| CSI n D | 向后移动 n(默认为 1) 个单元 |

| CSI n E | 将光标移动到 n(默认为 1) 行的下一行行首 |

| CSI n F | 将光标移动到 n(默认为 1) 行的前一行行首 |

| CSI n G | 将光标移动到当前行的第 n(默认为 1)列 |

|

CSI n ; m H

| 移动光标到指定位置,第 n 行,第 m 列。n 和 m 默认为 1,即 CSI ;5H 与 CSI 1;5H 等同。 |

| CSI n J | 清空屏幕。如果 n 为 0(或不指定),则从光标位置开始清空到屏幕末尾;如果 n 为 1,则从光标位置清空到屏幕开头;如果 n 为 2,则清空整个屏幕;如果 n 为 3,则不仅清空整个屏幕,同时还清空滚动缓存。 |

| CSI n K | 清空行,如果 n 为 0(或不指定),则从光标位置清空到行尾;如果 n 为 1,则从光标位置清空到行头;如果 n 为 2,则清空整行,光标位置不变。 |

| CSI n S | 向上滚动 n (默认为 1)行 |

| CSI n T | 向下滚动 n (默认为 1)行 |

| CSI n ; m f | 与 CSI n ; m H 功能相同 |

| CSI n m | 设置显示效果,如 CSI 1 m 表示设置粗体,CSI 4 m 为添加下划线。 |

我们可以通过 CSI n m 控制序列来控制显示效果,在设置一种显示以后,后续字符都会沿用这种效果,直到我们改变了显示效果。可以通过 CSI 0 m 来清楚显示效果。常见的显示效果可以在[SGR (Select Graphic Rendition) parameters](() parameters") 查到,这里受篇幅限制就不做赘述了。

上面的代码中,还定义了一些颜色,我们看到颜色的定义都是一些数字,其实每一个数字都对应一种颜色,这里列一下常见的颜色。

| 前景色 | 背景色 | 名称 | 前景色 | 背景色 | 名称 |

| — | — | — | — | — | — |

| 30 | 40 | 黑色 | 90 | 100 | 亮黑色 |

| 31 | 41 | 红色 | 91 | 101 | 亮红色 |

| 32 | 42 | 绿色 | 92 | 102 | 亮绿色 |

| 33 | 43 | 黄色 | 93 | 103 | 亮黄色 |

| 34 | 44 | 蓝色 | 94 | 104 | 亮蓝色 |

| 35 | 45 | 品红色(Magenta) | 95 | 105 | 亮品红色(Magenta) |

| 36 | 46 | 青色(Cyan) | 96 | 106 | 亮青色(Cyan) |

| 37 | 47 | 白色 | 97 | 107 | 亮白色 |

上面的代码中,使用了 CSI n;1m 的形式来定义颜色,其实是两种效果的,一个是具体颜色值,一个是加粗,一些命令行实现中会使用加粗效果来定义亮色。比如,如果直接定义 CSI 32 m 可能最终展示的是暗绿色,我们改成 CSI 32;1m 则将显示亮绿色。

颜色支持多种格式,上面的是 [3-bit 和 4-bit](() 格式,同时还有 [8-bit](() 和 [24-bit](()。代码中也有使用样例,这里不再赘述了。

矩阵渲染


在 matrix-rain 的代码中,index.js 里的核心功能是 MatrixRain 这个类:

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.colorsfg${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});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值