1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita株式会社Advent Calendar 2024

Day 24

Reactで画像をテキストに合わせて切り抜く(SVGとCSSのclip-pathを使用)

Last updated at Posted at 2024-12-24

この記事の概要

こういったレイアウト、地味に実装の仕方に悩みませんか?

まして、要件に「画面サイズと文章の折り返しにあわせて切り抜き位置が変化する」だった場合、なおさら。

少し幅が狭い画面 もっと幅が狭い画面

これらを、Reactにおいて、SVGのとCSSのclip-pathを使って実現しようと思います。

技術的なチャレンジという意味合いが強い記事です。コードが複雑にもなりますし、useEffectによる副作用も発生しているので、使い所はご注意ください。

完成形

import { useEffect, useRef, useState, useCallback } from "react";
import "./App.css";

const RADIUS = 16;
const ARC_PATH = `${RADIUS} ${RADIUS} 0 0 1`;

function App() {
  const backgroundRef = useRef<HTMLImageElement | null>(null);
  const paragraphRef = useRef<HTMLParagraphElement | null>(null);
  const [clipPath, setClipPath] = useState("");

  const updateDimensions = useCallback(() => {
    const backroundCurrent = backgroundRef.current;
    const paragraphCurrent = paragraphRef.current;
    if (backroundCurrent && paragraphCurrent) {
      const backgroundWidth = backroundCurrent.offsetWidth;
      const backgroundHeight = backroundCurrent.offsetHeight;
      const paragraphWidth = paragraphCurrent.offsetWidth;
      const paragraphHeight = paragraphCurrent.offsetHeight;
      const newClipPath = `
        M${RADIUS} 0
        L${backgroundWidth - RADIUS} 0
        A${ARC_PATH} ${backgroundWidth} ${RADIUS}
        L${backgroundWidth} ${backgroundHeight - RADIUS}
        A${ARC_PATH} ${backgroundWidth - RADIUS} ${backgroundHeight}
        L${paragraphWidth + RADIUS} ${backgroundHeight}
        A${ARC_PATH} ${paragraphWidth} ${backgroundHeight - RADIUS}
        L${paragraphWidth} ${backgroundHeight - paragraphHeight + RADIUS}
        A${RADIUS} ${RADIUS} 0 0 0 ${paragraphWidth - RADIUS} ${backgroundHeight - paragraphHeight}
        L${RADIUS} ${backgroundHeight - paragraphHeight}
        A${ARC_PATH} 0 ${backgroundHeight - paragraphHeight - RADIUS}
        L0 ${RADIUS}
        A${ARC_PATH} ${RADIUS} 0
        Z
      `;
      setClipPath(newClipPath);
    }
  }, []);

  useEffect(() => {
    updateDimensions();
    window.addEventListener("resize", updateDimensions);
    return () => {
      window.removeEventListener("resize", updateDimensions);
    };
  }, [updateDimensions]);

  return (
    <>
      <svg width="0" height="0">
        <clipPath id="clip">
          <path d={clipPath} />
        </clipPath>
      </svg>
      <div className="container">
        <div className="graphic">
          <img
            alt=""
            className="background"
            onLoad={updateDimensions}
            ref={backgroundRef}
            src="https://picsum.photos/id/681/1920/1080"
            width={1920}
            height={1080}
          />
          <p className="paragraph" ref={paragraphRef}>
            The quick brown fox jumps over the lazy dog
          </p>
        </div>
      </div>
    </>
  );
}

export default App;
.container {
  width: min(100%, 960px);
}

.graphic {
  position: relative;
}

