blockly3d/src/components/JoystickView.tsx

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>
</>
);
}