Canvasで画像を描画する

# はじめに
Canvas API を使用すると、ブラウザ上で画像やテキストを描画できるのですが、画像を切り取ったり、拡大・縮小したり、マウスクリックした状態でドラッグして移動したり...
といった色々なことができます。

https://developer.mozilla.org/ja/docs/Web/API/Canvas_API



弊社のいくつかのプロダクトでは、これらの技術を用いて合成画像を出力し、お客さんへ届けるという仕組みを構築しています。

本記事では、Canvas で画像を描画するための、非常に基本的な使い方を紹介します。


# まずは画像を描画する

canvas要素は、二次元描画コンテキストを提供します。
これは、図形、文字、画像、その他のオブジェクトを描画するのに使用します。

https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D

今回、横と縦が300 * 400のねこちゃん画像を描画します。
描画される範囲（枠）のサイズは広めの800 * 600としておきます。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Canvasで画像を描画</title>
  <style>
    canvas {
      border: 1px solid black;
    }
  </style>
</head>
<body>
  <canvas id="myCanvas" width="800" height="600"></canvas>
  <script src="script.js"></script>
</body>
</html>
```

```script.js
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

const image = new Image();
image.src = 'image.jpg';

image.onload = () => {
  ctx.drawImage(image, 0, 0);
};
```

![スクリーンショット 2024-07-26 18.40.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/c64854c0-b8fe-bedb-36ca-fd201c927b13.png)

ねこちゃん画像がそのままのサイズで枠内に描画されました。

# 画像のサイズを指定して描画

横と縦を1/2のサイズにしてみましょう。

```js
//画像を指定した横幅と高さで描画。
ctx.drawImage(image, 0, 0, 150, 200);
```

![スクリーンショット 2024-07-26 18.50.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/6012d8bb-0f7c-d633-ff32-17bf80e2ca2d.png)

小さくなりました。

# 画像の一部を切り取って描画
300 * 400の元画像のうち、150 * 200の範囲だけを切り取ってみましょう。

```js
// 元画像に対して、(0,0)の位置から150 * 200の範囲を切り取る。引数2~5までの値指定。
// 切り取った画像を、(0,0)の位置から150 * 200の範囲で描画する。引数6~9までの値指定。
ctx.drawImage(image, 0, 0, 150, 200, 0, 0, 150, 200); 
```

![スクリーンショット 2024-07-26 18.58.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/d25573f4-e5f6-58c8-7bf6-ba5d1e65ebbe.png)

左目だけ描画されました。

# 画像の中心を Canvas の中心に合わせて描画


```js
image.onload = () => {
  const canvasWidth = canvas.width;
  const canvasHeight = canvas.height;
  const imageWidth = image.width;
  const imageHeight = image.height;

  const x = (canvasWidth - imageWidth) / 2;
  const y = (canvasHeight - imageHeight) / 2;

  ctx.drawImage(image, x, y);
};
```

こうなります。

![スクリーンショット 2024-07-26 19.17.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/96bec516-a2e3-5d20-de1c-d5f558862904.png)

つまり、左上の赤丸の点の座標から描画すればOKということです。

![スクリーンショット 2024-07-26 19.17.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/836c8557-55ae-4883-2180-33946e3cdba0.png)

# 画像をCanvasの枠に合わせて拡大する

少し応用編です。
画像をCanvasの枠に合わせて拡大する場合、アスペクト比を維持することが重要です。
以下の関数を使用して、画像を Canvas の幅または高さに合わせて拡大します。

```js
const adjustSizeByWidth = (canvasWidth, imageAspectRatio, scale) => {
  const drawWidth = canvasWidth * scale;
  const drawHeight = drawWidth / imageAspectRatio;
  return { drawWidth, drawHeight };
};

const adjustSizeByHeight = (canvasHeight, imageAspectRatio, scale) => {
  const drawHeight = canvasHeight * scale;
  const drawWidth = drawHeight * imageAspectRatio;
  return { drawWidth, drawHeight };
};

const calculateImageSize = (imageWidth, imageHeight, canvasWidth, canvasHeight, scale = 1.0) => {
  const imageAspectRatio = imageWidth / imageHeight;
  const canvasAspectRatio = canvasWidth / canvasHeight;
  if (imageAspectRatio > canvasAspectRatio) {
    return adjustSizeByWidth(canvasWidth, imageAspectRatio, scale);
  } else {
    return adjustSizeByHeight(canvasHeight, imageAspectRatio, scale);
  }
};
```

実際に描画する箇所の処理はこうなります。

```js
image.onload = () => {
  const { drawWidth, drawHeight } = calculateImageSize(image.width, image.height, canvas.width, canvas.height);
  const x = (canvas.width - drawWidth) / 2;
  const y = (canvas.height - drawHeight) / 2;
  ctx.drawImage(image, x, y, drawWidth, drawHeight);
};
```

![スクリーンショット 2024-07-26 19.29.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/b8023028-9b7a-c2d8-b3bc-ab94c7009489.png)

今回は、横長の枠に対して縦長の画像を描画する形ですので、画像は枠に対して縦合わせになりました。

# 画像の拡大・縮小

HTML の input 要素を使用して、画像の拡大・縮小を実装してみましょう。
以下記述を追加します。

```html
<label for="zoom">Zoom: </label>
<input type="range" id="zoom" min="0.1" max="3" step="0.1" value="1">
```

JavaScript で input 要素の値を監視し、画像のスケールを変更します。
input要素の値の変更があればその都度描画します。
ただ注意点として、描画する前にclearRectでCanvasの描画状態を消しましょう。
そうしないと前回の描画が残った状態で次の描画を行なってしまいます。

```script.js
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
let scale = 1;

const drawImage = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const drawWidth = image.width * scale;
  const drawHeight = image.height * scale;
  const x = (canvas.width - drawWidth) / 2
  const y = (canvas.height - drawHeight) / 2
  ctx.drawImage(image, x, y, drawWidth, drawHeight);
};

const zoomInput = document.getElementById('zoom');
zoomInput.addEventListener('input', (event) => {
  scale = event.target.value;
  drawImage();
});

const image = new Image();
image.src = 'image.png';

image.onload = () => {
  drawImage();
};
```

<img width="500" alt="mojikyo45_640-2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/9bc8a269-7ef5-86d0-2be1-50b68b5af31c.gif">


# 画像の移動

マウスイベントを使用して、画像をドラッグして移動させることができます。

```script.js
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

let offsetX = 0;
let offsetY = 0;
let isDragging = false;
let startX, startY;

const drawImage = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  const drawWidth = image.width
  const drawHeight = image.height
  const x = (canvas.width - drawWidth) / 2 + offsetX;
  const y = (canvas.height - drawHeight) / 2 + offsetY;
  ctx.drawImage(image, x, y, drawWidth, drawHeight);
};

canvas.addEventListener('mousedown', (event) => {
  isDragging = true;
  startX = event.offsetX - offsetX;
  startY = event.offsetY - offsetY;
});

canvas.addEventListener('mousemove', (event) => {
  if (isDragging) {
    offsetX = event.offsetX - startX;
    offsetY = event.offsetY - startY;
    drawImage();
  }
});

canvas.addEventListener('mouseup', () => {
  isDragging = false;
});

canvas.addEventListener('mouseleave', () => {
  isDragging = false;
});

const image = new Image();
image.src = 'image.png';

image.onload = () => {
  drawImage();
};
```
<img width="500" alt="mojikyo45_640-2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/fe91d907-7e70-990d-8cef-1a68a36e3774.gif">

# 最後に
次回は、また別の処理について記事を書く予定をしています。
それではまた〜。