
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
こんにちクマ〜〜〜

マイケル
今日は最近作成した、このWordPressブログの
ヘッダーアニメーションの作り方について解説するぜ!
ヘッダーアニメーションの作り方について解説するぜ!


エレキベア
確か表示する度に形状が変わるようになってるクマね

マイケル
その通り!
これはProcessingというデジタルアートのプログラミング言語をJavaScriptに移植した、
p5.jsを使用して作成したんだ!
これはProcessingというデジタルアートのプログラミング言語をJavaScriptに移植した、
p5.jsを使用して作成したんだ!

エレキベア
確か前ゲーム数学を勉強していた時にも使用していたクマね

マイケル
使ったことがあったのと、今回のアニメーションは簡単な図形の組み合わせで実現できそうだったから使ってみたんだ!
さっそく作り方をみていこう!!
さっそく作り方をみていこう!!

エレキベア
楽しみクマ〜〜〜
p5jsの導入と動作確認

マイケル
まず前提として、開発環境は下記のようにTypeScript、Stylus環境を構築しているものとして進めていきます。

エレキベア
p5をTypeScriptで書くのは楽しみクマね
型定義のインストール

マイケル
まずはp5.jsの型定義を下記コマンドでインストールします。
また、WordPress環境に搭載されているjQueryも使用するため、こちらも合わせてインストールします。
また、WordPress環境に搭載されているjQueryも使用するため、こちらも合わせてインストールします。
// p5jsの型定義
npm install --save-dev @types/p5
// jqueryの型定義
npm install --save-dev @types/jquery
↑TypeScriptとjQueryの型定義をインストール
マイケル
p5.js自体もnpm install出来るのですが、今回はwebpack等も使用しないため
別途ダウンロードして配置することとしました。
別途ダウンロードして配置することとしました。

エレキベア
あくまで型定義だけ出来るようにしたわけクマね

マイケル
インストールしたら下記のように型定義ファイルに定義するのみ!
これによりtsファイル内で扱える様になります。
これによりtsファイル内で扱える様になります。
/**
* p5js型定義
*/
import module = require('p5');
export = module;
export as namespace p5;
declare global {
interface Window {
p5: typeof module,
}
}
/**
* jQuery型定義
*/
import jqModule = require('jquery')
export = jqModule;
export as namespace jquery;
declare global {
interface Window {
jquery: typeof jqModule,
}
}
↑型を定義する
エレキベア
導入は楽勝クマ〜〜〜
p5.min.jsの配置と読み込み

マイケル
次にp5自身をダウンロードしてブログの任意の場所に配置します。
下記の公式サイトからp5.min.jsを選んでダウンロードしましょう。
下記の公式サイトからp5.min.jsを選んでダウンロードしましょう。

エレキベア
これが本体クマか・・・

マイケル
配置したらPHPファイル内で下記のように読み込みます。
// JavaScript追加
add_action('wp_enqueue_scripts', function() {
wp_enqueue_script('eleki-custom-p5', get_stylesheet_directory_uri() . '/lib/js/p5.min.js');
wp_enqueue_script('eleki-custom-header', get_stylesheet_directory_uri() . '/src/js/header.js'); // こちらは自身で作成したjsファイル
});
↑jsファイルの読み込み
マイケル
これでp5の導入は完了です!

エレキベア
早く使いたいクマ〜〜〜
ヘッダーに挿入する

マイケル
導入が完了したら、簡単に動作確認してみましょう!
下記のようにコードを書いてブラウザで見てみます。
下記のようにコードを書いてブラウザで見てみます。
window.addEventListener("DOMContentLoaded", function(e) {
// ヘッダー用のsketchを作成
const sketch = (p: p5) => {
p.setup = () => {
p.createCanvas(400, 400);
};
p.draw = () => {
p.background(220);
p.ellipse(50, 50, 80, 80);
}
};
// ヘッダーに挿入
let element: HTMLElement | null = document.querySelector("#header");
if (element != null) {
new p5(sketch, element);
}
});
↑とりあえずCanvasを表示してみる

