p5.js応用


# はじめに

この記事は[福岡明太子クラブ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/fukuoka-mentaiko-club)の４日目の記事です。

# やること

本当はprocessingを使用して、mac上で動くグラフィカルなアプリを作成したかったのですが、
processingが上手く動作しなかったので、慣れる意味も込めてp5.jsを触ってみました。

とりあえず作ってみたものがこちらです。

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68616/7a21e8ca-b551-b087-4bc5-46c3f99e48b0.gif "sample.gif")

* ピンクの円をクリックすると選択状態になり、緑色になる
* 適当な場所をダブルクリックすると、そこに新たな円を作成して、選択した円と作成した円を線で結ぶ

# 環境

* macOS 10.15.1
* p5.js version 0.9.0

# 準備

[前回の記事](https://qiita.com/shoboo/private/1caff92eea709e3c8bdd)で作成したファイルがある前提とします。

# 作成

## ダブルクリックして円を作成する

タブルクリック時に円（bubble）を作成する

```js
const canvasX = 1000;
const canvasY = 1000;

const diameter = 50;

const canvasColor = 'skyblue';
const circleColor = 'pink';

// 最初に１回だけ実行される処理
function setup() {
  createCanvas(canvasX, canvasY);
  background(canvasColor);
}

// setup後に繰り返し実行される処理（フレーム単位）
function draw() {
}

// ダブルクリック時のコールバック
function doubleClicked() {
  createBubble(mouseX, mouseY);
  return false;
}

function createBubble(_x, _y) {
  fill(circleColor);
  noStroke();
  circle(_x, _y, diameter);
}
```

![sample5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68616/5c5ff74b-3a6f-a1dd-04f1-451f045ba5d7.gif "sample5.gif")

## 前に作成した円と新しい円に線を引く

```js
const canvasX = 1000;
const canvasY = 1000;

const diameter = 50;

const canvasColor = 'skyblue';
const circleColor = 'pink';

/* 追加 */
let bubbleX = 0;
let bubbleY = 0;

// 最初に１回だけ実行される処理
function setup() {
  createCanvas(canvasX, canvasY);
  background(canvasColor);
}

// setup後に繰り返し実行される処理（フレーム単位）
function draw() {
}

// ダブルクリック時のコールバック
function doubleClicked() {
  createBubble(mouseX, mouseY);
  return false;
}

function createBubble(_x, _y) {
  fill(circleColor);
  noStroke();
  circle(_x, _y, diameter);

  /* 追加 */
  drawLine(_x, _y);
  bubbleX = _x;
  bubbleY = _y;  
}

/* 追加 */
function drawLine(_x, _y) {
  if (bubbleX === 0 && bubbleY === 0) {
    return;
  }
  stroke(0);
  line(bubbleX, bubbleY, _x, _y)
}
```

![sample6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68616/f485506a-2d46-c24a-0c22-58004daf1226.gif "sample6.gif")

円の上に線が重なっているが今は気にしない（描画順によるもの）

## bubbleをクラス化する

現状の実装だとbubbleを選択する際に、クリック時のマウスの位置が作成した円の領域内にあるかを随時計算する必要があるのでbubbleをクラス化する。

### ダブルクリックして円を作成する

```js
const canvasX = 1000;
const canvasY = 1000;

const diameter = 50;

const canvasColor = 'skyblue';
const circleColor = 'pink';

const bubbles = [];

// 最初に１回だけ実行される処理
function setup() {
  createCanvas(canvasX, canvasY);
  background(canvasColor);
}

// setup後に繰り返し実行される処理（フレーム単位）
function draw() {
  for (let i = 0; i < bubbles.length; i++) {
    bubbles[i].show();
  }
}

function doubleClicked() {
  createBubble(mouseX, mouseY);
  return false;
}

function createBubble(_x, _y) {
  let bubble = new Bubble(_x, _y, diameter);
  bubbles.push(bubble);
}

class Bubble {

  constructor(_x, _y, _r) {
    this.x = _x;
    this.y = _y;
    this.r = _r;
    this.color = circleColor;
  }

  show() {
    noStroke();
    noFill();
    fill(this.color);
    circle(this.x, this.y, this.r);
  }
}
```

![sample7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68616/6f707ed8-5e35-6819-642f-ee36d0cc7fbf.gif "sample7.gif")

### 円をクリックして選択状態にする

```js
const canvasX = 1000;
const canvasY = 1000;

const diameter = 25;

const canvasColor = 'skyblue';
const selectedColor = 'green';
const unselectedColor = 'pink';

let bubbles = [];
let selectedBubble = null;

// 最初に１回だけ実行される処理
function setup() {
  createCanvas(canvasX, canvasY);
  background(canvasColor);
}

// setup後に繰り返し実行される処理（フレーム単位）
function draw() {
  for (let i = 0; i < bubbles.length; i++) {
    bubbles[i].show();
  }
}

// マウスクリック時のコールバック
function mousePressed() {
  // クリック時の座標が作成された円に含まれるかチェック
  for (let i = 0; i < bubbles.length; i++) {
    const b = bubbles[i];
    if (b.contains(mouseX, mouseY)) {
      b.select();
    }
  }
  return false;
}

function doubleClicked() {
  createBubble(mouseX, mouseY);
  return false;
}

function createBubble(_x, _y) {
  let bubble = new Bubble(_x, _y, diameter);
  bubbles.push(bubble);
}

class Bubble {

  constructor(_x, _y, _r) {
    this.x = _x;
    this.y = _y;
    this.r = _r;
    this.color = unselectedColor;
  }

  show() {
    noStroke();
    noFill();
    fill(this.color);
    circle(this.x, this.y, this.r * 2);
  }

  select() {
    if (selectedBubble !== null) {
      selectedBubble.unselect();
    }
    this.changeColor(selectedColor);
    selectedBubble = this;
  }

  unselect() {
    this.changeColor(unselectedColor);
    selectedBubble = null;
  }

  changeColor(_color) {
    this.color = _color;
  }

  contains(_x, _y) {
    let d = dist(_x, _y, this.x, this.y);
    return d < this.r;
  }
}
```

![sample 8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68616/021a5cfe-bfc8-b935-9d1f-d5b78faab6bb.gif "sample 8.gif")

### 選択したbubbleと作成したbubbleを線で結ぶ（最終形）

今回の場合、ロード時にcanvasの中央に円を作成し、bubbleを選択していない場合はbubbleを作成しないようにする。

```js
const canvasX = 1000;
const canvasY = 1000;

const centerX = canvasX/2;
const centerY = canvasY/2;

const diameter = 25;

const canvasColor = 'skyblue';
const selectedColor = 'green';
const unselectedColor = 'pink';

let bubbles = [];
let selectedBubble = null;

// 最初に１回だけ実行される処理
function setup() {
  createCanvas(canvasX, canvasY);
  background(canvasColor);
  let bubble = new Bubble(centerX, centerY, diameter);
  bubbles.push(bubble);
}

// setup後に繰り返し実行される処理（フレーム単位）
function draw() {
  for (let i = 0; i < bubbles.length; i++) {
    bubbles[i].show();
  }
}

// マウスクリック時のコールバック
function mousePressed() {
  for (let i = 0; i < bubbles.length; i++) {
    const b = bubbles[i];
    if (b.contains(mouseX, mouseY)) {
      b.select();
    }
  }
  return false;
}

function doubleClicked() {
  if (selectedBubble !== null) {
    createBubble(mouseX, mouseY)
  }
  return false;
}

function createBubble(_x, _y) {
  let bubble = new Bubble(_x, _y, diameter);
  bubbles.push(bubble);
  drawLine(_x, _y);
}

function drawLine(_x, _y) {
  stroke(0);
  line(selectedBubble.x, selectedBubble.y, _x, _y);
}

class Bubble {

  constructor(_x, _y, _r) {
    this.x = _x;
    this.y = _y;
    this.r = _r;
    this.color = unselectedColor;
  }

  show() {
    noStroke();
    noFill();
    fill(this.color);
    circle(this.x, this.y, this.r * 2);
  }

  select() {
    if (selectedBubble !== null) {
      selectedBubble.unselect();
    }
    this.changeColor(selectedColor);
    selectedBubble = this;
  }

  unselect() {
    this.changeColor(unselectedColor);
    selectedBubble = null;
  }

  changeColor(_color) {
    this.color = _color;
  }

  contains(_x, _y) {
    let d = dist(_x, _y, this.x, this.y);
    return d < this.r;
  }
}
```

![sample9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68616/48252734-5a5d-f517-fe4f-4b3161cfa9b0.gif "sample9.gif")

「前に作成した円と新しい円に線を引く」で線を引いた際に円の上に線が描画されていたが、こちらでは解決されています。

これはbubbleをdraw内で随時再描画させているためで、先の描画よりもbubbleの描画が後に行われているためです。

# 最後に

今回作成した内容は、作りたいと思っているものの一部なので偏った内容となっています。

ネット上のサンプルではかなり綺麗なグラフィックを描画しているものもあるので、もっと触って色々作れるように勉強していきます。

# 参考

公式で用意されているものがかなり分かりやすかったです。

* [公式サイト](https://p5js.org/)
* [YouTube](https://www.youtube.com/playlist?list=PLRqwX-V7Uu6Zy51Q-x9tMWIv9cueOFTFA)