🐧

Canvas2Dチョットワカルへの第一歩

2022/09/01に公開
1

はじめに

Canvasって、、すごいですよね!!(語彙力)
Web制作を始めた頃、つよつよなサイトは何でできているのか不思議でなりませんでした。
少しだけ、勉強をしてきて発見しました。
Canvasが使われている、、!こともある。
アニメーションなど、動くサイトが好きなのでかなり興味を惹かれました。
まだまだ知見は浅いですが、学んで得た知識を共有できればと思います。
また、Canvasに関する詳細な仕様については挙げればキリがないので記載していません。
「へーそんな感じかー」くらいの理解度になって頂ければ大変光栄です。

本記事の対象者

Canvasつよつよの方には、申し訳ないのですが
この記事でお役に立つことは難しいかもしれません。
間違いなどありましたら、ご指摘頂けますと幸いです。
対象として推定している方は、「Canvas、ナニソレオイシイノ」状態の方々です。(自分含む)
何ができて、何が難しいのかに焦点を当てて書けたらなと思っています。

そもそもCanvasってナニ

ご存じかもしれませんが、「Canvas」自体が指すものは
HTML5から登場した、比較的新しい要素です。
Canvasは、ブラウザ上に多彩なグラフィックを描画する機能を持っています。
その機能のことを「Canvas API」と呼び、それらは「コンテキストオブジェクト」という、
描画処理のためのメソッドやプロパティを持つオブジェクトによって提供されます。
コンテキストオブジェクトにはいくつか種類があり、代表的なものだと「Canvas2D」や「WebGLRenderingContext(WebGL)」があります。
JavaScriptにて、canvas内で使用するコンテキストオブジェクトを指定することで、
その機能を使うことが可能になります。

と、あんまりよくわからんことを述べましたが
つまるところ、Canvas(今回はCanvas2D)は主にJavaScriptを用いて
「ブラウザ上に何か動的な線とか丸とかをかけるツール」くらいの理解で良いのかなと思います。
具体的なできることや、実装の例などは後述します。

Canvas2Dの使い方

Canvasは前述したように、HTML要素ですのでサイズがあります。
デフォルトでは 300px × 150pxです。
CSSでサイズ指定可能ではありますが、推奨はされておりません。
JavaScriptで指定するか、後述する方法での指定が望ましいです。

ではまずは、使い方です。

HTML
<canvas id="canvas" width="500" height="500"><canvas>
JacaScript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');//Canvas2Dコンテキストを取得し「ctx」へ格納

このようにすれば使用可能です。
widthやheightはHTML要素の中に記載して指定が可能です。

ちなみに、

JavaScript
const ctx = canvas.getContext('3d');

とすれば、WebGLコンテキストオブジェクトを使用できます。

Canvas2Dでできること

次は実際にCanvas2Dを用いてできることと、その具体的な方法についてです。

1.線を描ける
2.四角を描ける
3.円(扇型,楕円)を描ける
4.任意の画像を描画できる
5.グラデーションとパターンの描画ができる
6.曲線を描ける
7.テキストを描画できる
8.ドロップシャドウを描画できる
9.描いたものを動的に変化させられる(アニメーションができる)

順番に説明する前に、Canvasのベースとなる機能を説明します。

描き方について

Canvasは、名前の通りブラウザ上のキャンバスにペンで絵を描いていくイメージで使用できます。
描ける領域はもちろん、Canvas要素の範囲内です。
JavaScriptで範囲を指定したい場合(今回は全画面表示)、前述したコードに続いて

JavaScript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');//Canvas2Dコンテキストを取得し「ctx」へ格納

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

となります。
そして、実際に描く時の流れは以下です。

JavaScript
ctx.beginPath();//いまから描画始めるよーの合図
ctx.moveTo(x,y);//最初にペンを置く場所の座標を指定(必要時,ペンを置き直す際も記載)
//この部分で何かしらの処理
ctx.closePath();//描画終了の合図

注意して頂きたいことは、高解像度ディスプレイ(レティナなど)では何もしないと
要素がぼやけて表示されてしまうことです。
対応方法は以下です。

JavaScript
canvas.width = window.innerWidth * devicePixelRatio;
canvas.height = window.innerHeight * devicePixelRatio;

Canvasに指定したサイズに、デバイスピクセル比を乗算することで対応可能です。
詳細はリンク先を御覧ください。
https://ics.media/entry/11020/

それでは次から、より具体的な方法について説明します。

1.線を描ける

JavaScript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const canvasW = canvas.width;
const canvasH = canvas.height;