マイケル
このように表示されれば導入は成功です!

エレキベア
(ヘッダがぶっ壊れたクマ・・・)
ヘッダー全体に描画する

マイケル
次はヘッダー全体にp5のCanvasを表示させてみましょう。
コードを下記のように修正して、ヘッダーのサイズからCanvasを生成し、
style指定をサイズ100%、object-fitをcover(領域全体表示)にします!
コードを下記のように修正して、ヘッダーのサイズからCanvasを生成し、
style指定をサイズ100%、object-fitをcover(領域全体表示)にします!
/**
* ヘッダークラス
*/
class Header {
private static readonly ELEM_HEADER = "#header-in";
private static width: number;
private static height: number;
/**
* ヘッダー表示処理
*/
public static showHeader(): void {
// sketch作成
const sketch = (p: p5) => {
p.setup = () => {
this.StartSketch(p);
};
p.draw = () => {
this.UpdateSketch(p);
};
p.windowResized = () => {
this.ResizedSketch(p);
};
};
// ヘッダーに挿入
let element: HTMLElement | null = document.querySelector(Header.ELEM_HEADER);
if (element != null) {
new p5(sketch, element);
}
}
/**
* 開始処理
*/
private static StartSketch(p: p5) {
// Canvasサイズを取得
this.width = $(this.ELEM_HEADER).width();
this.height = $(this.ELEM_HEADER).height();
// Canvas設定
let canvas = p.createCanvas(this.width, this.height);
canvas.style('position', 'absolute');
canvas.style('width', '100%');
canvas.style('height', '100%');
canvas.style('object-fit', 'cover');
// 初期表示
this.InitDrawCanvas(p);
}
/**
* 更新処理
*/
private static UpdateSketch(p: p5) {
// TODO 適当な図形を描画
p.ellipse(50, 50, 80, 80);
}
/**
* リサイズ処理
*/
private static ResizedSketch(p: p5) {
// サイズを再設定
this.width = $(Header.ELEM_HEADER).width();
this.height = $(Header.ELEM_HEADER).height();
p.resizeCanvas(this.width, this.height);
// 再描画
this.InitDrawCanvas(p);
}
/**
* 初期描画
*/
private static InitDrawCanvas(p: p5) {
// 黄色背景
p.background(255, 245, 0);
// 黒背景
p.fill(34, 34, 34);
p.rect(0, this.height / 2, this.width, this.height / 2);
}
}
↑ヘッダー全体にCanvasを表示させるjQuery(function() {
// 読み込み完了時
window.addEventListener("DOMContentLoaded", function(e) {
Header.showHeader();
});
})
↑読み込みが完了したら表示させる

マイケル
するとこのように全体に表示されることが確認できました。
これで土台は完璧です・・・!
これで土台は完璧です・・・!

エレキベア
これでやりたい放題クマ〜〜〜
アニメーションを実装する

マイケル
あとは自由にアニメーションを実装していくのみ!
p5.jsは多くのゲームエンジンと同じく開始処理、更新処理が用意されているので、
普段ゲーム開発を行っている皆さんなら簡単に作れると思います!
p5.jsは多くのゲームエンジンと同じく開始処理、更新処理が用意されているので、
普段ゲーム開発を行っている皆さんなら簡単に作れると思います!

エレキベア
ゲームプログラミング感覚で出来るクマね

マイケル
コードは全部載せきれないので、作った順序に沿って
要所要所を載せていこうかと思います。
要所要所を載せていこうかと思います。
ビルのランダム生成

