🍥

p5.jsで図形を回転させる

2021/03/02に公開
2

p5.js で図形を回転させる方法について解説していきます。

本記事は『p5.js で学ぶプログラミング入門』の補足です。

回転の考え方

p5.js における図形の回転は、translate 関数rotate 関数 を組み合わせて行います。英単語の意味どおり、translate は移動、rotate は回転を表します。

大事なポイントは、図形そのものを回転させるのではなく、キャンバスを回転させるということです。
絵を手描するときも、回転した四角を描くのって難しいですよね(四角はまあ描けるとしても顔とかは難しいですよね)。そんなときは紙のほうを回転させると、描くのは回転なしの で済みます。

p5.js でもこれと同じ考え方をします。p5js だけでなく、プログラミングでグラフィックを扱うときの一般的な考え方だと思います。

もう一つ押さえておくべきポイントは、キャンバスを回転させるときの 回転軸がキャンバスの原点 になることです。
初期値では「キャンバスの左上角」が原点なので、図形の中心を軸として回転させたくても、キャンバスの左上角を中心に回転(というより弧を描いて移動)してしまいます。

したがって、図形の中心を軸として回転させたければ、キャンバスの原点を図形の中心に移動する必要があります。
それが translate 関数です。図形を translate(移動)する関数ではなく、原点を translate する関数なのです。

ということで、図形をその中心を軸として回転する手順は次のようになります。

  1. translate 関数でキャンバスの原点を図形の中心に移動する
  2. rotate 関数でキャンバスを回転させる
  3. 図形を描画する(キャンバスの原点が移動しているので注意)

ちなみに、p5.js の rotate 関数は、初期値は回転角をラジアン(radian)で指定します。ラジアンは 360° を 2π で表す単位系です。
角度(degree)のほうがイメージしやすい人が多いと思いますので、本記事では setup 内で angleMode(DEGREES) を呼び出して回転角を角度(degree)で表すように変更します。

図形が一つだけのとき

まず、キャンバスの原点を変更せずに回転させてみましょう。

回転させる前の状態です。正方形(square 関数)で、座標をキャンバス中央にしています。

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);

  square(width / 2, height / 2, 80);
}

square の基準点は左上角なので以下のようにキャンパス中央からズレています。

初期配置

rotate 関数だけ使って回転させてみます。

let angle = 0;

function setup() {
  createCanvas(400, 400);
  angleMode(DEGREES); // 角度の単位をradianからdegreeに変更
}

function draw() {
  background(220);

  // 回転角度を1°ずつ増やす
  angle += 1;

  // 初期値の原点を中心にキャンバスを回転
  rotate(angle);

  // キャンバス中央を基準点として正方形を描画
  square(width / 2, height / 2, 80);
}

上記コードでは、キャンバスの回転中心(原点)がキャンバスの左上角になっているので、キャンバスの左上角を中心に四角が移動すると思います。すぐに画面から消えてしまいますが、しばらく待っていると 1 周まわって戻ってくるので、大きく回転していることがわかるかと思います。

では、translate 関数を使ってキャンバスの原点をキャンバス中央に変更してみましょう。以下のコードを rotate 関数の上に入れてください。

// 原点をキャンバスの中央に移動
translate(width / 2, height / 2);

どうなったでしょうか?
何も描画されなくなってしまったかと思います。

ここが難しいポイントなのですが、キャンバスの原点を移動するということは、square 関数の座標もその原点を (0, 0) とした座標系で指定する必要があるということです。

上記コードでは、square 関数は以下のように記述されていました。
引数にキャンバス中央の座標を指定していますね。しかし、この座標は キャンバスの原点がキャンバスの左上角だったときの座標系 を想定して指定したものです。

// キャンバス中央を基準点として正方形を描画
square(width / 2, height / 2, 80);

キャンバスの原点は、translate 関数によってキャンバスの中央に移動しました。
ですので、square 関数の座標(基準点)をキャンバス中央にしたければ、(0, 0) を指定すればよいことになります。

// キャンバス中央を基準点として正方形を描画
square(0, 0, 80);

どうでしょう、正方形の左上角を中心に回転するアニメーションになったと思います。紙に描いた正方形を、正方形の左上角を中心として回転させている状態です。

正方形の中央を軸として回転させるにはどうすればよいでしょうか。
キャンバスは、キャンバスの中央を中心として回転しているわけですから、正方形の中央とキャンバス中央が一致するように正方形の座標(基準点)を少しズラせば OK です。

// キャンバス中央=正方形の中央となるように描画
square(-40, -40, 80);

この中央をズラす計算が面倒だ…という人のために、p5.js では基本図形の基準点を変更する命令が用意されています。
square 関数や rect 関数の基準点を中央にするには、以下のように rectMode 関数 を呼び出します。

rectMode(CENTER); // 矩形の基準点を図形の中央にする

rectMode 関数は、プログラムで描画するすべての矩形の基準点が中央でよいなら、setup 内で 1 度だけ呼べば OK です。
そうでない場合は、図形描画の直前に呼び出して、rectMode(CORNER) で元の設定に戻します。fill などと同じですね。

