@jaimebboyjt/tres-fps-controls
Version:
Easy to use First Person Controls for TresJS
548 lines (547 loc) • 20.4 kB
JavaScript
/**
* 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
};