.background {
  clip-path: url(#clip);
  height: auto;
  max-width: 100%;
}

.paragraph {
  bottom: 0;
  font-size: 2rem;
  max-width: 60%;
  padding: 1rem 1rem 0;
  position: absolute;
}

全体を大まかに説明すると、以下のステップで成り立っています。

  1. 背景画像と前面の文章にrefを設定し、幅と高さを取得する
  2. 取得した幅と高さ+コンポーネント外で指定した角丸のパスを使って、切り抜きたい形のSVGのパスを定義する
  3. コンポーネント内でそのパスを使用し、idを付与し、CSSのclip-pathで呼び出す
  4. 画面のリサイズが発生した際は画像と文章のサイズを取得し直す
  5. 繰り返し

SVGのパスの書き方

SVGのパスは以下のようなコマンドで構成されています。

  • M x y - Move to(指定した座標に移動)
  • L x y - Line to(現在の位置から指定座標まで直線を引く)
  • A rx ry x-axis-rotation large-arc-flag sweep-flag x y - Arc(円弧を描く)
  • Z - Close path(パスを閉じる)

今回のコードも、色々と書いてありますが実質L, Aの組み合わせだけで作られています(M, Zは始点と終点といった意味で必要)。

そして、実際のコードを分解しながら説明します。

const RADIUS = 16;
const ARC_PATH = `${RADIUS} ${RADIUS} 0 0 1`;

const newClipPath = `
  M${RADIUS} 0
  L${backgroundWidth - RADIUS} 0
  A${ARC_PATH} ${backgroundWidth} ${RADIUS}
  L${backgroundWidth} ${backgroundHeight - RADIUS}
  A${ARC_PATH} ${backgroundWidth - RADIUS} ${backgroundHeight}
  L${paragraphWidth + RADIUS} ${backgroundHeight}
  A${ARC_PATH} ${paragraphWidth} ${backgroundHeight - RADIUS}
  L${paragraphWidth} ${backgroundHeight - paragraphHeight + RADIUS}
  A${RADIUS} ${RADIUS} 0 0 0 ${paragraphWidth - RADIUS} ${backgroundHeight - paragraphHeight}
  L${RADIUS} ${backgroundHeight - paragraphHeight}
  A${ARC_PATH} 0 ${backgroundHeight - paragraphHeight - RADIUS}
  L0 ${RADIUS}
  A${ARC_PATH} ${RADIUS} 0
  Z
`;

改めて上記が全体像です。

const RADIUS = 16;
const ARC_PATH = `${RADIUS} ${RADIUS} 0 0 1`;

円弧のパスは何度も使うので、使い回しできるように切り出しています。
RADIUSを変更すれば切り抜くときの角丸を一括で変更できます。

const newClipPath = `
  M${RADIUS} 0
  L${backgroundWidth - RADIUS} 0
  A${ARC_PATH} ${backgroundWidth} ${RADIUS}
  ...
`

左上(0, 0)から半径分だけ移動し、右上まで直線を引きます。
そして、右上の角を円弧で丸めています。

const newClipPath = `
  ...
  L${backgroundWidth} ${backgroundHeight - RADIUS}
  A${ARC_PATH} ${backgroundWidth - RADIUS} ${backgroundHeight}
  ...
`

右側の縦線を引き、右下の角を丸めています。

const newClipPath = `
  ...
  L${paragraphWidth + RADIUS} ${backgroundHeight}
  A${ARC_PATH} ${paragraphWidth} ${backgroundHeight - RADIUS}
  L${paragraphWidth} ${backgroundHeight - paragraphHeight + RADIUS}
  A${RADIUS} ${RADIUS} 0 0 0 ${paragraphWidth - RADIUS} ${backgroundHeight - paragraphHeight}
  ...
`

テキストの右下まで線を引き、テキストの右上に線を引いています。
右下、右上ともに角を丸めています。

ここだけはパスの重なりというか切り抜き方というか、そういった関係上ARC_PATHが使えず${RADIUS} ${RADIUS} 0 0 0と記載しています。

const newClipPath = `
  ...
  L${RADIUS} ${backgroundHeight - paragraphHeight}
  A${ARC_PATH} 0 ${backgroundHeight - paragraphHeight - RADIUS}
  L0 ${RADIUS}
  A${ARC_PATH} ${RADIUS} 0
  Z
  ...
`

テキストの左上まで線を引き、スタートの座標へ戻り、パスをクローズしています。

Reactとしての実装

難しいことはしていませんが、次のような処理をしています。

  1. useRefを使用して画像とテキストのサイズを取得
  2. useStateを使用してクリップパスを管理
  3. useEffectを使用して画面サイズの変更時にクリップパスを更新
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?