マイケル
まずはビルを生成してみましょう。
今回は高さをランダムで決めてニョキっと生えるアニメーションにしたいので、
目的の高さをコンストラクタで受け取り、更新処理内で目的の高さまで伸ばすようにしてみます。
今回は高さをランダムで決めてニョキっと生えるアニメーションにしたいので、
目的の高さをコンストラクタで受け取り、更新処理内で目的の高さまで伸ばすようにしてみます。
/**
* ビルクラス
*/
class Bill {
private p: p5;
private readonly width: number; // 横幅
private readonly targetHeight: number; // 目的の高さ
private readonly posX: number; // 出現させるX位置
private height: number; // 高さ
private addHeight: number; // フレーム毎に加える高さ
private totalTime: number; // 更新中の累計時間
constructor(p: p5, width: number, targetHeight: number, posX: number) {
this.p = p;
this.width = width;
this.targetHeight = targetHeight;
this.posX = posX;
}
public Start() {
// ビルの情報を設定
this.height = 0;
this.addHeight = 0.6 * (this.targetHeight/100);
this.totalTime = 0;
}
public Update() {
// 目的の高さになるまで加える
if (!this.isTargetHeight()) {
// 1秒間に120px伸びるとしてイージングを設定
let t = MathUtil.easeOutQuad((this.totalTime/1000)/(this.targetHeight/120));
this.height += this.addHeight * this.p.deltaTime * t;
this.totalTime += this.p.deltaTime;
}
}
public Draw() {
// ビル描画
this.p.fill(34, 34, 34);
this.p.rect(
this.posX - (this.width / 2), // X: 原点が左上のため、指定位置から横幅の半分を引いた位置にする
Header.height - this.height, // Y: 下から伸びるように見せるため、ヘッダー高さから縦幅を引いた位置にする
this.width,
this.height);
}
// 目的の高さに到達したか?
public isTargetHeight(): boolean {
return this.height > (this.targetHeight - 1.0);
}
}
↑ビルクラスの作成
エレキベア
シンプルな構成になっているクマね

マイケル
伸ばす動きには勢いを付けたいのでイージング関数を使用しています。
こちらも簡単に実装できるので試してみてください!
こちらも簡単に実装できるので試してみてください!
// イーズイン
public static easeInQuad(t: number) {
return t * t;
}
// イーズアウト
public static easeOutQuad(t: number) {
return t * (2 - t);
}
public static easeOutQuart(t: number): number {
return 1 - Math.pow(1 - t, 4);
}
↑イージング関数の実装
エレキベア
アニメーションの鉄板クマね

マイケル
あとはこのビルクラスをランダムな高さを与える様に生成して
更新処理内で更新、描画するようにすれば完了です!
更新処理内で更新、描画するようにすれば完了です!
/**
* ビル生成処理
*/
private static GenerateBills() {
// 初期化フラグをOFF
this.isInit = false;
// ビル配列初期化
this.bills = [];
// ビルのランダム値を設定
let billWidth: number = Math.max(80, Header.width / 10); // ビルの幅:ヘッダ幅/10(最小80px)
let billSpace: number = billWidth*0.8; // ビルの間隔:幅*0.8で重ねる
let billCount: number = Header.width / billSpace + 1; // ビルの数:間隔で割った数値+1
for (let i = 0; i < billCount; i++) {
// ビル生成
let bill: Bill = new Bill(
this.p,
billWidth * MathUtil.getRandom(0.8, 1.2), // 横幅:若干ブレを持たせる
Header.height * MathUtil.getRandom(0.35, 0.7), // 高さ:ヘッダ高さ*0.35〜0.7
i * billSpace);
// 開始処理
bill.Start();
this.bills.push(bill);
}
// ランダムにシャッフルする
this.bills.sort(()=> Math.random() - 0.5);
// 初期化フラグをON
this.isInit = true;
}
↑ランダムに生成する /**
* 更新処理
*/
private static UpdateSketch() {
// 初期化していなければ処理を行わない
if (!this.isInit || this.bills == null) {
return;
}
// 更新処理
for (let i = 0; i < this.bills.length; i++) {
this.bills[i].Update();
}
// 描画処理
this.p.background(255, 245, 0);
for (let i = 0; i < this.bills.length; i++) {
this.bills[i].Draw();
}
}
↑更新・描画処理

