feat: Add mouse interaction for physics-based distortion
- 마우스 움직임에 따라 왜곡 영역이 튕기는 효과를 추가했습니다. - `useMouseVelocity` 훅을 사용하여 마우스 속도와 가속도를 추적합니다. - `SpringPhysics` 클래스를 구현하여 스프링 기반 물리 효과를 시뮬레이션합니다. - `useMouseInteraction` 훅은 마우스 이벤트를 감지하고 `SpringPhysics`를 제어하여 왜곡 영역의 `dragVector`를 업데이트합니다. - `ImageDistortion` 컴포넌트에서 `mouseInteraction` prop을 통해 이 기능을 활성화/설정할 수 있습니다.
This commit is contained in:
parent
e531a7a762
commit
7f6a72c058
139
dist/index.d.mts
vendored
139
dist/index.d.mts
vendored
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React$1 from 'react';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,6 +107,65 @@ interface AnimationTicker {
|
|||||||
resume: () => void;
|
resume: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 물리 파라미터
|
||||||
|
*/
|
||||||
|
interface SpringPhysicsConfig {
|
||||||
|
/** 스프링 탄성 계수 (높을수록 빠르게 복원) */
|
||||||
|
stiffness: number;
|
||||||
|
/** 감쇠 계수 (높을수록 빨리 멈춤) */
|
||||||
|
damping: number;
|
||||||
|
/** 질량 (높을수록 느리게 움직임) */
|
||||||
|
mass: number;
|
||||||
|
/** 영향 반경 (정규화 좌표, 기본값 0.2) */
|
||||||
|
influenceRadius: number;
|
||||||
|
/** 최대 왜곡 강도 */
|
||||||
|
maxStrength: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 마우스 인터랙션 설정
|
||||||
|
*/
|
||||||
|
interface MouseInteractionConfig {
|
||||||
|
/** 마우스 인터랙션 활성화 여부 */
|
||||||
|
enabled: boolean;
|
||||||
|
/** 스프링 물리 파라미터 */
|
||||||
|
physics: SpringPhysicsConfig;
|
||||||
|
/** 최소 속도 임계값 (이보다 느리면 효과 없음) */
|
||||||
|
minVelocity?: number;
|
||||||
|
/** 최대 속도 제한 (이보다 빠르면 클램핑) */
|
||||||
|
maxVelocity?: number;
|
||||||
|
/** 속도 승수 (마우스 속도에 곱해지는 값) */
|
||||||
|
velocityMultiplier?: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 마우스 상태
|
||||||
|
*/
|
||||||
|
interface MouseState {
|
||||||
|
/** 현재 마우스 위치 (정규화 좌표) */
|
||||||
|
position: Point | null;
|
||||||
|
/** 이전 마우스 위치 */
|
||||||
|
prevPosition: Point | null;
|
||||||
|
/** 속도 벡터 */
|
||||||
|
velocity: Point;
|
||||||
|
/** 가속도 벡터 */
|
||||||
|
acceleration: Point;
|
||||||
|
/** 마우스가 컨테이너 위에 있는지 */
|
||||||
|
isHovering: boolean;
|
||||||
|
/** 드래그 중인지 */
|
||||||
|
isDragging: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 스프링 물리 상태
|
||||||
|
*/
|
||||||
|
interface SpringState {
|
||||||
|
/** 현재 변위 (displacement) */
|
||||||
|
displacement: Point;
|
||||||
|
/** 현재 속도 */
|
||||||
|
velocity: Point;
|
||||||
|
/** 목표 위치 (평형 상태는 {x:0, y:0}) */
|
||||||
|
target: Point;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageDistortion 컴포넌트 Props
|
* ImageDistortion 컴포넌트 Props
|
||||||
*/
|
*/
|
||||||
@ -122,15 +181,17 @@ interface ImageDistortionProps {
|
|||||||
/** 애니메이션 재생 여부 */
|
/** 애니메이션 재생 여부 */
|
||||||
isPlaying?: boolean;
|
isPlaying?: boolean;
|
||||||
/** 컨테이너 스타일 */
|
/** 컨테이너 스타일 */
|
||||||
style?: React.CSSProperties;
|
style?: React$1.CSSProperties;
|
||||||
/** 컨테이너 클래스명 */
|
/** 컨테이너 클래스명 */
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 마우스 인터랙션 설정 */
|
||||||
|
mouseInteraction?: MouseInteractionConfig;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* GPU 가속 이미지 왜곡 컴포넌트
|
* GPU 가속 이미지 왜곡 컴포넌트
|
||||||
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
||||||
*/
|
*/
|
||||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
declare const ImageDistortion: React$1.FC<ImageDistortionProps>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 에디터 편집 모드
|
* 에디터 편집 모드
|
||||||
@ -248,7 +309,7 @@ interface DistortionEditorProps {
|
|||||||
canvasStyle?: EditorCanvasStyle;
|
canvasStyle?: EditorCanvasStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const DistortionEditor: React.FC<DistortionEditorProps>;
|
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
|
||||||
|
|
||||||
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
||||||
state: EditorState;
|
state: EditorState;
|
||||||
@ -403,6 +464,57 @@ declare class AnimationLoop {
|
|||||||
static updateProgress(areas: DistortionArea[], deltaTime: number): DistortionArea[];
|
static updateProgress(areas: DistortionArea[], deltaTime: number): DistortionArea[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 기반 물리 시뮬레이션 엔진
|
||||||
|
* Hooke's Law와 감쇠를 적용한 스프링-댐퍼 시스템
|
||||||
|
*/
|
||||||
|
declare class SpringPhysics {
|
||||||
|
private config;
|
||||||
|
private state;
|
||||||
|
constructor(config: SpringPhysicsConfig);
|
||||||
|
/**
|
||||||
|
* 물리 파라미터 업데이트
|
||||||
|
*/
|
||||||
|
setConfig(config: Partial<SpringPhysicsConfig>): void;
|
||||||
|
/**
|
||||||
|
* 목표 위치 설정 (마우스 속도 기반)
|
||||||
|
*/
|
||||||
|
setTarget(velocity: Point, velocityMultiplier?: number): void;
|
||||||
|
/**
|
||||||
|
* 초기 속도 설정 (드래그 방향과 속도를 즉시 반영)
|
||||||
|
* 드래그 방향으로 즉시 튕기는 효과
|
||||||
|
*/
|
||||||
|
setInitialVelocity(velocity: Point, multiplier?: number): void;
|
||||||
|
/**
|
||||||
|
* 스프링 물리 업데이트 (Hooke's Law + Damping)
|
||||||
|
* F = -k * x - c * v
|
||||||
|
* a = F / m
|
||||||
|
* v += a * dt
|
||||||
|
* x += v * dt
|
||||||
|
*/
|
||||||
|
update(deltaTime: number): Point;
|
||||||
|
/**
|
||||||
|
* 즉시 충격 적용 (마우스 가속도 기반)
|
||||||
|
*/
|
||||||
|
applyImpulse(acceleration: Point, multiplier?: number): void;
|
||||||
|
/**
|
||||||
|
* 현재 변위 가져오기
|
||||||
|
*/
|
||||||
|
getDisplacement(): Point;
|
||||||
|
/**
|
||||||
|
* 현재 속도 가져오기
|
||||||
|
*/
|
||||||
|
getVelocity(): Point;
|
||||||
|
/**
|
||||||
|
* 상태 리셋
|
||||||
|
*/
|
||||||
|
reset(): void;
|
||||||
|
/**
|
||||||
|
* 마우스가 멈췄을 때 목표를 0으로 설정 (평형 상태로 복귀)
|
||||||
|
*/
|
||||||
|
returnToEquilibrium(): void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* requestAnimationFrame을 사용한 애니메이션 루프 훅
|
* requestAnimationFrame을 사용한 애니메이션 루프 훅
|
||||||
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
||||||
@ -410,4 +522,21 @@ declare class AnimationLoop {
|
|||||||
*/
|
*/
|
||||||
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
||||||
|
|
||||||
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor };
|
/**
|
||||||
|
* 마우스 위치, 속도, 가속도를 추적하는 훅
|
||||||
|
*/
|
||||||
|
declare const useMouseVelocity: (containerRef: React.RefObject<HTMLElement | null>) => {
|
||||||
|
getState: () => MouseState;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 인터랙션 기반 기존 영역 제어 훅
|
||||||
|
* 기존 영역을 손으로 튕기는 효과
|
||||||
|
*/
|
||||||
|
declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement | null>, config: MouseInteractionConfig) => {
|
||||||
|
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];
|
||||||
|
updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void;
|
||||||
|
reset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };
|
||||||
|
|||||||
139
dist/index.d.ts
vendored
139
dist/index.d.ts
vendored
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React$1 from 'react';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,6 +107,65 @@ interface AnimationTicker {
|
|||||||
resume: () => void;
|
resume: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 물리 파라미터
|
||||||
|
*/
|
||||||
|
interface SpringPhysicsConfig {
|
||||||
|
/** 스프링 탄성 계수 (높을수록 빠르게 복원) */
|
||||||
|
stiffness: number;
|
||||||
|
/** 감쇠 계수 (높을수록 빨리 멈춤) */
|
||||||
|
damping: number;
|
||||||
|
/** 질량 (높을수록 느리게 움직임) */
|
||||||
|
mass: number;
|
||||||
|
/** 영향 반경 (정규화 좌표, 기본값 0.2) */
|
||||||
|
influenceRadius: number;
|
||||||
|
/** 최대 왜곡 강도 */
|
||||||
|
maxStrength: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 마우스 인터랙션 설정
|
||||||
|
*/
|
||||||
|
interface MouseInteractionConfig {
|
||||||
|
/** 마우스 인터랙션 활성화 여부 */
|
||||||
|
enabled: boolean;
|
||||||
|
/** 스프링 물리 파라미터 */
|
||||||
|
physics: SpringPhysicsConfig;
|
||||||
|
/** 최소 속도 임계값 (이보다 느리면 효과 없음) */
|
||||||
|
minVelocity?: number;
|
||||||
|
/** 최대 속도 제한 (이보다 빠르면 클램핑) */
|
||||||
|
maxVelocity?: number;
|
||||||
|
/** 속도 승수 (마우스 속도에 곱해지는 값) */
|
||||||
|
velocityMultiplier?: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 마우스 상태
|
||||||
|
*/
|
||||||
|
interface MouseState {
|
||||||
|
/** 현재 마우스 위치 (정규화 좌표) */
|
||||||
|
position: Point | null;
|
||||||
|
/** 이전 마우스 위치 */
|
||||||
|
prevPosition: Point | null;
|
||||||
|
/** 속도 벡터 */
|
||||||
|
velocity: Point;
|
||||||
|
/** 가속도 벡터 */
|
||||||
|
acceleration: Point;
|
||||||
|
/** 마우스가 컨테이너 위에 있는지 */
|
||||||
|
isHovering: boolean;
|
||||||
|
/** 드래그 중인지 */
|
||||||
|
isDragging: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 스프링 물리 상태
|
||||||
|
*/
|
||||||
|
interface SpringState {
|
||||||
|
/** 현재 변위 (displacement) */
|
||||||
|
displacement: Point;
|
||||||
|
/** 현재 속도 */
|
||||||
|
velocity: Point;
|
||||||
|
/** 목표 위치 (평형 상태는 {x:0, y:0}) */
|
||||||
|
target: Point;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageDistortion 컴포넌트 Props
|
* ImageDistortion 컴포넌트 Props
|
||||||
*/
|
*/
|
||||||
@ -122,15 +181,17 @@ interface ImageDistortionProps {
|
|||||||
/** 애니메이션 재생 여부 */
|
/** 애니메이션 재생 여부 */
|
||||||
isPlaying?: boolean;
|
isPlaying?: boolean;
|
||||||
/** 컨테이너 스타일 */
|
/** 컨테이너 스타일 */
|
||||||
style?: React.CSSProperties;
|
style?: React$1.CSSProperties;
|
||||||
/** 컨테이너 클래스명 */
|
/** 컨테이너 클래스명 */
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 마우스 인터랙션 설정 */
|
||||||
|
mouseInteraction?: MouseInteractionConfig;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* GPU 가속 이미지 왜곡 컴포넌트
|
* GPU 가속 이미지 왜곡 컴포넌트
|
||||||
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
||||||
*/
|
*/
|
||||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
declare const ImageDistortion: React$1.FC<ImageDistortionProps>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 에디터 편집 모드
|
* 에디터 편집 모드
|
||||||
@ -248,7 +309,7 @@ interface DistortionEditorProps {
|
|||||||
canvasStyle?: EditorCanvasStyle;
|
canvasStyle?: EditorCanvasStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const DistortionEditor: React.FC<DistortionEditorProps>;
|
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
|
||||||
|
|
||||||
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
||||||
state: EditorState;
|
state: EditorState;
|
||||||
@ -403,6 +464,57 @@ declare class AnimationLoop {
|
|||||||
static updateProgress(areas: DistortionArea[], deltaTime: number): DistortionArea[];
|
static updateProgress(areas: DistortionArea[], deltaTime: number): DistortionArea[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 기반 물리 시뮬레이션 엔진
|
||||||
|
* Hooke's Law와 감쇠를 적용한 스프링-댐퍼 시스템
|
||||||
|
*/
|
||||||
|
declare class SpringPhysics {
|
||||||
|
private config;
|
||||||
|
private state;
|
||||||
|
constructor(config: SpringPhysicsConfig);
|
||||||
|
/**
|
||||||
|
* 물리 파라미터 업데이트
|
||||||
|
*/
|
||||||
|
setConfig(config: Partial<SpringPhysicsConfig>): void;
|
||||||
|
/**
|
||||||
|
* 목표 위치 설정 (마우스 속도 기반)
|
||||||
|
*/
|
||||||
|
setTarget(velocity: Point, velocityMultiplier?: number): void;
|
||||||
|
/**
|
||||||
|
* 초기 속도 설정 (드래그 방향과 속도를 즉시 반영)
|
||||||
|
* 드래그 방향으로 즉시 튕기는 효과
|
||||||
|
*/
|
||||||
|
setInitialVelocity(velocity: Point, multiplier?: number): void;
|
||||||
|
/**
|
||||||
|
* 스프링 물리 업데이트 (Hooke's Law + Damping)
|
||||||
|
* F = -k * x - c * v
|
||||||
|
* a = F / m
|
||||||
|
* v += a * dt
|
||||||
|
* x += v * dt
|
||||||
|
*/
|
||||||
|
update(deltaTime: number): Point;
|
||||||
|
/**
|
||||||
|
* 즉시 충격 적용 (마우스 가속도 기반)
|
||||||
|
*/
|
||||||
|
applyImpulse(acceleration: Point, multiplier?: number): void;
|
||||||
|
/**
|
||||||
|
* 현재 변위 가져오기
|
||||||
|
*/
|
||||||
|
getDisplacement(): Point;
|
||||||
|
/**
|
||||||
|
* 현재 속도 가져오기
|
||||||
|
*/
|
||||||
|
getVelocity(): Point;
|
||||||
|
/**
|
||||||
|
* 상태 리셋
|
||||||
|
*/
|
||||||
|
reset(): void;
|
||||||
|
/**
|
||||||
|
* 마우스가 멈췄을 때 목표를 0으로 설정 (평형 상태로 복귀)
|
||||||
|
*/
|
||||||
|
returnToEquilibrium(): void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* requestAnimationFrame을 사용한 애니메이션 루프 훅
|
* requestAnimationFrame을 사용한 애니메이션 루프 훅
|
||||||
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
||||||
@ -410,4 +522,21 @@ declare class AnimationLoop {
|
|||||||
*/
|
*/
|
||||||
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
||||||
|
|
||||||
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor };
|
/**
|
||||||
|
* 마우스 위치, 속도, 가속도를 추적하는 훅
|
||||||
|
*/
|
||||||
|
declare const useMouseVelocity: (containerRef: React.RefObject<HTMLElement | null>) => {
|
||||||
|
getState: () => MouseState;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 인터랙션 기반 기존 영역 제어 훅
|
||||||
|
* 기존 영역을 손으로 튕기는 효과
|
||||||
|
*/
|
||||||
|
declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement | null>, config: MouseInteractionConfig) => {
|
||||||
|
updateInteraction: (areas: DistortionArea[], deltaTime: number) => DistortionArea[];
|
||||||
|
updateConfig: (newConfig: Partial<MouseInteractionConfig>) => void;
|
||||||
|
reset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };
|
||||||
|
|||||||
504
dist/index.js
vendored
504
dist/index.js
vendored
@ -37,15 +37,18 @@ __export(index_exports, {
|
|||||||
ImageDistortion: () => ImageDistortion,
|
ImageDistortion: () => ImageDistortion,
|
||||||
SHADER_CONFIG: () => SHADER_CONFIG,
|
SHADER_CONFIG: () => SHADER_CONFIG,
|
||||||
ShaderManager: () => ShaderManager,
|
ShaderManager: () => ShaderManager,
|
||||||
|
SpringPhysics: () => SpringPhysics,
|
||||||
ThreeScene: () => ThreeScene,
|
ThreeScene: () => ThreeScene,
|
||||||
applyEasing: () => applyEasing,
|
applyEasing: () => applyEasing,
|
||||||
useAnimationFrame: () => useAnimationFrame,
|
useAnimationFrame: () => useAnimationFrame,
|
||||||
useDistortionEditor: () => useDistortionEditor
|
useDistortionEditor: () => useDistortionEditor,
|
||||||
|
useMouseInteraction: () => useMouseInteraction,
|
||||||
|
useMouseVelocity: () => useMouseVelocity
|
||||||
});
|
});
|
||||||
module.exports = __toCommonJS(index_exports);
|
module.exports = __toCommonJS(index_exports);
|
||||||
|
|
||||||
// src/components/ImageDistortion.tsx
|
// src/components/ImageDistortion.tsx
|
||||||
var import_react2 = require("react");
|
var import_react4 = require("react");
|
||||||
var THREE2 = __toESM(require("three"));
|
var THREE2 = __toESM(require("three"));
|
||||||
|
|
||||||
// src/engine/ThreeScene.ts
|
// src/engine/ThreeScene.ts
|
||||||
@ -134,7 +137,6 @@ var ThreeScene = class {
|
|||||||
* 씬 렌더링
|
* 씬 렌더링
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -252,6 +254,12 @@ var AnimationLoop = class {
|
|||||||
static updateAreaDragVectors(areas) {
|
static updateAreaDragVectors(areas) {
|
||||||
return areas.map((area) => {
|
return areas.map((area) => {
|
||||||
const { progress, movement } = area;
|
const { progress, movement } = area;
|
||||||
|
if (movement.duration <= 0) {
|
||||||
|
return {
|
||||||
|
...area,
|
||||||
|
dragVector: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
const easedProgress = applyEasing(progress, movement.easing);
|
const easedProgress = applyEasing(progress, movement.easing);
|
||||||
let dragVector;
|
let dragVector;
|
||||||
if (easedProgress < 0.5) {
|
if (easedProgress < 0.5) {
|
||||||
@ -281,6 +289,9 @@ var AnimationLoop = class {
|
|||||||
*/
|
*/
|
||||||
static updateProgress(areas, deltaTime) {
|
static updateProgress(areas, deltaTime) {
|
||||||
return areas.map((area) => {
|
return areas.map((area) => {
|
||||||
|
if (area.movement.duration <= 0) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||||
newProgress %= 1;
|
newProgress %= 1;
|
||||||
return {
|
return {
|
||||||
@ -315,6 +326,369 @@ var useAnimationFrame = (callback, isPlaying = true) => {
|
|||||||
}, [callback, isPlaying]);
|
}, [callback, isPlaying]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src/hooks/useMouseInteraction.ts
|
||||||
|
var import_react3 = require("react");
|
||||||
|
|
||||||
|
// src/hooks/useMouseVelocity.ts
|
||||||
|
var import_react2 = require("react");
|
||||||
|
var useMouseVelocity = (containerRef) => {
|
||||||
|
const mouseStateRef = (0, import_react2.useRef)({
|
||||||
|
position: null,
|
||||||
|
prevPosition: null,
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
acceleration: { x: 0, y: 0 },
|
||||||
|
isHovering: false,
|
||||||
|
isDragging: false
|
||||||
|
});
|
||||||
|
const lastUpdateTimeRef = (0, import_react2.useRef)(Date.now());
|
||||||
|
const prevVelocityRef = (0, import_react2.useRef)({ x: 0, y: 0 });
|
||||||
|
const toNormalized = (0, import_react2.useCallback)((clientX, clientY) => {
|
||||||
|
if (!containerRef.current) return null;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (clientX - rect.left) / rect.width,
|
||||||
|
y: (clientY - rect.top) / rect.height
|
||||||
|
};
|
||||||
|
}, [containerRef]);
|
||||||
|
const updatePosition = (0, import_react2.useCallback)((clientX, clientY) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const deltaTime = (now - lastUpdateTimeRef.current) / 1e3;
|
||||||
|
lastUpdateTimeRef.current = now;
|
||||||
|
const normalizedPos = toNormalized(clientX, clientY);
|
||||||
|
if (!normalizedPos) return;
|
||||||
|
const state = mouseStateRef.current;
|
||||||
|
const prevPos = state.position;
|
||||||
|
let velocity = { x: 0, y: 0 };
|
||||||
|
if (prevPos && deltaTime > 0) {
|
||||||
|
velocity = {
|
||||||
|
x: (normalizedPos.x - prevPos.x) / deltaTime,
|
||||||
|
y: (normalizedPos.y - prevPos.y) / deltaTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const prevVel = prevVelocityRef.current;
|
||||||
|
let acceleration = { x: 0, y: 0 };
|
||||||
|
if (deltaTime > 0) {
|
||||||
|
acceleration = {
|
||||||
|
x: (velocity.x - prevVel.x) / deltaTime,
|
||||||
|
y: (velocity.y - prevVel.y) / deltaTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
mouseStateRef.current = {
|
||||||
|
position: normalizedPos,
|
||||||
|
prevPosition: prevPos,
|
||||||
|
velocity,
|
||||||
|
acceleration,
|
||||||
|
isHovering: true,
|
||||||
|
isDragging: state.isDragging
|
||||||
|
};
|
||||||
|
prevVelocityRef.current = velocity;
|
||||||
|
}, [toNormalized]);
|
||||||
|
const handleMouseMove = (0, import_react2.useCallback)((e) => {
|
||||||
|
updatePosition(e.clientX, e.clientY);
|
||||||
|
}, [updatePosition]);
|
||||||
|
const handleMouseEnter = (0, import_react2.useCallback)(() => {
|
||||||
|
mouseStateRef.current.isHovering = true;
|
||||||
|
}, []);
|
||||||
|
const handleMouseLeave = (0, import_react2.useCallback)(() => {
|
||||||
|
mouseStateRef.current = {
|
||||||
|
position: null,
|
||||||
|
prevPosition: null,
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
acceleration: { x: 0, y: 0 },
|
||||||
|
isHovering: false,
|
||||||
|
isDragging: false
|
||||||
|
};
|
||||||
|
prevVelocityRef.current = { x: 0, y: 0 };
|
||||||
|
}, []);
|
||||||
|
const handleMouseDown = (0, import_react2.useCallback)(() => {
|
||||||
|
mouseStateRef.current.isDragging = true;
|
||||||
|
}, []);
|
||||||
|
const handleMouseUp = (0, import_react2.useCallback)(() => {
|
||||||
|
mouseStateRef.current.isDragging = false;
|
||||||
|
}, []);
|
||||||
|
const handleTouchMove = (0, import_react2.useCallback)((e) => {
|
||||||
|
if (e.touches.length > 0) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
updatePosition(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}, [updatePosition]);
|
||||||
|
const handleTouchStart = (0, import_react2.useCallback)((e) => {
|
||||||
|
mouseStateRef.current.isDragging = true;
|
||||||
|
mouseStateRef.current.isHovering = true;
|
||||||
|
if (e.touches.length > 0) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
updatePosition(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}, [updatePosition]);
|
||||||
|
const handleTouchEnd = (0, import_react2.useCallback)(() => {
|
||||||
|
mouseStateRef.current.isDragging = false;
|
||||||
|
mouseStateRef.current.isHovering = false;
|
||||||
|
mouseStateRef.current.position = null;
|
||||||
|
mouseStateRef.current.prevPosition = null;
|
||||||
|
mouseStateRef.current.velocity = { x: 0, y: 0 };
|
||||||
|
mouseStateRef.current.acceleration = { x: 0, y: 0 };
|
||||||
|
prevVelocityRef.current = { x: 0, y: 0 };
|
||||||
|
}, []);
|
||||||
|
(0, import_react2.useEffect)(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
container.addEventListener("mousemove", handleMouseMove);
|
||||||
|
container.addEventListener("mouseenter", handleMouseEnter);
|
||||||
|
container.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
container.addEventListener("mousedown", handleMouseDown);
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
container.addEventListener("touchmove", handleTouchMove, { passive: true });
|
||||||
|
container.addEventListener("touchstart", handleTouchStart, { passive: true });
|
||||||
|
container.addEventListener("touchend", handleTouchEnd);
|
||||||
|
container.addEventListener("touchcancel", handleTouchEnd);
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
container.removeEventListener("mouseenter", handleMouseEnter);
|
||||||
|
container.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
|
container.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
container.removeEventListener("touchmove", handleTouchMove);
|
||||||
|
container.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
container.removeEventListener("touchend", handleTouchEnd);
|
||||||
|
container.removeEventListener("touchcancel", handleTouchEnd);
|
||||||
|
};
|
||||||
|
}, [containerRef, handleMouseMove, handleMouseEnter, handleMouseLeave, handleMouseDown, handleMouseUp, handleTouchMove, handleTouchStart, handleTouchEnd]);
|
||||||
|
const getState = (0, import_react2.useCallback)(() => {
|
||||||
|
return { ...mouseStateRef.current };
|
||||||
|
}, []);
|
||||||
|
return {
|
||||||
|
getState
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/engine/SpringPhysics.ts
|
||||||
|
var SpringPhysics = class {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: 0, y: 0 },
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
target: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 물리 파라미터 업데이트
|
||||||
|
*/
|
||||||
|
setConfig(config) {
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 목표 위치 설정 (마우스 속도 기반)
|
||||||
|
*/
|
||||||
|
setTarget(velocity, velocityMultiplier = 1) {
|
||||||
|
this.state.target = {
|
||||||
|
x: velocity.x * velocityMultiplier,
|
||||||
|
y: velocity.y * velocityMultiplier
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 초기 속도 설정 (드래그 방향과 속도를 즉시 반영)
|
||||||
|
* 드래그 방향으로 즉시 튕기는 효과
|
||||||
|
*/
|
||||||
|
setInitialVelocity(velocity, multiplier = 1) {
|
||||||
|
this.state.velocity = {
|
||||||
|
x: velocity.x * multiplier,
|
||||||
|
y: velocity.y * multiplier
|
||||||
|
};
|
||||||
|
this.state.target = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 스프링 물리 업데이트 (Hooke's Law + Damping)
|
||||||
|
* F = -k * x - c * v
|
||||||
|
* a = F / m
|
||||||
|
* v += a * dt
|
||||||
|
* x += v * dt
|
||||||
|
*/
|
||||||
|
update(deltaTime) {
|
||||||
|
const { stiffness, damping, mass } = this.config;
|
||||||
|
const { displacement, velocity, target } = this.state;
|
||||||
|
const dx = displacement.x - target.x;
|
||||||
|
const dy = displacement.y - target.y;
|
||||||
|
const springForceX = -stiffness * dx;
|
||||||
|
const springForceY = -stiffness * dy;
|
||||||
|
const dampingForceX = -damping * velocity.x;
|
||||||
|
const dampingForceY = -damping * velocity.y;
|
||||||
|
const totalForceX = springForceX + dampingForceX;
|
||||||
|
const totalForceY = springForceY + dampingForceY;
|
||||||
|
const accelerationX = totalForceX / mass;
|
||||||
|
const accelerationY = totalForceY / mass;
|
||||||
|
const newVelocityX = velocity.x + accelerationX * deltaTime;
|
||||||
|
const newVelocityY = velocity.y + accelerationY * deltaTime;
|
||||||
|
const newDisplacementX = displacement.x + newVelocityX * deltaTime;
|
||||||
|
const newDisplacementY = displacement.y + newVelocityY * deltaTime;
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: newDisplacementX, y: newDisplacementY },
|
||||||
|
velocity: { x: newVelocityX, y: newVelocityY },
|
||||||
|
target
|
||||||
|
};
|
||||||
|
const isNearlyZero = (val) => Math.abs(val) < 1e-4;
|
||||||
|
if (isNearlyZero(this.state.displacement.x) && isNearlyZero(this.state.displacement.y) && isNearlyZero(this.state.velocity.x) && isNearlyZero(this.state.velocity.y)) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
return this.state.displacement;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 즉시 충격 적용 (마우스 가속도 기반)
|
||||||
|
*/
|
||||||
|
applyImpulse(acceleration, multiplier = 1) {
|
||||||
|
this.state.velocity.x += acceleration.x * multiplier;
|
||||||
|
this.state.velocity.y += acceleration.y * multiplier;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 현재 변위 가져오기
|
||||||
|
*/
|
||||||
|
getDisplacement() {
|
||||||
|
return { ...this.state.displacement };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 현재 속도 가져오기
|
||||||
|
*/
|
||||||
|
getVelocity() {
|
||||||
|
return { ...this.state.velocity };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 상태 리셋
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: 0, y: 0 },
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
target: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 마우스가 멈췄을 때 목표를 0으로 설정 (평형 상태로 복귀)
|
||||||
|
*/
|
||||||
|
returnToEquilibrium() {
|
||||||
|
this.state.target = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/hooks/useMouseInteraction.ts
|
||||||
|
var isPointInPolygon = (point, polygon) => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
var useMouseInteraction = (containerRef, config) => {
|
||||||
|
const { getState } = useMouseVelocity(containerRef);
|
||||||
|
const [interactingAreaIndex, setInteractingAreaIndex] = (0, import_react3.useState)(null);
|
||||||
|
const springPhysicsMapRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
|
||||||
|
const getSpringPhysics = (0, import_react3.useCallback)((areaIndex) => {
|
||||||
|
if (!springPhysicsMapRef.current.has(areaIndex)) {
|
||||||
|
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
|
||||||
|
}
|
||||||
|
return springPhysicsMapRef.current.get(areaIndex);
|
||||||
|
}, [config.physics]);
|
||||||
|
const updateInteraction = (0, import_react3.useCallback)((areas, deltaTime) => {
|
||||||
|
if (!config.enabled) return areas;
|
||||||
|
const mouseState = getState();
|
||||||
|
if (mouseState.isDragging && mouseState.position) {
|
||||||
|
if (interactingAreaIndex === null) {
|
||||||
|
for (let i = areas.length - 1; i >= 0; i--) {
|
||||||
|
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
|
||||||
|
setInteractingAreaIndex(i);
|
||||||
|
getSpringPhysics(i).reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interactingAreaIndex !== null) {
|
||||||
|
const velocityMult = config.velocityMultiplier || 1;
|
||||||
|
const spring = getSpringPhysics(interactingAreaIndex);
|
||||||
|
const velocityMag = Math.sqrt(
|
||||||
|
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
|
||||||
|
);
|
||||||
|
const minVel = config.minVelocity || 0.05;
|
||||||
|
const maxVel = config.maxVelocity || 5;
|
||||||
|
if (velocityMag >= minVel) {
|
||||||
|
let clampedVelocity = mouseState.velocity;
|
||||||
|
if (velocityMag > maxVel) {
|
||||||
|
const scale = maxVel / velocityMag;
|
||||||
|
clampedVelocity = {
|
||||||
|
x: mouseState.velocity.x * scale,
|
||||||
|
y: mouseState.velocity.y * scale
|
||||||
|
};
|
||||||
|
}
|
||||||
|
spring.setTarget(clampedVelocity, velocityMult);
|
||||||
|
} else {
|
||||||
|
spring.returnToEquilibrium();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (interactingAreaIndex !== null) {
|
||||||
|
const velocityMult = config.velocityMultiplier || 1;
|
||||||
|
const spring = getSpringPhysics(interactingAreaIndex);
|
||||||
|
const maxVel = config.maxVelocity || 5;
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
spring.setInitialVelocity(clampedVelocity, velocityMult);
|
||||||
|
setInteractingAreaIndex(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) > 1e-3 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 1e-3;
|
||||||
|
if (index !== interactingAreaIndex && !isSpringActive) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
const displacement = spring.update(deltaTime);
|
||||||
|
const displacementMag = Math.sqrt(displacement.x ** 2 + displacement.y ** 2);
|
||||||
|
if (displacementMag < 1e-3) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...area,
|
||||||
|
dragVector: {
|
||||||
|
x: area.dragVector.x - displacement.x,
|
||||||
|
y: area.dragVector.y - displacement.y
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [config, getState, interactingAreaIndex, getSpringPhysics]);
|
||||||
|
const updateConfig = (0, import_react3.useCallback)((newConfig) => {
|
||||||
|
const physicsConfig = newConfig.physics;
|
||||||
|
if (physicsConfig) {
|
||||||
|
springPhysicsMapRef.current.forEach((spring) => {
|
||||||
|
spring.setConfig(physicsConfig);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const reset = (0, import_react3.useCallback)(() => {
|
||||||
|
springPhysicsMapRef.current.forEach((spring) => {
|
||||||
|
spring.reset();
|
||||||
|
});
|
||||||
|
setInteractingAreaIndex(null);
|
||||||
|
}, []);
|
||||||
|
return {
|
||||||
|
updateInteraction,
|
||||||
|
updateConfig,
|
||||||
|
reset
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// src/utils/constants.ts
|
// src/utils/constants.ts
|
||||||
var SHADER_CONFIG = {
|
var SHADER_CONFIG = {
|
||||||
/** 최대 영역 개수 */
|
/** 최대 영역 개수 */
|
||||||
@ -354,19 +728,38 @@ var ImageDistortion = ({
|
|||||||
fragmentShaderPath,
|
fragmentShaderPath,
|
||||||
isPlaying = true,
|
isPlaying = true,
|
||||||
style,
|
style,
|
||||||
className
|
className,
|
||||||
|
mouseInteraction
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = (0, import_react2.useRef)(null);
|
const containerRef = (0, import_react4.useRef)(null);
|
||||||
const sceneRef = (0, import_react2.useRef)(null);
|
const sceneRef = (0, import_react4.useRef)(null);
|
||||||
const shaderManagerRef = (0, import_react2.useRef)(new ShaderManager());
|
const shaderManagerRef = (0, import_react4.useRef)(new ShaderManager());
|
||||||
const textureRef = (0, import_react2.useRef)(null);
|
const textureRef = (0, import_react4.useRef)(null);
|
||||||
const [isReady, setIsReady] = (0, import_react2.useState)(false);
|
const [isReady, setIsReady] = (0, import_react4.useState)(false);
|
||||||
const [imageLoaded, setImageLoaded] = (0, import_react2.useState)(false);
|
const [imageLoaded, setImageLoaded] = (0, import_react4.useState)(false);
|
||||||
const [currentAreas, setCurrentAreas] = (0, import_react2.useState)(areas);
|
const [currentAreas, setCurrentAreas] = (0, import_react4.useState)(areas);
|
||||||
(0, import_react2.useEffect)(() => {
|
const mouseInteractionHook = useMouseInteraction(
|
||||||
|
containerRef,
|
||||||
|
mouseInteraction || {
|
||||||
|
enabled: false,
|
||||||
|
physics: {
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 10,
|
||||||
|
mass: 1,
|
||||||
|
influenceRadius: 0.2,
|
||||||
|
maxStrength: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(0, import_react4.useEffect)(() => {
|
||||||
setCurrentAreas(areas);
|
setCurrentAreas(areas);
|
||||||
}, [areas]);
|
}, [areas]);
|
||||||
(0, import_react2.useEffect)(() => {
|
(0, import_react4.useEffect)(() => {
|
||||||
|
if (mouseInteraction) {
|
||||||
|
mouseInteractionHook.updateConfig(mouseInteraction);
|
||||||
|
}
|
||||||
|
}, [mouseInteraction, mouseInteractionHook]);
|
||||||
|
(0, import_react4.useEffect)(() => {
|
||||||
console.log("[ImageDistortion] useEffect \uC2E4\uD589, containerRef.current:", containerRef.current);
|
console.log("[ImageDistortion] useEffect \uC2E4\uD589, containerRef.current:", containerRef.current);
|
||||||
if (!containerRef.current) {
|
if (!containerRef.current) {
|
||||||
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
||||||
@ -392,7 +785,7 @@ var ImageDistortion = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [vertexShaderPath, fragmentShaderPath]);
|
}, [vertexShaderPath, fragmentShaderPath]);
|
||||||
(0, import_react2.useEffect)(() => {
|
(0, import_react4.useEffect)(() => {
|
||||||
if (!imageSrc || !isReady) {
|
if (!imageSrc || !isReady) {
|
||||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
||||||
return;
|
return;
|
||||||
@ -435,7 +828,7 @@ var ImageDistortion = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [imageSrc, isReady]);
|
}, [imageSrc, isReady]);
|
||||||
(0, import_react2.useEffect)(() => {
|
(0, import_react4.useEffect)(() => {
|
||||||
if (!sceneRef.current || !isReady) return;
|
if (!sceneRef.current || !isReady) return;
|
||||||
const resolution = sceneRef.current.getResolution();
|
const resolution = sceneRef.current.getResolution();
|
||||||
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
||||||
@ -464,14 +857,18 @@ var ImageDistortion = ({
|
|||||||
});
|
});
|
||||||
sceneRef.current.render();
|
sceneRef.current.render();
|
||||||
}, [currentAreas, isReady]);
|
}, [currentAreas, isReady]);
|
||||||
const animationCallback = (0, import_react2.useCallback)((deltaTime) => {
|
const animationCallback = (0, import_react4.useCallback)((deltaTime) => {
|
||||||
if (!isReady) return;
|
if (!isReady) return;
|
||||||
setCurrentAreas((prevAreas) => {
|
setCurrentAreas((prevAreas) => {
|
||||||
const updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||||
return AnimationLoop.updateAreaDragVectors(updatedAreas);
|
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||||
|
if (mouseInteraction?.enabled) {
|
||||||
|
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||||
|
}
|
||||||
|
return updatedAreas;
|
||||||
});
|
});
|
||||||
}, [isReady]);
|
}, [isReady, mouseInteraction, mouseInteractionHook]);
|
||||||
useAnimationFrame(animationCallback, isPlaying);
|
useAnimationFrame(animationCallback, isPlaying || mouseInteraction?.enabled || false);
|
||||||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@ -505,28 +902,28 @@ var ImageDistortion = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src/editor/DistortionEditor.tsx
|
// src/editor/DistortionEditor.tsx
|
||||||
var import_react5 = require("react");
|
var import_react7 = require("react");
|
||||||
|
|
||||||
// src/editor/hooks/useDistortionEditor.ts
|
// src/editor/hooks/useDistortionEditor.ts
|
||||||
var import_react3 = require("react");
|
var import_react5 = require("react");
|
||||||
var useDistortionEditor = (initialAreas = []) => {
|
var useDistortionEditor = (initialAreas = []) => {
|
||||||
const [state, setState] = (0, import_react3.useState)({
|
const [state, setState] = (0, import_react5.useState)({
|
||||||
selectedAreaId: initialAreas[0]?.id || null,
|
selectedAreaId: initialAreas[0]?.id || null,
|
||||||
areas: initialAreas,
|
areas: initialAreas,
|
||||||
editMode: "normal",
|
editMode: "normal",
|
||||||
draggingPointIndex: null
|
draggingPointIndex: null
|
||||||
});
|
});
|
||||||
const selectArea = (0, import_react3.useCallback)((areaId) => {
|
const selectArea = (0, import_react5.useCallback)((areaId) => {
|
||||||
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
||||||
}, []);
|
}, []);
|
||||||
const addArea = (0, import_react3.useCallback)((area) => {
|
const addArea = (0, import_react5.useCallback)((area) => {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
areas: [...prev.areas, area],
|
areas: [...prev.areas, area],
|
||||||
selectedAreaId: area.id
|
selectedAreaId: area.id
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
const removeArea = (0, import_react3.useCallback)((areaId) => {
|
const removeArea = (0, import_react5.useCallback)((areaId) => {
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
||||||
return {
|
return {
|
||||||
@ -536,13 +933,13 @@ var useDistortionEditor = (initialAreas = []) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
const updateArea = (0, import_react3.useCallback)((areaId, updates) => {
|
const updateArea = (0, import_react5.useCallback)((areaId, updates) => {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
|
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
const updatePoint = (0, import_react3.useCallback)((areaId, pointIndex, point) => {
|
const updatePoint = (0, import_react5.useCallback)((areaId, pointIndex, point) => {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
areas: prev.areas.map((area) => {
|
areas: prev.areas.map((area) => {
|
||||||
@ -555,16 +952,16 @@ var useDistortionEditor = (initialAreas = []) => {
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
const startDragging = (0, import_react3.useCallback)((pointIndex) => {
|
const startDragging = (0, import_react5.useCallback)((pointIndex) => {
|
||||||
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
|
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
|
||||||
}, []);
|
}, []);
|
||||||
const stopDragging = (0, import_react3.useCallback)(() => {
|
const stopDragging = (0, import_react5.useCallback)(() => {
|
||||||
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
||||||
}, []);
|
}, []);
|
||||||
const setEditMode = (0, import_react3.useCallback)((mode) => {
|
const setEditMode = (0, import_react5.useCallback)((mode) => {
|
||||||
setState((prev) => ({ ...prev, editMode: mode }));
|
setState((prev) => ({ ...prev, editMode: mode }));
|
||||||
}, []);
|
}, []);
|
||||||
const getSelectedArea = (0, import_react3.useCallback)(() => {
|
const getSelectedArea = (0, import_react5.useCallback)(() => {
|
||||||
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
|
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
|
||||||
}, [state.areas, state.selectedAreaId]);
|
}, [state.areas, state.selectedAreaId]);
|
||||||
return {
|
return {
|
||||||
@ -582,7 +979,7 @@ var useDistortionEditor = (initialAreas = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src/editor/components/EditorCanvas.tsx
|
// src/editor/components/EditorCanvas.tsx
|
||||||
var import_react4 = require("react");
|
var import_react6 = require("react");
|
||||||
|
|
||||||
// src/editor/constants.ts
|
// src/editor/constants.ts
|
||||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||||
@ -658,11 +1055,11 @@ var EditorCanvas = ({
|
|||||||
style: customStyle,
|
style: customStyle,
|
||||||
showEditor = true
|
showEditor = true
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = (0, import_react4.useRef)(null);
|
const containerRef = (0, import_react6.useRef)(null);
|
||||||
const [canvasSize, setCanvasSize] = (0, import_react4.useState)({ width: 0, height: 0 });
|
const [canvasSize, setCanvasSize] = (0, import_react6.useState)({ width: 0, height: 0 });
|
||||||
const [isDraggingArea, setIsDraggingArea] = (0, import_react4.useState)(false);
|
const [isDraggingArea, setIsDraggingArea] = (0, import_react6.useState)(false);
|
||||||
const [dragStartPos, setDragStartPos] = (0, import_react4.useState)(null);
|
const [dragStartPos, setDragStartPos] = (0, import_react6.useState)(null);
|
||||||
const editorStyle = (0, import_react4.useMemo)(() => ({
|
const editorStyle = (0, import_react6.useMemo)(() => ({
|
||||||
...DEFAULT_EDITOR_CANVAS_STYLE,
|
...DEFAULT_EDITOR_CANVAS_STYLE,
|
||||||
...customStyle,
|
...customStyle,
|
||||||
circleLevels: customStyle?.circleLevels || DEFAULT_EDITOR_CANVAS_STYLE.circleLevels,
|
circleLevels: customStyle?.circleLevels || DEFAULT_EDITOR_CANVAS_STYLE.circleLevels,
|
||||||
@ -679,13 +1076,13 @@ var EditorCanvas = ({
|
|||||||
...customStyle?.areaOutline
|
...customStyle?.areaOutline
|
||||||
}
|
}
|
||||||
}), [customStyle]);
|
}), [customStyle]);
|
||||||
(0, import_react4.useEffect)(() => {
|
(0, import_react6.useEffect)(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
setCanvasSize({ width: rect.width, height: rect.height });
|
setCanvasSize({ width: rect.width, height: rect.height });
|
||||||
}, [width, height]);
|
}, [width, height]);
|
||||||
const selectedArea = areas.find((a) => a.id === selectedAreaId);
|
const selectedArea = areas.find((a) => a.id === selectedAreaId);
|
||||||
const isPointInPolygon = (0, import_react4.useCallback)((point, polygon) => {
|
const isPointInPolygon2 = (0, import_react6.useCallback)((point, polygon) => {
|
||||||
let inside = false;
|
let inside = false;
|
||||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||||
const xi = polygon[i].x, yi = polygon[i].y;
|
const xi = polygon[i].x, yi = polygon[i].y;
|
||||||
@ -695,7 +1092,7 @@ var EditorCanvas = ({
|
|||||||
}
|
}
|
||||||
return inside;
|
return inside;
|
||||||
}, []);
|
}, []);
|
||||||
const handleMouseDown = (0, import_react4.useCallback)(
|
const handleMouseDown = (0, import_react6.useCallback)(
|
||||||
(pointIndex) => (e) => {
|
(pointIndex) => (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -703,22 +1100,22 @@ var EditorCanvas = ({
|
|||||||
},
|
},
|
||||||
[onStartDragging]
|
[onStartDragging]
|
||||||
);
|
);
|
||||||
const handleCanvasMouseDown = (0, import_react4.useCallback)(
|
const handleCanvasMouseDown = (0, import_react6.useCallback)(
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
const x = (e.clientX - rect.left) / rect.width;
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
const y = (e.clientY - rect.top) / rect.height;
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
const clickPoint = { x, y };
|
const clickPoint = { x, y };
|
||||||
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
|
if (isPointInPolygon2(clickPoint, selectedArea.basePoints)) {
|
||||||
setIsDraggingArea(true);
|
setIsDraggingArea(true);
|
||||||
setDragStartPos(clickPoint);
|
setDragStartPos(clickPoint);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[showEditor, selectedArea, isPointInPolygon]
|
[showEditor, selectedArea, isPointInPolygon2]
|
||||||
);
|
);
|
||||||
const handleMouseMove = (0, import_react4.useCallback)(
|
const handleMouseMove = (0, import_react6.useCallback)(
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
@ -741,7 +1138,7 @@ var EditorCanvas = ({
|
|||||||
},
|
},
|
||||||
[showEditor, draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
[showEditor, draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
||||||
);
|
);
|
||||||
const handleMouseUp = (0, import_react4.useCallback)(() => {
|
const handleMouseUp = (0, import_react6.useCallback)(() => {
|
||||||
if (draggingPointIndex !== null) {
|
if (draggingPointIndex !== null) {
|
||||||
onStopDragging();
|
onStopDragging();
|
||||||
}
|
}
|
||||||
@ -750,7 +1147,7 @@ var EditorCanvas = ({
|
|||||||
setDragStartPos(null);
|
setDragStartPos(null);
|
||||||
}
|
}
|
||||||
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
||||||
(0, import_react4.useEffect)(() => {
|
(0, import_react6.useEffect)(() => {
|
||||||
if (draggingPointIndex !== null || isDraggingArea) {
|
if (draggingPointIndex !== null || isDraggingArea) {
|
||||||
window.addEventListener("mouseup", handleMouseUp);
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
return () => window.removeEventListener("mouseup", handleMouseUp);
|
return () => window.removeEventListener("mouseup", handleMouseUp);
|
||||||
@ -769,7 +1166,7 @@ var EditorCanvas = ({
|
|||||||
y: posY * canvasHeight
|
y: posY * canvasHeight
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const drawDistortionCircle = (0, import_react4.useCallback)((ctx, points, canvasWidth, canvasHeight) => {
|
const drawDistortionCircle = (0, import_react6.useCallback)((ctx, points, canvasWidth, canvasHeight) => {
|
||||||
const segments = 128;
|
const segments = 128;
|
||||||
const centerU = 0.5;
|
const centerU = 0.5;
|
||||||
const centerV = 0.5;
|
const centerV = 0.5;
|
||||||
@ -1157,11 +1554,11 @@ var DistortionEditor = ({
|
|||||||
stopDragging,
|
stopDragging,
|
||||||
getSelectedArea
|
getSelectedArea
|
||||||
} = useDistortionEditor(initialAreas);
|
} = useDistortionEditor(initialAreas);
|
||||||
const [showEditor, setShowEditor] = (0, import_react5.useState)(true);
|
const [showEditor, setShowEditor] = (0, import_react7.useState)(true);
|
||||||
(0, import_react5.useEffect)(() => {
|
(0, import_react7.useEffect)(() => {
|
||||||
onAreasChange?.(state.areas);
|
onAreasChange?.(state.areas);
|
||||||
}, [state.areas, onAreasChange]);
|
}, [state.areas, onAreasChange]);
|
||||||
(0, import_react5.useEffect)(() => {
|
(0, import_react7.useEffect)(() => {
|
||||||
onSelectedAreaChange?.(state.selectedAreaId);
|
onSelectedAreaChange?.(state.selectedAreaId);
|
||||||
}, [state.selectedAreaId, onSelectedAreaChange]);
|
}, [state.selectedAreaId, onSelectedAreaChange]);
|
||||||
const handleAddArea = () => {
|
const handleAddArea = () => {
|
||||||
@ -1244,9 +1641,12 @@ var DistortionEditor = ({
|
|||||||
ImageDistortion,
|
ImageDistortion,
|
||||||
SHADER_CONFIG,
|
SHADER_CONFIG,
|
||||||
ShaderManager,
|
ShaderManager,
|
||||||
|
SpringPhysics,
|
||||||
ThreeScene,
|
ThreeScene,
|
||||||
applyEasing,
|
applyEasing,
|
||||||
useAnimationFrame,
|
useAnimationFrame,
|
||||||
useDistortionEditor
|
useDistortionEditor,
|
||||||
|
useMouseInteraction,
|
||||||
|
useMouseVelocity
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=index.js.map
|
//# sourceMappingURL=index.js.map
|
||||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
497
dist/index.mjs
vendored
497
dist/index.mjs
vendored
@ -1,5 +1,5 @@
|
|||||||
// src/components/ImageDistortion.tsx
|
// src/components/ImageDistortion.tsx
|
||||||
import { useEffect as useEffect2, useRef as useRef2, useState, useCallback } from "react";
|
import { useEffect as useEffect3, useRef as useRef4, useState as useState2, useCallback as useCallback3 } from "react";
|
||||||
import * as THREE2 from "three";
|
import * as THREE2 from "three";
|
||||||
|
|
||||||
// src/engine/ThreeScene.ts
|
// src/engine/ThreeScene.ts
|
||||||
@ -88,7 +88,6 @@ var ThreeScene = class {
|
|||||||
* 씬 렌더링
|
* 씬 렌더링
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -206,6 +205,12 @@ var AnimationLoop = class {
|
|||||||
static updateAreaDragVectors(areas) {
|
static updateAreaDragVectors(areas) {
|
||||||
return areas.map((area) => {
|
return areas.map((area) => {
|
||||||
const { progress, movement } = area;
|
const { progress, movement } = area;
|
||||||
|
if (movement.duration <= 0) {
|
||||||
|
return {
|
||||||
|
...area,
|
||||||
|
dragVector: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
const easedProgress = applyEasing(progress, movement.easing);
|
const easedProgress = applyEasing(progress, movement.easing);
|
||||||
let dragVector;
|
let dragVector;
|
||||||
if (easedProgress < 0.5) {
|
if (easedProgress < 0.5) {
|
||||||
@ -235,6 +240,9 @@ var AnimationLoop = class {
|
|||||||
*/
|
*/
|
||||||
static updateProgress(areas, deltaTime) {
|
static updateProgress(areas, deltaTime) {
|
||||||
return areas.map((area) => {
|
return areas.map((area) => {
|
||||||
|
if (area.movement.duration <= 0) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||||
newProgress %= 1;
|
newProgress %= 1;
|
||||||
return {
|
return {
|
||||||
@ -269,6 +277,369 @@ var useAnimationFrame = (callback, isPlaying = true) => {
|
|||||||
}, [callback, isPlaying]);
|
}, [callback, isPlaying]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src/hooks/useMouseInteraction.ts
|
||||||
|
import { useRef as useRef3, useCallback as useCallback2, useState } from "react";
|
||||||
|
|
||||||
|
// src/hooks/useMouseVelocity.ts
|
||||||
|
import { useRef as useRef2, useCallback, useEffect as useEffect2 } from "react";
|
||||||
|
var useMouseVelocity = (containerRef) => {
|
||||||
|
const mouseStateRef = useRef2({
|
||||||
|
position: null,
|
||||||
|
prevPosition: null,
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
acceleration: { x: 0, y: 0 },
|
||||||
|
isHovering: false,
|
||||||
|
isDragging: false
|
||||||
|
});
|
||||||
|
const lastUpdateTimeRef = useRef2(Date.now());
|
||||||
|
const prevVelocityRef = useRef2({ x: 0, y: 0 });
|
||||||
|
const toNormalized = useCallback((clientX, clientY) => {
|
||||||
|
if (!containerRef.current) return null;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (clientX - rect.left) / rect.width,
|
||||||
|
y: (clientY - rect.top) / rect.height
|
||||||
|
};
|
||||||
|
}, [containerRef]);
|
||||||
|
const updatePosition = useCallback((clientX, clientY) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const deltaTime = (now - lastUpdateTimeRef.current) / 1e3;
|
||||||
|
lastUpdateTimeRef.current = now;
|
||||||
|
const normalizedPos = toNormalized(clientX, clientY);
|
||||||
|
if (!normalizedPos) return;
|
||||||
|
const state = mouseStateRef.current;
|
||||||
|
const prevPos = state.position;
|
||||||
|
let velocity = { x: 0, y: 0 };
|
||||||
|
if (prevPos && deltaTime > 0) {
|
||||||
|
velocity = {
|
||||||
|
x: (normalizedPos.x - prevPos.x) / deltaTime,
|
||||||
|
y: (normalizedPos.y - prevPos.y) / deltaTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const prevVel = prevVelocityRef.current;
|
||||||
|
let acceleration = { x: 0, y: 0 };
|
||||||
|
if (deltaTime > 0) {
|
||||||
|
acceleration = {
|
||||||
|
x: (velocity.x - prevVel.x) / deltaTime,
|
||||||
|
y: (velocity.y - prevVel.y) / deltaTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
mouseStateRef.current = {
|
||||||
|
position: normalizedPos,
|
||||||
|
prevPosition: prevPos,
|
||||||
|
velocity,
|
||||||
|
acceleration,
|
||||||
|
isHovering: true,
|
||||||
|
isDragging: state.isDragging
|
||||||
|
};
|
||||||
|
prevVelocityRef.current = velocity;
|
||||||
|
}, [toNormalized]);
|
||||||
|
const handleMouseMove = useCallback((e) => {
|
||||||
|
updatePosition(e.clientX, e.clientY);
|
||||||
|
}, [updatePosition]);
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
mouseStateRef.current.isHovering = true;
|
||||||
|
}, []);
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
mouseStateRef.current = {
|
||||||
|
position: null,
|
||||||
|
prevPosition: null,
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
acceleration: { x: 0, y: 0 },
|
||||||
|
isHovering: false,
|
||||||
|
isDragging: false
|
||||||
|
};
|
||||||
|
prevVelocityRef.current = { x: 0, y: 0 };
|
||||||
|
}, []);
|
||||||
|
const handleMouseDown = useCallback(() => {
|
||||||
|
mouseStateRef.current.isDragging = true;
|
||||||
|
}, []);
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
mouseStateRef.current.isDragging = false;
|
||||||
|
}, []);
|
||||||
|
const handleTouchMove = useCallback((e) => {
|
||||||
|
if (e.touches.length > 0) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
updatePosition(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}, [updatePosition]);
|
||||||
|
const handleTouchStart = useCallback((e) => {
|
||||||
|
mouseStateRef.current.isDragging = true;
|
||||||
|
mouseStateRef.current.isHovering = true;
|
||||||
|
if (e.touches.length > 0) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
updatePosition(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}, [updatePosition]);
|
||||||
|
const handleTouchEnd = useCallback(() => {
|
||||||
|
mouseStateRef.current.isDragging = false;
|
||||||
|
mouseStateRef.current.isHovering = false;
|
||||||
|
mouseStateRef.current.position = null;
|
||||||
|
mouseStateRef.current.prevPosition = null;
|
||||||
|
mouseStateRef.current.velocity = { x: 0, y: 0 };
|
||||||
|
mouseStateRef.current.acceleration = { x: 0, y: 0 };
|
||||||
|
prevVelocityRef.current = { x: 0, y: 0 };
|
||||||
|
}, []);
|
||||||
|
useEffect2(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
container.addEventListener("mousemove", handleMouseMove);
|
||||||
|
container.addEventListener("mouseenter", handleMouseEnter);
|
||||||
|
container.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
container.addEventListener("mousedown", handleMouseDown);
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
container.addEventListener("touchmove", handleTouchMove, { passive: true });
|
||||||
|
container.addEventListener("touchstart", handleTouchStart, { passive: true });
|
||||||
|
container.addEventListener("touchend", handleTouchEnd);
|
||||||
|
container.addEventListener("touchcancel", handleTouchEnd);
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
container.removeEventListener("mouseenter", handleMouseEnter);
|
||||||
|
container.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
|
container.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
container.removeEventListener("touchmove", handleTouchMove);
|
||||||
|
container.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
container.removeEventListener("touchend", handleTouchEnd);
|
||||||
|
container.removeEventListener("touchcancel", handleTouchEnd);
|
||||||
|
};
|
||||||
|
}, [containerRef, handleMouseMove, handleMouseEnter, handleMouseLeave, handleMouseDown, handleMouseUp, handleTouchMove, handleTouchStart, handleTouchEnd]);
|
||||||
|
const getState = useCallback(() => {
|
||||||
|
return { ...mouseStateRef.current };
|
||||||
|
}, []);
|
||||||
|
return {
|
||||||
|
getState
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/engine/SpringPhysics.ts
|
||||||
|
var SpringPhysics = class {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: 0, y: 0 },
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
target: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 물리 파라미터 업데이트
|
||||||
|
*/
|
||||||
|
setConfig(config) {
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 목표 위치 설정 (마우스 속도 기반)
|
||||||
|
*/
|
||||||
|
setTarget(velocity, velocityMultiplier = 1) {
|
||||||
|
this.state.target = {
|
||||||
|
x: velocity.x * velocityMultiplier,
|
||||||
|
y: velocity.y * velocityMultiplier
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 초기 속도 설정 (드래그 방향과 속도를 즉시 반영)
|
||||||
|
* 드래그 방향으로 즉시 튕기는 효과
|
||||||
|
*/
|
||||||
|
setInitialVelocity(velocity, multiplier = 1) {
|
||||||
|
this.state.velocity = {
|
||||||
|
x: velocity.x * multiplier,
|
||||||
|
y: velocity.y * multiplier
|
||||||
|
};
|
||||||
|
this.state.target = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 스프링 물리 업데이트 (Hooke's Law + Damping)
|
||||||
|
* F = -k * x - c * v
|
||||||
|
* a = F / m
|
||||||
|
* v += a * dt
|
||||||
|
* x += v * dt
|
||||||
|
*/
|
||||||
|
update(deltaTime) {
|
||||||
|
const { stiffness, damping, mass } = this.config;
|
||||||
|
const { displacement, velocity, target } = this.state;
|
||||||
|
const dx = displacement.x - target.x;
|
||||||
|
const dy = displacement.y - target.y;
|
||||||
|
const springForceX = -stiffness * dx;
|
||||||
|
const springForceY = -stiffness * dy;
|
||||||
|
const dampingForceX = -damping * velocity.x;
|
||||||
|
const dampingForceY = -damping * velocity.y;
|
||||||
|
const totalForceX = springForceX + dampingForceX;
|
||||||
|
const totalForceY = springForceY + dampingForceY;
|
||||||
|
const accelerationX = totalForceX / mass;
|
||||||
|
const accelerationY = totalForceY / mass;
|
||||||
|
const newVelocityX = velocity.x + accelerationX * deltaTime;
|
||||||
|
const newVelocityY = velocity.y + accelerationY * deltaTime;
|
||||||
|
const newDisplacementX = displacement.x + newVelocityX * deltaTime;
|
||||||
|
const newDisplacementY = displacement.y + newVelocityY * deltaTime;
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: newDisplacementX, y: newDisplacementY },
|
||||||
|
velocity: { x: newVelocityX, y: newVelocityY },
|
||||||
|
target
|
||||||
|
};
|
||||||
|
const isNearlyZero = (val) => Math.abs(val) < 1e-4;
|
||||||
|
if (isNearlyZero(this.state.displacement.x) && isNearlyZero(this.state.displacement.y) && isNearlyZero(this.state.velocity.x) && isNearlyZero(this.state.velocity.y)) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
return this.state.displacement;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 즉시 충격 적용 (마우스 가속도 기반)
|
||||||
|
*/
|
||||||
|
applyImpulse(acceleration, multiplier = 1) {
|
||||||
|
this.state.velocity.x += acceleration.x * multiplier;
|
||||||
|
this.state.velocity.y += acceleration.y * multiplier;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 현재 변위 가져오기
|
||||||
|
*/
|
||||||
|
getDisplacement() {
|
||||||
|
return { ...this.state.displacement };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 현재 속도 가져오기
|
||||||
|
*/
|
||||||
|
getVelocity() {
|
||||||
|
return { ...this.state.velocity };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 상태 리셋
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: 0, y: 0 },
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
target: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 마우스가 멈췄을 때 목표를 0으로 설정 (평형 상태로 복귀)
|
||||||
|
*/
|
||||||
|
returnToEquilibrium() {
|
||||||
|
this.state.target = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/hooks/useMouseInteraction.ts
|
||||||
|
var isPointInPolygon = (point, polygon) => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
var useMouseInteraction = (containerRef, config) => {
|
||||||
|
const { getState } = useMouseVelocity(containerRef);
|
||||||
|
const [interactingAreaIndex, setInteractingAreaIndex] = useState(null);
|
||||||
|
const springPhysicsMapRef = useRef3(/* @__PURE__ */ new Map());
|
||||||
|
const getSpringPhysics = useCallback2((areaIndex) => {
|
||||||
|
if (!springPhysicsMapRef.current.has(areaIndex)) {
|
||||||
|
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
|
||||||
|
}
|
||||||
|
return springPhysicsMapRef.current.get(areaIndex);
|
||||||
|
}, [config.physics]);
|
||||||
|
const updateInteraction = useCallback2((areas, deltaTime) => {
|
||||||
|
if (!config.enabled) return areas;
|
||||||
|
const mouseState = getState();
|
||||||
|
if (mouseState.isDragging && mouseState.position) {
|
||||||
|
if (interactingAreaIndex === null) {
|
||||||
|
for (let i = areas.length - 1; i >= 0; i--) {
|
||||||
|
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
|
||||||
|
setInteractingAreaIndex(i);
|
||||||
|
getSpringPhysics(i).reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (interactingAreaIndex !== null) {
|
||||||
|
const velocityMult = config.velocityMultiplier || 1;
|
||||||
|
const spring = getSpringPhysics(interactingAreaIndex);
|
||||||
|
const velocityMag = Math.sqrt(
|
||||||
|
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
|
||||||
|
);
|
||||||
|
const minVel = config.minVelocity || 0.05;
|
||||||
|
const maxVel = config.maxVelocity || 5;
|
||||||
|
if (velocityMag >= minVel) {
|
||||||
|
let clampedVelocity = mouseState.velocity;
|
||||||
|
if (velocityMag > maxVel) {
|
||||||
|
const scale = maxVel / velocityMag;
|
||||||
|
clampedVelocity = {
|
||||||
|
x: mouseState.velocity.x * scale,
|
||||||
|
y: mouseState.velocity.y * scale
|
||||||
|
};
|
||||||
|
}
|
||||||
|
spring.setTarget(clampedVelocity, velocityMult);
|
||||||
|
} else {
|
||||||
|
spring.returnToEquilibrium();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (interactingAreaIndex !== null) {
|
||||||
|
const velocityMult = config.velocityMultiplier || 1;
|
||||||
|
const spring = getSpringPhysics(interactingAreaIndex);
|
||||||
|
const maxVel = config.maxVelocity || 5;
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
spring.setInitialVelocity(clampedVelocity, velocityMult);
|
||||||
|
setInteractingAreaIndex(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) > 1e-3 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 1e-3;
|
||||||
|
if (index !== interactingAreaIndex && !isSpringActive) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
const displacement = spring.update(deltaTime);
|
||||||
|
const displacementMag = Math.sqrt(displacement.x ** 2 + displacement.y ** 2);
|
||||||
|
if (displacementMag < 1e-3) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...area,
|
||||||
|
dragVector: {
|
||||||
|
x: area.dragVector.x - displacement.x,
|
||||||
|
y: area.dragVector.y - displacement.y
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [config, getState, interactingAreaIndex, getSpringPhysics]);
|
||||||
|
const updateConfig = useCallback2((newConfig) => {
|
||||||
|
const physicsConfig = newConfig.physics;
|
||||||
|
if (physicsConfig) {
|
||||||
|
springPhysicsMapRef.current.forEach((spring) => {
|
||||||
|
spring.setConfig(physicsConfig);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const reset = useCallback2(() => {
|
||||||
|
springPhysicsMapRef.current.forEach((spring) => {
|
||||||
|
spring.reset();
|
||||||
|
});
|
||||||
|
setInteractingAreaIndex(null);
|
||||||
|
}, []);
|
||||||
|
return {
|
||||||
|
updateInteraction,
|
||||||
|
updateConfig,
|
||||||
|
reset
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// src/utils/constants.ts
|
// src/utils/constants.ts
|
||||||
var SHADER_CONFIG = {
|
var SHADER_CONFIG = {
|
||||||
/** 최대 영역 개수 */
|
/** 최대 영역 개수 */
|
||||||
@ -308,19 +679,38 @@ var ImageDistortion = ({
|
|||||||
fragmentShaderPath,
|
fragmentShaderPath,
|
||||||
isPlaying = true,
|
isPlaying = true,
|
||||||
style,
|
style,
|
||||||
className
|
className,
|
||||||
|
mouseInteraction
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef2(null);
|
const containerRef = useRef4(null);
|
||||||
const sceneRef = useRef2(null);
|
const sceneRef = useRef4(null);
|
||||||
const shaderManagerRef = useRef2(new ShaderManager());
|
const shaderManagerRef = useRef4(new ShaderManager());
|
||||||
const textureRef = useRef2(null);
|
const textureRef = useRef4(null);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState2(false);
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState2(false);
|
||||||
const [currentAreas, setCurrentAreas] = useState(areas);
|
const [currentAreas, setCurrentAreas] = useState2(areas);
|
||||||
useEffect2(() => {
|
const mouseInteractionHook = useMouseInteraction(
|
||||||
|
containerRef,
|
||||||
|
mouseInteraction || {
|
||||||
|
enabled: false,
|
||||||
|
physics: {
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 10,
|
||||||
|
mass: 1,
|
||||||
|
influenceRadius: 0.2,
|
||||||
|
maxStrength: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useEffect3(() => {
|
||||||
setCurrentAreas(areas);
|
setCurrentAreas(areas);
|
||||||
}, [areas]);
|
}, [areas]);
|
||||||
useEffect2(() => {
|
useEffect3(() => {
|
||||||
|
if (mouseInteraction) {
|
||||||
|
mouseInteractionHook.updateConfig(mouseInteraction);
|
||||||
|
}
|
||||||
|
}, [mouseInteraction, mouseInteractionHook]);
|
||||||
|
useEffect3(() => {
|
||||||
console.log("[ImageDistortion] useEffect \uC2E4\uD589, containerRef.current:", containerRef.current);
|
console.log("[ImageDistortion] useEffect \uC2E4\uD589, containerRef.current:", containerRef.current);
|
||||||
if (!containerRef.current) {
|
if (!containerRef.current) {
|
||||||
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
||||||
@ -346,7 +736,7 @@ var ImageDistortion = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [vertexShaderPath, fragmentShaderPath]);
|
}, [vertexShaderPath, fragmentShaderPath]);
|
||||||
useEffect2(() => {
|
useEffect3(() => {
|
||||||
if (!imageSrc || !isReady) {
|
if (!imageSrc || !isReady) {
|
||||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
||||||
return;
|
return;
|
||||||
@ -389,7 +779,7 @@ var ImageDistortion = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [imageSrc, isReady]);
|
}, [imageSrc, isReady]);
|
||||||
useEffect2(() => {
|
useEffect3(() => {
|
||||||
if (!sceneRef.current || !isReady) return;
|
if (!sceneRef.current || !isReady) return;
|
||||||
const resolution = sceneRef.current.getResolution();
|
const resolution = sceneRef.current.getResolution();
|
||||||
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
||||||
@ -418,14 +808,18 @@ var ImageDistortion = ({
|
|||||||
});
|
});
|
||||||
sceneRef.current.render();
|
sceneRef.current.render();
|
||||||
}, [currentAreas, isReady]);
|
}, [currentAreas, isReady]);
|
||||||
const animationCallback = useCallback((deltaTime) => {
|
const animationCallback = useCallback3((deltaTime) => {
|
||||||
if (!isReady) return;
|
if (!isReady) return;
|
||||||
setCurrentAreas((prevAreas) => {
|
setCurrentAreas((prevAreas) => {
|
||||||
const updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||||
return AnimationLoop.updateAreaDragVectors(updatedAreas);
|
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||||
|
if (mouseInteraction?.enabled) {
|
||||||
|
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||||
|
}
|
||||||
|
return updatedAreas;
|
||||||
});
|
});
|
||||||
}, [isReady]);
|
}, [isReady, mouseInteraction, mouseInteractionHook]);
|
||||||
useAnimationFrame(animationCallback, isPlaying);
|
useAnimationFrame(animationCallback, isPlaying || mouseInteraction?.enabled || false);
|
||||||
return /* @__PURE__ */ jsx(
|
return /* @__PURE__ */ jsx(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@ -459,28 +853,28 @@ var ImageDistortion = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src/editor/DistortionEditor.tsx
|
// src/editor/DistortionEditor.tsx
|
||||||
import { useEffect as useEffect4, useState as useState4 } from "react";
|
import { useEffect as useEffect5, useState as useState5 } from "react";
|
||||||
|
|
||||||
// src/editor/hooks/useDistortionEditor.ts
|
// src/editor/hooks/useDistortionEditor.ts
|
||||||
import { useState as useState2, useCallback as useCallback2 } from "react";
|
import { useState as useState3, useCallback as useCallback4 } from "react";
|
||||||
var useDistortionEditor = (initialAreas = []) => {
|
var useDistortionEditor = (initialAreas = []) => {
|
||||||
const [state, setState] = useState2({
|
const [state, setState] = useState3({
|
||||||
selectedAreaId: initialAreas[0]?.id || null,
|
selectedAreaId: initialAreas[0]?.id || null,
|
||||||
areas: initialAreas,
|
areas: initialAreas,
|
||||||
editMode: "normal",
|
editMode: "normal",
|
||||||
draggingPointIndex: null
|
draggingPointIndex: null
|
||||||
});
|
});
|
||||||
const selectArea = useCallback2((areaId) => {
|
const selectArea = useCallback4((areaId) => {
|
||||||
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
||||||
}, []);
|
}, []);
|
||||||
const addArea = useCallback2((area) => {
|
const addArea = useCallback4((area) => {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
areas: [...prev.areas, area],
|
areas: [...prev.areas, area],
|
||||||
selectedAreaId: area.id
|
selectedAreaId: area.id
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
const removeArea = useCallback2((areaId) => {
|
const removeArea = useCallback4((areaId) => {
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
||||||
return {
|
return {
|
||||||
@ -490,13 +884,13 @@ var useDistortionEditor = (initialAreas = []) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
const updateArea = useCallback2((areaId, updates) => {
|
const updateArea = useCallback4((areaId, updates) => {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
|
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
const updatePoint = useCallback2((areaId, pointIndex, point) => {
|
const updatePoint = useCallback4((areaId, pointIndex, point) => {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
areas: prev.areas.map((area) => {
|
areas: prev.areas.map((area) => {
|
||||||
@ -509,16 +903,16 @@ var useDistortionEditor = (initialAreas = []) => {
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
const startDragging = useCallback2((pointIndex) => {
|
const startDragging = useCallback4((pointIndex) => {
|
||||||
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
|
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
|
||||||
}, []);
|
}, []);
|
||||||
const stopDragging = useCallback2(() => {
|
const stopDragging = useCallback4(() => {
|
||||||
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
||||||
}, []);
|
}, []);
|
||||||
const setEditMode = useCallback2((mode) => {
|
const setEditMode = useCallback4((mode) => {
|
||||||
setState((prev) => ({ ...prev, editMode: mode }));
|
setState((prev) => ({ ...prev, editMode: mode }));
|
||||||
}, []);
|
}, []);
|
||||||
const getSelectedArea = useCallback2(() => {
|
const getSelectedArea = useCallback4(() => {
|
||||||
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
|
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
|
||||||
}, [state.areas, state.selectedAreaId]);
|
}, [state.areas, state.selectedAreaId]);
|
||||||
return {
|
return {
|
||||||
@ -536,7 +930,7 @@ var useDistortionEditor = (initialAreas = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// src/editor/components/EditorCanvas.tsx
|
// src/editor/components/EditorCanvas.tsx
|
||||||
import { useRef as useRef3, useEffect as useEffect3, useState as useState3, useCallback as useCallback3, useMemo } from "react";
|
import { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react";
|
||||||
|
|
||||||
// src/editor/constants.ts
|
// src/editor/constants.ts
|
||||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||||
@ -612,10 +1006,10 @@ var EditorCanvas = ({
|
|||||||
style: customStyle,
|
style: customStyle,
|
||||||
showEditor = true
|
showEditor = true
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef3(null);
|
const containerRef = useRef5(null);
|
||||||
const [canvasSize, setCanvasSize] = useState3({ width: 0, height: 0 });
|
const [canvasSize, setCanvasSize] = useState4({ width: 0, height: 0 });
|
||||||
const [isDraggingArea, setIsDraggingArea] = useState3(false);
|
const [isDraggingArea, setIsDraggingArea] = useState4(false);
|
||||||
const [dragStartPos, setDragStartPos] = useState3(null);
|
const [dragStartPos, setDragStartPos] = useState4(null);
|
||||||
const editorStyle = useMemo(() => ({
|
const editorStyle = useMemo(() => ({
|
||||||
...DEFAULT_EDITOR_CANVAS_STYLE,
|
...DEFAULT_EDITOR_CANVAS_STYLE,
|
||||||
...customStyle,
|
...customStyle,
|
||||||
@ -633,13 +1027,13 @@ var EditorCanvas = ({
|
|||||||
...customStyle?.areaOutline
|
...customStyle?.areaOutline
|
||||||
}
|
}
|
||||||
}), [customStyle]);
|
}), [customStyle]);
|
||||||
useEffect3(() => {
|
useEffect4(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
setCanvasSize({ width: rect.width, height: rect.height });
|
setCanvasSize({ width: rect.width, height: rect.height });
|
||||||
}, [width, height]);
|
}, [width, height]);
|
||||||
const selectedArea = areas.find((a) => a.id === selectedAreaId);
|
const selectedArea = areas.find((a) => a.id === selectedAreaId);
|
||||||
const isPointInPolygon = useCallback3((point, polygon) => {
|
const isPointInPolygon2 = useCallback5((point, polygon) => {
|
||||||
let inside = false;
|
let inside = false;
|
||||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||||
const xi = polygon[i].x, yi = polygon[i].y;
|
const xi = polygon[i].x, yi = polygon[i].y;
|
||||||
@ -649,7 +1043,7 @@ var EditorCanvas = ({
|
|||||||
}
|
}
|
||||||
return inside;
|
return inside;
|
||||||
}, []);
|
}, []);
|
||||||
const handleMouseDown = useCallback3(
|
const handleMouseDown = useCallback5(
|
||||||
(pointIndex) => (e) => {
|
(pointIndex) => (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -657,22 +1051,22 @@ var EditorCanvas = ({
|
|||||||
},
|
},
|
||||||
[onStartDragging]
|
[onStartDragging]
|
||||||
);
|
);
|
||||||
const handleCanvasMouseDown = useCallback3(
|
const handleCanvasMouseDown = useCallback5(
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
const x = (e.clientX - rect.left) / rect.width;
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
const y = (e.clientY - rect.top) / rect.height;
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
const clickPoint = { x, y };
|
const clickPoint = { x, y };
|
||||||
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
|
if (isPointInPolygon2(clickPoint, selectedArea.basePoints)) {
|
||||||
setIsDraggingArea(true);
|
setIsDraggingArea(true);
|
||||||
setDragStartPos(clickPoint);
|
setDragStartPos(clickPoint);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[showEditor, selectedArea, isPointInPolygon]
|
[showEditor, selectedArea, isPointInPolygon2]
|
||||||
);
|
);
|
||||||
const handleMouseMove = useCallback3(
|
const handleMouseMove = useCallback5(
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
@ -695,7 +1089,7 @@ var EditorCanvas = ({
|
|||||||
},
|
},
|
||||||
[showEditor, draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
[showEditor, draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
||||||
);
|
);
|
||||||
const handleMouseUp = useCallback3(() => {
|
const handleMouseUp = useCallback5(() => {
|
||||||
if (draggingPointIndex !== null) {
|
if (draggingPointIndex !== null) {
|
||||||
onStopDragging();
|
onStopDragging();
|
||||||
}
|
}
|
||||||
@ -704,7 +1098,7 @@ var EditorCanvas = ({
|
|||||||
setDragStartPos(null);
|
setDragStartPos(null);
|
||||||
}
|
}
|
||||||
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
||||||
useEffect3(() => {
|
useEffect4(() => {
|
||||||
if (draggingPointIndex !== null || isDraggingArea) {
|
if (draggingPointIndex !== null || isDraggingArea) {
|
||||||
window.addEventListener("mouseup", handleMouseUp);
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
return () => window.removeEventListener("mouseup", handleMouseUp);
|
return () => window.removeEventListener("mouseup", handleMouseUp);
|
||||||
@ -723,7 +1117,7 @@ var EditorCanvas = ({
|
|||||||
y: posY * canvasHeight
|
y: posY * canvasHeight
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const drawDistortionCircle = useCallback3((ctx, points, canvasWidth, canvasHeight) => {
|
const drawDistortionCircle = useCallback5((ctx, points, canvasWidth, canvasHeight) => {
|
||||||
const segments = 128;
|
const segments = 128;
|
||||||
const centerU = 0.5;
|
const centerU = 0.5;
|
||||||
const centerV = 0.5;
|
const centerV = 0.5;
|
||||||
@ -1111,11 +1505,11 @@ var DistortionEditor = ({
|
|||||||
stopDragging,
|
stopDragging,
|
||||||
getSelectedArea
|
getSelectedArea
|
||||||
} = useDistortionEditor(initialAreas);
|
} = useDistortionEditor(initialAreas);
|
||||||
const [showEditor, setShowEditor] = useState4(true);
|
const [showEditor, setShowEditor] = useState5(true);
|
||||||
useEffect4(() => {
|
useEffect5(() => {
|
||||||
onAreasChange?.(state.areas);
|
onAreasChange?.(state.areas);
|
||||||
}, [state.areas, onAreasChange]);
|
}, [state.areas, onAreasChange]);
|
||||||
useEffect4(() => {
|
useEffect5(() => {
|
||||||
onSelectedAreaChange?.(state.selectedAreaId);
|
onSelectedAreaChange?.(state.selectedAreaId);
|
||||||
}, [state.selectedAreaId, onSelectedAreaChange]);
|
}, [state.selectedAreaId, onSelectedAreaChange]);
|
||||||
const handleAddArea = () => {
|
const handleAddArea = () => {
|
||||||
@ -1197,9 +1591,12 @@ export {
|
|||||||
ImageDistortion,
|
ImageDistortion,
|
||||||
SHADER_CONFIG,
|
SHADER_CONFIG,
|
||||||
ShaderManager,
|
ShaderManager,
|
||||||
|
SpringPhysics,
|
||||||
ThreeScene,
|
ThreeScene,
|
||||||
applyEasing,
|
applyEasing,
|
||||||
useAnimationFrame,
|
useAnimationFrame,
|
||||||
useDistortionEditor
|
useDistortionEditor,
|
||||||
|
useMouseInteraction,
|
||||||
|
useMouseVelocity
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=index.mjs.map
|
//# sourceMappingURL=index.mjs.map
|
||||||
2
dist/index.mjs.map
vendored
2
dist/index.mjs.map
vendored
File diff suppressed because one or more lines are too long
@ -5,7 +5,9 @@ import { ThreeScene } from '../engine/ThreeScene';
|
|||||||
import { ShaderManager } from '../engine/ShaderManager';
|
import { ShaderManager } from '../engine/ShaderManager';
|
||||||
import { AnimationLoop } from '../engine/AnimationLoop';
|
import { AnimationLoop } from '../engine/AnimationLoop';
|
||||||
import { useAnimationFrame } from '../hooks/useAnimationFrame';
|
import { useAnimationFrame } from '../hooks/useAnimationFrame';
|
||||||
|
import { useMouseInteraction } from '../hooks/useMouseInteraction';
|
||||||
import { SHADER_CONFIG } from '../utils/constants';
|
import { SHADER_CONFIG } from '../utils/constants';
|
||||||
|
import { MouseInteractionConfig } from '../types/interaction';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageDistortion 컴포넌트 Props
|
* ImageDistortion 컴포넌트 Props
|
||||||
@ -25,6 +27,8 @@ export interface ImageDistortionProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
/** 컨테이너 클래스명 */
|
/** 컨테이너 클래스명 */
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 마우스 인터랙션 설정 */
|
||||||
|
mouseInteraction?: MouseInteractionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,6 +43,7 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
|||||||
isPlaying = true,
|
isPlaying = true,
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
|
mouseInteraction,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const sceneRef = useRef<ThreeScene | null>(null);
|
const sceneRef = useRef<ThreeScene | null>(null);
|
||||||
@ -49,11 +54,33 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
|||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [currentAreas, setCurrentAreas] = useState<DistortionArea[]>(areas);
|
const [currentAreas, setCurrentAreas] = useState<DistortionArea[]>(areas);
|
||||||
|
|
||||||
|
// 마우스 인터랙션 훅
|
||||||
|
const mouseInteractionHook = useMouseInteraction(
|
||||||
|
containerRef,
|
||||||
|
mouseInteraction || {
|
||||||
|
enabled: false,
|
||||||
|
physics: {
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 10,
|
||||||
|
mass: 1,
|
||||||
|
influenceRadius: 0.2,
|
||||||
|
maxStrength: 1.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 영역 변경 시 상태 업데이트
|
// 영역 변경 시 상태 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentAreas(areas);
|
setCurrentAreas(areas);
|
||||||
}, [areas]);
|
}, [areas]);
|
||||||
|
|
||||||
|
// 마우스 인터랙션 설정 변경 시 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
if (mouseInteraction) {
|
||||||
|
mouseInteractionHook.updateConfig(mouseInteraction);
|
||||||
|
}
|
||||||
|
}, [mouseInteraction, mouseInteractionHook]);
|
||||||
|
|
||||||
// Three.js 씬 초기화
|
// Three.js 씬 초기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[ImageDistortion] useEffect 실행, containerRef.current:', containerRef.current);
|
console.log('[ImageDistortion] useEffect 실행, containerRef.current:', containerRef.current);
|
||||||
@ -187,14 +214,21 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
|||||||
if (!isReady) return;
|
if (!isReady) return;
|
||||||
|
|
||||||
setCurrentAreas((prevAreas) => {
|
setCurrentAreas((prevAreas) => {
|
||||||
// 진행도 업데이트
|
// 1. 기존 영역 애니메이션 업데이트
|
||||||
const updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||||
// 드래그 벡터 업데이트
|
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||||
return AnimationLoop.updateAreaDragVectors(updatedAreas);
|
|
||||||
});
|
|
||||||
}, [isReady]);
|
|
||||||
|
|
||||||
useAnimationFrame(animationCallback, isPlaying);
|
// 2. 마우스 인터랙션 적용 (기존 dragVector에 스프링 변위 추가)
|
||||||
|
if (mouseInteraction?.enabled) {
|
||||||
|
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedAreas;
|
||||||
|
});
|
||||||
|
}, [isReady, mouseInteraction, mouseInteractionHook]);
|
||||||
|
|
||||||
|
// 애니메이션은 항상 실행 (마우스 인터랙션 포함)
|
||||||
|
useAnimationFrame(animationCallback, isPlaying || mouseInteraction?.enabled || false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -16,6 +16,14 @@ export class AnimationLoop {
|
|||||||
return areas.map((area) => {
|
return areas.map((area) => {
|
||||||
const { progress, movement } = area;
|
const { progress, movement } = area;
|
||||||
|
|
||||||
|
// duration이 0이면 애니메이션 없음 (dragVector를 0으로 유지)
|
||||||
|
if (movement.duration <= 0) {
|
||||||
|
return {
|
||||||
|
...area,
|
||||||
|
dragVector: { x: 0, y: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 이징 적용
|
// 이징 적용
|
||||||
const easedProgress = applyEasing(progress, movement.easing);
|
const easedProgress = applyEasing(progress, movement.easing);
|
||||||
|
|
||||||
@ -56,6 +64,11 @@ export class AnimationLoop {
|
|||||||
deltaTime: number
|
deltaTime: number
|
||||||
): DistortionArea[] {
|
): DistortionArea[] {
|
||||||
return areas.map((area) => {
|
return areas.map((area) => {
|
||||||
|
// duration이 0이면 progress 업데이트 안 함
|
||||||
|
if (area.movement.duration <= 0) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||||
newProgress %= 1.0; // 루프
|
newProgress %= 1.0; // 루프
|
||||||
|
|
||||||
|
|||||||
149
src/engine/SpringPhysics.ts
Normal file
149
src/engine/SpringPhysics.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { Point } from '../types/area';
|
||||||
|
import { SpringPhysicsConfig, SpringState } from '../types/interaction';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 기반 물리 시뮬레이션 엔진
|
||||||
|
* Hooke's Law와 감쇠를 적용한 스프링-댐퍼 시스템
|
||||||
|
*/
|
||||||
|
export class SpringPhysics {
|
||||||
|
private config: SpringPhysicsConfig;
|
||||||
|
private state: SpringState;
|
||||||
|
|
||||||
|
constructor(config: SpringPhysicsConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: 0, y: 0 },
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
target: { x: 0, y: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 물리 파라미터 업데이트
|
||||||
|
*/
|
||||||
|
public setConfig(config: Partial<SpringPhysicsConfig>) {
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 목표 위치 설정 (마우스 속도 기반)
|
||||||
|
*/
|
||||||
|
public setTarget(velocity: Point, velocityMultiplier: number = 1.0) {
|
||||||
|
// 속도에 승수를 곱해서 목표 변위로 설정
|
||||||
|
this.state.target = {
|
||||||
|
x: velocity.x * velocityMultiplier,
|
||||||
|
y: velocity.y * velocityMultiplier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 초기 속도 설정 (드래그 방향과 속도를 즉시 반영)
|
||||||
|
* 드래그 방향으로 즉시 튕기는 효과
|
||||||
|
*/
|
||||||
|
public setInitialVelocity(velocity: Point, multiplier: number = 1.0) {
|
||||||
|
// 현재 속도를 즉시 변경
|
||||||
|
this.state.velocity = {
|
||||||
|
x: velocity.x * multiplier,
|
||||||
|
y: velocity.y * multiplier,
|
||||||
|
};
|
||||||
|
// 목표는 0 (평형 상태로 돌아가도록)
|
||||||
|
this.state.target = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 물리 업데이트 (Hooke's Law + Damping)
|
||||||
|
* F = -k * x - c * v
|
||||||
|
* a = F / m
|
||||||
|
* v += a * dt
|
||||||
|
* x += v * dt
|
||||||
|
*/
|
||||||
|
public update(deltaTime: number): Point {
|
||||||
|
const { stiffness, damping, mass } = this.config;
|
||||||
|
const { displacement, velocity, target } = this.state;
|
||||||
|
|
||||||
|
// 평형 위치로부터의 변위 (target은 마우스 속도에서 계산된 목표)
|
||||||
|
const dx = displacement.x - target.x;
|
||||||
|
const dy = displacement.y - target.y;
|
||||||
|
|
||||||
|
// 스프링 힘: F = -k * x (복원력)
|
||||||
|
const springForceX = -stiffness * dx;
|
||||||
|
const springForceY = -stiffness * dy;
|
||||||
|
|
||||||
|
// 감쇠 힘: F = -c * v (마찰력)
|
||||||
|
const dampingForceX = -damping * velocity.x;
|
||||||
|
const dampingForceY = -damping * velocity.y;
|
||||||
|
|
||||||
|
// 총 힘
|
||||||
|
const totalForceX = springForceX + dampingForceX;
|
||||||
|
const totalForceY = springForceY + dampingForceY;
|
||||||
|
|
||||||
|
// 가속도: a = F / m
|
||||||
|
const accelerationX = totalForceX / mass;
|
||||||
|
const accelerationY = totalForceY / mass;
|
||||||
|
|
||||||
|
// 속도 업데이트: v += a * dt
|
||||||
|
const newVelocityX = velocity.x + accelerationX * deltaTime;
|
||||||
|
const newVelocityY = velocity.y + accelerationY * deltaTime;
|
||||||
|
|
||||||
|
// 위치 업데이트: x += v * dt
|
||||||
|
const newDisplacementX = displacement.x + newVelocityX * deltaTime;
|
||||||
|
const newDisplacementY = displacement.y + newVelocityY * deltaTime;
|
||||||
|
|
||||||
|
// 상태 저장
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: newDisplacementX, y: newDisplacementY },
|
||||||
|
velocity: { x: newVelocityX, y: newVelocityY },
|
||||||
|
target,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 매우 작은 움직임은 0으로 처리 (정지 판정)
|
||||||
|
const isNearlyZero = (val: number) => Math.abs(val) < 0.0001;
|
||||||
|
if (isNearlyZero(this.state.displacement.x) && isNearlyZero(this.state.displacement.y) &&
|
||||||
|
isNearlyZero(this.state.velocity.x) && isNearlyZero(this.state.velocity.y)) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.state.displacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 즉시 충격 적용 (마우스 가속도 기반)
|
||||||
|
*/
|
||||||
|
public applyImpulse(acceleration: Point, multiplier: number = 1.0) {
|
||||||
|
// 가속도를 속도로 변환하여 즉시 적용
|
||||||
|
this.state.velocity.x += acceleration.x * multiplier;
|
||||||
|
this.state.velocity.y += acceleration.y * multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 변위 가져오기
|
||||||
|
*/
|
||||||
|
public getDisplacement(): Point {
|
||||||
|
return { ...this.state.displacement };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 속도 가져오기
|
||||||
|
*/
|
||||||
|
public getVelocity(): Point {
|
||||||
|
return { ...this.state.velocity };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 리셋
|
||||||
|
*/
|
||||||
|
public reset() {
|
||||||
|
this.state = {
|
||||||
|
displacement: { x: 0, y: 0 },
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
target: { x: 0, y: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스가 멈췄을 때 목표를 0으로 설정 (평형 상태로 복귀)
|
||||||
|
*/
|
||||||
|
public returnToEquilibrium() {
|
||||||
|
this.state.target = { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -111,7 +111,6 @@ export class ThreeScene {
|
|||||||
* 씬 렌더링
|
* 씬 렌더링
|
||||||
*/
|
*/
|
||||||
public render() {
|
public render() {
|
||||||
console.log('[ThreeScene] render() 호출됨, mesh:', this.mesh);
|
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
190
src/hooks/useMouseInteraction.ts
Normal file
190
src/hooks/useMouseInteraction.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { useRef, useCallback, useState } from 'react';
|
||||||
|
import { useMouseVelocity } from './useMouseVelocity';
|
||||||
|
import { SpringPhysics } from '../engine/SpringPhysics';
|
||||||
|
import { DistortionArea, Point } from '../types/area';
|
||||||
|
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 [interactingAreaIndex, setInteractingAreaIndex] = useState<number | null>(null);
|
||||||
|
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) {
|
||||||
|
// 아직 영역을 선택하지 않았으면 찾기
|
||||||
|
if (interactingAreaIndex === null) {
|
||||||
|
// 마우스 위치가 포함된 영역 찾기 (마지막 영역부터 - 위에 있는 영역 우선)
|
||||||
|
for (let i = areas.length - 1; i >= 0; i--) {
|
||||||
|
if (isPointInPolygon(mouseState.position, areas[i].basePoints)) {
|
||||||
|
setInteractingAreaIndex(i);
|
||||||
|
// 해당 영역의 스프링 리셋
|
||||||
|
getSpringPhysics(i).reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 드래그 중인 영역이 있으면 마우스 방향으로 실시간 늘어남
|
||||||
|
if (interactingAreaIndex !== null) {
|
||||||
|
const velocityMult = config.velocityMultiplier || 1.0;
|
||||||
|
const spring = getSpringPhysics(interactingAreaIndex);
|
||||||
|
|
||||||
|
// 속도 크기 확인
|
||||||
|
const velocityMag = Math.sqrt(
|
||||||
|
mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2
|
||||||
|
);
|
||||||
|
const minVel = config.minVelocity || 0.05;
|
||||||
|
const maxVel = config.maxVelocity || 5.0;
|
||||||
|
|
||||||
|
if (velocityMag >= minVel) {
|
||||||
|
// 속도 클램핑 (너무 빠른 움직임 제한)
|
||||||
|
let clampedVelocity = mouseState.velocity;
|
||||||
|
if (velocityMag > maxVel) {
|
||||||
|
const scale = maxVel / velocityMag;
|
||||||
|
clampedVelocity = {
|
||||||
|
x: mouseState.velocity.x * scale,
|
||||||
|
y: mouseState.velocity.y * scale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 드래그 중: 클램핑된 마우스 속도를 목표로 설정
|
||||||
|
spring.setTarget(clampedVelocity, velocityMult);
|
||||||
|
} else {
|
||||||
|
// 드래그 중이지만 마우스가 멈춰있으면 평형으로 복귀
|
||||||
|
spring.returnToEquilibrium();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 마우스를 놓았으면 마지막 속도를 초기 속도로 설정하여 튕김
|
||||||
|
if (interactingAreaIndex !== null) {
|
||||||
|
const velocityMult = config.velocityMultiplier || 1.0;
|
||||||
|
const spring = getSpringPhysics(interactingAreaIndex);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클램핑된 속도를 초기 속도로 설정하여 튕김
|
||||||
|
spring.setInitialVelocity(clampedVelocity, velocityMult);
|
||||||
|
setInteractingAreaIndex(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 모든 영역의 스프링 물리 업데이트
|
||||||
|
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 (index !== interactingAreaIndex && !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, interactingAreaIndex, 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();
|
||||||
|
});
|
||||||
|
setInteractingAreaIndex(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateInteraction,
|
||||||
|
updateConfig,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
};
|
||||||
202
src/hooks/useMouseVelocity.ts
Normal file
202
src/hooks/useMouseVelocity.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
import { Point } from '../types/area';
|
||||||
|
import { MouseState } from '../types/interaction';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 위치, 속도, 가속도를 추적하는 훅
|
||||||
|
*/
|
||||||
|
export const useMouseVelocity = (containerRef: React.RefObject<HTMLElement | null>) => {
|
||||||
|
const mouseStateRef = useRef<MouseState>({
|
||||||
|
position: null,
|
||||||
|
prevPosition: null,
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
acceleration: { x: 0, y: 0 },
|
||||||
|
isHovering: false,
|
||||||
|
isDragging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastUpdateTimeRef = useRef<number>(Date.now());
|
||||||
|
const prevVelocityRef = useRef<Point>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 픽셀 좌표를 정규화 좌표(0-1)로 변환
|
||||||
|
*/
|
||||||
|
const toNormalized = useCallback((clientX: number, clientY: number): Point | null => {
|
||||||
|
if (!containerRef.current) return null;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (clientX - rect.left) / rect.width,
|
||||||
|
y: (clientY - rect.top) / rect.height,
|
||||||
|
};
|
||||||
|
}, [containerRef]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 위치 업데이트 (마우스/터치 공통 로직)
|
||||||
|
*/
|
||||||
|
const updatePosition = useCallback((clientX: number, clientY: number) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const deltaTime = (now - lastUpdateTimeRef.current) / 1000; // 초 단위
|
||||||
|
lastUpdateTimeRef.current = now;
|
||||||
|
|
||||||
|
const normalizedPos = toNormalized(clientX, clientY);
|
||||||
|
if (!normalizedPos) return;
|
||||||
|
|
||||||
|
const state = mouseStateRef.current;
|
||||||
|
const prevPos = state.position;
|
||||||
|
|
||||||
|
// 속도 계산 (변위 / 시간)
|
||||||
|
let velocity: Point = { x: 0, y: 0 };
|
||||||
|
if (prevPos && deltaTime > 0) {
|
||||||
|
velocity = {
|
||||||
|
x: (normalizedPos.x - prevPos.x) / deltaTime,
|
||||||
|
y: (normalizedPos.y - prevPos.y) / deltaTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 가속도 계산 (속도 변화 / 시간)
|
||||||
|
const prevVel = prevVelocityRef.current;
|
||||||
|
let acceleration: Point = { x: 0, y: 0 };
|
||||||
|
if (deltaTime > 0) {
|
||||||
|
acceleration = {
|
||||||
|
x: (velocity.x - prevVel.x) / deltaTime,
|
||||||
|
y: (velocity.y - prevVel.y) / deltaTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 업데이트
|
||||||
|
mouseStateRef.current = {
|
||||||
|
position: normalizedPos,
|
||||||
|
prevPosition: prevPos,
|
||||||
|
velocity,
|
||||||
|
acceleration,
|
||||||
|
isHovering: true,
|
||||||
|
isDragging: state.isDragging,
|
||||||
|
};
|
||||||
|
prevVelocityRef.current = velocity;
|
||||||
|
}, [toNormalized]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 이동 핸들러
|
||||||
|
*/
|
||||||
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
|
updatePosition(e.clientX, e.clientY);
|
||||||
|
}, [updatePosition]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 진입
|
||||||
|
*/
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
mouseStateRef.current.isHovering = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 나감
|
||||||
|
*/
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
mouseStateRef.current = {
|
||||||
|
position: null,
|
||||||
|
prevPosition: null,
|
||||||
|
velocity: { x: 0, y: 0 },
|
||||||
|
acceleration: { x: 0, y: 0 },
|
||||||
|
isHovering: false,
|
||||||
|
isDragging: false,
|
||||||
|
};
|
||||||
|
prevVelocityRef.current = { x: 0, y: 0 };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 다운
|
||||||
|
*/
|
||||||
|
const handleMouseDown = useCallback(() => {
|
||||||
|
mouseStateRef.current.isDragging = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 업
|
||||||
|
*/
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
mouseStateRef.current.isDragging = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 터치 이동 핸들러
|
||||||
|
*/
|
||||||
|
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||||
|
if (e.touches.length > 0) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
updatePosition(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}, [updatePosition]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 터치 시작 핸들러
|
||||||
|
*/
|
||||||
|
const handleTouchStart = useCallback((e: TouchEvent) => {
|
||||||
|
mouseStateRef.current.isDragging = true;
|
||||||
|
mouseStateRef.current.isHovering = true;
|
||||||
|
if (e.touches.length > 0) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
updatePosition(touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}, [updatePosition]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 터치 종료 핸들러
|
||||||
|
*/
|
||||||
|
const handleTouchEnd = useCallback(() => {
|
||||||
|
mouseStateRef.current.isDragging = false;
|
||||||
|
mouseStateRef.current.isHovering = false;
|
||||||
|
mouseStateRef.current.position = null;
|
||||||
|
mouseStateRef.current.prevPosition = null;
|
||||||
|
mouseStateRef.current.velocity = { x: 0, y: 0 };
|
||||||
|
mouseStateRef.current.acceleration = { x: 0, y: 0 };
|
||||||
|
prevVelocityRef.current = { x: 0, y: 0 };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이벤트 리스너 등록
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 마우스 이벤트
|
||||||
|
container.addEventListener('mousemove', handleMouseMove);
|
||||||
|
container.addEventListener('mouseenter', handleMouseEnter);
|
||||||
|
container.addEventListener('mouseleave', handleMouseLeave);
|
||||||
|
container.addEventListener('mousedown', handleMouseDown);
|
||||||
|
window.addEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
// 터치 이벤트
|
||||||
|
container.addEventListener('touchmove', handleTouchMove, { passive: true });
|
||||||
|
container.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||||
|
container.addEventListener('touchend', handleTouchEnd);
|
||||||
|
container.addEventListener('touchcancel', handleTouchEnd);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// 마우스 이벤트 제거
|
||||||
|
container.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
container.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
|
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
container.removeEventListener('mousedown', handleMouseDown);
|
||||||
|
window.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
|
||||||
|
// 터치 이벤트 제거
|
||||||
|
container.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
container.removeEventListener('touchstart', handleTouchStart);
|
||||||
|
container.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
container.removeEventListener('touchcancel', handleTouchEnd);
|
||||||
|
};
|
||||||
|
}, [containerRef, handleMouseMove, handleMouseEnter, handleMouseLeave, handleMouseDown, handleMouseUp, handleTouchMove, handleTouchStart, handleTouchEnd]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 마우스 상태 가져오기
|
||||||
|
*/
|
||||||
|
const getState = useCallback((): MouseState => {
|
||||||
|
return { ...mouseStateRef.current };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getState,
|
||||||
|
};
|
||||||
|
};
|
||||||
11
src/index.ts
11
src/index.ts
@ -20,6 +20,14 @@ export type {
|
|||||||
AnimationTicker,
|
AnimationTicker,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
|
// 마우스 인터랙션 타입
|
||||||
|
export type {
|
||||||
|
SpringPhysicsConfig,
|
||||||
|
MouseInteractionConfig,
|
||||||
|
MouseState,
|
||||||
|
SpringState,
|
||||||
|
} from './types/interaction';
|
||||||
|
|
||||||
// 유틸리티 함수
|
// 유틸리티 함수
|
||||||
export { applyEasing } from './utils/easing';
|
export { applyEasing } from './utils/easing';
|
||||||
export { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants';
|
export { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants';
|
||||||
@ -28,6 +36,9 @@ export { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants
|
|||||||
export { ThreeScene } from './engine/ThreeScene';
|
export { ThreeScene } from './engine/ThreeScene';
|
||||||
export { ShaderManager } from './engine/ShaderManager';
|
export { ShaderManager } from './engine/ShaderManager';
|
||||||
export { AnimationLoop } from './engine/AnimationLoop';
|
export { AnimationLoop } from './engine/AnimationLoop';
|
||||||
|
export { SpringPhysics } from './engine/SpringPhysics';
|
||||||
|
|
||||||
// 훅
|
// 훅
|
||||||
export { useAnimationFrame } from './hooks/useAnimationFrame';
|
export { useAnimationFrame } from './hooks/useAnimationFrame';
|
||||||
|
export { useMouseVelocity } from './hooks/useMouseVelocity';
|
||||||
|
export { useMouseInteraction } from './hooks/useMouseInteraction';
|
||||||
63
src/types/interaction.ts
Normal file
63
src/types/interaction.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Point } from './area';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 물리 파라미터
|
||||||
|
*/
|
||||||
|
export interface SpringPhysicsConfig {
|
||||||
|
/** 스프링 탄성 계수 (높을수록 빠르게 복원) */
|
||||||
|
stiffness: number;
|
||||||
|
/** 감쇠 계수 (높을수록 빨리 멈춤) */
|
||||||
|
damping: number;
|
||||||
|
/** 질량 (높을수록 느리게 움직임) */
|
||||||
|
mass: number;
|
||||||
|
/** 영향 반경 (정규화 좌표, 기본값 0.2) */
|
||||||
|
influenceRadius: number;
|
||||||
|
/** 최대 왜곡 강도 */
|
||||||
|
maxStrength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 인터랙션 설정
|
||||||
|
*/
|
||||||
|
export interface MouseInteractionConfig {
|
||||||
|
/** 마우스 인터랙션 활성화 여부 */
|
||||||
|
enabled: boolean;
|
||||||
|
/** 스프링 물리 파라미터 */
|
||||||
|
physics: SpringPhysicsConfig;
|
||||||
|
/** 최소 속도 임계값 (이보다 느리면 효과 없음) */
|
||||||
|
minVelocity?: number;
|
||||||
|
/** 최대 속도 제한 (이보다 빠르면 클램핑) */
|
||||||
|
maxVelocity?: number;
|
||||||
|
/** 속도 승수 (마우스 속도에 곱해지는 값) */
|
||||||
|
velocityMultiplier?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마우스 상태
|
||||||
|
*/
|
||||||
|
export interface MouseState {
|
||||||
|
/** 현재 마우스 위치 (정규화 좌표) */
|
||||||
|
position: Point | null;
|
||||||
|
/** 이전 마우스 위치 */
|
||||||
|
prevPosition: Point | null;
|
||||||
|
/** 속도 벡터 */
|
||||||
|
velocity: Point;
|
||||||
|
/** 가속도 벡터 */
|
||||||
|
acceleration: Point;
|
||||||
|
/** 마우스가 컨테이너 위에 있는지 */
|
||||||
|
isHovering: boolean;
|
||||||
|
/** 드래그 중인지 */
|
||||||
|
isDragging: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스프링 물리 상태
|
||||||
|
*/
|
||||||
|
export interface SpringState {
|
||||||
|
/** 현재 변위 (displacement) */
|
||||||
|
displacement: Point;
|
||||||
|
/** 현재 속도 */
|
||||||
|
velocity: Point;
|
||||||
|
/** 목표 위치 (평형 상태는 {x:0, y:0}) */
|
||||||
|
target: Point;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user