JavaScriptでASCIIアートを生成する方法

はじめに

「Bad Apple!!」のような文字アート(ASCIIアート)は、オンライン上でよく見かけます。多くのツールが存在しますが、プログラマーならば自ら作ってみたくなるのが人情です。今回は、JavaScriptを使って画像や動画をASCIIアートに変換する方法を解説します。

基本原理

ASCIIアートの生成原理は比較的シンプルです。主なステップは以下の通りです。

  1. 文字セットの準備: 輝度(明るさ)の異なる文字のセットを用意します。例えば、['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.'] のような配列です。これらの文字は、輝度の高い順に並べる必要があります。
  2. グレースケール変換: 元の画像をグレースケール(白黒)に変換します。これは、各ピクセルのRGB値を加重平均して輝度値を計算することで実現します。一般的な加重係数は、人間の視覚の感度に基づいており、0.299, 0.587, 0.114 または 0.3086, 0.6094, 0.0820 などが使われます。
  3. 文字マッピング: グレースケール画像の各ピクセルの輝度値を、準備した文字セットのどの文字に対応させるかを決定します。輝度が低い(暗い)ほど、文字セットの先頭に近い「濃い」文字が選ばれます。
  4. (オプション)カラー保持: グレースケール変換をせず、元のRGB値をそのまま使って文字の色を設定することで、カラーのASCIIアートも生成できます。

HTML構造

まず、基本的なHTML構造を準備します。画像を表示し、生成されたASCIIアートを表示するコンテナを用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ASCII Art Generator</title>
    <style>
        body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f0f0f0; }
        #original-image, #ascii-container { max-width: 100%; height: auto; }
        #ascii-container { font-family: monospace; white-space: pre; line-height: 1; }
    </style>
</head>
<body>
    <img id="original-image" src="example.png" alt="Original Image">
    <div id="ascii-container"></div>
    <script src="script.js"></script>
</body>
</html>

JavaScriptによる実装(グレースケール版)

次に、JavaScriptコードを実装します。ここでは、まずグレースケールのASCIIアートを生成する基本的な例を示します。

const asciiContainer = document.getElementById('ascii-container');
const originalImage = document.getElementById('original-image');

const asciiChars = ['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.'];
const samplingInterval = 4; // ピクセルのサンプリング間隔

// 画像が読み込まれた後に処理を開始
originalImage.onload = function() {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d', { alpha: false });

    canvas.width = originalImage.naturalWidth;
    canvas.height = originalImage.naturalHeight;

    ctx.drawImage(originalImage, 0, 0);
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;

    let asciiArt = '';
    for (let y = 0; y < canvas.height; y += samplingInterval) {
        for (let x = 0; x < canvas.width; x += samplingInterval) {
            const index = (y * canvas.width + x) * 4;
            const r = data[index];
            const g = data[index + 1];
            const b = data[index + 2];

            // RGBを輝度に変換
            const brightness = 0.299 * r + 0.587 * g + 0.114 * b;

            // 輝度に基づいて文字を選択
            const charIndex = Math.floor((brightness / 255) * (asciiChars.length - 1));
            asciiArt += asciiChars[charIndex];
        }
        asciiArt += '\n'; // 行の終わり
    }

    asciiContainer.textContent = asciiArt;
};

// 画像がキャッシュされている場合も処理を保証
if (originalImage.complete) {
    originalImage.onload();
}

パフォーマンスの最適化

上記のコードはDOM操作を多用するため、大きな画像や動画ではパフォーマンスが低下します。より高速なレンダリングのために、Canvas APIを使用する方法が推奨されます。また、フォントサイズの調整も重要です。ブラウザによっては12px未満のフォントがサポートされない場合があるため、CSSの`transform: scale()`を利用して拡大縮小するテクニックが有効です。

JavaScriptによる実装(カラーCanvas版)

カラーを保持したASCIIアートを生成するには、各文字を個別の``タグで囲むよりも、Canvasを使って一度に描画する方が圧倒的に高速です。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Color ASCII Art</title>
    <style>
        body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f0f0f0; }
        #ascii-canvas { image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; }
    </style>
</head>
<body>
    <img id="source-image" src="example.png" alt="Source Image" style="display:none;">
    <canvas id="ascii-canvas"></canvas>
    <script src="color-ascii.js"></script>
