前回の基礎編(図形やテキストの描画)に続き、本記事ではCanvas APIの真骨頂である 「画像処理」「ピクセル加工」「滑らかなアニメーション」「ユーザー操作(マウス・キーボード)」 について徹底解説します!
1. 画像の描画と高度な視覚効果
Canvasでは、外部の画像ファイルを読み込んで描画するだけでなく、切り抜き、合成、ピクセルレベルのフィルター処理など、本格的な画像編集ツールのようなWebアプリケーションを作成できます。
画像の読み込みと基本的な描画(drawImage)
Canvasに画像を配置する際は、JavaScriptの Image オブジェクトを使用します。重要なポイントは、 「画像の読み込み(onload)が完了してから描画する」 という点です。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'sample.jpg'; // 画像パスの指定
// 画像が読み込まれてから描画を実行
img.onload = () => {
// 基本描画: drawImage(image, x, y)
ctx.drawImage(img, 10, 10);
// サイズ変更描画: drawImage(image, x, y, width, height)
ctx.drawImage(img, 200, 10, 150, 100);
};
このサンプルコードの場合、ローカルにあるsample.jpgを参照しています。
ここでonloadを使用せずに描画しようとすると、ブラウザが画像の読み込みを完了していない状態で描画を始めてしまい、Canvasに何も表示されなくなる可能性があるため注意してください。
drawImage()メソッドには第1引数に描画する画像、第2~5引数には位置とサイズを指定します。
Canvasで描画した図形を画像へ変換する処理
Canvas上に描いた図形やテキスト、加工した画像は、JavaScriptを使って簡単に PNGやJPEGなどの画像ファイルへ変換・保存 ができます。
1. 最も手軽な変換方法
最も一般的で手軽な方法は、Canvasの toDataURL() メソッドを使って描画内容を「Base64形式の文字列(Data URL)」に変換し、JavaScriptでimgタグに表示です。
<!-- HTML -->
<canvas id="myCanvas" width="400" height="300"></canvas>
<img id="image">
// JavaScript
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// (例) Canvasに図形を描画
ctx.fillStyle = '#2563eb';
ctx.fillRect(20, 20, 160, 160);
ctx.fillStyle = '#ffffff';
ctx.font = '16px sans-serif';
ctx.fillText('Canvas Image', 35, 105);
// Canvasの画像への変換
let img = canvas.toDataURL();
// img要素への変換した画像ファイルの追加
document.getElementById('image').src = img;
toDataURL()には引数として変換する拡張子や画質を指定することができます。
// PNG形式(初期値:透明度を保持する)
const pngData = canvas.toDataURL('image/png');
// JPEG形式(画質90%指定)
const jpgData = canvas.toDataURL('image/jpeg', 0.9);
// WebP形式(軽量高画質 ※対応ブラウザのみ)
const webpData = canvas.toDataURL('image/webp', 0.85);
2.画像のトリミング・合成
drawImage()メソッドには、9つの引数を渡すことができます。
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
| 引数 | 説明 |
|---|---|
| image | Imageオブジェクト |
| sx, sy | 元画像から切り抜く開始座標 |
| sWidth, sHeight | 元画像から切り抜く幅と高さ |
| dx, dy | Canvas上の描画する開始座標 |
| dWidth, dHeight | Canvas上の描画する幅と高さ |
また、Canvasで後から描画したものは、先にある描画物の上に重なるように表示されます。
その時、globalCompositeOperationプロパティを使うことでレイヤーの合成表現が可能になります。
ctx.globalCompositeOperation = '合成モード名';
設定できる値は以下になります。(一部抜粋)
| 設定値 | 効果・用途 |
|---|---|
| source-over | (初期値)新しい描画を上に重ねる |
| destination-out | 新しい描画と重なった既存の部分を透明にする |
| source-in | 新しい描画のうち、既存の描画と重なっている部分だけを残す |
| destination-over | 新しい描画を、既存の描画の裏に描く |
| multiply | 乗算。色を掛け合わせて暗くする |
| lighter | 加算。重なった部分の発色を明るくする |
ピクセル操作の基本の流れ
Canvasでピクセルを加工する処理は、必ず以下の3ステップで行います。
-
getImageDataでCanvasからピクセルデータを取り出す - 取り出した配列(
data)の値をループ処理で直接書き換える -
putImageDataで加工後のデータをCanvasに書き戻す
[ Canvas ] ──(getImageData)──> [ ピクセル配列 (RGBA) ]
│
(JavaScriptで計算・編集)
▼
[ Canvas ] <──(putImageData)─── [ 編集後のピクセル配列 ]
ImageDataの構造と配列の読み方
ここが一番の重要ポイントです。
getImageData() を実行すると ImageData オブジェクトが返ってきます。
const imageData = ctx.getImageData(x, y, width, height);
console.log(imageData.data); // Uint8ClampedArray (数値の配列)
この imageData.data に入っているのは、長大な1次元配列です。
中身は [R, G, B, A, R, G, B, A, R, G, B, A ...] という順番で、ピクセル1つにつき4つの要素(赤・緑・青・透明度) が連続して並んでいます。
- R (Red) : 0〜255
- G (Green) : 0〜255
- B (Blue) : 0〜255
- A (Alpha) : 0〜255(※CSSと異なり、不透明度も0〜255で表されます)
座標 (x, y) のピクセルを取り出す計算式
1次元配列として並んでいるため、特定の座標 (x, y) のピクセルデータが配列のどこにあるかを計算するには以下の法則を使います。
// 横幅が width のCanvasにおいて、座標 (x, y) のR(赤)のインデックス
const index = (y * width + x) * 4;
const r = data[index]; // 赤
const g = data[index + 1]; // 緑
const b = data[index + 2]; // 青
const a = data[index + 3]; // 不透明度
画像のグレースケール&ネガポジ反転
実際に画像を取り込んでピクセルを加工する具体例を見てみましょう。
全ピクセルをループ処理する基本構文
全ピクセルを順番に処理する場合、インデックスを4ずつ増やしながらループさせます。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'sample.jpg';
img.onload = () => {
// 1. 画像を描画
ctx.drawImage(img, 0, 0);
// 2. ピクセルデータを取得
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 3. 全ピクセルをループ処理 (4つ飛ばし)
for (let i = 0; i < data.length; i += 4) {
// data[i] : Red
// data[i + 1] : Green
// data[i + 2] : Blue
// data[i + 3] : Alpha
}
// 4. 結果を書き戻す
ctx.putImageData(imageData, 0, 0);
};
パターンA:グレースケール(白黒)化
RGBの平均値を計算し、それを3色すべてに適応させることで白黒画像に変換できます。
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// RGBの平均値を計算(輝度)
const avg = (r + g + b) / 3;
// RGBすべてに同じ値をセットするとグレーになる
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
補足:人間の目の感度に合わせたより自然な白黒にする場合は、
const avg = r * 0.299 + g * 0.587 + b * 0.114;という加重平均(NTSC係数)を使うのが一般的です。
パターンB:ネガポジ反転(色反転)
各色の値を255から引くことで、色を反転(ネガフィルム化)できます。
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // Rの反転
data[i + 1] = 255 - data[i + 1]; // Gの反転
data[i + 2] = 255 - data[i + 2]; // Bの反転
}
2. Canvasで作るアニメーション
Canvasでオブジェクトを動かす基本ロジックは「一度描画をクリア(消去)し、位置を少しずらして再描画する」という工程の繰り返しです。パラパラ漫画みたいなものです。
requestAnimationFrame
JavaScriptでタイマー処理といえば setInterval や setTimeout が思い浮かびますが、アニメーションには必ず requestAnimationFrame() を使用します。
-
setInterval: ブラウザの描画タイミングを無視して実行されるため、コマ落ち(カクつき)や画面のちらつきが発生しやすい。 -
requestAnimationFrame: ブラウザが画面を書き換える最適タイミング(通常60fps=1秒間に約60回)に同期して実行される。タブが非表示の時は自動で一時停止するため、動作が滑らかでバッテリー消費も抑えられる。
基本コード:画面内を跳ね返るボール
まずは、requestAnimationFrame を使った基本ループのサンプルです。
<canvas id="myCanvas" width="400" height="250" style="border: 1px solid #ccc;"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// ボールの初期状態
let x = 50; // X座標
let y = 50; // Y座標
let vx = 3; // X方向の移動速度
let vy = 2; // Y方向の移動速度
const radius = 15; // ボールの半径
// メインのアニメーションループ関数
function animate() {
// --------------------------------------------------
// STEP 1: 画面全体をクリア(消去)
// --------------------------------------------------
ctx.clearRect(0, 0, canvas.width, canvas.height);
// --------------------------------------------------
// STEP 2: 位置の更新 & 壁での跳ね返り判定
// --------------------------------------------------
x += vx;
y += vy;
// 左右の壁に当たったらX方向の速度を反転
if (x + radius > canvas.width || x - radius < 0) {
vx = -vx;
}
// 上下の壁に当たったらY方向の速度を反転
if (y + radius > canvas.height || y - radius < 0) {
vy = -vy;
}
// --------------------------------------------------
// STEP 3: ボールの再描画
// --------------------------------------------------
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = '#2563eb';
ctx.fill();
// --------------------------------------------------
// STEP 4: 次のフレーム(画面描画タイミング)で再度 animate を実行
// --------------------------------------------------
requestAnimationFrame(animate);
}
// アニメーションスタート
animate();
</script>
💡 裏技テクニック:残像エフェクト
ctx.clearRectの代わりにctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; ctx.fillRect(0, 0, canvas.width, canvas.height);のように半透明の四角形を重ね塗りすると、光の軌跡のような美しい残像エフェクトが簡単に作れます!
マウス・キーボードイベントとの連携
ユーザーの操作に合わせてキャンバス上の描画を動かす方法です。
マウス座標の正しい取得方法
e.clientX や e.clientY をそのまま使うと、ページ全体のスクロール位置やCanvasの余白によって「カーソルと描画位置がずれる」 という問題が起きます。
getBoundingClientRect() を使い、Canvas要素の左上からの相対座標 を計算するのが必須テクニックです。
// マウス移動に追従するサークル
canvas.addEventListener('mousemove', (e) => {
// Canvas要素の画面上の絶対位置を取得
const rect = canvas.getBoundingClientRect();
// Canvas内の相対座標を計算
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 描画
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(mouseX, mouseY, 20, 0, Math.PI * 2);
ctx.fillStyle = '#10b981';
ctx.fill();
});
キーボード操作でキャラクターを動かす
矢印キーを押している間、オブジェクトを上下左右に移動させる仕組みです。
let player = { x: 180, y: 100, speed: 4 };
let keys = {}; // 押されているキーの状態を保持するオブジェクト
// キーを押した時
window.addEventListener('keydown', (e) => { keys[e.key] = true; });
// キーを離した時
window.addEventListener('keyup', (e) => { keys[e.key] = false; });
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 押されているキーに応じて位置を更新
if (keys['ArrowLeft'] || keys['a']) player.x -= player.speed;
if (keys['ArrowRight'] || keys['d']) player.x += player.speed;
if (keys['ArrowUp'] || keys['w']) player.y -= player.speed;
if (keys['ArrowDown'] || keys['s']) player.y += player.speed;
// プレイヤー(四角形)を描画
ctx.fillStyle = '#ef4444';
ctx.fillRect(player.x, player.y, 40, 40);
requestAnimationFrame(gameLoop);
}
gameLoop();
座標系の変形(移動・回転・拡大縮小)
Canvasで図形や画像を回転させたり拡大縮小させたりする場合、「図形自体を回す」のではなく「Canvasの座標軸全体を回す」 という独特な仕組みになっています。
そのため、必ず save()(状態の保存) と restore()(状態の復元) のセットで挟み込んで処理を行います。
座標変形の基本メソッド
| メソッド | 役割 |
|---|---|
ctx.translate(x, y) |
原点 (0, 0) の位置を移動させる |
ctx.rotate(radians) |
座標軸を指定した角度(ラジアン)回転させる |
ctx.scale(x, y) |
座標軸を指定した倍率で拡大・縮小する |
ctx.save() |
現在の座標軸・スタイルの状態をスタックに保存する |
ctx.restore() |
最後に保存した save() の状態に戻す |
回転処理の標準テンプレート(中心点回転)
四角形をその場の中心でクルクル回転させる基本処理です。
let angle = 0; // 回転角度
function drawRotatingBox() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 1. 設定変更前の状態を保存
ctx.save();
// 2. 座標の原点(0,0)を、回転させたいオブジェクトの中心位置へ移動
ctx.translate(200, 120);
// 3. 座標軸を回転(度は Math.PI / 180 を掛けてラジアンに変換)
ctx.rotate((angle * Math.PI) / 180);
// 4. (0, 0) を中心にして四角形を描く
// ※幅50, 高さ50の場合、(-25, -25) から描くことで、(0,0)が中心になる
ctx.fillStyle = '#8b5cf6';
ctx.fillRect(-25, -25, 50, 50);
// 5. 保存していた元の状態(回転していない座標軸)に戻す
ctx.restore();
// 角度を更新
angle += 2;
requestAnimationFrame(drawRotatingBox);
}
drawRotatingBox();
⚠️ 注意:
save()とrestore()を忘れると、ループが回るたびに座標軸がどんどん歪んでいき、画面外へ吹き飛んでしまいます。回転や移動を行う際は必ずペアで使用しましょう!
実際にアニメーションを作ってみよう
ここまでで学んだことを使って実際にアニメーションを作ってみましょう!
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 1. 画像の読み込み
const img = new Image();
img.src = 'sample.jpg'; // ※同じフォルダに置いてください
// 飛び散る粒子を保存する配列
const particles = [];
// 2. マウス移動時に粒子を生成する
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// マウス直下のピクセル色を取得
let color = '#ffffff';
try {
const pixel = ctx.getImageData(Math.floor(x), Math.floor(y), 1, 1).data;
color = `rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`;
} catch (err) {
color = '#38bdf8'; // file:/// のCORSエラー発生時の安全色
}
// マウスを動かすたびに粒子を5個生成して配列に追加
for (let i = 0; i < 5; i++) {
particles.push({
x: x,
y: y,
// 全方向に弾け飛ばすための速度(方向)
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
size: Math.random() * 6 + 3, // 粒子の大きさ
color: color,
alpha: 1.0 // 透明度(徐々に消える)
});
}
});
// 3. アニメーションループ
function animate() {
// (1) 背景に元画像を描画
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// (2) すべての粒子を移動・描画・フェードアウト
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
// 粒子を移動させる
p.x += p.vx;
p.y += p.vy;
p.alpha -= 0.03; // 徐々に透明にする(消えていく)
// 描画
ctx.save();
ctx.globalAlpha = Math.max(0, p.alpha);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 完全に透明になった粒子は配列から削除して重くならないようにする
if (p.alpha <= 0) {
particles.splice(i, 1);
}
}
requestAnimationFrame(animate);
}
img.onload = animate;
実際に動かすとこのようになります
CSSアニメーションとの分別
Webでグラフィックを動かす場合、CSSアニメーションとCanvasで大まかに2種類に分かれますが、迷った場合は以下の表のように分別して実装していきましょう。
| 比較項目 | CSS | Canva |
|---|---|---|
| 仕組み | HTML要素(DOM)のプロパティ変更 | 単一のタグ内でピクセルを書き換え |
| 大量の要素 | 不得意(要素が増えると重くなる) | 得意(数千のパーティクルも高速で処理) |
| 拡大・理解度 | 常に鮮明(ベクター) | 解像度設定に依存する(ラスター) |
| 文字選択・アクセシビリティ | 非常に高い | 不可(描画された絵の扱いのため) |
| ピクセル操作 | 不可 | 可能(画像加工や透過判定) |
つまり…
・CSS:UIパーツのアニメーション、ボタンのホバー効果、要素数が少ない動き
・Canva:2dゲームやパーティクル描画、お絵かきツール、画像編集・加工アプリ等
といった使い分けをしていくとWebでのグラフィック操作が格別によくなっていきます!
まとめ
本記事では、Canvas APIの真骨頂である画像処理・ピクセル操作・インタラクティブなアニメーション実装について解説しました。
最後に、今回学んだ重要ポイントを振り返りましょう!
-
画像の描画:必ず
img.onloadのタイミングで描画し、drawImageの引数を使い分けることでトリミングや拡大縮小が自在に行える。 -
ピクセル加工:
getImageDataで取得できる1次元配列(RGBA)をループ処理することで、グレースケールやネガポジ反転など高度な画像フィルターを自作できる。 -
アニメーション:
requestAnimationFrameを使い、「画面クリア → 位置更新 → 再描画」のループを作るのが滑らかな表現の基本。 -
ユーザー操作:
getBoundingClientRect()を使うことで、スクロールや余白の影響を受けずに正確なマウス座標を取得できる。 - CSSとの使い分け:UI要素やシンプルな動きはCSS、大量のパーティクルやピクセル加工、2DゲームなどはCanvasを採用する。
Canvasの世界はここからが本番!
Canvas APIをマスターすると、単なるWebページの装飾にとどまらず、「ブラウザ上で動く画像編集ツール」「インタラクティブなWebゲーム」「視覚的インパクトのあるデータビジュアライゼーション」 など、作れるWebアプリケーションの幅が爆発的に広がります。
まずは本記事で紹介したサンプルコードの数値を打ち変えたり、粒子(パーティクル)の動きや色をカスタマイズするところから試してみてください。自分のコードで画面上のピクセルが滑らかに動き出す楽しさを、ぜひ体感してみましょう!


