import { useCallback, useState } from "react";
import type React from "react";
export const MAX_DIST = 60;
type Props = {
onInput: (moveX: number, moveY: number) => void;
maxDist?: number;
};
type Point = { x: number; y: number };
type UseJoystickReturn = {
isMoving: boolean;
basePos: Point;
stickPos: Point;
handleStart: (e: React.TouchEvent | React.MouseEvent) => void;
handleMove: (e: React.TouchEvent | React.MouseEvent) => void;
handleEnd: () => void;
};
const getClientPoint = (e: React.TouchEvent | React.MouseEvent): Point | null => {
if ("touches" in e) {
const touch = e.touches[0];
if (!touch) return null;
return { x: touch.clientX, y: touch.clientY };
}
return { x: e.clientX, y: e.clientY };
};
export const useJoystick = ({ onInput, maxDist }: Props): UseJoystickReturn => {
const [isMoving, setIsMoving] = useState(false);
const [basePos, setBasePos] = useState<Point>({ x: 0, y: 0 });
const [stickPos, setStickPos] = useState<Point>({ x: 0, y: 0 });
const limit = maxDist ?? MAX_DIST;
const handleStart = useCallback((e: React.TouchEvent | React.MouseEvent) => {
const point = getClientPoint(e);
if (!point) return;
setBasePos(point);
setStickPos({ x: 0, y: 0 });
setIsMoving(true);
}, []);
const handleMove = useCallback(
(e: React.TouchEvent | React.MouseEvent) => {
if (!isMoving) return;
const point = getClientPoint(e);
if (!point) return;
const dx = point.x - basePos.x;
const dy = point.y - basePos.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const limitedDist = Math.min(dist, limit);
const offsetX = Math.cos(angle) * limitedDist;
const offsetY = Math.sin(angle) * limitedDist;
const normalizedX = offsetX / limit;
const normalizedY = offsetY / limit;
setStickPos({ x: offsetX, y: offsetY });
onInput(normalizedX, normalizedY);
},
[isMoving, basePos.x, basePos.y, limit, onInput]
);
const handleEnd = useCallback(() => {
setIsMoving(false);
setStickPos({ x: 0, y: 0 });
onInput(0, 0);
}, [onInput]);
return { isMoving, basePos, stickPos, handleStart, handleMove, handleEnd };
};