<canvas> 要素を使って図形・画像・テキストを描画した際、「なぜか文字の輪郭が滲んでぼやける」「CSSでサイズを広げたら画質が荒くなった」「Retinaディスプレイやスマートフォンで見ると鮮明さがない」といった現象に直面することがあります。
この問題の多くは、Canvas固有の「描画サイズ(内部解像度)」と「CSSの表示サイズ」の不一致、および ディスプレイのデバイスピクセル比(devicePixelRatio) を考慮していないことが原因です。
この記事では、Canvasがぼやける根本的な仕組みから、devicePixelRatio を用いた高解像度(Retina)対応の最短コード、ctx.scale() の役割、レスポンシブ時の注意点までを解説します。
canvasがぼやける主な原因は「描画サイズ」と「表示サイズ」の違い
Canvasを扱う上で最初に押さえておくべきなのが、「描画領域のサイズ」と「画面上の見た目のサイズ」が別々に管理されているという点です。
canvasには描画サイズとCSS上の表示サイズがある
HTML仕様において、<canvas> には2種類のサイズが存在します。
- 描画サイズ(ビットマップサイズ):HTMLの
width/height属性(または JS のcanvas.width/canvas.height)で決まる、Canvas内部の実際のピクセル数。 - 表示サイズ(CSSサイズ):CSSの
width/heightプロパティで決まる、ブラウザ画面上でのレイアウト幅・高さ。
<!-- 描画用ビットマップ: 300 x 150 -->
<canvas id="canvas" width="300" height="150"></canvas>
/* 画面上の表示サイズ: 600px x 300px */
#canvas {
width: 600px;
height: 300px;
}
width・heightをCSSだけで指定するとぼやけることがある
上記のように「描画サイズが 300×150」の Canvas に対して、CSSだけで「表示サイズを 600×300」に引き伸ばすと、小さな画像(低解像度ビットマップ)を2倍に拡大表示した状態になります。
【内部のビットマップ】 【CSSで引き伸ばされた表示】
┌──────────────┐ ┌────────────────────────────┐
│ 300 × 150 px │ ──(CSS拡大)──→ │ 600 × 300 px │
│ (実際の解像度)│ │ (ピクセルが引き伸ばされて滲む)│
└──────────────┘ └────────────────────────────┘
特に <canvas> の width / height 属性を省略した場合、ブラウザ標準のデフォルト値として width="300" height="150" が自動的に適用されます。
CSSだけでサイズを指定してHTML側の属性を書いていない場合、300×150の描画結果がCSS上の表示サイズへ拡大・縮小されるため、輪郭がぼやけたり、描画が歪んで見えたりする原因になります。
Retina・高DPI画面でcanvasがぼやける理由
「HTMLの属性とCSSのサイズを同じ数値(例: 600×300)に揃えたのに、スマホやMacで見るとまだ少しぼやける」という場合があります。
これはRetinaやHiDPIディスプレイなど、高いdevicePixelRatioを持つ環境で起こりやすい現象です。
devicePixelRatioとは?
window.devicePixelRatio(DPR)は、「1つのCSSピクセルを描画するために、画面の物理ピクセル(デバイスピクセル)が何個使われているか」 を表す比率です。
console.log(window.devicePixelRatio);
devicePixelRatio は環境によって1、2、3だけでなく、1.25や1.5などの値になることもあります。
高DPI環境では1より大きな値になることが多く、その分だけCanvas内部にも十分な描画ピクセル数を確保する必要があります。
CSSピクセルと実際の描画ピクセル数は一致しないことがある
例えば、画面上の表示サイズが 300 × 150 CSS px で devicePixelRatio が 2 の場合、画面上では 600 × 300 個の物理ピクセルを使って細かく描画できる領域が用意されています。
しかし、Canvasの描画サイズを 300 × 150 のままにしておくと、ブラウザは「300×150 の描画結果」をデバイスピクセルに合わせて引き伸ばして表示するため、文字や細い線がぼやけて見えてしまいます。
devicePixelRatioを使ってcanvasを高解像度表示する
Retinaディスプレイでもくっきり鮮明に描画するための標準的な修正パターンです。
- Canvasの内部サイズを
devicePixelRatio倍する ctx.scale()で描画座標をCSSピクセル基準に戻す
<canvas id="canvas"></canvas>
/* CSSで画面上の見た目の大きさを決める */
#canvas {
width: 600px;
height: 300px;
}
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
// 1. 画面上の表示サイズを取得
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// 2. Canvas内部のビットマップサイズを物理ピクセル数(DPR倍)に設定
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
// 3. 描画コンテキストを拡大し、描画コード側の座標感覚をCSSピクセルと合わせる
ctx.scale(dpr, dpr);
// 4. 描画処理(座標はCSSピクセル感覚のままでOK)
ctx.fillStyle = '#333';
ctx.font = '24px sans-serif';
ctx.fillText('鮮明なテキスト描画', 20, 50);
ctx.fillRect(20, 80, 200, 100);
なぜctx.scale()が必要なの?
もし ctx.scale(dpr, dpr) を呼ばずに canvas.width だけを2倍(例: 1200×600)に広げた場合、描画領域自体のピクセル数が2倍になっているため、ctx.fillRect(0, 0, 100, 100) と指定して描いた四角形は、画面上で意図した大きさの半分(50×50 CSS px相当)に見えてしまいます。
ctx.scale(dpr, dpr) をあらかじめ実行しておくことで、描画コンテキスト全体の座標軸が拡大され、「JavaScriptの描画コードはCSSピクセル基準(見た目のサイズ)のまま書きつつ、内部的には高解像度なビットマップへレンダリングさせる」 ことが可能になります。
canvas.width・heightを書き換えると描画が消える理由
JavaScriptでcanvas.widthやcanvas.heightに値を代入すると、仕様上、Canvas内部のビットマップおよび描画状態(コンテキストの設定、現在のパス、scale()やfillStyleなどの状態)が初期化(クリア)されます。
// ✕ NG:描いた後にサイズを変更するとリセットされる
draw();
canvas.width = 600; // この瞬間に描画内容とコンテキスト設定が消える
// ◯ OK:サイズ設定 → コンテキスト設定 → 描画 の順に行う
canvas.width = 600;
canvas.height = 300;
ctx.scale(dpr, dpr);
draw();
高DPI対応を行う際は、必ず 「サイズ決定 → scale() 適用 → 描画実行」 という順番を守る必要があります。
レスポンシブなcanvasではリサイズ時にも再設定する
画面幅に応じて可変するレスポンシブなCanvasを作る場合は、ウィンドウのリサイズ時にも同様の再設定と再描画を行います。
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
function setupAndDraw() {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// 内部サイズを再設定(※これによって既存の描画はクリアされる)
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
// 座標系のスケールを再設定
ctx.scale(dpr, dpr);
// 再描画
render();
}
function render() {
ctx.fillStyle = '#0066cc';
ctx.fillRect(20, 20, 150, 80);
}
// 初回実行
setupAndDraw();
// 画面サイズ変更時に追従
window.addEventListener('resize', setupAndDraw);
※ より柔軟に特定の親コンテナの伸縮を監視したい場合は、ResizeObserver を用いて要素単位でリサイズを監視する設計も実務的です。
imageSmoothingEnabled=falseでは高DPI対応にならない
Canvasがぼやけたときの対策を検索していると、ctx.imageSmoothingEnabled = false; という設定を見かけることがあります。
ctx.imageSmoothingEnabled = false; // これだけでRetina対応になるわけではない
MDNの仕様解説にある通り、imageSmoothingEnabled は 「画像を拡大・縮小描画する際に、ブラウザが画像をスムージング(平滑化)するかどうか」 を切り替えるプロパティです。
true(初期値):拡大・縮小する画像を滑らかに補間するfalse:画像のスムージングを無効にする。ピクセルアートなど、拡大時にもピクセルの輪郭を残したい場合に使われる
これはあくまで 「拡大・縮小する画像をスムージングするかどうか」の設定 であり、Canvas全体のビットマップ解像度を引き上げるものではありません。
通常のテキストや図形の高精細化には前述の devicePixelRatio によるサイズ調整が必要です。
canvasがぼやけるときの確認点
Canvasの描画がぼやけてしまう場合は、以下のチェックリストを順に確認してください。
1. CSSだけでcanvasを拡大していないか
HTMLの width / height 属性を省略して CSS だけで大きくしていないか確認します。
2. canvasのwidth・heightを確認したか
canvas.width に設定したピクセル数が、画面上の表示サイズに対して小さすぎないか確認します。
3. devicePixelRatioを考慮しているか
canvas.width = rect.width * dpr のように、デバイスピクセル比を掛け合わせた解像度を確保しているか確認します。
4. リサイズ後に再描画しているか
canvas.width を書き換えた後、ctx.scale(dpr, dpr) を再適用して render() 関数を呼び直しているか確認します。
5. imageSmoothingEnabledだけで直そうとしていないか
スムージングの無効化設定と、解像度自体の引き上げを混同していないか確認します。
canvasのサイズ・devicePixelRatio・scaleの役割の違い
Canvasの高解像度化に関わる各プロパティの役割を整理すると以下のようになります。
| 項目 | 設定場所 | 主な役割 |
|---|---|---|
canvas.width / canvas.height |
HTML属性 / JS | 描画用ビットマップの実際の解像度(ピクセル数)を決める |
CSS width / height |
CSS | ブラウザ画面上で占めるレイアウト上の表示サイズを決める |
window.devicePixelRatio |
JS(ブラウザ環境値) | 1 CSSピクセルに対する画面の物理ピクセルの比率を取得する |
ctx.scale(dpr, dpr) |
JS(描画コンテキスト) | 描画コードの座標感覚をCSSピクセル基準に保つ |
ctx.imageSmoothingEnabled |
JS(描画コンテキスト) | 拡大・縮小する画像をスムージングするかどうかを制御する |
まとめ
Canvasの画像や文字がぼやける問題は、「内部ビットマップサイズ」「CSS表示サイズ」「ディスプレイのDPR」の関係性を正しく合わせることで解決できます。
- 基本原因:HTMLの描画サイズとCSSの表示サイズが乖離している、または高DPIディスプレイの物理ピクセル数に内部解像度が足りていない
高解像度化の手順:
- 表示サイズに
devicePixelRatioを掛けた値をcanvas.width/heightに設定する ctx.scale(dpr, dpr)で座標系をCSSピクセルに合わせる- その後に描画を行う
- リセット仕様の把握:Canvasのサイズ変更は描画内容やコンテキスト設定を初期化するため、サイズ変更後に必ず再描画を行う
ファイル選択(<input type="file">)や URL.createObjectURL() で読み込んだ画像をCanvas上でトリミング・加工する際にも、この高解像度化の基礎知識が重要になります。
URL.createObjectURL()を使って選択した画像をブラウザ上でプレビューする方法については、こちらの記事で解説しています。