マイケル
ブラウザで確認するとこのようにニョキっと生えてくるはずです!!

エレキベア
これは楽しいクマ〜〜〜
窓のランダム生成

マイケル
次に生成したビルの中に窓を配置してみます。
今回は横は3列固定として、高さに入る分だけ配置してみました!
今回は横は3列固定として、高さに入る分だけ配置してみました!
// 窓の情報を設定
this.windowWidth = this.width/3 - this.width/4; // X方向に3つ設定するとしていい感じに調整
this.windowHeight = MathUtil.getRandom(30, 50); // 決め内で高さは決める
this.windowHeightSpace = MathUtil.getRandom(10, 20); // 高さの間隔
this.windowHeightTopOffset = this.targetHeight/10; // オフセット
this.windowCountX = 3; // X方向の数は3固定
this.windowCountY = this.targetHeight / (this.windowHeight+this.windowHeightSpace) + 1; // Y方向は入り切る数分
// 隠す窓のindexをランダムで決める
this.windowHideArray = [];
for (let i = 0; i < this.windowCountX*this.windowCountY; i++) {
if (MathUtil.getRandom(0, 10) < 3) { // とりあえず3割
this.windowHideArray.push(i);
}
}
↑ビルクラスの中で窓情報も設定 // 窓リスト描画
this.p.fill(242, 242, 242);
let index = 0;
for (let x = 0; x < this.windowCountX; x++) {
for (let y = 0; y < this.windowCountY; y++) {
// 非表示に設定されていない場合
if (this.windowHideArray.indexOf(index) === -1) {
// 窓を描画
this.p.rect(
this.posX + (x-1)*this.width/this.windowCountX - (this.windowWidth/2), // X: (-1,0,1)に横幅をかけて窓の半分の幅を引く
(Header.height - this.height) + (this.windowHeightTopOffset) + y*(this.windowHeight+this.windowHeightSpace), // Y: ビルの位置+オフセット+個数分
this.windowWidth,
this.windowHeight
)
}
index++;
}
}
↑窓の描画処理

マイケル
上記を追加で実装すると、このように窓も表示されることを確認できます。
ランダムで消灯させているのがミソですね!
ランダムで消灯させているのがミソですね!

エレキベア
それっぽくなってきたクマね〜〜

マイケル
あとは同様に丸い窓も生成するように実装しました!
パーティクルの表示

マイケル
最後に、背景にシャボン玉のようなパーティクルを表示させてみましょう!
下記のように上下をスクロールするように移動させ、大きさが変化するようにすれば完了です!
下記のように上下をスクロールするように移動させ、大きさが変化するようにすれば完了です!
/**
* パーティクル(ドット円)クラス
*/
class Particle {
private p: p5;
private x: number;
private y: number;
private r: number;
private a: number;
private initX: number;
private upSpeed: number;
private shakeSpeed: number;
private shake: number;
private scale: number;
private frameCount: number;
constructor(p: p5) {
this.p = p;
}
public Start() {
// ランダムで位置を設定
this.x = MathUtil.getRandom(0, Header.width);
this.y = MathUtil.getRandom(0, Header.height);
this.r = MathUtil.getRandom(12, 25);
this.a = 0;
this.initX = this.x;
this.upSpeed = MathUtil.getRandom(0.03, 0.09); // 1ミリ秒ごとの移動距離
this.shakeSpeed = 0.003;
this.shake = MathUtil.getRandom(0, 10);
this.frameCount = Math.random();
}
public Update() {
this.frameCount += this.shakeSpeed * this.p.deltaTime;
this.x = this.initX + this.shake * Math.sin(this.frameCount);
// 上方向に移動させる
this.y -= this.upSpeed * this.p.deltaTime;
if (this.y < - this.r / 2) {
this.y = Header.height;
}
// 徐々に大きくする
this.scale = 1 - MathUtil.easeOutQuad(Math.max(0, this.y / (Header.height+100)));
// フェードイン
this.a = Math.min(255, this.a+50);
}
public Draw() {
this.p.stroke(34, 34, 34, this.a);
this.p.noFill()
this.p.circle(this.x, this.y, this.r*this.scale);
}
}
↑パーティクルクラスの作成
マイケル
横に揺らすのはsin関数を使用しています!
こちらも定番ですね!
こちらも定番ですね!