線の描き方です。

JavaScript
ctx.beginPath();
ctx.strokeStyle= "red"//;//線の色を指定
ctx.lineWidth = 10;//線の太さを指定(単位は不要。px)
ctx.moveTo(0,canvasH / 2);
ctx.lineTo(canvasW,canvasH / 2);//線の終点の座標を指定
ctx.stroke();//線を描画
ctx.closePath();//

これでブラウザには以下のように表示されます。

以下に注意点を記載します。

  • lineHeightやstrokeStyleの影響範囲は""同じパスの中全て""です。
    つまり、同じCanvas内に色の異なる線を2本描画したい場合は以下のようにする必要があります。
JavaScript
//上から半分の高さの位置に10pxの太さの赤い線を引く
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 10//(単位不要/px);
ctx.moveTo(0,canvasH / 2);
ctx.lineTo(canvasW,canvasH / 2);
ctx.stroke();
ctx.closePath();

//上から1/3の高さの位置に10pxの太さの青い線を引く
ctx.beginPath();
ctx.strokeStyle = 'blue';
ctx.moveTo(0, canvasH / 3);
ctx.lineTo(canvasW, canvasH / 3);
ctx.stroke();
ctx.closePath();

最初に指定したスタイルは新たに更新しなければ、次のパス内でも継承されます。
(上記では、2つ目のパスにおいてlineWidthを記載していないため、最初のパスで指定した「10」が継承されます。)

  • スタイルの指定は実際に描画を行う前にかく。
    実際に描画を行っている箇所は
JavaScript
ctx.stroke();

この部分なので、これよりも前でbeginPathよりも後に書けば問題ありません。

2.四角を描ける

次は四角形の描き方です。
四角形は塗りつぶすパターンと、線のみのパターンの描画が可能です。
まず、塗りつぶすパターンです。

ctx.beginPath();
ctx.fillStyle = "green";//塗りつぶしの色を指定
ctx.fillRect(x,y,w,h);
ctx.fill();//ここで実際に描画
ctx.closePath();

fillRect(x,y,w,h)は4つの引数を取ります。
x:描画する四角形の左上頂点のx座標
y:描画する四角形の左上頂点のy座標
w:描画する四角形の横幅
h:描画する四角形の高さ

次に、線のみの場合です。

JavaScript
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.rect(x,y,w,h);
ctx.stroke();//実際に描画
ctx.closePath();

線のみの時は、rect()を使用します。
引数はfillRect()と同じです。

複数の四角形を描画したい時、前項の線の描画の際はパスを分ける必要がありましたが
四角形の場合は、同じパス内に記載可能です。ただし、注意点がいくつかあります。

注意点は以下です。

  • fillStyleはfillRect()よりも前に記載しないと反映されない。
  • strokeStyleはrect()の後でも反映されるが、記載方法を統一した方が良いのでrectより前に書く方が良い。
  • 重なり順は、後に記載したものが上。

実際のコードを添付します。

3.円(扇型も)を描ける

円の場合も、塗りつぶすパターンと線のみのパターンがあります。

塗りつぶす円の場合

JavaScript
ctx.beginPath();
ctx.moveTo(x,y);
ctx.arc(x,y,radius, 0, Math.PI * 2, false);//円の情報を指定
ctx.fillStyle = "purple";//塗りつぶし色を指定
ctx.fill();//描画
ctx.closePath();

円の描画には、arc()メソッドを使用します。
arc(x,y,radius,startRadian,endRadian,counterclockwise)
x:円の中心のx座標
y:円の中心のy座標
radius:円の半径
startRadian:描画開始の角度(弧度で指定)
endRadian:描画終了の角度(弧度で指定)
counterclockwise:描画の方向を真偽値で指定

counterclockwiseとは、「反時計回り」のことで、文字通り開始角度から終了角度まで「反時計回り」に描画します。何も引数を指定しなければ「false」となり、時計回りで描画されます。

弧度に関する詳しい説明は下記記事に委ねます。
https://kenyu-life.com/2019/01/09/rad/

簡単に説明すると、
360°(度数法) = 2π(パイ)弧度法 となります。

JavaScriptにおいて、「π」は「Math.PI」と記載します。
度数法の角度を弧度法に変換する式は
(Math.PI/180)*degrees
こちらで、「degrees」に度数法の任意の角度を入れて得られた数字が弧度法の角度です。

こちらも、線の描画の時と同じく。
色の異なる円を複数描画したい場合は、パスを分ける必要があります。

