🐧

Canvas2Dアニメーション基本のキ

2022/09/13に公開

はじめに

ブラウザ上に、JavaScript(など)を用いて自由にグラフィックを描画できるCanvasという技術。
文字通り、真っ白なキャンバスに筆を下ろして絵を描くが如く
その可能性は無限大です。(やかましい)

今回は、基本的な描画方法に関する記載は致しておりません。
アニメーションの基盤となる考え方や手法について焦点を当てて書いてみようと思います。
(基本的な描画方法から知りたい方は、以前の記事をご覧頂けますと幸いです)
と言っても、だいぶCanvasにわかなので間違いなどあるかと思われます。
その際は、優しい口調でご指摘頂けますと大変嬉しい限りでございます。

この記事の目指すところ

この記事の対象としては、前回の「Canvas2Dチョットワカルへの第一歩」の記事を読んで頂いて
基本的な描画方法についてご理解頂けている且つ、
三角比について最低限の理解がある方を想定しております。
(自分の記事で理解してもらえたら本望)
そして、次の段階を求める方々。
「じゃあ、どうやってこいつら動かすの?」という状態の方々です。
あくまで「基本」を記載していくので、ご期待されているような
オサレサイトの複雑なアニメーションまでは到達致しませんので、
その点ご了承下さい。では、前置きが長いので本題入りましょう!!

アニメーションの考え方

「アニメーション」と聞くと、皆さんはどのようなものを思い浮かべますでしょうか?
Canvasにおいて、要素をアニメーションさせるには
パラパラ漫画のように、要素が動いているように「見せかける」ことが重要になります。
つまり、流れとしては以下です。

1.スタート地点の描画
2.描画のリセット
3.動かしたい方向へ再描画(以下2-3を目的位置まで繰り返す)
4.ゴール地点の描画

この処理を、requestAnimationFrameのループの中で行うのが基本です。
1と4はループの中で記載されないことが一般的ですが
考え方の基盤として、ここでは記載させて頂きました。

結局のところ、「リセット」と「再描画」の繰り返しを行うことで
要素が動いているように見せかけることができるようになります。

ちなみに、巷で話題のGSAPなどのTween機能とは何かと言うと
上記にアニメーションの流れにある
「スタート地点の描画(指定)」と「ゴール地点の描画(指定)」を行うだけで、
その真ん中の状態を「補完」してくれる機能のことです。
(めっちゃ便利)
なので、基本を抑えたらどんどん活用しちゃいましょう!!

まずは動いている様子を見る

文章だけでは、つまらんと思うのでまずは動かしてみましょう。

loadイベントの中でアニメーションに関する関数を読み込んでいます。
見たい方を残して、もう一つをコメントアウトして確認してみて下さい。
青い四角が動いていることがわかるかと思います。

アニメーションの手法

アニメーションは、本当にいろいろなものがありますが
それらの基盤は以下に通じていると考えています。
(※あくまで個人の見解です)

1.要素を移動させる
2.要素を変形させる
3.連続的に描画する
4.色を変化させる

偉そうなことを言っていますが、これから僕もアニメーションを用いているサイトを見て学びながら
上記で説明できない挙動に出くわすと思います。
そのときは、こっそり付け加えにきます。
皆さんも、何か気づきがありましたらぜひ教えて下さい。(他力本願)

では、順番に見ていきましょう。

1.要素を移動させる(基本)

これは先ほどの例であげた動きについてです。

正確には、移動している「ように見せる」ということになります。

コードは以下です。

JavaScript
//Canvas読み込み省略

let x = 100;
function translate() {
  x++
  ctx.clearRect(0,0,canvas.width, canvas.height);//リセット
  ctx.beginPath();
  ctx.fillStyle = 'blue';
  ctx.fillRect(x, 100, 100,100);
  ctx.fill();
  requestAnimationFrame(translate);//繰り返し呼び出す
}
translate();

流れとしては、前述した内容と同じく
1.描画のリセット
2.再描画
を繰り返しています。
再描画の際に、x座標を1px加えることで右に1pxずつ動くアニメーションが得られます。

ポイント1

ここで、初めて出てきたのが

JavaScript
ctx.clearRect(0,0,canvas.width, canvas.height);

この部分で、描画をリセットしています。

clearRect(x,y,w,h)
x:描画をリセットする範囲の左上頂点x座標
y:描画をリセットする範囲の左上頂点y座標
w:描画をリセットする範囲の横幅
h:描画をリセットする範囲の縦幅

