マガジン

高解像度画面でぼやける理由:CSSのピクセルと物理のピクセル

遊びを見つける

執筆にClaudeを使用しています。

正方形のタイルで芽のキャラクターを組み、拡大鏡で輪郭を確かめる写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

幅320、高さ180のゲーム画面を作り、線をくっきり描いたつもりでいる場面を考えてみます。ふだんのパソコンでは問題なく見えるのに、高解像度のノートパソコンやスマートフォンで開くと、線の縁がうっすらにじんで見えます。コードは同じで、画面の大きさ(320×180)の指定も変えていません。それでも見え方だけが変わります。 この記事が確かめるのは、「1ピクセル」という言葉が実は2種類あることです。一つは、CSSで width: 320px と書くときの「CSSピクセル」です。もう一つは、液晶や有機ELの画面に実際に並んでいる発光点で、「物理ピクセル」と呼びます。両者の比率を教えてくれる値が window.devicePixelRatio です。この値が何を表しているかが分かると、にじみの原因を自分で切り分けられるようになります。 記事の後半では、自分の環境で値を確かめる手順と、canvas(JavaScriptで絵を描くHTML要素)をこの比率に合わせる最小のコードを示します。数値の結果は環境ごとに違うので、どう出るかは読者の画面で確かめる形にします。

この記事の目次
  1. devicePixelRatioが表しているもの
  2. なぜぼやけるのか:ビットマップが引き伸ばされる場合
  3. 自分の画面で確かめる手順
  4. canvasをdprに合わせる最小コード
  5. 判断基準:どこまで高精細にするか
  6. 公式作品を開いて確かめること
  7. 次の行動
  8. 参考資料

devicePixelRatioが表しているもの

devicePixelRatio(以下dpr)は、おおまかに言うと「1つのCSSピクセルを、何個分の物理ピクセルで表示するか」という比率です。縦も横も同じ比率で数えます。

  • dprが1の画面では、1CSSピクセルが物理ピクセル1個にあたります。
  • dprが2の画面では、1CSSピクセルは縦2個×横2個、つまり4個の物理ピクセルで表示されます。
  • dprが3の画面では、縦3個×横3個の9個で表示されます。

CSSで width: 320px と書いた要素は、dprが2の画面でも「320 CSSピクセル分」の大きさで表示されます。ですから見かけの大きさはほぼ変わらず、中身を描くための物理ピクセルだけが増えます。高解像度画面で文字や図形が細かく滑らかに見えるのは、この増えた物理ピクセルを使えるからです。

dprは画面そのものの性質だけで決まるわけではありません。一般的なデスクトップブラウザでは、ブラウザの拡大・縮小(ズーム)を操作するとdprの値も変わります。100%の表示でdprが2の画面なら、200%に拡大した状態ではdprが4前後になる、という具合です。ズームしたときの値の動きは、ブラウザやOSの組み合わせで違うことがあるため、後の手順で自分の環境の値を見ておくと確実です。

また、dprは1.25や1.5のような整数でない値になることもあります。OSの表示スケール設定が125%や150%のときが典型です。この「割り切れない比率」が、後で述べるにじみの主な原因になります。

なぜぼやけるのか:ビットマップが引き伸ばされる場合

ぼやけは、「元の絵の画素数」と「表示に使われる物理ピクセル数」が一致しないときに起きます。ここでは、ブラウザが引き伸ばす対象によって二つに分けて考えます。

画像やcanvasは「画素の集まり」

PNGやJPEGなどの画像、そしてcanvasの内部は、画素(ドット)の並びで絵を持っています。これをビットマップと呼びます。ビットマップは、持っている画素数と画面上の物理ピクセル数が違うと、ブラウザが補間して引き伸ばします。補間とは、隣り合う画素の色を混ぜて、足りない画素を埋める処理です。この混ぜ合わせが「にじみ」として見えます。

冒頭の場面を数字で見てみます。canvasの内部を320×180で作り、CSSでも320×180の大きさに表示したとします。dprが1の画面なら、画素と物理ピクセルが1対1で対応するので、くっきり見えます。dprが2の画面では、同じ320×180の画素を、横640×縦360の物理ピクセルに引き伸ばして表示することになります。1画素を2×2の領域に広げる過程で補間が入り、縁が柔らかくなります。

