1つのimg
タグを使って代わる代わる画像を表示するとき、アスペクト比を維持しつつ枠いっぱいに表示する。(Javascript
, react
)
2021/09追記
リサイズのことを忘れていた。イベントリスナーを追加してリサイズすればいい:
// 結論にある、useEfect()のなかで、
img.addEventListener("load", justifyImg);
// 上の行のあとに、
let reqId = null;
const debouncedJustify = (e) => {
if (reqId != null) {
window.cancelAnimationFrame(reqId);
}
reqId = window.requestAnimationFrame(() => {
justifyImg();
reqId = null;
});
};
window.addEventListener("resize", debouncedJustify);
//を追加して、
return () => {
img.removeEventListener("load", justifyImg);
// 以下もreturnで返す関数に含める。
window.removeEventListener("resize", debouncedJustify);
};
ウィンドウをじっくりリサイズされると、resize
イベントが何度も発生するので、描画済みでない以前の予約は取り消すようにする。
2021/09追記はここまで。
実現したい仕様
react
を使っていて、div
のなかにimg
を設置している。このdiv
は、ウィンドウいっぱいという広めの空間(横:100vw
、縦:100vh
)をとっている。
img
のsrc
が設定された時に、画像が表示されるが、画像がdiv
の領域より小さいと、そのままの小さいサイズで表示される。
このときに、div
の領域に拡大して表示したい。かつ、アスペクト比は維持したい。こんな感じ:
縦に長いか横に長いかの2通りと、空間より小さいか大きいかの2通りがあるので、パターンとしては4通りある。 実装時の注意点は次のものなどがある:
- 外部パッケージを使わないときは、画像の情報を取得しにくい
- 表示するとき、
width
とheight
をまとめて指定するとアスペクト比の維持が困難
サンプルというか、結論のを使って作ったページ:
PCから利用を推奨。画像をドロップして試せる。複数のアスペクト比の異なる画像だとわかりやすい。
結論
長くなりそうなので、先に結論を書く。react
を使っている前提に注意:
function ImageComp(props) {
let Img = null;
// 画像が指定されてないときは何も表示しない。
if (props.imageSrc != null) {
Img = <img id="image" className="imagescls" alt="..." style={...} />;
}
useEffect(() => {
const img = document.getElementById("image");
if (img == null) {
return;
}
// divの取得
const container = img.parentElement;
// イベントリスナを使用して、サイズを調整。引き伸ばして枠に合わせる。
const justifyImg = (e) => {
if (img == null || img.width === undefined) {
return;
}
// 以前のサイズの読み取りを防ぐ
img.style.width = "";
img.style.height = "";
// 画像と空間の縦横それぞれのアスペクト比が必要
const widthRatio = container.clientWidth / img.width;
const heightRatio = container.clientHeight / img.height;
// 片方だけ合わせる
if (widthRatio < heightRatio) {
img.style.width = `${img.width * widthRatio}px`;
} else {
img.style.height = `${img.height * heightRatio}px`;
}
// 使い終わったらイベントリスナを削除
img.removeEventListener("load", justifyImg);
};
img.addEventListener("load", justifyImg);
if (props.imageSrc) {
img.src = props.imageSrc;
}
return () => {
img.removeEventListener("load", justifyImg);
};
}, [props.imageSrc]);
return (
<>
{Img}
</>
);
}
DOMのイベントリスナーを使用して、画像の読み込み完了を検知する。DOMを利用するので、react
のuseEffect
を使う。
横長の場合は、width
を指定してheight
を指定しない。これによって高さは自動調節される。
縦長の場合は、逆にすることで横幅が自動調整される。
空間より大きい場合は、img
のCSS指定が:
.imagescls {
/* ... */
/* 親のdivと同じ幅高さまでしか広がらないようにする */
max-width: 100vw;
max-height: 100vh;
}
となっているので、空間に収まるように縮小される。空間とは親のdiv
要素の幅高さがつくる領域。
これらの指定は、画像の読み込み後に行わないと正しい幅高さを得られないため(以前の画像のを読み込むことがある)、イベントリスナを利用して読み込み完了を確実に終わらせておく。
使うときは、<ImageComp imageSrc={source} />
で要素を生成すればいい。
以降は、試行錯誤のメモ。
画像情報の取得
まずは第一段階の画像情報の取得から。取得できないと、適切なサイズへの計算ができない。
probe-image-sizeを使う
最初は縦横の長さの取得はどうしようと思い、probe-image-size - npmを使って、取得しようと試みた。
Electron
で少し使ったくらいだったし、ブラウザ上で使うのはどうかと思ったが試した。
結果としてはダメだった。バイナリの取得が無理だった。probe-image-size
はURL
とStream
とBuffer
からの読み取りに対応している。stream
の読み取りをブラウザで簡単にやる方法が思いつかなかったので、これは試してない。
URL
からの読み込みについては、画像自体はローカルから読み込むので、URLがあるとすれば必然的にfileReader.readAsDataURL()
で読み込んだものになる。ところが、これを渡しても読み込んではくれなかった。失敗。
Buffer
からの読み込みについては、node
のfs
モジュールで読み込んだnode
のBuffer
クラスのインスタンスが必要であり、ブラウザ利用でわざわざ用意するのも抵抗があったので、却下。
ブラウザで気軽に取得できるArrayBuffer
ではダメなようだった。
以上から、probe-image-size
は使わなかった。
imgタグを使う
どうしようと思い悩んだ結果、img
タグなら画像を設定したあとに、width
やheight
が設定されているようなので、これを利用することにした。参考: HTML Standard
読み込みした後であれば、取得できそう。取得時に幅高さを指定しているとその値を取得してしまうので、何も設定していない状態から取得する必要がある。
ひとまず画像情報の取得はOK。
表示させる
表示に関しては、親のdiv
要素の幅か高さのいずれかを合わせて表示させたいと考えている。
画像と空間の幅と高さのそれぞれの比からどれだけ伸ばせばいいかを決定すればいい。大きいものは縮小することになるが、まあ問題はないだろう。 空間にアスペクト比を維持しつつぴったりフィットすればいいので、幅と高さの比で小さい方を使えばいい。
style
属性を使って設定することにした。width
とheight
は取得の関係上あまり手を加えるべきではない。
また、style
属性で幅高さを指定すると、width
とheight
も変化するので、新しい画像が設定されたときには、width
とheight
を戻すため一旦style
での指定を解除する必要がある。
このため、style
の切り替えで一瞬小さく表示されたり、アスペクト比が狂っているように見えてしまうことがある。端末の性能によっても見えやすかったりすると思う。
これを回避するには、opacity: 0;
などで見えなくしてから調整してもう一度映し出すというのが、手っ取り早い。これはまだ実装してないが。
おわり
意外と短く済んだ。表示させる段階で結構無駄なことをしてしまったけど、書いておくほどのことでもないので短くなった。
probe-image-size
を使おうと最初に思ったのは失敗だった。img
タグでも十分できる。
個人的には、画像設定してスライドショー的に放っておくのが好き。まだまだファイル選択の方法にも改善の余地がありそうなので、その辺はなんとかしていきたい。フォルダごと突っ込むAPIもあるのだが、上限が決まっているようで断念した。50件だったかな。
以上です。
頭の体操
コメント