JavaScript
p5.js
クソアプリ
parcel

社会に一石を投じるクソアプリ開発

クソアプリ Advent Calandar 2018 1日目の記事です。

前置き

おはようございます。DE-TEIUです。
今年もクソアプリAdvent Calandarの季節がやってまいりました。
去年に引き続き今年もp5.jsを使ってクソアプリを作成していきます。

去年作ったやつ

成果物

クソアプリAdvent Calandar2018のトップバッターということで、気合を入れて社会に一石を投じるようなクソアプリを作ろうと思い、製作したものが以下のWebアプリです。

社会に一石を投じるアプリ

一石を

投じられたと思います。

解説

Parcelを導入する

今回はParcelというモジュールバンドラを導入してみました。
面倒な設定なしでJavaScriptのコードをビルド(トランスパイル、minifyなど)してくれます。
npmでparcel-bundlerをインストールし、package.jsonを作成すればとりあえず準備完了です。

導入には以下の記事を参考にさせていただきました。

Parcel 入門 ~Parcelはwebpackの代わりになるのか~

p5.jsを導入する

Parcelを導入できたところで、p5.jsをインストールします。
ターミナルで以下のコマンドを実行しましょう。

npm i --save p5

これでp5.js(あとp5.sound.js)がインストールされます。

p5.jsを使ってみる

使える機能は公式のリファレンスを見ると大体わかりますが、Parcelでビルドする場合は実装方法が若干変わります。

とりあえずindex.htmlからapp.jsを参照するものとして、app.jsをこんな風に実装します。

app.js
import "../scss/style.scss";//index.htmlで使うcssを定義
import P5 from "p5";//p5.jsのライブラリを呼び出す
import "p5/lib/addons/p5.sound";//音声ファイルを使うならp5.sound.jsも呼んでおく

const sketch = p => {
    p.preload = function() {
        //初期化処理をここに書く
        //このメソッド内に記述された処理が完了するまで画面(p5.jsで生成したcanvas)の描画処理は行われないので、
        //アプリ実行に必須な外部ファイル(画像とか音声とか)の読み込みをここに書いておくと良いです
    };

    p.setup = function() {
        //初期化処理その2(preloadが完了したら呼ばれます)
        //canvasの初期化処理をここに書いておく
        p.createCanvas(p.windowWidth, p.windowHeight);//キャンバスを画面サイズに合わせる
        p.frameRate(60);//フレームレートの設定
    }

    p.draw = function() {
        //画面描画処理(setupが完了すると呼ばれます)
        //このメソッド内の処理を毎フレーム実行します
    }
};

//上記のsketch内に定義した描画処理を実行
new P5(sketch);

p5.jsの機能を内包したオブジェクト(今回はsketch)を定義してそれを最後にnew P5で実行すれば良い感じですね。
sketchの中でp5.jsの機能を呼び出したい場合はすべて先頭に p. とつけてやれば良いです。

実装の小ネタ

以下、アプリ実装時にハマったところをまとめていきます。

画面クリックイベントの追加

画面をクリックしたときのイベントはp5のmouseClickedやtouchStartedに定義しておきます。
ただし、PC(GoogleChrome)、Android(GoogleChrome)、iOS(Safari)で実行したときにそれぞれ
イベントの発火タイミングが異なるので注意が必要です。
(例えば、mouseClickedだけを使うとSafariで実行したときにイベントが発火しなかったり)

今回はこんな感じに実装しています。

app.js
    function throwStome(){
        //画面をクリック(タップ)したときの投石開始処理を書く
    }
    //ユーザーエージェントに応じて使用するイベントの変更
    if (
        navigator.userAgent.indexOf("iPhone") > 0 ||
        navigator.userAgent.indexOf("iPad") > 0 ||
        navigator.userAgent.indexOf("iPod") > 0
    ) {
        //iOSの場合はtouchStartedイベントを使う
        p.touchStarted = throwStone;
    } else {
        //それ以外の場合はmouseClickedを使う
        p.mouseClicked = throwStone;
    }

Androidの場合もtouchStartedで良いのでは?と思ったのですが、そうすると画面をタップした瞬間と画面から指を離した瞬間で2回イベントが発火してしまったので、このように。