JavaScript
ctx.beginPath();
ctx.moveTo(canvasW / 2, canvasH / 2);
ctx.fillStyle = "purple";
ctx.arc(canvasW / 2, canvasH / 2, 50, 0, Math.PI * 2, false);
ctx.fill();
ctx.closePath();

ctx.beginPath();
ctx.fillStyle = "red";
ctx.moveTo(canvasW / 2, canvasH / 2);
ctx.arc(canvasW / 2, canvasH / 3, 50, 0, Math.PI * 2, false);
ctx.fill();
ctx.closePath();

ちなみに、扇型の描画方法は開始角度と終了角度を調整すれば可能です。

JavaScript
//中心角45°の緑色に塗りつぶされた扇型の描画
ctx.beginPath();
ctx.fillStyle = "green";
ctx.moveTo(canvasW / 2, canvasH / 2);
ctx.arc(canvasW / 2, canvasH / 2, 50, 0, Math.PI/4, false);
ctx.fill();
ctx.closePath();

開始角0度、つまり円の中心右側より「false」時計回りに描画が開始されて
π/4  = 45度 進んで描画終了となります。

楕円の場合

JavaScript
ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle [, anticlockwise])

楕円を描画するときは 「ellipse()」を用います。引数は上記で詳細は
x:中心のx座標
y:中心のy座標
radiusX:横の半径
radiusY:縦の半径
rotation:傾き
startAngle:開始角度
endAngle:終了角度
anticlockwise:描画方向
となります。

線のみの描画の場合

基本は今までと同じ方法で描画可能です。

JavaScript
//太さ10pxでピンク色の線の、塗り潰されていない円を描画
ctx.beginPath();
ctx.strokeStyle = "pink";
ctx.lineWidth = 10;
// ctx.moveTo(canvasW*3/4, canvasH / 2);//不要
ctx.arc(canvasW*3 / 4, canvasH / 2, 50, 0, Math.PI * 2, false);
ctx.stroke();
ctx.closePath();

ここで注意が必要なのは、線のみの描画の場合 moveToが不要になる点です。
moveToは描き始めの座標を指定するメソッドです。
円の描画の場合、arc()にて中心の座標と開始角度を指定することによって、描き始めの座標を
指定することができます。
塗り潰す円の場合は、この理由でmoveToの記載は不要です。
しかし、扇型の場合はmoveTo()によって描き初めの座標を円の中心にしなければ
形が成立しません。
どうなるかは、後述するCodePenにてお確かめ頂ければと思います。
そして、線のみの円を記載する場合は逆にmoveToを記載してはいけません。
記載してしまうと、円の中心から描画が始まってしまいます。

以下、参考コードです。

4.任意の画像を描画できる

let img = new Image();//Imageインスタンスを生成
//画像へのパスを記載(JSファイルからの相対パス、もしくは絶対パスでも可)
img.src = '../images/test.png';

//画像ファイルの読み込みが完了したら画像を描画する
img.addEventListener('load', function () {
  ctx.drawImage(img, 0, 0,200,100);
});

画像の描画にはdrawImage()メソッドを使用します。
記載方法は3パターンあります。

1.drawImage(img,dx,dy)
2.drawImage(img,dx,dy,dw,dh)
3.drawImage(img,sx,sy,sw,sh,dx,dy,dw,dh)

第一引数は共通で、Imageインスタンスを格納した変数を指定します。
その他については以下です。
dx:画像の左上の頂点のx座標
dy:画像の左上の頂点のy座標
dw:描画する横幅
dh:描画する縦幅
sx:描画したい画像の描画したい範囲の左上の頂点のx座標
sy:描画したい画像の描画したい範囲の左上の頂点のy座標
sw:描画したい画像の描画したい範囲の横幅
sh:描画したい画像の描画したい範囲の縦幅

言葉で説明することは、なかなか難しいので
参考サイトのリンクを貼っておきます!
https://blog.katsubemakito.net/html5/canvas-image
http://www.htmq.com/canvas/drawImage_s.shtml

画像の描画サイズとしては、
パターン1:画像元データのまんま
パターン2:dw×dhの範囲に元画像全範囲を拡大縮小して表示
パターン3:画像のsw×shの範囲を、dw×dhのサイズに拡大縮小して表示

となります。

5.グラデーションとパターンの描画ができる

グラデーションについて

まずは、グラデーションです。
線形グラデーションと円形グラデーションがあります。
グラデーションを描画するのに、前提として何かしらの図形を描写する必要があります。
今回は四角形を例に説明します。

線形グラデーション

コードは以下です。

