mcpField
A
ascii-animation

ascii-animation

github.com/iart-ai创意与媒体632.3k 次安装18 周前更新
代码仓库

关于 ascii-animation

面向网页、终端和 3D 场景创作者的 ASCII 动画技能,讲解如何将亮度映射为字符,并生成式动画、图像或视频转换及 Three.js 场景渲染。还涵盖终端输出与可重复帧验证。

主要功能

  1. 01按亮度映射字符密度
  2. 02渲染生成式文本动画
  3. 03将图像和视频转换为 ASCII
  4. 04通过 Three.js 渲染 3D 场景
  5. 05使用 ANSI 序列制作终端动画
  6. 06固定时间与随机种子复现帧

适用场景

  1. 01制作终端加载动画、横幅或片头
  2. 02将视频帧转为 ASCII 动画
  3. 03为 Canvas 或 WebGL 添加字符效果
  4. 04把 3D 场景渲染为字符画

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/iart-ai/web-animation-skills --skill ascii-animation

安装后在 Claude Code 中输入/ascii-animation确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
references/
rendering.md
scripts/
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

完成前:

  1. 可独立打开——无控制台错误,CDN(如使用 Three.js)加载成功,已应用等宽字体。
  2. ?t=N 会渲染一帧确定性画面(注入时间 + 固定种子),且不会持续循环。
  3. 已在 3 个模拟时间点截图——符合需求,没有纵向拉伸或反色错误。
  4. 遵循 prefers-reduced-motion 设置——停止/减慢 rAF 循环,并显示静态帧。
  5. 缓动经过刻意设计——有意限制帧率(复古风格可设为 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 参数。

评论

支持 Markdown