クモのようにコツコツと

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

【React & p5.js】「三角関数しらべMath」を作った(三角関数の理解を深めるアプリ)

Reactアプリの続きです。前回のスケール・プレイヤーまではReactとtone.jsを連携して音楽系のアプリを作っていました。今回はReactとp5.jsを連携し三角関数の単位円の交点をグリングリンと動かしました。グラフィック、モーション、3Dの制作に多用される三角関数の理解を深めるために作りました。それではいきましょう!

【目次】

※参考:前回記事
【React & Tone.js】スケールプレイヤーを作った(いろいろなスケールを調べて音も聴けるアプリ) - クモのようにコツコツと

※参考:【React】ReactでWebアプリを作るシリーズまとめ
qiita.com

作ったもの

p5.jsの三角関数系のメソッドを使って単位円の交点を動かす(左上のテキストは計算結果)
f:id:idr_zz:20211230174215j:plain

角度のスライダーを変えると単位円の交点も動く。
f:id:idr_zz:20211230174218j:plain


画面の下部にはJSのMathオブジェクトの三角関数系のメソッドの計算結果が表示される。
f:id:idr_zz:20211230174220j:plain

こちらも角度のスライダーを変えると計算結果の値が変わる。 f:id:idr_zz:20211230174222j:plain


作ったアプリがこちら

※参考:三角関数しらべMath

アプリの使い方

※参考:このアプリについて | 三角関数しらべMath

ソースコード

※参考:GitHub - ryo-i/sankaku-kansu-shirabe-math


下記のような用途に活用できる。

  • 角度ごとの三角関数の計算結果を単位円グラフで視覚的に確認できる
  • JS(Mathオブジェクト)の三角関数系の組み込み関数による計算結果を確認できる
  • 単位円グラフに用いるp5.jsの関数とJS組み込み関数の計算結果を比較できる
  • 三角関数の主要な公式とJS組み込み関数の計算結果を比較できる
  • 三角関数の公式の根拠となる主要な証明式を参照できる

このアプリで実現したかったこと

以前、JSの3DライブラリThree.jsを触ってみた時、三角関数が使われていた。当時、言葉の意味を調べたが何をやっているのかはあまり理解できていなかった。

※参考:【Three.js】カメラ制御を実現している「三角関数」を理解する!(サイン、コサイン、タンジェント) - クモのようにコツコツと

その後、p5.jsのモーション作品でも三角関数系が多用されていることがわかった。

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


グラフィック、モーション、3Dを制作するにあたり三角関数の理解の必要性を感じ、数学の書籍や動画、ネット記事で勉強した。

※参考:三角関数について調べたこと · Issue #1 · ryo-i/sankaku-kansu-shirabe-math · GitHub

それによって得られた知見を形にする目的で本アプリを作った。

  • 角度を変化させたときにリアルタイムで三角関数の計算を行い、その結果をテキストやp5.jsの単位円で表現したい
  • JSのMathオブジェクトに用意されている三角関数系のメソッドとp5.jsのメソッドとの比較も行いたい
  • 数学の本に書いてある三角関数系の公式の計算結果とJS関数の値の比較も行いたい
  • 単位円は動的に動かせるようにp5.jsで描画したい
  • p5.jsがReact環境で動き、JSXのinputタグの値と同期できるかも確かめたい

p5.jsをReact環境で動かしたいのでいつものNextスターターキットをベースにした。

※参考:GitHub - ryo-i/next-app-started

Reactとp5.jsの連携

React内でのp5.jsの連携で試したこと

※参考:p5.jsをインポート、canvasのリサイズ、値の受け渡し · Issue #3 · ryo-i/sankaku-kansu-shirabe-math · GitHub

p5.jsのインストール

まずはp5.jsのインストール

$ npm i p5

※参考:p5 - npm

TypeScript向けのp5.jsの@typesもあったのでインストール

$ npm i @types/p5

※参考:@types/p5 - npm

React内でp5.jsをインポート

importから先のReactでの連携方法が公式ドキュメントだと見当たらない(下記のQ & Aくらい?)

※参考:How do I import p5 via npm? - Libraries - Processing Foundation

その他の参考にした記事

※参考:VS Code & TypeScriptとp5.jsで始めるモダンなクリエイティブコーディング入門 - ICS MEDIA
※参考:React+TypeScriptではじめるp5.jsチュートリアル - Qiita
※参考:webpackに入門する - p5.jsの開発環境作り | Chanomic Blog
※参考:p5.jsのExamplesをReactで写経してみた

Next環境ではwindow not definedエラーになった。next/dynamicとの併用が必要なようだった。

※参考:"window not defined" error in NextJS app · Issue #47 · P5-wrapper/react · GitHub
※参考:p5.js×Next.js×TypeScriptを動かしてみる - Qiita
※参考:Advanced Features: Dynamic Import | Next.js


なお、p5.jsのcanvasタグを任意のタグの中に生成するにはparentを使う方法もある(next環境ではうまくいかなかった)

※参考:reference | p5.js
※参考:【p5.js】canvasタグを任意の場所に表示させる - 元Webデザイナー兼コーダーの備忘録
※参考:p5.jsをReact+Typescriptのコンポーネント内で扱う | 株式会社NiMiL
※参考:p5.js & React & TypeScript

Reactとp5.js間の値の受け渡し

さらにReactとp5.js間の相互の値の受け渡しを試しした。

