こんにちは。エンジニアの辻です。
普段はフロントエンドの構築を担当している私ですが、1つ悩みがあります。
それはcanvasが苦手な事です。
canvasの何が難しいって、デバックしづらい事もさることながら、数学や物理学の知識がほぼ必須なところです。
学生時代が終わってはや数年。数学や物理学なんてもう完全に忘却の彼方です。
そこでcanvasへの苦手意識を矯正することも兼ねて、今回から不定期でcanvasを中心とした連載をはじめていきたいと思います。
記念すべき初回は、canvas上で線形変換を使ってボックスを回転させてみます。
線形変換とは?
線形変換とは『平面上の(x, y)が変換fによって、(x’, y’)に移動する。この時の関係式を以下のように定数項を含まない一次式で表せる場合、この移動を線形変換(1次変換)という』だそうです。
数式で表すと以下のようになります。
上記の式を計算してみると以下のようになります。
結果としてベクトルを得ることができます。
高校数学の行列の分野で出てきたヤツですね! なんとも懐かしいです!
今回は線形変換の中でも、サイン、コサインを使って反時計回りに移動させるものを使って、canvas上でボックスを動かしてみたいと思います。
反時計回りに移動させる線形変換の式はコチラです。
上記の式をJavaScriptで表すと下記のようになります。
1 2 3 4 5 6 7 |
const linearTransformation = function (target = {x: 0, y: 0}, radian) { // 変換対象の{x, y}に対して線形変換し、その結果を返却します。 return { x : target.x * Math.cos(radian) + target.y * -(Math.sin(radian)), y : target.x * Math.sin(radian) + target.y * Math.cos(radian), } } |
線形変換についてもっと知りたいという方はwikipediaをご覧ください。(丸投げ)
線型写像 線形変換行列の例 – wikipedia
canvas上で線形変換を使ってボックスを回転させてみる
今回の成果物
まず先に今回の成果物のスクリーンショットをどうぞ。
上図では画面中央を原点として画面いっぱいにcanvasを配置しています。
赤ボックスが初期配置のオブジェクトです。
原点からx軸に75、y軸に75の位置に配置しました。
赤ボックスはxとyの値が同じなので、原点とのなす角度は45°ですね。
この赤ボックスを線形変換を使って、反時計周りに45°移動させたのが青ボックス。
また、赤ボックスを反時計周りに135°移動させたのが緑ボックスです。
赤ボックスは原点から45°の位置にいるので、青ボックスの角度は90°。緑ボックスは180°の位置にいます。
図解すると以下のようになります。
ソースコードの解説
ソースコードは以下の通りです。構成自体はシンプルです。
index.htmlにcanvasの処理を書いたmain.jsを読み込んでいます。それだけです。
肝となるmain.jsの全文は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
// 画面の高さと幅 let width = 0; let height = 0; // canvas 要素 let canvas = null; let context = null; // 初期化処理 const init = function () { // 画面の高さを取得 width = window.innerWidth; height = window.innerHeight; // canvas を生成 canvas = document.getElementById('canvas'); context = canvas.getContext('2d'); canvas.width = width; canvas.height = height; // 原点を移動 context.translate(width / 2, height / 2); // y軸のプラス・マイナスを反転する context.scale(1, -1); // 背景を描画 context.fillStyle = '#0099cc'; context.fillRect(-(width / 2), -(height / 2), canvas.width, canvas.height); }; // 円を描く処理 const drawCircle = function (x, y, radius = 10, color = '#000000') { context.beginPath(); context.arc(x, y, radius, 0, Math.PI * 2, false); context.fillStyle = color; context.fill(); context.stroke(); }; // 線分を描く処理 const drawLine = function (startX, startY, endX, endY) { context.beginPath(); context.moveTo(startX, startY); context.lineTo(endX, endY); context.stroke(); }; // 四角形を描く処理 const drawRect = function (x, y, width = 20, height = 20, color = '#000000') { context.beginPath(); context.rect(x, y, width, height); context.fillStyle = color; context.fill(); context.stroke(); }; // 度をラジアンに変換する処理 const convertToRadian = function (degree) { return degree * (Math.PI / 180); }; // 画面の中心を原点として、target を線形変換(回転)し、変換後の位置を返却する const linearTransformation = function (target, degree) { // ラジアンを取得 const radian = convertToRadian(degree); // 線形変換後のオブジェクトを返却する return { x: target.x * Math.cos(radian) + target.y * -Math.sin(radian), y: target.x * Math.sin(radian) + target.y * Math.cos(radian), }; }; window.onload = function () { // 初期化処理 init(); // 原点を描画 drawCircle(0, 0, 4, '#000000'); // y軸を描画 drawLine(0, -(height / 2), 0, height / 2); // x軸を描画 drawLine(-(width / 2), 0, width / 2, 0); // 線形変換前の点オブジェクト const rect01 = { x: 75, y: 75, }; // ボックスの大きさ const boxSize = 20; // 線形変換前のオブジェクトを描画 drawRect(rect01.x - boxSize / 2, rect01.y - boxSize / 2, boxSize, boxSize, 'red'); // 角度45°で線形変換する const rect02 = linearTransformation(rect01, 45); // 線形変換後のオブジェクトを描画 drawRect(rect02.x - boxSize / 2, rect02.y - boxSize / 2, boxSize, boxSize, 'blue'); // 角度135°で線形変換する const rect03 = linearTransformation(rect01, 135); drawRect(rect03.x - boxSize / 2, rect03.y - boxSize / 2, boxSize, boxSize, 'green'); }; |
main.jsの処理の流れは、おおまかにこんな感じです。
- ページが読み込まれた後にinitを実行して、canvasを取得する
- canvasの原点を画面中央にし、y軸のプラス・マイナスを反転する
- x軸とy軸を描画する
- 線形変換前の点オブジェクトを用意して、赤ボックスを描画する
- 赤ボックスを角度45°で線形変換した青ボックスを描画する
- 赤ボックスを角度135°で線形変換した緑ボックスを描画する
では、main.jsの内容をピックアップして解説していきますね。
canvasの原点を画面中央に移動する
1 2 3 4 |
// 原点を移動 context.translate(width / 2, height / 2) // y軸のプラス・マイナスを反転 context.scale(1, -1) |
canvasを初期化した後に、translate()とscale()を使って、canvasの原点を画面中央へ移動しています。
canvasは特に何も設定しなければ、画面左上が原点となります。
そして、x軸は画面右方向へ行けばプラス。y軸は画面下方向へ行けばプラスになります。
この状態を数学でよく見る2次元平面図に変換するために、translate()とscale()を使っています。
まずtranslate()を使って、canvasを画面横幅の半分だけ水平方向右向きへ移動し、画面縦幅の半分だけ垂直方向下向きへ移動させます。
これで画面中央を原点に設定する事ができます。
次に、scale()を使ってy軸のプラス・マイナスを逆転させます。
scale()は要素を拡大・縮小する時に使いますが、引数にマイナスを入れることで要素を反転した上で拡大・縮小を行うことができます。
今回は第2引数に-1を入れているので、x軸に対して上下が反転して1倍の大きさとなります。
つまり、y軸のプラス・マイナスが反転した状態になります。
ちなみに、context.scale(-1, 1)とすると、x軸のプラス・マイナスが反転します。
原点と軸を描画
1 2 3 4 5 6 |
// 原点を描画 drawCircle(0, 0, 4, '#000000') // y軸を描画 drawLine(0, - (height / 2), 0, height / 2) // x軸を描画 drawLine(- (width / 2), 0, width / 2, 0) |
初期化した後は、原点と軸を描画しています。
画面中央に移動したので、原点はx = 0、y = 0 で描画できます。
x軸とy軸はそれぞれ画面の端から端まで線を描画しているだけですね。
度(°)をラジアンに変換する
1 2 3 4 |
// 度をラジアンに変換する処理 const convertToRadian = function (degree) { return degree * ( Math.PI / 180 ) } |
線形変換の処理を見ていく前に、度(°)をラジアン(rad)に変換する処理を見ていきます。
ラジアンとは角度を表す単位であり、円の半径と同じ長さの弧を切り取った時の円の中央の角が1ラジアンです。
半径が1の円を考えてみましょう。円周は直径 * πなので、2πになります。
したがって、円一周分(360°)のラジアンは2π[rad]となり、半円(180°)のラジアンはπ[rad]となります。
上記の関数は度を引数として受け取り、この180 : π[rad]の比率を使ってラジアンに変換して、結果を返しています。
線形変換で出てくるサインとコサインは、JavaScriptではMath.cos()、Math.sin()で取得できるのですが、引数にラジアンが必要になります。
なので、度からラジアンへの変換処理が必要になるわけですね。
Math.cos()や、Math.sin()、ラジアンを詳しく知りたい方は以下をご覧ください。
線形変換(回転)を行う
1 2 3 4 5 6 7 8 9 10 11 |
// 画面の中心を原点として、target を線形変換(回転)し、変換後の位置を返却する const linearTransformation = function (target, degree) { // ラジアンを取得 const radian = convertToRadian(degree); // 線形変換後のオブジェクトを返却する return { x: target.x * Math.cos(radian) + target.y * -Math.sin(radian), y: target.x * Math.sin(radian) + target.y * Math.cos(radian), }; }; |
さて、ようやく本命の登場です。
上記は線形変換(回転)を行う処理です。
まず引数として、変換対象のx、yを持ったオブジェクト(target)と、回転させる角度(degree)を受け取っています。
(※引数で受け取っているdegreeはラジアンではなく度です。)
次に受け取った度をラジアンに変換します。
radian = convertToRadian(degree)のところですね。
そして、ラジアンを変換行列のMath.cos()や、Math.sin()にいれて線形変換を行います。
2×2の行列と2×1の行列の積なので、最終的に2×1の行列(ベクトル)が算出されます。
この計算結果のxとyが、角度分だけ移動した座標となります。
あとは、計算結果を元にボックスを描画するだけですね。
まとめ
今回はシンプルな線形変換を使って、canvas上のボックスを反時計回りに回転させて移動させてみました。
こうしてcanvasを構築してみると、JavaScriptの練習にもなりますし、数学の勉強もできて一石二鳥ですね。
次回もcanvasを触っていきます!
よろしければ、次の記事「JavaScriptとcanvasと線形変換 その2 ~任意の点を中心にした回転編~」もご覧ください。