JavaScript
ctx.beginPath();
let linearGradient = ctx.createLinearGradient(canvasW / 2, canvasH / 2, canvasW / 2, canvasH / 2 + 200);
linearGradient.addColorStop(0.0,'red');//グラデーションさせる色を位置を指定(0%の位置に赤)
linearGradient.addColorStop(1.0, 'blue');//グラデーションさせる色を位置を指定(100%の位置に青)
ctx.fillStyle = linearGradient;//グラデーション描画の指示を出す
ctx.fillRect(canvasW / 2, canvasH / 2, 300, 200);
ctx.fill();
ctx.closePath();

グラデーションを描画するためには、まずcreateLinearGradient()メソッドを用いる必要があります。
createLinearGradient(x0,y0,x1,y1)は引数を4つとり
x0:グラデーション開始位置のx座標
y0:グラデーション開始位置のy座標
x1:グラデーション終了位置のx座標
y1:グラデーション終了位置のy座標
となります。
これを、変数に格納し使用する色を指定します。

今回は、linearGradientという変数に格納しています。
そして、fillstyleに変数を指定してから任意の図形を描画します。
今回の場合はfillRect()で四角形を描画していますが
これ以降にctx.fillStyle = linearGradient;を記載しても反映されないので注意して下さい。
また、追加できる色の数に上限はありません。

以下、リファレンスです。参照下さい。
http://www.htmq.com/canvas/createLinearGradient.shtml

円形グラデーション

円形グラデーションの方法は以下です。

JavaScript
ctx.beginPath();
let radialGradient = ctx.createRadialGradient(canvasW / 2, canvasH / 2, 20, canvasW / 2, canvasH / 2, 100);//グラデーションを開始する円・終了する円の中心座標と半径を指定
radialGradient.addColorStop(0.0, 'red');//0%位置(中心)に赤を追加
radialGradient.addColorStop(1.0, 'blue');//100%位置(外側)に青を追加
ctx.fillStyle = radialGradient;
ctx.arc(canvasW / 2, canvasH / 2, 100, 0, Math.PI * 2, false);
ctx.fill();
ctx.closePath();

createRadialGradient(x0,y0,r0,x1,y1,r1)
x0:グラデーションを開始する円の中心のx座標
y0:グラデーションを開始する円の中心のy座標
r0:グラデーションを開始する円の半径
x1:グラデーションを終了する円の中心のx座標
y1:グラデーションを終了する円の中心のy座標
r1:グラデーションを終了する円の半径

基本的には、グラデーションを開始する円と終了する円の中心座標は等しくなるかと思います。
注意点は線形グラデーションの時と同じく、円を描画する前にスタイルを指定することです。

以下リファレンスです。
http://www.htmq.com/canvas/createRadialGradient.shtml

こちらで、色々試してみて下さい。

パターンについて

パターンとは、Canvasの描画領域上に任意の画像を敷き詰めて描画する手法のことです。
まずは、任意の画像を読み込む必要があります。
方法は前項と同様に行います。

JavaScript
let img = new Image();
img.src = '../images/test.png';

そして、パターンの指定を行います。

JavaScript
window.addEventListener('load', function () {
  ctx.beginPath();
  let imagePattern = ctx.createPattern(img, 'repeat-x');
  ctx.fillStyle = imagePattern;
  ctx.rect(0, 0, canvasW, canvasH);
  ctx.fill();
  ctx.closePath();
});

描画開始位置は、デフォルトではCanvas要素の左上です。
上記のようにrect()で開始位置を指定すれば、そこから描画されます。

createPattern()は二つの引数をとり
第一引数:Imageインスタンスを格納した変数
第二引数:パターンの繰り返し方の指定

第二引数には
repeat = 水平/垂直両方向に繰り返す
repeat-x = 水平方向に繰り返す
repeat-y = 垂直方向に繰り返す
no-repeat = 繰り返さない
の4種類が指定可能です。

以下に参考サイトと、パターンCodePen貼っておくので
ブタちゃん並べて遊んでみて下さい!🐷(特大)

http://www.htmq.com/canvas/createPattern.shtml

6.曲線を描ける

曲線の描画方法は2種類あって、2次ベジェ曲線と3次ベジェ曲線です。
両者の違いは、制御点の数です。
2次はひとつ、3次は2つあります。

言葉での説明だけではイメージが湧かないかもしれないので
詳細はリファレンスをご覧ください。
http://www.htmq.com/canvas/quadraticCurveTo.shtml#:~:text=ベジェ曲線とは、制御,て形状が決まります。

具体的な描画方法は以下です。

