Chapter 11

座標変換

miku
miku
2021.11.10に更新

座標

Canvasはピクセルの塊に対して操作を行う機能だ。このピクセルの色は白、あちらのピクセルは赤…のように位置に対して指示をするが、位置を決めるためにはルールが必要だ。

ルールの例を挙げると、

  • 左右がx軸で右にいくほど大きくなる。
  • 上下がy軸で下にいくほど大きくなる。
  • 原点は左上である。

のようなもので、このようなルールを決めているのが座標で、この座標のおかげで位置が指定できるわけである。

ピクセルはある意味物理的なものなので、移動したり拡大したり回転することはできないが、座標のルールは変更することができる。これが今から行おうとしている座標変換である。

座標の移動

circle(0, 0, 10) を実行すると、画面左上に円が描画されるが、これは原点が左上にあるという座標のルールがあるからである。

ここで translate() という関数を利用すると、左上が原点であるというルールを変更することができる。

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

  translate(width / 2, height / 2);
  circle(0, 0, 60);
}

translate(x, y)(0, 0) にあった原点の位置が (x, y) にくるように座標の位置が調整される。上記では translate(width / 2, height / 2) と指定しているので、原点が画面中央になるように座標の移動が行われる。

そのあと、円を (0, 0) に配置しているが、今原点は画面中央にあるので、円は画面中央に描かれる。

translate(20, 10); // (0 + 20, 0 + 10) で原点が (20, 10) にくる座標に変わる
translate(5, 6); // (20 + 5, 10 + 6) で原点が (25, 16) にくる座標に変わる

この関数は絶対的な移動ではなく、相対的な移動であることに注意が必要だ。今の原点の位置が (tx, ty)translate() に指定した位置が (nx, ny) だとすると、新たな原点の位置は (tx + nx, ty + ny) になる。

translate() はこの関数を実行した後の描画に影響を及ぼすので、すでに描かれているものが移動することはない。

座標の拡大縮小

scale() は座標の拡大縮小を行う関数だ。

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

  scale(2, 2);
  circle(100, 100, 60);
}

これまで位置や直径などを数値で指定してきたが、単位を指定する必要は無かった。これは座標の 1 がピクセルの1pxに対応しているからである。

たとえば座標 (20, 30) は原点から20px右に、30px下に移動した地点で、円の直径 10 は10pxのことである。scale() はこの 1 = 1px の比率を変更する関数だ。

デフォルトでは、x軸/y軸ごとに 1 = 1px の設定がされている。x軸の1px、y軸の1pxを (1, 1) と表現してみよう。ここで scale(2, 2) を適用する。

scale() は既存のものに掛け算の適用を行うので、scale(2, 2)(1, 1)x を2倍、y を2倍するという意味になる。つまり (1 * 2, 1 * 2)(2, 2) になる。

これで、横に1移動するのに2px、縦に1移動するのに2pxかかる座標になった。円の直径のサイズも影響を受け、たとえば 60 を指定すると、2倍の120pxで円が描画される。

scale(2, 3) のように指定すると縦横比が変わるので、たとえばこの状態で circle() を使用すると、縦長の楕円が描画される。

scale(2, 3); // 横に1移動するのに2px, 縦に1移動するのに3pxかかる
scale(10, 5); // 横に1移動するのに20px, 縦に1移動するのに15pxかかる

translate() と同じように、この関数も絶対的な変更ではなく、相対的な変更である。今の拡大比率が (sx, sy)scale() に指定した比率が (nx, ny) だとすると、新たな拡大比率は (sx * nx, sy * ny) になる。

scale() はこの関数を実行した後の描画に影響を及ぼすので、すでに描かれているものが拡大縮小することはない。

// 原点から見て、(100px, 100px)右下の位置に直径30pxの円が描画される
circle(100, 100, 30);
scale(2, 2);
// 原点から見て、(200px, 200px)右下の位置に直径60pxの円が描画される
circle(100, 100, 30);

