UNPKG

@jaimebboyjt/tres-fps-controls

Version:

Easy to use First Person Controls for TresJS

548 lines (547 loc) 20.4 kB
/** * name: @jaimebboyjt/tres-fps-controls * version: v0.0.53 * (c) 2025 * description: Easy to use First Person Controls for TresJS * author: Jaime Torrealba <jaime@torrealba.uk> (https://github.com/JaimeTorrealba) */ var de = Object.defineProperty; var fe = (t, n, e) => n in t ? de(t, n, { enumerable: !0, configurable: !0, writable: !0, value: e }) : t[n] = e; var k = (t, n, e) => (fe(t, typeof n != "symbol" ? n + "" : n, e), e); import { getCurrentScope as me, onScopeDispose as he, unref as B, watch as J, reactive as j, computed as pe, ref as w, watchEffect as ee, defineComponent as O, onUnmounted as te, openBlock as oe, createElementBlock as ne, createCommentVNode as ve, toRefs as ye, shallowRef as q, provide as ke, Fragment as ge, createVNode as we, createElementVNode as Ee, renderSlot as be, inject as Le, onDeactivated as Ce } from "vue"; import { useTresContext as V, useLoop as Me } from "@tresjs/core"; import { EventDispatcher as Te, Vector3 as F, Euler as Se } from "three"; function xe(t) { return me() ? (he(t), !0) : !1; } function D(t) { return typeof t == "function" ? t() : B(t); } const Pe = typeof window < "u" && typeof document < "u"; typeof WorkerGlobalScope < "u" && globalThis instanceof WorkerGlobalScope; const Be = Object.prototype.toString, De = (t) => Be.call(t) === "[object Object]", se = () => { }, N = Pe ? window : void 0; function Re(t) { var n; const e = D(t); return (n = e == null ? void 0 : e.$el) != null ? n : e; } function T(...t) { let n, e, o, s; if (typeof t[0] == "string" || Array.isArray(t[0]) ? ([e, o, s] = t, n = N) : [n, e, o, s] = t, !n) return se; Array.isArray(e) || (e = [e]), Array.isArray(o) || (o = [o]); const r = [], a = () => { r.forEach((d) => d()), r.length = 0; }, c = (d, u, f, p) => (d.addEventListener(u, f, p), () => d.removeEventListener(u, f, p)), i = J( () => [Re(n), D(s)], ([d, u]) => { if (a(), !d) return; const f = De(u) ? { ...u } : u; r.push( ...e.flatMap((p) => o.map((y) => c(d, p, y, f))) ); }, { immediate: !0, flush: "post" } ), l = () => { i(), a(); }; return xe(l), l; } function Ae(t) { return typeof t == "function" ? t : typeof t == "string" ? (n) => n.key === t : Array.isArray(t) ? (n) => t.includes(n.key) : () => !0; } function Ke(...t) { let n, e, o = {}; t.length === 3 ? (n = t[0], e = t[1], o = t[2]) : t.length === 2 ? typeof t[1] == "object" ? (n = !0, e = t[0], o = t[1]) : (n = t[0], e = t[1]) : (n = !0, e = t[0]); const { target: s = N, eventName: r = "keydown", passive: a = !1, dedupe: c = !1 } = o, i = Ae(n); return T(s, r, (d) => { d.repeat && D(c) || i(d) && e(d); }, a); } function _e(t, n, e = {}) { return Ke(t, n, { ...e, eventName: "keydown" }); } const je = { ctrl: "control", command: "meta", cmd: "meta", option: "alt", up: "arrowup", down: "arrowdown", left: "arrowleft", right: "arrowright" }; function ie(t = {}) { const { reactive: n = !1, target: e = N, aliasMap: o = je, passive: s = !0, onEventFired: r = se } = t, a = j(/* @__PURE__ */ new Set()), c = { toJSON() { return {}; }, current: a }, i = n ? j(c) : c, l = /* @__PURE__ */ new Set(), d = /* @__PURE__ */ new Set(); function u(h, m) { h in i && (n ? i[h] = m : i[h].value = m); } function f() { a.clear(); for (const h of d) u(h, !1); } function p(h, m) { var b, L; const E = (b = h.key) == null ? void 0 : b.toLowerCase(), x = [(L = h.code) == null ? void 0 : L.toLowerCase(), E].filter(Boolean); E && (m ? a.add(E) : a.delete(E)); for (const g of x) d.add(g), u(g, m); E === "meta" && !m ? (l.forEach((g) => { a.delete(g), u(g, !1); }), l.clear()) : typeof h.getModifierState == "function" && h.getModifierState("Meta") && m && [...a, ...x].forEach((g) => l.add(g)); } T(e, "keydown", (h) => (p(h, !0), r(h)), { passive: s }), T(e, "keyup", (h) => (p(h, !1), r(h)), { passive: s }), T("blur", f, { passive: !0 }), T("focus", f, { passive: !0 }); const y = new Proxy( i, { get(h, m, b) { if (typeof m != "string") return Reflect.get(h, m, b); if (m = m.toLowerCase(), m in o && (m = o[m]), !(m in i)) if (/[+_-]/.test(m)) { const E = m.split(/[+_-]/g).map((S) => S.trim()); i[m] = pe(() => E.every((S) => D(y[S]))); } else i[m] = w(!1); const L = Reflect.get(h, m, b); return n ? D(L) : L; } } ); return y; } const Je = (t, n = 0) => { const e = w(!1), o = w(0), s = w(0), r = t.gravity ?? 9.8, c = ie()[t.key]; J(c, (u) => { u && (e.value || (s.value = Date.now()), e.value = !0); }); const i = () => (Date.now() - s.value) / 1e3 * 3, l = (u) => n + 6 * u - 0.5 * r * u ** 2; return { getJump: () => e.value ? (o.value = l(i()), o.value <= n && (e.value = !1), o.value) : n, isJumping: e }; }, We = (t, n) => { const e = w(0), o = w(0), s = w(!1), r = w(!1), a = w(!1), c = j({ forward: !1, backward: !1, right: !1, left: !1 }), { forward: i, backward: l, right: d, left: u, run: f, creep: p } = n, y = ie(), h = y[i.key], m = y[l.key], b = y[d.key], L = y[u.key], E = y[`${f.key}+${i.key}`], S = y[`${p.key}+${i.key}`], x = () => { o.value = t, c.forward = !0; }, g = () => { o.value = -t, c.backward = !0; }, R = () => { o.value = f.speed ?? t * 2, r.value = !0; }, W = () => { o.value = t, r.value = !1; }, X = () => { o.value = t, a.value = !1; }, A = () => { o.value = p.speed ?? t * 0.25, a.value = !0; }, K = () => { o.value = 0, c.forward = !1, c.backward = !1; }, _ = () => { e.value = -t, c.left = !0; }, P = () => { e.value = t, c.right = !0; }, v = () => { e.value = 0, c.left = !1, c.right = !1; }; return ee(() => { h.value ? (x(), E.value ? R() : r.value = !1, S.value ? A() : a.value = !1) : m.value ? g() : K(), b.value ? P() : L.value ? _() : v(); }), J([o, e], ([Y, $]) => { Y !== 0 || $ !== 0 ? s.value = !0 : s.value = !1; }), { sidewardMove: e, forwardMove: o, isWalking: s, isRunning: r, isCreeping: a, moveForward: x, moveBackward: g, moveLeft: _, moveRight: P, applyCreep: A, applyRun: R, stopCreep: X, stopRun: W, stopSideward: v, stopForward: K, direction: c }; }, Xe = (t) => { const { renderer: n } = V(), { wheelActionUp: e, wheelActionDown: o, actions: s, leftClick: r, rightClick: a, middleClick: c } = t; s.actions.map((i) => { _e(i.key, () => { i.action(); }); }), T(n.value.domElement, "click", (i) => { i.button === 0 ? r == null || r.action() : i.button === 1 ? c == null || c.action() : i.button === 2 && (a == null || a.action()); }), n.value.domElement.onwheel = (i) => { i.deltaY > 0 ? o.action() : e.action(); }; }, M = Object.freeze({ idle: "idle", walking: "walking", running: "running", jumping: "jumping", creeping: "creeping" }), Ye = Object.freeze({ forward: "w", backward: "s", left: "a", right: "d" }), $e = (t) => { const n = ["forward", "backward", "left", "right"], e = []; return n.map((s) => { const [r] = t.value.filter((a) => a.name === s); r ? e.push(r) : e.push({ name: s, key: Ye[s] }); }), ["run", "creep", "jump"].map((s) => { const [r] = t.value.filter((a) => a.name === s); r ? e.push(r) : e.push(!1); }), e; }, qe = (t) => { const n = [ "leftClick", "rightClick", "middleClick", "wheelActionUp", "wheelActionDown", "actions" ], e = []; return n.map((o) => { const [s] = t.value.filter((r) => r.name === o); s ? e.push(s) : e.push(!1); }), e; }, re = "ontouchstart" in window, Fe = (t, n) => { const { active: e, speed: o, amplitude: s } = t.value, r = w(n), a = (l) => l === M.running ? o * 3 : l === M.creeping ? o * 0.1 : o, c = (l) => l === M.creeping ? s * 0.25 : s; return (l, d, u) => (d && e ? r.value = Math.sin(l * a(u)) * c(u) : (r.value > n + 0.01 || r.value < -0.01 - n) && (r.value += (n - r.value) * 0.1), r.value); }, Oe = { type: "change" }, Ve = { type: "lock" }, Ne = { type: "unlock" }, G = Math.PI / 2; class He extends Te { constructor(e, o) { super(); k(this, "domElement"); k(this, "camera"); k(this, "isLocked", !1); k(this, "minPolarAngle", 0); // radians k(this, "maxPolarAngle", Math.PI); // radians k(this, "vector", new F()); k(this, "euler", new Se(0, 0, 0, "YXZ")); k(this, "previousTouch"); k(this, "onMouseMoveBind", this.onMouseMove.bind(this)); k(this, "onPointerlockChangeBind", this.onPointerlockChange.bind(this)); k(this, "onPointerlockErrorBind", this.onPointerlockError.bind(this)); k(this, "onTouchMoveBind", this.onTouchMove.bind(this)); k(this, "onTouchEndBind", this.onTouchEnd.bind(this)); o === void 0 && (console.warn( 'THREE.PointerLockControls: The second parameter "domElement" is now mandatory.' ), o = document.body), this.camera = e, this.domElement = o, this.connect(); } onTouchMove(e) { let o; switch (e.touches.length) { case 1: e.touches[0].target === this.domElement && (o = e.touches[0]); break; case 2: e.touches[0].target === this.domElement ? o = e.touches[0] : e.touches[1].target === this.domElement && (o = e.touches[1]); break; } if (!o) return; const s = this.previousTouch ? o.pageX - this.previousTouch.pageX : 0, r = this.previousTouch ? o.pageY - this.previousTouch.pageY : 0; this.updatePosition(s, r, 4e-3), this.previousTouch = o; } onTouchEnd() { this.previousTouch = void 0; } onMouseMove(e) { if (this.isLocked === !1) return; const o = e.movementX || e.mozMovementX || e.webkitMovementX || 0, s = e.movementY || e.mozMovementY || e.webkitMovementY || 0; this.updatePosition(o, s, 2e-3); } updatePosition(e, o, s) { this.euler.setFromQuaternion(this.camera.quaternion), this.euler.y -= e * s, this.euler.x -= o * s, this.euler.x = Math.max( G - this.maxPolarAngle, Math.min(G - this.minPolarAngle, this.euler.x) ), this.camera.quaternion.setFromEuler(this.euler), this.dispatchEvent(Oe); } onPointerlockChange() { this.domElement.ownerDocument.pointerLockElement === this.domElement ? (this.dispatchEvent(Ve), this.isLocked = !0) : (this.dispatchEvent(Ne), this.isLocked = !1); } onPointerlockError() { console.error("THREE.PointerLockControls: Unable to use Pointer Lock API"); } connect() { this.domElement.addEventListener("touchmove", this.onTouchMoveBind, !1), this.domElement.addEventListener("touchend", this.onTouchEndBind, !1), this.domElement.ownerDocument.addEventListener( "mousemove", this.onMouseMoveBind ), this.domElement.ownerDocument.addEventListener( "pointerlockchange", this.onPointerlockChangeBind ), this.domElement.ownerDocument.addEventListener( "pointerlockerror", this.onPointerlockErrorBind ); } disconnect() { this.domElement.removeEventListener( "touchmove", this.onTouchMoveBind, !1 ), this.domElement.removeEventListener("touchend", this.onTouchEndBind, !1), this.domElement.ownerDocument.removeEventListener( "mousemove", this.onMouseMoveBind ), this.domElement.ownerDocument.removeEventListener( "pointerlockchange", this.onPointerlockChangeBind ), this.domElement.ownerDocument.removeEventListener( "pointerlockerror", this.onPointerlockErrorBind ); } dispose() { this.disconnect(); } getObject() { return this.camera; } getDirection() { const e = new F(0, 0, -1); return (o) => o.copy(e).applyQuaternion(this.camera.quaternion); } moveForward(e) { this.vector.setFromMatrixColumn(this.camera.matrix, 0), this.vector.crossVectors(this.camera.up, this.vector), this.camera.position.addScaledVector(this.vector, e); } moveRight(e) { this.vector.setFromMatrixColumn(this.camera.matrix, 0), this.camera.position.addScaledVector(this.vector, e); } lock() { typeof this.domElement.requestPointerLock < "u" && !re && this.domElement.requestPointerLock(); } unlock() { typeof this.domElement.requestPointerLock < "u" && this.domElement.ownerDocument.exitPointerLock(); } } const Ie = ["args"], Ue = /* @__PURE__ */ O({ __name: "PointerLockControls", props: { makeDefault: { type: Boolean, default: !1 }, camera: {}, domElement: {}, selector: {} }, emits: ["isLock", "change"], setup(t, { expose: n, emit: e }) { const o = t, s = e, { camera: r, renderer: a, extend: c, controls: i } = V(), l = w(null); let d; c({ PointerLockControls: He }); const u = (f) => { s("isLock", f); }; return J(l, (f) => { f && o.makeDefault ? i.value = f : i.value = null; const p = document.getElementById(o.selector || ""); d = p || a.value.domElement, T(i.value, "change", () => s("change", i.value)), T(d, "click", () => { var y, h, m; (y = i.value) == null || y.lock(), (h = i.value) == null || h.addEventListener("lock", () => u(!0)), (m = i.value) == null || m.addEventListener("unlock", () => u(!1)); }); }), te(() => { var f, p; (f = i.value) == null || f.removeEventListener("lock", () => u(!0)), (p = i.value) == null || p.removeEventListener("unlock", () => u(!1)), l.value && l.value.dispose(); }), n({ value: i }), (f, p) => (f.camera || B(r)) && (f.domElement || B(a)) ? (oe(), ne("TresPointerLockControls", { key: 0, ref_key: "controlsRef", ref: l, args: [f.camera || B(r), f.domElement || B(a).domElement] }, null, 8, Ie)) : ve("", !0); } }), ot = /* @__PURE__ */ O({ __name: "fpsControls", props: { makeDefault: { type: Boolean }, camera: {}, domElement: {}, selector: {}, moveSpeed: { default: 0.1 }, headBobbing: { default: () => ({ active: !0, speed: 5, amplitude: 0.25 }) }, controlsKeys: { default: () => [ { name: "forward", key: "w" }, { name: "backward", key: "s" }, { name: "left", key: "a" }, { name: "right", key: "d" }, // Optional actions key map { name: "jump", key: "space" }, { name: "run", key: "Shift" }, { name: "creep", key: "ctrl" }, // Mouse actions { name: "leftClick", action: () => { } }, { name: "rightClick", action: () => { } }, { name: "middleClick", action: () => { } }, { name: "wheelActionUp", action: () => { } }, { name: "wheelActionDown", action: () => { } }, // Key actions { name: "actions", actions: [ { name: "action", key: "e", action: () => { } }, { name: "action", key: "q", action: () => { } }, { name: "action", key: "r", action: () => { } }, { name: "action", key: "f", action: () => { } } ] } ] } }, emits: ["state", "isLock", "change"], setup(t, { expose: n, emit: e }) { var I, U; const o = t, s = e, { controlsKeys: r, moveSpeed: a, headBobbing: c } = ye(o), { camera: i } = V(), l = w(M.idle), d = q(), u = q(), f = q(), p = ((U = (I = i == null ? void 0 : i.value) == null ? void 0 : I.position) == null ? void 0 : U.y) ?? 0, y = new F(), [h, m, b, L, E, S, x] = $e(r), [g, R, W, X, A, K] = qe(r), { getJump: _, isJumping: P } = Je(x, p), v = We(a.value, { forward: h, backward: m, left: b, right: L, run: E, creep: S }); ke("moveMethods", v); const Y = Fe(c, p); Xe({ actions: K, wheelActionUp: X, wheelActionDown: A, leftClick: g, rightClick: R, middleClick: W }), ee(() => { v.isRunning.value ? l.value = M.running : v.isCreeping.value ? l.value = M.creeping : v.isWalking.value ? l.value = M.walking : P.value ? l.value = M.jumping : l.value = M.idle, s("state", { state: l.value, direction: v.direction }); }); const $ = (C) => { f.value = C, s("isLock", C); }, ce = () => H(), H = () => { i.value && (u.value.rotation.copy(i.value.rotation), u.value.position.copy(i.value.position).add(i.value.getWorldDirection(y).multiplyScalar(2.5)), s("change", d)); }; n({ root: d, models: u, moveMethods: { forward: () => v.moveForward(), backward: () => v.moveBackward(), left: () => v.moveLeft(), right: () => v.moveRight(), run: () => v.applyRun(), creep: () => v.applyCreep(), stopCreep: () => v.stopCreep(), stopRun: () => v.stopRun(), stopSideward: () => v.stopSideward(), stopForward: () => v.stopForward() } }); const { onBeforeRender: le } = Me(); return le(({ elapsed: C }) => { if (f.value || re) { if (d.value.value.moveForward(v.forwardMove.value), d.value.value.moveRight(v.sidewardMove.value), !i.value) return; i.value.position.y = _(), i.value.position.y += Y(C, v.isWalking.value, l.value), (v.isWalking.value || P.value) && H(); } }), (C, z) => (oe(), ne(ge, null, [ we(Ue, { ref_key: "PointerLockControlsRef", ref: d, "make-default": "", camera: C.camera, domElement: C.domElement, selector: C.selector, onChange: ce, onIsLock: z[0] || (z[0] = (ue) => $(ue)) }, null, 8, ["camera", "domElement", "selector"]), Ee("TresGroup", { ref_key: "wrapperRef", ref: u }, [ be(C.$slots, "default") ], 512) ], 64)); } }), ze = (t, n, e) => { n > 0.15 && n < 0.25 ? t.applyCreep() : n > 0.9 ? t.applyRun() : n > 0.3 && n < 0.9 ? (t.stopCreep(), t.stopRun(), t.moveForward()) : n < -0.15 && t.moveBackward(), e > 0.3 ? t.moveRight() : e < -0.3 && t.moveLeft(); }, Q = (t, n, e) => { "ontouchstart" in window ? (document.ontouchmove = null, document.ontouchend = null) : (document.onmousemove = null, document.onmouseup = null), n.style.top = `${e.top}px`, n.style.left = `${e.left}px`, t.stopCreep(), t.stopRun(), t.stopForward(), t.stopSideward(); }, ae = (t) => { const n = t.targetTouches ? t.targetTouches[0].pageX : t.clientX, e = t.targetTouches ? t.targetTouches[0].pageY : t.clientY; return { x: n, y: e }; }, Ge = (t, n, e, o, s, r) => { n = n || window.event; const a = ae(n); e.x = a.x, e.y = a.y, "ontouchstart" in window ? (o.ontouchmove = (c) => Z(t, c, e, o, s, r), o.ontouchend = () => Q(t, o, r)) : (o.onmousemove = (c) => Z(t, c, e, o, s, r), o.onmouseup = () => Q(t, o, r)); }, Z = (t, n, e, o, s, r) => { const a = ae(n); let c = a.x - e.x, i = a.y - e.y; const l = c * c + i * i, d = s * s; if (l > d) { const p = Math.sqrt(l); c /= p, i /= p, c *= s, i *= s; } o.style.top = `${i + o.clientHeight / 2}px`, o.style.left = `${c + o.clientWidth / 2}px`; const u = -(i - r.top + o.clientHeight / 2) / s, f = (c - r.left + o.clientWidth / 2) / s; ze(t, u, f); }, nt = O({ name: "MobileJoystick", props: { containerStyles: { type: String, default: "position:absolute; bottom:35px; width:160px; height:160px; background:rgba(126, 126, 126, 0.5); border:#fff solid medium; border-radius:50%; left:20%; transform:translateX(-50%);z-index:9999;" }, joystickStyles: { type: String, default: "position: absolute; left: 50px; top: 50px; width: 60px; height: 60px; border-radius: 50%; background: #fff;" }, maxRadius: { type: Number, default: 60 } }, setup(t) { const n = Le("moveMethods"), e = document.body; e.style.cssText = "overscroll-behavior-y: contain;"; const o = document.createElement("div"); o.style.cssText = t.containerStyles; const s = document.createElement("div"); s.style.cssText = t.joystickStyles, o.appendChild(s), e.appendChild(o); const r = j({ x: 0, y: 0 }), a = t.maxRadius, c = { left: s.offsetLeft, top: s.offsetTop }; s !== void 0 && "ontouchstart" in window && T(s, "touchstart", (i) => { Ge(n, i, r, s, a, c); }), te(() => { e.removeChild(o); }), Ce(() => { e.removeChild(o); }); } }); export { nt as MobileJoystick, ot as fpsControls };