HTML CANVAS / 初心者向け

ブラウザの中に、
絵を描くための入り口。

<canvas> は、JavaScript から 1 ピクセルずつ絵を描ける「白紙」です。このページでは、白紙の置き方から、図形・文字・アニメーション・マウス操作までを、その場で動かせるデモと一緒に順番に見ていきます。

目次を開く(全 18 章)

各デモの「⬇ 保存」を押すと、そのままダブルクリックで動く HTML ファイルとして保存できます。

前編があります

HTML・CSS・JavaScript の基本から確認したい方は、HTML 編CSS 編JavaScript 編 を先にどうぞ。このページは、その 3 つを使えることを前提にしています。

01

Canvas とは、何もない「白紙」のこと

<canvas> はそれ自体では何も表示しません。ただの透明な長方形です。そこに JavaScript から「ここに青い四角を置いて」「ここに円を描いて」と命令していくと、はじめて絵が現れます。絵の具で塗るのと同じで、一度描いたものは記録されません。あとから「あの円だけ動かす」ということはできず、消して描き直すことになります。

img

できあいの画像

用意した絵をそのまま出すだけ。加工はできません。写真やロゴはこれで十分です。

svg

図形が部品になる

円や線が DOM 要素として残るので、CSS で色を変えたりクリックを拾ったりできます。拡大しても劣化しません。図やアイコン向き。

canvas

ピクセルを塗る

描いた結果だけが残ります。要素が増えないので、何千個の点を毎秒 60 回描き直すような用途に強い。ゲーム・グラフ・画像加工向き。

使い分けの目安

動かない図・アイコン・文字が主役なら SVG。要素の数が多い、毎フレーム描き変える、ピクセル単位で加工したい、なら Canvas。迷ったら SVG から始めて、重くなってから Canvas に移すのが安全です。

02

最初の 3 行:白紙を置いて、筆を取り出す

やることは 3 つだけです。HTML に <canvas> を置き、JavaScript でその要素を取得し、 getContext('2d') で「筆」にあたるオブジェクト(コンテキスト)を受け取ります。以降の描画命令は、すべてこの筆 ctx に対して出します。

index.html
<canvas id="stage" width="420" height="240">
  ここは canvas に対応していないブラウザにだけ表示されます。
</canvas>

<script src="main.js" defer></script>
main.js
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');   // これが「筆」

ctx.fillStyle = '#2f80b8';             // 塗る色を決めて
ctx.fillRect(20, 20, 120, 80);         // 塗る

実際に動かすと、次のようになります。「実行」を押すと、上のコードがそのまま実行されます。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
// 背景を塗る
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);

このページのデモでは canvasctx があらかじめ用意されています。上の 2 行は省略しています。

よくある間違い

大きさは HTML の width / height 属性で指定します。 CSS の width で指定すると、420×240 の絵を無理やり引き伸ばすことになり、ぼやけます。詳しくは 15 章で扱います。

03

座標系:原点は左上、y は下向き

Canvas の座標は 左上が (0, 0)、x は右へ、y は下へ増えます。数学のグラフとは y の向きが逆なので、最初はここでつまずきます。単位は「canvas の中のピクセル」で、画面上の実際の大きさとは別物です。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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);
}
0.5 ピクセルのずらし

太さ 1 の線は「座標を中心に左右 0.5 ずつ」描かれます。 x = 40 に引くと 39.5〜40.5 にまたがり、2 本のピクセルに薄く分かれてにじみます。 x + 0.5 にすると 40〜41 にぴったり収まり、くっきりします。

04

四角形:3 つだけ覚えれば描ける

四角形だけは特別扱いで、パスを組み立てなくても 1 行で描けます。引数はどれも (x, y, 幅, 高さ) です。

  • fillRect() … 塗りつぶした四角
  • strokeRect() … 枠線だけの四角
  • clearRect() … その範囲を透明に戻す(消しゴム)
このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
// 塗りつぶし
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() を呼んで、はじめて描かれます。

  1. ctx.beginPath()

    新しいパスを始める。これを忘れると前の線が残り続けます。

  2. ctx.moveTo(x, y)

    ペンを持ち上げて、この位置へ移動する。

  3. ctx.lineTo(x, y)

    ペンを下ろしたまま、この位置まで動かす(何回でも)。

  4. ctx.closePath()

    始点まで線を戻して閉じる。閉じた図形にしたいときだけ。

  5. ctx.fill() / ctx.stroke()

    塗る/線を引く。両方呼んでもかまいません。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
// 閉じた三角形
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 が下向きなので時計回り)
00右(3 時の方向)=ここが開始点
90 度Math.PI / 2下(6 時の方向)
180 度Math.PI左(9 時の方向)
270 度Math.PI * 1.5上(12 時の方向)
このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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() です。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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 の設定値は次に描くものに効きます。すでに描かれたものは変わりません。だから「色を決める → 描く」の順序が絶対で、逆にすると効きません。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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);
戻し忘れに注意

globalAlphasetLineDashshadowColor などは、設定したままだと以降のすべての描画に効き続けます。後片づけが面倒になったら save() / restore() を使いましょう。

09

グラデーションと影

fillStyle には色名のほかに、グラデーションのオブジェクトも入れられます。まずグラデーションを作り、addColorStop(位置, 色) で色を置いてから、fillStyle に渡します。位置は 0〜1 の割合です。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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() に渡した座標になります。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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 非対応のブラウザでは表示できません。
JavaScript
// 画面に出さない 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);

実際の画像ファイルを使うときは、読み込みが終わるのを待ってから描きます。

JavaScript
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() は必ず対で使います。 これを挟んでおけば、変形も色も自動で元に戻るので、後片づけを考えなくて済みます。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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. clearRect() で消す

    消し忘れると、軌跡が残り続けます(それを狙う演出もあります)。

  2. 位置を更新する

    「1 フレームあたり 2px」ではなく「1 秒あたり 150px」で考え、経過時間を掛けます。

  3. 描く

    いつもの描画命令です。

  4. requestAnimationFrame(自分自身)

    次の 1 コマを予約します。setInterval より滑らかで、タブが裏に回ると自動で止まります。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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);

キャンバスをクリックすると、その場所に円が増えます。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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* イベントを使うのが今の標準です。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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 倍あるため、既定のままだとぼやけます。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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() を忘れるとどうなるか

パスは beginPath() を呼ぶまで積み上がり続けます。 stroke()そのとき溜まっているパス全部を描くので、以前の線が新しい色で描き直されます。

このデモは canvas 非対応のブラウザでは表示できません。
JavaScript
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 つの題材を、まず動く最小形(基本形)でつくり、そのあと使えるところまで仕上げた完成形へと育てていきます。

ADVANCED / 発展編 つくって覚える 5 つの題材 アナログ時計 / 棒グラフ / お絵かきツール / 跳ねるボール / 15 パズル。
それぞれ「基本形」と「完成形」の 2 本立て、全 10 デモ。
最後に

Canvas でいちばん大事な感覚は、「命令した順に、上から絵の具を重ねている」ということです。思ったとおりに描けないときは、描く順番と、そのときの ctx の設定を疑ってみてください。