- `@/types` 경로를 사용하여 타입 관련 import 경로를 수정했습니다. - `@/engine` 경로를 사용하여 엔진 관련 import 경로를 수정했습니다. - `@/editor` 경로를 사용하여 에디터 관련 import 경로를 수정했습니다. - `@/components` 경로를 사용하여 컴포넌트 관련 import 경로를 수정했습니다. - `@/hooks` 경로를 사용하여 훅 관련 import 경로를 수정했습니다. - `@/utils` 경로를 사용하여 유틸리티 관련 import 경로를 수정했습니다.
203 lines
6.6 KiB
TypeScript
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,
|
|
};
|
|
};
|