关于 ascii-animation
面向网页、终端和 3D 场景创作者的 ASCII 动画技能,讲解如何将亮度映射为字符,并生成式动画、图像或视频转换及 Three.js 场景渲染。还涵盖终端输出与可重复帧验证。
主要功能
- 01按亮度映射字符密度
- 02渲染生成式文本动画
- 03将图像和视频转换为 ASCII
- 04通过 Three.js 渲染 3D 场景
- 05使用 ANSI 序列制作终端动画
- 06固定时间与随机种子复现帧
适用场景
- 01制作终端加载动画、横幅或片头
- 02将视频帧转为 ASCII 动画
- 03为 Canvas 或 WebGL 添加字符效果
- 04把 3D 场景渲染为字符画
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ iart-ai/ web-animation-skills --skill ascii-animation安装后在 Claude Code 中输入/ascii-animation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdreferences/rendering.mdscripts/img-to-ascii.mjs详细介绍
ASCII 动画
完全使用文本字符渲染动态效果。ASCII 输出极其轻量、风格鲜明,可用于浏览器(<pre>/canvas)、终端(ANSI),以及任何 3D 场景(Three.js AsciiEffect)。
适用场景
- 构建终端/CLI 开场动画、加载动画、横幅,或营造复古/黑客风格。
- 将图像、视频帧或 3D 场景转换为动态 ASCII。
- 在现有 canvas/WebGL 渲染上添加 ASCII 后期效果。
- 创建生成式文本场(等离子、正弦波、噪声、隧道)。
核心概念:亮度字符梯度
将亮度(0..1)映射到墨迹密度相匹配的字符。按从深到浅排列字符。使用 Math.round(lum * (ramp.length - 1)) 获取索引。
常见字符梯度(从深到浅):
- 短(10):
.:-=+*#%@ - 中等(16):
.'\^",:;Il!i><~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$` 已截断——见下表。 - 标准 70 级(Paul Bourke),最适合照片:
$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,"^`'.(反转后用于深色文字绘制在浅色背景上)。
根据 sRGB 计算相对亮度(符合感知特性):
const lum = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; // 0..1
在浅色背景上绘制深色文字时进行反转:lum = 1 - lum。
字符单元格宽高比校正(最容易踩的坑)
等宽字体的字符单元格高度大于宽度——大约为 宽:高 = 0.5。对正方形像素网格进行采样会导致图像在垂直方向拉伸。校正方法是采样少于列数的行数:目标宽度为 cols 个字符时,使用 rows = Math.round(cols * (imgH / imgW) * fontAspect),其中 fontAspect ≈ 0.5。等效地,在绘制到离屏 canvas 时,将其高度设为 cols * aspect * 0.5。
Web 渲染:<pre> 与 canvas
<pre>+ textContent:最简单。每行用\n分隔,组成一个字符串。以 30fps 运行时,可处理约 120×60 个字符。设置white-space: pre; font-family: monospace; line-height: 1;。- Canvas
fillText:需要为每个字符设置颜色、使用更大的网格或达到 60fps 时使用。将每个字符绘制在x * cellW, y * cellH处。比创建成千上万个 DOM 节点更快。
生成式 <pre> 文本场(等离子):
const pre = document.querySelector('pre');
const COLS = 100, ROWS = 50, ramp = ' .:-=+*#%@';
function frame(t) {
let out = '';
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
const v = Math.sin(x * 0.2 + t * 0.001)
+ Math.sin(y * 0.3 + t * 0.0013)
+ Math.sin((x + y) * 0.15 + t * 0.0007);
const lum = (v + 3) / 6; // normalize -3..3 to 0..1
out += ramp[Math.round(lum * (ramp.length - 1))];
}
out += '\n';
}
pre.textContent = out;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
将图像/视频转换为 ASCII
将源内容绘制到尺寸为 cols × rows 的小型离屏 canvas 上,使用 getImageContext().getImageData 读取像素,然后将每个像素映射为一个字符。对于视频,从 <video> 元素逐帧重复此操作。
function videoToAscii(video, cols = 120) {
const aspect = video.videoHeight / video.videoWidth;
const rows = Math.round(cols * aspect * 0.5); // cell aspect correction
const cv = document.createElement('canvas');
cv.width = cols; cv.height = rows;
const ctx = cv.getContext('2d', { willReadFrequently: true });
const ramp = ' .:-=+*#%@', pre = document.querySelector('pre');
(function tick() {
ctx.drawImage(video, 0, 0, cols, rows);
const { data } = ctx.getImageData(0, 0, cols, rows);
let out = '';
for (let i = 0; i < data.length; i += 4) {
const lum = (0.2126*data[i] + 0.7152*data[i+1] + 0.0722*data[i+2]) / 255;
out += ramp[Math.round(lum * (ramp.length - 1))];
if (((i / 4) + 1) % cols === 0) out += '\n';
}
pre.textContent = out;
requestAnimationFrame(tick);
})();
}
对于 Node 图像转 ASCII,请参阅 scripts/img-to-ascii.mjs。
使用 Three.js 将 3D 转换为 ASCII
AsciiEffect 封装渲染器,并将任意场景以 ASCII 形式渲染到 DOM 元素中。应通过该效果进行渲染,而非直接使用渲染器。
import { AsciiEffect } from 'three/addons/effects/AsciiEffect.js';
const effect = new AsciiEffect(renderer, ' .:-=+*#%@', { invert: true });
effect.setSize(innerWidth, innerHeight);
effect.domElement.style.color = '#0f0';
effect.domElement.style.backgroundColor = 'black';
document.body.appendChild(effect.domElement);
// in loop: effect.render(scene, camera); // NOT renderer.render
终端/CLI 动画
使用 ANSI 转义码循环播放:隐藏光标、移动到起始位置、打印帧,并将帧率限制在 12–24 fps。退出时重新显示光标。
const ESC = '\x1b[';
process.stdout.write(ESC + '?25l'); // hide cursor
function frame(t) {
process.stdout.write(ESC + 'H'); // cursor to top-left
// build and write rows...
}
const id = setInterval(() => frame(Date.now()), 1000 / 20);
process.on('SIGINT', () => {
clearInterval(id);
process.stdout.write(ESC + '?25h' + ESC + '2J'); // show cursor, clear
process.exit();
});
使用 ESC + '2J' 清除整个屏幕,使用 ESC + 'H' 返回起始位置(每帧开销比清屏更低)。使用 \x1b[38;2;R;G;Bm 设置颜色(真彩色),并使用 \x1b[0m 重置颜色。
交付与验证(独立 HTML)
打包的辅助脚本(
scripts/):scripts/seek-shot.sh anim.html 0 1.5 3会冻结?t=N测试框架并截取每个时刻的画面;scripts/contact-sheet.sh sheet.png frame-*.png会将这些截图拼成一张总览图,方便快速检查。参见scripts/README.md。
对于自包含的 Web ASCII 作品(生成式场景、图像/视频→ASCII、AsciiEffect 场景),交付物应是一个可直接在浏览器中打开的 HTML 文件——<pre>/canvas、渐变字符表和 rAF 循环均内联(如使用 Three.js,则从 CDN 加载)。无需构建步骤。单文件是合适的选择;不要使用打包工具。(终端/CLI 作品的验证方式不同——捕获 stdout 或终端截图。)
输出要求:
- 一个
.html文件:渲染目标、亮度渐变字符表和requestAnimationFrame循环都放在同一个内联<script>中。 - 使用可注入的时间来驱动帧(不要直接使用
Date.now()/performance.now()),并为任何随机过程设置种子,以便复现某一帧。
定位测试框架——在确定性帧上冻结 rAF 循环。 ?t=N 会在模拟时间 N 时恰好渲染一帧,而不是循环播放,因此截图可以复现。在循环读取时间的位置传入 N,并固定随机种子:
<script>
const t = new URLSearchParams(location.search).get("t");
// your frame(time){…} reads `time`, not Date.now(); RNG uses a fixed seed
if (t !== null) { frame(parseFloat(t)); } // render ONE frame, no rAF
else { (function loop(now){ frame(now); requestAnimationFrame(loop); })(0); }
window.__ready = true;
</script>
验证循环——渲染 → 冻结 → 截图 → 检查:在几个模拟时间点渲染(?t=0、?t=1000、?t=2000),分别截图,并检查保真度(渐变字符表是否能正确呈现由暗到亮的变化、动画是否随时间演变)和瑕疵(缺少字符单元格宽高比校正导致的纵向拉伸、浅色背景下反色错误、网格被裁切、等宽字体加载前出现 FOUC——字体加载完成后字符单元格尺寸才会稳定,因此请等待)。可使用任何无头工具:
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/ascii.html?t=1000" frame-mid.png
完成前:
- 可独立打开——无控制台错误,CDN(如使用 Three.js)加载成功,已应用等宽字体。
?t=N会渲染一帧确定性画面(注入时间 + 固定种子),且不会持续循环。- 已在 3 个模拟时间点截图——符合需求,没有纵向拉伸或反色错误。
- 遵循
prefers-reduced-motion设置——停止/减慢 rAF 循环,并显示静态帧。 - 缓动经过刻意设计——有意限制帧率(复古风格可设为 12–24fps),渐变字符表的顺序经过仔细安排。
快速参考
| 需求 | 方法 |
|---|---|
| 简单的生成式场景 | <pre> + textContent,正弦/噪声 → 渐变字符表 |
| 照片保真度 | 70 级 Bourke 渐变字符表,使用 sRGB 权重计算亮度 |
| 每字符着色 / 60fps | 对每个单元格调用 Canvas fillText |
| 视频 | <video> → 离屏 canvas → 每帧调用 getImageData |
| 3D 场景 | Three.js AsciiEffect.render(scene, camera) |
| 终端 | 使用 ANSI \x1b[H 回到行首 + 限制为 12–24fps,隐藏光标 |
| 宽高比校正 | rows = cols * imgAspect * 0.5 |
参考文件
references/rendering.md— 完整的亮度渐变字符表(10/70/扩展版)、像素采样计算与字符单元格宽高比校正、<pre>与 canvas 的权衡及代码示例、支持真彩色的 ANSI 终端帧循环,以及完整的AsciiEffect接线方式。scripts/img-to-ascii.mjs— 可运行的 Node 脚本,可将 PNG/JPG 文件转换为 ASCII 文本,并提供--cols、--invert和--ramp参数。