2D平面で加法定理を使う

三角関数の加法定理をご存じでしょうか?

高校の数学で習うやつです。
検索しても、実際のプログラムでの使い方や有効点が見つかることは皆無に近い。
まるで、入学試験のためだけにあるようです。

図形の回転が連続するケースでは、角度を保持するプログラムがほとんどです。
しかし、こういったケースでは、sin, cos 値を保存しておいた方が、
if 文が少なくなり、不具合が少なくなったりします。

-------

ある矩形ABCDがあったとして、A点を固定にしてC点を動かす。
変化後の矩形AB’C’D’ を求める。
但し、矩形の縦横比(Aspect比)は変えないものとする。



矩形の拡大率(α)は、次式で求まる。

元の矩形の幅と高さをそれぞれ、w,hとし、変化後の幅と高さをw’,h’ とする。
また、回転角をθ とする。
A,C’ は既知であるので、B’ かD’ のいずれかがわかれば良い。

幾つかの方法が考えられるが、
単純なのは、拡大率(α)と回転角(θ) を求めて、次の行列に代入して計算する方法だろう。

この方法の欠点は、θ を求めると特定の象限において、符号の操作が必要になることだろう。
例えばatanでは、(-π/2 ≦ θ ≦ π/2)の範囲でしか、答えが求まらず、
acosでは(0 ≦ θ ≦ π)の範囲でしか求まらない。
C’ をA点を中心に回転させるとわかりやすい。
この方法は意外と間違いを犯しやすく、テストケースも増えるので、選択すべきではない。

A,C,C’ の座標がわかっているので、
ベクトルAC、AC’ の内積と外積からcosθ,sinθ を算出する。

内積の公式


外積の公式


今回のケースに当てはめ、cos,sinを解くと、


HTML5のcanvasクラスでは、Context.transformで行列演算が可能なので
行列式のパラメタ(cos,sin,-sin,cos)を指定すれば良い。

一度回転させて、後から更に回転を追加する場合を考える。
図では、C’ からC'’ に制御点が移動したとする。


まず前出の方法で、ベクトルAC’、AC'’ の内積と外積からcos(ε),sin(ε)を算出する。

既にθ 回転していて、更にε の回転が加わるとすると、加法定理により


拡大率も同様に、既に拡大率β であり、
これに拡大率α が加わるとすれば、βxα が合成拡大率になる。


更に発展させて、拡大縮小回転が断続的に連続発生するケースを考えてみます。



サンプルを実行するとわかりやすい。

操作としては、
A点をCanvas中央点として、クリック点をPとする。
    この時画面には、,龍觀舛表示されている。

その後、スワイプ操作でP’ まで移動し、ここで指を離す。
    この時画面には、△龍觀舛表示されている。

再び、Q点でクリックを行う。
    この時画面には、△龍觀舛表示されている。

スワイプでQ’ まで移動し指を離す。
    この時画面には、の矩形が表示されている。

実現方法はいろいろあるが、
スワイプOFF時に、その時点の拡大率(βxα)・回転状態(sin(θ+ε),cos(θ+ε))を保存しておき、
クリック時に、β=βxα, sin(θ)=sin(θ+ε), cos(θ)=cos(θ+ε)
として再開始すれば良い。

Javascriptでのソースコードは次のようになる。
<!DOCTYPE html>
<html>
<!-- 
// (c)Hundredsoft Corporation. 2013 All right reserved.
//
//	UTF-8で保存して下さい
-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>拡大縮小回転</title>

