상세 컨텐츠

본문 제목

touch event - @react-three/fiber

three.js

by 폴리프레임 2024. 11. 23. 01:21

본문

반응형

데스크 탑에서 Drag 를 이용하여 3D 도형의 회전을 수행했으나, 터치 스크린에서는 touchstart, touchmove, touchend 이벤트를 추가해야합니다. 이를 추가하여 구현합니다.

// Blocks.jsx
/* eslint-disable */
import React, { useRef, useMemo, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Canvas, useFrame } from "@react-three/fiber";
import { Html } from "@react-three/drei";
import * as THREE from "three";

function createMesh(geometry, material, position, name) {
  const mesh = new THREE.Mesh(geometry, material.clone());
  mesh.position.set(...position);
  mesh.name = name;
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

function Box({ geometry, material, position, name }) {
  const navigate = useNavigate();
  
  const [color, setColor] = useState(() => new THREE.Color(Math.random(), Math.random(), Math.random()));
  const [showTooltip, setShowTooltip] = useState(false);

  const mesh = useMemo(
    () => createMesh(geometry, material, position, name),
    [geometry, material, position, name]
  );

  const handlePointerOver = (event) => {
    document.body.style.cursor = 'pointer';
    setShowTooltip(true);
  };

  const handlePointerOut = (event) => {
    document.body.style.cursor = 'default';
    setShowTooltip(false);
  };

  const handleClick = (event) => {
    // 랜덤 색상 생성
    const newColor = new THREE.Color(Math.random(), Math.random(), Math.random());
    setColor(newColor);
    navigate(`/${name}`);
  };

  return (
    <mesh
      position={position}
      castShadow
      receiveShadow
      onPointerOver={handlePointerOver}
      onPointerOut={handlePointerOut}
      onClick={handleClick}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial attach="material" color={color} />
      {showTooltip && (
        <Html position={[0, 1.5, 0]} center>
          <div style={{
            background: "rgba(0, 0, 0, 0.75)",
            color: "white",
            padding: "5px",
            borderRadius: "5px",
            whiteSpace: "nowrap" // 텍스트가 한 줄로 유지되도록 설정
          }}>
            {name}
          </div>
        </Html>
      )}
    </mesh>
  );
}

const Blocks = React.memo(function Blocks(props) {
  const modelRef = useRef();
  const isDraggingRef = useRef(false);
  const rotationRef = useRef({ x: 0, y: 0 });
  const [scale, setScale] = useState([2.5, 2.5, 2.5]);

  // Define geometry and material within Blocks
  const boxGeometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
  const material = useMemo(() => new THREE.MeshStandardMaterial(), []);

  useFrame((state) => {
    if (modelRef.current) {
      // 드래그 중일 때 회전만 적용
      if (isDraggingRef.current) {
        modelRef.current.rotation.y += rotationRef.current.y;
        modelRef.current.rotation.x += rotationRef.current.x;
        rotationRef.current.x = 0;
        rotationRef.current.y = 0;
      } else {
        // 드래그가 아닐 때 y축 위치 애니메이션 적용
        modelRef.current.position.y = -1.5 + Math.sin(state.clock.getElapsedTime()) * 0.3;
      }
    }
  });

  useEffect(() => {
    const handleMouseDown = () => {
      isDraggingRef.current = true;
    };

    const handleMouseUp = () => {
      isDraggingRef.current = false;
    };

    const handleMouseMove = (event) => {
      if (isDraggingRef.current) {
        rotationRef.current = {
          x: event.movementY * 0.005,
          y: event.movementX * 0.005,
        };
      }
    };

    const handleTouchStart = () => {
      isDraggingRef.current = true;
    };

    const handleTouchEnd = () => {
      isDraggingRef.current = false;
    };

    const handleTouchMove = (event) => {
      if (isDraggingRef.current && event.touches.length === 1) {
        const touch = event.touches[0];
        rotationRef.current = {
          x: touch.movementY * 0.005,
          y: touch.movementX * 0.005,
        };
      }
    };

    window.addEventListener("mousedown", handleMouseDown);
    window.addEventListener("mouseup", handleMouseUp);
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("touchstart", handleTouchStart);
    window.addEventListener("touchend", handleTouchEnd);
    window.addEventListener("touchmove", handleTouchMove);

    return () => {
      window.removeEventListener("mousedown", handleMouseDown);
      window.removeEventListener("mouseup", handleMouseUp);
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("touchstart", handleTouchStart);
      window.removeEventListener("touchend", handleTouchEnd);
      window.removeEventListener("touchmove", handleTouchMove);
    };
  }, []);

  useEffect(() => {
    const handleResize = () => {
      if (window.innerWidth < 768) {
        setScale([1, 1, 1]);
      } else {
        setScale([2.5, 2.5, 2.5]);
      }
    };

    window.addEventListener("resize", handleResize);

    // Initial check
    handleResize();

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return (
    <group
      {...props}
      dispose={null}
      ref={modelRef}
      position={[0, 0, 0]}
      scale={scale}
    >
      <Box
        geometry={boxGeometry}
        material={material}
        position={[1.5, 0, 0]}
        name="Box A"
      />
      <Box
        geometry={boxGeometry}
        material={material}
        position={[-1.5, 0, 0]}
        name="Box B"
      />
      <Box
        geometry={boxGeometry}
        material={material}
        position={[0, 1.5, 0]}
        name="Box C"
      />
      <Box
        geometry={boxGeometry}
        material={material}
        position={[0, -1.5, 0]}
        name="Box D"
      />
    </group>
  );
});

export default Blocks;

 

사방에서 들어오는 에니메이션을 추가한 코드입니다.

/* eslint-disable */
import React, { useRef, useMemo, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Canvas, useFrame } from "@react-three/fiber";
import { Html } from "@react-three/drei";
import { gsap } from "gsap"; // 애니메이션을 위해 gsap 사용
import * as THREE from "three";

function createMesh(geometry, material, position, name) {
  const mesh = new THREE.Mesh(geometry, material.clone());
  mesh.position.set(...position);
  mesh.name = name;
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

// function Box({ geometry, material, position, name }) {
  function Box({ geometry, material, position, initialPosition, name }) {
  // const navigate = useNavigate();
  // Random color generation
  const [color, setColor] = useState(
    () => new THREE.Color(Math.random(), Math.random(), Math.random())
  );
  const [showTooltip, setShowTooltip] = useState(false);

  // const mesh = useMemo(
  //   () => createMesh(geometry, material, position, name),
  //   [geometry, material, position, name]
  // );
  const mesh = useRef();

  useEffect(() => {
    const animatePosition = () => {
        if (mesh.current) {
          gsap.fromTo(mesh.current.position,
            { 
              x: initialPosition[0], 
              y: initialPosition[1], 
              z: initialPosition[2] 
            },
            {
              x: position[0],
              y: position[1],
              z: position[2],
              duration:1.5,
              ease: "power2.inOut"
            }
          );
        } else {
      requestAnimationFrame(animatePosition);
    }
  };
  requestAnimationFrame(animatePosition);
  }, [position, initialPosition]);

  const handlePointerOver = (event) => {
    document.body.style.cursor = "pointer";
    setShowTooltip(true);
  };
  const handlePointerOut = (event) => {
    document.body.style.cursor = "default";
    setShowTooltip(false);
  };
  const handleClick = (event) => {
    const newColor = new THREE.Color(
      Math.random(),
      Math.random(),
      Math.random()
    );
    setColor(newColor);
    // navigate(`/${name}`)
    console.log("Clicked :", `/${name}`);
  };
  return (
    <mesh
      ref={mesh}
      position={initialPosition}
      castShadow
      receiveShadow
      onPointerOver={handlePointerOver}
      onPointerOut={handlePointerOut}
      onClick={handleClick}
    >
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial attach="material" color={color} />
      {showTooltip && (
        <Html position={[0.5, 0, 0]} center>
          <div
            style={{
              background: "rgba(0, 0, 0, 0.70)",
              color: "white",
              padding: "5px",
              borderRadius: "5px",
              whiteSpace: "nowrap", // 텍스트가 한 줄로 유지되도록 설정
            }}
          >
            {name}
          </div>
        </Html>
      )}
    </mesh>
  );
}

const Blocks = React.memo(function Blocks(props) {
  const modelRef = useRef();
  const isDraggingRef = useRef(false);
  const rotationRef = useRef({ x: 0, y: 0 });
  const [scale, setScale] = useState([2.5, 2.5, 2.5]);

  // Define geometry and material within Blocks
  const boxGeometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
  const material = useMemo(() => new THREE.MeshStandardMaterial(), []);

  // useFrame((state) => {
  //   modelRef.current.position.y =
  //     -1.5 + Math.sin(state.clock.elapsedTime) * 0.3;
  // });
  useFrame((state) => {
    if (modelRef.current) {
      // 드래그 중일때만 회전 만 적용
      if (isDraggingRef.current) {
        modelRef.current.rotation.y += rotationRef.current.y;
        modelRef.current.rotation.x += rotationRef.current.x; // 회전 후에 값을 초기화하여 부드러운 회전 유지
        rotationRef.current.x = 0;
        rotationRef.current.y = 0;
      } else {
        // 드래그가 아닐 때 y축 위치 애니메이션 적용
        modelRef.current.position.y =
          -1.5 + Math.sin(state.clock.getElapsedTime()) * 0.3;
      }
    }
  });

  useEffect(() => {
    const handleMouseDown = () => {
      isDraggingRef.current = true;
    };
    const handleMouseUp = () => {
      isDraggingRef.current = false;
    };
    const handleMouseMove = (event) => {
      if (isDraggingRef.current) {
        rotationRef.current = {
          x: event.movementY * 0.005,
          y: event.movementX * 0.005,
        };
      }
    };
    // touch event for moblie
    const handleTouchStart = () => {
      isDraggingRef.current = true;
    };

    const handleTouchEnd = () => {
      isDraggingRef.current = false;
    };

    const handleTouchMove = (event) => {
      if (isDraggingRef.current && event.touches.length === 1) {
        const touch = event.touches[0];
        rotationRef.current = {
          x: touch.movementY * 0.005,
          y: touch.movementX * 0.005,
        };
      }
    };

    window.addEventListener("mousedown", handleMouseDown);
    window.addEventListener("mouseup", handleMouseUp);
    window.addEventListener("mousemove", handleMouseMove);
    window.addEventListener("touchstart", handleTouchStart);
    window.addEventListener("touchend", handleTouchEnd);
    window.addEventListener("touchmove", handleTouchMove);

    return () => {
      window.removeEventListener("mousedown", handleMouseDown);
      window.removeEventListener("mouseup", handleMouseUp);
      window.removeEventListener("mousemove", handleMouseMove);
      window.removeEventListener("touchstart", handleTouchStart);
      window.removeEventListener("touchend", handleTouchEnd);
      window.removeEventListener("touchmove", handleTouchMove);
    };
  }, []);

  useEffect(() => {
    const handleResize = () => {
      if (window.innerWidth < 600) {
        //768
        setScale([1, 1, 1]);
      } else {
        setScale([2.5, 2.5, 2.5]);
      }
    };
    window.addEventListener("resize", handleResize);

    // Initial check
    handleResize();

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return (
    <group
      {...props}
      dispose={null}
      ref={modelRef}
      position={[0, 0, 0]}
      scale={scale}
      // rotation={[0.25, 0, 0]}
    >
     <Box 
        geometry={boxGeometry} 
        material={material} 
        position={[1.5, 0, 0]} 
        initialPosition={[3, 0, 0]} // 초기 위치를 오른쪽 멀리 설정 
        name="Box A" 
      /> 
    <Box 
        geometry={boxGeometry} 
        material={material} 
        position={[-1.5, 0, 0]} 
        initialPosition={[-3, 0, 0]} // 초기 위치를 왼쪽 멀리 설정 
        name="Box B" 
      /> 
    <Box 
        geometry={boxGeometry} 
        material={material} 
        position={[0, 1.5, 0]} 
        initialPosition={[0, 3, 0]} // 초기 위치를 위쪽 멀리 설정 
        name="Box C" 
      /> 
    <Box 
        geometry={boxGeometry} 
        material={material} 
        position={[0, -1.5, 0]} 
        initialPosition={[0, -3, 0]} // 초기 위치를 아래쪽 멀리 설정 
        name="Box D"
        />
      
    </group>
  );
});

export default Blocks;

'three.js' 카테고리의 다른 글

OrbitControls - target, maxPolarAngle  (0) 2024.12.06
이미지 매핑하기  (1) 2024.11.24
video backgorund with @react-three/fibre  (1) 2024.11.22
resize - Three.js  (0) 2024.11.22
lookAt(), getWorldDirection()  (1) 2024.11.21

관련글 더보기