#いのちの輝き君 eyes
xeyes のパクリオマージュとして「いのちの輝きくん」がマウスを見つめる物を作ろうと思い立った。
できあがったのは↓
形や目はランダム生成。
Delphi なので、この手のデスクトップマスコットを作るのは本当に簡単。
具体的には下記の記事を参照。
■FireMonkey で Window Drag
https://qiita.com/pik/items/907641319e4de4d74805
■Delphi + FireMonkey で TaskTray / StatusBar にアイコンとメニューを実装する
https://qiita.com/pik/items/bd3448e2cf0c4c528027
なので、それだけだったら新たに記事を書く必要は無かった!
つまり問題はそこじゃなく、楕円の内部に収まるように目玉を動かす部分!
幾何学をもう一度
やりたいことは、マウスカーソルの位置と楕円の接点を求め(図の赤丸部分)、そこに接するように目玉を配置する事
ここで、図中の要素に名前を付ける
図中の名前 | 意味 | 変数名 |
---|---|---|
c (cx, cy) | 中心点 | FCenterX, FCenterY |
m (mx, my) | マウスの座標 | AMouseX, AMouseY |
ra | 長軸の半分 | FRA |
rb | 短軸の半分 | FRB |
θ | 線分 c-m の角度 | Theta |
er | 目玉の半径 | FEyeR |
x, y | 線分 c-m と楕円の接点 | X, Y |
θを求める
接点 X, Y を求めるのは線分 m の傾きが解ればいいので、まずθを求める。
θは ArcTan を使って dy/dx から求められる
\theta = tan^{-1}(\frac{dy}{dx})
dx, dy は
dx = mx - cx\\
dy = my - cy\\
なので
\theta = tan^{-1}(\frac{my - cy}{mx - cx})
となる。
これをコードに落とすとこう
(ArcTan2 は分子と分母を別々に渡せる ArcTan のバリエーション)
var Theta := ArcTan2(AMouseY - FCenter.Y, AMouseX - FCenter.X);
X, Y を求める
θが求まったので、x, y を求める。
X を求める
この時、中心点 c で半径 ra の真円(下図の赤い線)を考える。
図中の点 Q は、真円の円周上にある物とすると、線分 OQ の長さは ra となる(真円なので)
次に三角形 OQx の底辺 Ox の長さを求める。
ここでは、三角関数の cos を使う。
cos は三角関数の定義で
上図の三角形の
\frac{b}{a} = cos(\theta)
と決められている。
ここでは、上図の三角形の底辺 b (=Ox) の長さを求めたい訳だから、両辺に a を掛けてやればよい。
\begin{align}
\frac{b}{a} &= cos(\theta)\\
\\
\frac{b}{a} * a &= cos(\theta) * a\\
\\
b &= a * cos(\theta)
\end{align}
これを使って図の三角形 OQx の底辺 Ox の長さ(x座標)を表すと
x = ra * cos(\theta)
となる。
Y を求める
y を求める方法は2つ。
x と同じように rb を半径とした真円を考える方法と、楕円の公式を解く方法がある。
せっかくなので、ここでは楕円の公式を使う方法を紹介する。
楕円の公式は
\frac{x^2}{a^2} + \frac{y^2}{b^2} = 1
なので、これを y について解くと
y = \frac{b}{a}\sqrt{a^2 - x^2}
となるので a に ra, b に rb, x に ra * cosθ を代入すると
\begin{align}
y &= \frac{rb}{ra}\sqrt{ra^2 - (ra * cos(θ))^2}\\
\\
& = \frac{rb}{ra}\sqrt{ra^2 - ra^2 * cos(θ)^2}\\
\\
& = \frac{rb}{ra}ra\sqrt{1 - cos(θ)^2}\\
\\
& = rb\sqrt{1 - cos(θ)^2}\\
\end{align}
こうなる。
ここで三角関数の定義1↓より
\sqrt{1 - cos(θ)^2} = sin(θ)
なので、
y = rb * sin(θ)
となる。
総合すると
x = ra * cos(\theta)
y = rb * sin(\theta)
となり、これをコードに落とすと
var X := FRA * Cos(Theta);
var Y := FRB * Sin(Theta);
こうなる。
ただ、このままだと目玉の半径が考慮されておらず、目玉がはみ出てしまうので、楕円のサイズを目玉の半径分だけ小さくして考える。
上記を考慮すると
x = (ra - er) * cos(θ)
y = (rb - er) * sin(θ)
var X := (FRA - FEyeR) * Cos(Theta);
var Y := (FRB - FEyeR) * Sin(Theta);
となる。
座標系に合せる
ここまでで数学上は X, Y が求まっているが、目玉の座標系は Delphi FireMonkey の座標系(左上原点)に合せなければならない。
座標系に合せた座標を (x', y') とすると
図のようになり、x は c 点からの座標であったので座表原点 o に合せるため ra 分を加算し、図形は左上が原点になるので er 分を引く。
つまり、
x' = x + ra - er
y' = y + rb - er
となる。
目玉の座標計算コード
ここまでをコードで書くと
procedure TExpoCell.SetEyePos(const AMouseX, AMouseY: Single);
begin
var Theta := ArcTan2(AMouseY - FCenter.Y, AMouseX - FCenter.X);
var X := (FRA - FEyeR) * Cos(Theta);
var Y := (FRB - FEyeR) * Sin(Theta);
X := X + FRA - FEyeR;
Y := Y + FRB - FEyeR;
FEye.SetBounds(X, Y, FEyeD, FEyeD); // FEyeD は目玉の直径
end;
となり、更に式変形して
procedure TExpoCell.SetEyePos(const AMouseX, AMouseY: Single);
begin
var Theta := ArcTan2(AMouseY - FCenter.Y, AMouseX - FCenter.X);
var X := (FRA - FEyeR) * (Cos(Theta) + 1);
var Y := (FRB - FEyeR) * (Sin(Theta) + 1);
FEye.SetBounds(X, Y, FEyeD, FEyeD);
end;
となる。
できあがったコードはスゴくシンプル!
こんなに短いのに目玉がちゃんとマウスを追いかける!
ソースコード一式
Delphi 10.4 Sydney Release 1 (Delphi 10.4.1) で制作。
まとめ
最近ゲームを作っていなかったので幾何学計算に手間取った!
これで、xeyes もどきが量産されれば幸い。
-
sin(θ)^2 + cos(θ)^2 = 1 ↩