WGGの活動log

ゲームクリエイターやらエンジニアを目指してる大学生が,技術的なことを書いたり遊んだゲームの感想を書いたりするブログです.ちなみにWGGは「ワグ」と読みます.

p5.jsとスマートフォンでインタラクティブアートを作る

この記事はProcessing AdventCalendar 2018 の25日目です
昨日の記事は t_ritoco さんの【Processing】試しに名刺を作ってみる【コンビニプリント】でした

はじめに

皆さん,Processingやp5.jsをどういった用途で使っていますか?
ジェネラティブアート (Generative Art) や,インタラクティブアート (Interative Art) なんかを作るために使ってる人が多いかと思います.
実行結果がすぐに,絵で見られることからプログラミング初学者の学習用に使っている場合もあるかもしれません.
この記事は,主にインタラクティブアートを作る人,作ってみたい人向けの記事です.
ですが,p5を使用してゲーム開発をしてみたい方にも需要があるかもしれません.

目次

  • 今回作ったもの
  • モバイル端末でインタラクティブアートを作る
  • Part1. 準備
  • Part2. スマートフォンで実行したときの問題点と解決法
  • Part3. ジャイロセンサの使用
  • Part4. マルチタップ
  • Part5. 実践
  • 終わりに

今回作ったもの

http://wggsh.github.io/games/ProcessingAdventCalendar2018/05/
こちらのURLにスマートフォンでアクセスしてみてください(機種によってはカクつくかもしれません)
スマートフォンを適当に傾けると映っているオブジェクトの形状が変わります.
www.youtube.com

モバイル端末でインタラクティブアートを作る

インタラクティブアートを作る場合,入力機器が必要になります.PCだけで行う場合,せいぜいマウスとキーボード,カメラぐらいしか用意できないのですが,(おそらく殆どの方が持っているであろう)スマートフォンには,加速度ジャイロ (角速度) GPSなど様々なセンサが搭載されており,これを利用しないのはとても勿体ないです.また,スマートフォンで実行できると,作ったものを公開して各々の持っているスマホで遊んでもらう,といった体験方法が可能になります.p5.jsを使用してWebアプリとして公開することで,ストアを経由する必要もありません (自分の場合これがメインの理由)

そこで今回は,スマートフォン (モバイル端末) でインタラクティブアートを作るにあたってのp5.jsやその他必要な知識についての話を書いていきます.
自分の開発環境は iPhoneX (iOS12.1)です.iOSのバージョン違いや,Androidでは挙動が変わる場合があるかもしれません.

今回作成したものは全て,
Processing AdventCalendar 2018
に公開しています.

またソースコード
https://github.com/WGGSH/ProcessingAdventCalendar2018
に置いています.
github.com

Part1. 準備

まずは最も基本的なp5.jsのプログラムを作成します.
今回の開発では利便性を考慮してTypeScriptを使用していますが,TypeScriptを書いたことがない方は,生成されたjsのプログラムの方も確認してみてください.
p5の型定義ファイルは公式からは提供されていないはずなので,こちらを使用すると良いと思います

まずは以下のプログラムを作成しました.
マウスでクリックした箇所に円が表示されるだけのプログラムです.

<!-- index.html -->


<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Part 1</title>
    <script src="js/p5.min.js"></script>
    <script src="js/main.js"></script>
</head>
<body>
</body>
</html>
// main.ts

/// <reference path="p5.global-mode.d.ts" />

function setup(): void {
  createCanvas(windowWidth, windowHeight);
}

function draw(): void {
  background(0);

  if (mouseIsPressed) {
    fill(255);
    ellipse(mouseX, mouseY, 50, 50);
  }
}

Part2. スマートフォンで実行したときの問題点と解決法

PCのブラウザ上で実行する場合,これでも特に大きな問題はおこりません.しかし,スマートフォンで実行するといくつか問題点が発生します.

  1. 左上に余白ができる
  2. スワイプすると画面がスクロールされる
  3. URLバー等が邪魔
  4. 画面の向きを帰ると表示が崩れる
2-1 左上の余白

f:id:wgg00sh:20181217141547p:plain
キャンバスの大きさを windowWidth / windowHeight に設定したはずなのに,左上に白い部分ができています.
この問題は,cssを適用することで解決できます.また,viewportの指定を行うことで,拡大などにも対応します.

<!-- index.html -->
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1,viewport-fit=cover" />
  <link rel="stylesheet" href="./style.css">
