クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【p5.js】Generative Design with p5.js「画像 P_4_1_1_01」のコードを読み解く

p5.jsの続きです。前回は「Generative Design with p5.js」の「文字 P_3_1_1_01」を見ました。今回は画像編「画像 P_4_1_1_01」を見ていきます。それでは行きましょう!

【目次】

※参考:前回記事
https://https://www.i-ryo.com/entry/2020/05/28/190542

※参考:p5.jsを習得するためにやったことまとめ
qiita.com

画像 P_4_1_1_01

書籍「Generative Design with p5.js」より

ソースコード一覧
※参考:Generative Design with p5.js[p5.js版ジェネラティブデザイン] ―ウェブでのクリエイティブ・コーディング

画像 P_4_1_1_01

最初は画像1枚。カーソルを載せると四角い枠が現れる。クリックするとその枠の範囲が切り抜かれた升目状になる。マウスを動かすと元に戻る。

※参考:p5.js Web Editor

JSコード全体

// P_4_1_1_01
//
// Generative Gestaltung – Creative Coding im Web
// ISBN: 978-3-87439-902-9, First Edition, Hermann Schmidt, Mainz, 2018
// Benedikt Groß, Hartmut Bohnacker, Julia Laub, Claudius Lazzeroni
// with contributions by Joey Lee and Niels Poldervaart
// Copyright 2018
//
// http://www.generative-gestaltung.de
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * cutting and multiplying an area of the image
 *
 * MOUSE
 * position x/y         : area position
 * left click           : multiply the area
 *
 * KEYS
 * 1-3                  : area size
 * r                    : toggle random area
 * s                    : save png
 */
'use strict';

var img;

var tileCountX = 4;
var tileCountY = 4;
var tileCount = tileCountX * tileCountY;
var imgTiles = [];
var tileWidth;
var tileHeight;
var cropX;
var cropY;

var selectMode = true;
var randomMode = false;

function preload() {
  img = loadImage('data/image.jpg');
}

function setup() {
  createCanvas(800, 600);

  image(img);
  tileWidth = width / tileCountY;
  tileHeight = height / tileCountX;

  noCursor();
  noFill();
  stroke(255);
}

function draw() {
  if (selectMode) {
    // in selection mode, a white selection rectangle is drawn over the image
    cropX = constrain(mouseX, 0, width - tileWidth);
    cropY = constrain(mouseY, 0, height - tileHeight);
    image(img, 0, 0);
    rect(cropX, cropY, tileWidth, tileHeight);
  } else {
    // reassemble image
    var imgIndex = 0;
    for (var gridY = 0; gridY < tileCountY; gridY++) {
      for (var gridX = 0; gridX < tileCountX; gridX++) {
        image(imgTiles[imgIndex], gridX * tileWidth, gridY * tileHeight);
        imgIndex++;
      }
    }
  }
}

function cropTiles() {
  tileWidth = width / tileCountY;
  tileHeight = height / tileCountX;
  imgTiles = [];

  for (var gridY = 0; gridY < tileCountY; gridY++) {
    for (var gridX = 0; gridX < tileCountX; gridX++) {
      if (randomMode) {
        cropX = int(random(mouseX - tileWidth / 2, mouseX + tileWidth / 2));
        cropY = int(random(mouseY - tileHeight / 2, mouseY + tileHeight / 2));
      }
      cropX = constrain(cropX, 0, width - tileWidth);
      cropY = constrain(cropY, 0, height - tileHeight);
      imgTiles.push(img.get(cropX, cropY, tileWidth, tileHeight));
    }
  }
}

function mouseMoved() {
  selectMode = true;
}

function mouseReleased() {
  selectMode = false;
  cropTiles();
}

function keyReleased() {
  if (key == 's' || key == 'S') saveCanvas(gd.timestamp(), 'png');

  if (key == 'r' || key == 'R') {
    randomMode = !randomMode;
    cropTiles();
  }

  if (key == '1') {
    tileCountX = 4;
    tileCountY = 4;
    cropTiles();
  }
  if (key == '2') {
    tileCountX = 10;
    tileCountY = 10;
    cropTiles();
  }
  if (key == '3') {
    tileCountX = 20;
    tileCountY = 20;
    cropTiles();
  }
}

