🙌

Google Apps Scriptでプリクラを作ってみた(クライアント側実装編)

2022/12/29に公開

文化祭でプリクラを展示したい!

という経緯については、https://zenn.dev/tyokoyama/articles/d4d49276df9942をご覧ください。

HTML5の各種APIを使って実現してみました。

Webカメラで撮影した画像をブラウザで表示させる。

VideoタグでWebカメラの映像を再生(?)させます。その映像をCanvasに転送(?)させてカメラの映像を見えるようにしました。Webカメラの取得にはgetUserMedia()を使っています。

index.html
<video id="video"></video>
<canvas id="camera" width="720" height="560"></canvas>

(GASのプロジェクトでは拡張子が全てHTMLになるので、拡張子は気にしないでください)
Canvasへの映像の転送は、Canvasに対してアニメーションのリクエストをする必要があります。

javascript.html
const video = document.getElementById("video");
const camera = document.getElementById("camera");

video.autoplay = true;
video.style.display = "none";
navigator.mediaDevices.getUserMedia({
  video: true,
  audio: false,
}).then(stream => {
    video.srcObject = stream;
}).catch(e => {
    console.log(e);
});

var canvasCtx = camera.getContext('2d');
_canvasUpdate();

console.log("canvas描画開始");
var requestId;
function _canvasUpdate() {
  canvasCtx.drawImage(video, 0, 0, camera.width, camera.height);
  requestId = requestAnimationFrame(_canvasUpdate);
};

ボタンを押して、映像の転送を止める(シャッターボタン)

シャッターボタンを押すと、映像の転送を止めて、写真を確定させます。映像の転送を止めるのは、window.cancelAnimationFrame()を呼び出すだけです。

javascript.html
// 撮影ボタン(videoからの映像の更新を止める)
function shutter() {
  cancelAnimationFrame(requestId);
}

マウスでドラッグすると線が描けるようにする

Canvasをマウスや画面タッチで線を描けるようにしました。これはマウスイベントonDown、onMove、onUpをそれぞれ処理をしています。考え方は、マウスのボタンを押した時が開始座標で、onMoveが発生するタイミングで線を描けば、それらしく線がひけます。マウスのボタンを話せば線を引く処理は終わります。

javascript.html
function onDown(e) {
  if(isPaint == 1) {
    let x = e.clientX - e.target.getBoundingClientRect().left;
    let y = e.clientY - e.target.getBoundingClientRect().top;

    isDrag = 1;
    pX = x;
    pY = y;
    canvasCtx.beginPath();
    console.log("onDown");
  }
}

var pX = 0;
var pY = 0;
function onMove(e) {
  if(isDrag) {
    let x = e.clientX - e.target.getBoundingClientRect().left;
    let y = e.clientY - e.target.getBoundingClientRect().top;
    drawLine(pX, pY, x, y);
    pX = x;
    pY = y;
    console.log("onMove");
  }
}

function onUp(e) {
  paintEnd();
  console.log("onUp");
}
 
function drawLine(px, py, x, y) {
  canvasCtx.moveTo(pX, pY);
  canvasCtx.lineTo(x, y);
  canvasCtx.stroke();
}

function paintEnd() {
  isDrag = 0;
  canvasCtx.closePath();
}

camera.addEventListener('mousedown', onDown);
camera.addEventListener('mousemove', onMove);
camera.addEventListener('mouseup', onUp);

画面をスワイプすると線が描ける

処理はマウスの時と同じですが、マウスのイベントと同じではないので、実装する必要があります。(画面タッチに対応しているデバイスである必要があるため、テストが面倒でした…)引数のtouches配列もできればマウスと同じ命名であって欲しかったですね。

javascript.html
  function onTouchStart(e) {
    if(isPaint == 1) {
      let x = e.touches[0].clientX - e.target.getBoundingClientRect().left;
      let y = e.touches[0].clientY - e.target.getBoundingClientRect().top;
      if(mode == 1) {
        isDrag = 1;
        pX = x;
        pY = y;
        canvasCtx.beginPath();
      }
      console.log("onTouchStart");        
    }
  }

  function onTouchMove(e) {
    if(isDrag) {
      let x = e.touches[0].clientX - e.target.getBoundingClientRect().left;
      let y = e.touches[0].clientY - e.target.getBoundingClientRect().top;
      drawLine(pX, pY, x, y);
      pX = x;
      pY = y;
      console.log("onTouchMove");
    }
  }

  function onTouchEnd(e) {
    paintEnd();
    console.log("onTouchEnd");
  }
  
camera.addEventListener("touchstart", onTouchStart);
camera.addEventListener("touchend", onTouchEnd);
camera.addEventListener("touchmove", onTouchMove);

スタンプ用画像を画面に表示させる

