imgタグの画像を切り替えるとき、アスペクト比を維持しつつリサイズする

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)をとっている。

imgsrcが設定された時に、画像が表示されるが、画像がdivの領域より小さいと、そのままの小さいサイズで表示される。 このときに、divの領域に拡大して表示したい。かつ、アスペクト比は維持したい。こんな感じ:

アスペクト比を保ちつつ拡大するイメージ図

縦に長いか横に長いかの2通りと、空間より小さいか大きいかの2通りがあるので、パターンとしては4通りある。 実装時の注意点は次のものなどがある:

  • 外部パッケージを使わないときは、画像の情報を取得しにくい
  • 表示するとき、widthheightをまとめて指定するとアスペクト比の維持が困難

サンプルというか、結論のを使って作ったページ:

React App
Please use this app to pad extra space, or do something

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を利用するので、reactuseEffectを使う。

横長の場合は、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-sizeURLStreamBufferからの読み取りに対応している。streamの読み取りをブラウザで簡単にやる方法が思いつかなかったので、これは試してない。

URLからの読み込みについては、画像自体はローカルから読み込むので、URLがあるとすれば必然的にfileReader.readAsDataURL()で読み込んだものになる。ところが、これを渡しても読み込んではくれなかった。失敗。

Bufferからの読み込みについては、nodefsモジュールで読み込んだnodeBufferクラスのインスタンスが必要であり、ブラウザ利用でわざわざ用意するのも抵抗があったので、却下。 ブラウザで気軽に取得できるArrayBufferではダメなようだった。

以上から、probe-image-sizeは使わなかった。

imgタグを使う

どうしようと思い悩んだ結果、imgタグなら画像を設定したあとに、widthheightが設定されているようなので、これを利用することにした。参考: HTML Standard

読み込みした後であれば、取得できそう。取得時に幅高さを指定しているとその値を取得してしまうので、何も設定していない状態から取得する必要がある。

ひとまず画像情報の取得はOK。

表示させる

表示に関しては、親のdiv要素の幅か高さのいずれかを合わせて表示させたいと考えている。

画像と空間の幅と高さのそれぞれの比からどれだけ伸ばせばいいかを決定すればいい。大きいものは縮小することになるが、まあ問題はないだろう。 空間にアスペクト比を維持しつつぴったりフィットすればいいので、幅と高さの比で小さい方を使えばいい。

style属性を使って設定することにした。widthheightは取得の関係上あまり手を加えるべきではない。

また、style属性で幅高さを指定すると、widthheightも変化するので、新しい画像が設定されたときには、widthheightを戻すため一旦styleでの指定を解除する必要がある。 このため、styleの切り替えで一瞬小さく表示されたり、アスペクト比が狂っているように見えてしまうことがある。端末の性能によっても見えやすかったりすると思う。 これを回避するには、opacity: 0;などで見えなくしてから調整してもう一度映し出すというのが、手っ取り早い。これはまだ実装してないが。

おわり

意外と短く済んだ。表示させる段階で結構無駄なことをしてしまったけど、書いておくほどのことでもないので短くなった。

probe-image-sizeを使おうと最初に思ったのは失敗だった。imgタグでも十分できる。

個人的には、画像設定してスライドショー的に放っておくのが好き。まだまだファイル選択の方法にも改善の余地がありそうなので、その辺はなんとかしていきたい。フォルダごと突っ込むAPIもあるのだが、上限が決まっているようで断念した。50件だったかな。

以上です。


頭の体操

https://amzn.to/3kOVCDl

コメント

タイトルとURLをコピーしました