scale() を利用すると、描くオブジェクトの拡大縮小ができるが、描画位置も拡大縮小されてしまう。大抵の場合は位置まで変更されると不便なので、translate() とうまく組み合わせて位置は固定にするのが普通だ。これについては後述する。

座標の回転

rotate() は座標の原点を軸に回転が行われる関数だ。

function setup() {
  createCanvas(windowWidth, windowHeight);
  stroke(240); // 線の色
  angleMode(DEGREES); // 角度の指定を一般的に使用されている角に変更

  rotate(45); // 座標を45度回転
  line(0, 0, width, 0);
}

デフォルトでは通常の座標なので、xの値が増えれば右に、yの値が増えれば下に移動するが、rotate() を使用することで、座標を回転させることができる。

rotate() に指定するのは座標を回転させるための角度なのだが、普段我々が一般的に使用している1周が360度の角ではなく、単位がラジアンという角を指定する必要がある。このラジアンについては後の章で扱うので、今回は1周が360度の角を指定したい。このために angleMode(DEGREES) を指定する。

この状態で rotate(45) と書くと、座標が時計回りに45度回転することになる。この座標だと我々から見て、 x の値が増えれば右下に、y の値が増えれば左下に移動することになる。

座標変換のリセット

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

  translate(width / 2, height / 2);
  circle(0, 0, 30);
  // ここで translate() の設定をもとに戻したい
}

これまで説明した3つの座標変換の関数の適用後に、もとの状態に戻したい場合があるので、その方法を幾つか挙げる。

座標変換のリセット方法 その1

function setup() {
  createCanvas(windowWidth, windowHeight);
  translate(width / 2, height / 2); // A
}

function draw() {
  // この時点で A の変換はリセットされている
}

draw() が呼び出される前に自動で座標変換の現在値がリセットされる。なので、上記のように setup() に座標変換の関数を設定しても draw() に入った時点ではリセットされている。これは draw() の1回目だけではなく、毎フレームリセットされる。

座標変換のリセット方法 その2

translate(11, 22);
scale(33, 44);
rotate(55, 66);
resetMatrix();
// translate, rotateが(0, 0), scaleが(1, 1)に戻る

2つ目の方法は translate(), scale(), rotate() の値を初期値に戻す resetMatrix() を使用することだ。すべて初期値に戻ってしまうが、具体的な現在値を知らなくても戻すことができる。

座標変換のリセット方法 その3

3つ目の方法は、初期値になるように足したり掛けたりすることだ。まずは、座標の初期値と計算方法を確認する。

  • translate() に関連する、座標の位置の初期値は (0, 0)
  • scale() に関連する、座標の拡大縮小の初期値は (1, 1)
  • rotate() に関連する、座標の角度の初期値は 0

で、

  • translate() の計算は足し算
  • scale() の計算は掛け算
  • rotate() の計算は足し算

になる。

なので、

  • translate() の現在値が (x, y) だった場合、(-x, -y) を指定すれば (x - x, y - y)(0, 0) に戻る
  • scale() の現在値が (x, y) だった場合、(1/x, 1/y) を指定すれば、(x * 1/x, y * 1/y)(1, 1) に戻る
  • rotate() の現在値が a だった場合、-a を指定すれば 0 に戻る

となる。

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

  const tx = width / 2;
  const ty = height / 2;
  const s = 2;
  const a = 45;

  angleMode(DEGREES);

  translate(tx, ty);
  translate(-tx, -ty);
  // この時点で座標の位置が (0, 0) に戻っている

  scale(s, s);
  scale(1 / s, 1 / s);
  // この時点で座標のスケールが (1, 1) に戻っている

  rotate(a);
  rotate(-a);
  // この時点で座標の角度が 0 に戻っている
}

座標変換のリセット方法 その4

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);

  rotate(30); // 座標の角度が30度になる
  push();
  rotate(10); // 座標の角度が40度になる
  pop(); // 座標の角度が40度に戻る
}

push() を呼び出すと、その時点での座標の位置、スケール、角度が保存される。pop() を呼び出すと、直前の push() に保存した座標情報を復元する。

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

  push(); // A
  push(); // B
  push(); // C
  pop(); // Cの復元
  pop(); // Bの復元
  pop(); // Aの復元
}