JavaScript

//2次ベジェ曲線
ctx.beginPath();
ctx.moveTo(x1,y1);//曲線の始点座標
ctx.quadraticCurveTo(cx1,cy1,x2,y2);
ctx.stroke();

//3次ベジェ曲線
ctx.beginPath();
ctx.moveTo(x1,y1);//曲線の視点座標
ctx.bezierCurveTo(cx1,cy1,cx2,cy2,x2,y2);
ctx.stroke();

2次ベジェ曲線には
quadraticCurveTo(cx1,cy1,x2,y2); を使用します。
cx1:一つ目の制御点のx座標
cy1:一つ目の制御点のy座標
x2:曲線の終点のx座標
y2:曲線の終点のy座標

3次ベジェ曲線には
bezierCurveTo(cx1,cy1,cx2,cy2,x2,y2);を使用します。
cx2:二つ目の制御点のx座標
cy2:二つ目の制御点のy座標

CodePenを貼っておきますので、制御点を動かしてどのように変化するかお試し下さい。

7.テキストを描画できる

Canvas2Dでは、テキストを描画することも可能です。
しかしこれは、あくまで画像としての描画になるので
文書構造としての意味は全く持ちません。
このことから、サイト内で重要な意味を持つテキストをCanvas2Dで描画することは
望ましくないだろうということが言えます。

描画方法は以下です。

JavaScript
//塗りつぶしテキスト
ctx.font = '50px bold sans-serif';
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'start';
ctx.fillStyle = 'red';
ctx.fillText('Canvas2D色々できるね',y);

//縁取りテキスト
ctx.font = '50px bold sans-serif';
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'start';
ctx.strokeStyle = 'blue';
ctx.strokeText('Canvas2D色々できるね',x,y);

ctx.fontによって、フォントのスタイルを指定します。
CSSで言うところの
「font-size」「font-weight」「font-family」の設定です。

ctx.textBaselineで、フォントの縦方向の位置を調整します
CSSでいうところの、「vertical-align」に当たります。
具体的な描画位置は、リファレンスをご確認ください。
http://www.htmq.com/canvas/textBaseline.shtml

ctx.textAlignはテキストの寄せ方を指定します。
CSSで言うところの、「text-align」です。(そのまんま)
指定方法も同じですが、一応リファレンスも貼っておきます。
http://www.htmq.com/canvas/textAlign.shtml

ctx.fillStyle or ctx.strokeStyle
この箇所で色の指定をします。

最後に、ctx.fillText() or ctx.strokeText()で描画を行います
どちらも引数を4つ取ることができます。
第一引数:描画したいテキストの内容
第二引数 :描画開始位置のx座標
第三引数: 描画開始位置のy座標
第四引数:描画範囲の最大値(任意です)

ちなみに、ctx.lineWidth = 20
などとすれば、縁取り文字の縁の幅も指定可能のようでした。

注意点は特にありませんが、実際の描画結果は
次項の最後のまとめて記載します。

8.ドロップシャドウを描画できる

描画方法は以下です。

JavaScript
ctx.shadowBlur = 10;//ぼかし幅
ctx.shadowOffsetX = 5;//水平方向の影のサイズ
ctx.shadowOffsetY = 5;//垂直方向の影のサイズ
ctx.shadowColor = 'grey';//影の色を指定して描画

ドロップシャドウは、図形にもテキストにも指定可能ですが
いくつか仕様があるようでした。

  • 実際の描画は、「shadowColor」を指定した時点で行われる。
  • shadowColorの指定より下にある要素に対して影が適応される。
  • それぞれの要素の影スタイルを変更したいときは、変更したい要素の直前にスタイルを記載すると可能。

前項のテキストの描画結果も含めたCodePenを添付するので
shadowColorの記載位置を変えて、どのテキストに描画されるのか試してみることをお勧めします。

9.描いたものを動的に変化させられる(アニメーションができる)

ここまでの説明で、Canvas2Dを用いて様々な要素を描画してきました。
しかし、ここからが恐らく皆さんの求めているものかもしれません。
静止画が書けることはわかった。
でも、あのオサレサイトにあるみたいな動きはどうやって再現するんだ??
その全てを解説するとなると、途方もない量になると思いますし
根本的に、僕の技量が到底及びません。
なので、ここからはCanvas2Dアニメーションの基盤となるであろう手法
スキル、知識などについて言及したいと思います。
心を天使にしてご覧頂けますと幸いです。

Canvasアニメーションの基本

Canvasにおけるアニメーションには、共通した流れがあります。
それが以下です。

