【ブログ改造計画】WordPressのヘッダアニメーションをp5.jsで実装する【WordPress】

JavaScript
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜〜
マイケル
マイケル
今日は最近作成した、このWordPressブログの
ヘッダーアニメーションの作り方について解説するぜ!
04 タイトルアニメーション2↑ヘッダアニメーションの作成
エレキベア
エレキベア
確か表示する度に形状が変わるようになってるクマね
マイケル
マイケル
その通り!
これはProcessingというデジタルアートのプログラミング言語をJavaScriptに移植した、
p5.jsを使用して作成したんだ!

p5.js | home

エレキベア
エレキベア
確か前ゲーム数学を勉強していた時にも使用していたクマね
マイケル
マイケル
使ったことがあったのと、今回のアニメーションは簡単な図形の組み合わせで実現できそうだったから使ってみたんだ!
さっそく作り方をみていこう!!
エレキベア
エレキベア
楽しみクマ〜〜〜
スポンサーリンク

p5jsの導入と動作確認

マイケル
マイケル
まず前提として、開発環境は下記のようにTypeScript、Stylus環境を構築しているものとして進めていきます。
エレキベア
エレキベア
p5をTypeScriptで書くのは楽しみクマね

型定義のインストール

マイケル
マイケル
まずはp5.jsの型定義を下記コマンドでインストールします。
また、WordPress環境に搭載されているjQueryも使用するため、こちらも合わせてインストールします。
// p5jsの型定義
npm install --save-dev @types/p5
// jqueryの型定義
npm install --save-dev @types/jquery
↑TypeScriptとjQueryの型定義をインストール
マイケル
マイケル
p5.js自体もnpm install出来るのですが、今回はwebpack等も使用しないため
別途ダウンロードして配置することとしました。
エレキベア
エレキベア
あくまで型定義だけ出来るようにしたわけクマね
マイケル
マイケル
インストールしたら下記のように型定義ファイルに定義するのみ!
これにより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を選んでダウンロードしましょう。

home | p5.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を表示してみる
20220509 01↑表示された!
マイケル
マイケル
このように表示されれば導入は成功です!
エレキベア
エレキベア
(ヘッダがぶっ壊れたクマ・・・)

ヘッダー全体に描画する

マイケル
マイケル
次はヘッダー全体にp5のCanvasを表示させてみましょう。
コードを下記のように修正して、ヘッダーのサイズから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();
    });
})
↑読み込みが完了したら表示させる
20220509 02↑ええやん・・・
マイケル
マイケル
するとこのように全体に表示されることが確認できました。
これで土台は完璧です・・・!
エレキベア
エレキベア
これでやりたい放題クマ〜〜〜

アニメーションを実装する

マイケル
マイケル
あとは自由にアニメーションを実装していくのみ!
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();
        }
    }
↑更新・描画処理
01 ビルランダム生成
マイケル
マイケル
ブラウザで確認するとこのようにニョキっと生えてくるはずです!!
エレキベア
エレキベア
これは楽しいクマ〜〜〜

窓のランダム生成

マイケル
マイケル
次に生成したビルの中に窓を配置してみます。
今回は横は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++;
            }
        }
↑窓の描画処理
02 窓ランダム生成
マイケル
マイケル
上記を追加で実装すると、このように窓も表示されることを確認できます。
ランダムで消灯させているのがミソですね!
エレキベア
エレキベア
それっぽくなってきたクマね〜〜
マイケル
マイケル
あとは同様に丸い窓も生成するように実装しました!

パーティクルの表示

マイケル
マイケル
最後に、背景にシャボン玉のようなパーティクルを表示させてみましょう!
下記のように上下をスクロールするように移動させ、大きさが変化するようにすれば完了です!
/**
 * パーティクル(ドット円)クラス
 */
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関数を使用しています!
こちらも定番ですね!
エレキベア
エレキベア
三角関数はマジで使えるクマ〜〜
マイケル
マイケル
あとはこれを適当な数だけ生成すると、
下記のように背景で浮かぶシャボン玉が確認できます!
03 パーティクル↑パーティクルが生成された(ビルのアニメーション開始タイミングも調整している)
エレキベア
エレキベア
レモンスカッシュみたいで美味そうクマ
マイケル
マイケル
これで完成!!
・・・と思いきや、さすがにこの数だけ生成すると、古い端末ではカクカクだったため
数を減らして調整しました・・・。
04 タイトルアニメーション↑調整して完成!
エレキベア
エレキベア
微炭酸になったクマね

リサイズ処理

マイケル
マイケル
リサイズ処理についての補足になるのですが、
リサイズ検知の度に生成しなおすのはうざったいため、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あれば充分ですね。
    // フレームレート設定
    this.p.frameRate(30);
↑フレームレート設定
エレキベア
エレキベア
ゲームなら60欲しいところクマね
マイケル
マイケル
そしてこちらは基本になりますが、動きにはdeltaTimeを使用してフレームレートが落ちた際にも速度は変わらないようにしておきます。
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の方を変えるのは恐らく難しいと思うので、その場合はHTMLのload自体を早くするのがよいと思います。
lazyloadというJavaScriptライブラリを導入すれば画像読み込みを遅延させることができるので、こちらも検討してみてください!
lazyload – GitHub
エレキベア
エレキベア
導入するのも簡単そうクマね

おわりに

マイケル
マイケル
というわけで今回はp5でヘッダアニメーションを作成してみました!
どうだったかな??
エレキベア
エレキベア
こんなお手軽で高機能なライブラリがブラウザ上で使えるなんて驚いたクマ
もっといろいろ作ってみたいクマね
マイケル
マイケル
せっかくだからp5.jsでゲームを作ってみるのも面白そうだなと思ったよ!
また機会があったら触ってみよう!!
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!
エレキベア
エレキベア
クマ〜〜〜

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

コメント