import * as THREE from "three" import { Canvas, type ThreeElements } from "@react-three/fiber"; import React, { useEffect, useState, forwardRef, useMemo, type ReactNode, useCallback, Suspense } from "react"; import { useSpring, animated } from '@react-spring/three' import { useJoystickControls } from "./stores/useJoystickControls"; const JoystickComponents = (props: EcctrlJoystickProps) => { /** * Preset values/components */ let joystickCenterX: number = 0 let joystickCenterY: number = 0 let joystickHalfWidth: number = 0 let joystickHalfHeight: number = 0 let joystickMaxDis: number = 0 let joystickDis: number = 0 let joystickAng: number = 0 const touch1MovementVec2 = useMemo(() => new THREE.Vector2(), []) const joystickMovementVec2 = useMemo(() => new THREE.Vector2(), []) const [windowSize, setWindowSize] = useState({ innerHeight, innerWidth }) const joystickDiv: null | HTMLDivElement = document.querySelector("#ecctrl-joystick") /** * Animation preset */ const [springs, api] = useSpring( () => ({ topRotationX: 0, topRotationY: 0, basePositionX: 0, basePositionY: 0, config: { tension: 600 } }) ) /** * Joystick component geometries */ const joystickBaseGeo = useMemo(() => new THREE.CylinderGeometry(2.3, 2.1, 0.3, 16), []) const joystickStickGeo = useMemo(() => new THREE.CylinderGeometry(0.3, 0.3, 3, 6), []) const joystickHandleGeo = useMemo(() => new THREE.SphereGeometry(1.4, 8, 8), []) /** * Joystick component materials */ const joystickBaseMaterial = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.3 }), []) const joystickStickMaterial = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.3 }), []) const joystickHandleMaterial = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.7 }), []) /** * Joystick store setup */ const setJoystick = useJoystickControls((state) => state.setJoystick) const resetJoystick = useJoystickControls((state) => state.resetJoystick) // Touch move function const onTouchMove = useCallback((e: TouchEvent) => { e.preventDefault(); e.stopImmediatePropagation(); const touch1 = e.targetTouches[0]; const touch1MovementX = touch1.pageX - joystickCenterX const touch1MovementY = -(touch1.pageY - joystickCenterY) touch1MovementVec2.set(touch1MovementX, touch1MovementY) joystickDis = Math.min(Math.sqrt(Math.pow(touch1MovementX, 2) + Math.pow(touch1MovementY, 2)), joystickMaxDis) joystickAng = touch1MovementVec2.angle() joystickMovementVec2.set(joystickDis * Math.cos(joystickAng), joystickDis * Math.sin(joystickAng)) const runState = joystickDis > joystickMaxDis * (props.joystickRunSensitivity ?? 0.9) // Apply animations api.start({ topRotationX: -joystickMovementVec2.y / joystickHalfHeight, topRotationY: joystickMovementVec2.x / joystickHalfWidth, basePositionX: joystickMovementVec2.x * 0.002, basePositionY: joystickMovementVec2.y * 0.002, }) // Pass valus to joystick store setJoystick(joystickDis, joystickAng, runState) }, [api, windowSize]) // Touch end function const onTouchEnd = (e: TouchEvent) => { // Reset animations api.start({ topRotationX: 0, topRotationY: 0, basePositionX: 0, basePositionY: 0, }) // Reset joystick store values resetJoystick() } // Reset window size function const onWindowResize = () => { setWindowSize({ innerHeight: window.innerHeight, innerWidth: window.innerWidth }) } useEffect(() => { if (!joystickDiv) return; const joystickPositionX = joystickDiv.getBoundingClientRect().x const joystickPositionY = joystickDiv.getBoundingClientRect().y joystickHalfWidth = joystickDiv.getBoundingClientRect().width / 2 joystickHalfHeight = joystickDiv.getBoundingClientRect().height / 2 joystickMaxDis = joystickHalfWidth * 0.65 joystickCenterX = joystickPositionX + joystickHalfWidth joystickCenterY = joystickPositionY + joystickHalfHeight joystickDiv.addEventListener("touchmove", onTouchMove, { passive: false }) joystickDiv.addEventListener("touchend", onTouchEnd) window.visualViewport?.addEventListener("resize", onWindowResize) return () => { joystickDiv.removeEventListener("touchmove", onTouchMove) joystickDiv.removeEventListener("touchend", onTouchEnd) window.visualViewport?.removeEventListener("resize", onWindowResize) } }) return ( ) } const ButtonComponents = ({ buttonNumber = 1, ...props }: EcctrlJoystickProps) => { /** * Button component geometries */ const buttonLargeBaseGeo = useMemo(() => new THREE.CylinderGeometry(1.1, 1, 0.3, 16), []) const buttonSmallBaseGeo = useMemo(() => new THREE.CylinderGeometry(0.9, 0.8, 0.3, 16), []) const buttonTop1Geo = useMemo(() => new THREE.CylinderGeometry(0.9, 0.9, 0.5, 16), []) const buttonTop2Geo = useMemo(() => new THREE.CylinderGeometry(0.9, 0.9, 0.5, 16), []) const buttonTop3Geo = useMemo(() => new THREE.CylinderGeometry(0.7, 0.7, 0.5, 16), []) const buttonTop4Geo = useMemo(() => new THREE.CylinderGeometry(0.7, 0.7, 0.5, 16), []) const buttonTop5Geo = useMemo(() => new THREE.CylinderGeometry(0.7, 0.7, 0.5, 16), []) /** * Button component materials */ const buttonBaseMaterial = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.3 }), []) const buttonTop1Material = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.5 }), []) const buttonTop2Material = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.5 }), []) const buttonTop3Material = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.5 }), []) const buttonTop4Material = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.5 }), []) const buttonTop5Material = useMemo(() => new THREE.MeshNormalMaterial({ transparent: true, opacity: 0.5 }), []) const buttonDiv: null | HTMLDivElement = document.querySelector("#ecctrl-button") /** * Animation preset */ const [springs, api] = useSpring( () => ({ buttonTop1BaseScaleY: 1, buttonTop1BaseScaleXAndZ: 1, buttonTop2BaseScaleY: 1, buttonTop2BaseScaleXAndZ: 1, buttonTop3BaseScaleY: 1, buttonTop3BaseScaleXAndZ: 1, buttonTop4BaseScaleY: 1, buttonTop4BaseScaleXAndZ: 1, buttonTop5BaseScaleY: 1, buttonTop5BaseScaleXAndZ: 1, config: { tension: 600 } }) ) /** * Button store setup */ const pressButton1 = useJoystickControls((state) => state.pressButton1) const pressButton2 = useJoystickControls((state) => state.pressButton2) const pressButton3 = useJoystickControls((state) => state.pressButton3) const pressButton4 = useJoystickControls((state) => state.pressButton4) const pressButton5 = useJoystickControls((state) => state.pressButton5) const releaseAllButtons = useJoystickControls((state) => state.releaseAllButtons) // Pointer down function const onPointerDown = (number: number) => { switch (number) { case 1: pressButton1() api.start({ buttonTop1BaseScaleY: 0.5, buttonTop1BaseScaleXAndZ: 1.15, }) break; case 2: pressButton2() api.start({ buttonTop2BaseScaleY: 0.5, buttonTop2BaseScaleXAndZ: 1.15, }) break; case 3: pressButton3() api.start({ buttonTop3BaseScaleY: 0.5, buttonTop3BaseScaleXAndZ: 1.15, }) break; case 4: pressButton4() api.start({ buttonTop4BaseScaleY: 0.5, buttonTop4BaseScaleXAndZ: 1.15, }) break; case 5: pressButton5() api.start({ buttonTop5BaseScaleY: 0.5, buttonTop5BaseScaleXAndZ: 1.15, }) break; default: break; } } // Pointer up function const onPointerUp = () => { releaseAllButtons(); api.start({ buttonTop1BaseScaleY: 1, buttonTop1BaseScaleXAndZ: 1, buttonTop2BaseScaleY: 1, buttonTop2BaseScaleXAndZ: 1, buttonTop3BaseScaleY: 1, buttonTop3BaseScaleXAndZ: 1, buttonTop4BaseScaleY: 1, buttonTop4BaseScaleXAndZ: 1, buttonTop5BaseScaleY: 1, buttonTop5BaseScaleXAndZ: 1, }) } useEffect(() => { if (!buttonDiv) return buttonDiv.addEventListener("pointerup", onPointerUp) return () => { buttonDiv.removeEventListener("pointerup", onPointerUp) } }) return ( {/* Button 1 */} {buttonNumber > 0 && onPointerDown(1)} /> } {/* Button 2 */} {buttonNumber > 1 && onPointerDown(2)} /> } {/* Button 3 */} {buttonNumber > 2 && onPointerDown(3)} /> } {/* Button 4 */} {buttonNumber > 3 && onPointerDown(4)} /> } {/* Button 5 */} {buttonNumber > 4 && onPointerDown(5)} /> } ) } export const EcctrlJoystick = forwardRef((props, ref) => { const joystickWrapperStyle: React.CSSProperties = { userSelect: "none", MozUserSelect: "none", WebkitUserSelect: "none", msUserSelect: "none", touchAction: "none", pointerEvents: "none", overscrollBehavior: "none", position: 'fixed', zIndex: '9999', height: props.joystickHeightAndWidth || '200px', width: props.joystickHeightAndWidth || '200px', left: props.joystickPositionLeft || '0', bottom: props.joystickPositionBottom || '0', } const buttonWrapperStyle: React.CSSProperties = { userSelect: "none", MozUserSelect: "none", WebkitUserSelect: "none", msUserSelect: "none", touchAction: "none", pointerEvents: "none", overscrollBehavior: "none", position: 'fixed', zIndex: '9999', height: props.buttonHeightAndWidth || '200px', width: props.buttonHeightAndWidth || '200px', right: props.buttonPositionRight || '0', bottom: props.buttonPositionBottom || '0', } return (
e.preventDefault()}> {props.children}
{ props.buttonNumber !== 0 &&
e.preventDefault()}> {props.children}
}
) }) export type EcctrlJoystickProps = { // Joystick props children?: ReactNode; joystickRunSensitivity?: number; joystickPositionLeft?: number; joystickPositionBottom?: number; joystickHeightAndWidth?: number; joystickCamZoom?: number; joystickCamPosition?: [x: number, y: number, z: number]; joystickBaseProps?: ThreeElements['mesh']; joystickStickProps?: ThreeElements['mesh']; joystickHandleProps?: ThreeElements['mesh']; // Touch buttons props buttonNumber?: number; buttonPositionRight?: number; buttonPositionBottom?: number; buttonHeightAndWidth?: number; buttonCamZoom?: number; buttonCamPosition?: [x: number, y: number, z: number]; buttonGroup1Position?: [x: number, y: number, z: number]; buttonGroup2Position?: [x: number, y: number, z: number]; buttonGroup3Position?: [x: number, y: number, z: number]; buttonGroup4Position?: [x: number, y: number, z: number]; buttonGroup5Position?: [x: number, y: number, z: number]; buttonLargeBaseProps?: ThreeElements['mesh']; buttonSmallBaseProps?: ThreeElements['mesh']; buttonTop1Props?: ThreeElements['mesh']; buttonTop2Props?: ThreeElements['mesh']; buttonTop3Props?: ThreeElements['mesh']; buttonTop4Props?: ThreeElements['mesh']; buttonTop5Props?: ThreeElements['mesh']; };