<script type="text/javascript">
new function() {
var m_img = new Image();
var m_ctx = null;
var m_O = null;
var m_baseX = 0;
var m_baseY = 0;
var m_cos = 1;
var m_sin = 0;
var m_prop = 1;
var m_Lcos = 1;
var m_Lsin = 0;
var m_Lprop = 1;

var mmove = function(x, y){
	// Click座標-中心 ベクトル
	var vx0 = m_baseX - m_O.x;
	var vy0 = m_baseY - m_O.y;
	var r0 = Math.sqrt(vx0*vx0 + vy0*vy0);

	// スワイプ座標-中心 ベクトル
	var vx1 = x - m_O.x;
	var vy1 = y - m_O.y;
	var r1 = Math.sqrt(vx1*vx1 + vy1*vy1);

	var cos = (vx0*vx1 + vy0*vy1) / (r0*r1); // 内積から
	var sin = (vx0*vy1 - vy0*vx1) / (r0*r1); // 外積から
	var prop = r1 / r0;
	draw(prop, cos, sin);
};
var draw = function(prop, cos, sin){
	m_Lcos = m_cos*cos - m_sin*sin; // 加法定理(cos)
	m_Lsin = m_sin*cos + m_cos*sin; // 加法定理(sin)
	m_Lprop = m_prop * prop;     // 拡大率の加法は乗じるだけ
	m_ctx.clearRect(0, 0, m_O.x*2, m_O.y*2);
	m_ctx.save();

	 // 行列計算
	m_ctx.transform(
		m_Lcos*m_Lprop,  m_Lsin*m_Lprop,
		-m_Lsin*m_Lprop, m_Lcos*m_Lprop,  m_O.x, m_O.y);

	// 先に画像中心を原点に移動(適用順に注意)
	m_ctx.transform(1, 0, 0, 1, -m_img.width/2, -m_img.height/2);

	m_ctx.drawImage(m_img, 0, 0);
	m_ctx.restore();
};

test1 = {
init: function(){
	var mouseDown = false;
	var canvas = document.getElementById('c');

	canvas.addEventListener('touchstart',
		function(e) {
			e.preventDefault();
			mouseDown = true;
			var n = e.touches.length;
			var rect=e.target.getBoundingClientRect();
			m_baseX = e.touches[n-1].pageX-rect.left;
			m_baseY = e.touches[n-1].pageY-rect.top;
			m_cos = m_Lcos;
			m_sin = m_Lsin;
			m_prop = m_Lprop;
		}, false);

	canvas.addEventListener('mousedown',
		function(e) {
			e.preventDefault();
			mouseDown = true;
			var rect=e.target.getBoundingClientRect();
			m_baseX = e.clientX-rect.left;
			m_baseY = e.clientY-rect.top;
			m_cos = m_Lcos;
			m_sin = m_Lsin;
			m_prop = m_Lprop;
		}, false);

	canvas.addEventListener('touchend',
		function(e) {mouseDown = false;}, false);

	canvas.addEventListener('mouseup',
		function(e) {mouseDown = false;}, false);

	canvas.addEventListener('touchmove',
		function (e) {
			e.preventDefault();
			var rect = e.target.getBoundingClientRect();
			if (mouseDown) {
				var n = e.touches.length;
				mmove(e.touches[n-1].pageX-rect.left,
					e.touches[n-1].pageY-rect.top);
			}
		}, false);

	canvas.addEventListener('mousemove',
		function (e) {
			e.preventDefault();
			var rect = e.target.getBoundingClientRect();
			if (mouseDown) {
				mmove(e.clientX-rect.left, e.clientY-rect.top);
			}
		}, false);

	m_img.onload = (function(){
		m_O = {x: canvas.width/2, y: canvas.height/2};
		m_ctx = canvas.getContext("2d");
		draw(1, 1 ,0);
	});
	m_img.width = 260;
	m_img.height = 390;
	m_img.src = "test.jpg";
}
}
};
</script>
</head>
<body onload="test1.init()">
<canvas id="c" width="640" height="640" style="background-color:#e0e0e0;">
</canvas>
</body>
</html>


意外と面倒な処理ですが、コードの通り、演算処理でif文は一つもありません。
この方法の利点は、メンバ変数の使用が少ない事と、θ を計算しない(三角関数の公式は使っていますが、 関数としてのsinやcos は使っていない。使っているのは加減乗除とルートのみ。)ことで、if 文を省ける事が挙げられます。


実行サンプルはこちら


IE9以降/Chrome/Safari/FF/Androidで、確認しています。




Tags: プログラムメモ
author : HUNDREDSOFT | - | -