Files
m20_monitor_web/static/js/virtual-joystick.js

232 lines
7.8 KiB
JavaScript
Raw Permalink Normal View History

2025-11-01 01:11:35 +08:00
window.customElements.define('virtual-joystick', class VirtualJoystick extends HTMLElement {
static #style = `
:host {
--radius: 65px;
--size: calc(var(--radius) * 2);
}
:host,
slot {
position: relative;
display: block;
width: var(--size);
height: var(--size);
touch-action: none;
}
slot {
--x: var(--radius);
--y: var(--radius);
border: 1px solid gray;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
box-sizing: border-box;
transition: opacity 1s, background-color .2s;
&[part*="active"] {
background-color: rgba(255, 255, 255, 0.6);
&:after {
background-color: rgba(255, 255, 255, 0.8);
transition: transform .1s, background-color 0.2s;
}
}
}
slot:after,
slot:before {
content: "";
display: block;
position: absolute;
box-sizing: border-box;
border-radius: 50%;
width: 50px;
height: 50px;
}
slot:after {
border: 1px solid gray;
background-color: rgba(255, 255, 255, 0.5);
transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y)));
transition: transform .4s, background-color .2s;
}
slot:before {
border: 1px solid rgb(139, 139, 139);
transform: translate(calc(-50% + var(--radius)), calc(-50% + var(--radius)));
}
[part*="dynamic"] {
opacity: 0;
}
[part*="active"] {
opacity: 1;
}
[part*="box"]:after {
transform: translate(calc(-50% + clamp(0px, var(--x), var(--size))), calc(-50% + clamp(0px, var(--y), var(--size))));
}
`
static #getDir = (degree) => {
const dirs = ['ne', 'n', 'nw', 'w', 'sw', 's', 'se'];
const acute = 45;
let threshold = 22.5;
for (let dir of dirs) {
if (degree >= threshold && degree < (threshold += acute)) {
return dir;
}
}
return 'e';
}
static #getUniqueDir(a = '', b = '') {
let dir = '';
if (a.includes(b[0]) === false) {
dir = b[0];
}
if (b[1] && a.includes(b[1]) === false) {
dir += b[1];
}
return dir;
}
#setXY(x, y) {
this.#element.style.setProperty('--x', `${x}px`);
this.#element.style.setProperty('--y', `${y}px`);
};
#calcCrow({ clientX, clientY }) {
const { lock } = this.dataset;
this.#rect = this.#element.getBoundingClientRect();
const dx = lock === 'x' ? this.#r : clientX - this.#rect.left;
const dy = lock === 'y' ? this.#r : clientY - this.#rect.top;
const dxr = dx - this.#r;
const dyr = dy - this.#r;
const hypot = Math.hypot(dxr, dyr);
this.#crow = { dx, dy, dxr, dyr, hypot };
}
#log({
degree = 0,
force = 0,
radian = 0,
distance = 0,
direction = '',
hypot = 0,
capture = '',
release = '',
x = this.#rect.width + this.#rect.left,
y = this.#rect.top + this.#rect.top,
}) {
Object.assign(
this.dataset,
{ degree, force, radian, distance, direction, hypot, capture, release, x, y }
);
}
#isInside(event) {
const { clientX, clientY } = event;
const {
left: x,
top: y,
width: w,
height: h
} = this.dataset.mode ? this.getBoundingClientRect() : this.#rect;
const inside = clientX >= x && clientX <= x + w && clientY >= y && clientY <= y + h;
return inside;
}
#r = 0
#element = null
#rect = null
#crow = null
#pointers = []
constructor() {
super();
let output = {};
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>${VirtualJoystick.#style}</style>
<slot part="joystick"></slot>
`;
this.#element = this.shadowRoot.lastElementChild;
if (this.dataset.mode === 'semi' || this.dataset.mode === 'dynamic') {
this.#element.part.add('dynamic');
output = { x: 0, y: 0 };
}
if (this.dataset.shape) {
this.#element.part.add('box');
}
this.#rect = this.#element.getBoundingClientRect();
this.#r = this.#rect.width / 2;
this.#log(output);
}
connectedCallback() {
document.addEventListener('pointerdown', this.#start);
document.addEventListener('pointermove', this.#move);
document.addEventListener('pointerup', this.#up);
}
#start = (event) => {
const { clientX, clientY } = event;
const attachEvents = () => {
this.#pointers.push(event.pointerId);
this.#element.part.add('active');
this.#bind(event);
this.dispatchEvent(new CustomEvent('joystickdown'));
};
if (this.#pointers.length && this.dataset.mode !== 'fixed') {
return;
}
this.#rect = this.#element.getBoundingClientRect();
if (this.#isInside(event)) {
if (this.dataset.mode) {
if (this.dataset.mode !== 'fixed') {
this.dataset.mode === 'semi' && this.#element.part.remove('dynamic');
const { top, left } = this.getBoundingClientRect();
this.#element.style.left = `${clientX - left - this.#r}px`;
this.#element.style.top = `${clientY - top - this.#r}px`;
}
this.#calcCrow(event);
return attachEvents();
}
this.#calcCrow(event);
if (this.#crow.hypot <= this.#r || this.dataset.shape) {
attachEvents();
}
}
}
#move = (event) => {
if (this.#pointers.at(-1) === event.pointerId) {
this.#calcCrow(event);
this.#bind(event);
this.dispatchEvent(new CustomEvent('joystickmove'));
}
}
#bind = () => {
const { dx, dy, dxr, dyr, hypot } = this.#crow;
const r = this.#r;
const angle = Math.atan2(dyr, dxr);
let degree = angle * 180 / Math.PI;
let x = dx;
let y = dy;
const force = hypot / r;
if (!this.dataset.shape && hypot > r) {
x = r * Math.cos(angle) + r;
y = r * Math.sin(angle) + r;
}
degree = (degree > 0 ? 360 : 0) - degree;
const direction = + this.dataset.threshold > force ? '' : VirtualJoystick.#getDir(degree);
this.#log({
hypot,
degree,
force,
direction,
capture: VirtualJoystick.#getUniqueDir(this.dataset.direction, direction),
release: VirtualJoystick.#getUniqueDir(direction, this.dataset.direction),
x: x + this.#rect.left,
y: y + this.#rect.top,
radian: (angle > 0 ? 2 * Math.PI : 0) - angle,
distance: Math.min(hypot, r),
});
this.#setXY(x, y);
};
#up = (event) => {
if (this.#pointers.at(-1) === event.pointerId) {
this.#pointers.pop();
this.#element.part.remove('active');
this.#log({ release: this.dataset.direction });
this.#setXY(this.#r, this.#r);
this.dispatchEvent(new CustomEvent('joystickup'));
}
const pointerIndex = this.#pointers.indexOf(event.pointerId);
if (pointerIndex !== -1) {
this.#pointers.splice(pointerIndex, 1);
}
};
});