つまり上記は、canvas全体をリセットしています。

ポイント2

リセット ⇆ 再描画 を繰り返す方法として用いているのが
requestAnimationFrameメソッドです。
これは、JavaScriptのメソッドなのでCanvasに限らず利用できます。
詳細の説明は他サイトに委ねますが、
挙動としては、「渡した関数を、ブラウザの表示を邪魔しないタイミングで呼び出す」となります。
以下、リファレンスと参考サイトです。

https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame

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

つまり、このメソッド単体では繰り返しはできずに
一回呼び出されるだけで終わってしまいます。

そこで、疑似的なループを作成します。

JavaScript
function loop(){
//繰り返したい処理
requestAnimationFrame(loop)
}
loop()

上記のように、繰り返したい関数の中でrequestAnimationFrameを用いて自分を呼び出すことで
その関数が呼び出されたときに、繰り返し呼ばれることになります。
この繰り返される関数のなかでx座標を1pxずつ増やすことで
右方向に移動する動きが得られるという流れです。

1.要素を移動させる(+三角比)

では、ちょっとだけ応用して
動きに三角比を用いてみましょう。
三角比の特徴は、その周期性にあります。
どんなに角度(ラジアン)が増えようと、例えプロペラ並みに回転しようとも
得られる数値は必ず「-1 〜 +1」の間を周期的に増減します。
(三角比に関する詳細の説明は他サイトに委ねます。)

振幅運動

まずは横方向に周期的に動くアニメーションです。
今回はx座標100pxから500pxまでを周期的に移動させてみます。
つまり、300pxを基準にして前後に200pxずつ移動させることになります。
よって、変化の値として欲しい値は「-200px 〜 +200px」となります。
これは、三角比の変化値である 「-1 〜 +1」を利用することによって得られます。

まずは、-200 〜 +200を周期的に推移する変数を記載してみます。

JavaScript
const myNumber = 200; //推移させたい数値の最大値(絶対値)
const division = 1000; //Date.nowをそのまま使うと周期が早すぎるので、希望の数値で割る
const digit = 3; //表示したい小数点以降の桁数
const startTime = Date.now() / division; //アニメーション開始時間
let elapsedTime = 0; //アニメーション開始時間からの経過時間を格納
function move() {
  elapsedTime = Date.now() / division - startTime; //開始時間-関数内で現在時間を引けば経過時間が得られる
  const period = Math.sin(elapsedTime).toFixed(digit) * myNumber;
  console.log(period);
  requestAnimationFrame(move);
}
move();

上記のコードでperiodが -200 〜 +200 の間で推移します。
この内容は以下記事を参考に(もろパクリ)させて頂きました
ありがとうございます!!

https://zenn.dev/kobito/articles/a71c332f2d28b6

説明はコード内に記載しておりますので、ご確認下さい。
上記で希望の範囲を推移する変数「period」を得られました。
あとは、これを表示したい要素のx座標に反映させるのみです。

JavaScript
const myNumber = 200; //推移させたい数値の最大値(絶対値)
const division = 1000; //Date.nowをそのまま使うと周期が早すぎるので、希望の数値で割る
const digit = 3; //表示したい小数点以降の桁数
const startTime = Date.now() / division; //アニメーション開始時間
let elapsedTime = 0; //アニメーション開始時間からの経過時間を格納
let x = 300;
function move() {
  elapsedTime = Date.now() / division - startTime; //開始時間-関数内で現在時間を引けば経過時間が得られる
  const period = Math.sin(elapsedTime).toFixed(digit) * myNumber;
  ctx.clearRect(0, 0, canvasW, canvasH);
  ctx.beginPath();
  ctx.fillStyle = "blue";
  ctx.fillRect(x + period, 100, 200, 50);
  requestAnimationFrame(move);
}
move();

これでx座標 100px 〜 500pxの間を振幅運動する四角形の描画ができました。

円周運動

次は、円周運動です。今回は正円の円周に沿って要素を移動させてみます。
円周運動については、先ほどの振幅運動の方法と同じで
x座標のみでなく、y座標も同じように変数化すれば可能となります。
x座標y座標共に100px〜500pxの間を移動する要素を描画してみたいと思います。

