knowledge base

マークアップ/フロントエンドエンジニアのWEB制作における備忘録です。平日はWEB屋、休日は社会人劇団の主宰・劇作家をしています。

Canvasのレスポンシブ対応

Canvasのレスポンシブ対応はなぜ難しいか

Canvasは複雑なグラフィックの描画から、マルチデバイスに対応したアニメーションの実装、3Dの描画まで幅広く対応できる優れものです。

ところがレスポンシブ対応となると格段に難易度が上がります。

その理由について整理してみると、

  • 属性値でサイズを指定するため、CSSでのサイズ指定ができない【こちらを参照】
  • 画像描画の場合、素材となる画像から切り出すサイズ・開始座標を絶対値で指定してCanvasに描画するという考え方である【こちらを参照】

主にこの2点により、レスポンシブデザインやRetina対応ではセオリーとなっている「width:100%」などといった、親要素を基準としたサイズ指定ができないことが難易度を高くしているゆえんとなっています。

ただし、いくつかのテクニックやメソッドを組み合わせることでこれらに対応することが可能となります。

ちなみにzoomやtransformをページ全体にかけるという方法も考えられますが、サイト特性によっては他の要素に縮小をかけられない場合もあるかと思いますので、ここではcanvasのみで実装が完結する方法をご紹介します。

レスポンシブ対応

先に、基本的な考え方からご紹介します。

  1. 素材となる画像と同じアスペクト比となる要素を準備し、その要素のサイズをCanvasの属性値として設定する
  2. 素材となる画像サイズと現在のCanvasサイズとの比率を算出し、その値にもとづいて画像をCanvas上に描画する。

1.2を画像ロード時に、2をリサイズ時に行うことでレスポンシブ対応を実現することができます。

素材と同じアスペクト比を持つ要素を準備する

基本的なロジックは以下の記事と同様です。

shinimae.hatenablog.com

透過画像を用いてもよいのですがソース的に美しくないため、padding-topを用いた方法を採用します。

 

■HTML

<div id="canvas-container">
  <canvas id="canvas"></canvas>
</div>

CSS

#canvas-container{
    position: relative;
    height: 0;
    overflow: hidden;
    padding-top: 56.25%;
}
  #canvas{
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
  }

div#canvas-containerはリサイズしてもアスペクト比16:9を維持し続けます。

canvasはスタイル上ではこのDIV要素と同サイズになるよう指定していますが、先述したとおりCSSでサイズを指定できないので、ここから先はJavascriptを用いて実装してゆきます。

親要素のサイズにもとづいてCanvasのサイズを指定する

下記ソースの通りです。ドキュメント上に描画された要素のサイズはclientWidth/clientHeightで取得することができるため、それをCanvasのDOMオブジェクトのプロパティに代入します。

var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

//親要素のサイズをCanvasに指定
canvas.width = container.clientWidth;
canvas.height = container.clientHeight; 

素材となる画像を描画

画像の描画はdrawImageメソッドを用います。

drawImageメソッドは9個の引数によって画像の指定した範囲をCanvas上に描画することができるのですが、今回は特にクロップなどは行わないため、引数を3つのみ指定します。

第1引数は素材となる画像のオブジェクトを、第2・第3引数には描画範囲のx座標・y座標を指定します(画像を丸ごと描画するならば両者とも0を指定します)。

詳しくはリファレンスをご覧になったほうがより理解しやすいかと思いますので、併せてご覧ください。

先ほどのソースに追記してゆきます。

var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

//イメージオブジェクトを生成
var img = new Image();
img.src = "/path/to/image/hogehoge.png";

//親要素のサイズをCanvasに指定
canvas.width = container.clientWidth;
canvas.height = container.clientHeight; 

//画像読み込み後にdrawImageメソッドでCanvas上に描画する
img.onload = function(){
    ctx.drawImage(img, 0, 0);
}

画面サイズに合わせて、Canvasを(内部的に)縮小させる

CSSでの「width:100%」に相当する描画ロジックを作ります。setTransformメソッドを用いることでCanvasを変形します。

setTransformメソッドについては後述しますので、まずは先ほどのソースに追記をします。

var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

//イメージオブジェクトを生成
var img = new Image();
img.src = "/path/to/image/hogehoge.png";

//親要素のサイズをCanvasに指定
canvas.width = container.clientWidth;
canvas.height = container.clientHeight; 

//画像読み込み後に、setTramsformメソッドで適切な比率に縮小
//その上で、drawImageメソッドでCanvas上に描画する
img.onload = function(){
    var scale = canvas.width / img.width;
    ctx.setTransform(scale, 0, 0, scale, 0, 0);
    ctx.drawImage(img, 0, 0);
}

setTransformメソッドは、CSSでのtransform同様に、scale/skew/translateのすべてを行うことができます。

6つの引数をとり、それぞれの引数に値を指定することで、これらの変形を同時に行うことができます。

6つの引数の意味については、以下の通りリファレンスより引用します。

setTransform(a, b, c, d, e, f)メソッドのそれぞれの引数は、 setTransform(伸縮x, 傾斜y, 傾斜x, 伸縮y, 移動x, 移動y)となります。 何も変形しない場合の引数の値は、setTransform(1, 0, 0, 1, 0, 0)となります。

今回のケースでは、単に伸縮のみを行うため、算出した伸縮率を第1引数・第4引数に指定しています。

リサイズ時

ここまでの一連の流れを、リサイズ時に再描画することで、レスポンシブ対応が可能になります。

先ほどのソースに追記します。

var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

//イメージオブジェクトを生成
var img = new Image();
img.src = "/path/to/image/hogehoge.png";

//親要素のサイズをCanvasに指定
canvas.width = container.clientWidth;
canvas.height = container.clientHeight; 

//画像読み込み後に、setTramsformメソッドで適切な比率に縮小
//その上で、drawImageメソッドでCanvas上に描画する
img.onload = function(){
    var scale = canvas.width / img.width;
    ctx.setTransform(scale, 0, 0, scale, 0, 0);
    ctx.drawImage(img, 0, 0);
}
//リサイズ時
window.onresize = function(){
    var scale = 0;
    //再描画のため必ずCanvasの描画領域をクリアする
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    canvas.width = container.clientWidth;
    canvas.height = container.clientHeight; 
    scale = canvas.width / img.width;
    ctx.setTransform(scale, 0, 0, scale, 0, 0);
    ctx.drawImage(img, 0, 0);
}

こちらをまとめます。

var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var isInit = false; var img = new Image(); img.src = "/path/to/image/hogehoge.png";
//描画 var render = function(){ var scale = 0;
if(isInit){ ctx.clearRect(0, 0, canvas.width, canvas.height);
}else{
isInit = true;
} canvas.width = container.clientWidth; canvas.height = container.clientHeight; scale = canvas.width / img.width; ctx.setTransform(scale, 0, 0, scale, 0, 0); ctx.drawImage(img, 0, 0); } //メイン var main = function(){ img.addEventListener('load', render, false); window.addEventListener('resize', render, false); } //メイン実行 main();

こうすることで、Canvasでもレスポンシブ対応をすることができます。

以下がそのサンプルです(画面をリサイズして、画像が拡縮されることを確認してください)。

See the Pen Canvas Demo by Shin Imae (@shinimae) on CodePen.