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';
|
||||
|
||||
/**
|
||||
@ -107,6 +107,65 @@ interface AnimationTicker {
|
||||
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
|
||||
*/
|
||||
@ -122,15 +181,17 @@ interface ImageDistortionProps {
|
||||
/** 애니메이션 재생 여부 */
|
||||
isPlaying?: boolean;
|
||||
/** 컨테이너 스타일 */
|
||||
style?: React.CSSProperties;
|
||||
style?: React$1.CSSProperties;
|
||||
/** 컨테이너 클래스명 */
|
||||
className?: string;
|
||||
/** 마우스 인터랙션 설정 */
|
||||
mouseInteraction?: MouseInteractionConfig;
|
||||
}
|
||||
/**
|
||||
* GPU 가속 이미지 왜곡 컴포넌트
|
||||
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
||||
*/
|
||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
||||
declare const ImageDistortion: React$1.FC<ImageDistortionProps>;
|
||||
|
||||
/**
|
||||
* 에디터 편집 모드
|
||||
@ -248,7 +309,7 @@ interface DistortionEditorProps {
|
||||
canvasStyle?: EditorCanvasStyle;
|
||||
}
|
||||
|
||||
declare const DistortionEditor: React.FC<DistortionEditorProps>;
|
||||
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
|
||||
|
||||
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
||||
state: EditorState;
|
||||
@ -403,6 +464,57 @@ declare class AnimationLoop {
|
||||
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을 사용한 애니메이션 루프 훅
|
||||
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
||||
@ -410,4 +522,21 @@ declare class AnimationLoop {
|
||||
*/
|
||||
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';
|
||||
|
||||
/**
|
||||
@ -107,6 +107,65 @@ interface AnimationTicker {
|
||||
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
|
||||
*/
|
||||
@ -122,15 +181,17 @@ interface ImageDistortionProps {
|
||||
/** 애니메이션 재생 여부 */
|
||||
isPlaying?: boolean;
|
||||
/** 컨테이너 스타일 */
|
||||
style?: React.CSSProperties;
|
||||
style?: React$1.CSSProperties;
|
||||
/** 컨테이너 클래스명 */
|
||||
className?: string;
|
||||
/** 마우스 인터랙션 설정 */
|
||||
mouseInteraction?: MouseInteractionConfig;
|
||||
}
|
||||
/**
|
||||
* GPU 가속 이미지 왜곡 컴포넌트
|
||||
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
||||
*/
|
||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
||||
declare const ImageDistortion: React$1.FC<ImageDistortionProps>;
|
||||
|
||||
/**
|
||||
* 에디터 편집 모드
|
||||
@ -248,7 +309,7 @@ interface DistortionEditorProps {
|
||||
canvasStyle?: EditorCanvasStyle;
|
||||
}
|
||||
|
||||
declare const DistortionEditor: React.FC<DistortionEditorProps>;
|
||||
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
|
||||
|
||||
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
||||
state: EditorState;
|
||||
@ -403,6 +464,57 @@ declare class AnimationLoop {
|
||||
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을 사용한 애니메이션 루프 훅
|
||||
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
||||
@ -410,4 +522,21 @@ declare class AnimationLoop {
|
||||
*/
|
||||
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,
|
||||
SHADER_CONFIG: () => SHADER_CONFIG,
|
||||
ShaderManager: () => ShaderManager,
|
||||
SpringPhysics: () => SpringPhysics,
|
||||
ThreeScene: () => ThreeScene,
|
||||
applyEasing: () => applyEasing,
|
||||
useAnimationFrame: () => useAnimationFrame,
|
||||
useDistortionEditor: () => useDistortionEditor
|
||||
useDistortionEditor: () => useDistortionEditor,
|
||||
useMouseInteraction: () => useMouseInteraction,
|
||||
useMouseVelocity: () => useMouseVelocity
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/components/ImageDistortion.tsx
|
||||
var import_react2 = require("react");
|
||||
var import_react4 = require("react");
|
||||
var THREE2 = __toESM(require("three"));
|
||||
|
||||
// src/engine/ThreeScene.ts
|
||||
@ -134,7 +137,6 @@ var ThreeScene = class {
|
||||
* 씬 렌더링
|
||||
*/
|
||||
render() {
|
||||
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
/**
|
||||
@ -252,6 +254,12 @@ var AnimationLoop = class {
|
||||
static updateAreaDragVectors(areas) {
|
||||
return areas.map((area) => {
|
||||
const { progress, movement } = area;
|
||||
if (movement.duration <= 0) {
|
||||
return {
|
||||
...area,
|
||||
dragVector: { x: 0, y: 0 }
|
||||
};
|
||||
}
|
||||
const easedProgress = applyEasing(progress, movement.easing);
|
||||
let dragVector;
|
||||
if (easedProgress < 0.5) {
|
||||
@ -281,6 +289,9 @@ var AnimationLoop = class {
|
||||
*/
|
||||
static updateProgress(areas, deltaTime) {
|
||||
return areas.map((area) => {
|
||||
if (area.movement.duration <= 0) {
|
||||
return area;
|
||||
}
|
||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||
newProgress %= 1;
|
||||
return {
|
||||
@ -315,6 +326,369 @@ var useAnimationFrame = (callback, isPlaying = true) => {
|
||||
}, [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
|
||||
var SHADER_CONFIG = {
|
||||
/** 최대 영역 개수 */
|
||||
@ -354,19 +728,38 @@ var ImageDistortion = ({
|
||||
fragmentShaderPath,
|
||||
isPlaying = true,
|
||||
style,
|
||||
className
|
||||
className,
|
||||
mouseInteraction
|
||||
}) => {
|
||||
const containerRef = (0, import_react2.useRef)(null);
|
||||
const sceneRef = (0, import_react2.useRef)(null);
|
||||
const shaderManagerRef = (0, import_react2.useRef)(new ShaderManager());
|
||||
const textureRef = (0, import_react2.useRef)(null);
|
||||
const [isReady, setIsReady] = (0, import_react2.useState)(false);
|
||||
const [imageLoaded, setImageLoaded] = (0, import_react2.useState)(false);
|
||||
const [currentAreas, setCurrentAreas] = (0, import_react2.useState)(areas);
|
||||
(0, import_react2.useEffect)(() => {
|
||||
const containerRef = (0, import_react4.useRef)(null);
|
||||
const sceneRef = (0, import_react4.useRef)(null);
|
||||
const shaderManagerRef = (0, import_react4.useRef)(new ShaderManager());
|
||||
const textureRef = (0, import_react4.useRef)(null);
|
||||
const [isReady, setIsReady] = (0, import_react4.useState)(false);
|
||||
const [imageLoaded, setImageLoaded] = (0, import_react4.useState)(false);
|
||||
const [currentAreas, setCurrentAreas] = (0, import_react4.useState)(areas);
|
||||
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);
|
||||
}, [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);
|
||||
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.");
|
||||
@ -392,7 +785,7 @@ var ImageDistortion = ({
|
||||
}
|
||||
};
|
||||
}, [vertexShaderPath, fragmentShaderPath]);
|
||||
(0, import_react2.useEffect)(() => {
|
||||
(0, import_react4.useEffect)(() => {
|
||||
if (!imageSrc || !isReady) {
|
||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
||||
return;
|
||||
@ -435,7 +828,7 @@ var ImageDistortion = ({
|
||||
}
|
||||
};
|
||||
}, [imageSrc, isReady]);
|
||||
(0, import_react2.useEffect)(() => {
|
||||
(0, import_react4.useEffect)(() => {
|
||||
if (!sceneRef.current || !isReady) return;
|
||||
const resolution = sceneRef.current.getResolution();
|
||||
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
||||
@ -464,14 +857,18 @@ var ImageDistortion = ({
|
||||
});
|
||||
sceneRef.current.render();
|
||||
}, [currentAreas, isReady]);
|
||||
const animationCallback = (0, import_react2.useCallback)((deltaTime) => {
|
||||
const animationCallback = (0, import_react4.useCallback)((deltaTime) => {
|
||||
if (!isReady) return;
|
||||
setCurrentAreas((prevAreas) => {
|
||||
const updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
return AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
if (mouseInteraction?.enabled) {
|
||||
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||
}
|
||||
return updatedAreas;
|
||||
});
|
||||
}, [isReady]);
|
||||
useAnimationFrame(animationCallback, isPlaying);
|
||||
}, [isReady, mouseInteraction, mouseInteractionHook]);
|
||||
useAnimationFrame(animationCallback, isPlaying || mouseInteraction?.enabled || false);
|
||||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
"div",
|
||||
{
|
||||
@ -505,28 +902,28 @@ var ImageDistortion = ({
|
||||
};
|
||||
|
||||
// src/editor/DistortionEditor.tsx
|
||||
var import_react5 = require("react");
|
||||
var import_react7 = require("react");
|
||||
|
||||
// src/editor/hooks/useDistortionEditor.ts
|
||||
var import_react3 = require("react");
|
||||
var import_react5 = require("react");
|
||||
var useDistortionEditor = (initialAreas = []) => {
|
||||
const [state, setState] = (0, import_react3.useState)({
|
||||
const [state, setState] = (0, import_react5.useState)({
|
||||
selectedAreaId: initialAreas[0]?.id || null,
|
||||
areas: initialAreas,
|
||||
editMode: "normal",
|
||||
draggingPointIndex: null
|
||||
});
|
||||
const selectArea = (0, import_react3.useCallback)((areaId) => {
|
||||
const selectArea = (0, import_react5.useCallback)((areaId) => {
|
||||
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
||||
}, []);
|
||||
const addArea = (0, import_react3.useCallback)((area) => {
|
||||
const addArea = (0, import_react5.useCallback)((area) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
areas: [...prev.areas, area],
|
||||
selectedAreaId: area.id
|
||||
}));
|
||||
}, []);
|
||||
const removeArea = (0, import_react3.useCallback)((areaId) => {
|
||||
const removeArea = (0, import_react5.useCallback)((areaId) => {
|
||||
setState((prev) => {
|
||||
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
||||
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) => ({
|
||||
...prev,
|
||||
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) => ({
|
||||
...prev,
|
||||
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 }));
|
||||
}, []);
|
||||
const stopDragging = (0, import_react3.useCallback)(() => {
|
||||
const stopDragging = (0, import_react5.useCallback)(() => {
|
||||
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
||||
}, []);
|
||||
const setEditMode = (0, import_react3.useCallback)((mode) => {
|
||||
const setEditMode = (0, import_react5.useCallback)((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;
|
||||
}, [state.areas, state.selectedAreaId]);
|
||||
return {
|
||||
@ -582,7 +979,7 @@ var useDistortionEditor = (initialAreas = []) => {
|
||||
};
|
||||
|
||||
// src/editor/components/EditorCanvas.tsx
|
||||
var import_react4 = require("react");
|
||||
var import_react6 = require("react");
|
||||
|
||||
// src/editor/constants.ts
|
||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||
@ -658,11 +1055,11 @@ var EditorCanvas = ({
|
||||
style: customStyle,
|
||||
showEditor = true
|
||||
}) => {
|
||||
const containerRef = (0, import_react4.useRef)(null);
|
||||
const [canvasSize, setCanvasSize] = (0, import_react4.useState)({ width: 0, height: 0 });
|
||||
const [isDraggingArea, setIsDraggingArea] = (0, import_react4.useState)(false);
|
||||
const [dragStartPos, setDragStartPos] = (0, import_react4.useState)(null);
|
||||
const editorStyle = (0, import_react4.useMemo)(() => ({
|
||||
const containerRef = (0, import_react6.useRef)(null);
|
||||
const [canvasSize, setCanvasSize] = (0, import_react6.useState)({ width: 0, height: 0 });
|
||||
const [isDraggingArea, setIsDraggingArea] = (0, import_react6.useState)(false);
|
||||
const [dragStartPos, setDragStartPos] = (0, import_react6.useState)(null);
|
||||
const editorStyle = (0, import_react6.useMemo)(() => ({
|
||||
...DEFAULT_EDITOR_CANVAS_STYLE,
|
||||
...customStyle,
|
||||
circleLevels: customStyle?.circleLevels || DEFAULT_EDITOR_CANVAS_STYLE.circleLevels,
|
||||
@ -679,13 +1076,13 @@ var EditorCanvas = ({
|
||||
...customStyle?.areaOutline
|
||||
}
|
||||
}), [customStyle]);
|
||||
(0, import_react4.useEffect)(() => {
|
||||
(0, import_react6.useEffect)(() => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
setCanvasSize({ width: rect.width, height: rect.height });
|
||||
}, [width, height]);
|
||||
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;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const xi = polygon[i].x, yi = polygon[i].y;
|
||||
@ -695,7 +1092,7 @@ var EditorCanvas = ({
|
||||
}
|
||||
return inside;
|
||||
}, []);
|
||||
const handleMouseDown = (0, import_react4.useCallback)(
|
||||
const handleMouseDown = (0, import_react6.useCallback)(
|
||||
(pointIndex) => (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -703,22 +1100,22 @@ var EditorCanvas = ({
|
||||
},
|
||||
[onStartDragging]
|
||||
);
|
||||
const handleCanvasMouseDown = (0, import_react4.useCallback)(
|
||||
const handleCanvasMouseDown = (0, import_react6.useCallback)(
|
||||
(e) => {
|
||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = (e.clientY - rect.top) / rect.height;
|
||||
const clickPoint = { x, y };
|
||||
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
|
||||
if (isPointInPolygon2(clickPoint, selectedArea.basePoints)) {
|
||||
setIsDraggingArea(true);
|
||||
setDragStartPos(clickPoint);
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[showEditor, selectedArea, isPointInPolygon]
|
||||
[showEditor, selectedArea, isPointInPolygon2]
|
||||
);
|
||||
const handleMouseMove = (0, import_react4.useCallback)(
|
||||
const handleMouseMove = (0, import_react6.useCallback)(
|
||||
(e) => {
|
||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
@ -741,7 +1138,7 @@ var EditorCanvas = ({
|
||||
},
|
||||
[showEditor, draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
||||
);
|
||||
const handleMouseUp = (0, import_react4.useCallback)(() => {
|
||||
const handleMouseUp = (0, import_react6.useCallback)(() => {
|
||||
if (draggingPointIndex !== null) {
|
||||
onStopDragging();
|
||||
}
|
||||
@ -750,7 +1147,7 @@ var EditorCanvas = ({
|
||||
setDragStartPos(null);
|
||||
}
|
||||
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
||||
(0, import_react4.useEffect)(() => {
|
||||
(0, import_react6.useEffect)(() => {
|
||||
if (draggingPointIndex !== null || isDraggingArea) {
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
return () => window.removeEventListener("mouseup", handleMouseUp);
|
||||
@ -769,7 +1166,7 @@ var EditorCanvas = ({
|
||||
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 centerU = 0.5;
|
||||
const centerV = 0.5;
|
||||
@ -1157,11 +1554,11 @@ var DistortionEditor = ({
|
||||
stopDragging,
|
||||
getSelectedArea
|
||||
} = useDistortionEditor(initialAreas);
|
||||
const [showEditor, setShowEditor] = (0, import_react5.useState)(true);
|
||||
(0, import_react5.useEffect)(() => {
|
||||
const [showEditor, setShowEditor] = (0, import_react7.useState)(true);
|
||||
(0, import_react7.useEffect)(() => {
|
||||
onAreasChange?.(state.areas);
|
||||
}, [state.areas, onAreasChange]);
|
||||
(0, import_react5.useEffect)(() => {
|
||||
(0, import_react7.useEffect)(() => {
|
||||
onSelectedAreaChange?.(state.selectedAreaId);
|
||||
}, [state.selectedAreaId, onSelectedAreaChange]);
|
||||
const handleAddArea = () => {
|
||||
@ -1244,9 +1641,12 @@ var DistortionEditor = ({
|
||||
ImageDistortion,
|
||||
SHADER_CONFIG,
|
||||
ShaderManager,
|
||||
SpringPhysics,
|
||||
ThreeScene,
|
||||
applyEasing,
|
||||
useAnimationFrame,
|
||||
useDistortionEditor
|
||||
useDistortionEditor,
|
||||
useMouseInteraction,
|
||||
useMouseVelocity
|
||||
});
|
||||
//# 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
|
||||
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";
|
||||
|
||||
// src/engine/ThreeScene.ts
|
||||
@ -88,7 +88,6 @@ var ThreeScene = class {
|
||||
* 씬 렌더링
|
||||
*/
|
||||
render() {
|
||||
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
/**
|
||||
@ -206,6 +205,12 @@ var AnimationLoop = class {
|
||||
static updateAreaDragVectors(areas) {
|
||||
return areas.map((area) => {
|
||||
const { progress, movement } = area;
|
||||
if (movement.duration <= 0) {
|
||||
return {
|
||||
...area,
|
||||
dragVector: { x: 0, y: 0 }
|
||||
};
|
||||
}
|
||||
const easedProgress = applyEasing(progress, movement.easing);
|
||||
let dragVector;
|
||||
if (easedProgress < 0.5) {
|
||||
@ -235,6 +240,9 @@ var AnimationLoop = class {
|
||||
*/
|
||||
static updateProgress(areas, deltaTime) {
|
||||
return areas.map((area) => {
|
||||
if (area.movement.duration <= 0) {
|
||||
return area;
|
||||
}
|
||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||
newProgress %= 1;
|
||||
return {
|
||||
@ -269,6 +277,369 @@ var useAnimationFrame = (callback, isPlaying = true) => {
|
||||
}, [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
|
||||
var SHADER_CONFIG = {
|
||||
/** 최대 영역 개수 */
|
||||
@ -308,19 +679,38 @@ var ImageDistortion = ({
|
||||
fragmentShaderPath,
|
||||
isPlaying = true,
|
||||
style,
|
||||
className
|
||||
className,
|
||||
mouseInteraction
|
||||
}) => {
|
||||
const containerRef = useRef2(null);
|
||||
const sceneRef = useRef2(null);
|
||||
const shaderManagerRef = useRef2(new ShaderManager());
|
||||
const textureRef = useRef2(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [currentAreas, setCurrentAreas] = useState(areas);
|
||||
useEffect2(() => {
|
||||
const containerRef = useRef4(null);
|
||||
const sceneRef = useRef4(null);
|
||||
const shaderManagerRef = useRef4(new ShaderManager());
|
||||
const textureRef = useRef4(null);
|
||||
const [isReady, setIsReady] = useState2(false);
|
||||
const [imageLoaded, setImageLoaded] = useState2(false);
|
||||
const [currentAreas, setCurrentAreas] = useState2(areas);
|
||||
const mouseInteractionHook = useMouseInteraction(
|
||||
containerRef,
|
||||
mouseInteraction || {
|
||||
enabled: false,
|
||||
physics: {
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
mass: 1,
|
||||
influenceRadius: 0.2,
|
||||
maxStrength: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
useEffect3(() => {
|
||||
setCurrentAreas(areas);
|
||||
}, [areas]);
|
||||
useEffect2(() => {
|
||||
useEffect3(() => {
|
||||
if (mouseInteraction) {
|
||||
mouseInteractionHook.updateConfig(mouseInteraction);
|
||||
}
|
||||
}, [mouseInteraction, mouseInteractionHook]);
|
||||
useEffect3(() => {
|
||||
console.log("[ImageDistortion] useEffect \uC2E4\uD589, containerRef.current:", 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.");
|
||||
@ -346,7 +736,7 @@ var ImageDistortion = ({
|
||||
}
|
||||
};
|
||||
}, [vertexShaderPath, fragmentShaderPath]);
|
||||
useEffect2(() => {
|
||||
useEffect3(() => {
|
||||
if (!imageSrc || !isReady) {
|
||||
console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady });
|
||||
return;
|
||||
@ -389,7 +779,7 @@ var ImageDistortion = ({
|
||||
}
|
||||
};
|
||||
}, [imageSrc, isReady]);
|
||||
useEffect2(() => {
|
||||
useEffect3(() => {
|
||||
if (!sceneRef.current || !isReady) return;
|
||||
const resolution = sceneRef.current.getResolution();
|
||||
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
||||
@ -418,14 +808,18 @@ var ImageDistortion = ({
|
||||
});
|
||||
sceneRef.current.render();
|
||||
}, [currentAreas, isReady]);
|
||||
const animationCallback = useCallback((deltaTime) => {
|
||||
const animationCallback = useCallback3((deltaTime) => {
|
||||
if (!isReady) return;
|
||||
setCurrentAreas((prevAreas) => {
|
||||
const updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
return AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
if (mouseInteraction?.enabled) {
|
||||
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||||
}
|
||||
return updatedAreas;
|
||||
});
|
||||
}, [isReady]);
|
||||
useAnimationFrame(animationCallback, isPlaying);
|
||||
}, [isReady, mouseInteraction, mouseInteractionHook]);
|
||||
useAnimationFrame(animationCallback, isPlaying || mouseInteraction?.enabled || false);
|
||||
return /* @__PURE__ */ jsx(
|
||||
"div",
|
||||
{
|
||||
@ -459,28 +853,28 @@ var ImageDistortion = ({
|
||||
};
|
||||
|
||||
// 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
|
||||
import { useState as useState2, useCallback as useCallback2 } from "react";
|
||||
import { useState as useState3, useCallback as useCallback4 } from "react";
|
||||
var useDistortionEditor = (initialAreas = []) => {
|
||||
const [state, setState] = useState2({
|
||||
const [state, setState] = useState3({
|
||||
selectedAreaId: initialAreas[0]?.id || null,
|
||||
areas: initialAreas,
|
||||
editMode: "normal",
|
||||
draggingPointIndex: null
|
||||
});
|
||||
const selectArea = useCallback2((areaId) => {
|
||||
const selectArea = useCallback4((areaId) => {
|
||||
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
||||
}, []);
|
||||
const addArea = useCallback2((area) => {
|
||||
const addArea = useCallback4((area) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
areas: [...prev.areas, area],
|
||||
selectedAreaId: area.id
|
||||
}));
|
||||
}, []);
|
||||
const removeArea = useCallback2((areaId) => {
|
||||
const removeArea = useCallback4((areaId) => {
|
||||
setState((prev) => {
|
||||
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
||||
return {
|
||||
@ -490,13 +884,13 @@ var useDistortionEditor = (initialAreas = []) => {
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
const updateArea = useCallback2((areaId, updates) => {
|
||||
const updateArea = useCallback4((areaId, updates) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
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) => ({
|
||||
...prev,
|
||||
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 }));
|
||||
}, []);
|
||||
const stopDragging = useCallback2(() => {
|
||||
const stopDragging = useCallback4(() => {
|
||||
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
||||
}, []);
|
||||
const setEditMode = useCallback2((mode) => {
|
||||
const setEditMode = useCallback4((mode) => {
|
||||
setState((prev) => ({ ...prev, editMode: mode }));
|
||||
}, []);
|
||||
const getSelectedArea = useCallback2(() => {
|
||||
const getSelectedArea = useCallback4(() => {
|
||||
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
|
||||
}, [state.areas, state.selectedAreaId]);
|
||||
return {
|
||||
@ -536,7 +930,7 @@ var useDistortionEditor = (initialAreas = []) => {
|
||||
};
|
||||
|
||||
// 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
|
||||
var DEFAULT_EDITOR_CANVAS_STYLE = {
|
||||
@ -612,10 +1006,10 @@ var EditorCanvas = ({
|
||||
style: customStyle,
|
||||
showEditor = true
|
||||
}) => {
|
||||
const containerRef = useRef3(null);
|
||||
const [canvasSize, setCanvasSize] = useState3({ width: 0, height: 0 });
|
||||
const [isDraggingArea, setIsDraggingArea] = useState3(false);
|
||||
const [dragStartPos, setDragStartPos] = useState3(null);
|
||||
const containerRef = useRef5(null);
|
||||
const [canvasSize, setCanvasSize] = useState4({ width: 0, height: 0 });
|
||||
const [isDraggingArea, setIsDraggingArea] = useState4(false);
|
||||
const [dragStartPos, setDragStartPos] = useState4(null);
|
||||
const editorStyle = useMemo(() => ({
|
||||
...DEFAULT_EDITOR_CANVAS_STYLE,
|
||||
...customStyle,
|
||||
@ -633,13 +1027,13 @@ var EditorCanvas = ({
|
||||
...customStyle?.areaOutline
|
||||
}
|
||||
}), [customStyle]);
|
||||
useEffect3(() => {
|
||||
useEffect4(() => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
setCanvasSize({ width: rect.width, height: rect.height });
|
||||
}, [width, height]);
|
||||
const selectedArea = areas.find((a) => a.id === selectedAreaId);
|
||||
const isPointInPolygon = useCallback3((point, polygon) => {
|
||||
const isPointInPolygon2 = useCallback5((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;
|
||||
@ -649,7 +1043,7 @@ var EditorCanvas = ({
|
||||
}
|
||||
return inside;
|
||||
}, []);
|
||||
const handleMouseDown = useCallback3(
|
||||
const handleMouseDown = useCallback5(
|
||||
(pointIndex) => (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -657,22 +1051,22 @@ var EditorCanvas = ({
|
||||
},
|
||||
[onStartDragging]
|
||||
);
|
||||
const handleCanvasMouseDown = useCallback3(
|
||||
const handleCanvasMouseDown = useCallback5(
|
||||
(e) => {
|
||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = (e.clientY - rect.top) / rect.height;
|
||||
const clickPoint = { x, y };
|
||||
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
|
||||
if (isPointInPolygon2(clickPoint, selectedArea.basePoints)) {
|
||||
setIsDraggingArea(true);
|
||||
setDragStartPos(clickPoint);
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
[showEditor, selectedArea, isPointInPolygon]
|
||||
[showEditor, selectedArea, isPointInPolygon2]
|
||||
);
|
||||
const handleMouseMove = useCallback3(
|
||||
const handleMouseMove = useCallback5(
|
||||
(e) => {
|
||||
if (!showEditor || !selectedArea || !containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
@ -695,7 +1089,7 @@ var EditorCanvas = ({
|
||||
},
|
||||
[showEditor, draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
||||
);
|
||||
const handleMouseUp = useCallback3(() => {
|
||||
const handleMouseUp = useCallback5(() => {
|
||||
if (draggingPointIndex !== null) {
|
||||
onStopDragging();
|
||||
}
|
||||
@ -704,7 +1098,7 @@ var EditorCanvas = ({
|
||||
setDragStartPos(null);
|
||||
}
|
||||
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
||||
useEffect3(() => {
|
||||
useEffect4(() => {
|
||||
if (draggingPointIndex !== null || isDraggingArea) {
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
return () => window.removeEventListener("mouseup", handleMouseUp);
|
||||
@ -723,7 +1117,7 @@ var EditorCanvas = ({
|
||||
y: posY * canvasHeight
|
||||
};
|
||||
};
|
||||
const drawDistortionCircle = useCallback3((ctx, points, canvasWidth, canvasHeight) => {
|
||||
const drawDistortionCircle = useCallback5((ctx, points, canvasWidth, canvasHeight) => {
|
||||
const segments = 128;
|
||||
const centerU = 0.5;
|
||||
const centerV = 0.5;
|
||||
@ -1111,11 +1505,11 @@ var DistortionEditor = ({
|
||||
stopDragging,
|
||||
getSelectedArea
|
||||
} = useDistortionEditor(initialAreas);
|
||||
const [showEditor, setShowEditor] = useState4(true);
|
||||
useEffect4(() => {
|
||||
const [showEditor, setShowEditor] = useState5(true);
|
||||
useEffect5(() => {
|
||||
onAreasChange?.(state.areas);
|
||||
}, [state.areas, onAreasChange]);
|
||||
useEffect4(() => {
|
||||
useEffect5(() => {
|
||||
onSelectedAreaChange?.(state.selectedAreaId);
|
||||
}, [state.selectedAreaId, onSelectedAreaChange]);
|
||||
const handleAddArea = () => {
|
||||
@ -1197,9 +1591,12 @@ export {
|
||||
ImageDistortion,
|
||||
SHADER_CONFIG,
|
||||
ShaderManager,
|
||||
SpringPhysics,
|
||||
ThreeScene,
|
||||
applyEasing,
|
||||
useAnimationFrame,
|
||||
useDistortionEditor
|
||||
useDistortionEditor,
|
||||
useMouseInteraction,
|
||||
useMouseVelocity
|
||||
};
|
||||
//# 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 { AnimationLoop } from '../engine/AnimationLoop';
|
||||
import { useAnimationFrame } from '../hooks/useAnimationFrame';
|
||||
import { useMouseInteraction } from '../hooks/useMouseInteraction';
|
||||
import { SHADER_CONFIG } from '../utils/constants';
|
||||
import { MouseInteractionConfig } from '../types/interaction';
|
||||
|
||||
/**
|
||||
* ImageDistortion 컴포넌트 Props
|
||||
@ -25,6 +27,8 @@ export interface ImageDistortionProps {
|
||||
style?: React.CSSProperties;
|
||||
/** 컨테이너 클래스명 */
|
||||
className?: string;
|
||||
/** 마우스 인터랙션 설정 */
|
||||
mouseInteraction?: MouseInteractionConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,6 +43,7 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
||||
isPlaying = true,
|
||||
style,
|
||||
className,
|
||||
mouseInteraction,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const sceneRef = useRef<ThreeScene | null>(null);
|
||||
@ -49,11 +54,33 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
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(() => {
|
||||
setCurrentAreas(areas);
|
||||
}, [areas]);
|
||||
|
||||
// 마우스 인터랙션 설정 변경 시 업데이트
|
||||
useEffect(() => {
|
||||
if (mouseInteraction) {
|
||||
mouseInteractionHook.updateConfig(mouseInteraction);
|
||||
}
|
||||
}, [mouseInteraction, mouseInteractionHook]);
|
||||
|
||||
// Three.js 씬 초기화
|
||||
useEffect(() => {
|
||||
console.log('[ImageDistortion] useEffect 실행, containerRef.current:', containerRef.current);
|
||||
@ -187,14 +214,21 @@ export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
||||
if (!isReady) return;
|
||||
|
||||
setCurrentAreas((prevAreas) => {
|
||||
// 진행도 업데이트
|
||||
const updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
// 드래그 벡터 업데이트
|
||||
return AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
});
|
||||
}, [isReady]);
|
||||
// 1. 기존 영역 애니메이션 업데이트
|
||||
let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||||
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
|
||||
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 (
|
||||
<div
|
||||
|
||||
@ -16,6 +16,14 @@ export class AnimationLoop {
|
||||
return areas.map((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);
|
||||
|
||||
@ -56,6 +64,11 @@ export class AnimationLoop {
|
||||
deltaTime: number
|
||||
): DistortionArea[] {
|
||||
return areas.map((area) => {
|
||||
// duration이 0이면 progress 업데이트 안 함
|
||||
if (area.movement.duration <= 0) {
|
||||
return area;
|
||||
}
|
||||
|
||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||
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() {
|
||||
console.log('[ThreeScene] render() 호출됨, mesh:', this.mesh);
|
||||
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,
|
||||
};
|
||||
};
|
||||
13
src/index.ts
13
src/index.ts
@ -20,6 +20,14 @@ export type {
|
||||
AnimationTicker,
|
||||
} from './types';
|
||||
|
||||
// 마우스 인터랙션 타입
|
||||
export type {
|
||||
SpringPhysicsConfig,
|
||||
MouseInteractionConfig,
|
||||
MouseState,
|
||||
SpringState,
|
||||
} from './types/interaction';
|
||||
|
||||
// 유틸리티 함수
|
||||
export { applyEasing } from './utils/easing';
|
||||
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 { ShaderManager } from './engine/ShaderManager';
|
||||
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