144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
import { create } from "nipplejs";
|
|
import { joystickValues } from "../joystickInput";
|
|
|
|
// const isTouch = navigator.maxTouchPoints > 0;
|
|
const isTouch = true; // debug
|
|
|
|
export function JoystickView() {
|
|
const moveZoneRef = useRef<HTMLDivElement>(null);
|
|
const jumpBtnRef = useRef<HTMLDivElement>(null);
|
|
const lookZoneRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
|
|
if (!isTouch)
|
|
return;
|
|
|
|
const moveZone = moveZoneRef.current;
|
|
const jumpBtn = jumpBtnRef.current;
|
|
const lookZone = lookZoneRef.current;
|
|
|
|
if (!moveZone || !jumpBtn || !lookZone)
|
|
return;
|
|
|
|
let moveJoystick: ReturnType<typeof create>;
|
|
let lookJoystick: ReturnType<typeof create>;
|
|
|
|
const setup = () => {
|
|
moveJoystick = create({
|
|
zone: moveZone,
|
|
mode: 'static',
|
|
position: { left: '80px', bottom: '80px' },
|
|
color: 'white',
|
|
shape: 'circle',
|
|
size: 100,
|
|
});
|
|
|
|
lookJoystick = create({
|
|
zone: lookZone,
|
|
mode: 'dynamic',
|
|
dataOnly: true,
|
|
});
|
|
|
|
moveJoystick.on('move', (evt) => {
|
|
joystickValues.move.x = evt.data.vector.x;
|
|
joystickValues.move.y = evt.data.vector.y;
|
|
});
|
|
moveJoystick.on('end', () => {
|
|
joystickValues.move = { x: 0, y: 0 };
|
|
});
|
|
|
|
lookJoystick.on('move', (evt) => {
|
|
joystickValues.look = evt.data.vector;
|
|
});
|
|
lookJoystick.on('end', () => {
|
|
joystickValues.look = { x: 0, y: 0 };
|
|
});
|
|
};
|
|
|
|
const teardown = () => {
|
|
moveJoystick?.destroy();
|
|
lookJoystick?.destroy();
|
|
joystickValues.move = { x: 0, y: 0 };
|
|
joystickValues.look = { x: 0, y: 0 };
|
|
joystickValues.jump = false;
|
|
};
|
|
|
|
setup();
|
|
|
|
const onJumpDown = () => { joystickValues.jump = true; };
|
|
const onJumpUp = () => { joystickValues.jump = false; };
|
|
|
|
jumpBtn.addEventListener('pointerdown', onJumpDown);
|
|
jumpBtn.addEventListener('pointerup', onJumpUp);
|
|
jumpBtn.addEventListener('pointercancel', onJumpUp);
|
|
jumpBtn.addEventListener('pointerleave', onJumpUp);
|
|
|
|
// When the OS interrupts a touch (app switch, notification, etc.) the browser
|
|
// fires pointercancel instead of pointerup, leaving nipplejs with a stuck active
|
|
// identifier. In dynamic mode this silently drops all subsequent touches on the
|
|
// look zone. Destroying and recreating resets nipplejs's internal touch tracking.
|
|
const onVisibilityChange = () => {
|
|
if (document.hidden) {
|
|
teardown();
|
|
} else {
|
|
setup();
|
|
}
|
|
};
|
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
|
|
return () => {
|
|
teardown();
|
|
jumpBtn.removeEventListener('pointerdown', onJumpDown);
|
|
jumpBtn.removeEventListener('pointerup', onJumpUp);
|
|
jumpBtn.removeEventListener('pointercancel', onJumpUp);
|
|
jumpBtn.removeEventListener('pointerleave', onJumpUp);
|
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
};
|
|
}, []);
|
|
|
|
if (!isTouch)
|
|
return null;
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
ref={moveZoneRef}
|
|
id="move-zone"
|
|
style={{ position: 'absolute', left: 0, bottom: 0, width: '30vw', height: '30vw', touchAction: 'none' }}
|
|
/>
|
|
<div
|
|
ref={lookZoneRef}
|
|
id="look-zone"
|
|
style={{ position: 'absolute', right: 0, bottom: 0, width: '100%', height: '100%', touchAction: 'none' }}
|
|
/>
|
|
<div
|
|
ref={jumpBtnRef}
|
|
id="jump-zone"
|
|
style={{
|
|
position: 'absolute',
|
|
right: 30,
|
|
bottom: 30,
|
|
width: 80,
|
|
height: 80,
|
|
borderRadius: '50%',
|
|
background: 'rgba(255,255,255,0.25)',
|
|
border: '2px solid rgba(255,255,255,0.6)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: 'white',
|
|
fontSize: 14,
|
|
fontWeight: 'bold',
|
|
touchAction: 'none',
|
|
userSelect: 'none',
|
|
pointerEvents: 'auto',
|
|
}}
|
|
>
|
|
JUMP
|
|
</div>
|
|
</>
|
|
);
|
|
}
|