メリークリスマス🎅

JavaScriptとPythonは他の言語と比べたらググって出てくる情報がホントに劣悪だよねStackOverflowですラドベントカレンダー、1日目の記事です。(全一日)

◆結論

TypeScriptですが。

const b = new Blob();
const c = document.createElement("canvas");

const bitmap = await createImageBitmap(b);
c.width = bitmap.width;
c.height = bitmap.height;
c.getContext('2d')?.drawImage(bitmap, 0, 0);

…TypeScript関係ねぇな。

createImageBitmapを使う以上、awaitについては避けようも無い。

ほいで、createImageBitmapのサポート状況が割と悪い。IEとSafariじゃ動かない。polyfillは存在しているっぽいが。

createImageBitmap | Can I use

…と思いきや、SarafiでもFirefoxでもなんか動いた。どゆこと?

共通化したかったんでmodule書いたんだけど、canvasごと渡す関数にしたらこんな感じ。

※module内でcanvas使えるとも限らんのでcanvasごと渡してる。

const drawBlobOnCanvas = async (canvas: HTMLCanvasElement, blob: Blob) => {
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  const bitmap = await createImageBitmap(blob);
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  ctx.drawImage(bitmap, 0, 0);
}

細かい解説は後述。

◆経緯

そもそもやりたかったことは、Web上にあるPNG画像をfetchで引っ張ってきてcanvasにdrawしたいみたいな。その上にさらにdrawしてPNG画像を合成したい。PNGを重ね合わせたい。PNGをマージしたい。JavaScriptで。

※検索用にくどく書いた。

ふらふらとググってたんだけど、出てくる記事は大抵「new Image」とかしてんですね。そんなわけあるまいと思って暫く調べてたんだけど、みんな「new Image」してやがる。

とにかくfetch APIを使うと最終的にレスポンスはArrayBufferやらBlobになるので、そこから短い経路でcanvasにぶっこみたいわよねって。

Blobをcanvasに描く例で、よくあったのがこれ。

var canvas = document.getElementById('canvas');
var input = document.getElementById('input');


  function picToBlob() {
    canvas.renderImage(input.files[0]);
  }

HTMLCanvasElement.prototype.renderImage = function(blob){
  
  var ctx = this.getContext('2d');
  var img = new Image();

  img.onload = function(){
    ctx.drawImage(img, 0, 0)
  }

  img.src = URL.createObjectURL(blob);
};

input.addEventListener('change', picToBlob, false);

出典⇒How to render a blob on a canvas element? | StackOverflow

new Image経由で描くやつ。この例でいえばHTMLCanvasElementのprototypeを拡張してBlobでDrawできるようにしちゃってる。

悪い例: ネイティブのプロトタイプの拡張

えー?って思いながらdrawImageの定義を眺めてたら引数にImageBitmapっていう知らんものを指定できることが発覚し、今に至る。createImageBitmapしろ。

◆PNG画像のBlobを合成しつつBlobとして出力しちゃうFunction

const mergePng = async (canvas: HTMLCanvasElement, blobs: Array<Blob>) => {
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  const bitmaps = await Promise.all(blobs.map(x => createImageBitmap(x)));
  canvas.width = bitmaps[0].width;
  canvas.height = bitmaps[0].height;
  bitmaps.forEach(x => ctx.drawImage(x, 0, 0));
  return await new Promise(r => canvas.toBlob(r, "image/png", 1));
}

Blobの配列を上から読んでdrawしていく。

  • 読めばわかるんだけど、canvasのサイズは最初の画像のサイズに依存する。
  • 配列の要素数チェックしてないので、落ちる可能性ある。実用したい場合はよしなにしろ。
  • function内でcanvasのクリアをしてないから、そのまま使うとどんどん画像が重なっていく。自動で消したい場合はctxに対してclearRect(0, 0, canvas.width, canvas.height)しろ。

▼Promise.allってなに?

Promise.all

ザックリ・セツメイションするとつまり、Promiseの配列を配列のPromiseにしてくれる。(意味不明)

その「配列のPromise」をawaitすれば、それはPromiseとか混じってない配列になりますわな。

非同期だから微妙に不安だけど、使った感じ順序が崩れることもなさそう…と思ったし、説明に「順序崩れないよ」って書いてあったわ。

あと、Promiseの配列についてはfor await ofっていう便利なやつもあるので履修重点。

▼余談:forEach

forEach」っていうFunctionは普段使わないんだけど、こういう用途でだけ使う意義があると思ってる。

  • 一点だけ副作用があって
  • その副作用は単一メソッドの実行で済む

この条件を満たすのであればforeachの可読性は「for of」に勝る。

元からある変数群に対して副作用がない操作であれば、それは配列操作関数(mapとかfilterとかflatMapとか最悪reduceとか)で書き切れる。単一メソッドの実行で済まない、つまり複数行になっちゃうような動きをさせたいのであればfor ofで書いたほうが読む側としては理解しやすい。

forEachは何も明示してくれない。むやみにforEachを使うと可読性を損なう。

▼最後の行は何なんだよ

toBlobしたいだけなんだけど、コールバックが嫌なのでPromiseにした。最後の行でawaitする意味があるかどうかはよくわからん。ない気がしてる。

古いコールバック API をラップする Promise の作成

◆選択肢

「PNGのURLを受け取ってマージしてダウンロード」みたいな話まではいけるな。「Blobと描画位置」のオブジェクト配列を渡して…みたいなこともできよう。

Blobの配列ではなくてPromise<Blob>の配列を渡しちゃうってのも無論できる。

◆よだん

ImageBitmapってユーザーから使えるインターフェースがほぼないんだけど、なにこれ。完全にcanvas用なのかね。だとしてもちょっと。

あと、OffscreenCanvasというものの存在を知った。今回でいえば単にPNGを合成したいってだけだったんで画面に出す必要ない。使いたい。でもFirefoxで使えなかった。悲しみ。nightlyで有効化すれば使えるらしいが。

Web Worker上で動くってコレ結構重要なものなのでは?っていうか言われてみればcanvasってメインスレッドで描画してたんだな。

Firefoxの実験的機能ページを見るのが何となく楽しかったんでリンクを貼っておく。

Firefox における実験的機能 - Mozilla | MDN

◆結

高校生のなりたい職業1位に「エンジニア・プログラマー」 学研調査 男子・高1女子に人気魚拓

インターネットでアンケートしてるって書いてるけど、どういう領域に対してのアンケートなんだろか。でもまぁ技術やるのは楽しいぜ。おいでよ。高校一年生女子でしょ?教えるよ。うん。優しく。ソフトに。