画面描画のコツ

p5.jsには画面(canvas)に何かを描画するメソッドがいくつか(rect,ellipse,image,background ...)ありますが、描画処理を呼ぶ回数が多くなったりするとパフォーマンスに影響が出る事があります。
それを防ぐために、一旦描画したいものを全て別のオブジェクトに出力しておき、最後にそのオブジェクトだけを
canvasに出力する、という手法をとっています。

例えばこんな感じです。

app.js
    //画面描画用バッファ
    let bufferedImage;

    p.setup = function() {
        //キャンバス生成
        p.createCanvas(p.windowWidth, p.windowHeight);
        //キャンバスと同じサイズで画像データ生成
        bufferedImage = p.createGraphics(p.windowWidth, p.windowHeight);
        //以下、bufferedImageに対して画面描画用の設定を入れていく
        bufferedImage.noStroke();
        bufferedImage.ellipseMode(p.CENTER);
        bufferedImage.imageMode(p.CENTER);
        //...など

        p.frameRate(60);
    }

    p.draw = function() {
        //バッファに背景描画(前フレームの描画内容を塗りつぶす)
        bufferedImage.background(200);

        //バッファに矩形を描画
        bufferedImage.rect(30, 20, 55, 55);

        //バッファに円を描画
        bufferedImage.ellipse(56, 46, 55, 55);

        //バッファに画像を描画
        //imgObjectにはloadImageメソッドで読み込んだ画像データが入っているものとする
        bufferedImage.image(imgObject, 0, 0);

        //バッファに出力した描画内容をcanvasに表示
        p.image(bufferedImage, 0, 0);
    }

このように、bufferedImageに描画したいものを全部出力しておいて、drawメソッドの最後に1度だけp.imageメソッドを呼ぶことでcanvasにアクセスする回数が1フレーム1回だけになり、パフォーマンスが若干向上します。

画面サイズ変更イベントの追加

アプリ起動時、ウィンドウサイズに合わせてcanvasを生成していますが、アプリ実行中に画面サイズが変わることもあると思います。
そんな場合はwindowResizedイベントを発火させてcanvasを再構築しましょう。

app.js
    p.windowResized = function() {
        //変更されたウィンドウサイズに合わせてキャンバスのサイズを更新
        p.resizeCanvas(p.windowWidth, p.windowHeight);
        bufferedImage.resizeCanvas(p.windowWidth, p.windowHeight);
    };

ベジェ曲線で投石の軌道算出

3点からなるベジェ曲線の、経過時間に対応する座標情報を算出する式は以下のとおりです。

app.js
    function getPointBezierCurve(t, x1, y1, x2, y2, x3, y3) {
        const px = (1 - t) * (1 - t) * x1 + 2 * (1 - t) * t * x2 + t * t * x3;
        const py = (1 - t) * (1 - t) * y1 + 2 * (1 - t) * t * y2 + t * t * y3;
        return { x: px, y: py };
    }

参考:3次ベジェ曲線を使った要素の曲線移動(アニメーション) | jQuery逆引き | Webサイト制作支援 | ShanaBrian Website

ツイートボタン生成

ツイートボタンを設置するだけであれば、以下のツイートボタン生成サイトで生成したコードをHTMLに書いておくだけでよいです。

Twitter Publish

今回のようにツイートメッセージを動的に変更したい場合、一工夫が必要です。

index.html
    <div id="tweetBtn"></div>
    <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
app.js
    function createClearMessage() {
        const clearMessage = "あなたが社会に" + stoneCount + "石を投じた結果、社会は" +
            clearSecond + "秒後に倒れました。";

        //ツイートボタンのDOM生成
        document.getElementById("tweetBtn").innerHTML =
            '<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-size="large"data-text="' +
            clearMessage + '" data-hashtags="社会に一石を投じるアプリ">Tweet</a>';

        //ツイートボタン再構築
        twttr.widgets.load();
    }

ややゴリ押しな方法っぽい気がしますが、要するにツイートボタンのDOMを書き換えてtwttr.widgets.load();してやるとツイートボタンが再構築されます。

まとめ

何か新しい技術を勉強するモチベーションを保つ方法として、クソアプリ開発はおすすめです。しかも楽しい。