canvasにHTMLの内容を描画したい

かっこいいサイト見てたら、canvasにHTMLの内容を描画して背景なんかに引いてぐにゃぐにゃさせているようなのがあった。僕はそういうぐにゃぐにゃするようなのすごく好きなので、一体これどうやってるんだろうな、みたいな気持ちになって見てた。

たまたま、そのことをふと思い出して調べてみたら普通にMdnに載ってたので、早速試してみた。 文献これです。

DOM オブジェクトを Canvas に描画する

記述されている通りにやると確かに動くけど、もっとがっつりやりたいよね、という気持ちになる。
早い話、document.getElementByIdかなんかで要素を取得して、svgにぶち込んで、丸ごと描画させるみたいなことをやりたい気持ちになってくる。

そこで上記の文献を読むと、

SVG は valid な XML でなければならないので、埋め込む HTML も well-formed なものでなければいけません。

なるほど。参考コードが載ってるのでそれに則って一気にやってみる。

  // canvasの取得とサイズ指定
  const c = document.getElementById('c');
  const ctx = c.getContext('2d');
  const h = window.innerHeight;
  const w = window.innerWidth;
  c.width = w;
  c.height = h;

  // canvasに表示したい要素の取得
  const html = document.getElementById('js-html');

  /**
  * well-formedなやつ
  */

  // html作成
  const doc = document.implementation.createHTMLDocument('');
  // そのbodyの中に取得してきたDOMを書き込む
  doc.write(html);
  // svg化するのに使うぞ
  doc.documentElement.setAttribute("xmlns", doc.documentElement.namespaceURI);
  const serializeHtml = (new XMLSerializer).serializeToString(doc);

あれ、ほんでこれどうすればいいんだ?みたいな気持ちになる。
serializeHtmlを見ると当たり前だけど、シリアライズされて文字列になったHTMLが入っている。
これはつまりこれをsvgにはめ込んでやればオッケーってことなのか?

  const data = `
    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0, 0, ${w}, ${h}' width='${w}' height='${h}'>
      <foreignObject width='100%' height='100%'>
        ${serializeHtml}
      </foreignObject>
    </svg>
  `;

  // svgファイルに変換
  const svg = new Blob([data], {type: "image/svg+xml;charset=utf-8"});

  // svgを生成した後、それをimgで読みたいため、urlを生成する
  const DOMURL = self.URL || self.webkitURL || self;
  const url = DOMURL.createObjectURL(svg);

  const img = new Image();
  img.src = url;

  // GO!!
  img.onload = () => {
    ctx.drawImage(img, 0, 0);
    // urlを破棄する
    DOMURL.revokeObjectURL(url);
  }

やってみると多分でかいcanvasがあるだけで何も表示されないと思う。
そこでChromeのdeveloperツール開いて、ネットワークパネルを見ると破滅した画像のリソースが見つかった。それを開くと

This page contains the following errors:
error on line 4 at column 11: internal error: detected an error in element content

Below is a rendering of the page up to the first error.

はぁ。

確認すると4行目には<!DOCTYPE html>がいる。つまりこいつがダメってことか。
だけどdocument.implementation.createHTMLDocumentで生成すればそれは必ず入るので、この方法ではこの場合ダメなんじゃなかろうか。

そういうわけで。

普通に取得してきたDOMをシリアライズして<div xmlns="http://www.w3.org/1999/xhtml"></div>で囲ってぶち込みましょう。
問題は解決です。

  // canvasに表示したい要素の取得
  const html = document.getElementById('js-html');
  const serializeHtml = (new XMLSerializer).serializeToString(html);

  const data = `
    <svg xmlns='http://www.w3.org/2000/svg' viewBox='0, 0, ${w}, ${h}' width='${w}' height='${h}'>
      <foreignObject width='100%' height='100%'>
        ${serializeHtml}
      </foreignObject>
    </svg>
  `;

はい。

スタイルどうするねん問題

<div xmlns="http://www.w3.org/1999/xhtml"></div>のなかに<style></stye>で書いてやれば普通にあたる。 だけど愚直に記述するのも厳しいので、cssファイルを取得してきて、打ち込むことにしましょう。
僕はaxiosを使います。以下コード全文

  
  const c = document.getElementById('c');
  const ctx = c.getContext('2d');
  const h = window.innerHeight;
  const w = window.innerWidth;
  c.width = w;
  c.height = h;
  
  // html 取得 & SVG作成
  const html = document.getElementById('js-html');

  // Get well-formed markup
  const serializeHtml = (new XMLSerializer).serializeToString(html);

  axios.get('./style.css')
    .then(res => {
      const style = res.data;
      const data = `
        <svg xmlns='http://www.w3.org/2000/svg' viewBox='0, 0, ${w}, ${h}' width='${w}' height='${h}'>
          <foreignObject width='100%' height='100%'>
            <div xmlns="http://www.w3.org/1999/xhtml">
            <style>
              ${style}
            </style>
            ${serializeHtml}
            </div>
          </foreignObject>
        </svg>
      `;
      const svg = new Blob([data], {type: "image/svg+xml;charset=utf-8"});

      // svgを生成した後、それをimgで読みたいため、urlを生成する
      const DOMURL = self.URL || self.webkitURL || self;
      const url = DOMURL.createObjectURL(svg);

      const img = new Image();
      img.src = url;

      // GO!!
      img.onload = () => {
        ctx.drawImage(img, 0, 0);
        // urlを破棄する
        DOMURL.revokeObjectURL(url);
      }
    })
    .catch(err => {
      console.log(err);
    });

結局

画像なんかはまたあれだし、便利にやってくれるhtml2canvasと言うライブラリがあるので、それを使いましょう。

html2canvas