✏️

JavaScript でジョイスティックもどきを作ってみよう!

2022/10/11に公開

こんにちは、株式会社palanxR事業部 でエンジニアをやっている 笹井 です。

概要

この動画にあるジョイスティックもどきを JavaScript で作っていきます!

必要な知識は三角関数ベクトルです!

一瞬「無理だ...」と思った皆さん、諦めないでください!

こういった実装は、
コードの意味を理解しようとするのではなく
手順だけを理解していく姿勢がオススメです!

いろんな用語が出てきますが、
このくらいフワッとした認識くらいがちょうど良いです!
内積: 求めておくと便利な値
外積: 求めておくと便利な値
コサイン: 角度を求めることができる値 & x成分を表現する値
サイン: 角度を求めることができる値 & y成分を表現する値

高校数学永遠赤点だった自分と一緒に頑張りましょう!

手順

1: ベクトル → 内積(dot) → コサイン → ラジアン → 角度を求める

分かっている値

求める値

1-1: 2つのベクトルから内積(dot)を求める

const v1 = { x: 0, y: 1 };
const v2 = { x: 1, y: 1 };
const dot = v1.x * v2.x + v1.y * v2.y;

1-2: 内積(dot)からコサインを求める

const absV1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
const absV2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
const cos = dot / (absV1 * absV2);

1-3: コサインからラジアンを求める

const radians = Math.acos(cos);

1-4: ラジアンから角度を求める

const degrees = Math.floor(radians * 180 / Math.PI);

1-5: でも...

ここまでで求めることができる角度の範囲は 0〜180度

ですが、サインを求めることで -180〜180度 = 360度 求めることができます!

次はその方法です!

2: ベクトル → 外積(cross) → サイン を求める

分かっている値

求める値

2-1: 2つのベクトルから外積(cross)を求める

const v1 = { x: 0, y: 1 };
const v2 = { x: 1, y: 1 };
const cross = v1.x * v2.y - v1.y * v2.x;

2-3: 外積(cross)からサインを求める

const absV1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
const absV2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
const sin = cross / (absV1 * absV2);

3: 組み合わせて正確な角度を求める

const radians = Math.acos(cos);
const oldDegrees = Math.floor(radians * 180 / Math.PI);
const newDegrees = sin > 0 ? oldDegrees: -oldDegrees;

実際のコード

// MEMO: コントローラーの中央の値
let centerX = uiControllerRect.left + uiController.clientWidth / 2;
let centerY = uiControllerRect.top + uiController.clientHeight / 2;

// MEMO: コントローラーの中央からどれだけ離れているかの値
let touchX = 
  Math.abs(event.touches[0].clientX - centerX) < 35 
    ? event.touches[0].clientX - centerX
    : event.touches[0].clientX - centerX > 0 ? 35 : -35;
let touchY = Math.abs(event.touches[0].clientY - centerY) < 35
    ? event.touches[0].clientY - centerY
    : event.touches[0].clientY - centerY > 0 ? 35 : -35;

// MEMO: 縦のベクトル
const v1 = { x: 0, y: -1 };

// MEMO: 操作中のベクトル
const v2 = { x: touchX, y: touchY };

// MEMO: 内積
let dot = v1.x * v2.x + v1.y * v2.y;

// MEMO: 外積
let cross = v1.x * v2.y - v1.y * v2.x;

// MEMO: 縦のベクトルの大きさ
let absV1 = Math.sqrt(v1.x * v1.x + v1.y * v1.y);

// MEMO: 操作中のベクトルの大きさ
let absV2 = Math.sqrt(v2.x * v2.x + v2.y * v2.y);

// MEMO: コサイン
let cos = dot / (absV1 * absV2);

// MEMO: サイン
let sin = cross / (absV1 * absV2);

// MEMO: コサインからラジアン
let radians = Math.acos(cos);

// MEMO: ラジアンからの角度
let degrees = Math.floor(radians * 180 / Math.PI);

uiControlerChild.style.left = `${touchX}px`;
uiControlerChild.style.top = `${touchY}px`;
uiInfo.innerText = `degree(角度): ${sin > 0 ? degrees: -degrees}
length(速度): ${absV2}`

参考

https://qiita.com/kitasenjudesign/items/3227c3a6e176b3d24d14
https://ngroku.com/?p=5086
https://toburau.hatenablog.jp/entry/20080305/1204703630
https://qiita.com/MinoDriven/items/6718b5e70e3fb321ff9b
https://atarimae.biz/archives/23642#i

Discussion