浏览器和终端中的 ASCII 艺术
这个想法很简单:获取一张图像,测量每个像素的亮度,然后用字符替换它。像 @ 或 # 这样密集的字符代表暗区,而像 . 或 这样较轻的字符代表亮区。将足够多的这些替换在网格中串联起来,你就能得到一个完全由文本组成的可识别图像。
这个核心思想同时支撑着终端中的 ASCII 艺术和浏览器中的 ASCII 艺术,而现代实现已经将其推向了远超经典 7 位 ASCII 的境界。
核心要点
- ASCII 艺术将像素亮度映射到排序字符调色板中的字符,其中像
@这样的密集字形代表暗区,像.这样的稀疏字形代表亮区 - Unicode 块元素和盲文图案提供了比传统 ASCII 字符调色板显著更精细的分辨率
- 浏览器实现依赖于
<canvas>像素数据和等宽 CSS,而终端渲染器使用 ANSI 转义序列来实现颜色 - 终端能力差异很大——从基本的 16 色 ANSI 到 24 位真彩色、SIXEL 以及 Kitty 图形协议
像素到字符的映射工作原理
每个 ASCII 渲染器都遵循大致相同的流程:
- 将源图像或视频帧采样到单元格网格中
- 计算每个单元格的平均亮度(luminance)
- 将该亮度值映射到排序调色板中的字符
- 输出生成的字符网格,可选择性地添加颜色
一个基本的 JavaScript 实现如下所示:
const palette = ' .:-=+*#%@';
function brightnessToChar(brightness) {
const index = Math.floor((brightness / 255) * (palette.length - 1));
return palette[index];
}
这里,brightness 预期是一个介于 0(黑色)和 255(白色)之间的值。该函数将其标准化为调色板字符串中的索引,其中较早的字符代表较浅的色调,较后的字符代表较深的色调。
输出质量在很大程度上取决于你的字符调色板。经典 ASCII 能提供大约 10-15 个有用的阴影级别。现代实现做得更好。
超越 ASCII:使用 Unicode 的文本模式图形
使用 Unicode 的文本模式图形解锁了显著更精细的分辨率。你可以使用以下字符,而不是稀疏的 ASCII 调色板:
- 块元素(
█ ▓ ▒ ░)为每个单元格提供四个阴影级别 - 盲文图案(
⣿ ⠿ ⠛)在单个字符中编码最多 8 个点,允许比传统 ASCII 调色板更高的有效分辨率 - 制表符用于结构化布局和边框
Chafa 是一个终端图像查看器,它使用 Unicode 块和盲文字符在终端中以接近照片级的质量渲染图像。与纯 ASCII 相比,差异非常显著。
Discover how at OpenReplay.com.
浏览器中的 ASCII 艺术:Canvas 和 WebGL 方法
基于浏览器的 JavaScript ASCII 渲染通常使用 <canvas> 元素作为图像源。流程如下:
- 将图像或视频帧绘制到离屏 canvas
- 使用
ctx.getImageData()读取像素数据 - 采样像素网格并将每个单元格映射到字符
- 将结果渲染到
<pre>元素中,或使用fillText()绘制回 canvas
像 p5.js 这样的库使这一过程变得简单,让你能够实时处理实时网络摄像头源或视频帧。为了获得更高的性能,WebGL 着色器可以在 GPU 上处理亮度采样和字符查找,这在以 30 fps 渲染完整视频时很重要。
对于静态显示,HTML/CSS 设置很简单但很重要:
.ascii-art {
font-family: monospace;
white-space: pre;
line-height: 1;
letter-spacing: 0;
}
没有 white-space: pre,浏览器会折叠空格并破坏布局。没有等宽字体,字符宽度不等,网格就会崩溃。设置 letter-spacing: 0 可以防止某些浏览器默认引入的细微水平间隙。
终端 ASCII 图形:ANSI 颜色和协议差异性
在终端环境中,ASCII 或 Unicode 字符被写入标准输出。通过 ANSI 转义序列添加颜色。现代终端支持 24 位(真彩色)输出:
printf '\033[38;2;255;100;0m%s\033[0m\n' "orange text"
这使用 38;2;R;G;B 序列将前景色设置为 RGB(255, 100, 0),然后使用 \033[0m 重置格式。
像 jp2a 和 ascii-image-converter 这样的工具使用这种方法从图像文件生成彩色终端 ASCII 图形。
然而,终端能力差异很大。一些终端仅支持基本的 16 色 ANSI。其他终端支持 256 色模式、完整的 24 位真彩色、SIXEL 图形或 Kitty 图形协议,后者可以在终端内渲染实际的像素图像,而不是字符近似。Unicode 宽度处理在不同终端之间也有所不同,因此盲文或宽字符可能会因环境而错位。
结论
无论你是为浏览器编写 JavaScript ASCII 渲染器,还是通过终端工具传输图像数据,底层逻辑都是相同的:像素变成字符,亮度变成密度,颜色变成转义码或 CSS。浏览器为你提供对字体和布局的精确控制,而终端则提供速度和可组合性。环境的选择决定了工具,但转换流程——采样、测量、映射、渲染——保持不变。
常见问题
大多数等宽字体字符的高度大于宽度,因此每个字符单元格不是完美的正方形。这种垂直拉伸会扭曲图像。你可以通过在 CSS 中调整 line-height、在转换前将源图像缩放为非正方形纵横比,或相对于列采样更少的行来进行补偿。
按视觉密度从浅到深对字符进行排序。更长的调色板可以提供更多的阴影级别和更平滑的渐变。通过渲染渐变图像并检查可见的条带来测试你的调色板。Unicode 块元素和盲文图案比标准 ASCII 字符提供更精细的粒度。
可以。在浏览器中,将每个视频帧绘制到离屏 canvas,读取像素数据,并在每个动画帧上将单元格映射到字符。为了在 30 fps 下获得可接受的性能,保持字符网格分辨率适中,或将亮度计算卸载到 WebGL 着色器。像 Chafa 这样的终端工具也支持视频输入。
盲文字符是 Unicode,其渲染宽度取决于终端模拟器和活动字体。一些终端将它们视为宽字符,其他终端则视为窄字符。如果你的输出看起来有问题,请尝试使用不同的终端或字体。像 Chafa 这样的工具会检测终端能力并相应地调整其输出模式。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.