JavaScript
const myNumber = 200; //推移させたい数値の最大値(絶対値)
const division = 1000; //Date.nowをそのまま使うと周期が早すぎるので、希望の数値で割る
const digit = 3; //表示したい小数点以降の桁数
const startTime = Date.now() / division; //アニメーション開始時間
let elapsedTime = 0; //アニメーション開始時間からの経過時間を格納
let x = 300;//x座標の初期値を設定(移動させたい範囲の中央)
let y = 300;//y座標の初期値を設定(移動させたい範囲の中央)
function moveCircle() {
  elapsedTime = Date.now() / division - startTime; 
  const periodX = Math.sin(elapsedTime).toFixed(digit) * myNumber;//-200 〜 +200の間を推移
  const periodY = Math.cos(elapsedTime).toFixed(digit) * myNumber;//-200 〜 +200の間を推移
  ctx.clearRect(0, 0, canvasW, canvasH);
  ctx.beginPath();
  ctx.fillStyle = "blue";
  ctx.fillRect(x + periodX, y + periodY, 50, 50);//
  requestAnimationFrame(moveCircle);
}
moveCircle();

ポイントは、Y座標の取りうる値「periodY」はMath.cosを使用している点です。
先ほどのように、sinでも同様に-200〜+200の間を変化する値を実現することは可能です。
しかし、円周運動を実現するためには
sin=1(periodX=200)の時、periodY=0,sin=0(periodX=0)の時、periodY=0 or 200
となる必要があるため、sinでは実現できません。
以上の理由よりcosを用いて実現しました。

2.要素を変形させる(基本)

次は、要素の変形アニメーションを実装してみたいと思います。
具体的な変形はまず、要素の拡大縮小の動きについてです。

要素の拡大/縮小

方法は2パターンあると考えていて、
・描画のサイズ自体を変数で変化させる方法
・scaleを用い、拡大/縮小させる方法
です。

描画のサイズ自体を変数で変化させる

JavaScript
//拡大
let scaleUpW = 50;
let scaleUpH = 50;
function scaleUp() {
  ctx.clearRect(0, 0, canvasW, canvasH);
  scaleUpW++;
  scaleUpH++;
  if (scaleUpW > 300) {
    scaleUpW = 50;
    scaleUpH = 50;
  }
  ctx.beginPath();
  ctx.fillRect(50, 50, scaleUpW, scaleUpH);
  ctx.closePath();
  requestAnimationFrame(scaleUp);
}
scaleUp();

//縮小
let scaleDownW =300;
let scaleDownH = 300;
function scaleDown() {
  ctx.clearRect(350, 0, canvasW, canvasH);
  scaleDownW--;
  scaleDownH--;
  if (scaleDownW < 50) {
    scaleDownW = 300;
    scaleDownH = 300;
  }
  ctx.beginPath();
  ctx.fillRect(450,50, scaleDownW,scaleDownH);
  ctx.closePath();
  requestAnimationFrame(scaleDown);
}
scaleDown();

上記コードで、拡大/縮小を繰り返す四角形の描画可能です。
行っていることは、要素の移動アニメーションとほぼ変わりません。
変化させる値を位置ではなく、サイズにしたということです。

ポイントは、条件式を用いて拡大縮小の範囲を設定している点です。
そうしなければ、無限に拡大縮小をしてしまうためです。
(もちろん、無限にしたい場合があれば不要ですが…)

また、今回は四角形の拡大縮小を行いましたが
円の場合も同様で、変化させる値を半径とすれば実現可能です。

scaleを用い、拡大/縮小させる

ここで使用するのは、
オブジェクト.scale(x,y)です
x:x方向の拡大率(1が100%)
y:y方向の拡大率

このscaleの値を変化させて拡大縮小させます。
他の箇所は同様なので、説明を割愛させて頂きます。

ベジェ曲線の変形

今回は2次ベジェ曲線を変形させてみようと思います。

JavaScript
let y = 150;//制御点のy座標初期値
function curve() {
  y++;
  ctx.clearRect(0, 0, canvasW, canvasH);
  ctx.beginPath();
  ctx.moveTo(100, 100);
  ctx.quadraticCurveTo(600,y,1100,100);//制御点の座標を変数化
  ctx.stroke();
  if (y > 1000) {
    y = 150;
  }
  requestAnimationFrame(curve);
}
curve();

制御点のy座標を変数化し増加させています。
拡大縮小の際と同様に、条件式で上限を設けています。
下記codepenで制御点の移動も描画し可視化していますのでご確認下さい。