値の受け渡しには「react-p5-wrapper」パッケージを利用するのが一番良さそうだった。

※参考:[連載]Reactとp5.jsでクソかっこいいサイトを作ろう!!(part 1) - Qiita
※参考:p5.jsをReactのコンポーネントとして実行する方法 | bauhausify
※参考:reactでp5jsも使ってさらにリアルタイムデータベースも使いたい1|bkc|note

インストールコマンド

npm i react-p5-wrapper

※参考:react-tabs - npm

p5.jsからReactに値を渡す方法は苦戦したが下記のQ & Aがとても参考になった!

※参考:javascript - Passing value from p5 sketch to React (with react-p5-wrapper) - Stack Overflow

ソースコード

上記の挙動を実現しているソースコードはこちら

※参考:GitHub - ryo-i/sankaku-kansu-shirabe-math

unitCircleモジュール

p5.jsの描画を行なっているモジュール。Reactから値を受け取っている。

※参考:sankaku-kansu-shirabe-math/unitCircle.ts at main · ryo-i/sankaku-kansu-shirabe-math · GitHub


react-p5-wrapperからP5Instanceをインポート

import { P5Instance } from 'react-p5-wrapper'

unitCircle()関数を定義

const unitCircle = (p5: P5Instance) => {
    let angle: number = 0;
    // 中略
};

引数はp5と命名したP5Instance。変数angleは角度で初期値は0

react環境ではP5Instanceの引数p5をp5メソッドの頭につければメソッドが動くようになる。


以下、unitCircle()の中の処理

setup()メソッド(ページ読み込み時に実行)

    p5.setup = (): void => {
        const windowWidth: number = p5.windowWidth;
        // console.log('windowWidth', windowWidth);

        if (windowWidth < 500) {
            p5.createCanvas(windowWidth -60, windowWidth -60);
        } else {
            p5.createCanvas(500, 500);
        }
    };

windowWidth()メソッドでブラウザの幅を取得

※参考:reference | p5.js

createCanvas()メソッドでcanvasサイズを設定

※参考:reference | p5.js

ブラウザ幅が500以上ならブラウザ幅より60px狭い正方形。それ以外は500pxの正方形に。


updateWithProps()メソッド

    p5.updateWithProps = (props): void => {
        if (props.angle || props.angle === 0) {
            angle = props.angle;
            // console.log('angle', angle);
        }
    };

updateWithProps()はP5-wrapperのメソッド

※参考:GitHub - P5-wrapper/react: A wrapper component that allows you to utilise P5 sketches within React apps.

後述するReactのJSXのpropsから角度の値を受け取り、変数angleに渡している。


draw()メソッド(常に実行)

    p5.draw = (): void => {
    // 中略
    };

以下、draw()メソッドの中身。下にいくほど後から処理されるので描画のレイヤー的には上に重なる。


まず単位円の描画に用いる各種サイズの変数を定義

        // サイズ設定
        const canvasSize: number = p5.width;
        const canvasHalfSize: number = canvasSize * 0.5;
        const radiusRatio: number = 0.8;
        const radiusSize: number = canvasHalfSize * radiusRatio;
        const pointRatio: number = 0.03;
        const pointSize: number = canvasSize * pointRatio;
        const memorySize: number = 5;

三角関数の定義

        // 三角関数
        const radian: number = angle * (p5.PI / 180);
        const sin: number = p5.sin(radian);
        const cos: number = p5.cos(radian);
        const tan: number = p5.tan(radian);

radianangleと円周率PIからラジアンを定義。さらにラジアンからsin()cos()tan()で三角関数を定義。

※参考:reference | p5.js
※参考:reference | p5.js
※参考:reference | p5.js
※参考:reference | p5.js


単位円描画の初期設定。

        // 初期設定
        p5.fill('#000');
        p5.stroke('#000');
        p5.strokeWeight(1);
        p5.background('#eee');

fill()は面の色、stroke()は線の色、strokeWeight()は線の太さ、background()は背景色。

※参考:reference | p5.js
※参考:reference | p5.js
※参考:reference | p5.js
※参考:reference | p5.js


単位円の描画

        // 単位円
        p5.stroke('#666');
        p5.noFill();
        p5.ellipse(
            canvasHalfSize,
            canvasHalfSize,
            canvasSize * radiusRatio,
            canvasSize * radiusRatio
        );

noFill()は面の色なし、ellipse()は円

※参考:reference | p5.js
※参考:reference | p5.js


直角三角形の描画

        // 三角形
        p5.noStroke();
        p5.fill('#fff');
        p5.triangle(
            canvasHalfSize,
            canvasHalfSize,
            canvasHalfSize + (cos * radiusSize),
            canvasHalfSize - (sin * radiusSize),
            canvasHalfSize + (cos * radiusSize),
            canvasHalfSize,
        );

noStroke()は線なし、triangle()は三角形

※参考:reference | p5.js
※参考:reference | p5.js


X軸、Y軸の十字線の描画

        // X軸、Y軸の十字線
        p5.stroke('#666');
        p5.line(0, canvasHalfSize, canvasSize, canvasHalfSize);
        p5.line(canvasHalfSize, 0,  canvasHalfSize, canvasSize);

line()は線

※参考:reference | p5.js


