| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const TOUCH_ALPHA = 0.18; |
| const TOUCH_DEADZONE = 0.12; |
|
|
| export class TouchSource { |
| id = "touch"; |
| connected = false; |
| command = new Float32Array(3); |
| axes = { jaw: 0, orbitX: 0, orbitY: 0 }; |
| pressed = { stick: false, a: false, b: false }; |
| onAction = () => {}; |
|
|
| #getVelocityLimits; |
| #active = false; |
| #target = [0, 0]; |
| #mq = null; |
| #onMq = null; |
| #disposers = []; |
|
|
| constructor({ getVelocityLimits }) { |
| this.#getVelocityLimits = getVelocityLimits; |
| } |
|
|
| init() { |
| this.#mq = window.matchMedia("(pointer: coarse)"); |
| |
| |
| |
| if (window.__microduckTouched) this.connected = true; |
| this.#onMq = () => { this.connected = this.connected || this.#mq.matches; }; |
| this.#mq.addEventListener("change", this.#onMq); |
| this.#onMq(); |
| const armTouch = () => { this.connected = true; }; |
| window.addEventListener("touchstart", armTouch, { once: true, passive: true }); |
| this.#disposers.push(() => window.removeEventListener("touchstart", armTouch)); |
|
|
| const zone = document.getElementById("touch-zone"); |
| const stick = document.getElementById("touch-stick"); |
| const nub = stick?.querySelector(".nub"); |
| if (zone && stick && nub) this.#bindStick(zone, stick, nub); |
|
|
| this.#bindButton("touch-a", "a", () => this.onAction("alternateKick")); |
| this.#bindButton("touch-b", "b", () => this.onAction("quack")); |
| } |
|
|
| dispose() { |
| this.#mq?.removeEventListener("change", this.#onMq); |
| for (const off of this.#disposers) off(); |
| this.#disposers = []; |
| } |
|
|
| isActive() { |
| return this.#active; |
| } |
|
|
| poll() { |
| const [x, y] = this.#target; |
| const [limF, limB, limA] = this.#getVelocityLimits(); |
| const tvx = y >= 0 ? y * limF : y * -limB; |
| const twz = -x * limA; |
| this.command[0] += TOUCH_ALPHA * (tvx - this.command[0]); |
| this.command[2] += TOUCH_ALPHA * (twz - this.command[2]); |
| |
| |
| if (this.pressed.stick) this.#active = true; |
| else if (this.#active && Math.abs(this.command[0]) + Math.abs(this.command[2]) < 0.01) { |
| this.#active = false; |
| this.command.fill(0); |
| } |
| |
| this.axes.jaw = this.pressed.b ? 1 : 0; |
| } |
|
|
| #on(el, type, fn) { |
| el.addEventListener(type, fn); |
| this.#disposers.push(() => el.removeEventListener(type, fn)); |
| } |
|
|
| #bindStick(zone, stick, nub) { |
| let pointerId = null; |
| let center = null; |
| const setFrom = (e) => { |
| const zr = zone.getBoundingClientRect(); |
| const R = stick.offsetWidth / 2; |
| const travel = R * 0.62; |
| let dx = e.clientX - zr.left - center.x; |
| let dy = e.clientY - zr.top - center.y; |
| const d = Math.hypot(dx, dy); |
| if (d > travel) { dx *= travel / d; dy *= travel / d; } |
| nub.style.transform = `translate(${dx}px, ${dy}px)`; |
| const nx = dx / travel, ny = -dy / travel; |
| const mag = Math.hypot(nx, ny); |
| const live = mag >= TOUCH_DEADZONE; |
| this.#target[0] = live ? nx : 0; |
| this.#target[1] = live ? ny : 0; |
| }; |
| const release = () => { |
| pointerId = null; |
| center = null; |
| this.pressed.stick = false; |
| stick.classList.remove("live"); |
| nub.style.transform = ""; |
| |
| stick.style.left = ""; |
| stick.style.top = ""; |
| stick.style.bottom = ""; |
| this.#target[0] = 0; |
| this.#target[1] = 0; |
| }; |
| this.#on(zone, "pointerdown", (e) => { |
| e.preventDefault(); |
| pointerId = e.pointerId; |
| |
| |
| |
| |
| const zr = zone.getBoundingClientRect(); |
| const R = stick.offsetWidth / 2; |
| center = { x: e.clientX - zr.left, y: e.clientY - zr.top }; |
| stick.style.left = `${center.x - R}px`; |
| stick.style.top = `${center.y - R}px`; |
| stick.style.bottom = "auto"; |
| this.pressed.stick = true; |
| stick.classList.add("live"); |
| setFrom(e); |
| |
| |
| try { zone.setPointerCapture(e.pointerId); } catch {} |
| }); |
| this.#on(zone, "pointermove", (e) => { |
| if (e.pointerId === pointerId) setFrom(e); |
| }); |
| this.#on(zone, "pointerup", (e) => { |
| if (e.pointerId === pointerId) release(); |
| }); |
| this.#on(zone, "pointercancel", (e) => { |
| if (e.pointerId === pointerId) release(); |
| }); |
| } |
|
|
| #bindButton(elId, key, fire) { |
| const el = document.getElementById(elId); |
| if (!el) return; |
| this.#on(el, "pointerdown", (e) => { |
| e.preventDefault(); |
| this.pressed[key] = true; |
| el.classList.add("down"); |
| fire(); |
| try { el.setPointerCapture(e.pointerId); } catch {} |
| }); |
| const release = () => { |
| this.pressed[key] = false; |
| el.classList.remove("down"); |
| }; |
| this.#on(el, "pointerup", release); |
| this.#on(el, "pointercancel", release); |
| } |
| } |
|
|