dprが1.5のような整数でない場合は、さらに事情が複雑です。1画素が物理ピクセル1.5個分にあたるため、画素の境目と物理ピクセルの境目がそろいません。細い1ピクセル幅の線は、隣の物理ピクセルにまたがって薄く塗られ、にじみが目立ちやすくなります。

文字やベクターは物理ピクセルに合わせて描かれる

一方、HTMLの文字やSVG、CSSで描いた枠線や角丸は、ビットマップではありません。ブラウザが表示のたびに、物理ピクセルの数に合わせて描き直します。そのため、同じページの中でも「文字はくっきりしているのに、canvasの中の文字だけぼやける」ということが起こります。ぼやけを見つけたら、まず「これはビットマップか、ブラウザが描いているものか」を区別するのが最初の切り分けになります。

自分の画面で確かめる手順

次の手順は、どれも読者の環境で行えます。結果は環境ごとに違うので、メモを取りながら進めてください。

  1. ブラウザの開発者ツール(多くのブラウザでF12キー、またはメニューの「開発」系の項目から開けます)を開き、コンソールに window.devicePixelRatio と入力して値を確認します。
  2. 同じ画面でブラウザのズームを変え(例:90%、110%、125%、150%)、そのたびに同じ値を入力して、値がどう動くかを記録します。
  3. OSの表示スケール設定を変えられる環境なら、スケールを変えたときの値も記録します。設定を変えると画面表示が大きく変わるので、元の設定をメモしてから行ってください。
  4. 次の節のコードを自分のHTMLファイルとして保存して開き、ズームを変えながら、図形と文字の縁の見え方を見比べます。

記録は、次のような表にまとめると読み取りやすくなります。

  • ズーム100%、dpr:(自分の値)、見え方(くっきり/少しにじむ/にじむ):(自分の観察)
  • ズーム125%、dpr:(自分の値)、見え方(くっきり/少しにじむ/にじむ):(自分の観察)
  • ズーム150%、dpr:(自分の値)、見え方(くっきり/少しにじむ/にじむ):(自分の観察)

結果の読み方は次のとおりです。

  • dprが整数(1、2、3)のときにくっきりして、1.25や1.5のときだけにじむなら、割り切れない比率が原因の可能性が高いと言えます。
  • どの値でもcanvasの中だけがぼやけ、周囲の文字はくっきりしているなら、canvasの内部解像度が足りていない可能性が高いと言えます。
  • ズームを変えても値が変わらない環境もあります。その場合は、その環境ではズームがdprに反映されない仕組みだと分かります。

起こりやすい誤りは、画面のスクリーンショットだけで判断することです。スクリーンショットは保存時に拡大縮小されることがあり、見え方が実際とずれます。判断は実際の画面を目で見て行い、必要なら拡大鏡機能などで縁を確認してください。

canvasをdprに合わせる最小コード

canvasのにじみを減らす基本は、「表示サイズ(CSSピクセル)はそのまま、内部の画素数だけをdpr倍にする」ことです。次のコードは単独のHTMLファイルとして動きます。外部ライブラリもサーバーも使いません。

html
<canvas id="c" style="border:1px solid #888"></canvas>
<p id="info"></p>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const info = document.getElementById('info');

function draw() {
  const dpr = window.devicePixelRatio || 1;
  const cssW = 320, cssH = 180;
  canvas.width = Math.round(cssW * dpr);
  canvas.height = Math.round(cssH * dpr);
  canvas.style.width = cssW + 'px';
  canvas.style.height = cssH + 'px';
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, cssW, cssH);
  ctx.lineWidth = 1;
  ctx.strokeStyle = '#224';
  ctx.strokeRect(20.5, 20.5, 100, 60);
  ctx.beginPath();
  ctx.arc(220, 90, 40, 0, Math.PI * 2);
  ctx.stroke();
  ctx.font = '20px sans-serif';
  ctx.fillText('Hello', 20, 140);
  info.textContent = 'devicePixelRatio = ' + dpr +
    ' / canvas内部 ' + canvas.width + '×' + canvas.height;
}
draw();
window.addEventListener('resize', draw);
</script>