この push()pop() は、いわゆるスタックというデータ構造で成り立っており、後に入れたものを先に取り出すようになっている。pop() で復元をする際、その情報はスタックから削除される。

push()pop() を利用する際には注意が必要で、座標変換の3つの関数以外に設定した情報も保存と復元がされる。今まで利用してきた関数でいうと、fill(), noFill(), noStroke(), stroke(), strokeWeight(), imageMode(), resetMatrix() などが挙げられる。

push();
stroke(200);
fill(100);
circle(100, 100, 100);
pop();
// この時点で push() するときの状態に戻っているので、
// 上記の stroke() と fill() の影響は push() ~ pop() の中だけになる

話が逸れるが、これはうまく使えば便利な機能で、stroke()fill() のようなグローバルの設定を push()pop() で挟むことで、色の変更の影響を局所化することができる。

位置を変えずに拡大縮小させる

let s;

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

  s = 1;
}

function draw() {
  clear();
  translate(width / 2, height / 2);
  scale(s, s);
  circle(0, 0, 10);
  resetMatrix();

  s += 0.1;
}

前述したように、scale() での比率変更は位置も影響を受ける。ただし、唯一影響を受けない位置がある。それは (0, 0) だ。

1移動するのに何pxかかろうが移動しなければ 0 のままなので、位置を変えずに拡大縮小させるには (0, 0) に描画すればいい。scale() の前に translate() を使用して原点の位置を移動させておけば好きな位置で固定したまま拡大縮小ができる。

特定の位置を軸にして回転させる

rotate() による回転は原点を軸として回転が行われるので、translate() で原点が (x, y) になるように座標を移動してしまえば好きな位置を軸として回転させることができる。

let r;

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

  stroke(240);
  noFill();

  r = 0;
}

function draw() {
  clear();

  translate(width / 4, height / 2);
  rotate(r);
  rect(0, 0, 100, 100);
  resetMatrix();
  drawMarker(width / 4, height / 2);

  translate((width / 4) * 3, height / 2);
  rotate(r);
  rect(-50, -50, 100, 100);
  resetMatrix();
  drawMarker((width / 4) * 3, height / 2);

  r += 0.01;
}

// (x, y) を中心に十字マークを描く
function drawMarker(x, y) {
  line(x - 10, y, x + 10, y);
  line(x, y - 10, x, y + 10);
}

適用の順序

translate(), scale(), rotate() これら3つの関数は適用する順番が変わると表示結果も変わる。

座標変換を利用した作例

円を回転させても見た目が変わらないので矩形を利用した回転を作りたい。まずは矩形の描き方について解説する。

rect(x, y, width, height);

rect(x, y, width, height)(x, y)~(x + width, y + height) の領域に矩形を描画する。

rectMode(CENTER);

rectMode(CENTER) を呼び出しておくと、以降の rect()(x, y) が中央位置に変わる。

const n = 20;
let s = 0.9;
let a;

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  rectMode(CENTER);
  colorMode(HSB);
  strokeWeight(4);
  a = 0;
}

function draw() {
  clear();
  translate(width / 2, height / 2);

  // n個の矩形を描画する
  for (let i = 0; i < n; i++) {
    scale(s); // sが1未満なので呼び出すほど縮小していく
    rotate(a);
    fill(i * 20, 100, 100);
    rect(0, 0, 200, 200);
  }

  a += 0.5;
}

まず translate(width / 2, height / 2) で座標の原点を画面中央に持っていく。rectMode(CENTER) になっているので、(0, 0) に矩形を描画すると画面真ん中に表示されることになる。n 個の矩形が完全に重ならないように1未満の値 s を用意して scale() に指定する。作例だと s = 0.9 なので、最初の矩形サイズは0.9倍、次の矩形サイズは0.81倍とだんだん小さくなっていく。rotate()a を足していき同じような形にするが、動きを付けたいため、毎フレーム a の値を増やし、ベースとなる一番外側の矩形の角度が毎回変わるようにする。