2.要素を変形させる(+三角比)

要素の拡大/縮小

三角比の周期性を用いて、拡大縮小させてみます。
今回は円でやってみます。
半径が100〜300の間を周期的に変化する円の描画です。

JavaScript
const myNumber = 200; //推移させたい数値の最大値(絶対値)
const division = 1000; //Date.nowをそのまま使うと周期が早すぎるので、希望の数値で割る
const digit = 3; //表示したい小数点以降の桁数
const startTime = Date.now() / division; //アニメーション開始時間
let elapsedTime = 0; //アニメーション開始時間からの経過時間を格納
let radius = 200;//半径初期値
let x = 100;
function scaleCircle() {
  elapsedTime = Date.now() / division - startTime; //開始時間-関数内で現在時間を引けば経過時間が得られる
  const period = Math.sin(elapsedTime).toFixed(digit) * 100; //-100 〜 +100の間を推移
  ctx.clearRect(0, 0, canvasW, canvasH);
  radius = period + 200;//100〜300を推移
  ctx.beginPath();
  ctx.fillStyle = 'skyblue';
  ctx.arc(300, 300,radius, 0, Math.PI * 2);
  ctx.fill();
  requestAnimationFrame(scaleCircle);
}
scaleCircle();

半径(radius)を三角比を用いて周期的に変化させています。
方法は要素の移動の時と同じなので、説明は割愛します。

ベジェ曲線の変形

こちらも、制御点を三角比を用いて周期的に移動させてみようと思います。
今回は、3次ベジェ曲線でやってみようと思います。

JavaScript
const myNumber = 300; //推移させたい数値の最大値(絶対値)
const division = 1000; //Date.nowをそのまま使うと周期が早すぎるので、希望の数値で割る
const digit = 3; //表示したい小数点以降の桁数
const startTime = Date.now() / division; //アニメーション開始時間
let elapsedTime = 0; //アニメーション開始時間からの経過時間を格納
let radius = 200;//半径初期値
function bezierCurve() {
  elapsedTime = Date.now() / division - startTime; //開始時間-関数内で現在時間を引けば経過時間が得られる
  const period1 = Math.sin(elapsedTime).toFixed(digit) * myNumber; //-300 〜 +300の間を推移
  const period2 = Math.cos(elapsedTime).toFixed(digit) * myNumber; //-300 〜 +300の間を推移
  const point1 = 300 + period1;//0〜600の間を推移
  const point2 = 300 + period2;//point1とは別周期で同様の範囲を推移
  ctx.clearRect(0, 0, canvasW, canvasH);
  ctx.beginPath();
  ctx.moveTo(100, 300);
  ctx.lineWidth = 5;
  ctx.bezierCurveTo(350, point1, 850, point2, 1100, 300);
  ctx.stroke();
  requestAnimationFrame(bezierCurve);
}
bezierCurve();

制御点の変化にMath.sinとMath.cosをそれぞれ使用している意味は特にありません。
(違う動きをした方が面白そうだったから)

同様にcodepenで制御点の動きも可視化しているのでご確認お願いします。

3.連続的に描画する

これは、SVGの一筆書きアニメーションを想像して頂ければわかりやすいかもしれません。
(SVGナンモワカランので違ったらすいません)
これまでは、clearRectで描画をリセットしてきましたが
リセットせずに描画することで、パスを書くようなアニメーションが実現できます。
ただ、SVGのようにイラレでパスデータを作成できないため
Canvas2Dにおけるパスは、完全に己の数学的スキルにかかっていると思っています。

ということで、数学的知識は皆無なので
謎のトルネードを描画してみたいと思います。

JavaScript
const myNumber = 200; //推移させたい数値の最大値(絶対値)
const division = 1000; //Date.nowをそのまま使うと周期が早すぎるので、希望の数値で割る
const digit = 3; //表示したい小数点以降の桁数
const startTime = Date.now() / division; //アニメーション開始時間
let elapsedTime = 0; //アニメーション開始時間からの経過時間を格納
let x = 0;
function tornado() {
  elapsedTime = Date.now() / division - startTime; //開始時間-関数内で現在時間を引けば経過時間が得られる
  const period = Math.sin(elapsedTime).toFixed(digit) * myNumber; //-200 〜 +200の間を推移
  let periodY = period + 200; //0 〜 +400の間を推移
  x++;
  ctx.beginPath();
  ctx.arc(x,periodY,5,0,Math.PI*2);
  ctx.fill();
  if (x > canvasW) {
    x = 0;
    periodY = 200;
  }
  ctx.stroke();
  requestAnimationFrame(tornado);
}
tornado();

