いのちの輝きくん eyes の作り方

#いのちの輝き君 eyes 
xeyes の~~パクリ~~オマージュとして「[いのちの輝きくん](https://logo.expo2025.or.jp/)」がマウスを見つめる物を作ろうと思い立った。
できあがったのは↓

■Windows
![InochiShine2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/e4d8c22d-947c-b9ec-c455-355f767adaea.gif)

■macOS
![ino.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/1c32489b-ac11-b70e-1a84-23ffc13e391d.gif)

形や目はランダム生成。

Delphi なので、この手のデスクトップマスコットを作るのは本当に簡単。
具体的には下記の記事を参照。

■FireMonkey で Window Drag
https://qiita.com/pik/items/907641319e4de4d74805

■Delphi + FireMonkey で TaskTray / StatusBar にアイコンとメニューを実装する
https://qiita.com/pik/items/bd3448e2cf0c4c528027

なので、それだけだったら新たに記事を書く必要は無かった！
つまり問題はそこじゃなく、**楕円の内部に収まるように目玉を動かす**部分！

# 幾何学をもう一度

やりたいことは、マウスカーソルの位置と楕円の接点を求め（図の赤丸部分）、そこに接するように目玉を配置する事

![eye1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/8b2b5002-67f0-c957-36c1-6d2a9d992fdd.png)

ここで、図中の要素に名前を付ける

![eye5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/1bf7c321-ee70-5f36-3d78-89195f84048f.png)

| 図中の名前 | 意味                  | 変数名             |
|------------|-----------------------|--------------------|
| 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 から求められる

```math
\theta = tan^{-1}(\frac{dy}{dx})
```

dx, dy は

```math
dx = mx - cx\\
```
```math
dy = my - cy\\
```
なので

```math
\theta = tan^{-1}(\frac{my - cy}{mx - cx})
```

となる。
これをコードに落とすとこう
([ArcTan2](http://docwiki.embarcadero.com/Libraries/Rio/ja/System.Math.ArcTan2) は分子と分母を別々に渡せる [ArcTan](http://docwiki.embarcadero.com/Libraries/Rio/ja/System.ArcTan) のバリエーション)

```pascal
var Theta := ArcTan2(AMouseY - FCenter.Y, AMouseX - FCenter.X);
```

## X, Y を求める
θが求まったので、x, y を求める。

### X を求める

この時、中心点 c で半径 ra の真円（下図の赤い線）を考える。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/09f73e1e-e645-0439-ae58-89e587d9d0e8.png)


図中の点 Q は、真円の円周上にある物とすると、線分 OQ の長さは ra となる（真円なので）

次に三角形 OQx の底辺 Ox の長さを求める。
ここでは、三角関数の cos を使う。

cos は三角関数の定義で

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/cc1e2cc7-6182-b61a-891d-fcf8f277be31.png)


上図の三角形の 

```math
\frac{b}{a} = cos(\theta)
```

と決められている。

ここでは、上図の三角形の底辺 b (=Ox) の長さを求めたい訳だから、両辺に a を掛けてやればよい。

```math
\begin{align}
\frac{b}{a} &= cos(\theta)\\
\\
\frac{b}{a} * a &= cos(\theta) * a\\
\\
b &= a * cos(\theta)
\end{align}
```

これを使って図の三角形 OQx の底辺 Ox の長さ（x座標）を表すと

```math
  x = ra * cos(\theta)
```

となる。

### Y を求める

y を求める方法は2つ。
x と同じように rb を半径とした真円を考える方法と、楕円の公式を解く方法がある。
せっかくなので、ここでは楕円の公式を使う方法を紹介する。

楕円の公式は

```math
\frac{x^2}{a^2} + \frac{y^2}{b^2} = 1
```

なので、これを y について解くと

```math
y = \frac{b}{a}\sqrt{a^2 - x^2}
```

となるので a に ra, b に rb, x に ra * cosθ を代入すると

```math
\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]↓より

```math
\sqrt{1 - cos(θ)^2} = sin(θ)
```

なので、

```math
y = rb * sin(θ)
```

となる。

総合すると

```math
x = ra * cos(\theta)
```
```math
y = rb * sin(\theta)
```

となり、これをコードに落とすと

```pascal
var X := FRA * Cos(Theta);
var Y := FRB * Sin(Theta);
```

こうなる。

ただ、このままだと目玉の半径が考慮されておらず、目玉がはみ出てしまうので、楕円のサイズを目玉の半径分だけ小さくして考える。

![eye3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/18291253-8932-97ef-e7bf-bd657204b9ec.png)

上記を考慮すると

```math
x = (ra - er) * cos(θ)
```

```math
y = (rb - er) * sin(θ)
```

```pascal
var X := (FRA - FEyeR) * Cos(Theta);
var Y := (FRB - FEyeR) * Sin(Theta);
```

となる。

## 座標系に合せる

ここまでで数学上は X, Y が求まっているが、目玉の座標系は Delphi FireMonkey の座標系（左上原点）に合せなければならない。
座標系に合せた座標を (x', y') とすると

![eye4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12977/f35e0508-1e99-b758-222a-331b12937afb.png)

図のようになり、x は c 点からの座標であったので座表原点 o に合せるため ra 分を加算し、図形は左上が原点になるので er 分を引く。
つまり、

```math
x' = x + ra - er
```

```math
y' = y + rb - er
```
となる。

# 目玉の座標計算コード

ここまでをコードで書くと

```pascal
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;
```
となり、更に式変形して

```pascal
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;
```

となる。
できあがったコードはスゴくシンプル！
こんなに短いのに目玉がちゃんとマウスを追いかける！

## ソースコード一式

https://bitbucket.org/freeonterminate/inochishine/src/master/

[Delphi 10.4 Sydney Release 1 (Delphi 10.4.1)](https://www.embarcadero.com/jp/products/rad-studio/whats-new-in-10-4-sydney) で制作。

# まとめ
最近ゲームを作っていなかったので幾何学計算に手間取った！
これで、xeyes もどきが量産されれば幸い。

[^1]: sin(θ)^2 + cos(θ)^2 = 1