考察キッチン

KOUSATSU-KITCHEN

html2canvasで画像位置(サイズ)がズレるので対処法

htmlの指定の要素内だけを画像化(スクリーンショット風画像に)出来る『html2canvas』。

公式web : https://html2canvas.hertzen.com/

(筆者が動作確認済みのバージョンは1.0.0-rc.5)

<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.5/dist/html2canvas.min.js"></script>

手軽な方法では、上記をbodyの綴じタグ()の直前にコピペしますと、

jsにてhtml2canvas()関数↓

 html2canvas(document.querySelector("#capture")).then(canvas => {
 document.body.appendChild(canvas)
 });

が利用できるようになります。 上は公式サイトにあるサンプルです。この書き方だと、htmlの一番最期にcanvas要素が追加され、そこに画像化した結果が出力されます。(画像化した後どうするかは.then(canvas => { })の中で決める感じです)

とても便利なのですが、ブラウザ、html、cssの条件によっては上手く動作しません。

画像のズレもその一つで、出力された画像の幅と高さは合っているのに、上や横に変な余白ができてる……??なんて事が起こります。逆に、位置は合ってるのにサイズが小さすぎ、大きすぎっていう場合も。

 

原因チェックと対処法

スクロールすると上に余白ができる

これはかなり起こりやすい現象らしく、『html2canvas scroll』で検索すると事例がたくさん出てきます。スクロール量を下へと増やした時は余白が大きく、減らした時は余白が小さくなる症状が特徴です。

対処法1『scrollYを調整』

html2canvasのオプションにcanvas上の画像位置を調節する『scrollX』『scrollY』というのが有るので、スクロールした分だけ『scrollY』を引き算しよう、という主旨の対処法。

 html2canvas(画像化したい要素, {オプション名: 値, オプション名2: 値})
オプションは上記のように書き加えます(概念)。具体的には以下。

 html2canvas(element,
  {
   scrollX: 0,
   scrollY: -window.scrollY,
  });

window.scrollYでスクロールした高さを取得できます。 この対処で解決する場合は一番スマートでしょうか。

対処法2『ページトップに戻してから処理』

スクロールでズレるなら、スクロールしてない状態に戻してから処理すればいいじゃない!という対処法。『window.scrollTo(0, 0);』でスクロールが初期化されるので、これをhtml2canvas();の直前に実行するようにします。

 window.scrollTo(0, 0);
 html2canvas(element);

 

 対処法3『html2canvasをダウングレード』

1.0.0-alpha.12のバージョンであれば問題なかった、という事例があるようです。ただ、cssやhtmlで最新の仕様を活用している場合、画像化する際に表現できない可能性があるので、そのあたりは場合によりけりです。

 

スクロールバーの幅に応じて横にずれる

症状の特徴としては、ウィンドウ幅を変えてもスクロールバーが表示されている限りは横ズレの量が一定です。そして、スクロールバーを表示されないようにすると解消する。

対処法1『scrollXを調整』

バーの幅に応じてhtml2canvasのオプション『scrollX』で調整します。 まず、肝心のスクロールバーの幅を取得しましょう。

let scrollBar = (window.innerWidth - document.body.clientWidth);

そしてオプションの書き方を確認↓。

html2canvas(画像化したい要素, {オプション名: 値, オプション名2: 値}) 上記を踏まえ、状況に応じて計算します。

【画像化したい要素を横幅中央配置している場合】

html2canvas(element, {scrollX: -scrollBar/2})

text-align: center;や、flex-boxでの中央配置をした時は上記。

【画像化したい要素をposition: absoluteを使って"%"(比率)で配置している場合】

html2canvas(element, {scrollX: -scrollBar * 比率/100});

比率/100は left: 20% の場合は 20/100 にします。約分して1/5でもいいです。

上の2例から分かるかもしれませんが、CSS構成が複雑になるほど調整する値の計算も複雑になります。 画像化したい要素のページ上での位置(x座標)の計算がなるべくシンプルになるようなレイアウトを心がけるのが吉です……。

 

スクロールバーの幅とレスポンシブ判定の齟齬

前項の派生。特殊な条件のようで、実はそうでもない。レスポンシブレイアウトを作っているときにhtml2canvasを使おうとすると、おそらくは打ち当たります。

ウィンドウのサイズが〇〇px以下の時
(画像化したい)要素の横幅を画面サイズいっぱいに表示

ウィンドウのサイズが〇〇pxを越える時
(画像化したい)要素の横幅を〇〇pxで固定し、応じて余白ができる ↑画像化したい要素を中央配置等をしていて、余白の幅はその都度調整される

