[HTML5] 複数のCanvasを合成する

今回は複数のcanvasタグを合成し、最終的に1つのcanvasに画像として結合する処理を取り上げたいと思います。 ↑こんな感じで画像を表示したcanvasと、キャプションを表示したcanvasを合成します。

Canvas合体

サンプル

以下のページから実際にサンプルを実行できます。 miku3.net

  • 「=」ボタンをクリックすると左2つのCanvasを合成し、一番右側のcanvasに合成結果を出力します。
  • 消しゴムボタンをクリックすると合成結果を削除します。

ソースコード

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>CANVAS合成</title>
  <link rel="stylesheet" href="style.css" type="text/css" media="all">
</head>
<body>

<h1>CANVAS合成</h1>

<section id="contents">
  <div class="flex-container">
    <!-- 合成するcanvasその1 -->
    <canvas id="image1" width="200" height="170"></canvas>
    <div><img src="icon/plus-solid.svg" width="32" height="32"></div>

    <!-- 合成するcanvasその2 -->
    <canvas id="image2" width="200" height="170"></canvas>
    <p><button type="button" id="btn-concat"><img src="icon/equals-solid.svg" width="32" height="32"></button></p>

    <!-- 合成結果用のcanvas -->
    <canvas id="concat" width="200" height="170"></canvas>
  </div>

  <!-- 消しゴム -->
  <p id="eraser"><button type="button" id="btn-eraser"><img src="icon/eraser-solid.svg" width="32" height="32"></button></p>
</section>

<ul>
  <li>「=」ボタンをクリックすると2つのcanvasを合成します</li>
  <li><img src="icon/eraser-solid.svg" width="18" height="18">」ボタンをクリックすると合成結果をクリアします</li>
</ul>

<script>
window.onload = () => {
  // #image1に画像を描画
  drawImage1();

  // #image2にテキストを描画
  drawImage2();

  // 「+」ボタンを押したら合成
  document.querySelector("#btn-concat").addEventListener("click", ()=>{
    concatCanvas("#concat", ["#image1", "#image2"]);
  });

  // 「消しゴム」ボタンを押したらクリア
  document.querySelector("#btn-eraser").addEventListener("click", ()=>{
    eraseCanvas("#concat");
  });

};

/**
 * [onload] うな重の画像を描画
 */
function drawImage1(){
  const Unaju = new Image();
  Unaju.src = "image/unajyu.png";
  Unaju.onload = () =>{
    const canvas = document.querySelector("#image1");
    const ctx = canvas.getContext("2d");
    ctx.drawImage(Unaju, 0, 0, canvas.width, canvas.height);
  }
}

/**
 * [onload] テキスト「うな重」を描画
 */
function drawImage2(){
  const canvas = document.querySelector("#image2");
  const ctx = canvas.getContext("2d");
  ctx.font = "32px serif";
  ctx.fillStyle = "Red";
  ctx.fillText("うな重", 45, 150);
}

/**
 * Canvas合成
 *
 * @param {string} base 合成結果を描画するcanvas(id)
 * @param {array} asset 合成する素材canvas(id)
 * @return {void}
 */
 async function concatCanvas(base, asset){
  const canvas = document.querySelector(base);
  const ctx = canvas.getContext("2d");

  for(let i=0; i<asset.length; i++){
    const image1 = await getImagefromCanvas(asset[i]);
    ctx.drawImage(image1, 0, 0, canvas.width, canvas.height);
  }
}

/**
 * Canvasをすべて削除する
 *
 * @param {string} target 対象canvasのid
 * @return {void}
 */
function eraseCanvas(target){
  const canvas = document.querySelector(target);
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, canvas.width, canvas.height);
}

/**
 * Canvasを画像として取得
 *
 * @param {string} id  対象canvasのid
 * @return {object}
 */
function getImagefromCanvas(id){
  return new Promise((resolve, reject) => {
    const image = new Image();
    const ctx = document.querySelector(id).getContext("2d");
    image.onload = () => resolve(image);
    image.onerror = (e) => reject(e);
    image.src = ctx.canvas.toDataURL();
  });
}
</script>
</body>
</html>

解説

大雑把なロジック

考え方は非常にシンプルで、合体させたいcanvasを画像に変換し、最終結果を出力するcanvasにdrawImage()で貼り付けているだけです。

canvasを画像に変換

getImagefromCanvas()関数で指定したcanvasを画像に変換しています。

function getImagefromCanvas(id){
  return new Promise((resolve, reject) => {
    const image = new Image();
    const ctx = document.querySelector(id).getContext("2d");
    image.onload = () => resolve(image);
    image.onerror = (e) => reject(e);
    image.src = ctx.canvas.toDataURL();
  });
}

ポイントはPromiseを利用することです。image.srcでcanvasのデータを画像として登録する際に非同期処理となるため、うまく後続処理が行われません。同様にgetImagefromCanvas()を利用しているconcatCanvas()では関数を定義する際にasyncを付加し、getImagefromCanvas()を呼ぶ前にawaitをつけています。

async function concatCanvas(base, asset){
  //.....
  for(let i=0; i<asset.length; i++){
    const image1 = await getImagefromCanvas(asset[i]);
    //.....
  }
}

素材

参考ページ