Canvasで画像を描画する（２）

# はじめに
前回の [Canvasで画像を描画する](https://qiita.com/generokenken/items/46bc89382cd36f5cc4c1)の続きとなります。
今回は、前回作ったCanvasにもう少し手を加えていき、最終的に合成画像をダウンロードするところまでいきたいと思います。

話をわかりやすくするために一つのscript.jsに処理を書いていますので、記述量が多くなってしまっていますが、ご了承ください。

# 画像の角を丸くする

クリッピングパスを使って画像を切り取ります。

https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Compositing#%E3%82%AF%E3%83%AA%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0%E3%83%91%E3%82%B9

今回、角の半径が50ピクセルとなるように切り取っていきます。


```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');

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

const createRoundedClipPath = (ctx, x, y, width, height, radius) => {
  ctx.beginPath(); // 新しいパスを開始
  ctx.moveTo(x + radius, y); // 左上の角から開始
  ctx.lineTo(x + width - radius, y); // 上辺を右に向かって描画
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius); // 右上の角を丸くする
  ctx.lineTo(x + width, y + height - radius); // 右辺を下に向かって描画
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); // 右下の角を丸くする
  ctx.lineTo(x + radius, y + height); // 下辺を左に向かって描画
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius); // 左下の角を丸くする
  ctx.lineTo(x, y + radius); // 左辺を上に向かって描画
  ctx.quadraticCurveTo(x, y, x + radius, y); // 左上の角を丸くする
  ctx.closePath(); // パスを閉じる
  ctx.clip(); // クリッピングパスを設定
};

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;

  // クリッピングパスを作成して角を丸くする
  const radius = 50; // 角の半径
  ctx.save(); // 現在の描画状態を保存
  createRoundedClipPath(ctx, x, y, drawWidth, drawHeight, radius);
  ctx.drawImage(image, x, y, drawWidth, drawHeight);
  ctx.restore(); // save()で保存した描画状態を復元
};

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();
};
```

ctx.save()は現在の描画状態を保存し、ctx.restore()は以前に保存した描画状態を復元します。
クリッピングパスの処理は、コンテキスト（ctx）の状態を変えてしまうため、何もしなければ、次の描画の際、状態が変わったコンテキストを引き継いで意図しない描画を行なってしまいます。
結果として、画像をマウスクリックしながら移動した時にdrawImageを繰り返しますが、その際の切り取りが意図通りでなくなってしまうため、ctx.save()やctx.restore()の処理が必要となります。

![画面収録 2024-07-31 19.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/22e37293-af71-6111-fedf-56a001ae549f.gif)


角が丸くなり、画像をマウスクリックしながら移動させることができました。

# 2枚の画像を一つのCanvasに描画する

背景画像（image2.png)を追加したいと思います。
背景にしたい画像を先に描画し、その後に猫ちゃん画像を描画します。
そうすることで、背景にしたい画像の上に猫ちゃん画像が重なって描画されます。

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

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

const createRoundedClipPath = (ctx, x, y, width, height, radius) => {
  ctx.beginPath(); // 新しいパスを開始
  ctx.moveTo(x + radius, y); // 左上の角の内側から開始
  ctx.lineTo(x + width - radius, y); // 上辺を右に向かって描画
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius); // 右上の角を丸くする
  ctx.lineTo(x + width, y + height - radius); // 右辺を下に向かって描画
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); // 右下の角を丸くする
  ctx.lineTo(x + radius, y + height); // 下辺を左に向かって描画
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius); // 左下の角を丸くする
  ctx.lineTo(x, y + radius); // 左辺を上に向かって描画
  ctx.quadraticCurveTo(x, y, x + radius, y); // 左上の角を丸くする
  ctx.closePath(); // パスを閉じる
  ctx.clip(); // クリッピングパスを設定
};

