画像の狙った座標に他の画像をぴったり重ねる方法

Webアプリにて、カード置き場の画像に描かれた枠内に、カード画像をきっちり並べる、というコンポーネントを実装する機会があったので、そのとき用いた方法を記します。
（フレームワークにReactを用いましたが、カードの配置自体はCSSの機能で実装可能です）

実装イメージ：
<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/86539a9f-059f-7e7a-8479-7237d0e30c79.png">


# 用いる画像

カードの画像：
cardBack.png / cardFace.png
<img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/fb73b038-72d5-063f-4664-311344e7a130.png"> <img width="80" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/2575fd79-38f9-334f-86e8-930b47a3b279.png">

ボード（カード置き場）の画像：
board.png
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/68464536-919c-7377-ccab-d86cf7eccbd3.png">
予めボード画像の大きさやカードの座標を以下のように調べておきます
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/e33bb58e-4f86-99da-fefb-e8d02651022a.png">


# 実装コード



## カード1枚の場合

まずは簡単に、1枚だけカードを配置してみます。

#### Boardクラス

- ボード画像を、ウィンドウの高さに合わせて表示します（height: 100vh）
- ボード画像とカード画像を重ねて表示するため、基準となる親要素のdivを作ります
- divを、子要素であるボード画像のサイズに合わせます（display: inline-block, height: 100vh）
- divを基準にカードを配置するため、position: relativeとします

#### Cardクラス

- カードのスタイルをcreateCardImageStyle関数を用いて作成します
- 親要素であるdiv（= ボード画像と同じサイズ）を基準に配置するため、position: absoluteとします
- カードの位置は、ボードの元画像における座標と、ボードの元画像のサイズから、割合を計算して指定します。割合で指定することにより、画面上の画像サイズに依存せずに位置を指定することができます（top、leftを%指定）
- カードのサイズも同様に、ボードの元画像における枠の比率に合わせます（widthを%指定）
- ここではカードの左上ではなく中央を基準に揃えるため、カードの高さの半分だけ上に、幅の半分だけ左にずらします（transform: translate(-50%, -50%)）

```javascript:index.js
import React from 'react';
import ReactDOM from 'react-dom';

const cardImage = "./cardBack.png"; // カード画像
const boardImage = "./board.png"; // ボード画像

const boardImageWidth = 1757; // ボード画像の幅（ピクセル数）
const boardImageHeight = 2126; // ボード画像の高さ（ピクセル数）
const firstLeftPosition = 318; // カードを置く座標（左から何ピクセル目か）
const firstTopPosition = 395; // カードを置く座標（上から何ピクセル目か）
const frameWidth = 340; // ボード画像に描かれたカード用の枠の幅（ピクセル数）。カード画像の幅ではないことに注意

function createCardImageStyle(leftPosition, topPosition, frameWidth) {
  return {
    position: "absolute",
    top: (topPosition / boardImageHeight) * 100 + "%",
    left: (leftPosition / boardImageWidth) * 100 + "%",
    width: (frameWidth / boardImageWidth) * 100 + "%",
    transform: "translate(-50%, -50%)",
  };
}

class Card extends React.Component {
  render(){
    return (
      <img src={cardImage} style={createCardImageStyle(firstLeftPosition, firstTopPosition, frameWidth)} />
    );
  }
}

class Board extends React.Component {
  render(){
    return (
      <div style={{display: "inline-block", position: "relative", height: "100vh"}}>
        <img src={boardImage} style={{height: "100vh"}} />
        <Card/>
      </div>
    );
  }
}

ReactDOM.render(
  <React.StrictMode>
    <Board />
  </React.StrictMode>,
  document.getElementById('root')
);
```

実行結果：
<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/b46da343-4ac0-acd0-3bed-61cc10558c50.png">

## カード複数枚の場合

1枚のときと考え方は変わりません。
for文を用いて、配列にカードを格納します。

#### Boardクラス

- Cardの代わりにCardsコンポーネントを使います

#### Cardsクラス

- for文を用いて、縦横3つずつカードを作り、cards配列に格納します
- 必要な情報を、propsを経由してカードへ渡します。keyは要素の識別用です。
- srcは使用するカード画像のソースです。ここでは交互に表裏になるよう並べるため、i % 2 == j % 2による判定を入れています
- leftPositionは、1枚目のカードの座標（firstLeftPosition）に、対象のカードまでの差分（leftPositionDiff * i）を足して求めます
- topPositionも同様です

#### Cardクラス

- props経由で取得した情報を用いて、カードを表示します

```javascript:index.js
import React from 'react';
import ReactDOM from 'react-dom';

const cardBackImage = "./cardBack.png"; // カード画像
const cardFaceimage = "./cardFace.png"; // カード画像2
const boardImage = "./board.png"; // ボード画像

const boardImageWidth = 1757; // ボード画像の幅（ピクセル数）
const boardImageHeight = 2126; // ボード画像の高さ（ピクセル数）
const firstLeftPosition = 318; // 1枚目のカードを置く座標（左から何ピクセル目か）
const firstTopPosition = 395; // 1枚目のカードを置く座標（上から何ピクセル目か）
const leftPositionDiff = 554; // カード同士の座標の差
const topPositionDiff = 682; // カード同士の座標の差
const frameWidth = 340; // ボード画像に描かれたカード用の枠の幅（ピクセル数）。カード画像の幅ではないことに注意

function createCardImageStyle(leftPosition, topPosition, frameWidth) {
  return {
    position: "absolute",
    top: (topPosition / boardImageHeight) * 100 + "%",
    left: (leftPosition / boardImageWidth) * 100 + "%",
    width: (frameWidth / boardImageWidth) * 100 + "%",
    transform: "translate(-50%, -50%)",
  };
}

class Card extends React.Component {
  render(){
    return (
      <img src={this.props.src} style={createCardImageStyle(this.props.leftPosition, this.props.topPosition, frameWidth)} />
    );
  }
}

class Cards extends React.Component {
  render(){
    const cards = [];
    for (let j = 0; j < 3; j++){
      for (let i = 0; i < 3; i++){
        cards.push(<Card key={"card" + i + "_" + j} src={(i % 2 == j % 2)?cardBackImage:cardFaceimage} leftPosition={firstLeftPosition + leftPositionDiff * i} topPosition={firstTopPosition + topPositionDiff * j} />);
      }
    }
    return (
      <span>
        {cards}
      </span>
    );
  }
}

class Board extends React.Component {
  render(){
    return (
      <div style={{display: "inline-block", position: "relative", height: "100vh"}}>
        <img src={boardImage} style={{height: "100vh"}} />
        <Cards/>
      </div>
    );
  }
}

ReactDOM.render(
  <React.StrictMode>
    <Board />
  </React.StrictMode>,
  document.getElementById('root')
);

```

実行結果：
<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/974a4134-7e16-c313-d38a-23c5f70b79de.png">

ウィンドウサイズを変更しても、ボード画像に合わせてカードが配置されます
<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176222/f8ab8d9b-50be-94c7-34b8-1a7c78b525eb.png">