X座標、Y座標の描画

        // X座標
        p5.stroke('#aaa');
        p5.line(
            canvasHalfSize + (cos * radiusSize),
            canvasHalfSize,
            canvasHalfSize + (cos * radiusSize),
            canvasHalfSize - (sin * radiusSize)
        );

        // Y座標
        p5.line(
            canvasHalfSize,
            canvasHalfSize - (sin * radiusSize),
            canvasHalfSize + (cos * radiusSize),
            canvasHalfSize - (sin * radiusSize)
        );

半径の半分(0.5)のメモリの描画

        // 半径の半分(0.5)のメモリ
        p5.stroke('#666');
        p5.line(
            canvasHalfSize + (radiusSize * 0.5),
            canvasHalfSize - memorySize,
            canvasHalfSize + (radiusSize * 0.5),
            canvasHalfSize + memorySize,
        );
        p5.line(
            canvasHalfSize - memorySize,
            canvasHalfSize - (radiusSize * 0.5),
            canvasHalfSize + memorySize,
            canvasHalfSize - (radiusSize * 0.5),
        );
        p5.line(
            canvasHalfSize - (radiusSize * 0.5),
            canvasHalfSize - memorySize,
            canvasHalfSize - (radiusSize * 0.5),
            canvasHalfSize + memorySize,
        );
        p5.line(
            canvasHalfSize - memorySize,
            canvasHalfSize + (radiusSize * 0.5),
            canvasHalfSize + memorySize,
            canvasHalfSize + (radiusSize * 0.5),
        );

単位円の半径の線の描画

        // 半径
        p5.stroke('#999');
        p5.strokeWeight(3);
        p5.line(
            canvasHalfSize,
            canvasHalfSize,
            canvasHalfSize + (cos * radiusSize),
            canvasHalfSize - (sin * radiusSize)
        );

X軸、Y軸が交わる原点の描画

        // 原点
        p5.fill('#A63744');
        p5.noStroke();
        p5.ellipse(
            canvasHalfSize,
            canvasHalfSize,
            pointSize,
            pointSize
        );

単位円と半径が交わる交点の描画

        // 交点
        p5.ellipse(
            canvasHalfSize + (cos * radiusSize),
            canvasHalfSize - (sin * radiusSize),
            pointSize,
            pointSize
        );

単位円の左上に表示しているテキストの描画

        // テキスト
        p5.textSize(14);
        p5.noStroke();
        p5.fill('#000');
        p5.text('X', canvasSize - 20, canvasHalfSize - 5);
        p5.text('Y', canvasHalfSize + 5, 20);
        p5.text('θ: ' + angle, 5, 20);
        p5.text('rad: ' + radian.toFixed(4), 5, 40);
        p5.text('sin: ' + sin.toFixed(4), 5, 60);
        p5.text('cos: ' + cos.toFixed(4), 5, 80);
        p5.text('tan: ' + tan.toFixed(4), 5, 100);

textSize()は文字サイズ、text()は文字

※参考:reference | p5.js
※参考:reference | p5.js

toFixed()で小数4桁で四捨五入している

※参考:Number.prototype.toFixed() - JavaScript | MDN

以上がdraw()メソッドの中身


windowResized()メソッドでブラウザがリサイズされた時にcanvasサイズを変更

    p5.windowResized = () => {
        const windowWidth: number = p5.windowWidth;
        // console.log('windowWidth', windowWidth);

        if (windowWidth < 600) {
            p5.resizeCanvas(windowWidth -60, windowWidth -60);
        }
    };

※参考:reference | p5.js

以上がunitCircle()の中の処理


最後にunitCircleモジュールをエクスポート

export default unitCircle;

Inner_newコンポーネント

Reactのメイン部分。unitCircleモジュールも読み込んでp5.jsのcanvasタグと連携している。

※参考:sankaku-kansu-shirabe-math/Inner_new.tsx at main · ryo-i/sankaku-kansu-shirabe-math · GitHub


各種パッケージをインポート

import React, { useState, useEffect }  from 'react';
import styled from 'styled-components';
import { P5WrapperProps } from 'react-p5-wrapper';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import sketch from '../modules/sketch/unitCircle';
import dynamic from "next/dynamic";

react-p5-wrapperからP5WrapperPropsをインポート

また、今回もページが長くなったため、タブメニューを追加するために「react-tabs」をインストールした

npm i react-tabs

※参考:react-tabs - npm

sketchの名前でunitCircleモジュールをインスポート

最後に、Next環境でP5WrapperPropsを動かすにはnext/dynamicが必要だったのでインポート

※参考:Advanced Features: Dynamic Import | Next.js


ReactP5Wrapperdynamic()で'react-p5-wrapper'を実行

const ReactP5Wrapper = dynamic(() => import('react-p5-wrapper')
    .then(mod => mod.ReactP5Wrapper as any), {
    ssr: false
}) as unknown as React.NamedExoticComponent<P5WrapperProps>

p5.jsの処理で使っているdocumentdynamic()ssrを無効にしないとエラーになった。

※参考:p5.js×Next.js×TypeScriptを動かしてみる - Qiita


styled-componentsでCSS in JS設定

// CSS in JS
const UnitCircle = styled.figure`
  canvas {
    border-radius: 10px;
    margin: 0 auto;
    display: block;
  }
`;
 // 以下略
  }