エレキベア
三角関数はマジで使えるクマ〜〜

マイケル
あとはこれを適当な数だけ生成すると、
下記のように背景で浮かぶシャボン玉が確認できます!
下記のように背景で浮かぶシャボン玉が確認できます!


エレキベア
レモンスカッシュみたいで美味そうクマ

マイケル
これで完成!!
・・・と思いきや、さすがにこの数だけ生成すると、古い端末ではカクカクだったため
数を減らして調整しました・・・。
・・・と思いきや、さすがにこの数だけ生成すると、古い端末ではカクカクだったため
数を減らして調整しました・・・。


エレキベア
微炭酸になったクマね
リサイズ処理

マイケル
リサイズ処理についての補足になるのですが、
リサイズ検知の度に生成しなおすのはうざったいため、timerを使用して一定間隔で処理するのが現実的です。
リサイズ検知の度に生成しなおすのはうざったいため、timerを使用して一定間隔で処理するのが現実的です。
/**
* リサイズ処理
*/
private static ResizedSketch() {
// アニメーション停止フラグが設定されていたら処理を行わない
if (this.isStopAnimation) {
return;
}
// 100px以上変更していない場合には処理を行わない
if (Math.abs(this.prevWidth - $(Header.ELEM_HEADER).width()) < 100) {
return;
}
// timerを使用して一定秒数ごとに処理させる
if (this.resizeTimer > 0) {
window.clearTimeout(this.resizeTimer);
}
this.resizeTimer = window.setTimeout(() => {
// サイズを再設定
Header.width = $(Header.ELEM_HEADER).width();
Header.height = $(Header.ELEM_HEADER).height();
this.p.resizeCanvas(Header.width, Header.height);
// 横幅を保持
this.prevWidth = $(Header.ELEM_HEADER).width();
// ビルを再生成
this.InitBills();
}, 500)
}
↑timerを使用して一定間隔で処理を行う
マイケル
また、iOSでは何故かスクロールする度にリサイズ検知されてしまうという不具合が発生したため、横幅が一定の幅以上変更された場合にのみ行うよう対処してあります・・・。

エレキベア
カナシマシマシクマ・・・。
処理負荷対策

マイケル
最後におまけとして、処理負荷対策についても紹介しておきます。

エレキベア
負荷はある程度はありそうクマからね
フレームレートの設定とdeltaTimeの使用

マイケル
p5.jsではデフォルトで60FPSに設定されているため、
調整しておくことをおすすめします。アニメーションであれば30FPSあれば充分ですね。
調整しておくことをおすすめします。アニメーションであれば30FPSあれば充分ですね。
// フレームレート設定
this.p.frameRate(30);
↑フレームレート設定
エレキベア
ゲームなら60欲しいところクマね

マイケル
そしてこちらは基本になりますが、動きにはdeltaTimeを使用してフレームレートが落ちた際にも速度は変わらないようにしておきます。
P5.jsに用意されていますが、単位がmsになっていたためそこは注意が必要です。
P5.jsに用意されていますが、単位がmsになっていたためそこは注意が必要です。
// 上方向に移動させる
this.y -= this.upSpeed * this.p.deltaTime;
↑deltaTimeを使用した移動
エレキベア
ゲーム開発の定番クマね
画面に映っていない時は停止する

