Chapter 16

三角関数

miku
miku
2021.11.25に更新

circle(x, y, 直径) のように座標の指定は xy を使用してきた。たとえば原点から右に30pxの座標ならば (30, 0) のようになる。では斜めに移動する場合はどうすればいいのだろう。斜めということは30のような距離以外に角度が必要である。しかし角度と距離が分かってていても circle(45度, 距離30, 直径) のように指定することはできない。あくまでも xy をうまく変化させることで斜めに移動する必要があるからだ。つまり、情報として存在するのが45度の方向に30進むということだが、実際にはそれに合わせた座標 (x, y) を知る必要がある。この章では、そのような計算をするために必要な三角関数について扱う。

三角関数

半径1の円を用意して、円の中央が原点になるように配置する。ここで、原点から角θの方向に長さ1だけ進んだ位置、つまり円周上にある点の座標(x, y)を求めたい。

進む距離(上記画像の青線)が 1 と固定なので、あとは角θの角度によって点の座標が決まる。具体的な点の座標を求めるには、三角関数のcos, sinという機能を利用する。

cos(θ) = cosθ = 角θ方向に1進んだときのx座標を計算する
sin(θ) = sinθ = 角θ方向に1進んだときのy座標を計算する

三角関数という名前の通り、cos, sinは関数で、角θを受け取ることで、その方向に1進んだときの位置を返す。x座標はcos、y座標はsinが計算する。

cos, sinに角θを指定したという表記は cos(θ), sin(θ) と書けるが、一般的には更に省略した cosθ, sinθ と記述する。

cos30° = 30度の方向に1進んだときのx座標を計算する
sin30° = 30度の方向に1進んだときのy座標を計算する

角度が具体的に決まっている場合、たとえば角度が30°だと、cos30°, sin30°と書く。

Math.cos();
Math.sin();

JavaScriptには cosθ, sinθ を計算する Math.cos(), Math.sin() が実装されている。引数には任意の角度を渡せばいいのだが、たとえば30度を指定する場合、Math.cos(30) のように記述することはできない。というのも、普段我々が使用している角度というのは0~360度の範囲である度数法というものだが、それとは別にラジアンという単位で角を表す弧度法で指定する必要があるからだ。

弧度法 / ラジアン

半径1の円を用意し、中心角が θ の弧を描く。このとき円周上にできる弧の長さは角θと一対一の対応になる。

たとえば30度でできる弧の長さを測ってみると約0.523598だが、逆にいうとその長さになる角は30度以外にない。なので、弧の長さ自体を角として扱ってしまおうというのが弧度法の考え方だ。単位はラジアンで、30度 = 約0.523598ラジアンになる。

Math.cos(0.523599); // 約30度の方向に1進んだときのx座標を計算する
Math.sin(0.523599); // 約30度の方向に1進んだときのy座標を計算する

cos()sin() には弧度法の角で指定しなければならないので、上記のように書けばいいのだが、30度や60度のような角度を弧度法の角に変換すると、ほとんどの場合きりのいい数にはならないので、とてもじゃないが覚えられない。なので、うまく指定できる方法を考えたいところだ。

まず、度数法の最大の角が360度であるように、弧度法でも最大の角がいくつになるのかを考える。最大の弧の長さは円周の長さで、円周を求める式は直径×円周率だ。半径が 1 の円で考えているので、直径は 2、つまり弧度法の最大の角は円周率の2倍ということになる。

円周率はたいていのプログラミング言語に標準で定義されており、JavaScriptだと Math.PI で参照できる。p5.jsにも PI で定義されている。

円周率の2倍が弧度法の最大の角なのでコードで書くと PI * 2 で表せる。つまり 360度 = PI * 2 だ。両辺を 2 で割ると 180度 = PI となり、これは覚えやすそうな対応だ。

PI が180度ということさえ覚えていれば、360度はその2倍なので PI * 2、90度は半分なので PI / 2、45度は1/4なので PI / 4 と導ける。

Math.cos(PI / 6); // 30度の方向に1進んだときのx座標を計算する
Math.sin(PI / 6); // 30度の方向に1進んだときのy座標を計算する

先程の30度の指定も PI / 6 と簡潔に指定ができる。

これら挙げた例は180の約数なのでうまくいったが、たとえば23度や45.678度のような角度が、ラジアンだといくつになるかを計算したい場合がある。

