アンチエイリアスとサブピクセルレンダリング、devicePixelRatio
高解像度ディスプレイで画像がぼやける理由と1pxボーダーが消える理由を内部から説明でき、devicePixelRatioを使って画像とCanvasを鮮明に描けるようになる。論理ピクセルと物理ピクセルの対応、DPRによるスケール、アンチエイリアスの仕組みを原理から解説します。
- 1.CSSが扱う論理ピクセル(CSSピクセル)と画面の物理ピクセルは別物で、その比が devicePixelRatio。DPRが2なら CSS上の1pxは縦横2個ずつ計4個の物理ピクセルに展開され、CSSの座標と実際の発光素子は1対1ではない。
- 2.画像とCanvasがぼやけるのは、論理ピクセル単位で用意した素材を物理ピクセル数の多い面へ引き伸ばすため。intrinsicな画素数をDPR倍にして srcset の x記述子や Canvas のバッキングストア拡大で渡せば、1物理ピクセル=1ソース画素に揃い鮮明になる。
- 3.1pxボーダーが細く見えたり消えたりするのは、CSSの0.5pxや非整数座標が物理ピクセル境界に乗らず、アンチエイリアスで複数素子に薄く分配されるため。DPRで割った値やtransform scaleで物理1px相当を狙うと回避できる。
「ぼやける」「1pxが消える」の正体は同じ1本の軸
高解像度ディスプレイで起きる代表的な悩み、画像がぼやける・Canvasの線が眠い・1pxボーダーが細すぎる/消えるは、見た目はバラバラでも原因は1本の軸に集約できます。それは「CSSが書く座標の単位」と「画面が光らせる素子の単位」が一致していないことです。前者を論理ピクセル(CSSピクセル)、後者を物理ピクセル(デバイスピクセル)と呼び、両者の比を devicePixelRatio(DPR)が表します。本稿はこの2つの座標系の対応関係を土台に、画像とCanvasを鮮明に描く方法、そして1pxボーダー問題の発生機構を内部動作から整理します。ペイントとラスタライズが描画全体のどこに位置するかは ブラウザのピクセルパイプライン図解 を合わせて押さえてください。
論理ピクセルと物理ピクセル、そしてDPR
devicePixelRatio は 物理ピクセル数 ÷ 論理ピクセル数 です。値が2なら、CSS上の 1px は画面上では縦2個・横2個の計4個の物理ピクセルとして描かれます。なぜこんな段差が要るのかというと、画素密度が上がった画面でCSSの 1px をそのまま1物理ピクセルに割り当てると、文字もボタンも物理的に小さくなりすぎて読めなくなるからです。そこでOS/ブラウザは論理ピクセルを「だいたい一定の見かけ角サイズ」に保つ基準として定義し、密度の余剰をDPRという倍率に吸収させます。
| 概念 | 単位 | 誰が使うか | 例(DPR=2) |
|---|---|---|---|
| 論理ピクセル(CSSピクセル) | CSS px | CSS・JSのレイアウト座標 | 幅 360px |
| 物理ピクセル(デバイスピクセル) | 発光素子1個 | 実際の画面ハードウェア | 幅 720素子 |
| devicePixelRatio | 物理÷論理の比 | 両者の橋渡し | 2.0 |
注意したいのは、DPRは整数とは限らないことです。多くのスマートフォンは2.625や3など端数や3倍を取り、ブラウザのズーム操作でも値が動きます(ズーム150%はDPRに乗算的に効きます)。さらにDPRは実行中に変化しうる点も重要です。ウィンドウを別密度のモニタへドラッグした、ユーザーがズームした、といった場面で値が変わるため、一度読んだ値をキャッシュしっぱなしにすると描画解像度がずれます。
// DPRの変化を監視する。ズームやモニタ移動で発火する
function onDprChange(cb) {
const mql = matchMedia(`(resolution: ${devicePixelRatio}dppx)`);
mql.addEventListener("change", () => {
cb(devicePixelRatio);
onDprChange(cb); // 新しいDPRで張り直す(クエリ値が固定のため)
}, { once: true });
}
CSSの width: 100px が物理的に何個の素子になるかはDPR次第です。DPRが1なら100素子、2なら200素子、2.625なら262.5素子です。CSSは常に論理ピクセルで考え、物理ピクセルへの変換はブラウザが最終ラスタライズで行う、という二層構造を前提に置いてください。
アンチエイリアスとサブピクセルの分配
ラスタライザは、図形の輪郭が物理ピクセルの格子の途中を通るとき、その素子を中間的な濃さで塗ります。これがアンチエイリアスです。たとえば斜め線や曲線がピクセル境界をまたぐと、被覆率(その素子を図形がどれだけ覆うか)に応じてアルファを按分し、階段状のギザギザを目立たなくします。鮮明さの議論はすべて「輪郭が物理格子に乗るか、途中を通るか」に帰着します。
ここで紛らわしいのが「サブピクセル」という語の二義性です。実務で問題になるのは主に前者です。
| 呼び方 | 意味 | 関係する症状 |
|---|---|---|
| サブピクセル位置(幾何) | 物理格子の整数境界からずれた小数座標 | 線や画像のにじみ・1px問題 |
| サブピクセルレンダリング(LCD) | R/G/B各サブ素子を別々に使う文字描画手法 | テキストの色付き縁・回転で破綻 |
LCDサブピクセルレンダリング(ClearType等)は1つの物理ピクセルを構成するR・G・Bの帯を個別の濃淡素子とみなし、文字の横解像度を実効3倍にしてにじみを抑える手法です。ただし配列がRGB前提のため画面回転や非標準パネルで色割れし、近年は高DPI化で恩恵が薄れ、要素にtransformが掛かると無効化されることもあります。一方、幾何的なサブピクセル位置のずれはDPRに関わらず常に存在し、こちらが画像・Canvas・1px問題の主役です。
画像とCanvasの鮮明化
画像がぼやける機構は単純です。DPRが2の画面に幅360CSS pxで表示する画像は、物理的には720素子分の面を占めます。ここへ360画素しか持たない画像を流し込むと、ブラウザは1ソース画素を2素子へ引き伸ばし(バイリニア補間など)、結果として輪郭が複数素子へにじみます。解決は「物理ピクセル数に見合うソース画素を渡す」一点です。<img> では srcset の x記述子で密度別の素材を用意し、ブラウザにDPRで選ばせます。
<!-- DPRに応じてソースを選ばせる。2xは横720画素相当の素材 -->
<img src="logo.png"
srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x"
width="360" alt="ロゴ">
レイアウト上の表示サイズ(ここでは width="360")は論理ピクセルのまま据え置き、素材の中身だけを高密度化するのが要点です。表示幅とソース解像度を混同して箱まで大きくしてしまうと崩れます。CLSを避ける比率予約や loading/decoding の使い分けは 画像のデコードとレンダリング に、srcset/sizes を含む幅可変の作法は レスポンシブデザイン に分けてあります。
Canvasはさらに明示的な対処が要ります。Canvasにはバッキングストア(実際に画素を持つ内部ビットマップ。canvas.width/height)と表示サイズ(CSSの幅高)の2つがあり、両者がずれると拡大縮小が起きます。既定では canvas.width=300 を360CSS pxへ伸ばせばぼやけ、DPR=2なら本来720素子必要な面を300画素で埋めることになります。定石はバッキングストアをCSSサイズのDPR倍に取り、描画コンテキストをDPRでスケールして、座標は論理ピクセルのまま書けるようにする方法です。
function setupHiDPICanvas(canvas, cssW, cssH) {
const dpr = window.devicePixelRatio || 1;
// 内部解像度は物理ピクセルに合わせる
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
// 見かけのサイズは論理ピクセルで固定
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
const ctx = canvas.getContext("2d");
// 以降は論理ピクセル座標で描けば物理解像度で出る
ctx.scale(dpr, dpr);
return ctx;
}
鮮明さを生むのは canvas.width/height を物理ピクセル数に増やす操作で、これを怠ると ctx.scale だけ呼んでも内部解像度が足りずぼやけます。ctx.scale(dpr, dpr) の役割はあくまで描画座標を論理ピクセルに戻すための便宜です。両者は別の仕事なので片方だけでは不十分です。
1pxボーダー問題の発生機構
Retina系で「1pxの線が細すぎる/かすれる」と言われる現象は、実は2つの異なる原因が混在しています。
1つ目はDPR起因の見た目の細さ。CSSの border: 1px はDPR=3の画面では3物理素子分の太さで描かれ、これは設計上正しいのですが、デザイナーが期待する「ヘアライン(物理1素子の極細線)」より太く見えることがあります。逆に物理1素子ぴったりを狙って 1px / dpr(DPR=3なら約0.333px)を指定すると、輪郭が物理格子の整数境界に乗らず、アンチエイリアスで隣接素子へ薄く分配され、かすれとして現れます。
2つ目は非整数座標起因のにじみ。これはDPRが1の環境でも起きます。要素の位置やサイズが小数CSS pxになると、線の中心が物理格子の整数境界からずれ、被覆率按分で線が2素子へ半分ずつ塗られ、結果として薄くぼけた2pxのように見えます。Canvasで ctx.lineWidth=1 の縦線を整数x座標に引くと、線は中心が境界に乗るため左右へ0.5ずつ広がり、まさにこの現象が出ます。回避には座標を0.5ずらす(中心を素子中央に置く)のが古典的定石です。
| 症状 | 原因 | 対処の方向 |
|---|---|---|
| 線が太く感じる | 1px = DPR個の物理素子で描かれる | transform: scaleY(計算値) や 1px/dpr で薄く |
| 線がかすれる | 1px/dpr が物理整数境界に乗らず按分 | 整数物理素子へ吸着させる手法を使う |
| 線が2pxににじむ(DPR=1でも) | 小数座標で被覆率が左右へ分配 | 座標を整数化、Canvasは+0.5補正 |
実務でヘアラインを安定させる定番は、疑似要素に通常の1pxボーダーを描き、transform: scale() でDPRの逆数に縮める手法です。スケール変換は合成段で効くため按分の事故が起きにくく、物理1素子に近い線を比較的安全に得られます。
/* DPR非依存で物理1px相当のヘアラインを狙う */
.hairline { position: relative; }
.hairline::after {
content: "";
position: absolute; inset: 0;
border-bottom: 1px solid #ccc;
transform: scaleY(0.5); /* DPR=2想定。実際はメディアクエリで分岐 */
transform-origin: bottom;
}
@media (min-resolution: 3dppx) {
.hairline::after { transform: scaleY(0.3333); }
}
1px問題に当たったら、DPR=1のモニタでも症状が出るかを最初に確認してください。出るなら小数座標のにじみで、レイアウト値の整数化やCanvasの0.5補正が効きます。高DPI機でだけ出るならDPR由来の太さ/かすれで、scale手法やメディアクエリでの分岐が筋です。原因が違えば対処も違うため、この切り分けを飛ばすと的外れな修正になります。
transformやscaleがなぜ合成段で安価に効くのか、どの操作がレイヤーを生むかは ブラウザのレイヤー化とGPUコンポジット を参照してください。
まとめ
症状が画像のぼやけでもCanvasの眠さでも1pxの消失でも、根は論理ピクセルと物理ピクセルの座標系のずれです。両者の比が devicePixelRatio で、DPR=2なら CSSの1pxは4物理素子に展開され、座標と発光素子は1対1ではありません。輪郭が物理格子の途中を通るとアンチエイリアスが被覆率で複数素子へ濃さを按分し、これがにじみ・かすれの正体です。画像は srcset の x記述子で物理ピクセル数に見合うソースを渡し、CanvasはバッキングストアをDPR倍に取って ctx.scale(dpr, dpr) で座標を戻せば鮮明になります。1pxボーダーは「DPR由来の太さ/かすれ」か「小数座標のにじみ(DPR=1でも出る)」かをまず切り分け、前者は transform: scale とメディアクエリ、後者は座標整数化や0.5補正で対処します。画像周りの取得・デコードは 画像のデコードとレンダリング、ラスタライズの位置づけは ブラウザのピクセルパイプライン図解 で補完してください。
Web/フロントエンド Article
アンチエイリアスとサブピクセルレンダリング、devicePixelRatioを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
devicePixelRatio
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 6
導入後に効く点
画像とCanvasがぼやけるのは、論理ピクセル単位で用意した素材を物理ピクセル数の多い面へ引き伸ばすため。intrinsicな画素数をDPR倍にして srcset の x記述子や Canvas のバッキングストア拡大で渡せば、1物理ピクセル=1ソース画素に揃い鮮明になる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 6
判断チェックリスト
- 自社の用途が「devicePixelRatio / Canvas」に近いか確認する。
- 強みである「CSSが扱う論理ピクセル(CSSピクセル)と画面の物理ピクセルは別物で、その比が devicePixelRatio。DPRが2なら CSS上の1pxは縦横2個ずつ計4個の物理ピクセルに展開され、CSSの座標と実際の発光素子は1対1ではない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。