// style.css

*{
  padding: 0;
  margin: 0;
  background: black;
}

canvas{
  display:block;
}
2-2 スワイプ時にスクロールされる問題

f:id:wgg00sh:20181217135230p:plain
Part1. で作成したプログラムは,PCの場合マウスのドラッグで円を動かせますが,スマートフォンの場合,スワイプ操作になり,画面のスクロールが発生します.これを解消するには,Webページ自体にスワイプ時に発生するイベントを無効化する必要があります.
javascriptのプログラムで以下を追記します.

// main.ts

function setup() : void {

  window.addEventListener("touchstart", function (event) { event.preventDefault(); }, { passive: false });
  window.addEventListener("touchmove", function (event) { event.preventDefault(); }, { passive: false });

  createCanvas(windowWidth, windowHeight);
}

今回は setup() の中に書きましたが,p5のプログラムと関連のない部分なので,htmlに直接記述しても良いかもしれません

2-3 URLバーの非表示

スマートフォンでアプリのように実行する場合,そもそもURLバーが邪魔です.ストアからダウンロードしたアプリには存在しないので.
そこで,ブラウザでもURLバーを表示せずに実行できるWebアプリモードを利用します.この手法はユーザーに少しの操作を要求するので,常に有効というわけではないかもしれません.

また,今回iPhoneでしか動作確認を行っていないので,Android端末でこれを行う方法は知らないです.

まずはhtmlファイルを編集します.タグに以下を記述します.

<!-- index.html -->

<head>
  <meta name="apple-mobile-web-app-capable" content="yes">
</head>

次に,Webページを開いて,「ホーム画面に追加」を選択します.
f:id:wgg00sh:20181217144950p:plain

ホーム画面にアイコンが作成されるので,それをタップして開きます.

f:id:wgg00sh:20181217145042p:plain

このように,URLバーや下のメニューバー(?) が無くなった状態でWebページを開けます.

2-4 画面の向きを変えると表示が崩れる

キャンバスのサイズは createCanvas() 関数で指定した大きさになるので,その後に画面の向きを変更すると,縦横の比率が合わなくなります.この問題を解決するためには,画面の向きが変更されたことを検出してキャンバスを再生成するのが良さそうです.

p5.jsの組み込み変数 deviceOrientation の値の変化を見ることで,画面の向きが変わったかを調べることができます.この種の変数は基本的に前フレームの値を保持したp〇〇の変数も用意されているのですが(pMouseXなど),pDeviceOrientationはどうやら用意されていませんでした.そのため,前フレームのデバイスの姿勢を保持する変数を自作しています.
(注:deviceTurned() といういかにもな名前のメソッドも用意されていたのですが,端末の縦横判定と微妙に異なったため,今回はdeviceOrientationを使用しました.)
また,この方法はWebアプリモードでは機能しませんでした.おそらくWebアプリモード自体が縦横の向き変更に対応しきれていないのだと思います.

// main.ts

let pDeviceOrientation: any;

function setup(): void {
  // setup() に以下を追加
  pDeviceOrientation = deviceOrientation;
}

function draw(): void {
  // draw() に以下を追加
  if (pDeviceOrientation !== undefined && pDeviceOrientation !== deviceOrientation) {
    // 向きが変わったとき
    noCanvas();
    createCanvas(window.innerWidth, windowHeight);
  }
  pDeviceOrientation = deviceOrientation;
}

これで,スマートフォンで実行した場合の問題点はおおよそ解決できました.
修正後のデータ一式はこちらに載せております

github.com

Part3. ジャイロセンサの使用

このパートで作るものはこちらになっています
f:id:wgg00sh:20181224134929p:plain
スマートフォンを傾けると,ライトセーバーのように光線の向きが変わります.

スマートフォンで容易に使用できる入力機器として,ジャイロセンサがあります.
p5.jsなら組み込み変数 rotationX 等を使用することで,そのデータを扱うことができます.

// main.ts

/// <reference path="p5.global-mode.d.ts" />

let pDeviceOrientation: any;

function setup(): void {

  createCanvas(windowWidth, windowHeight);

  background(0);
  colorMode(HSB, 360, 100, 100, 100);

  pDeviceOrientation = deviceOrientation;
}