……という挙動を期待してcssを書いたとき、 『可変幅で画面いっぱい→固定幅で余白あり』に切り替わる境目の何pxかで画像化をすると 画像のズレ幅がウィンドウ幅に応じて大きくなったり小さくなったりするのが特徴。

 

対処法

検索してもコレぞという対処法はなかったですが、筆者が試行錯誤しまして「一応、描画ズレがなくなる対処法」までは辿り着きました。 残念ながらコードの効率とかは推敲しきれていません……。ただ、自前の環境ではこれで直ったので、ヒントにでもなれば幸いです。


では、具体例を1つ出しながら説明します。

【固定座標→相対座標 となるcss・htmlの例1】

<head>
  //中略//
  <style>
  * {
    margin: 0;
    padding: 0;
  }
   #parent {
    width: 100%;
    display: flex;
    justify-content: center;
   }
   #element {
    width: 100%;
    max-width: 480px;
    height: 300px;
    background-color: #000;
   }
  </style>
 </head>
 <body> 
  <div id="parent">
    <div id="element"></div>
  </div>
 </body>

この[ #element ]は、基本的にはbody幅いっぱい(100%)に広がります。ですから、当然x座標も0であり、固定です。ですが、bodyの幅が480pxを越えるとそれ以上は幅が固定化し、その上flex-boxで中央配置をしている為、余白がうまれます。 (ウィンドウ幅に合わせて要素のx座標が変化する、この中央配置が問題の核だったりする)

『ズレ方がcssの定義で変わってくる』ようだ……

この[ #element ]をhtml2canvasで画像化したい場合、大きく分けて

  1. scrollbarが有ってもなくても要素のx座標がwindow幅に応じて決まる範囲

  2. scrollbarの影響で(x座標が)固定位置になってしまった範囲

  3. scrollbarが有っても無くても(x座標が)固定位置の範囲

この3パターンのウィンドウ幅で描画画像のズレ方が変わってきます。

何故かというと、(おそらく)html2canvasは(デフォルトでは)『スクロールバー幅の影響でhtmlの表示状態が変わることを感知しない』からです。

実態としてはスクロールバーの幅の分だけ表示領域が狭くなっていても、ウィンドウ幅(window.innerWidth)からhtmlの表示状態を計算し描画するということ。

ですから、

『ウィンドウ幅に応じて要素のx座標が変化するcss』を組んでいて、それが画像化したい要素のx座標にも影響を及ぼすとなると、スクロールバーの幅の分だけズレてしまうんですよね。

これが3パターンのうちの1です。

この範囲は前項『スクロールバーの幅に応じて横にずれる』の対処法が使えます。

html2canvas(element, {scrollX: -scrollBar/2})

しかし、[例1]の場合は、480px以下では画面幅いっぱいに要素が表示されます。
となればスクロールバーの分だけ広めの幅で解釈したとして、x座標は 0 のまま変わりません。 『ウィンドウ幅に関係なく、要素のx座標が固定のcss』の範囲に入ると、スクロールバーの分の誤差がx座標の位置ズレに影響しない。 これは、3パターンのうちの3です。 この範囲は、scrollXは"0"です。

html2canvas(element, {scrollX:  0})

そして、 ウィンドウ幅が『480px 以上 かつ 480px + スクロールバーの幅』以内になった時。 html2canvas側の解釈では余白が存在するのに、実態は余白なしでx座標が固定、という範囲。

パターン①では「スクロールバーの分だけズレる」のですが、パターン③では「ズレがない」ので、そこの境目は、ウィンドウ幅に応じ、ズレの量が『スクロール幅 px ⇆ 0px』の間で繋ぐように徐々に減少または増加します。

html2canvas(element, {scrollX: (window.innerWidth - 480)/2;})

"480"という数値は画面いっぱいのcssに切り替わるウィンドウ幅(px)ですから、此処は各々cssに合わせて変更してください。


.

3パターンあるのがわかったので、条件分岐

スクロールバーの分だけ狭く表示されるのが原因なので、『cssが切り替わるウィンドウ幅』+『スクロールバーの幅』が問題が生じる境目です。

[例1]の場合は、ウィンドウ幅が「480px + スクロールバー幅」pxの時点から『横幅いっぱい』の状態になります。 480px +(scrollbarの幅)pxですね。 これをこの記事上は『keyWidth』とでもしておきます。

 let scrollBar = window.innerWidth - document.body.clientWidth;
 let keyWidth = 480 + scrollbar;

そして前述した3パターンに分けて、 ズレを補正する値を決めます。変数" x "に格納。

if (window.innerWidth  >=  keyWidth)) {
   //scrollbarが有っても無くてもx座標が増減する範囲
   x = -scrollBar/2;
 } else if(window.innerWidth > 480) { 
    //scrollbarの影響でx座標が固定になってしまった範囲
   x = (window.innerWidth - 480)/2;
 } else {
    //scrollbarが有っても無くてもx座標が固定の範囲
   x = 0;
 }

この " x " をオプション『scrollX』にセットします。

html2canvas(element, { scrollX: x })

[例1]の条件の場合は上記で横の位置ズレはなくなりました。

サイズもズレる

html2canvasは、常にスクロールバーの分だけ大きめのウィンドウサイズで解釈して描画します。

つまり、位置だけでなく、画像のサイズもズレます。(勘弁して)

サイズを調整するには、『width』もしくは『height』というオプションを使います。

html2canvas(element, { 
       scrollX: x ,
       width: //任意の値
})

『画像化したい要素.clientWidth』で、その時点での要素の幅は取得できますが、「ウィンドウ幅に応じて幅が変化する範囲」では、スクロールバーの分だけ大きく解釈してしまうので、そこだけ調整します。

        //スクロールバーによるレイアウトズレを補正
        if (window.innerWidth  >=  keyWidth) {
            x = -scrollBar/2;
        } else if(window.innerWidth > 480) { 
            x = -(window.innerWidth - 480)/2;
            captureWidth = 画像化したい要素.clientWidth + (cssが切り替わる幅  - document.body.clientWidth);
        } else {
            x = 0;
            captureWidth = 画像化したい要素.clientWidth + scrollBar;
        };
コピペ用html(レスポンシブ中央配置)

『画面いっぱい』→『幅固定/中央配置/余白あり』のcssでhtml2canvasを利用するコピペサンプルは以下です。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>中央配置でもhtml2canvas!</title>
    <meta name="description" content="This site is a test site.">
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        #parent {
            position: relative;
            width: 100%;
            height: 100vh;

            display: flex;
            justify-content: center;
            align-items: center;
        }
        #element {
            width: 100%;
            height: 300px;
            max-width: 480px;

            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #000;
        }
        .demo {
            width: 100%;
            padding: 135px 0;
            margin: 5px;
            font-size: 0.8rem;
            background-color: #fff;
            text-align: center;
        }
        .preview {
          width: 100%;
          position: relative;
          display: flex;
          align-items: center;
          justify-content: center;
          flex-direction: column;

          margin-bottom: 20px;
        }
        .click {
            border: none;
            background-color: #fff;
            border: solid 1px #000;
            padding: 5px 10px;

            margin-bottom: 20px;
        }
    </style>
  </head>
  <body>
      <div id="parent">
        <div id="element">
          <div class="demo">demo</div>
        </div>
      </div>
      <div class="preview">
        <button id="btn" class="click">画像化</button>
        <div id="result"></div>
      </div>

    <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
    <script>
        const btn = document.getElementById("btn");

        function shot(){
        //現在の状態を取得
        let capture = document.querySelector("#element");
        let scrollBar = (window.innerWidth - document.body.clientWidth);
        let keyWidth = 480 + scrollBar;
        let x;
        let captureWidth = capture.clientWidth;
        //スクロールバーによるレイアウトズレを補正
        if (window.innerWidth  >=  keyWidth) {
            x = -scrollBar/2;
        } else if(window.innerWidth > 480) { 
            x = -(window.innerWidth - 480)/2;
            captureWidth = capture.clientWidth + (480  - document.body.clientWidth);
        } else {
            x = 0;
            captureWidth = capture.clientWidth + scrollBar;
        };
        //html2canvas関数
        html2canvas(
          capture, {
            scrollX: x,
            scrollY: -window.scrollY,
            width: captureWidth,
            scale: 3
          }).then(function(canvas){
          var result = document.querySelector("#result");
          result.appendChild(canvas)
        });
      }

      btn.addEventListener("click", shot);
    </script>
    
  </body>
</html>

表示ズレは各自で対処

html2canvasは、jsでひとつひとつcssの解釈を定義しているらしいです。 今でも少しずつアップデートはされているようですし、最新版でシンプルに対処できるなら一番いいですが、 原因を見極めながら各自で対応するほうがもしかしたら『いますぐ』というニーズにはハマるかもしれません……。