const drawImage = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 2枚目の画像を合成
  const drawWidth2 = image2.width;
  const drawHeight2 = image2.height;
  const x2 = (canvas.width - drawWidth2) / 2;
  const y2 = (canvas.height - drawHeight2) / 2;

  ctx.drawImage(image2, x2, y2, drawWidth2, drawHeight2);

  const drawWidth = image1.width;
  const drawHeight = image1.height;
  const x = (canvas.width - drawWidth) / 2 + offsetX;
  const y = (canvas.height - drawHeight) / 2 + offsetY;

  // クリッピングパスを作成して角を丸くする
  const radius = 50; // 角の半径
  ctx.save(); // 現在の描画状態を保存
  createRoundedClipPath(ctx, x, y, drawWidth, drawHeight, radius);
  ctx.drawImage(image1, x, y, drawWidth, drawHeight);
  ctx.restore(); // 以前に保存した描画状態を復元
};

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 image1 = new Image();
image1.src = 'image.png';

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

image1.onload = () => {
  if (image2.complete) {
    drawImage();
  }
};

image2.onload = () => {
  if (image1.complete) {
    drawImage();
  }
};
```

![画面収録 2024-07-31 19 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/a62c952a-4639-fb9f-78e7-6ea8f78b2ae0.gif)


背景画像が追加されました。

# 合成画像としてダウンロードする

downloadImage関数の追加とダウンロードボタンの設置を追加します。

Canvas要素をcanvas.toDataURL()を用いてbase64のデータURL形式に変換することで、aタグのdownload属性を使って画像としてダウンロードすることができます。


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

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

const createRoundedClipPath = (ctx, x, y, width, height, radius) => {
  ctx.beginPath(); // 新しいパスを開始
  ctx.moveTo(x + radius, y); // 左上の角の内側から開始
  ctx.lineTo(x + width - radius, y); // 上辺を右に向かって描画
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius); // 右上の角を丸くする
  ctx.lineTo(x + width, y + height - radius); // 右辺を下に向かって描画
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); // 右下の角を丸くする
  ctx.lineTo(x + radius, y + height); // 下辺を左に向かって描画
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius); // 左下の角を丸くする
  ctx.lineTo(x, y + radius); // 左辺を上に向かって描画
  ctx.quadraticCurveTo(x, y, x + radius, y); // 左上の角を丸くする
  ctx.closePath(); // パスを閉じる
  ctx.clip(); // クリッピングパスを設定
};

const drawImage = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 2枚目の画像を合成
  const drawWidth2 = image2.width;
  const drawHeight2 = image2.height;
  const x2 = (canvas.width - drawWidth2) / 2;
  const y2 = (canvas.height - drawHeight2) / 2;

  ctx.drawImage(image2, x2, y2, drawWidth2, drawHeight2);

  const drawWidth = image1.width;
  const drawHeight = image1.height;
  const x = (canvas.width - drawWidth) / 2 + offsetX;
  const y = (canvas.height - drawHeight) / 2 + offsetY;

  // クリッピングパスを作成して角を丸くする
  const radius = 50; // 角の半径
  ctx.save(); // 現在の描画状態を保存
  createRoundedClipPath(ctx, x, y, drawWidth, drawHeight, radius);
  ctx.drawImage(image1, x, y, drawWidth, drawHeight);
  ctx.restore(); // 以前に保存した描画状態を復元
};

const downloadImage = () => {
  const link = document.createElement('a');
  link.download = 'canvas_image.png';
  link.href = canvas.toDataURL();
  console.log(canvas.toDataURL());
  link.click();
};

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 image1 = new Image();
image1.src = 'image.png';

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

image1.onload = () => {
  if (image2.complete) {
    drawImage();
  }
};

image2.onload = () => {
  if (image1.complete) {
    drawImage();
  }
};

// ダウンロードボタンを追加
const downloadButton = document.createElement('button');
downloadButton.innerText = 'Download Image';
downloadButton.addEventListener('click', downloadImage);
document.body.appendChild(downloadButton);
```

![MOV to GIF 2024-07-31 19.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2730383/9e7fd83a-dd26-b5d9-c13b-c4b9695388a5.gif)


# 最後に
基本的な内容ではありますが、これまでやってきた内容を組み合わせれば色々な合成画像を作ることができると思います。
この記事をきっかけに初めてCanvasを使った実装をする方などの参考になればと思います！