Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
3.7 kB · 132 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133import { useSpring } from "@react-spring/web";import { useDrag } from "@use-gesture/react";import throttle from "lodash.throttle";import { useImperativeHandle, useRef } from "react";
import { type GamepadKey, useGamepadValue } from "./useGamepadStore";
function distance(x1: number, y1: number, x2 = 0, y2 = 0) { const a = x1 - x2; const b = y1 - y2;
return Math.sqrt(a * a + b * b);}const minDistance = 10;export function useJoystick({ axisSnap, axis, onDrag = () => {}, throttleMs = 100, gamepadKeys, sticky, ref,}: { axisSnap?: boolean; axis?: "x" | "y" | undefined; onDrag?: (values: { x: number; y: number }, down: boolean) => void; throttleMs?: number; gamepadKeys?: { x: GamepadKey; y: GamepadKey }; sticky?: boolean; ref?: React.RefObject<{ reset: () => void; set: (x: number, y: number) => void; } | null>;} = {}) { const callback = useRef(throttle(onDrag, throttleMs)); const containerRef = useRef<HTMLDivElement>(null); const dragDown = useRef(false); const maxDistance = useRef(0); const offsetRef = useRef<[number, number]>([0, 0]); const [{ xy }, set] = useSpring(() => ({ xy: [0, 0], config: { mass: 1, tension: 280, friction: 30 }, }));
useImperativeHandle(ref, () => ({ reset() { set({ xy: [0, 0] }); callback.current({ x: 0, y: 0 }, false); }, set(x, y) { set({ xy: [x, y] }); }, }));
function startAtOffset(xy: [number, number]) { offsetRef.current = xy; set({ xy, immediate: true }); } const bind = useDrag(({ down, first, movement: [x, y] }) => { if (first) { maxDistance.current = (containerRef.current?.getBoundingClientRect()[axis === "y" ? "height" : "width"] || 0) / 2; } dragDown.current = down; x += offsetRef.current[0]; y += offsetRef.current[1];
const dist = distance(x, y); if (dist > maxDistance.current) { if (axis) { x = maxDistance.current * (x > 0 ? 1 : -1); y = maxDistance.current * (y > 0 ? 1 : -1); } else { const theta = Math.abs(Math.atan(y / x)); x = maxDistance.current * Math.cos(theta) * (x > 0 ? 1 : -1); y = maxDistance.current * Math.sin(theta) * (y > 0 ? 1 : -1); } } if (axisSnap && (Math.abs(x) > minDistance || Math.abs(y) > minDistance)) { if (x < minDistance && x > minDistance * -1) x = 0; if (y < minDistance && y > minDistance * -1) y = 0; } if (!down && sticky) return; set({ xy: down ? (axis ? [axis === "x" ? x : 0, axis === "y" ? y : 0] : [x, y]) : [0, 0], immediate: down, }); callback.current?.( down ? { x: x / maxDistance.current, y: y / maxDistance.current } : { x: 0, y: 0 }, down, ); });
const gamepadValues = useRef([0, 0]);
function setGamepadValue() { if (dragDown.current) return; if (!maxDistance.current) { maxDistance.current = (containerRef.current?.getBoundingClientRect()[axis === "y" ? "height" : "width"] || 0) / 2; } let [x, y] = gamepadValues.current; x *= maxDistance.current; y *= maxDistance.current; const dist = distance(x, y); if (dist > maxDistance.current) { const theta = Math.abs(Math.atan(y / x)); x = maxDistance.current * Math.cos(theta) * (x > 0 ? 1 : -1); y = maxDistance.current * Math.sin(theta) * (y > 0 ? 1 : -1); } set({ xy: [x, y], immediate: true, }); callback.current?.( { x: x / maxDistance.current, y: y / maxDistance.current, }, true, ); } useGamepadValue(gamepadKeys?.x, (value) => { gamepadValues.current[0] = value; setGamepadValue(); }); useGamepadValue(gamepadKeys?.y, (value) => { gamepadValues.current[1] = value; setGamepadValue(); }); return [xy, bind, containerRef, startAtOffset] as const;}