冒頭コメントの前半はクレジット。 後半の概要の訳。

画像の領域を切り取り、乗算する
マウス
位置x / y:エリアの位置
左クリック:エリアを乗算
キー
1-3:面積
r:ランダム領域を切り替えます
s:pngを保存

全体構成

// グローバル変数

function preload() {
  // ページ読み込み前の処理
}

function setup() {
  // ページ読み込み時の処理
}

function draw() {
  // 一定時間ごとに繰り返し実行
}

function cropTiles() {
  // タイルのトリミング
}

function mouseMoved() {
  // マウスが移動している時の処理
}

function mouseReleased() {
  // マウスを離した時の処理
}


function keyReleased() {
  // キーが離れた時の処理
}

いつものsetup()draw()keyReleased()などの他にもいくつかメソッドが見受けられる。

cropTiles()はp5.jsの組み込みメソッドではなく今回独自に生成された関数だった。

mouseReleased()は初めて出て来たマウスを離した時の処理だった。

The mouseReleased() function is called every time a mouse button is released.

mouseReleased()関数は、マウスボタンが離されるたびに呼び出されます。

※参考:reference | p5.js

グローバル変数

冒頭のグローバル変数の設定。改行が入って3グループに分かれている。

まず変数imgを宣言

var img;

値はまだない

次にタイル関係の設定がある。

var tileCountX = 4;
var tileCountY = 4;
var tileCount = tileCountX * tileCountY;
var imgTiles = [];
var tileWidth;
var tileHeight;
var cropX;
var cropY;

変数tileCountは式になっていて変数tileCountXtileCountYを掛けた値

最後は「Mode」ということで初期設定だろうか。

var selectMode = true;
var randomMode = false;

真偽値(truefalse)が設定されている。

setup()メソッドの中身

setup()メソッド全体

ページ読み込み時の処理

function setup() {
  createCanvas(800, 600);

  image(img);
  tileWidth = width / tileCountY;
  tileHeight = height / tileCountX;

  noCursor();
  noFill();
  stroke(255);
}

ブロックごとに見ていく

キャンバスを作成

  createCanvas(800, 600);
  • createCanvas()メソッド実行。引数は左から800、600

キャンバス作成

画像読み込む、タイルサイズ設定

  image(img);
  tileWidth = width / tileCountY;
  tileHeight = height / tileCountX;
  • image()メソッド実行。引数はimg
  • tileWidthwidth割るtileCountY
  • tileHeightheight割るtileCountX

image()メソッドは以前も出て来た、p5.jsのキャンバス上で画像を描画するメソッド。今回は変数imgを読み込む。

※参考:【p5.js】Generative Design with p5.js「画像 P_4_0_01」のコードを読み解く - クモのようにコツコツと

tileWidthはタイルの幅でキャンバス幅をタイルの横側の数で割る。

tileHeightはタイルの高さでキャンバス高さをタイルの縦側の数で割る

カーソル、塗り、線の設定

  noCursor();
  noFill();
  stroke(255);
  • noCursor()メソッド実行
  • noFill()メソッド実行
  • stroke()メソッド実行(引数は255)

noCursor() はカーソルを非表示にするメソッド

※参考:【p5.js】Generative Design with p5.js「文字 P_3_1_1_01」のコードを読み解く - クモのようにコツコツと

noFill()は塗りを無しにするメソッド

※参考:【p5.js】Generative Design with p5.js「形 P_2_0_02、03」のコードを読み解く - クモのようにコツコツと

stroke()は線の色で255はRGB3色とも真っ白ということ。

draw()メソッドの中身

draw()メソッド全体

一定時間ごとに繰り返し実行