マイケル
そしてもう一点、画面に映っていない間はアニメーションを停止
させるようにしました。
こちらはスクロール範囲にターゲット(ヘッダー)が入っているかどうかでフラグを設定するようにしています。
させるようにしました。
こちらはスクロール範囲にターゲット(ヘッダー)が入っているかどうかでフラグを設定するようにしています。
// スクロール検知処理
$(window).on('load scroll', function() {
// ヘッダーが要素から外れた時にアニメーションを停止させる
ScrollUtil.addCallbackWhenVisible(
'#header',
() => Header.setIsStopAnimation(false),
() => Header.setIsStopAnimation(true));
});
↑スクロール検知処理 /**
* スクロール関連
*/
export class ScrollUtil {
/**
* 要素が画面に表示された時の処理を設定する
* @param targetName 対象の要素名
* @param visibleCallback 画面に表示されている時の処理
* @param hideCallback 画面から表示されない時の処理
*/
public static addCallbackWhenVisible(targetName: string, visibleCallback: () => void, hideCallback: () => void) {
// スクロール範囲の取得
let scrollTop = $(window).scrollTop();
let scrollBottom = scrollTop + $(window).height();
// ターゲット範囲の取得
let targetTop = $(targetName).offset().top;
let targetBottom = targetTop + $(targetName).height();
// ターゲットが画面内に入っているか?
if (scrollBottom > targetTop && scrollTop < targetBottom) {
visibleCallback();
} else {
hideCallback();
}
}
}
↑ターゲットがスクロール範囲に入っているか
エレキベア
なるほどクマ
確かに記事読んでいる間もずっと動いていたら大迷惑クマ
確かに記事読んでいる間もずっと動いていたら大迷惑クマ

マイケル
対策は今のところこれくらいしかしていないけど、
もし全然動作しない!みたいな声が出てきた時には、「フレームレートが一定以下だったら画像に差し替える」ような対策も検討しようかと思っています!
もし全然動作しない!みたいな声が出てきた時には、「フレームレートが一定以下だったら画像に差し替える」ような対策も検討しようかと思っています!

エレキベア
問題ないことを祈るクマ・・・
p5jsの表示タイミングが遅い

マイケル
処理負荷対策とは少し話がずれるかもしれませんが、DOMContentLoadedで読みこんでいるのに表示タイミングが遅いような・・・と感じる場面が出てくると思います。
それはp5.jsのsetup関数がHTMLのload後に呼ばれるためです。
それはp5.jsのsetup関数がHTMLのload後に呼ばれるためです。

エレキベア
なんと、そうだったのクマね・・・

マイケル
p5.jsの方を変えるのは恐らく難しいと思うので、その場合はHTMLのload自体を早くするのがよいと思います。
lazyloadというJavaScriptライブラリを導入すれば画像読み込みを遅延させることができるので、こちらも検討してみてください!
lazyloadというJavaScriptライブラリを導入すれば画像読み込みを遅延させることができるので、こちらも検討してみてください!

エレキベア
導入するのも簡単そうクマね
おわりに

マイケル
というわけで今回はp5でヘッダアニメーションを作成してみました!
どうだったかな??
どうだったかな??

エレキベア
こんなお手軽で高機能なライブラリがブラウザ上で使えるなんて驚いたクマ
もっといろいろ作ってみたいクマね
もっといろいろ作ってみたいクマね

マイケル
せっかくだからp5.jsでゲームを作ってみるのも面白そうだなと思ったよ!
また機会があったら触ってみよう!!
また機会があったら触ってみよう!!

マイケル
それでは今日はこの辺で!
アデュー!!
アデュー!!

エレキベア
クマ〜〜〜
【ブログ改造計画】WordPressのヘッダアニメーションをp5.jsで実装する【WordPress】〜完〜
コメント