mcpField
A
ascii-animation

ascii-animation

github.com/iart-aiクリエイティブ・メディア632.3k 回インストール18 週間前に更新
リポジトリ

ascii-animation について

Web、ターミナル、3D シーン向けの ASCII アニメーションスキルです。明るさから文字へのマッピング、生成アニメーション、画像・動画変換、Three.js 描画、再現可能なフレーム検証を扱います。

主な機能

  1. 01明るさを文字の密度に変換
  2. 02生成テキストアニメーションの描画
  3. 03画像や動画の ASCII 変換
  4. 04Three.js による 3D シーン描画
  5. 05ANSI コードによるターミナルアニメーション
  6. 06時間とシードを固定したフレーム再現

活用シーン

  1. 01ターミナルのローダー、バナー、イントロ制作
  2. 02動画フレームを ASCII アニメーションに変換
  3. 03Canvas や WebGL に文字エフェクトを追加
  4. 043D シーンを文字で描画

インストール

コマンドをコピーしてターミナルで実行します。

$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 で区切った 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

完了前に確認すること:

  1. スタンドアロンで開ける — コンソールエラーがなく、CDN(使用する場合は Three.js)が読み込まれ、等幅フォントが適用されている。
  2. ?t=N で決定論的なフレームが 1 つレンダーされる(時刻を注入し、シードを固定)。ライブのループは動作しない。
  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 — PNG/JPG ファイルを ASCII テキストに変換する、実行可能な Node スクリプト。--cols、--invert、--ramp フラグに対応しています。

コメント