import { useRef, useCallback, useState } from 'react'; import { useMouseVelocity } from './useMouseVelocity'; import { SpringPhysics } from '@/engine/SpringPhysics'; import { DistortionArea, Point } from '@/types'; import { MouseInteractionConfig } from '@/types/interaction'; /** * 점이 사각형 내부에 있는지 확인 */ const isPointInPolygon = (point: Point, polygon: Point[]): boolean => { let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x, yi = polygon[i].y; const xj = polygon[j].x, yj = polygon[j].y; const intersect = ((yi > point.y) !== (yj > point.y)) && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; }; /** * 마우스 인터랙션 기반 기존 영역 제어 훅 * 마우스가 지나가는 모든 영역에 효과 적용 */ export const useMouseInteraction = ( containerRef: React.RefObject, config: MouseInteractionConfig ) => { const { getState } = useMouseVelocity(containerRef); const [interactingAreaIndices, setInteractingAreaIndices] = useState>(new Set()); const springPhysicsMapRef = useRef>(new Map()); /** * 특정 영역의 스프링 물리 엔진 가져오기 (없으면 생성) */ const getSpringPhysics = useCallback((areaIndex: number): SpringPhysics => { if (!springPhysicsMapRef.current.has(areaIndex)) { springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics)); } return springPhysicsMapRef.current.get(areaIndex)!; }, [config.physics]); /** * 기존 영역들의 dragVector를 마우스 인터랙션으로 업데이트 */ const updateInteraction = useCallback((areas: DistortionArea[], deltaTime: number): DistortionArea[] => { if (!config.enabled) return areas; const mouseState = getState(); // 마우스 클릭/드래그 중이고 위치가 있으면 if (mouseState.isDragging && mouseState.position) { // 현재 마우스 위치가 포함된 모든 영역 찾기 const currentlyInAreas = new Set(); for (let i = 0; i < areas.length; i++) { if (isPointInPolygon(mouseState.position, areas[i].basePoints)) { currentlyInAreas.add(i); // 새로 진입한 영역이면 스프링 리셋 if (!interactingAreaIndices.has(i)) { getSpringPhysics(i).reset(); } } } // 이전에 인터랙션하던 영역에서 벗어났으면 평형으로 복귀 interactingAreaIndices.forEach((areaIndex) => { if (!currentlyInAreas.has(areaIndex)) { getSpringPhysics(areaIndex).returnToEquilibrium(); } }); // 인터랙션 영역 업데이트 setInteractingAreaIndices(currentlyInAreas); // 현재 위치의 모든 영역에 속도 적용 const velocityMult = config.velocityMultiplier || 1.0; const velocityMag = Math.sqrt( mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 ); const minVel = config.minVelocity || 0.05; const maxVel = config.maxVelocity || 5.0; // 속도 클램핑 let clampedVelocity = mouseState.velocity; if (velocityMag > maxVel) { const scale = maxVel / velocityMag; clampedVelocity = { x: mouseState.velocity.x * scale, y: mouseState.velocity.y * scale, }; } currentlyInAreas.forEach((areaIndex) => { const spring = getSpringPhysics(areaIndex); if (velocityMag >= minVel) { // 드래그 중: 마우스 속도를 목표로 설정 spring.setTarget(clampedVelocity, velocityMult); } else { // 드래그 중이지만 마우스가 멈춰있으면 평형으로 복귀 spring.returnToEquilibrium(); } }); } else { // 마우스를 놓았으면 인터랙션 중이던 모든 영역에 튕김 효과 if (interactingAreaIndices.size > 0) { const velocityMult = config.velocityMultiplier || 1.0; const maxVel = config.maxVelocity || 5.0; // 속도 클램핑 const velocityMag = Math.sqrt( mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 ); let clampedVelocity = mouseState.velocity; if (velocityMag > maxVel) { const scale = maxVel / velocityMag; clampedVelocity = { x: mouseState.velocity.x * scale, y: mouseState.velocity.y * scale, }; } // 모든 인터랙션 영역에 초기 속도 설정 interactingAreaIndices.forEach((areaIndex) => { const spring = getSpringPhysics(areaIndex); spring.setInitialVelocity(clampedVelocity, velocityMult); }); setInteractingAreaIndices(new Set()); } } // 모든 영역의 스프링 물리 업데이트 return areas.map((area, index) => { const spring = springPhysicsMapRef.current.get(index); if (!spring) return area; // 현재 드래그 중인 영역이거나 스프링이 활성 상태일 때만 업데이트 const springVelocity = spring.getVelocity(); const springDisplacement = spring.getDisplacement(); const isSpringActive = Math.sqrt(springVelocity.x ** 2 + springVelocity.y ** 2) > 0.001 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 0.001; // 드래그 중이 아니고 스프링도 비활성이면 업데이트 안 함 if (!interactingAreaIndices.has(index) && !isSpringActive) { return area; } // 스프링 물리 업데이트 const displacement = spring.update(deltaTime); // 변위가 거의 0이면 원래 dragVector 유지 const displacementMag = Math.sqrt(displacement.x ** 2 + displacement.y ** 2); if (displacementMag < 0.001) { return area; } // 스프링 변위를 dragVector에 추가 (기존 애니메이션과 혼합) // dragVector는 텍스처 샘플링 좌표 이동이므로, // 마우스 드래그 방향과 반대로 적용해야 이미지가 드래그 방향으로 밀림 // 예: 우→좌 드래그(velocity < 0) → dragVector > 0 → 이미지 왼쪽으로 밀림 return { ...area, dragVector: { x: area.dragVector.x - displacement.x, y: area.dragVector.y - displacement.y, }, }; }); }, [config, getState, interactingAreaIndices, getSpringPhysics]); /** * 물리 파라미터 업데이트 */ const updateConfig = useCallback((newConfig: Partial) => { const physicsConfig = newConfig.physics; if (physicsConfig) { // 모든 스프링 물리 엔진의 설정 업데이트 springPhysicsMapRef.current.forEach((spring) => { spring.setConfig(physicsConfig); }); } }, []); /** * 모든 영역의 스프링 상태 리셋 */ const reset = useCallback(() => { springPhysicsMapRef.current.forEach((spring) => { spring.reset(); }); setInteractingAreaIndices(new Set()); }, []); return { updateInteraction, updateConfig, reset, }; };