スタンプの機能を実装するのは、マウスクリックやタッチイベントを処理すればいいのですが、どんなスタンプが押せるかというのを表示させるためにGoogle Driveに保存した画像を表示させるようにしました。ただ、そのままDrive.getDownloadUrl()を使ってimgタグで表示させると、表示はできるものの、画面タッチの時に表示されない問題が出ました。これは、Cross Originなので、ポリシーを設定する必要がありますが、ポリシーを設定していないので汚染されているという警告が出て処理が止まるようです。対応はサーバー側で設定するのが良さそうですが、Google Driveはいじれそうにないので、base64Encodeでエンコードしてjavascript側でblobに変換するという対応にしました。(以下のソースコードはVue.jsを利用した後のコードですが、気にしないでください)
Google Apps ScriptにはTemplate機能があるので、比較的便利に使えます。ただ、わかりにくくなりそうなので、使わないように実装したいところです。なんとなく。

index.html
      <div id="stampctrl">
        <div class="modal" v-bind:class="{ 'is-active': isActive }">
          <div class="modal-background"></div>
          <div class="modal-content" style="background-color: white;">
            <?
                let folder = DriveApp.getFolderById("[画像保存フォルダのID]");
                let files = folder.getFiles();
                let no = 0;
                while(files.hasNext()) {
            ?>
            <div>
                <label>
                  <input type="radio" name="stamp" onclick="setStamp(<?= no ?>)" />
                  <img name="img" width="128" height="128" src="data:image/jpeg;base64,<?= Utilities.base64Encode(files.next().getBlob().getBytes()) ?>"></img>
                </label>
              <?
                  no++;
                  }
              ?>
            </div>
          </div>
          <button class="modal-close is-large" aria-label="close" @click="deactivate()"></button>
        </div>
      </div>

マウスクリックと画面タッチで画像を表示させる

ソースコードは画面タッチの時のみを載せていますが、画面タッチの時だけは、onDownが後で動作した(ような気がする)ので2回イベント処理が実行されます。(スタンプ画像が2回表示されてしまう)その対応のために、preventDefault()を呼び出してイベントの伝搬をしないようにしています。

javascript.html
  function onTouchStart(e) {
    if(isPaint == 1) {
      undostack.unshift(canvasCtx.getImageData(0, 0, camera.width, camera.height));
      let x = e.touches[0].clientX - e.target.getBoundingClientRect().left;
      let y = e.touches[0].clientY - e.target.getBoundingClientRect().top;
      if(mode == 1) {
        isDrag = 1;
        pX = x;
        pY = y;
        canvasCtx.beginPath();
      } else if(mode == 2) {
        // 画面タッチ時のスタンプ
        e.preventDefault();
        drawStamp(imageNo, x, y);
      }
      console.log("onTouchStart");        
    }
  }

消しゴム機能の追加

消しゴムはCanvasのContextのglobalCompositeOperationプロパティにdestination-outを設定するだけです。ただし、白で描画するようになる(背景色で描画?)だけなので、カメラの映像を出すプリクラでは消しゴムとして機能することはありませんでした。(プリクラの初期値は撮影した画像なので、白で消すと白で線を描いたのと変わらない)

javascript.html
  function setEraser() {
    mode = 1;
    canvasCtx.globalCompositeOperation = 'destination-out';
  }

ちなみに、消しゴムモードを元に戻すためには、globalCompositeOperationプロパティにsource-overを代入してください。

Undo機能の追加

undo機能はCanvasに描画されているデータをそのまま配列に保存するだけです。

javascript.html
var undostack = [];

// スタックにCanvasに描画されているデータを保存
undostack.unshift(canvasCtx.getImageData(0, 0, camera.width, camera.height));

// スタックにあるデータをCanvasに戻す
if(undostack.length <= 0) return;
let undoImage = undostack.shift();
canvasCtx.putImageData(undoImage, 0, 0);

データ送信

描き終わったデータはGoogle Apps ScriptのAPIで送信します。この辺は便利になっています。google.script.runを使うと、クライアント側からサーバのスクリプトの関数を呼び出すことができます。withSuccessHandlerで通信成功時の処理を実装し、withFailureHandlerで失敗時の処理を実装することができます。

javascript.html
// 確定ボタン(canvasのデータをGoogle Driveへ)
console.log("send");
let data = camera.toDataURL("image/jpeg");
console.log(data);
google.script.run.withSuccessHandler(sendSuccess).withFailureHandler(sendFailure).saveDrive(data);

まとめ

HTML5になってからAPIが充実してきたので、ブラウザベースのアプリケーションでも比較的できることが増えてきました。今後も学校の文化祭に向けてのアプリケーションのアイデアを練っていこうかなと思ったりしています。
ちなみに、プリクラを作りましたが、文化祭の展示の部では上位3位以内にかすりもしませんでした。技術だけで勝つこと難しい…。

Discussion