ポイントは3つあります。

  • canvas.width と canvas.height は内部の画素数で、canvas.style.width と style.height は表示サイズです。この2組を別々に決めるのが要点です。
  • ctx.setTransform(dpr, 0, 0, dpr, 0, 0) により、以降の描画座標はCSSピクセルのまま書けます。座標の計算をdpr倍に書き換える必要はありません。
  • 最後の resize の登録は、ズームや画面の移動に追従するためです。ズーム時に resize が発火するかどうかはブラウザで違うことがあるので、自分の環境でズームを変えて、画面下の表示が更新されるか確かめてください。

画面下の表示には、dprと内部の画素数が出ます。たとえばdprが2なら、内部は640×360になるはずです。これは掛け算で出せる値ですが、実際の表示は読者の画面で確認してください。

なお、わざと粗い画素を見せたいドット絵のゲームでは、逆の判断になります。内部解像度を低いままにして、CSSの image-rendering: pixelated を指定すると、補間によるにじみを抑え、画素の角を保ったまま拡大できます。ただし、dprが整数でない環境では画素の大きさが不ぞろいに見えることがあります。意図した見た目になるか、複数の倍率で確かめてください。

判断基準:どこまで高精細にするか

ぼやけを避けるために解像度を上げると、別の費用が発生します。

  • 描画の負荷が増えます。 dprが2なら、描く画素数は縦横2倍で4倍、dprが3なら9倍です。毎フレーム描き直すゲームでは、端末によっては動きが重くなります。
  • 素材の容量が増えます。 画像を2倍・3倍の大きさで用意すると、ファイルが大きくなります。

アソバースに作品をアップロードする場合は、容量の上限が判断材料になります。確認済みの範囲では、アップロードは20MiB以内、ZIP展開後は合計100MiB・1,000ファイル以内、1ファイル30MiB以内で、直下のindex.htmlが入口、素材は相対パスで参照します。高解像度の画像を何種類も同梱する場合は、この上限の中に収まるかを先に見積もってください。

現実的な落としどころは、次のような方針です。

  1. 線や文字、図形のように後から描き直せるものは、dprに合わせてcanvasやCSSで描く。
  2. 画像素材は、必要な場面だけ高解像度版を用意する。HTMLの srcset 属性を使うと、画面の比率に応じて画像を切り替えられます。
  3. 負荷が心配なときは、Math.min(window.devicePixelRatio, 2) のように上限をかけ、描画量を抑える。
  4. ドット絵の作品は、にじませない拡大を選ぶ。

公式作品を開いて確かめること

アソバースの公式作品13本は、すべて独立したHTMLで外部通信なしに動く、と確認されています。共通の仕様として、プレイ画面の拡大で進行を保つこと、「動きを抑える」設定に対応していることも確認済みです。ただし、各作品が内部でcanvasを使うのか、DOMで描くのか、dprをどう扱っているかは、確認済み事項には含まれていません。この部分は作品を開いて確かめることになります。

確かめる方法として、同じ作品を、ブラウザのズームを変えて開き直し、図形や文字の縁の見え方を比べてみてください。「プレイ画面の拡大」がブラウザのズームとどう関係するかも、操作して見比べれば分かります。

また、カバー画像は生成したアートワークで、ゲーム画面のスクリーンショットではないと確認されています。カバー画像の見え方だけで、ゲーム内の描画の精細さを判断することはできません。

アソバースのゲームは、sandbox付きのiframe(ページの中に別のページを埋め込む枠)で実行されます。このiframeの中で window.devicePixelRatio がどの値になるかは、作品の中に上記のコードを入れた小さな確認用ページを投稿前に作って、自分で表示を見るのが確実です。

次の行動

まず、自分のブラウザの開発者ツールで window.devicePixelRatio の値を確認し、ズームを変えて値がどう動くか表に記録してください。次に、この記事のコードを保存して開き、dprが整数のときと整数でないときで、図形と文字の縁がどう違って見えるかを見比べます。作る側の人は、自作のcanvasに setTransform を加えて、見え方の差が縮まるかを確かめてみてください。

参考資料

  • MDN Web Docs「Window: devicePixelRatio プロパティ」
  • MDN Web Docs「Canvas API」および「CanvasRenderingContext2D: setTransform()」
  • MDN Web Docs「image-rendering」
  • MDN Web Docs「レスポンシブイメージ(srcset 属性)」