1.前に描画したものを消す
2.描画するものの位置や形などを変更する
3.描画する
4.1に戻る

これの繰り返しで、描画した要素をアニメーションさせることができます。
まずはシンプルなアニメーションを例に、具体的な方法を記載します。

requestAnimationFrameを用いたアニメーション(基本)

今回はまず、「要素を右方向に移動させるアニメーション」
を例に説明したいと思います。(アニメーションと言えるかわかりませんが!)

コードの全体像は以下です。

JavaScript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

canvas.width = window.innerWidth * devicePixelRatio;
canvas.height = window.innerHeight * devicePixelRatio;

const canvasW = canvas.width;
const canvasH = canvas.height;

//移動させたい要素のx座標を変数化。初期値を設定
let x = 0;

//移動させたい要素を描画する関数
function render() {
  ctx.beginPath();
  ctx.fillStyle = 'blue';
  ctx.fillRect(x, canvasH / 2, 500, 300);
  ctx.closePath();
}

//描画を更新する関数
function update() {
  //描画を削除
  ctx.clearRect(0, 0, canvasW, canvasH);
  x++;//x座標を1pxずつ増やす
  //x座標が500pxを超えたら。つまり、500px移動したら0に戻る(左端に戻す)
  if (x > 500) {
    x = 0;
  }
  //要素を再描画する
  render();
  //自分(update関数)を描画が終了するたびに呼び出す
  window.requestAnimationFrame(update);
}

update();

描画の削除方法は以下です。

JavaScript
ctx.clearRext(x,y,width,height);

clearRect(x,y,width,height)によって、任意の範囲の要素を削除することができます。
x:描画を削除したい領域の左上頂点のx座標
y:描画を削除したい領域の左上頂点のy座標
width:描画を削除したい領域の横幅
height:描画を削除したい領域の縦幅

requestAnimationFrameを用いたアニメーションのポイントとしては
1.描画するコードと、アニメーション用のコードをそれぞれ関数化して分ける
2.requestAnimationFrame()を使用して、関数を繰り返し呼び出す

であると考えています。

1については、文章の通りなので割愛します。

2について
requestAnimationFrame()は、ブラウザの描画速度(FPS)に合わせてコールバック関数を実行してくれるメソッドです。
しかし、

JavaScript
window.requestAnimationFrame(update);

のように、普通に呼び出すだけでは1回の呼び出して終了してしまいます。
これを、FPSに従って繰り返し呼び出すために、

JavaScript
function update(){
 window.requestAnimationFrame(update);
}

update();

関数の中で、自分をrequestAnimationFrame()のコールバック関数とすることで
繰り返し呼び出されるようにできます。

詳しい挙動に関しては、以下記事が大変参考になりますので
是非ご覧ください!

https://www.webdesignleaves.com/pr/jquery/requestAnimationFrame.html

三角関数を用いたアニメーション

Canvasに限らず、アニメーションを実装するに当たって三角関数を活用することは多々あるように思います。
三角関数の大きな特徴として挙げられるのは、その「連続性」「周期性」にあります。
引数(ラジアン)を入れると、必ず「-1〜+1」の範囲の値が返ります。

三角関数に関しては、以下のサイトが大変分かりやすかったので
ぜひご覧ください。

https://sbfl.net/blog/2017/02/17/javascript-animation-with-trig-functions/

それでは、三角関数の性質を利用してシンプルな波線のアニメーションを作成したいと思います。
参考にさせて頂いたサイトは以下です。
https://ics.media/entry/18812/
こちらサイトでも、かなりわかりやすく解説されてあるのですが
「三角関数・Canvas、ナンモワカラン」状態の僕が見ると、少しだけ難しく感じたので
もう少し掘り下げて説明してみたいと思います。

今回実装するのは以下のような波線アニメーションになります。

それでは説明に入りますが、その前に。
前提知識として、線分の描画方法をもう少し詳しくみておきます。

線分の描画(ちょっとだけ応用)

この記事の冒頭の方で述べたように、線分は以下のコードで描画できます。

JavaScript
//コンテキストの生成などは省略(前項と同じ)

ctx.beginPath();
ctx.moveTo(0, canvasH / 2);//始点
ctx.lineTo(canvasW, canvasH/2);//終点
ctx.stroke();//描画
ctx.closePath();

しかし、これでは一本の直線のみしか描画できません。
線分の角度をジグザグに描画したい場合は以下のようにします。