180度 = PI なのだから、両辺を180で割ると、1度 = PI / 180 となる。これがわかっていれば、23度は両辺を23倍して 23度 = 23 * PI / 180 、45.678度は 45.678度 = 45.678 * PI / 180 になる。つまり、n度をラジアンに変換するには n * PI / 180 という計算式になる。

ついでにラジアンから角度に変換する式も覚えておこう。180度 = PI の両辺を PI で割ると、 180度 / PI = 1 となる。この右辺の 1 は1ラジアンのことなので、nラジアンが何度か知りたい場合は、両辺に n をかければいい。つまり、nラジアンを角度に変換するには n * 180 / PI という計算式になる。

// 角度 -> ラジアン
function toRad(deg) {
  return (deg * Math.PI) / 180;
}

// ラジアン -> 角度
function toDeg(rad) {
  return (rad * 180) / Math.PI;
}

Math.cos(toRad(30)); // 30度の方向に1進んだときのx座標を計算する
toDeg(PI / 6); // 29.999...のような値が返るが、実質30のことである

この辺は関数化しておくと便利だ。

p5.jsでのcosθ, sinθの扱い

p5.jsではより cosθ, sinθ の扱いが楽になるような実装がされている。

cos(PI / 4);
sin(PI / 6);

円周率が Math.PI ではなく PI で参照できるように、cosθ, sinθcos(), sin() で参照できる。

// 角度からラジアンに変換
radians(30); // 0.523598...

// ラジアンから角度に変換 
degrees(PI / 6); // 29.999...

角度とラジアンを相互に変換する機能が関数として定義されている。

TWO_PI // PI * 2 (360°)
TAU // PI * 2 (360°)
HALF_PI // PI / 2 (90°)
QUARTER_PI // PI / 4 (45°)

PI 以外にも 45° / 90° / 360° に対応したラジアン値の定数が用意されている。

cos(PI / 4); // 0.7071067811865476

angleMode(DEGREES);
cos(45); // 0.7071067811865476

rotate(30); // 座標を30°回転する

angleMode(DEGREES) で、本来ラジアンを指定するところを角度で指定できる。座標変換の rotate() もその対象である。これはp5.jsの関数だけで、JavaScriptのビルトイン関数である Math.cos()Math.sin() には利用できない。

今後は基本的にp5.jsで定義されている三角関数を利用し、angleMode() を利用するかは用途によって変わる。

角θ方向に進む距離を可変にする場合

cosθ, sinθ の計算では、角θ方向に進む距離が 1 であるという制約があるが、これを任意の距離にしたい場合があるので、その場合の計算方法を考える。

(cosθ, sinθ) の位置を (x, y) として、(x, y), (x, 0), (0, 0)を結ぶと、直角三角形ができるのがわかる。

角θ方向に進む距離を可変にするということは、上記画像の青線の長さを変えるということだが、実際に青線の長さを変えた場合、同じように点を結んで、できる形はやはり直角三角形で、角θの角度は変わらない。

2つの三角形を用意して、2つの角が共通の場合、その2つの三角形は形が同じでスケールが違うだけの関係と証明することができ、2つの三角形は相似である、と呼ぶ。

上記画像にある青線の長さ1の三角形も、そこから長さを変えてできる三角形も、角θと直角が共通しているので、2つの三角形は相似である、ということがわかる。

相似であると、各辺の比は、辺の長さに関わらず常に同じになるという特徴がある。

青線の長さを可変にする、たとえば 1 から r に変えた場合、長さをr倍にするということなので、相似の三角形の辺である、緑線・赤線の長さもr倍になる。緑線・赤線はそれぞれ青線を引いた先の (x, y) に対応する。

なので、青線の長さ、つまり角θ方向に進む距離をr倍にすると、青線の先にある (x, y) が (x * r, y * r) になり、そもそも (x, y) は (cosθ, sinθ) のことなので、(cosθ * r, sinθ * r) で角θ方向に r 進んだときの位置を計算できる。

const r = 5; // 半径(青線の長さ)
const angle = 30; // 角度
angleMode(DEGREES); // ラジアンではなく角度で指定できるようにする
cos(angle) * r; // angle度の方向にr進んだときのx座標を計算する
sin(angle) * r; // angle度の方向にr進んだときのy座標を計算する

タンジェント(tan)

Math.tan(); // JavaScriptでの実装
tan(); // p5.jsでの実装(上のエイリアス)

原点から (cosθ, sinθ) までの直線の傾きを tanθ と表す。

