できあいの画像
用意した絵をそのまま出すだけ。加工はできません。写真やロゴはこれで十分です。
HTML CANVAS / 初心者向け
<canvas> は、JavaScript から 1 ピクセルずつ絵を描ける「白紙」です。このページでは、白紙の置き方から、図形・文字・アニメーション・マウス操作までを、その場で動かせるデモと一緒に順番に見ていきます。
HTML・CSS・JavaScript の基本から確認したい方は、HTML 編 → CSS 編 → JavaScript 編 を先にどうぞ。このページは、その 3 つを使えることを前提にしています。
01
<canvas> はそれ自体では何も表示しません。ただの透明な長方形です。そこに JavaScript から「ここに青い四角を置いて」「ここに円を描いて」と命令していくと、はじめて絵が現れます。絵の具で塗るのと同じで、一度描いたものは記録されません。あとから「あの円だけ動かす」ということはできず、消して描き直すことになります。
用意した絵をそのまま出すだけ。加工はできません。写真やロゴはこれで十分です。
円や線が DOM 要素として残るので、CSS で色を変えたりクリックを拾ったりできます。拡大しても劣化しません。図やアイコン向き。
描いた結果だけが残ります。要素が増えないので、何千個の点を毎秒 60 回描き直すような用途に強い。ゲーム・グラフ・画像加工向き。
動かない図・アイコン・文字が主役なら SVG。要素の数が多い、毎フレーム描き変える、ピクセル単位で加工したい、なら Canvas。迷ったら SVG から始めて、重くなってから Canvas に移すのが安全です。
02
やることは 3 つだけです。HTML に <canvas> を置き、JavaScript でその要素を取得し、
getContext('2d') で「筆」にあたるオブジェクト(コンテキスト)を受け取ります。以降の描画命令は、すべてこの筆 ctx に対して出します。
<canvas id="stage" width="420" height="240">
ここは canvas に対応していないブラウザにだけ表示されます。
</canvas>
<script src="main.js" defer></script>
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d'); // これが「筆」
ctx.fillStyle = '#2f80b8'; // 塗る色を決めて
ctx.fillRect(20, 20, 120, 80); // 塗る
実際に動かすと、次のようになります。「実行」を押すと、上のコードがそのまま実行されます。
// 背景を塗る
ctx.fillStyle = '#eaf2f8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 四角
ctx.fillStyle = '#2f80b8';
ctx.fillRect(30, 40, 140, 90);
// 円
ctx.fillStyle = '#ff6a3d';
ctx.beginPath();
ctx.arc(280, 85, 48, 0, Math.PI * 2);
ctx.fill();
// 文字
ctx.fillStyle = '#0d2338';
ctx.font = 'bold 22px sans-serif';
ctx.fillText('Hello, Canvas!', 30, 190);
このページのデモでは canvas と ctx があらかじめ用意されています。上の 2 行は省略しています。
大きさは HTML の width / height 属性で指定します。
CSS の width で指定すると、420×240 の絵を無理やり引き伸ばすことになり、ぼやけます。詳しくは 15 章で扱います。
03
Canvas の座標は 左上が (0, 0)、x は右へ、y は下へ増えます。数学のグラフとは y の向きが逆なので、最初はここでつまずきます。単位は「canvas の中のピクセル」で、画面上の実際の大きさとは別物です。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 40px ごとの方眼を引く
ctx.strokeStyle = '#d6e3ee';
ctx.lineWidth = 1;
for (let x = 0; x <= canvas.width; x += 40) {
ctx.beginPath();
ctx.moveTo(x + 0.5, 0); // 0.5 ずらすと線がにじまない
ctx.lineTo(x + 0.5, canvas.height);
ctx.stroke();
}
for (let y = 0; y <= canvas.height; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y + 0.5);
ctx.lineTo(canvas.width, y + 0.5);
ctx.stroke();
}
// 代表的な座標に点を打つ
const points = [[0, 0], [120, 80], [300, 200]];
ctx.font = '13px sans-serif';
for (const [x, y] of points) {
ctx.fillStyle = '#ff6a3d';
ctx.beginPath();
ctx.arc(x, y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#0d2338';
ctx.fillText(`(${x}, ${y})`, x + 12, y + 17);
}
太さ 1 の線は「座標を中心に左右 0.5 ずつ」描かれます。
x = 40 に引くと 39.5〜40.5 にまたがり、2 本のピクセルに薄く分かれてにじみます。
x + 0.5 にすると 40〜41 にぴったり収まり、くっきりします。
04
四角形だけは特別扱いで、パスを組み立てなくても 1 行で描けます。引数はどれも (x, y, 幅, 高さ) です。
fillRect() … 塗りつぶした四角strokeRect() … 枠線だけの四角clearRect() … その範囲を透明に戻す(消しゴム)// 塗りつぶし
ctx.fillStyle = '#2f80b8';
ctx.fillRect(25, 45, 110, 80);
// 枠線だけ
ctx.strokeStyle = '#0d2338';
ctx.lineWidth = 4;
ctx.strokeRect(155, 45, 110, 80);
// 塗ってから、内側をくり抜く
ctx.fillStyle = '#ff6a3d';
ctx.fillRect(285, 45, 110, 80);
ctx.clearRect(310, 65, 60, 40);
ctx.fillStyle = '#0d2338';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('fillRect', 80, 155);
ctx.fillText('strokeRect', 210, 155);
ctx.fillText('clearRect', 340, 155);
くり抜いた部分から、背景の市松模様が透けて見えます=そこが「透明」です。
05
四角形以外の図形は「パス」で作ります。紙の上でペンを動かす手順そのままです。
パスを組み立てただけでは何も見えません。最後に fill() か stroke() を呼んで、はじめて描かれます。
新しいパスを始める。これを忘れると前の線が残り続けます。
ペンを持ち上げて、この位置へ移動する。
ペンを下ろしたまま、この位置まで動かす(何回でも)。
始点まで線を戻して閉じる。閉じた図形にしたいときだけ。
塗る/線を引く。両方呼んでもかまいません。
// 閉じた三角形
ctx.beginPath();
ctx.moveTo(110, 40);
ctx.lineTo(180, 155);
ctx.lineTo(40, 155);
ctx.closePath();
ctx.fillStyle = '#9dc6e2';
ctx.fill();
ctx.strokeStyle = '#1b4e77';
ctx.lineWidth = 3;
ctx.stroke();
// 閉じない折れ線
ctx.beginPath();
ctx.moveTo(240, 150);
ctx.lineTo(280, 60);
ctx.lineTo(325, 130);
ctx.lineTo(385, 50);
ctx.strokeStyle = '#ff6a3d';
ctx.stroke();
ctx.fillStyle = '#0d2338';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('closePath あり', 110, 195);
ctx.fillText('closePath なし', 312, 195);
06
ctx.arc(x, y, 半径, 開始角, 終了角) で円弧を描きます。角度は度ではなくラジアンです。Math.PI が 180 度、Math.PI * 2 が一周です。度から変換したいときは 度 * Math.PI / 180 と書きます。
| 角度 | ラジアン | 向き(y が下向きなので時計回り) |
|---|---|---|
| 0 | 0 | 右(3 時の方向)=ここが開始点 |
| 90 度 | Math.PI / 2 | 下(6 時の方向) |
| 180 度 | Math.PI | 左(9 時の方向) |
| 270 度 | Math.PI * 1.5 | 上(12 時の方向) |
ctx.lineWidth = 4;
// まるごと一周 = 円
ctx.beginPath();
ctx.arc(80, 105, 50, 0, Math.PI * 2);
ctx.fillStyle = '#9dc6e2';
ctx.fill();
// 半周だけ = 弧
ctx.beginPath();
ctx.arc(210, 105, 50, 0, Math.PI);
ctx.strokeStyle = '#1b4e77';
ctx.stroke();
// 中心から始めて閉じる = 扇形
ctx.beginPath();
ctx.moveTo(340, 105);
ctx.arc(340, 105, 50, -Math.PI / 2, Math.PI / 4);
ctx.closePath();
ctx.fillStyle = '#ff6a3d';
ctx.fill();
ctx.fillStyle = '#0d2338';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('円', 80, 195);
ctx.fillText('弧', 210, 195);
ctx.fillText('扇形(円グラフの一片)', 340, 195);
07
なめらかな曲線はベジェ曲線で描きます。曲線は制御点のほうへ引っぱられますが、制御点そのものを通るわけではありません。制御点が 1 つなら quadraticCurveTo()、
2 つなら bezierCurveTo() です。
ctx.lineWidth = 3;
// 制御点 1 つ:(110, 25) に引っぱられる
ctx.beginPath();
ctx.moveTo(30, 180);
ctx.quadraticCurveTo(110, 25, 190, 180);
ctx.strokeStyle = '#1b4e77';
ctx.stroke();
// 制御点 2 つ:S 字も描ける
ctx.beginPath();
ctx.moveTo(230, 180);
ctx.bezierCurveTo(250, 35, 370, 215, 395, 70);
ctx.strokeStyle = '#ff6a3d';
ctx.stroke();
// 制御点の位置を確認用に打つ
function point(x, y) {
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#9aa9b6';
ctx.fill();
}
point(110, 25);
point(250, 35);
point(370, 215);
灰色の点が制御点です。曲線がその方向へふくらんでいるのが分かります。
08
ctx の設定値は次に描くものに効きます。すでに描かれたものは変わりません。だから「色を決める → 描く」の順序が絶対で、逆にすると効きません。
ctx.strokeStyle = '#1b4e77';
// 線の太さ(lineWidth)
[1, 3, 8].forEach((w, i) => {
ctx.lineWidth = w;
ctx.beginPath();
ctx.moveTo(25, 30 + i * 26);
ctx.lineTo(175, 30 + i * 26);
ctx.stroke();
});
// 線の端の形(lineCap)
ctx.lineWidth = 14;
['butt', 'round', 'square'].forEach((cap, i) => {
ctx.lineCap = cap;
ctx.beginPath();
ctx.moveTo(255, 30 + i * 26);
ctx.lineTo(370, 30 + i * 26);
ctx.stroke();
});
ctx.lineCap = 'butt';
// 破線(setLineDash)
ctx.setLineDash([12, 6]); // 12px 描いて 6px 空ける
ctx.lineWidth = 3;
ctx.strokeStyle = '#ff6a3d';
ctx.beginPath();
ctx.moveTo(25, 140);
ctx.lineTo(395, 140);
ctx.stroke();
ctx.setLineDash([]); // 元に戻す
// 半透明の重なり(globalAlpha)
ctx.globalAlpha = 0.45;
ctx.fillStyle = '#2f80b8';
ctx.fillRect(25, 170, 130, 50);
ctx.fillStyle = '#ff6a3d';
ctx.fillRect(100, 185, 130, 50);
ctx.globalAlpha = 1; // 戻し忘れに注意
ctx.fillStyle = '#0d2338';
ctx.font = '13px sans-serif';
ctx.fillText('lineWidth 1 / 3 / 8', 25, 105);
ctx.textAlign = 'right';
ctx.fillText('lineCap butt / round / square', 395, 105);
globalAlpha・setLineDash・shadowColor などは、設定したままだと以降のすべての描画に効き続けます。後片づけが面倒になったら save() / restore() を使いましょう。
09
fillStyle には色名のほかに、グラデーションのオブジェクトも入れられます。まずグラデーションを作り、addColorStop(位置, 色) で色を置いてから、fillStyle に渡します。位置は 0〜1 の割合です。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 線形グラデーション:(x1, y1) から (x2, y2) へ
const linear = ctx.createLinearGradient(25, 0, 395, 0);
linear.addColorStop(0, '#1b4e77');
linear.addColorStop(0.5, '#2f80b8');
linear.addColorStop(1, '#ff6a3d');
ctx.fillStyle = linear;
ctx.fillRect(25, 25, 370, 55);
// 放射グラデーション:内側の円から外側の円へ
const radial = ctx.createRadialGradient(110, 165, 4, 110, 165, 52);
radial.addColorStop(0, '#ffffff');
radial.addColorStop(1, '#2f80b8');
ctx.fillStyle = radial;
ctx.beginPath();
ctx.arc(110, 165, 52, 0, Math.PI * 2);
ctx.fill();
// 影
ctx.shadowColor = 'rgb(13 35 56 / 40%)';
ctx.shadowBlur = 18;
ctx.shadowOffsetY = 8;
ctx.fillStyle = '#ff6a3d';
ctx.fillRect(260, 120, 120, 90);
ctx.shadowColor = 'transparent'; // 影を切る
10
ctx.font は CSS の font ショートハンドと同じ書き方です。
サイズと書体は省略できません('20px' だけでは効きません)。基準点の扱いが独特で、既定では文字のベースライン左端が fillText() に渡した座標になります。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#0d2338';
ctx.font = 'bold 28px sans-serif';
ctx.fillText('Canvas の文字', 25, 55);
ctx.font = '15px sans-serif';
ctx.fillStyle = '#2f80b8';
ctx.fillText('サイズと書体は必ずセットで指定する', 25, 82);
// 基準点を中央に変える
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = 'bold 24px sans-serif';
ctx.fillStyle = '#ff6a3d';
ctx.fillText('中央ぞろえ', 210, 135);
// 同じ座標に縁取りを重ねる
ctx.lineWidth = 1.5;
ctx.strokeStyle = '#0d2338';
ctx.strokeText('中央ぞろえ', 210, 135);
// 文字の幅を測って、背景を敷く
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
ctx.font = '15px sans-serif';
const label = 'measureText() で幅が測れる';
const w = ctx.measureText(label).width;
ctx.fillStyle = '#eaf2f8';
ctx.fillRect(25, 180, w + 20, 30);
ctx.fillStyle = '#0d2338';
ctx.fillText(label, 35, 200);
textAlign は 'left' | 'center' | 'right'、
textBaseline は 'top' | 'middle' | 'alphabetic' | 'bottom'。ボタンの中央に文字を置きたいときは、両方を 'center' / 'middle' にすると計算が要りません。
11
drawImage() は引数の数で意味が変わります。
3 個なら等倍、5 個なら大きさ指定、9 個なら「元画像の一部を切り出して置く」(スプライト)です。
| 引数 | 意味 |
|---|---|
(img, x, y) | そのままの大きさで置く |
(img, x, y, w, h) | w × h に伸縮して置く |
(img, sx, sy, sw, sh, x, y, w, h) | 元画像の (sx, sy) から sw × sh を切り出し、w × h で置く |
下のデモでは、外部ファイルを読み込む代わりに別の canvas を「絵の元」として使っています。
canvas は画像として扱えるので、これも drawImage() にそのまま渡せます。
// 画面に出さない canvas を作り、そこに「元の絵」を描く
const src = document.createElement('canvas');
src.width = 100;
src.height = 100;
const s = src.getContext('2d');
s.fillStyle = '#1b4e77';
s.fillRect(0, 0, 100, 100);
s.fillStyle = '#ff6a3d';
s.beginPath();
s.arc(50, 50, 34, 0, Math.PI * 2);
s.fill();
s.fillStyle = '#ffffff';
s.fillRect(0, 46, 100, 8);
// 1. 等倍
ctx.drawImage(src, 20, 40);
// 2. 大きさを指定して縮小
ctx.drawImage(src, 145, 40, 60, 60);
// 3. 左上 50x50 だけを切り出して拡大
ctx.drawImage(src, 0, 0, 50, 50, 240, 40, 140, 140);
ctx.fillStyle = '#0d2338';
ctx.font = '13px sans-serif';
ctx.fillText('等倍', 20, 200);
ctx.fillText('縮小', 145, 200);
ctx.fillText('切り出して拡大', 240, 200);
実際の画像ファイルを使うときは、読み込みが終わるのを待ってから描きます。
const img = new Image();
img.addEventListener('load', () => {
ctx.drawImage(img, 0, 0); // 読み込み完了後に描く
});
img.src = 'assets/images/photo.png'; // src の代入は最後で構わない
img.src を入れた直後に drawImage() を呼んでも、まだ画像が届いていないので何も描かれません。エラーにもならないため気づきにくい定番のつまずきです。必ず load を待ちましょう。
12
図形を回転させる方法は、ふつう思いつくのは「回転後の座標を三角関数で計算する」ですが、 Canvas では逆に座標系のほうを動かして、いつも同じ位置に描くのが定石です。
translate(x, y) … 原点をずらすrotate(角度) … 原点を中心に回す(ラジアン)scale(sx, sy) … 拡大・縮小するsave() / restore() … 変形や色の設定を丸ごと保存・復元する
save() と restore() は必ず対で使います。
これを挟んでおけば、変形も色も自動で元に戻るので、後片づけを考えなくて済みます。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 原点をキャンバスの中央へ移す
ctx.translate(210, 120);
for (let i = 0; i < 12; i++) {
ctx.save(); // 今の状態を保存
ctx.rotate((Math.PI * 2 / 12) * i); // i 番目の角度へ回す
ctx.fillStyle = i % 2 === 0 ? '#2f80b8' : '#ff6a3d';
ctx.fillRect(28, -9, 72, 18); // いつも同じ座標に描くだけ
ctx.restore(); // 回転を取り消す
}
ctx.fillStyle = '#0d2338';
ctx.beginPath();
ctx.arc(0, 0, 14, 0, Math.PI * 2);
ctx.fill();
fillRect(28, -9, 72, 18) は 12 回とも同じです。変わっているのは座標系だけ。
13
Canvas に「動かす」命令はありません。全部消して、少しずらして、また描く。これを 1 秒に約 60 回くり返すと動いて見えます。そのくり返しを担当するのが
requestAnimationFrame() です。
消し忘れると、軌跡が残り続けます(それを狙う演出もあります)。
「1 フレームあたり 2px」ではなく「1 秒あたり 150px」で考え、経過時間を掛けます。
いつもの描画命令です。
次の 1 コマを予約します。setInterval より滑らかで、タブが裏に回ると自動で止まります。
const r = 22;
let x = 70;
let y = 60;
let vx = 160; // 1 秒あたり 160px 進む
let vy = 120;
let last = performance.now();
function frame(now) {
const dt = (now - last) / 1000; // 前のコマからの経過秒
last = now;
x += vx * dt;
y += vy * dt;
// 壁に当たったら向きを反転
if (x < r || x > canvas.width - r) vx = -vx;
if (y < r || y > canvas.height - r) vy = -vy;
// 1. 消す
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#eaf2f8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 2. 描く
ctx.fillStyle = '#ff6a3d';
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
// 3. 次のコマを予約
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
画面外に出ると自動的に停止します(このページ側の仕組み)。
x += 3 のように固定値で足すと、120Hz の画面では 2 倍速になってしまいます。
x += 速度 * 経過秒 にしておけば、どんな環境でも同じ速さで動きます。
14
イベントが教えてくれる clientX / clientY は画面上の座標で、
canvas の中の座標ではありません。getBoundingClientRect() で canvas の位置と表示サイズを調べ、
canvas の解像度に合わせて換算します。この 3 行が定型句です。
const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * (canvas.width / rect.width);
const y = (event.clientY - rect.top) * (canvas.height / rect.height);
キャンバスをクリックすると、その場所に円が増えます。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#8fa3b4';
ctx.font = '15px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('クリックしてみてください', canvas.width / 2, 32);
canvas.addEventListener('pointerdown', (event) => {
const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * (canvas.width / rect.width);
const y = (event.clientY - rect.top) * (canvas.height / rect.height);
ctx.beginPath();
ctx.arc(x, y, 10 + Math.random() * 24, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${Math.round(Math.random() * 360)} 70% 55% / 0.65)`;
ctx.fill();
});
pointerdown でパスを始め、pointermove のたびに lineTo() と stroke() を呼びます。マウス・指・ペンをまとめて扱える pointer* イベントを使うのが今の標準です。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 5;
ctx.lineCap = 'round'; // 点で打っても丸くなる
ctx.lineJoin = 'round'; // 角がとがらない
ctx.strokeStyle = '#1b4e77';
let drawing = false;
function positionOf(event) {
const rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (canvas.width / rect.width),
y: (event.clientY - rect.top) * (canvas.height / rect.height)
};
}
canvas.addEventListener('pointerdown', (event) => {
drawing = true;
canvas.setPointerCapture(event.pointerId); // 外に出ても追い続ける
const p = positionOf(event);
ctx.beginPath();
ctx.moveTo(p.x, p.y);
});
canvas.addEventListener('pointermove', (event) => {
if (!drawing) return;
const p = positionOf(event);
ctx.lineTo(p.x, p.y);
ctx.stroke();
});
canvas.addEventListener('pointerup', () => {
drawing = false;
});
スマホやタブレットでは、指でなぞると線が引けます。
15
canvas には大きさが 2 つあります。絵の解像度(width / height 属性)と、
画面上の表示サイズ(CSS)です。この 2 つがずれると、絵が引き伸ばされてぼやけます。
Retina などの高精細ディスプレイでは、CSS で何も指定しなくても物理ピクセルが 2 倍あるため、既定のままだとぼやけます。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 左:小さく描いたものを 2 倍に引き伸ばした状態
const low = document.createElement('canvas');
low.width = 95;
low.height = 55;
const l = low.getContext('2d');
l.fillStyle = '#0d2338';
l.font = 'bold 17px sans-serif';
l.fillText('ぼやける', 5, 34);
ctx.drawImage(low, 25, 70, 190, 110);
// 右:はじめから実寸で描いた状態
ctx.fillStyle = '#0d2338';
ctx.font = 'bold 34px sans-serif';
ctx.fillText('くっきり', 240, 138);
ctx.strokeStyle = '#d6e3ee';
ctx.strokeRect(25.5, 70.5, 190, 110);
ctx.font = '13px sans-serif';
ctx.fillStyle = '#8fa3b4';
ctx.fillText('引き伸ばし', 25, 205);
ctx.fillText('実寸', 240, 205);
対策は決まりきっていて、「表示したいサイズ × デバイスピクセル比」を解像度にし、
scale() で描画側の座標をもとに戻すだけです。こうしておけば、描くコードは今までどおり CSS ピクセルの感覚で書けます。
function setupCanvas(canvas, cssWidth, cssHeight) {
const dpr = window.devicePixelRatio || 1;
// 画面上の見た目の大きさ
canvas.style.width = cssWidth + 'px';
canvas.style.height = cssHeight + 'px';
// 実際の解像度は dpr 倍にする
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); // 以降は cssWidth / cssHeight の座標で描ける
return ctx;
}
const ctx = setupCanvas(document.getElementById('stage'), 420, 240);
ctx.fillRect(10, 10, 100, 50); // 見た目どおりの 100x50
canvas.width に値を入れると、中身が消えて設定もすべて初期化されます。リサイズのたびに呼ぶなら、そのあとで描き直しと scale() のやり直しが必要です。ちなみに、この仕様は「全部消したい」ときの手軽な手段としても使えます。
16
エラーが出ないのに思ったとおりに描けないとき、原因はたいていこの中にあります。
| 症状 | 原因と対処 |
|---|---|
| 何も表示されない | fill() / stroke() を呼んでいない。パスを組み立てただけでは描かれません。 |
canvas is null |
要素より先にスクリプトが動いている。<script> に defer を付けます。 |
| ぼやける・伸びる | CSS で大きさを変えている。解像度は属性で指定し、dpr 対応を入れます。 |
| 前の線まで色が変わる | beginPath() の呼び忘れ。下のデモで実際の見え方を確認できます。 |
| クリック位置がずれる | clientX をそのまま使っている。座標変換の定型句を挟みます。 |
| 画像が描かれない | load を待たずに drawImage() を呼んでいる。 |
| 残像が残る | 毎フレームの clearRect() 忘れ。 |
| 色や透明度が意図せず効く | 設定が残っている。save() / restore() で囲みます。 |
パスは beginPath() を呼ぶまで積み上がり続けます。
stroke() はそのとき溜まっているパス全部を描くので、以前の線が新しい色で描き直されます。
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = 5;
// 1 本目:水色で引いたつもり
ctx.moveTo(30, 70);
ctx.lineTo(390, 70);
ctx.strokeStyle = '#9dc6e2';
ctx.stroke();
// 2 本目:beginPath を呼んでいないので 1 本目も一緒に描き直される
ctx.moveTo(30, 120);
ctx.lineTo(390, 120);
ctx.strokeStyle = '#ff6a3d';
ctx.stroke();
ctx.fillStyle = '#0d2338';
ctx.font = '14px sans-serif';
ctx.fillText('2 本ともオレンジになる = beginPath() の呼び忘れ', 30, 175);
1 本目の ctx.moveTo の前に ctx.beginPath(); を書き足すと直ります。
17
よく使うものだけを 1 枚に。まずはこれだけ手元に置いておけば足ります。
| 用途 | 書き方 | ひとこと |
|---|---|---|
| 準備 | canvas.getContext('2d') | これが「筆」 |
| 塗り四角 | fillRect(x, y, w, h) | パス不要 |
| 枠四角 | strokeRect(x, y, w, h) | 太さは lineWidth |
| 消す | clearRect(x, y, w, h) | 透明に戻る |
| パス開始 | beginPath() | 忘れると事故る |
| 移動 / 直線 | moveTo(x, y) / lineTo(x, y) | ペンの上げ下げ |
| 円・弧 | arc(x, y, r, 開始, 終了) | 一周は Math.PI * 2 |
| 曲線 | quadraticCurveTo() / bezierCurveTo() | 制御点で引っぱる |
| 確定 | fill() / stroke() | ここで初めて描かれる |
| 色 | fillStyle / strokeStyle | 描く前に設定する |
| 線 | lineWidth / lineCap / setLineDash() | 戻し忘れ注意 |
| 文字 | font / fillText(text, x, y) | サイズと書体は必須 |
| 文字幅 | measureText(text).width | 背景敷きや折り返しに |
| 画像 | drawImage(img, ...) | 3 / 5 / 9 引数 |
| 変形 | translate() / rotate() / scale() | 座標系を動かす |
| 状態 | save() / restore() | 必ず対で |
| コマ送り | requestAnimationFrame(fn) | 消す → 動かす → 描く |
| 書き出し | canvas.toDataURL('image/png') | 画像として保存できる |
18
基礎はここまでです。ctx に命令を並べる、という一本道しかないことが分かれば、あとは組み合わせるだけ。発展編では 5 つの題材を、まず動く最小形(基本形)でつくり、そのあと使えるところまで仕上げた完成形へと育てていきます。
Canvas でいちばん大事な感覚は、「命令した順に、上から絵の具を重ねている」ということです。思ったとおりに描けないときは、描く順番と、そのときの ctx の設定を疑ってみてください。