UNPKG

zextra

Version:

zikojs extra components

188 lines (152 loc) 5.43 kB
import { UIElement, tags, call_with_optional_props } from 'ziko/dom' import { stepAlign, calculateScaledValues, clampCoordinates, getKeyboardDelta, createJoystickDOM, renderJoystickUI, dispatchJoystickEvents } from './utils.js' const {span, div} = tags; class UIJoystick extends UIElement{ constructor({ label = "2D Joystick", shape = "circle", xMin = -1, xMax = 1, yMin = -1, yMax = 1, dx = 0.01, dy = 0.01, autoCenter = true, } = {}) { super({element : 'div'}); this.settings = { label : "2D Joystick", shape : "circle", xMin : -1, xMax : 1, yMin : -1, yMax : 1, dx : 0.01, dy : 0.01, autoCenter : true, }; this.setAttr({class : 'zextra joystick-container'}); this.label_ui = span({class : 'zextra joystick-label'}, label); this.handle_ui = div({class : 'zextra joystick-handle'}); this.base_ui = div( { class : `zextra joystick-base shape-${shape}`, 'tab-index' : 0, role : 'slider', 'aria-label' : label, }, div({class : 'zextra joystick-axis-x'}), div({class : 'zextra joystick-axis-y'}), this.handle_ui ); // this.readout_ui = div({class : 'zextra joystick-values'}) this.append( span({class : 'zextra joystick-label'}, label), this.base_ui, ) this.normX = 0; this.normY = 0; this.eventTracker = { lastX: null, lastY: null }; this.isDragging = false; this.activeKeys = new Set(); this.keyLoopId = null; this.base = this.base_ui.element; this.domRefs = { handle : this.handle_ui.element, }; this._bindEvents(); this._updatePosition(0, 0); } _updatePosition(rawNormX, rawNormY) { const { clampedX, clampedY } = clampCoordinates(rawNormX, rawNormY, this.settings.shape); this.normX = clampedX; this.normY = clampedY; const { x, y } = calculateScaledValues(clampedX, clampedY, this.settings); const isAnimated = !this.isDragging && this.activeKeys.size === 0 && this.settings.autoCenter; renderJoystickUI(this.handle_ui, clampedX, clampedY, x, y, isAnimated); dispatchJoystickEvents(this.element, x, y, this.eventTracker); } _bindEvents() { // Pointer Input Handler this.base.addEventListener("pointerdown", (e) => { e.preventDefault(); e.stopPropagation(); this.base.focus(); this.isDragging = true; this.base.setPointerCapture(e.pointerId); const rect = this.base.getBoundingClientRect(); const radius = rect.width / 2; const centerX = rect.left + radius; const centerY = rect.top + radius; const onPointerMove = (moveEvt) => { if (!this.isDragging) return; moveEvt.preventDefault(); const rawNormX = (moveEvt.clientX - centerX) / radius; const rawNormY = (moveEvt.clientY - centerY) / radius; this._updatePosition(rawNormX, rawNormY); }; const onPointerUp = (upEvt) => { this.isDragging = false; if (this.base.hasPointerCapture(upEvt.pointerId)) { this.base.releasePointerCapture(upEvt.pointerId); } if (this.settings.autoCenter) { this._updatePosition(0, 0); } this.base.removeEventListener("pointermove", onPointerMove); this.base.removeEventListener("pointerup", onPointerUp); this.base.removeEventListener("pointercancel", onPointerUp); }; onPointerMove(e); this.base.addEventListener("pointermove", onPointerMove); this.base.addEventListener("pointerup", onPointerUp); this.base.addEventListener("pointercancel", onPointerUp); }); // Keyboard Animation Loop Handler const processKeyLoop = () => { if (this.activeKeys.size === 0) { if (this.keyLoopId) { cancelAnimationFrame(this.keyLoopId); this.keyLoopId = null; } if (this.settings.autoCenter) this._updatePosition(0, 0); return; } const { kx, ky } = getKeyboardDelta(this.activeKeys); if (kx !== 0 || ky !== 0) this._updatePosition(this.normX + kx, this.normY + ky); this.keyLoopId = requestAnimationFrame(processKeyLoop); }; this.base.addEventListener("keydown", (e) => { const validKeys = ["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown", "KeyW", "KeyA", "KeyS", "KeyD"]; if (validKeys.includes(e.code) || validKeys.includes(e.key)) { e.preventDefault(); this.activeKeys.add(e.code || e.key); if (!this.keyLoopId) { this.keyLoopId = requestAnimationFrame(processKeyLoop); } } }); this.base.addEventListener("keyup", (e) => { this.activeKeys.delete(e.code || e.key); }); this.base.addEventListener("blur", () => { this.activeKeys.clear(); if (this.settings.autoCenter) { this._updatePosition(0, 0); } }); } } export const Joystick = call_with_optional_props(UIJoystick)