JavaScript
ctx.beginPath();
ctx.moveTo(0, canvasH / 2);//始点
ctx.lineTo(100,canvasH/2-100)//次の点
ctx.lineTo(200,canvasH/2+100)//次の点
ctx.lineTo(300,canvasH/2-100)//次の点
ctx.lineTo(400, canvasH / 2 + 100)//次の点
//....................繰り返し
ctx.lineTo(canvasW, canvasH/2);//終点
ctx.stroke();//描画
ctx.closePath();

lineToで次の点を記載していき、strokeで全てを繋ぎ合わせます。
そうすることで、好きなようにジグザグ線を書くことが可能になります。

描画のためのcanvasを用意

参考サイトとの対応箇所がわかりやすいように、タイトルと変数名や関数名は極力同じにさせて頂きたいと思います。

JavaScript

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

//今回はcanvasを全画面に指定します
canvas.width = window.innerWidth * devicePixelRatio;
canvas.height = window.innerHeight * devicePixelRatio;

const canvasW = canvas.width;
const canvasH = canvas.height;

function draw() {
  // 画面をリセット
  context.clearRect(0, 0, canvasW,canvasH);
  context.lineWidth = 10; // 線の太さ
  context.beginPath(); // 線の開始
  context.strokeStyle = "white"; // 線の色
  context.moveTo(0, stageH / 2); // 開始点
  context.lineTo(stageW, stageH / 2); // 終了点
  context.stroke(); // 線を描く
}

この部分は、今まで説明していますので割愛します。
参考サイトと本記事では変数名が若干異なっておりますので目だけ通してください。

曲線を描く

先ほどのコードを変更し、曲線っぽいものを描画できるようにします。

JavaScript
//draw関数の中身を以下に更新します。
function draw() {
  // 画面をリセット
  ctx.clearRect(0, 0, canvasW,canvasH);
  ctx.lineWidth = 5; // 線の太さ
  ctx.beginPath(); // 線の開始

  const segmentNum = 10; // 分割数
  const amplitude = canvasH / 3; // 振幅
  const time = Date.now() / 1000; // 媒介変数(時間)
  
  for (let i = 0; i < segmentNum; i++){
    const x = (i / (segmentNum-1)) * canvasW;
    // ラジアン
    const radian = (i / segmentNum) * Math.PI+time;
    // Y座標
    const y = amplitude * Math.sin(radian) + canvasH / 2;
  
    if (i === 0) {
      ctx.moveTo(x, y);
    } else {
      ctx.lineTo(x, y);
    }
  }
  ctx.stroke(); // 線を描く
}

draw();

解説

僕が難しく感じたところを、順に言語化していきます。

JavaScript
const x = (i / (segmentNum-1)) * canvasW;

一見すると、頭の中が「????????」になりました。
冷静になりましょう。
このコードで行いたいことは、「線の分割」です。
先ほどの「線分の描画」の項目にあった、ジグザグの線分の描画方法を思い出してください。

draw関数の中で行われていることも同様です。
始点から始まり、次点、次点、次点、次点、、、、、終点と結んだ線を描画しています。
その、全ての点のy座標を動的に変化させることによって波線を描画しています。

ここで言うところの「変数x」というのは、分割点のx座標を表しています。
つまり上記式は、canvasWを i/分割数 をして、それぞれのx座標を求めていることになります。

また、「分割数 - 1」としている理由は、逆に-1をしない式で描画してみるとわかるかと思いますが、
条件式が、「i < segmentNum」としており、i = segmentNumとはならないため
「分割数 - 1」としなければ、画面右端まで描画されないことになります。

JavaScript
 // ラジアン
 const radian = (i / segmentNum) * Math.PI+time;
 // Y座標
 const y = amplitude * Math.sin(radian) + canvasH / 2;

radianについて、これは三角比に代入する角度の変数です。
(i/segmentNum)によって分数値を得て、Math.PIで「π」を乗算します
そして、time(Date.now() / 1000)によって、経時的な数値を加算することで
波線に変化を持たせます。
1000で徐算しているのは、y座標の変化を調整するためです
試しに徐算をなしで描画してみると、エライことになるのがわかるかと思います。

次に、変数yについてですが
「Math.sin(radian)」この数値は、radianにどんな数値が入ろうとも
必ず「-1 〜 +1」の範囲で変化します。
つまり、「amplitude * Math.sin(radian)」は
「-canvasH/3 〜 +canvasH/3」の範囲で変化し、それにcanvasH/2を足すことで
高さ中央の位置を中心に、上下にcanvasH/3(amplitude)の範囲でy座標が変化することになります。