もはや解説は不要かと思われますが、
x座標を徐々に右に移動させながら、y座標を三角比で周期的に増減させています。
x座標が画面右端にたどり着いたら、0に戻して再度描画が始まるように条件式を書いています。
(破滅的な数学力でごめんなさい)

連続的描画は、数学的な式で表現できる範囲で可能となりますので
己を信じて、色々と遊んでみてください。

4.色を変化させる

最後に要素の色を動的に変化させる方法です。
通常、Web制作において色を指定するときはrgb(a)を使用するかと思いますが
アニメーションさせたい場合は、HSLカラーを用います。
HSLについて簡単にご説明すると
H:色相(Hue)
S:彩度(Saturation)%
L:明度(Luminance)%
の三つの値を用いて、色を指定する方法です。

色相は,360度の円で表現されるので
色をアニメーションさせたい場合は、0〜360の間で変化する変数を作れば良いです。
HSLについての詳細の解説は下記記事をご参照ください。

https://ics.media/tutorial-createjs/color_hsl/

では、色が経時的に変化する円を描画してみます。

JavaScript
let hue = 0;
function colorful() {
  hue += 0.5;
  if (hue > 360) {//hueが360を超えたら0にリセット
    hue = 0;
  }
  ctx.beginPath();
  ctx.arc(200,200, 200, 0, Math.PI * 2);
  ctx.fillStyle = `hsl(${hue},100%,50%)`;
  ctx.fill();
  requestAnimationFrame(colorful);
}
colorful();

実装例

では、ここからは実際のアニメーションの作例をご紹介したいと思います。
使用している技術についても一緒に提示します。
アニメーションのバリエーションが少ないのは許してください。
まだ僕も、Canvas2D入門の敷居を跨げたかどうかのレベルですので…

テーマ深海

3色のパーティクルをランダムに発生させ、上昇。画面上に辿り着いたらy座標を画面縦幅に再設定し上昇させる挙動です。
参考サイトがあるので、詳細の説明はそちらを参照ください。

https://liginc.co.jp/548806

使用技術:要素の移動

紅葉

3種類の紅葉を上から動きをつけながら舞い散らせました。
setTransformメソッドを用いて、回転の動きをつけています。
詳細は参考サイトの記載されておりますので、ご確認下さい。

https://www.otwo.jp/blog/canvas_sakura/

使用技術:要素の変形、色を変える

なんかすごい波

これは、我らがICS MEDIA様の記事をもろパクリさせて頂いたものですが
実際に作ったらこんななるよという例として載せさせて頂きます。
線分の分割点のy座標をパーリンノイズを用いて動的に変化させて
ランダムな波を表現しています。
詳細は、参考サイトをご覧ください。

https://ics.media/entry/18812/

使用技術:要素の形を変える

パーティクルマウスストーカー

カーソルの動きに連動してランダムカラーのパーティクルが発生する例です。
こちらは、ICS MEDIA様の記事を参考にしましたが
Create.jsを用いて実装してあったものを、pureCanvasに書き換え
かつ、画面端全てにパーティクルが到達したら跳ね返るような挙動をつけています。

https://ics.media/tutorial-createjs/particle/

使用技術:要素の移動、色を変える

今後の課題

つらつらとアニメーションについてまとめて来ましたが、一つ課題があります。
それは、requestAnimationFrameによるループ処理では
アニメーションの時間設定や進捗管理ができないことです。
これは、アニメーションの経過時間を正規化(意味が違ったらすいません)することで解決できますが
それについては、いつか別記事でまとめられたらと思っています。

requestAnimationFrameの参考で上げたサイトに、それについても説明がありますので
ご覧ください。

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

最後に

ここまでお読み頂きありがとうございます。
今回は、Canvas2Dアニメーションの基盤となるであろう技術についてまとめてみました。
まだまだ僕もCanvasにわかなので、説明不足な点も多々あるかと思いますが
少しでもCanvas2Dに興味を持って頂き、可能性を感じて
色んなアニメーションを作ってもらえたらと思います。
(それを見て勉強させてもらいます)
実装例については、詳細の説明を割愛していますので
もし需要と僕の余裕があれば説明記事を書くかもしれません。
ありがとうございました。

Discussion