はじめに
「Bad Apple!!」のような文字アート(ASCIIアート)は、オンライン上でよく見かけます。多くのツールが存在しますが、プログラマーならば自ら作ってみたくなるのが人情です。今回は、JavaScriptを使って画像や動画をASCIIアートに変換する方法を解説します。
基本原理
ASCIIアートの生成原理は比較的シンプルです。主なステップは以下の通りです。
- 文字セットの準備: 輝度(明るさ)の異なる文字のセットを用意します。例えば、
['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.']のような配列です。これらの文字は、輝度の高い順に並べる必要があります。 - グレースケール変換: 元の画像をグレースケール(白黒)に変換します。これは、各ピクセルのRGB値を加重平均して輝度値を計算することで実現します。一般的な加重係数は、人間の視覚の感度に基づいており、
0.299, 0.587, 0.114または0.3086, 0.6094, 0.0820などが使われます。 - 文字マッピング: グレースケール画像の各ピクセルの輝度値を、準備した文字セットのどの文字に対応させるかを決定します。輝度が低い(暗い)ほど、文字セットの先頭に近い「濃い」文字が選ばれます。
- (オプション)カラー保持: グレースケール変換をせず、元の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などのライブラリが便利です。これについては、読者の課題としましょう。