canvasを動的にリサイズさせる
canvasに線などを描画しているときにウィンドウや親要素のサイズが変更すると,画像のサイズはそのままに描画領域の大きさだけを自動でリサイズするcanvasが必要になったので作ってみました.画像が勝手に伸縮したりぼやけたりするのを防ぐために,divラッパーやResize Observerを使い実装しました.
canvas要素の大きさと画像の大きさの関係
canvas要素では,画像や図形を「キャンバス」といった領域に描画します.そして,そのキャンバスを実際に画面で表示される領域の大きさに合わせて表示します.つまりキャンバスと実際に表示される領域の大きさを適切に設定しないと,画像が伸縮して表示されてしまいます.
例えば正方形の大きさのキャンバスを用意し,そこに棒人間を描いたとします.canvas要素は画面上に表示される自分自身の大きさに合わせて,このキャンバスに書かれた画像を表示します.ここでcanvas要素の表示領域の大きさがキャンバスと異なり長方形だった場合,棒人間の画像は伸縮した状態で画面上に表示されます.
canvasの2種類のwidth/height
キャンバスの大きさとcanvas要素の表示領域の大きさを設定するために,canvas要素には2種類のプロパティを持ちます.1つ目はDOM要素のプロパティとしてのwidth
/height
,2つ目はCSSプロパティとしてのwidth
/height
です.
DOM要素のプロパティのwidth
/height
はcanvasが保持するキャンバスの大きさを指定します.キャンバスの大きさはピクセル単位で設定します.
CSSプロパティのwidth
/height
は実際に画面に描画されるcanvas要素の大きさを指定します.つまり,他のDOM要素のCSSプロパティのwidth/heightと同じ効果です.0.75
や50%
,200px
,10rem
といった形で設定します.
canvas要素でキャンバスに描かれた画像が縦横に伸縮しないように画面上に表示させるためには,この2種類のプロパティでアスペクト比が維持されるように設定する必要があります.
画面解像度と画像のぼやけの関係
キャンバスのサイズと画面表示上のサイズをうまく設定しても,表示された画像がぼやけて表示されてしまうことがります.これは仮想ディスプレイと実際のディスプレイの解像度が異なる場合に起こります.
ブラウザーなどで表示される要素のサイズは仮想のディスプレイに表示した際のサイズとして算出されます.このとき,仮想ディスプレイの画面解像度は実際のディスプレイの解像度と必ずしも一致しておらず,Retinaなどの高解像度ディスプレイなどでは仮想ディスプレイ上の1ピクセルに対応する画素数よりも実際のディスプレイ上での画素数のほうが多くなります.
この画面解像度の違いが実際のディスプレイへの画像の表示に影響を与えます.仮想ディスプレイで描画された画像を実際のディスプレイに表示するときは,画像の座標を実際のディスプレイ上での座標に変換します.ここで仮想ディスプレイと実際のディスプレイで画面解像度が異なると,座標の変換時に実際のディスプレイ上では小数点の位置に表示する点や線が表れたりします.実際のディスプレイでは小数点の位置にそれらを表示させることはできないため,ブラウザーはアンチエイリアシングを行い,色の濃淡を変えることで画像の滑らかな変化を表現しようとします.しかしこれは画像のぼやけを生み出す原因にもなります.
画像をぼやけさせずに表示するには,アンチエイリアシングが行われないようにします.そして,そのためにはキャンバスの大きさを実際のディスプレイの解像度におけるcanvas要素の表示領域の大きさに合わせます.
Resize Observerによる動的なサイズ変更
canvas要素で表示する画像を伸縮させず,ぼやけさせないように表示するには,キャンバスのサイズとCSSサイズを適切に設定することが必要だと分かりました.ここで,canvas要素の表示領域の大きさが親要素の大きさの変更などに応じて変わるとき,どのようにしてサイズのプロパティを変更すればいいでしょうか.
私が初めに思い付いたのは,canvasの親としてdiv要素のラッパーを追加し,そのdiv要素のresizeイベントでキャンバスサイズやCSSサイズを更新する手法でした.しかしresizeイベントでは,canvas要素の大きさを取得することはできても,実際のディスプレイにおけるサイズを取得することはできません.
const canvasRef = useRef<HTMLCanvasElement>(null);
<div onResize={/* イベントハンドラー */}>
<canvas ref={ref} style={{
width: "100%",
height: "100%,
}} />
</div>
この動作を行うにはResizeObserver
を使用します.ResizeObserver
は特定のDOM要素を監視し,DOM要素のサイズが変更されたときに,コールバック関数を呼び出します.
ResizeObserver
のコンストラクターで,監視対象のDOM要素のサイズが変更されたときのコールバック関数を渡します.コールバック関数の引数は監視対象のDOM要素 (ResizeObserverEntry
) の配列です.今回はdivラッパーひとつのみ監視するので,配列の先頭要素だけを取り出します.
また,実際のディスプレイにおけるDOM要素のサイズはResizeObserverEntry.devicePixelContentBoxSize
で取得することができます.ここからblockSize
とinlineSize
をキャンバスのサイズに設定することで,キャンバスの解像度を実際のディスプレイの解像度と一致させ,座標系の変換なしに画像をディスプレイに表示させることができます.なお,devicePixelContentBoxSize
は配列となっており,DOM要素が複数の段によって構成されるときはそれぞれの段のサイズが設定されていますが,canvas要素の場合は1要素しかないため,配列の先頭のデータのみ取得します.
const observer = new ResizeObserver(([entry]) => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const size = entry.devicePixelContentBoxSize[0];
canvas.height = size.blockSize;
canvas.width = size.inlineSize;
});
ResizeObserver.observe
でサイズ変更を監視させたいDOM要素を登録します.今回はcanvas要素の親であるdivラッパーを登録します.このメソッドの第2引数には監視のオプションをオブジェクトとして渡すことができます.オプションにはサイズ変更を検知するボックスモデルをbox
で指定できます.今回は実際のディスプレイにおける表示サイズが変更されたときにコールバック変数を実行したいので,device-pixel-content-box
を設定します.
observer.observe(container, { box: "device-pixel-content-box" });
成果物
これらを一つのコンポーネント "AutoResizedCanvas" としてまとめました.このコンポーネントは親コンポーネントの大きさに合わせてキャンバスのサイズが自動的に変化します.またpropsとして渡されるReact.RefObject
を通して,内部のcanvas要素を取得できます.キャンバスへの画像の描画はrefを通じて行うことが可能です.
PropsにはonResize
も設定できるようになっています.これはキャンバスのサイズが変更されたときに呼び出されるイベントハンドラーを設定します.キャンバスのサイズ変更時にキャンバスの描画内容を更新したいため,このようなpropを追加しました.
AutoResizedCanvas.tsx
動作例として,下のリンク先のオーディオプレーヤーでオーディオの波形を表示するのに使用しています.
まとめ
今回はResize Observerを使って,親DOM要素のサイズ変更に追従して動的にサイズが変わるキャンバスコンポーネントを作成しました.キャンバスのサイズとCSSでのサイズ,実際に表示されるディスプレイ上でのサイズを考慮することで,画像の伸縮やぼやけを起こさずにサイズを変更させることができました.
このコンポーネントは先に紹介したオーディオプレーヤーで使ったものをカスタムコンポーネントとして分離したものです.上手くカスタムコンポーネントとして実装できたので,今後のアプリ開発でも使ってみようと思います.
Discussion