function draw(): void {

  if (pDeviceOrientation !== undefined && pDeviceOrientation !== deviceOrientation) {
    // 向きが変わったとき
    noCanvas();
    createCanvas(window.innerWidth, windowHeight);
  }
  pDeviceOrientation = deviceOrientation;

  blendMode(BLEND);
  background(0,5);
  blendMode(ADD);

  translate(windowWidth / 2, windowHeight / 2);

  stroke(rotationZ, 85, 10);
  strokeWeight(3);
  // ジャイロセンサの値を元に線の位置を指定する
  let startPos: p5.Vector = new p5.Vector(0, 0);
  let endPos: p5.Vector = new p5.Vector(rotationY / 90 * width, -rotationX / 90 * height);
  for (let i: number = 0; i < 25; i++){
    let startRandVec: p5.Vector = p5.Vector.random2D().mult(random(0, 10));
    let endRandVec: p5.Vector = p5.Vector.random2D().mult(random(0, 10));
    line(startPos.x + startRandVec.x, startPos.y + startRandVec.y, endPos.x + endRandVec.x, endPos.y + endRandVec.y);
  }
}

ジャイロセンサだけでなく,加速度なども扱うことができるので,p5.js公式リファレンスのこの辺りを自由に使ってみてください
f:id:wgg00sh:20181224114912p:plain
p5js.org

Part4. マルチタップ

このパートで作成したものはこちらになります.

f:id:wgg00sh:20181224135023p:plain

PCでは基本的にマウスを使ったクリックしか行えませんが(タッチスクリーン除く),スマートフォンでは多くの場合マルチタップが可能で,画面上の複数個所をタッチしてもその全て(上限有)を記録できます
p5.js では組み込み変数 touchesを用いてタッチされた点と,タッチされ続けているかの情報を取得できます.

// main.ts

/// <reference path="p5.global-mode.d.ts" />


let pDeviceOrientation: any;

// タッチした情報を格納するクラス
class touchObject{
  private touch: object;
  private id: number;
  private count: number;
  constructor(_touch: object) {
    this.touch = _touch;
    this.id = _touch.id;
    this.count = 0;
  }

  public update(): boolean{
    this.count++;

    // 削除判定
    // 同じIDのtouchが存在しなければ削除
    let isExist: boolean=false;
    touches.forEach(element => {
      if (element.id === this.id) {
        this.touch = element;
        isExist = true;
      }
    });

    this.draw();


    return isExist;
  }

  private draw(): void{
    noFill();
    stroke(abs(this.id * 73) % 360, 70, 10);
    strokeWeight(this.count/2);
    ellipse(this.touch.x, this.touch.y, this.count*this.count/20, this.count*this.count/20);
  }
}

let touchObjectList: touchObject[];

function setup(): void {
  createCanvas(windowWidth, windowHeight);

  background(0);
  colorMode(HSB, 360, 100, 100, 100);

  touchObjectList = new Array();

  pDeviceOrientation = deviceOrientation;
}

function draw(): void {
  if (pDeviceOrientation !== undefined && pDeviceOrientation !== deviceOrientation) {
    // 向きが変わったとき
    noCanvas();
    createCanvas(window.innerWidth, windowHeight);
  }
  pDeviceOrientation = deviceOrientation;

  blendMode(BLEND);
  background(0,2);
  blendMode(ADD);
  stroke(255);
  if (touches.length != 0) {

    // 初出現のIDを探す
    touches.forEach(element => {
      console.log(element);
      let isExist: boolean = false;
      touchObjectList.forEach(object => {
        if (element.id === object.id) {
          isExist = true;
        }
      });
      if (isExist === false) {
        // 要素の追加
        touchObjectList.push(new touchObject(element));
      }
    });
  }


  touchObjectList.forEach(element => {
    if (element.update() === false) {
      touchObjectList.pop(element);
    }
  });

}

Part5. 実践

f:id:wgg00sh:20181224134658p:plain
冒頭であげた作品も,これまでの知識を踏まえて作りました.リサージュ曲線をベースにした形です.立体感のある形状ですが,p5の3D機能がまだ不十分なため,2D機能のみで作っています.
ソースコードこちらに置いています.解説するには非常に長いので,気になる点などありましたらコメントやTwitterにリプライ送ってください.

終わりに

こんな感じで,p5.jsとスマートフォンを用いてネイティブアプリ風のWebページを作成する方法をお話しました.
この記事を読んでくださった方は是非オリジナルの作品を作ってみてください.