rectMode(CENTER);
square(0, 0, 80);
rectMode(CORNER);

図形が複数のとき

一つの図形を回転させるだけなら上記の方法でいいのですが、図形が複数あって、そのうちの一つだけを回転させたいときはどうなるでしょうか。

試しに、正方形の下に円を一つ描いてみます。この円は回転させたくないとします。

正方形の下に円

let angle = 0;

function setup() {
  createCanvas(400, 400);
  angleMode(DEGREES);
  rectMode(CENTER);
}

function draw() {
  background(220);

  // 回転角度を1°ずつ増やす
  angle += 1;

  // 原点をキャンバスの中央に移動
  translate(width / 2, height / 2);

  // 原点を中心にキャンバスを回転
  rotate(angle);

  // キャンバス中央を基準点として正方形を描画
  square(0, 0, 80);

  // 円を正方形の下に描く(これは回転させない)
  circle(0, height / 3, 80);
}

月と地球のように回転してしまいましたね。(関係のない話ですが、地球の自転と月の公転はなぜ完全一致しているのでしょうね)

最初に説明したとおり、rotate 関数は図形を回転させているのではなく、キャンバスそのものを回転させています。したがって、正方形の下に描画した円も、キャンバス中央を中心として回転してしまいます。

円の位置を固定するためには、少なくともキャンバスの回転状態を元に戻す必要があります。

translate(width / 2, height / 2);
rotate(angle);
square(0, 0, 80);

// キャンバスの回転を元に戻す
rotate(-angle);

circle(0, height / 3, 80);

これで正方形だけが回転するようになります。

ただ、今回はたまたまキャンバス中央を回転中心としたので原点がわかりやすくキャンバス中央になっていますが、変な位置に原点があったら、他の図形を描画するときに計算がややこしくなります。
したがって、原点もちゃんとキャンバスの左上角に戻しておくのがよいでしょう。

translate(width / 2, height / 2);
rotate(angle);
square(0, 0, 80);

// キャンバスの回転を元に戻す
rotate(-angle);

// キャンバスの原点を左上角に戻す
translate(-width / 2, -height / 2);

circle(width / 2, height * 5 / 6, 80);  // 元の座標系で位置指定

円の座標を「キャンバスの左上角が原点」の座標系で指定しなおすことを忘れずに。
y 方向の位置は height/3 + height/2 となるので、計算して height * 5/6 としています。

とはいえ、この戻す作業も増えてくると面倒ですよね。回転させたい図がたくさんあると頭がこんがらがりそうです。
幸いにも、p5.js には簡単に元の状態に戻せるコマンドが用意されています。

元に戻すためには「どこからどこまでの範囲を戻すか」を指定する必要があるので、「ここから」「ここまで」を指定します。
そのために、push 関数pop 関数 を使います。

push();  // ここから
translate(width / 2, height / 2);
rotate(angle);
square(0, 0, 80);
pop();   // ここまで(もとに戻す)

circle(width / 2, height * 5 / 6, 80);  // 元の座標系で位置指定

なお、rectMode 関数も push/pop で設定/解除できるので、基準座標を一時的に CENTER にする場合は push-pop の間に入れてしまえば OK です。

コード全体を再掲します。

let angle = 0;

function setup() {
  createCanvas(400, 400);
  angleMode(DEGREES);  // 角度単位をdegreeに
  rectMode(CENTER);    // 矩形の基準座標を図形中央に設定
}

function draw() {
  background(220);

  angle += 1;  // 回転角度を1°ずつ増やす

  push();
  translate(width / 2, height / 2);  // 原点をキャンバスの中央に移動
  rotate(angle);     // 原点を中心にキャンバスを回転
  square(0, 0, 80);  // 原点移動・回転したキャンバス座標系で正方形を描画
  pop();  // 設定を元に戻す

  // 円を正方形の下に描く(これは回転させない)
  circle(0, height / 3, 80);
}

おまけ

class を使って複数のオブジェクトを回転させるときに push/pop は威力を発揮します。

https://editor.p5js.org/ojk/present/CSjONFqQ2

風車

class Windmill {
  constructor() {
    this.x = random(width);
    this.y = random(height);
    this.size = random(10, 50);
    this.deg = 0;
    this.vel = 50/this.size;
    this.col = color(random(100, 255), random(100, 255), random(100, 255));
  }

  draw() {
    this.deg += this.vel;
    push();
    fill(this.col);
    translate(this.x, this.y);
    rotate(this.deg);
    square(0, 0, this.size);
    pop();
  }
}

let wms = [];
const N = 100;

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  rectMode(CENTER);
  noStroke();

  for (let i = 0; i < N; i += 1) {
    wms.push(new Windmill());
  }
}

function draw() {
  background(255);

  for (const wm of wms) {
    wm.draw();
  }
}

Discussion

手羽先手羽先

translateの説明など、とても分かりやすくて助かりました!ありがとうございます。