UNPKG

@jaimebboyjt/tres-fps-controls

Version:

Easy to use First Person Controls for TresJS

9 lines (8 loc) 15.5 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) */ (function(k,s){typeof exports=="object"&&typeof module<"u"?s(exports,require("vue"),require("@tresjs/core"),require("three")):typeof define=="function"&&define.amd?define(["exports","vue","@tresjs/core","three"],s):(k=typeof globalThis<"u"?globalThis:k||self,s(k.tresfpscontrols={},k.Vue,k.TresjsCore,k.Three))})(this,function(k,s,L,D){"use strict";var Be=Object.defineProperty;var Pe=(k,s,L)=>s in k?Be(k,s,{enumerable:!0,configurable:!0,writable:!0,value:L}):k[s]=L;var g=(k,s,L)=>(Pe(k,typeof s!="symbol"?s+"":s,L),L);function ee(t){return s.getCurrentScope()?(s.onScopeDispose(t),!0):!1}function R(t){return typeof t=="function"?t():s.unref(t)}const te=typeof window<"u"&&typeof document<"u";typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const oe=Object.prototype.toString,ne=t=>oe.call(t)==="[object Object]",$=()=>{},J=te?window:void 0;function se(t){var n;const e=R(t);return(n=e==null?void 0:e.$el)!=null?n:e}function C(...t){let n,e,o,r;if(typeof t[0]=="string"||Array.isArray(t[0])?([e,o,r]=t,n=J):[n,e,o,r]=t,!n)return $;Array.isArray(e)||(e=[e]),Array.isArray(o)||(o=[o]);const c=[],a=()=>{c.forEach(d=>d()),c.length=0},l=(d,f,m,v)=>(d.addEventListener(f,m,v),()=>d.removeEventListener(f,m,v)),i=s.watch(()=>[se(n),R(r)],([d,f])=>{if(a(),!d)return;const m=ne(f)?{...f}:f;c.push(...e.flatMap(v=>o.map(w=>l(d,v,w,m))))},{immediate:!0,flush:"post"}),u=()=>{i(),a()};return ee(u),u}function re(t){return typeof t=="function"?t:typeof t=="string"?n=>n.key===t:Array.isArray(t)?n=>t.includes(n.key):()=>!0}function ie(...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:r=J,eventName:c="keydown",passive:a=!1,dedupe:l=!1}=o,i=re(n);return C(r,c,d=>{d.repeat&&R(l)||i(d)&&e(d)},a)}function ce(t,n,e={}){return ie(t,n,{...e,eventName:"keydown"})}const ae={ctrl:"control",command:"meta",cmd:"meta",option:"alt",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright"};function F(t={}){const{reactive:n=!1,target:e=J,aliasMap:o=ae,passive:r=!0,onEventFired:c=$}=t,a=s.reactive(new Set),l={toJSON(){return{}},current:a},i=n?s.reactive(l):l,u=new Set,d=new Set;function f(p,h){p in i&&(n?i[p]=h:i[p].value=h)}function m(){a.clear();for(const p of d)f(p,!1)}function v(p,h){var T,S;const b=(T=p.key)==null?void 0:T.toLowerCase(),P=[(S=p.code)==null?void 0:S.toLowerCase(),b].filter(Boolean);b&&(h?a.add(b):a.delete(b));for(const E of P)d.add(E),f(E,h);b==="meta"&&!h?(u.forEach(E=>{a.delete(E),f(E,!1)}),u.clear()):typeof p.getModifierState=="function"&&p.getModifierState("Meta")&&h&&[...a,...P].forEach(E=>u.add(E))}C(e,"keydown",p=>(v(p,!0),c(p)),{passive:r}),C(e,"keyup",p=>(v(p,!1),c(p)),{passive:r}),C("blur",m,{passive:!0}),C("focus",m,{passive:!0});const w=new Proxy(i,{get(p,h,T){if(typeof h!="string")return Reflect.get(p,h,T);if(h=h.toLowerCase(),h in o&&(h=o[h]),!(h in i))if(/[+_-]/.test(h)){const b=h.split(/[+_-]/g).map(B=>B.trim());i[h]=s.computed(()=>b.every(B=>R(w[B])))}else i[h]=s.ref(!1);const S=Reflect.get(p,h,T);return n?R(S):S}});return w}const le=(t,n=0)=>{const e=s.ref(!1),o=s.ref(0),r=s.ref(0),c=t.gravity??9.8,l=F()[t.key];s.watch(l,f=>{f&&(e.value||(r.value=Date.now()),e.value=!0)});const i=()=>(Date.now()-r.value)/1e3*3,u=f=>n+6*f-.5*c*f**2;return{getJump:()=>e.value?(o.value=u(i()),o.value<=n&&(e.value=!1),o.value):n,isJumping:e}},ue=(t,n)=>{const e=s.ref(0),o=s.ref(0),r=s.ref(!1),c=s.ref(!1),a=s.ref(!1),l=s.reactive({forward:!1,backward:!1,right:!1,left:!1}),{forward:i,backward:u,right:d,left:f,run:m,creep:v}=n,w=F(),p=w[i.key],h=w[u.key],T=w[d.key],S=w[f.key],b=w[`${m.key}+${i.key}`],B=w[`${v.key}+${i.key}`],P=()=>{o.value=t,l.forward=!0},E=()=>{o.value=-t,l.backward=!0},j=()=>{o.value=m.speed??t*2,c.value=!0},W=()=>{o.value=t,c.value=!1},X=()=>{o.value=t,a.value=!1},K=()=>{o.value=v.speed??t*.25,a.value=!0},_=()=>{o.value=0,l.forward=!1,l.backward=!1},q=()=>{e.value=-t,l.left=!0},A=()=>{e.value=t,l.right=!0},y=()=>{e.value=0,l.left=!1,l.right=!1};return s.watchEffect(()=>{p.value?(P(),b.value?j():c.value=!1,B.value?K():a.value=!1):h.value?E():_(),T.value?A():S.value?q():y()}),s.watch([o,e],([Y,V])=>{Y!==0||V!==0?r.value=!0:r.value=!1}),{sidewardMove:e,forwardMove:o,isWalking:r,isRunning:c,isCreeping:a,moveForward:P,moveBackward:E,moveLeft:q,moveRight:A,applyCreep:K,applyRun:j,stopCreep:X,stopRun:W,stopSideward:y,stopForward:_,direction:l}},fe=t=>{const{renderer:n}=L.useTresContext(),{wheelActionUp:e,wheelActionDown:o,actions:r,leftClick:c,rightClick:a,middleClick:l}=t;r.actions.map(i=>{ce(i.key,()=>{i.action()})}),C(n.value.domElement,"click",i=>{i.button===0?c==null||c.action():i.button===1?l==null||l.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"}),de=Object.freeze({forward:"w",backward:"s",left:"a",right:"d"}),me=t=>{const n=["forward","backward","left","right"],e=[];return n.map(r=>{const[c]=t.value.filter(a=>a.name===r);c?e.push(c):e.push({name:r,key:de[r]})}),["run","creep","jump"].map(r=>{const[c]=t.value.filter(a=>a.name===r);c?e.push(c):e.push(!1)}),e},he=t=>{const n=["leftClick","rightClick","middleClick","wheelActionUp","wheelActionDown","actions"],e=[];return n.map(o=>{const[r]=t.value.filter(c=>c.name===o);r?e.push(r):e.push(!1)}),e},O="ontouchstart"in window,pe=(t,n)=>{const{active:e,speed:o,amplitude:r}=t.value,c=s.ref(n),a=u=>u===M.running?o*3:u===M.creeping?o*.1:o,l=u=>u===M.creeping?r*.25:r;return(u,d,f)=>(d&&e?c.value=Math.sin(u*a(f))*l(f):(c.value>n+.01||c.value<-.01-n)&&(c.value+=(n-c.value)*.1),c.value)},ve={type:"change"},ye={type:"lock"},ke={type:"unlock"},N=Math.PI/2;class we extends D.EventDispatcher{constructor(e,o){super();g(this,"domElement");g(this,"camera");g(this,"isLocked",!1);g(this,"minPolarAngle",0);g(this,"maxPolarAngle",Math.PI);g(this,"vector",new D.Vector3);g(this,"euler",new D.Euler(0,0,0,"YXZ"));g(this,"previousTouch");g(this,"onMouseMoveBind",this.onMouseMove.bind(this));g(this,"onPointerlockChangeBind",this.onPointerlockChange.bind(this));g(this,"onPointerlockErrorBind",this.onPointerlockError.bind(this));g(this,"onTouchMoveBind",this.onTouchMove.bind(this));g(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 r=this.previousTouch?o.pageX-this.previousTouch.pageX:0,c=this.previousTouch?o.pageY-this.previousTouch.pageY:0;this.updatePosition(r,c,.004),this.previousTouch=o}onTouchEnd(){this.previousTouch=void 0}onMouseMove(e){if(this.isLocked===!1)return;const o=e.movementX||e.mozMovementX||e.webkitMovementX||0,r=e.movementY||e.mozMovementY||e.webkitMovementY||0;this.updatePosition(o,r,.002)}updatePosition(e,o,r){this.euler.setFromQuaternion(this.camera.quaternion),this.euler.y-=e*r,this.euler.x-=o*r,this.euler.x=Math.max(N-this.maxPolarAngle,Math.min(N-this.minPolarAngle,this.euler.x)),this.camera.quaternion.setFromEuler(this.euler),this.dispatchEvent(ve)}onPointerlockChange(){this.domElement.ownerDocument.pointerLockElement===this.domElement?(this.dispatchEvent(ye),this.isLocked=!0):(this.dispatchEvent(ke),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 D.Vector3(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"&&!O&&this.domElement.requestPointerLock()}unlock(){typeof this.domElement.requestPointerLock<"u"&&this.domElement.ownerDocument.exitPointerLock()}}const ge=["args"],Ee=s.defineComponent({__name:"PointerLockControls",props:{makeDefault:{type:Boolean,default:!1},camera:{},domElement:{},selector:{}},emits:["isLock","change"],setup(t,{expose:n,emit:e}){const o=t,r=e,{camera:c,renderer:a,extend:l,controls:i}=L.useTresContext(),u=s.ref(null);let d;l({PointerLockControls:we});const f=m=>{r("isLock",m)};return s.watch(u,m=>{m&&o.makeDefault?i.value=m:i.value=null;const v=document.getElementById(o.selector||"");d=v||a.value.domElement,C(i.value,"change",()=>r("change",i.value)),C(d,"click",()=>{var w,p,h;(w=i.value)==null||w.lock(),(p=i.value)==null||p.addEventListener("lock",()=>f(!0)),(h=i.value)==null||h.addEventListener("unlock",()=>f(!1))})}),s.onUnmounted(()=>{var m,v;(m=i.value)==null||m.removeEventListener("lock",()=>f(!0)),(v=i.value)==null||v.removeEventListener("unlock",()=>f(!1)),u.value&&u.value.dispose()}),n({value:i}),(m,v)=>(m.camera||s.unref(c))&&(m.domElement||s.unref(a))?(s.openBlock(),s.createElementBlock("TresPointerLockControls",{key:0,ref_key:"controlsRef",ref:u,args:[m.camera||s.unref(c),m.domElement||s.unref(a).domElement]},null,8,ge)):s.createCommentVNode("",!0)}}),be=s.defineComponent({__name:"fpsControls",props:{makeDefault:{type:Boolean},camera:{},domElement:{},selector:{},moveSpeed:{default:.1},headBobbing:{default:()=>({active:!0,speed:5,amplitude:.25})},controlsKeys:{default:()=>[{name:"forward",key:"w"},{name:"backward",key:"s"},{name:"left",key:"a"},{name:"right",key:"d"},{name:"jump",key:"space"},{name:"run",key:"Shift"},{name:"creep",key:"ctrl"},{name:"leftClick",action:()=>{}},{name:"rightClick",action:()=>{}},{name:"middleClick",action:()=>{}},{name:"wheelActionUp",action:()=>{}},{name:"wheelActionDown",action:()=>{}},{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 G,Q;const o=t,r=e,{controlsKeys:c,moveSpeed:a,headBobbing:l}=s.toRefs(o),{camera:i}=L.useTresContext(),u=s.ref(M.idle),d=s.shallowRef(),f=s.shallowRef(),m=s.shallowRef(),v=((Q=(G=i==null?void 0:i.value)==null?void 0:G.position)==null?void 0:Q.y)??0,w=new D.Vector3,[p,h,T,S,b,B,P]=me(c),[E,j,W,X,K,_]=he(c),{getJump:q,isJumping:A}=le(P,v),y=ue(a.value,{forward:p,backward:h,left:T,right:S,run:b,creep:B});s.provide("moveMethods",y);const Y=pe(l,v);fe({actions:_,wheelActionUp:X,wheelActionDown:K,leftClick:E,rightClick:j,middleClick:W}),s.watchEffect(()=>{y.isRunning.value?u.value=M.running:y.isCreeping.value?u.value=M.creeping:y.isWalking.value?u.value=M.walking:A.value?u.value=M.jumping:u.value=M.idle,r("state",{state:u.value,direction:y.direction})});const V=x=>{m.value=x,r("isLock",x)},Te=()=>z(),z=()=>{i.value&&(f.value.rotation.copy(i.value.rotation),f.value.position.copy(i.value.position).add(i.value.getWorldDirection(w).multiplyScalar(2.5)),r("change",d))};n({root:d,models:f,moveMethods:{forward:()=>y.moveForward(),backward:()=>y.moveBackward(),left:()=>y.moveLeft(),right:()=>y.moveRight(),run:()=>y.applyRun(),creep:()=>y.applyCreep(),stopCreep:()=>y.stopCreep(),stopRun:()=>y.stopRun(),stopSideward:()=>y.stopSideward(),stopForward:()=>y.stopForward()}});const{onBeforeRender:Se}=L.useLoop();return Se(({elapsed:x})=>{if(m.value||O){if(d.value.value.moveForward(y.forwardMove.value),d.value.value.moveRight(y.sidewardMove.value),!i.value)return;i.value.position.y=q(),i.value.position.y+=Y(x,y.isWalking.value,u.value),(y.isWalking.value||A.value)&&z()}}),(x,Z)=>(s.openBlock(),s.createElementBlock(s.Fragment,null,[s.createVNode(Ee,{ref_key:"PointerLockControlsRef",ref:d,"make-default":"",camera:x.camera,domElement:x.domElement,selector:x.selector,onChange:Te,onIsLock:Z[0]||(Z[0]=xe=>V(xe))},null,8,["camera","domElement","selector"]),s.createElementVNode("TresGroup",{ref_key:"wrapperRef",ref:f},[s.renderSlot(x.$slots,"default")],512)],64))}}),Le=(t,n,e)=>{n>.15&&n<.25?t.applyCreep():n>.9?t.applyRun():n>.3&&n<.9?(t.stopCreep(),t.stopRun(),t.moveForward()):n<-.15&&t.moveBackward(),e>.3?t.moveRight():e<-.3&&t.moveLeft()},U=(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()},H=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}},Ce=(t,n,e,o,r,c)=>{n=n||window.event;const a=H(n);e.x=a.x,e.y=a.y,"ontouchstart"in window?(o.ontouchmove=l=>I(t,l,e,o,r,c),o.ontouchend=()=>U(t,o,c)):(o.onmousemove=l=>I(t,l,e,o,r,c),o.onmouseup=()=>U(t,o,c))},I=(t,n,e,o,r,c)=>{const a=H(n);let l=a.x-e.x,i=a.y-e.y;const u=l*l+i*i,d=r*r;if(u>d){const v=Math.sqrt(u);l/=v,i/=v,l*=r,i*=r}o.style.top=`${i+o.clientHeight/2}px`,o.style.left=`${l+o.clientWidth/2}px`;const f=-(i-c.top+o.clientHeight/2)/r,m=(l-c.left+o.clientWidth/2)/r;Le(t,f,m)},Me=s.defineComponent({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=s.inject("moveMethods"),e=document.body;e.style.cssText="overscroll-behavior-y: contain;";const o=document.createElement("div");o.style.cssText=t.containerStyles;const r=document.createElement("div");r.style.cssText=t.joystickStyles,o.appendChild(r),e.appendChild(o);const c=s.reactive({x:0,y:0}),a=t.maxRadius,l={left:r.offsetLeft,top:r.offsetTop};r!==void 0&&"ontouchstart"in window&&C(r,"touchstart",i=>{Ce(n,i,c,r,a,l)}),s.onUnmounted(()=>{e.removeChild(o)}),s.onDeactivated(()=>{e.removeChild(o)})}});k.MobileJoystick=Me,k.fpsControls=be,Object.defineProperty(k,Symbol.toStringTag,{value:"Module"})});