</body>
</html>
const sourceImage = document.getElementById('source-image');
const asciiCanvas = document.getElementById('ascii-canvas');
const ctx = asciiCanvas.getContext('2d', { alpha: false });

const asciiChars = ['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.'];
const samplingInterval = 6;
const fontSize = 10;

sourceImage.onload = function() {
    const canvas = document.createElement('canvas');
    const sourceCtx = canvas.getContext('2d', { alpha: false });
    canvas.width = sourceImage.naturalWidth;
    canvas.height = sourceImage.naturalHeight;
    sourceCtx.drawImage(sourceImage, 0, 0);

    const imageData = sourceCtx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;

    asciiCanvas.width = (canvas.width / samplingInterval) * fontSize;
    asciiCanvas.height = (canvas.height / samplingInterval) * fontSize;

    ctx.font = `${fontSize}px monospace`;

    for (let y = 0, yCanvas = 0; y < canvas.height; y += samplingInterval, yCanvas++) {
        for (let x = 0, xCanvas = 0; x < canvas.width; x += samplingInterval, xCanvas++) {
            const index = (y * canvas.width + x) * 4;
            const r = data[index];
            const g = data[index + 1];
            const b = data[index + 2];

            // RGBを輝度に変換して文字を選択
            const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
            const charIndex = Math.floor((brightness / 255) * (asciiChars.length - 1));
            const char = asciiChars[charIndex];

            // 文字の色を設定して描画
            ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
            ctx.fillText(char, xCanvas * fontSize, (yCanvas + 1) * fontSize);
        }
    }
};

if (sourceImage.complete) {
    sourceImage.onload();
}

動画からのASCIIアート生成

画像の原理を応用すれば、動画もフレームごとに処理してASCIIアートに変換できます。`requestAnimationFrame`を使ったアニメーションループを実装します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Video to ASCII Art</title>
    <style>
        body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #111; }
        #ascii-canvas { image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges; }
    </style>
</head>
<body>
    <video id="source-video" controls style="display:none;">
        <source src="example.mp4" type="video/mp4">
    </video>
    <canvas id="ascii-canvas"></canvas>
    <script src="video-ascii.js"></script>
</body>
</html>
const video = document.getElementById('source-video');
const asciiCanvas = document.getElementById('ascii-canvas');
const ctx = asciiCanvas.getContext('2d', { alpha: false });

const asciiChars = ['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.'];
const samplingInterval = 8;
const fontSize = 8;
const targetFPS = 30;

let isPlaying = false;
let animationId;

const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d', { alpha: false });

video.onloadeddata = function() {
    offscreenCanvas.width = video.videoWidth;
    offscreenCanvas.height = video.videoHeight;

    asciiCanvas.width = (offscreenCanvas.width / samplingInterval) * fontSize;
    asciiCanvas.height = (offscreenCanvas.height / samplingInterval) * fontSize;

    ctx.font = `${fontSize}px monospace`;

    video.play();
    isPlaying = true;
    renderFrame();
};

function renderFrame() {
    if (!isPlaying) return;

    offscreenCtx.drawImage(video, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
    const imageData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height);
    const data = imageData.data;

    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, asciiCanvas.width, asciiCanvas.height);

    for (let y = 0, yCanvas = 0; y < offscreenCanvas.height; y += samplingInterval, yCanvas++) {
        for (let x = 0, xCanvas = 0; x < offscreenCanvas.width; x += samplingInterval, xCanvas++) {
            const index = (y * offscreenCanvas.width + x) * 4;
            const r = data[index];
            const g = data[index + 1];
            const b = data[index + 2];

            const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
            const charIndex = Math.floor((brightness / 255) * (asciiChars.length - 1));
            const char = asciiChars[charIndex];

            ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
            ctx.fillText(char, xCanvas * fontSize, (yCanvas + 1) * fontSize);
        }
    }

    animationId = requestAnimationFrame(renderFrame);
}

video.onpause = video.onended = function() {
    isPlaying = false;
    cancelAnimationFrame(animationId);
};

GIFの処理

GIF画像の処理は、フレームを個別に抽出してから、上記の動画処理と同様の手法で行うことができます。フレーム抽出には、gif.jsなどのライブラリが便利です。これについては、読者の課題としましょう。

タグ: javascript ASCIIアート Canvas API 画像処理 ビデオ処理

5月20日 10:03 投稿