function draw() {
  if (selectMode) {
    // in selection mode, a white selection rectangle is drawn over the image
    cropX = constrain(mouseX, 0, width - tileWidth);
    cropY = constrain(mouseY, 0, height - tileHeight);
    image(img, 0, 0);
    rect(cropX, cropY, tileWidth, tileHeight);
  } else {
    // reassemble image
    var imgIndex = 0;
    for (var gridY = 0; gridY < tileCountY; gridY++) {
      for (var gridX = 0; gridX < tileCountX; gridX++) {
        image(imgTiles[imgIndex], gridX * tileWidth, gridY * tileHeight);
        imgIndex++;
      }
    }
  }
}

ブロックごとに見ていく。

if文の大枠

  if (selectMode) {
    // in selection mode, a white selection rectangle is drawn over the image
    // (処理)
  } else {
    // reassemble image
    // (処理)
  }
  • もしselectModeがtrueならば
  • 画像の上に白い四角を描画
  • さもなくば画像を再構成

if部分のコメント

in selection mode, a white selection rectangle is drawn over the image

選択モードでは、白い選択長方形が画像の上に描画されます

else部分のコメント

reassemble image

画像を再構成する

白い四角を描画

if文のif部分(画像の上に白い四角を描画)

    // in selection mode, a white selection rectangle is drawn over the image
    cropX = constrain(mouseX, 0, width - tileWidth);
    cropY = constrain(mouseY, 0, height - tileHeight);
    image(img, 0, 0);
    rect(cropX, cropY, tileWidth, tileHeight);
  • 変数cropXconstrain()メソッドを実行(引数は左からmouseX,0,width引くtileWidth`)
  • 変数cropYconstrain()メソッドを実行(引数は左からmouseY ,0,height 引くtileHeight `)
  • image()メソッドを実行(引数は左からimg, 0, 0
  • rect()メソッドを実行(引数は左からcropX, cropY, tileWidth, tileHeight

`constrain()メソッドは初めて出て来た!

Constrains a value between a minimum and maximum value.

値を最小値と最大値の間で制約します。

`constrain()メソッドの引数

constrain(n, low, high)

n Number: number to constrain
low Number: minimum limit
high Number: maximum limit

n番号:制約する番号
低い数:最小制限
高い数:上限

※参考:reference | p5.js

cropXconstrain()mouseXを「0」から「キャンバス幅引くタイル幅」にする。

cropYconstrain()mouseYを「0」から「キャンバス高さ引くタイル高さ」にする。

image()メソッドは第2引数が横位置、第3引数が縦位置なので画像位置の初期値はいずれも0

rect()メソッドは四角の描画で横位置はcropX、縦位置はcropY、幅はtileWidth、高さはtileHeight

この部分で画面上に白い四角が描画される。

画像を再構成

次はif文のelse部分(画像を再構成)

    // reassemble image
    var imgIndex = 0;
    for (var gridY = 0; gridY < tileCountY; gridY++) {
      for (var gridX = 0; gridX < tileCountX; gridX++) {
        image(imgTiles[imgIndex], gridX * tileWidth, gridY * tileHeight);
        imgIndex++;
      }
    }
  • 変数imgIndexの値を0
  • for文が多重ループになっている
  • 外側のfor文の条件:変数gridYの初期値は0gridYtileCountYの値分繰り返す、gridYを1つ加算
  • 内側のfor文の条件:変数gridXの初期値は0gridXtileCountXの値分繰り返す、gridXを1つ加算
  • image()メソッドを実行(引数は左から配列imgTiles[]imgIndexキー, gridX掛けるtileWidth, gridY掛けるtileHeight
  • imgIndexに値を1つ加算

多重ループでタイルの横の数、縦の数分処理を繰り返す。これで升目状になる。

for文の中ではimage()メソッドで配列imgTilesの中のimgIndexの画像を横のグリッド数掛けるタイル幅、縦のグリッド数掛けるタイル高さの位置に配置する。 配列imgTilesimgIndexキーの値を一つ加算して次の画像を描画

この部分で升目状の画像に再構成されるわけだ!

cropTiles()関数

cropTiles()関数全体

タイルのトリミング設定。「crop」は「作物」という意味でタイルを作るということか。

function cropTiles() {
  tileWidth = width / tileCountY;
  tileHeight = height / tileCountX;
  imgTiles = [];

  for (var gridY = 0; gridY < tileCountY; gridY++) {
    for (var gridX = 0; gridX < tileCountX; gridX++) {
      if (randomMode) {
        cropX = int(random(mouseX - tileWidth / 2, mouseX + tileWidth / 2));
        cropY = int(random(mouseY - tileHeight / 2, mouseY + tileHeight / 2));
      }
      cropX = constrain(cropX, 0, width - tileWidth);
      cropY = constrain(cropY, 0, height - tileHeight);
      imgTiles.push(img.get(cropX, cropY, tileWidth, tileHeight));
    }
  }
}

これはp5.jsの組み込みメソッドではなく本件で独自に作成している関数。

変数設定

最初に変数の設定

  tileWidth = width / tileCountY;
  tileHeight = height / tileCountX;
  imgTiles = [];
  • 変数tileWidthwidth割るtileCountY
  • 変数tileHeightheight割るtileCountX
  • 変数imgTilesを空の配列に

いずれも先程のdraw()メソッドの処理の中に出てくる変数!

多重ループ部分

その後の処理はボリュームがあるのでさらに細かく見ていく。まずは外側の多重ループ

for (var gridY = 0; gridY < tileCountY; gridY++) {
    for (var gridX = 0; gridX < tileCountX; gridX++) {
      // 処理
    }
  }
  • 外側のfor文の条件:変数gridYの初期値は0gridYtileCountYの数繰り返す、gridYを1つ加算する
  • 内側のfor文の条件:変数gridXの初期値は0gridXtileCountXの数繰り返す、gridXを1つ加算する

先程のdraw()の後に出て来た多重ループと似ている。グリッドの横位置、縦位置をタイル数文増やして行く設定と思われる。

if文

for文の中にはif文がある

      if (randomMode) {
        cropX = int(random(mouseX - tileWidth / 2, mouseX + tileWidth / 2));
        cropY = int(random(mouseY - tileHeight / 2, mouseY + tileHeight / 2));
      }
  • もしrandomModetrueであれば
  • cropXint()メソッドを実行(引数はrandom()メソッド)
    random()メソッドの引数は左からmouseX引くtileWidth割る2mouseX足すtileWidth割る2
  • cropYint()メソッドを実行(引数はrandom()メソッド)
    random()メソッドの引数引数は左からmouseY引くtileHeight割る2mouseY足すtileHeight割る2

int()メソッドは少数を切り捨てて整数にするメソッド。

※参考:【p5.js】Generative Design with p5.js「形 P_2_0_02、03」のコードを読み解く - クモのようにコツコツと

random()メソッドは引数2つの間のランダムな数値を出す

※参考:【p5.js】クリエイティブコーディングに挑戦(その1) - クモのようにコツコツと

cropXはマウスの縦位置から高さの半分の間、cropYは横位置からタイルの横幅の半分を引いた値のランダムな数値が出る。

これがランダムモードのトリミング位置になる。

その他の処理

if文の後にいくつかの処理がある

      cropX = constrain(cropX, 0, width - tileWidth);
      cropY = constrain(cropY, 0, height - tileHeight);
      imgTiles.push(img.get(cropX, cropY, tileWidth, tileHeight));
  • 変数cropXconstrain()メソッドを実行(引数は左からcropX, 0, width引くtileWidth
  • 変数cropYconstrain()メソッドを実行(引数は左からcropY, 0, height引くtileHeight
  • imgTilesオブジェクトのpush()メソッドを実行
    push()メソッドの引数でimgオブジェクトのget()メソッドを実行
    (引数は左からcropX, cropY, tileWidth, tileHeight

constrain()メソッドは先程draw()メソッド出て来た。値を最小値と最大値の間で制約する。

cropXを0〜キャンバス幅引くタイル幅、cropYを0〜キャンバス高さ引くタイル高さの範囲にする。

imgTilesは配列なのでここで使われるpush()はJS組み込みメソッドの配列追加か。

※参考:Array.prototype.push() - JavaScript | MDN

p5.jsのpush()メソッドは図の描画だがpop()メソッドとセットになるはずなので。

※参考:【p5.js】Generative Design with p5.js「文字 P_3_1_1_01」のコードを読み解く - クモのようにコツコツと

で、そのpush()の中に追加されるのはimgの画像にget()メソッドを実行したもの。

このget()メソッドは今回初めて出て来た!

Get a region of pixels, or a single pixel, from the canvas.

キャンバスからピクセルの領域または単一のピクセルを取得します。

※参考:reference | p5.js

なるほど、このメソッドでトリミングした画像が作られる訳だ!

引数は4つある

get(x, y, w, h)

x Number: x-coordinate of the pixel
y Number: y-coordinate of the pixel
w Number: width
h Number: height

x番号:ピクセルのx座標
y番号:ピクセルのy座標
w番号:幅
h番号:高さ

ピクセル横位置がcropX、ピクセル縦位置がcropY、画像幅がtileWidth、画像高さがtileHeightになると!

mouseMoved()メソッド

マウスが移動している時の処理

function mouseMoved() {
  selectMode = true;
}
  • selectModetrue

マウスを動かすとselectModetrueになる。

mouseReleased()メソッド

マウスを離した時の処理

function mouseReleased() {
  selectMode = false;
  cropTiles();
}
  • selectModefalse
  • cropTiles()関数を実行

マウスを離す、とあるがクリックの度に実行、という動きのようだ。クリックする度にcropTiles()が実行されて升目状の画像が作られる。

keyReleased()メソッドの中身

keyReleased()メソッド全体

キーが離れた時の処理

function keyReleased() {
  if (key == 's' || key == 'S') saveCanvas(gd.timestamp(), 'png');

  if (key == 'r' || key == 'R') {
    randomMode = !randomMode;
    cropTiles();
  }

  if (key == '1') {
    tileCountX = 4;
    tileCountY = 4;
    cropTiles();
  }
  if (key == '2') {
    tileCountX = 10;
    tileCountY = 10;
    cropTiles();
  }
  if (key == '3') {
    tileCountX = 20;
    tileCountY = 20;
    cropTiles();
  }
}

いつもの画像保存以外にもいくつかif文が見受けられる。

sまたはSキーの処理

  if (key == 's' || key == 'S') saveCanvas(gd.timestamp(), 'png');
  • もしsまたはSキーを押したら
  • キャンバスをpng画像に保存する。

これはいつものやつ

rまたはRキーの処理

  if (key == 'r' || key == 'R') {
    randomMode = !randomMode;
    cropTiles();
  }
  • もしrまたはRを押したら
  • randomModeの設定を逆にする
  • cropTiles() メソッドを実行

ランダムモードを切り替えてcropTiles() の画像描画が実行される

1キーの処理

  if (key == '1') {
    tileCountX = 4;
    tileCountY = 4;
    cropTiles();
  }
  • もし1キーを押したら
  • tileCountX4
  • tileCountY4
  • cropTiles() メソッドを実行

2キーの処理

  if (key == '2') {
    tileCountX = 10;
    tileCountY = 10;
    cropTiles();
  }
  • もし2キーを押したら
  • tileCountX10
  • tileCountY10
  • cropTiles() メソッドを実行

3キーの処理

  if (key == '3') {
    tileCountX = 20;
    tileCountY = 20;
    cropTiles();
  }
}
  • もし3キーを押したら
  • tileCountX20
  • tileCountY20
  • cropTiles() メソッドを実行

1〜3キーを押すとトリミングのサイズが変わる!

動作確認

数字キー1〜3を押すとトリミング枠のサイズが変わる。 f:id:idr_zz:20200606183551p:plain

rまたはRキーを押すとトリミング範囲がランダムになる。 f:id:idr_zz:20200607082204p:plain

最後に

前回は画像のリサイズでしたが、今回は画像をトリミングして新たに画像を生成する、という内容でした。画像生成系の新しいメソッドに遭遇しました。トリミングする場所によって違う模様みたいになって面白い挙動です。

次回はまた「形」編の続きを見て行きたく思います。それではまた!


※参考:p5.jsを習得するためにやったことまとめ
qiita.com