`;

Inner()コンポーネント(このファイルの処理本体)

// Component
function Inner() {
// 中略
}

以下、Inner()コンポーネント内の処理


フック初期設定、angleは角度、radはラジアン、sincostanが三角関数

  // Hooks
  const [angle, setAngle] = useState(30);
  const [rad, setRad] = useState(0);
  const [sin, setSin] = useState(0);
  const [cos, setCos] = useState(0);
  const [tan, setTan] = useState(0);

角度の初期値は30でそれ以外は0


円周率の設定

  // 円周率
  let pi:number = Math.PI;

Math.PIプロパティ

※参考:Math.PI - JavaScript | MDN


角度からラジアンの変換

  // 角度→ラジアン
  const angle2radian = (angle: number): void => {
    let getRadian: number = angle * (pi / 180);
    // getRadian = Number(getRadian.toFixed(4));
    setRad(getRadian);
  };

円周率を使って角度からラジアンを算出。先ほどのp5.jsと同じ計算式。


ラジアンから三角比を返すJS関数の結果をフックにセットする。

  // 三角比
  const trigonometricRatio = (radian: number): void => {
    setSin(Math.sin(radian));
    setCos(Math.cos(radian));
    setTan(Math.tan(radian));
  }

Math.sin()がサイン、Math.cos()がコサイン、Math.tan()がタンジェント

※参考:Math.sin() - JavaScript | MDN
※参考:Math.cos() - JavaScript | MDN
※参考:Math.tan() - JavaScript | MDN


角度変更

  // 角度変更
  const changeAngle = (e: React.ChangeEvent<HTMLInputElement>): void => {
    let getValue: number = Number(e.target.value);

    if (getValue > 360) {
      getValue = 360;
    } else if (getValue < -360) {
      getValue = -360;
    }

    setAngle(getValue);
  };
  • 後述するJSXのinputタグから角度の値を受け取る
  • 360度を超えた場合は360度、-360度を超えた場合は-360度で固定
  • 角度をフックに登録する

コサインのプラマイチェック(三角比の相互関係の計算で使う設定)

  // cosのプラマイチェック
  const checkCos = (): number => {
    let cos: number;
    if ((90 < angle && angle < 270) || (-270 < angle && angle < -90)) {
      cos = -(Math.sqrt(1 - Math.pow(sin, 2)));
    } else {
      cos = Math.sqrt(1 - Math.pow(sin, 2));
    }
    return cos;
  };

コサインが90度〜270度または-270度〜-90度の範囲の場合は計算結果をマイナスに、それ以外はプラスにする

Math.sqrt()は平方根、Math.pow()は二乗

※参考:Math.sqrt() - JavaScript | MDN
※参考:Math.pow() - JavaScript | MDN


サインのプラマイチェック(三角比の相互関係の計算で使う設定)

  // sinのプラマイチェック
  const checkSin = (): number => {
    let sin: number;
    if ((180 < angle && angle < 360) || (-180 < angle && angle < 0)) {
      sin = -(Math.sqrt(1 - Math.pow(cos, 2)));
    } else {
      sin = Math.sqrt(1 - Math.pow(cos, 2));
    }
    return sin;
  };

上記のコサインのプラマイチェックと同じだが角度の範囲が180〜360度または-180度〜0度


useEffect()で実行する初期設定

  // 初期設定
  useEffect(() => {
    angle2radian(angle);
    trigonometricRatio(rad);
  });

上記のangle2radian()関数とtrigonometricRatio()関数を実行


ここからはJSXタグの設定

  // JSX
  return (
    <>
    // 中略
    </>
  );

以下、JSXの中身


UnitCircleコンポーネントでp5.jsの単位円を表示

        <UnitCircle>
          <dl>
            <dt>単位円(半径r = 1)</dt>
          </dl>
          <dd>
            <ReactP5Wrapper
              sketch={sketch}
              angle={angle}
            />
          </dd>
        </UnitCircle>
  • ReactP5Wrapperコンポーネントにp5.jsのcanvas要素を配置
  • sketch属性でsketchモジュールを読み込む
  • angle属性はpropsで値をangleフックに

Resultコンポーネントに単位円の下のinputタグやテキスト(JSのMathオブジェクトの計算結果など)

        <Result>
          <Tabs>
          <dl>
          // 中略
          </dl>
          </Tabs>
        </Result>

タブの表示切り替えをするのでreact-tabsのTabsコンポーネントを設定

以下、Resultモジュールの中身


角度変更のinputタグ

            <dt>角度(θ)</dt>
            <dd>
              <input type="number" value={angle} min="-360" max="360" onChange={changeAngle} />度(-360度〜360度)
              <input type="range" name="hue" value={angle} min="-360" max="360" onChange={changeAngle} />
            </dd>
  • type="number"のvalueをangleに、範囲は-360〜360、onChangeでchangeAngle()関数を実行
  • type="range"のvalueをangleに、範囲は-360〜360、onChangeでchangeAngle()関数を実行

テキストの直接入力、スライダー、両方の変更ができるようにした。


タブメニューの設定

            <menu>
              <TabList>
                <Tab>ラジアン</Tab>
                <Tab>三角比の関数</Tab>
                <Tab>三角比の算出</Tab>
                <Tab>三平方の定理</Tab>
                <Tab>三角比の相互関係</Tab>
                <Tab>sin → cos, tan</Tab>
                <Tab>cos → sin, tan</Tab>
                <Tab>tan → sin, cos</Tab>
                <Tab>逆三角関数</Tab>
                <Tab>双曲線関数</Tab>
              </TabList>
            </menu>

react-tabsのタブ設定。表示範囲は下記のTabPanelに対応している


ラジアン(rad)

            <TabPanel>
              <dt>ラジアン(rad)</dt>
              <dd>
                三角関数の角度は度数法ではなく弧度法(ラジアン)を用いる。<br />
              </dd>
              <dt>円周率(Math.PI)</dt>
              <dd>
                円周率(π)の値を返すJS関数
                <span>Math.PI = {pi}</span>
              </dd>
              <dt>度数→ラジアンを算出</dt>
              <dd>
                円周率を使って度数からラジアンを算出できる<br />
                θ * (π / 180) = rad
                <span>
                  <ul>
                    <li>θ * (Math.PI / 180) = {rad}</li>
                    <li>{angle} * (Math.PI / 180) = {rad}</li>
                  </ul>
                </span>
              </dd>
              <dt>ラジアン→度数を算出</dt>
              <dd>
                逆算でラジアンから度数を算出できる<br />
                rad / (π / 180) = θ
                <span>
                  <ul>
                    <li>rad / (Math.PI / 180) = {rad / (pi / 180)}</li>
                    <li>{rad} / (Math.PI / 180) = {rad / (pi / 180)}</li>
                  </ul>
                </span>
                <details>
                  <summary>証明</summary>
                  <ul>
                    <li>半径rと同じ長さの弧 = 1rad<br />
                    (通常は単位radは省略する)</li>
                    <li>円周の長さ = 2πrなので<br />
                      360度のrad = 2π</li>
                    <li>2で割ると<br />
                      180度のrad = π </li>
                    <li>さらに180で割ると<br />
                      1度あたりのrad = π / 180</li>
                    <li>角度(θ)をかけて度数からラジアンを算出<br />
                      θ * (π / 180) = rad</li>
                    <li>逆算でラジアンから度数を算出<br />
                      θ * (π / 180) = rad<br />
                      rad = θ * (π / 180)<br />
                      rad / (π / 180) = θ
                    </li>
                  </ul>
                </details>
              </dd>
            </TabPanel>
  • 円周率Math.PIを使って角度angleの度数をラジアンに変換している
  • 逆算で円周率Math.PIを使ってラジアンradの度数を度数に変換している

三角比のJS関数

            <TabPanel>
              <dt>三角比のJS関数</dt>
              <dd>
                ラジアンから三角比を算出するJSの関数
              </dd>
              <dt>サイン(Math.sin())</dt>
              <dd>
                Y座標(直角三角形の高さ)
                <span>
                  <ul>
                    <li>Math.sin(rad) = {sin}</li>
                    <li>Math.sin({rad}) = {sin}</li>
                  </ul>
                </span>
              </dd>
              <dt>コサイン(Math.cos())</dt>
              <dd>
                X座標(直角三角形の底辺)
                <span>
                  <ul>
                    <li>Math.cos(rad) = {cos}</li>
                    <li>Math.cos({rad}) = {cos}</li>
                  </ul>
                </span>
              </dd>
              <dt>タンジェント(Math.tan())</dt>
              <dd>
                X座標とY座標の傾き
                <span>
                  <ul>
                    <li>Math.tan(rad) = {tan}</li>
                    <li>Math.tan({rad}) = {tan}</li>
                  </ul>
                </span>
              </dd>
            </TabPanel>

ラジアンradを引数に入れてMath.sin()でサイン、Math.cos()でコサイン、Math.tan()でタンジェントを算出

サインは直角三角形の高さ、コサインは直角三角形の底辺、タンジェントは傾きになる。


三角比の算出

            <TabPanel>
              <dt>三角比の算出</dt>
              <dd>
                三角比 = 直角三角形の2辺の長さの比率<br />
                ※斜辺の長さは単位円の半径rなので1
              </dd>
              <dt>サインの算出</dt>
              <dd>
                高さ(sin) / 斜辺(1) = サイン
                <span>{sin} / 1  = {sin / 1}</span>
              </dd>
              <dt>コサインの算出</dt>
              <dd>
                底辺(cos) / 斜辺(1) = コサイン
                <span>{cos} / 1 = {cos / 1}</span>
              </dd>
              <dt>タンジェントの算出</dt>
              <dd>
                高さ(sin) / 底辺(cos) = タンジェント
                <span>{sin} / {cos} = {sin / cos}</span>
                ※<b>三角比の相互関係の公式①</b>でもある<br />
                tan θ = sin θ / cos θ
              </dd>
            </TabPanel>
  • 直角三角形の高さ、底辺、斜辺の中の2辺の比率からもサイン、コサイン、タンジェントが算出できる
  • 先ほどのJS関数(Math.sin(), Math.cos(), Math.tan())と誤差はあるもののほぼ同じ結果になることがわかる
  • タンジェントは三角比の相互関係の公式①でもある

三平方の定理

            <TabPanel>
              <dt>三平方の定理</dt>
              <dd>
                直角三角形の3辺の長さは下記の関係になる<br />
                底辺(cos)<sup>2</sup> + 高さ(sin)<sup>2</sup> = 斜辺(1)<sup>2</sup>
                <span>
                  <ul>
                    <li>(Math.pow({cos}, 2)) + (Math.pow({sin}, 2)) = {(Math.pow(cos, 2)) + (Math.pow(sin, 2))}</li>
                    <li>{(Math.pow(cos, 2))} + {(Math.pow(sin, 2))} = {(Math.pow(cos, 2)) + (Math.pow(sin, 2))}</li>
                  </ul>
                </span>
                ※<b>三角比の相互関係の公式②</b>でもある<br />
                sin<sup>2</sup>θ + cos<sup>2</sup>θ = 1<br />
                <details>
                  <summary>証明(正方形を用いた証明)</summary>
                  <figure><img src="img/sanheiho.jpg" alt="三平方の定理の証明" /></figure>
                  <ul>
                    <li>
                      一辺 = 底辺 + 高さの大正方形の中に<br />
                      一辺 = 斜辺の小正方形がある
                    </li>
                    <li>
                      大正方形の面積は以下の2つといえる<br />
                      大正方形の面積 = (底辺 + 高さ)<sup>2</sup><br />
                      大正方形の面積 = 三角形4つの面積 + 小正方形の面積
                    </li>
                    <li>
                      三角形4つの面積 = (底辺 * 高さ / 2) * 4<br />
                      三角形4つの面積 = 2 * 底辺 * 高さ
                    </li>
                    <li>小正方形の面積 = 斜辺<sup>2</sup></li>
                    <li>
                      代入すると<br />
                      (底辺 + 高さ)<sup>2</sup> = (2 * 底辺 * 高さ) + 斜辺<sup>2</sup><br />
                      底辺<sup>2</sup> + (2 * 底辺 * 高さ) + 高さ<sup>2</sup> = (2 * 底辺 * 高さ) + 斜辺<sup>2</sup><br />
                      底辺<sup>2</sup> + 高さ<sup>2</sup> = 斜辺<sup>2</sup>
                    </li>
                  </ul>
                </details>
              </dd>
            </TabPanel>
  • 直角三角形の底辺の二乗と高さの二乗を足すと斜辺の二乗になる
  • 実際に計算すると結果は誤差はあるもののほぼ斜辺の二乗(=1)になることがわかる
  • 三角比の相互関係の公式②でもある

三角比の相互関係

            <TabPanel>
              <dt>三角比の相互関係</dt>
              <dd>
                三角比の相互関係の公式③<br />
                1 + tan<sup>2</sup>θ = 1 / cos<sup>2</sup>θ
                <span>
                  <ul>
                    <li>1 + Math.pow({tan}, 2) = 1 / Math.pow({cos}, 2)</li>
                    <li>1 + {Math.pow(tan, 2)} = 1 / {Math.pow(cos, 2)}</li>
                    <li>{1 + Math.pow(tan, 2)} = {1 / Math.pow(cos, 2)}</li>
                  </ul>
                </span>
                公式①、②の組み合わせで求まる公式<br />
                ※公式①は<b>タンジェントの算出式</b>、公式②は<b>三平方の定理</b>
                <details>
                <summary>証明</summary>
                  <ul>
                    <li>
                      公式②より<br />
                      sin<sup>2</sup>θ + cos<sup>2</sup>θ = 1
                    </li>
                    <li>
                      両辺をcos<sup>2</sup>θで割る<br />
                      (sin<sup>2</sup>θ / cos<sup>2</sup>θ) + (cos<sup>2</sup>θ / cos<sup>2</sup>θ) = 1 / cos<sup>2</sup>θ
                    </li>
                    <li>約分すると<br />
                      (sin θ / cos θ)<sup>2</sup> + 1 = 1 / cos<sup>2</sup>θ
                    </li>
                    <li>
                      公式①よりタンジェントを代入<br />
                      tan<sup>2</sup>θ + 1 = 1 / cos<sup>2</sup>θ
                    </li>
                  </ul>
                </details>
              </dd>
            </TabPanel>
  • 三角比の相互関係の公式①(=タンジェントの公式)、公式②(=三平方の定理)から求まる公式③
  • 右辺と左辺は常に等しくなる

三角比の相互公式を使うと三角比のうちの1つだけがわかると残りの2つも算出できる。


サインからコサイン、タンジェントを算出

            <TabPanel>
              <dt>サインからコサイン、タンジェントを算出</dt>
              <dd>
                サインだけわかっている場合
                <span>sin = {sin}</span>
                公式②からコサインを算出
                <ul>
                  <li>sin<sup>2</sup>θ + cos<sup>2</sup>θ = 1</li>
                  <li>cos<sup>2</sup>θ = 1 - sin<sup>2</sup>θ</li>
                </ul>
                <span>
                  <ul>
                    <li>cos<sup>2</sup> = 1 - Math.pow({sin}, 2)</li>
                    <li>cos<sup>2</sup> = 1 - {Math.pow(sin, 2)}</li>
                    <li>cos<sup>2</sup> = {1 - Math.pow(sin, 2)}</li>
                    <li>cos = Math.sqrt({1 - Math.pow(sin, 2)})</li>
                    <li>cos = {Math.sqrt(1 - Math.pow(sin, 2))}</li>
                    <li>角度が90度〜270度(-270度〜-90度)の場合はマイナス<br />
                    cos = {checkCos()} ({angle}度)</li>
                  </ul>
                </span>
                公式①からタンジェントを算出<br />
                tan θ = sin θ / cos θ
                <span>
                  <ul>
                    <li>tan = {sin} / {checkCos()} </li>
                    <li>tan = {sin / checkCos()} </li>
                  </ul>
                </span>
              </dd>
            </TabPanel>
  • 三角比の相互関係の公式①、②を使ってサインからコサイン、タンジェントを算出
  • ただしコサインの角度が90度〜270度(-270度〜-90度)の場合はマイナスにする必要があるためcheckCos()関数でチェック

コサインからサイン、タンジェントを算出

            <TabPanel>
              <dt>コサインからサイン、タンジェントを算出</dt>
              <dd>
                コサインだけわかっている場合
                <span>cos = {cos}</span>
                公式②からサインを算出
                <ul>
                  <li>sin<sup>2</sup>θ + cos<sup>2</sup>θ = 1</li>
                  <li>sin<sup>2</sup>θ = 1 - cos<sup>2</sup>θ</li>
                </ul>
                <span>
                  <ul>
                    <li>sin<sup>2</sup> = 1 - Math.pow({cos}, 2)</li>
                    <li>sin<sup>2</sup> = 1 - {Math.pow(cos, 2)}</li>
                    <li>sin<sup>2</sup> = {1 - Math.pow(cos, 2)}</li>
                    <li>sin = Math.sqrt({1 - Math.pow(cos, 2)})</li>
                    <li>sin = {Math.sqrt(1 - Math.pow(cos, 2))}</li>
                    <li>角度が180度〜360度(0度〜-180度)の場合はマイナス<br />
                    sin = {checkSin()} ({angle}度)</li>
                  </ul>
                </span>
                公式①からタンジェントを算出<br />
                tan θ = sin θ / cos θ
                <span>
                  <ul>
                    <li>tan = {checkSin()} / {cos} </li>
                    <li>tan = {checkSin() / cos} </li>
                  </ul>
                </span>
              </dd>
            </TabPanel>
  • 三角比の相互関係の公式①、②を使ってコサインからサイン、タンジェントを算出
  • ただしサインの角度が180度〜360度(0度〜-180度)の場合はマイナスにする必要があるためcheckSin()関数でチェック

タンジェントからサイン、コサインを算出

            <TabPanel>
              <dt>タンジェントからサイン、コサインを算出</dt>
              <dd>
                タンジェントだけわかっている場合
                <span>tan = {tan}</span>
                公式③からコサインを算出
                <ul>
                  <li>1 + tan<sup>2</sup>θ = 1 / cos<sup>2</sup>θ</li>
                  <li>1 / cos<sup>2</sup>θ = 1 + tan<sup>2</sup>θ</li>
                  <li>cos<sup>2</sup>θ = 1 / 1 + tan<sup>2</sup>θ</li>
                </ul>
                <span>
                  <ul>
                    <li>cos<sup>2</sup> = 1 / 1 + Math.pow({tan}, 2)</li>
                    <li>cos<sup>2</sup> = 1 / 1 + {Math.pow(tan, 2)}</li>
                    <li>cos<sup>2</sup> = 1 / {1 + Math.pow(tan, 2)}</li>
                    <li>cos = Math.sqrt(1) / Math.sqrt({1 + Math.pow(tan, 2)})</li>
                    <li>cos = {Math.sqrt(1)} / {Math.sqrt(1 + Math.pow(tan, 2))}</li>
                    <li>cos = {Math.sqrt(1) / Math.sqrt(1 + Math.pow(tan, 2))}</li>
                    <li>角度が90度〜270度(-270度〜-90度)の場合はマイナス<br />
                    cos = {checkCos()} ({angle}度)</li>
                  </ul>
                </span>
                公式①からサインを算出<br />
                <ul>
                  <li>tan θ = sin θ / cos θ</li>
                  <li>sin θ / cos θ = tan θ</li>
                  <li>sin θ = tan θ * cos θ</li>
                </ul>
                <span>
                  <ul>
                    <li>sin = {tan} * {checkCos()}</li>
                    <li>sin = {tan * checkCos()}</li>
                  </ul>
                </span>
              </dd>
            </TabPanel>
  • 三角比の相互関係の公式①、③を使ってタンジェントからサイン、コサインを算出
  • ただしコサインの角度が90度〜270度(-270度〜-90度)の場合はマイナスにする必要があるためcheckCos()関数でチェック

逆三角関数のJS関数

            <TabPanel>
              <dt>逆三角関数のJS関数</dt>
              <dd>三角比からラジアンを算出するJS関数<br />
              ※0度〜90度まではすべて同じ結果で、他はatan2()の0〜180度と-180度〜0度がラジアンと一致する</dd>
              <dt>アークタンジェント2(Math.atan2())</dt>
              <dd>
                Y座標, X座標からラジアンを算出<br />
                単位円ではY座標 = sin, x座標 = cos
                <span>
                  <ul>
                    <li>Math.atan2(sin, cos) = {Math.atan2(sin, cos)}</li>
                    <li>Math.atan2({sin}, {cos}) = {Math.atan2(sin, cos)}</li>
                  </ul>
                </span>
              </dd>
              <dt>アークサイン(Math.asin())</dt>
              <dd>
                サインからラジアンを算出
                <span>
                  <ul>
                    <li>Math.asin(sin) = {Math.asin(sin)}</li>
                    <li>Math.asin({sin}) = {Math.asin(sin)}</li>
                  </ul>
                </span>
              </dd>
              <dt>アークコサイン(Math.acos())</dt>
              <dd>
                コサインからラジアンを算出
                <span>
                  <ul>
                    <li>Math.acos(cos) = {Math.acos(cos)}</li>
                    <li>Math.acos({cos}) = {Math.acos(cos)}</li>
                  </ul>
                </span>
              </dd>
              <dt>アークタンジェント(Math.atan())</dt>
              <dd>
                タンジェントからラジアンを算出
                <span>
                  <ul>
                    <li>Math.atan(tan) = {Math.atan(tan)}</li>
                    <li>Math.atan({tan}) = {Math.atan(tan)}</li>
                  </ul>
                </span>
              </dd>
            </TabPanel>

Math.atan2()がアークタンジェント2、Math.asin()がアークサイン、Math.acos()がアークコサイン、Math.atan()がアークタンジェント、

※参考:Math.atan2() - JavaScript | MDN
※参考:Math.asin() - JavaScript | MDN
※参考:Math.acos() - JavaScript | MDN
※参考:Math.atan() - JavaScript | MDN

逆三角関数の手計算は微分を用いた計算量が多い証明式のため、このJS関数を使うのが便利と思われる。

なお、0〜90度まではどの逆三角関数もラジアンと一致するがそれ以外の角度はMath.atan2()が360度(0〜180度と-180度〜0度)をカバーしている。

また、Math.atan2()はX座標、Y座標を直接引数に入れることでラジアンを算出することができるのでその意味でもMath.atan2()が一番広い用途で使えそうに感じた。


双曲線関数のJS関数

            <TabPanel>
              <dt>双曲線関数のJS関数</dt>
              <dd>三角比から双曲線関数を算出するJS関数</dd>
              <dt>ハイパーボリックサイン(Math.sinh())</dt>
              <dd>
                <span>
                  <ul>
                    <li>Math.sinh(sin) = {Math.sinh(sin)}</li>
                    <li>Math.sinh({sin}) = {Math.sinh(sin)}</li>
                  </ul>
                </span>
              </dd>
              <dt>ハイパーボリックコサイン(Math.cosh())</dt>
              <dd>
                <span>
                  <ul>
                    <li>Math.cosh(cos) = {Math.cosh(cos)}</li>
                    <li>Math.cosh({cos}) = {Math.cosh(cos)}</li>
                  </ul>
                </span>
              </dd>
              <dt>ハイパーボリックタンジェント(Math.tanh())</dt>
              <dd>
                <span>
                  <ul>
                    <li>Math.tanh(tan) = {Math.tanh(tan)}</li>
                    <li>Math.tanh({tan}) = {Math.tanh(tan)}</li>
                  </ul>
                </span>
              </dd>
              <dt>ハイパーボリックアークサイン(Math.asinh())</dt>
              <dd>
                <span>
                  <ul>
                    <li>Math.asinh(sin) = {Math.asinh(sin)}</li>
                    <li>Math.asinh({sin}) = {Math.asinh(sin)}</li>
                  </ul>
                </span>
              </dd>
              <dt>ハイパーボリックアークコサイン(Math.acosh())</dt>
              <dd>
                <span>
                  <ul>
                    <li>Math.acosh(cos) = {Math.acosh(cos)}</li>
                    <li>Math.acosh({cos}) = {Math.acosh(cos)}</li>
                  </ul>
                </span>
              </dd>
              <dt>ハイパーボリックアークタンジェント(Math.atanh())</dt>
              <dd>
                <span>
                  <ul>
                    <li>Math.atanh(tan) = {Math.atanh(tan)}</li>
                    <li>Math.atanh({tan}) = {Math.atanh(tan)}</li>
                  </ul>
                </span>
              </dd>
            </TabPanel>
  • ハイパーボリックサイン(Math.sinh())
  • ハイパーボリックコサイン(Math.cosh())
  • ハイパーボリックタンジェント(Math.tanh())
  • ハイパーボリックアークサイン(Math.asinh())
  • ハイパーボリックアークコサイン(Math.acosh())
  • ハイパーボリックアークタンジェント(Math.atanh())

※参考:Math.sinh() - JavaScript | MDN
※参考:Math.cosh() - JavaScript | MDN
※参考:Math.tanh() - JavaScript | MDN
※参考:Math.asinh() - JavaScript | MDN
※参考:Math.acosh() - JavaScript | MDN
※参考:Math.atanh() - JavaScript | MDN

これまでのどの計算結果とも一致せず、用途をよく理解できてない。角度によって結果がどう変化するかを知るために追加した。

以上、Inner()コンポーネント内の処理


最後にInnerコンポーネントをエクスポート

export default Inner;

最後に

ということで三角関数の理解を深めるためのアプリを作りました。今回はモーションとしては単位円の交点を回転させるのみです。JSのMathオブジェクトで行なった計算も三角関数の基礎的な公式のみです。

今回これを作ることで、初めてゼロから自力で三角関数を使ったモーション作りを経験できました。また、本の知識では理解しづらかった三角関数の公式などを、リアルタイムの計算を見ることでより体感的に理解することができました。

なお、三角関数は他にも正弦定理、余弦定理、加法定理、三角関数/逆三角関数の微分などいろいろな概念や公式があります。

※参考:三角関数について調べたこと · Issue #1 · ryo-i/sankaku-kansu-shirabe-math · GitHub

しかし今の自分にはこれらすべてを理解するのは難しいです。今後、実際に何かを作るときに必要性を感じた時点で、少しずつ理解を深めていきたく思います。(例えば下記のような三角関数の記事も今回のアプリを作る前と後では実際に理解度が変わりました)

※参考:三角関数は何に使えるのか 〜 サイン・コサイン・タンジェントの活躍 〜 - Qiita


これが2021年最後のブログ更新になります。来年からは三角関数をつかってもっといろいろなモーション作りをしていきたく思います。それではまた!


※参考:【React】ReactでWebアプリを作るシリーズまとめ
qiita.com