TypeScript 粒子アニメーション

粒子のアニメーションを作る。

particle.jsとかは使わずに自力で実装する。

こちらの記事を参考にしまくる。

www.otwo.jp

参考にした記事はjsで書いているが、

僕はreactで開発していたので、tsxに書き直した。

import "./Article.css"
import React, { useEffect, useRef } from 'react';

const Article: React.FC = () => {
    const canvasRef = useRef<HTMLCanvasElement>(null);

    useEffect(() => {
        const canvas = canvasRef.current;
        if (!canvas) return;
        console.log(canvas);

        const ctx = canvas.getContext('2d');
        if (!ctx) return;

        let intParticle = 0;
        let aryParticle: any[] = [];
        let canvasSize: { width: number; height: number } = { width: 0, height: 0 };

        function init() {
            calc();
            draw();

        }

        window.onresize = function () {
            calc();
        };

        function calc() {
            // Canvas要素が初期化される前にCanvasに関する処理を実行しないようにする
            if (!canvas) return;

            canvasSize.width = canvas.width = document.body.clientWidth;
            canvasSize.height = canvas.height = document.body.clientHeight;
            intParticle = Math.floor((canvas.width / 300 * 10) + (canvas.height / 300 * 10));
            if (aryParticle.length < intParticle) {
                create(aryParticle.length);
            }
        }

        function create(start: number) {
            // Canvas要素が初期化される前にCanvasに関する処理を実行しないようにする
            if (!canvas) return;

            for (let i = start; i < intParticle; i++) {
                aryParticle.push({
                    position: {
                        x: random(0, canvas.width),
                        y: random(0, canvas.height)
                    },
                    direction: {
                        x: random(0.3, 2, true) * ((random(0, 1) ? -1 : 1)),
                        y: random(0.3, 2, true) * ((random(0, 1) ? -1 : 1))
                    },
                    circle: 2
                });
            }
        }

        function random(min: number, max: number, deci = false) {
            let result = Math.random() * (max + 1 - min) + min;
            return (deci) ? result : Math.floor(result);
        }

        function draw() {
            // Canvas要素が初期化される前にCanvasに関する処理を実行しないようにする
            if (!canvas) return;
            // Canvas要素が初期化される前にCanvasに関する処理を実行しないようにする
            if (!ctx) return;

            ctx.clearRect(0, 0, canvas.width, canvas.height);
            for (let i = 0; i < intParticle; i++) {
                let _p = aryParticle[i];
                ctx.beginPath();
                ctx.fillStyle = "#ffffff";
                ctx.arc(_p.position.x, _p.position.y, _p.circle, 0, 2 * Math.PI);
                ctx.fill();

                aryParticle[i].position.x += _p.direction.x;
                aryParticle[i].position.y += _p.direction.y;

                if (_p.position.x < 0 || _p.position.x > canvas.width) {
                    aryParticle[i].direction.x *= -1;
                }
                if (_p.position.y < 0 || _p.position.y > canvas.height) {
                    aryParticle[i].direction.y *= -1;
                }

                if (_p.position.x < -_p.circle) aryParticle[i].position.x = _p.circle;
                if (_p.position.x > canvas.width + _p.circle) aryParticle[i].position.x = canvas.width - (_p.circle * 2);
                if (_p.position.y < -_p.circle) aryParticle[i].position.y = _p.circle;
                if (_p.position.y > canvas.height + _p.circle) aryParticle[i].position.y = canvas.height - (_p.circle * 2);

                for (let _i = 0; _i < intParticle; _i++) {
                    let _n = aryParticle[_i];
                    if (i != _i) {
                        let _dist = Math.abs(_p.position.x - _n.position.x) + Math.abs(_p.position.y - _n.position.y);
                        if (_dist < 200) {
                            ctx.beginPath();
                            ctx.globalAlpha = ((_dist) > (100) ? 1 : 1 - (((100) - (_dist)) / (100)));
                            ctx.strokeStyle = "#ffffff";
                            ctx.moveTo(_p.position.x, _p.position.y);
                            ctx.lineTo(_n.position.x, _n.position.y);
                            ctx.stroke();
                        }
                    }
                }
            }
            requestAnimationFrame(draw);
        }

        init();

        return () => {
            window.onresize = null;
        };
    }, []);

    return (
        <div>
            <p>article</p>
            <canvas id="canvas" ref={canvasRef}></canvas>
        </div>);
};

export default Article;

これを実行します。

このときArticle.cssの#canvasの背景をちゃんと設定しないと、 粒子と色が被ってなにも見えなくなります。

はじめcanvasの背景色が白だったので、なにもみえず、うんこ漏らしました。 デバッグで粒子の描画がちゃんと行われていること、 ディベロッパーツールでcanvasの設定が正しいことを確認し、 canvasの背景か!って閃きました。

結果

解説

const canvas = canvasRef.current;
      
const ctx = canvas.getContext('2d');

useRefでhtml要素を参照する

canvas要素はgetContextメソッドをもっており、これによって描画を行うためのオブジェクトを返す。

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

指定した短形ないのピクセルを消去します。座標と幅、高さが引数です。

    ctx.beginPath();
                ctx.fillStyle = "#ffffff";
                ctx.arc(_p.position.x, _p.position.y, _p.circle, 0, 2 * Math.PI);
                ctx.fill();

beginPathは描画のためのパスを初期化します。 これをしないと、fillごとにパスを変える、 つまり色を変えることができなくなるみたいです。

ctx.fillStyle = "#ffffff";: fillStyleプロパティは、描画される図形の塗りつぶしの色を指定します。 白色(#ffffff)ですね。

ctx.arc(p.position.x, p.position.y, _p.circle, 0, 2 * Math.PI);: arcメソッドは、円を描画するためのメソッドです。

引数には中心のx座標、中心のy座標、半径、開始角度、終了角度が指定されます。

ここでは、p.position.xとp.position.yで円の中心座標が指定されており、_p.circleで円の半径が指定されています。

また、0から2πまでの角度を指定していますので、円の全体が描画されます。

ctx.fill();: fillメソッドは、描画された図形を塗りつぶします。

                   ctx.globalAlpha = ((_dist) > (100) ? 1 : 1 - (((100) - (_dist)) / (100)));
                            ctx.strokeStyle = "#ffffff";
                            ctx.moveTo(_p.position.x, _p.position.y);
                            ctx.lineTo(_n.position.x, _n.position.y);
                            ctx.stroke();

globalAlphaプロパティは、描画される図形の不透明度を指定します。 この値は0(完全に透明)から1(完全に不透明)の範囲で指定されます。 ここでは、2つのパーティクルの距離(_dist)が100未満の場合は1(完全に不透明)、それ以外の場合は1から距離に応じて不透明度が減少します。

strokeStyleプロパティは、描画される線の色を指定します。

moveToメソッドは、現在の描画位置を指定した座標に移動します。

lineToメソッドは、現在の描画位置から指定した座標までの直線を描画します。

ctx.stroke();: strokeメソッドは、描画された線を実際に描画します。

requestAnimationFrame(draw);

requestAnimationFrameは、ブラウザにアニメーションを描画するための適切なタイミングで指定した関数を呼び出すためのメソッドです。 draw関数の中で、drawを呼び出しているわけやんな。

window.onresize = function () {
            calc();
        };

window.onresizeはウィンドウが変わったときのイベント。それを定義している。

振り返り

はじめてのjsアニメーションやったけど、意外と簡単だった。 cssのアニメーションとか、ライブラリを使ったアニメーションとかもやってみたい。