react-image-crop を活用して効率的にイベント会場の座席エリアを作成する

みなさんこんにちは。

寒さが日に日に増してきた今日この頃、いかがお過ごしでしょうか? アソビュー!株式会社でフロントエンドエンジニアをしております櫻井です。

今回は、画像から特定の範囲を切り出せるライブラリ「react-image-crop」を使って、弊社の「座席指定システム」を実装した事例をご紹介します。

座席指定システム

まずは、今回実装した機能の概要をお話しします。
この「座席指定システム」については、これまでも弊社のエンジニアやマネージャーが何度かテックブログ内で紹介してきました。
詳しい経緯や背景については、以下の過去の記事もぜひご覧ください。

プロジェクトマネージャーからプロダクトオーナーへの転身。スクラムの実践を通じて学んだ課題と魅力 - asoview! Tech Blog
座席指定システムにおける購入導線のアクセシビリティ改善 - asoview! Tech Blog

座席指定システムには、大きく分けて2つの機能があります:

  1. 一般ユーザー向け機能:コンサートやミュージカルなどのイベントで、会場の座席図から好きな座席を選んで購入できる機能
  2. 事業者向け機能:会場の座席図の登録や管理ができる機能

それぞれの機能について、詳しく見ていきます。

一般ユーザー向け機能

一般ユーザー向け機能では、座席選択は段階的に進めていきます。

  1. まず、図1のように会場の全体図から大まかなエリア(Aブロック、Bブロックなど)を選択します(以降、エリアと呼びます)
  2. ドームやアリーナなど大規模な会場の場合は、2〜3段階に分けて範囲を絞っていきます
  3. 最終的に、図2のように具体的な座席を選べる詳細な座席図が表示されます

図1. 会場の全体図からおおまかなエリア(Aブロック、Bブロックなど)を選択する画面

図2. 図1のブロックの座席を選択する画面

事業者向け機能

このユーザー向け機能を実現するために、まず各エリアの情報をデータとして登録する必要があります。具体的には「この座席ブロックは会場のどの範囲に位置するのか」という情報です。図1の画像で言えば、中央よりやや左上の明るくなっている部分がそれにあたります。

そこで私たちは、下記図3のような会場の座席図上で各エリアの範囲を簡単に設定できる機能を開発しました。

図3. エリア設定のサンプル画面

このエリア設定を実現するために使用したのが、これから紹介するreact-image-cropです。

react-image-cropについて

react-image-cropは、その名の通り画像の一部を切り取る(クロップする)ためのReactライブラリです。NPMページには以下のように紹介されています:

www.npmjs.com

An image cropping tool for React with no dependencies. (依存関係のないReact用の画像クロッピングツール)

シンプルな説明ですが、実際の機能は非常に強力です。

https://raw.githubusercontent.com/sekoyo/react-image-crop/master/crop-demo.gif
図4. NPMのサンプル画像

このライブラリを使用することで、会場の座席図上で「どの範囲をAブロックとするか」などの設定を、視覚的かつ直感的に行うことができます。

実装例

それでは具体的な実装方法を見ていきましょう。今回は10.0.8のバージョンを使用しています。

"dependencies": {
  "react-image-crop": "^10.0.8"
},

まず、クロップ対象の画像コンポーネントを用意します。react-image-crop から ReactCrop を import して、img タグを内包します。 ReactCrop コンポーネントには様々な props を設定できますが、今回は以下の機能を使用します:

  • crop: クロップ中の選択範囲情報を含むオブジェクトを渡します(下記 Crop インターフェイスを参照)。x座標、y座標、width(幅)、height(高さ)、unit(単位:%またはpx)を指定できます
  • onChange: ドラッグやリサイズなど、クロップ操作中に発生する全てのイベントで呼び出されます
  • onComplete: クロップ操作(ドラッグやリサイズ)が完了した時に発火します
  • keepSelection: 選択領域外をクリックしても選択を保持するかどうかを制御します(true: 保持する、false: 解除する)

ImageCropper コンポーネント

実装の核となるクロップ機能は、以下のように実装します:

import ReactCrop, { type Crop } from 'react-image-crop';
  :
export const ImageCropper: FC<Props> = ({
}) => {
  const [crop, setCrop] = useState<Crop>();
  const [completedCrop, setCompletedCrop] = useState<PixelCrop>();

  const onCropChange = useCallback(
    (_: PixelCrop, percentCrop: PercentCrop) => {
      setCrop(percentCrop);
    },
    [isCropping],
  );

  const onCropComplete = useCallback((c: PixelCrop) => {
    setCompletedCrop(c);
  }, []);

  return (
    <ReactCrop
      crop={crop}
      keepSelection
      onChange={onCropChange}
      onComplete={onCropComplete}
    >
      <img
        ref={imgRef}
        src={imageSrc}
      />
    </ReactCrop>
  );
}

Crop オブジェクトのインターフェース

Crop オブジェクトのインターフェースは以下の通りです:

export interface Crop {
    x: number;    // X座標
    y: number;    // Y座標
    width: number;  // 幅
    height: number; // 高さ
    unit: 'px' | '%'; // 単位(ピクセルまたはパーセント)
}

実際の操作結果は以下のようになります:

onCompleted 発火後にコールバックで受け取るデータ

このように、選択した範囲の位置とサイズを正確に取得できています。この情報を使って、必要なエリア分の操作を行い、API を通じてデータを保存します。

今後の展望

現在のシステムでは四角形(オプションで円形・楕円)の選択のみ可能ですが、より複雑な座席配置に対応するため、将来的には自由な形状での範囲選択機能の追加を検討しています。これにより、より精密な座席エリアの設定が可能になると考えています。

まとめ

今回は react-image-crop について紹介しました。react-image-cropは、直感的な操作性と柔軟な実装が可能な優れたライブラリでした。画像からの範囲選択機能を実装する際は、ぜひ検討してみてください。

We're hiring!

アソビューでは、より良いプロダクトを世の中に届けるため、共に挑戦していただけるエンジニアを募集しています。カジュアル面談も実施していますので、興味を持たれた方はぜひエントリーをお願いいたします!

www.asoview.com

speakerdeck.com

技術情報を発信する公式アカウントもございます。ぜひフォローをお願いします! https://twitter.com/Asoview_dev