JavaScript
if (i === 0) {
   ctx.moveTo(x, y);
} else {
   ctx.lineTo(x, y);
}

i===0の時、つまり始点の時は moveToでペンを置きます
それ以外は、lineToで点を置いていきます。
その後に、strokeで全ての点を結んでジグザグの波を作ります。

分割数が10なので、この数を大きくすればより滑らかな波を描画できます。
ここまでのコードで、繰り返しブラウザをリロードすると経時的に波が変化する様子を見ることができると思います。

後は、この関数をrequestAnimationFrame()を用いて描画速度に応じて呼び出せば良いので、

JavaScript
function draw() {
  // 画面をリセット
  ctx.clearRect(0, 0, canvasW,canvasH);
  ctx.lineWidth = 5; // 線の太さ
  ctx.beginPath(); // 線の開始

  const segmentNum = 100; // 分割数
  const amplitude = canvasH / 3; // 振幅
  const time = Date.now() / 1000; // 媒介変数(時間)
  
  for (let i = 0; i < segmentNum; i++){
    const x = (i / (segmentNum-1)) * canvasW;
    // ラジアン
    const radian = (i / segmentNum) * Math.PI+time;
    // const radian = (segmentNum/i) * Math.PI+time;
    // Y座標
    // const y = amplitude * Math.sin(radian) + canvasH / 2;
    const y = amplitude * Math.cos(radian) + canvasH / 2;
  
    if (i === 0) {
      ctx.moveTo(x, y);
    } else {
      ctx.lineTo(x, y);
    }
  }
  ctx.stroke(); // 線を描く

  window.requestAnimationFrame(draw);
}

draw();

これで、波の描画ができました!!

Canvas2Dちょっと学んでみて、感じたコツ的なもの

これはもう、僕が言うのも大変烏滸がましいと思いますが
ちょっとだけ学んでみて、いくつかスキルを身につけていくためのコツ的なものを
考えてみたので、お時間ある方は目を通してみてください。

1.紙にかいて考える

Canvas2Dは、ブラウザ左上座標(0,0)を基準に全ての位置指定を座標で行う必要があります。
理想のグラフィックを、ブラウザ上で座標指定しながら思い通りに描画することは
最初のうちはかなり難しいかもしれません。
なので、僕は方眼用紙に一度書いてみてからコードの反映するようにしてみました。
その方が、座標を目で見てわかるので描きやすく感じました。

2.変数には具体的な数値を入れて考える

学ぶにあたって、参考サイトのコードを解読しているとき。
大抵詰まるところは、「この変数はナンダ??」と言うところです。
なので、具体的な数値に置き換えてみて描画がどのように変化するかを見ることで
変数の意味するところを考えてみました。

3.変数の取りうる範囲を考える

変数に数値が格納されている場合。その変数はどの範囲で推移するものなのかを考えます。

4.自分の言葉で説明してみる

これは、何を学ぶにあたっても重要なことだと思っています。
参考サイトを真似してコードを書いただけでは、実際自分だけで考えて描くときに
手も足も出ません(でした。)。
なので、コード内にコメントで説明を書くなり
誰かに説明してみるなりして、アウトプットするのが良いように思います。

今後、言及したいこと

今回の記事では、requestAnimationFrameを用いてFPSに応じた
アニメーションの実装までしか書くことができませんでした。
今後、任意の時間や任意の距離に応じたアニメーションの実装方法について検討して
記事にまとめられたらと思います。
合わせて、より実践的なCanvas使用例についてもまとめたいと思っています。

おわりに

今回Canvas2Dをちょっと学んでみて、すぐに記事を書いたのですが
やはり理解が浅い箇所が多々あって。本当に多々あって。。
調べながら、検証しながら書いたので書き上げるのに時間がかかりました。
しかし、想像以上に勉強になりました。
多分この記事を書いたことで、基本的な描画方法については
忘れないはずです。(忘れたらこの記事読みます)
多彩なアニメーションを作るためには、「たくさん見る」こととと「たくさん作る」ことが
大切であると考えています。いろんなサイトをひたすらに見て目を養って
手を動かして、アウトプットをしてスキルを磨いていきたいです。
ここまで読んで頂き、本当にありがとうございます。
これからCanvas2Dを学び始めるどなたかのお力に、少しでもなれたら本望です。

Discussion

とんび@鳶嶋工房とんび@鳶嶋工房

こんにちは、最近 canvas いろいろ調べてて辿り着きました。
fillRect() は fill() がなくても描画され、逆に rect() + stroke() は、strokeRect() を使えば stroke() なしでも描画されませんか?