responsive-image-canvas/src/hooks/useMouseInteraction.ts
BaekRyang c18f3fffb5 Refactor: Update import paths with alias
- `@/types` 경로를 사용하여 타입 관련 import 경로를 수정했습니다.
- `@/engine` 경로를 사용하여 엔진 관련 import 경로를 수정했습니다.
- `@/editor` 경로를 사용하여 에디터 관련 import 경로를 수정했습니다.
- `@/components` 경로를 사용하여 컴포넌트 관련 import 경로를 수정했습니다.
- `@/hooks` 경로를 사용하여 훅 관련 import 경로를 수정했습니다.
- `@/utils` 경로를 사용하여 유틸리티 관련 import 경로를 수정했습니다.
2025-11-06 09:41:12 +09:00

203 lines
6.6 KiB
TypeScript

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<HTMLElement | null>,
config: MouseInteractionConfig
) => {
const { getState } = useMouseVelocity(containerRef);
const [interactingAreaIndices, setInteractingAreaIndices] = useState<Set<number>>(new Set());
const springPhysicsMapRef = useRef<Map<number, SpringPhysics>>(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<number>();
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<MouseInteractionConfig>) => {
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,
};
};