CSS Gridレイアウト内のHTML Canvas(react-konva)をレスポンシブ対応させる方法

今回作りたかったのは、CSS GridのGridアイテム内にHTML Canvasを組み込んだレイアウトです。でも、これがなかなかうまくいかず、悪戦苦闘してようやく対応させることができました。以下はその備忘録です。

以下は基本的なコードの抜粋です。Gridレイアウトは、1024px以上のディスプレイでは2カラム、それ以下のサイズでは1カラムのレイアウトになります。CSS部分は、Tailwind CSSを使用しています。また、Canvasを記述するのに、react-konvaを使用しています。

react-konvaについては説明を省きますが、簡単に言うと、CanvasReactでも手軽に記述できるようにしたパッケージです。react-konvaを使用しないCanvas、つまりVanilla Canvasでも、記述方法が違うだけでやることは同じです。

以下のコードでは、useLayoutEffectフックの中でcheckSize関数をEventListernerとして登録し、ウィンドウがリサイズされると、checkSize関数が呼び出される仕組みになっています。そしてcheckSize関数で取得したウィンドウのサイズを元に、Canvasエレメントのscale属性(scaleXとscaleY)を調整しています。

親コンポーネント(抜粋)

// 2カラム→1カラムのGridレイアウト
<div className="grid grid-cols-1 lg:grid-cols-2 mx-auto max-w-7xl">
	<Konva />
	<div>
     Lorem ipsum...
	</div>
</div>

子コンポーネント

// Konvaコンポーネント

import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';

const BASE_WIDTH = 800;
const BASE_HEIGHT = 800;

export default function Konva() {
  const [dimensions, setDimensions] = React.useState({
    width: 1,
    height: 1
  });
  const divRef = React.useRef(null);
  const checkSize = () => {
    if (divRef.current?.offsetHeight && divRef.current?.offsetWidth) {
      setDimensions({
        width: divRef.current.offsetWidth,
        height: divRef.current.offsetHeight
      });
    }
  };

  React.useLayoutEffect(() => {
    checkSize();
    window.addEventListener('resize', checkSize);

    return () => window.removeEventListener('resize', checkSize);
  }, []);

  const scale = dimensions.width / BASE_WIDTH > 1 ? 1 : dimensions.width / BASE_WIDTH;

  return (
    <div
      ref={divRef}
    >
      <Stage
        width={BASE_WIDTH}
        height={BASE_HEIGHT * scale}
        scaleX={scale}
        scaleY={scale}
      >
        <Layer>
          <Circle
            x={BASE_WIDTH / 2}
            y={BASE_HEIGHT / 2}
            radius={300}
          />
        </Layer>
      </Stage>
    </div>
  );
}

原因と対処方法

Canvas内に描かれている円(Circleエレメント)をレスポンシブ対応、つまりウィンドウのサイズに応じて拡大縮小したかったのですが、うまく拡大縮小してくれません。具体的に言うと、ウィンドウのサイズが小さくなって1カラムになると、円のサイズが元のサイズのまま動きません。

どうやら、個々のGridアイテム内にあるCanvasのサイズが、元々のGridアイテムのサイズを超えるとレスポンシブにならないようです。Stageエレメントのサイズをいじってみたりしたのですが、うまくいかず難儀しました。

色々と調べてみた結果、Stageエレメントの親となるdivエレメントのclassName属性に、min-w-0min-width: 0) を追加したら、うまくレスポンシブしてくれました。min-widthは必ずしも0である必要はなく、任意の数値で構いません。

<div
   ref={divRef} className="min-w-0" // min-width: 0
>

総括すると、CanvasCSS Gridレイアウトとの親和性があまりいいとは言えません。CSS Grid内に図形等を描画してレスポンシブ対応させるなら、SVGの方が適していると思います。

参考URL: Canvas expands when inside a CSS grid #2447