直線の傾きというのは x が 1 進んだときに y がいくつ進むのかを表すものだ。今は、x方向に cosθ、y方向に sinθ 移動しているので、x が 1 のときは、y は sinθ / cosθ になる。つまり tanθ = sinθ / cosθ である。

アークタンジェント(atan/atan2)

Math.atan(); // JavaScriptでの実装
atan(); // p5.jsでの実装(上のエイリアス)

atan(x)tanθ = x となる θ を計算する。つまり直線の傾きを入れると θ を計算してくれる。

たとえば atan(1) の場合、傾きが 1 となる角度だから45度に応じたラジアン値 (0.7853...) が返却される。

この関数が返す値の範囲は角度で言うと-90度~90度までとなる。

というのも、円一周360度で考えると、傾きが 1 になる角度は、45度だけではなく225度でも同様だが、これだと atan() はどちらを返せばいいのかが分からなくなるので、180度の範囲で限定されているわけだ。

45度と225度を区別するように範囲を0~360度で考えたい場合は、代替となる atan2() という関数を使用する。

Math.atan2(); // JavaScriptでの実装
atan2(); // p5.jsでの実装(上のエイリアス)

atan2(y, x)tanθ = y / x となる θ を計算する。言い換えると、原点から (x, y) までを結んだ直線がx軸となす角度を計算する。

順番が (y, x) と y を先に指定するので注意が必要である。

atan() と違い、引数には傾きを求める計算前の (y, x) を指定するので、例えば傾きが45度で同じになる (1, 1) と (-1, -1) が区別できる。

タンジェントの逆関数の値を計算したい場合は atan() を、点の偏角を求めたい場合は atan2() を使用するといいだろう。

cos, sinの利用例

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

  angleMode(DEGREES);
  const r = 100;

  for (let angle = 0; angle < 360; angle += 10) {
    const x = cos(angle) * r;
    const y = sin(angle) * r;
    circle(x, y, 10);
  }
}

0度、10度、20度…と、角度を表す angle を固定の値で増やし、その方向に円を配置する作例。

cos, sinの利用例 その2

const r = 100;
const n = 15;

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

  translate(width / 2, height / 2);
  for (let i = 0; i < n; i++) {
    const angle = (360 / n) * i;
    const x = cos(angle) * r;
    const y = sin(angle) * r;
    circle(x, y, 5 + i * 5);
  }
}

先ほどの作例と同じ形だが、回転するたびに円の大きさを増やしていく作例。

cos, sinの利用例 その3

const r = 150;
const n = 60;

function setup() {
  createCanvas(windowWidth, windowHeight);
  angleMode(DEGREES);
  rectMode(CENTER);
  colorMode(HSB);

  translate(width / 2, height / 2);
  for (let i = 0; i < n; i++) {
    const angle = (360 / n) * i;
    let x = cos(angle) * r;
    let y = sin(angle) * r;

    x += random(-20, 20);
    y += random(-20, 20);

    fill(random(360), 100, 100);

    if (random([true, false])) {
      const d = random(20, 60);
      circle(x, y, d);
    } else {
      push();
      translate(x, y);
      rotate(random(360));
      const size = random(20, 60);
      rect(0, 0, size, size);
      pop();
    }
  }
}

回転する際に配置するオブジェクトを1/2の確率で円と矩形で分ける作例。

random() に配列を指定すると中の要素をランダムに一つだけ返すので、random([true, false]) で50%の確率判定に利用できる。

atan2の利用例

atan2() の機能を確認するために、矢印を描画してマウスの方向に向けるコードを書きたい。

画面中央の座標は (width / 2, height / 2) で、目標の座標は (mouseX, mouseY) だ。ここで画面中央の座標を (0, 0) にするために、目標の座標から画面中央の座標を引く。つまり (mouseX - width / 2, mouseY - height / 2) を計算する。計算した (x, y)atan2()y, x の順番に指定すれば角度が返ってくる。座標をずらしたとしても、得られる角度は変わらないので、これで画面中央からマウス位置までの角度を取得することができる。

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

function draw() {
  const rad = atan2(mouseY - height / 2, mouseX - width / 2);

  clear();

  stroke(240);
  strokeWeight(1);
  noFill();
  translate(width / 2, height / 2);
  rotate(rad);
  line(0, 0, 60, 0);
  line(60, 0, 50, -10);
  line(60, 0, 50, 10);
  resetMatrix();

  stroke(100);
  strokeWeight(2);
  noFill();
  line(0, mouseY, width, mouseY);
  line(mouseX, 0, mouseX, height);
}