ascii-animation について
Web、ターミナル、3D シーン向けの ASCII アニメーションスキルです。明るさから文字へのマッピング、生成アニメーション、画像・動画変換、Three.js 描画、再現可能なフレーム検証を扱います。
主な機能
- 01明るさを文字の密度に変換
- 02生成テキストアニメーションの描画
- 03画像や動画の ASCII 変換
- 04Three.js による 3D シーン描画
- 05ANSI コードによるターミナルアニメーション
- 06時間とシードを固定したフレーム再現
活用シーン
- 01ターミナルのローダー、バナー、イントロ制作
- 02動画フレームを ASCII アニメーションに変換
- 03Canvas や WebGL に文字エフェクトを追加
- 043D シーンを文字で描画
インストール
コマンドをコピーしてターミナルで実行します。
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で区切った 1 つの文字列を使います。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 への変換
ソースを小さなオフスクリーン canvas に cols × rows のサイズで描画し、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〜24fps に制限します。終了時にカーソルを再表示します。
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 ファイル 1 つです。<pre>/canvas、ランプ、rAF ループをインラインにします(使用する場合は CDN の Three.js を使用します)。ビルド工程は不要です。適切なのはファイル 1 つです。バンドラーを使う必要はありません。(ターミナル/CLI の作品は検証方法が異なります。stdout またはターミナルのスクリーンショットを取得してください。)
出力要件:
.htmlファイル 1 つに、レンダー対象、輝度ランプ、requestAnimationFrameループを 1 つのインライン<script>にまとめます。- フレームは注入可能な時刻を基準にし(
Date.now()/performance.now()を直接使わないでください)、乱数を使う場合はシードを固定して、フレームを再現可能にします。
シーク用ハーネス — rAF ループを決定論的なフレームで固定します。 ?t=N はループ再生の代わりに、シミュレーション時刻 N のフレームを正確に 1 つレンダーするため、スクリーンショットを再現できます。ループが時刻を読み取る箇所に 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で決定論的なフレームが 1 つレンダーされる(時刻を注入し、シードを固定)。ライブのループは動作しない。- 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— PNG/JPG ファイルを ASCII テキストに変換する、実行可能な Node スクリプト。--cols、--invert、--rampフラグに対応しています。