diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 55def7c..b0371f6 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -15,7 +15,8 @@
"Bash(find:*)",
"Bash(nul)",
"Bash(cd:*)",
- "Bash(ls -la /d/Projects/WebstormProjects/raonnuri/src/app/\\\\[locale\\\\]/)"
+ "Bash(ls -la /d/Projects/WebstormProjects/raonnuri/src/app/\\\\[locale\\\\]/)",
+ "Bash(find \"D:\\\\Projects\\\\WebstormProjects\\\\raonnuri\\\\src\\\\app\\\\[locale]\\\\interaction\" -type f \\\\\\( -name \"*.tsx\" -o -name \"*.ts\" \\\\\\) 2>/dev/null | head -10)"
],
"deny": [],
"ask": []
diff --git a/README.md b/README.md
index 2a16f9b..a5bf830 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,23 @@
# Responsive Image Canvas
-GPU 가속 이미지 왜곡 효과를 제공하는 React 컴포넌트 라이브러리입니다. Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 애니메이션을 구현합니다.
+GPU 가속 이미지 왜곡 효과를 제공하는 React 컴포넌트 라이브러리입니다.
+Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 애니메이션, 마우스/터치 인터랙션, 파티클 이펙트를 구현합니다.
## 특징
-- 🚀 GPU 가속 렌더링 (Three.js + WebGL)
-- 🎨 최대 8개의 독립적인 왜곡 영역 지원
-- ⚡ 60fps 실시간 애니메이션
-- 🎯 정규화된 좌표계 (0.0 - 1.0)
-- 🔧 TypeScript 완벽 지원
-- 📦 ESM & CommonJS 모두 지원
+- GPU 가속 렌더링 (Three.js + WebGL)
+- 최대 8개의 독립적인 왜곡 영역 지원
+- 스프링 물리 기반 마우스/터치 인터랙션
+- 이모지 & 스프라이트 시트 파티클 이펙트
+- 모션 프리셋 & 커스텀 이징 함수
+- 렌즈 왜곡 (볼록/오목) 효과
+- 영역 편집을 위한 에디터 컴포넌트
+- TypeScript & ESM/CJS 지원
## 설치
```bash
-npm install responsive-image-canvas
+npm install @baekryang/responsive-image-canvas
```
### Peer Dependencies
@@ -23,25 +26,38 @@ npm install responsive-image-canvas
npm install react react-dom three
```
+| 패키지 | 버전 |
+|--------|------|
+| `react` | `^18.0.0 \|\| ^19.0.0` |
+| `react-dom` | `^18.0.0 \|\| ^19.0.0` |
+| `three` | `>=0.150.0` |
+
+---
+
## 기본 사용법
+### 이미지 왜곡 표시 (View Mode)
+
```tsx
-import { ImageDistortion, DistortionArea } from 'responsive-image-canvas';
+import { ImageDistortion } from '@baekryang/responsive-image-canvas';
+import type { DistortionArea } from '@baekryang/responsive-image-canvas';
const areas: DistortionArea[] = [
{
id: 'area-1',
basePoints: [
- { x: 0.2, y: 0.2 }, // 좌상단
- { x: 0.4, y: 0.2 }, // 우상단
- { x: 0.4, y: 0.4 }, // 우하단
- { x: 0.2, y: 0.4 }, // 좌하단
+ { x: 0.3, y: 0.3 }, // 좌상단
+ { x: 0.7, y: 0.3 }, // 우상단
+ { x: 0.7, y: 0.7 }, // 우하단
+ { x: 0.3, y: 0.7 }, // 좌하단
],
movement: {
- vectorA: { x: 0.1, y: 0.1 },
- vectorB: { x: -0.1, y: -0.1 },
+ preset: 'horizontal',
+ vectorA: { x: 0.1, y: 0 },
+ vectorB: { x: -0.1, y: 0 },
duration: 2.0,
easing: 'easeInOut',
+ strength: 0.15,
},
distortionStrength: 0.5,
progress: 0,
@@ -51,204 +67,745 @@ const areas: DistortionArea[] = [
function App() {
return (
-
-
+
+ );
+}
+```
+
+> **좌표계**: 모든 좌표는 **정규화 좌표(0.0 ~ 1.0)** 를 사용합니다. `(0, 0)`은 이미지 좌상단, `(1, 1)`은 우하단입니다.
+
+---
+
+## 컴포넌트
+
+### `
`
+
+이미지 왜곡 및 인터랙션 렌더링을 담당하는 메인 컴포넌트입니다.
+
+```tsx
+
+```
+
+| Prop | 타입 | 필수 | 설명 |
+|------|------|:----:|------|
+| `imageSrc` | `string` | O | 이미지 URL |
+| `areas` | `DistortionArea[]` | O | 왜곡 영역 배열 |
+| `mouseInteraction` | `MouseInteractionConfig` | | 마우스/터치 인터랙션 설정 |
+| `spriteEffectAreas` | `SpriteEffectArea[]` | | 파티클 이펙트 영역 |
+| `isPlaying` | `boolean` | | 애니메이션 재생 여부 (기본: `true`) |
+| `style` | `CSSProperties` | | 컨테이너 스타일 |
+| `className` | `string` | | 컨테이너 클래스 |
+
+### `
`
+
+영역 편집을 위한 시각적 에디터 오버레이입니다. 꼭짓점 드래그, 영역 선택 등을 제공합니다.
+
+```tsx
+
{ /* ... */ }}
+ onUpdateArea={(areaId, updates) => { /* ... */ }}
+ draggingPointIndex={draggingIndex}
+ onStartDragging={(index) => { /* ... */ }}
+ onStopDragging={() => { /* ... */ }}
+ style={editorCanvasStyle}
+ showEditor={true}
+ onSelectArea={(areaId) => { /* ... */ }}
+ spriteEffectAreas={spriteEffectAreas}
+/>
+```
+
+| Prop | 타입 | 필수 | 설명 |
+|------|------|:----:|------|
+| `areas` | `DistortionArea[]` | O | 왜곡 영역 배열 |
+| `selectedAreaId` | `string \| null` | O | 선택된 영역 ID |
+| `imageSrc` | `string` | O | 이미지 URL |
+| `width` | `number` | O | 이미지 너비 (px) |
+| `height` | `number` | O | 이미지 높이 (px) |
+| `onUpdatePoint` | `(areaId, pointIndex, point) => void` | O | 꼭짓점 이동 콜백 |
+| `onUpdateArea` | `(areaId, updates) => void` | O | 영역 업데이트 콜백 |
+| `draggingPointIndex` | `number \| null` | O | 드래그 중인 포인트 인덱스 |
+| `onStartDragging` | `(index) => void` | O | 드래그 시작 콜백 |
+| `onStopDragging` | `() => void` | O | 드래그 종료 콜백 |
+| `style` | `EditorCanvasStyle` | | 에디터 UI 스타일 |
+| `showEditor` | `boolean` | | 에디터 표시 여부 (기본: `true`) |
+| `onSelectArea` | `(areaId) => void` | | 영역 선택 콜백 |
+| `spriteEffectAreas` | `SpriteEffectArea[]` | | 파티클 이펙트 영역 |
+
+### ``, ``
+
+영역 목록 관리 및 파라미터 편집을 위한 보조 에디터 컴포넌트입니다.
+
+---
+
+## Hooks
+
+### `useDistortionEditor`
+
+에디터 상태 관리를 위한 핵심 훅입니다.
+
+```tsx
+import { useDistortionEditor, DEFAULT_AREA } from '@baekryang/responsive-image-canvas';
+
+const {
+ state, // { areas, selectedAreaId, editMode, draggingPointIndex }
+ selectArea, // (areaId: string | null) => void
+ addArea, // (area: DistortionArea) => void
+ removeArea, // (areaId: string) => void
+ updateArea, // (areaId: string, updates: Partial) => void
+ updatePoint, // (areaId: string, pointIndex: number, point: Point) => void
+ startDragging, // (pointIndex: number) => void
+ stopDragging, // () => void
+ getSelectedArea, // () => DistortionArea | null
+} = useDistortionEditor(initialAreas);
+```
+
+#### 사용 예시
+
+```tsx
+// 새 영역 추가
+const handleAddArea = () => {
+ addArea({
+ id: `area-${Date.now()}`,
+ basePoints: [
+ { x: 0.3, y: 0.3 },
+ { x: 0.7, y: 0.3 },
+ { x: 0.7, y: 0.7 },
+ { x: 0.3, y: 0.7 },
+ ],
+ movement: {
+ preset: 'none',
+ vectorA: { x: 0, y: 0 },
+ vectorB: { x: 0, y: 0 },
+ duration: DEFAULT_AREA.DURATION,
+ easing: DEFAULT_AREA.EASING,
+ strength: 0.15,
+ },
+ distortionStrength: DEFAULT_AREA.DISTORTION_STRENGTH,
+ progress: 0,
+ dragVector: { x: 0, y: 0 },
+ });
+};
+
+// 선택된 영역 업데이트
+updateArea(state.selectedAreaId, {
+ distortionStrength: 0.8,
+ lensEffect: { strength: 0.3 },
+});
+```
+
+### `useMouseInteraction`
+
+마우스/터치 기반 스프링 물리 인터랙션을 제공합니다.
+
+```tsx
+import { useMouseInteraction } from '@baekryang/responsive-image-canvas';
+
+const {
+ updateInteraction, // (areas, deltaTime) => DistortionArea[]
+ updateConfig, // (newConfig: Partial) => void
+ reset, // () => void
+ isDragging, // () => boolean
+ getInteractingAreaIndices, // () => Set
+ getMouseState, // () => MouseState
+} = useMouseInteraction(containerRef, mouseConfig);
+```
+
+### `useAnimationFrame`
+
+requestAnimationFrame 기반 애니메이션 루프입니다.
+
+```tsx
+import { useAnimationFrame } from '@baekryang/responsive-image-canvas';
+
+useAnimationFrame((deltaTime) => {
+ // deltaTime: 초 단위
+}, isPlaying);
+```
+
+---
+
+## 마우스 인터랙션
+
+스프링 물리 기반의 마우스/터치 인터랙션을 설정합니다.
+
+```tsx
+import type { MouseInteractionConfig } from '@baekryang/responsive-image-canvas';
+
+const mouseConfig: MouseInteractionConfig = {
+ enabled: true,
+ physics: {
+ stiffness: 80, // 탄성 계수 (높을수록 빠르게 반응)
+ damping: 8, // 감쇠 계수 (높을수록 빠르게 정지)
+ mass: 1.0, // 질량 (높을수록 무겁게 반응)
+ influenceRadius: 0.15, // 영향 반경 (정규화 좌표)
+ maxStrength: 0.5, // 최대 왜곡 강도
+ },
+ minVelocity: 0.1,
+ maxVelocity: 1.0,
+ velocityMultiplier: 0.15,
+};
+
+
+```
+
+### 물리 프리셋 예시
+
+```tsx
+// 부드러운 반응
+const soft = {
+ stiffness: 30, damping: 20, mass: 2.0, maxStrength: 0.3, influenceRadius: 0.15
+};
+
+// 탄성 있는 반응
+const bouncy = {
+ stiffness: 150, damping: 5, mass: 1.0, maxStrength: 0.5, influenceRadius: 0.15
+};
+
+// 무거운 반응
+const heavy = {
+ stiffness: 80, damping: 15, mass: 3.0, maxStrength: 0.4, influenceRadius: 0.15
+};
+```
+
+영역별로 개별 물리 설정도 가능합니다:
+
+```tsx
+const area: DistortionArea = {
+ // ...
+ physics: {
+ stiffness: 150,
+ damping: 5,
+ mass: 1.0,
+ influenceRadius: 0.15,
+ maxStrength: 0.5,
+ },
+};
+```
+
+---
+
+## 파티클 이펙트
+
+이미지 위에 이모지 또는 스프라이트 이미지 기반 파티클 이펙트를 추가합니다.
+
+### 이모지 파티클
+
+```tsx
+import type { SpriteEffectArea, SpriteEffectConfig } from '@baekryang/responsive-image-canvas';
+
+const spriteEffectAreas: SpriteEffectArea[] = [
+ {
+ id: 'effect-area-1',
+ position: { x: 0.5, y: 0.5 }, // 이펙트 중심 (정규화 좌표)
+ radius: 0.15, // 방출 반경
+ effects: [
+ {
+ id: 'hearts',
+ emoji: '❤️', // 이모지 → Canvas 텍스처로 자동 변환
+ trigger: 'touch', // 'touch': 클릭 시 발사 | 'ambient': 지속 방출
+ blendMode: 'normal',
+ maxParticles: 40,
+ burstCount: 8, // touch 트리거 시 한번에 생성할 파티클 수
+ lifetime: [1, 2.5], // [최소, 최대] 수명 (초)
+ initialScale: [0.04, 0.08], // [최소, 최대] 크기 (정규화)
+ initialSpeed: [0.06, 0.12], // [최소, 최대] 초기 속도
+ emitAngle: [0, 360], // 방출 각도 범위 (도)
+ overLifetime: {
+ scale: [1, 0.3], // 수명에 따른 크기 변화
+ opacity: [1, 0], // 수명에 따른 투명도 변화
+ velocityDamping: 0.94, // 속도 감쇠 (0~1)
+ },
+ },
+ ],
+ },
+];
+
+
+```
+
+### 지속 방출 (Ambient) 이펙트
+
+```tsx
+const fireEffect: SpriteEffectConfig = {
+ id: 'fire',
+ emoji: '🔥',
+ trigger: 'ambient',
+ blendMode: 'normal',
+ maxParticles: 30,
+ emitRate: 3, // 초당 생성 파티클 수
+ lifetime: [1, 2.5],
+ initialScale: [0.04, 0.08],
+ initialSpeed: [0.02, 0.05],
+ emitAngle: [250, 290], // 위쪽 방향으로 제한
+ emitRadius: 0.15,
+ overLifetime: {
+ scale: [1, 0.4],
+ opacity: [1, 0],
+ velocityDamping: 0.97,
+ },
+};
+```
+
+### 스프라이트 이미지 사용
+
+이모지 대신 URL 기반 스프라이트 이미지를 사용할 수 있습니다:
+
+```tsx
+const effect: SpriteEffectConfig = {
+ id: 'custom-sprite',
+ spriteUrl: '/sprites/particle.png', // emoji 대신 spriteUrl 사용
+ trigger: 'ambient',
+ // ... 나머지 설정 동일
+};
+```
+
+### 스프라이트 시트
+
+```tsx
+const effect: SpriteEffectConfig = {
+ id: 'animated-sprite',
+ spriteUrl: '/sprites/explosion-sheet.png',
+ spriteSheet: {
+ columns: 4,
+ rows: 4,
+ totalFrames: 16,
+ fps: 24,
+ loop: false,
+ },
+ // ... 나머지 설정
+};
+```
+
+---
+
+## 모션 프리셋
+
+영역 애니메이션에 사용할 수 있는 빌트인 모션 프리셋입니다.
+
+| 프리셋 | 동작 |
+|--------|------|
+| `none` | 움직임 없음 |
+| `horizontal` | 좌우 왕복 |
+| `vertical` | 상하 왕복 |
+| `rotate-cw` | 시계 방향 회전 |
+| `rotate-ccw` | 반시계 방향 회전 |
+| `pulse` | 맥동 (확대/축소) |
+| `diagonal-1` | 대각선 (↗↙) |
+| `diagonal-2` | 대각선 (↘↖) |
+
+```tsx
+const area: DistortionArea = {
+ // ...
+ movement: {
+ preset: 'horizontal',
+ vectorA: { x: 0.1, y: 0 },
+ vectorB: { x: -0.1, y: 0 },
+ duration: 2.0,
+ easing: 'easeInOut',
+ strength: 0.15,
+ },
+};
+```
+
+### 커스텀 모션 프리셋 등록
+
+```tsx
+import { registerMotionPresets } from '@baekryang/responsive-image-canvas';
+
+registerMotionPresets({
+ 'wave': (strength) => ({ x: strength * 0.5, y: strength * 0.3 }),
+ 'spiral': (strength) => ({ x: strength, y: 0 }),
+}, ['spiral']); // 두 번째 인자: 회전형 프리셋 이름 목록
+```
+
+### 이징 함수
+
+| 이징 | 설명 |
+|------|------|
+| `linear` | 등속 |
+| `easeIn` / `easeInQuad` / `easeInCubic` | 느리게 시작 → 빠르게 |
+| `easeOut` / `easeOutQuad` / `easeOutCubic` | 빠르게 시작 → 느리게 |
+| `easeInOut` | 양쪽 모두 부드럽게 |
+
+---
+
+## 렌즈 효과 & 스텝 이징
+
+### 렌즈 왜곡
+
+영역에 볼록/오목 렌즈 효과를 적용합니다.
+
+```tsx
+const area: DistortionArea = {
+ // ...
+ lensEffect: {
+ strength: 0.5, // 양수: 볼록, 음수: 오목 (-1.0 ~ 1.0)
+ },
+};
+```
+
+### 스텝 이징
+
+애니메이션을 이산적인 단계로 분할합니다.
+
+```tsx
+const area: DistortionArea = {
+ // ...
+ snapSteps: 3, // 0: 부드러운 연속 애니메이션, 1~5: 단계 수
+};
+```
+
+---
+
+## 에디터 스타일 커스터마이징
+
+`EditorCanvas`의 시각적 요소를 커스터마이징할 수 있습니다.
+
+```tsx
+import type { EditorCanvasStyle } from '@baekryang/responsive-image-canvas';
+
+const editorStyle: EditorCanvasStyle = {
+ // 가이드 원 (최대 3단계)
+ circleLevels: [
+ { radius: 0.5, opacity: 0.4, lineWidth: 2.5, color: 'rgba(255,107,157,0.8)', dashPattern: [10, 5] },
+ { radius: 0.33, opacity: 0.7, lineWidth: 3, color: 'rgba(255,107,157,0.9)', dashPattern: [8, 4] },
+ { radius: 0.167, opacity: 1.0, lineWidth: 4, color: 'rgba(255,107,157,1)', dashPattern: [6, 3] },
+ ],
+ circleFillColor: 'rgba(255, 107, 157, 0.08)',
+
+ // 중심점
+ centerPoint: {
+ radius: 6,
+ fillColor: 'rgba(255, 107, 157, 1)',
+ strokeColor: 'rgba(255, 255, 255, 0.9)',
+ strokeWidth: 2.5,
+ },
+
+ // 꼭짓점 핸들
+ pointHandle: {
+ size: 18,
+ fillColor: '#FF6B9D',
+ strokeColor: '#ffffff',
+ strokeWidth: 2.5,
+ labelColor: '#FF6B9D',
+ labelFontSize: 12,
+ },
+
+ // 영역 외곽선
+ areaOutline: {
+ selectedColor: '#FF6B9D',
+ unselectedColor: 'rgba(0, 48, 255, 0.55)',
+ selectedWidth: 2.5,
+ unselectedWidth: 4,
+ unselectedDashPattern: [6, 4],
+ selectedFillColor: 'rgba(255, 107, 157, 0.12)',
+ unselectedFillColor: 'rgba(156, 163, 175, 0.05)',
+ },
+};
+```
+
+---
+
+## 데이터 영속화
+
+`DistortionArea`에서 런타임 필드(`progress`, `dragVector`)를 제외하고 저장합니다.
+
+### 저장
+
+```tsx
+const areasToSave = state.areas.map(area => ({
+ id: area.id,
+ basePoints: area.basePoints,
+ movement: area.movement,
+ distortionStrength: area.distortionStrength,
+ physics: area.physics,
+ lensEffect: area.lensEffect,
+ snapSteps: area.snapSteps,
+}));
+
+// DB에 저장 (Firestore, REST API 등)
+await saveToDatabase(areasToSave);
+```
+
+### 로드
+
+```tsx
+const loadedAreas: DistortionArea[] = savedData.map(area => ({
+ ...area,
+ basePoints: area.basePoints as [Point, Point, Point, Point],
+ movement: {
+ ...area.movement,
+ easing: area.movement.easing as EasingFunction,
+ },
+ progress: 0, // 런타임 상태 초기화
+ dragVector: { x: 0, y: 0 }, // 런타임 상태 초기화
+}));
+```
+
+---
+
+## 통합 예제: View + Editor
+
+```tsx
+import {
+ ImageDistortion,
+ EditorCanvas,
+ useDistortionEditor,
+ DEFAULT_AREA,
+} from '@baekryang/responsive-image-canvas';
+import type {
+ DistortionArea,
+ MouseInteractionConfig,
+ SpriteEffectArea,
+ EditorCanvasStyle,
+} from '@baekryang/responsive-image-canvas';
+
+function ImageEditor({ imageSrc, imageWidth, imageHeight }) {
+ const [isEditing, setIsEditing] = useState(true);
+
+ const {
+ state, selectArea, addArea, removeArea,
+ updateArea, updatePoint, startDragging, stopDragging,
+ } = useDistortionEditor([]);
+
+ const mouseConfig: MouseInteractionConfig = {
+ enabled: !isEditing,
+ physics: { stiffness: 80, damping: 8, mass: 1.0, influenceRadius: 0.15, maxStrength: 0.5 },
+ };
+
+ return (
+
+ {isEditing ? (
+ updateArea(id, updates)}
+ draggingPointIndex={state.draggingPointIndex}
+ onStartDragging={startDragging}
+ onStopDragging={stopDragging}
+ onSelectArea={selectArea}
+ />
+ ) : (
+
+ )}
);
}
```
-## Props
+---
-### `ImageDistortionProps`
+## 타입 레퍼런스
-| Prop | 타입 | 필수 | 기본값 | 설명 |
-|------|------|------|--------|------|
-| `imageSrc` | `string` | ✓ | - | 이미지 소스 URL |
-| `areas` | `DistortionArea[]` | ✓ | - | 왜곡 영역 배열 |
-| `vertexShaderPath` | `string` | ✗ | `/shaders/distortion.vert.glsl` | 커스텀 버텍스 셰이더 경로 |
-| `fragmentShaderPath` | `string` | ✗ | `/shaders/distortion.frag.glsl` | 커스텀 프래그먼트 셰이더 경로 |
-| `isPlaying` | `boolean` | ✗ | `true` | 애니메이션 재생 여부 |
-| `style` | `CSSProperties` | ✗ | - | 컨테이너 스타일 |
-| `className` | `string` | ✗ | - | 컨테이너 클래스명 |
-
-## 타입 정의
-
-### `DistortionArea`
-
-```typescript
-interface DistortionArea {
- id: string; // 고유 식별자
- basePoints: [Point, Point, Point, Point]; // 사각형의 네 모서리
- movement: DistortionMovement; // 애니메이션 설정
- distortionStrength: number; // 왜곡 강도 (0.0 - 1.0)
- progress: number; // 애니메이션 진행도 (0.0 - 1.0)
- dragVector: Point; // 현재 드래그 벡터
-}
-```
-
-### `Point`
+### 핵심 타입
```typescript
interface Point {
- x: number; // 0.0 - 1.0 (정규화된 좌표)
- y: number; // 0.0 - 1.0 (정규화된 좌표)
+ x: number; // 0.0 ~ 1.0
+ y: number; // 0.0 ~ 1.0
+}
+
+interface DistortionArea {
+ id: string;
+ basePoints: [Point, Point, Point, Point]; // [좌상, 우상, 우하, 좌하]
+ movement: DistortionMovement;
+ distortionStrength: number; // 0.0 ~ 1.0
+ progress: number; // 0.0 ~ 1.0 (런타임)
+ dragVector: Point; // (런타임)
+ physics?: SpringPhysicsConfig;
+ lensEffect?: { strength: number }; // -1.0 ~ 1.0
+ snapSteps?: number; // 0 ~ 5
+}
+
+interface DistortionMovement {
+ preset?: MotionPreset;
+ vectorA: Point;
+ vectorB: Point;
+ duration: number; // 초
+ easing: EasingFunction;
+ strength?: number;
}
```
-### `DistortionMovement`
+### 인터랙션 타입
```typescript
-interface DistortionMovement {
- vectorA: Point; // 시작 벡터
- vectorB: Point; // 종료 벡터
- duration: number; // 지속 시간 (초)
- easing: EasingFunction; // 이징 함수
+interface SpringPhysicsConfig {
+ stiffness: number;
+ damping: number;
+ mass: number;
+ 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;
}
```
-### `EasingFunction`
+### 파티클 이펙트 타입
+
+```typescript
+type SpriteEffectTrigger = 'ambient' | 'touch';
+type SpriteBlendMode = 'normal' | 'additive';
+
+interface SpriteEffectConfig {
+ id: string;
+ trigger: SpriteEffectTrigger;
+ emoji?: string; // 이모지 (spriteUrl과 택1)
+ spriteUrl?: string; // 스프라이트 이미지 URL
+ blendMode?: SpriteBlendMode;
+ maxParticles: number;
+ emitRate?: number; // ambient용 (초당 생성 수)
+ burstCount?: number; // touch용 (클릭당 생성 수)
+ lifetime: [number, number]; // [최소, 최대] 초
+ initialScale: [number, number];
+ initialSpeed: [number, number];
+ emitAngle?: [number, number]; // 도
+ emitOffset?: Point;
+ emitRadius?: number;
+ overLifetime?: SpriteParticleOverLifetime;
+ spriteSheet?: SpriteSheetConfig;
+}
+
+interface SpriteEffectArea {
+ id: string;
+ position: Point;
+ radius?: number; // 기본: 0.1
+ effects: SpriteEffectConfig[];
+}
+
+interface SpriteParticleOverLifetime {
+ scale?: number[]; // [시작, 끝] 또는 [시작, 중간, 끝]
+ opacity?: number[];
+ rotationSpeed?: number; // 라디안/초
+ velocityDamping?: number; // 0 ~ 1
+}
+
+interface SpriteSheetConfig {
+ columns: number;
+ rows: number;
+ totalFrames: number;
+ fps: number;
+ loop?: boolean;
+}
+```
+
+### 이징 & 프리셋 타입
```typescript
type EasingFunction =
| 'linear'
- | 'easeIn'
- | 'easeOut'
- | 'easeInOut'
- | 'easeInQuad'
- | 'easeOutQuad';
+ | 'easeIn' | 'easeOut' | 'easeInOut'
+ | 'easeInQuad' | 'easeOutQuad'
+ | 'easeInCubic' | 'easeOutCubic';
+
+type BuiltInMotionPreset =
+ | 'none' | 'horizontal' | 'vertical'
+ | 'rotate-cw' | 'rotate-ccw'
+ | 'pulse' | 'diagonal-1' | 'diagonal-2';
+
+type MotionPreset = BuiltInMotionPreset | (string & {});
```
-## 고급 사용법
+---
-### 영역 동적 추가/제거
-
-```tsx
-function DynamicDistortion() {
- const [areas, setAreas] = useState([]);
-
- const addArea = () => {
- const newArea: DistortionArea = {
- id: `area-${Date.now()}`,
- basePoints: [
- { x: 0.3, y: 0.3 },
- { x: 0.7, y: 0.3 },
- { x: 0.7, y: 0.7 },
- { x: 0.3, y: 0.7 },
- ],
- movement: {
- vectorA: { x: 0.15, y: 0 },
- vectorB: { x: -0.15, y: 0 },
- duration: 3.0,
- easing: 'easeInOut',
- },
- distortionStrength: 0.6,
- progress: 0,
- dragVector: { x: 0, y: 0 },
- };
-
- setAreas([...areas, newArea]);
- };
-
- return (
-
-
-
-
- );
-}
-```
-
-### 유틸리티 함수 사용
-
-```tsx
-import { DEFAULT_AREA, applyEasing } from 'responsive-image-canvas';
-
-// 기본 설정값 사용
-const newArea = {
- ...DEFAULT_AREA,
- id: 'my-area',
- basePoints: [/* ... */],
-};
-
-// 이징 함수 직접 사용
-const easedValue = applyEasing(0.5, 'easeInOut');
-console.log(easedValue); // 0.5
-```
-
-## 셰이더 파일
-
-패키지는 기본 셰이더 파일을 포함하고 있습니다:
-- `dist/distortion.vert.glsl` - 버텍스 셰이더
-- `dist/distortion.frag.glsl` - 프래그먼트 셰이더
-
-웹 서버에서 이 파일들을 정적 파일로 제공해야 합니다.
-
-### Vite 설정 예시
+## 상수
```typescript
-// vite.config.ts
-import { defineConfig } from 'vite';
+import { DEFAULT_AREA, SHADER_CONFIG, ANIMATION_CONFIG } from '@baekryang/responsive-image-canvas';
-export default defineConfig({
- publicDir: 'public',
- // node_modules의 셰이더 파일을 복사
- build: {
- rollupOptions: {
- output: {
- assetFileNames: 'assets/[name].[ext]',
- },
- },
- },
-});
+DEFAULT_AREA.DISTORTION_STRENGTH // 0.5
+DEFAULT_AREA.DURATION // 2.0
+DEFAULT_AREA.EASING // 'easeInOut'
+DEFAULT_AREA.LENS_STRENGTH // 0
+DEFAULT_AREA.SNAP_STEPS // 0
+
+SHADER_CONFIG.MAX_AREAS // 8
+ANIMATION_CONFIG.TARGET_FPS // 60
```
-셰이더 파일을 public 폴더로 복사:
+---
-```bash
-cp node_modules/responsive-image-canvas/dist/*.glsl public/shaders/
+## 유틸리티
+
+```typescript
+import {
+ applyEasing,
+ registerMotionPreset,
+ registerMotionPresets,
+ unregisterMotionPreset,
+ getRegisteredPresets,
+ presetToVector,
+ isRotationPreset,
+} from '@baekryang/responsive-image-canvas';
+
+// 이징 함수 직접 사용
+const easedValue = applyEasing(0.5, 'easeInOut'); // 0.5
+
+// 커스텀 모션 프리셋
+registerMotionPreset('wobble', (strength) => ({
+ x: strength * Math.sin(Date.now() * 0.001),
+ y: 0,
+}));
+
+// 프리셋 → 벡터 변환
+const vector = presetToVector('horizontal', 0.15); // { x: 0.15, y: 0 }
```
-## 성능 최적화
-
-### 1. 영역 수 제한
-최대 8개의 영역까지 지원하지만, 성능을 위해 4개 이하를 권장합니다.
-
-### 2. 이미지 크기 최적화
-큰 이미지는 성능에 영향을 줄 수 있습니다. 적절한 크기로 리사이징하세요.
-
-### 3. 애니메이션 일시정지
-필요하지 않을 때는 `isPlaying={false}`로 설정하세요.
+---
## 제한사항
- WebGL을 지원하지 않는 브라우저에서는 동작하지 않습니다
-- 모바일 환경에서는 성능이 제한될 수 있습니다
- 최대 8개의 왜곡 영역만 지원합니다
-
-## 브라우저 지원
-
-- Chrome 60+
-- Firefox 60+
-- Safari 12+
-- Edge 79+
+- `emoji`와 `spriteUrl`은 하나만 사용 가능합니다 (둘 다 없으면 텍스처 로드 실패)
## 라이선스
-MIT
-
-## 기여
-
-이슈와 PR을 환영합니다!
-
-## 관련 프로젝트
-
-- [Three.js](https://threejs.org/)
-- [React Three Fiber](https://github.com/pmndrs/react-three-fiber)
+MIT
\ No newline at end of file
diff --git a/package.json b/package.json
index ce6eb28..447d967 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@baekryang/responsive-image-canvas",
- "version": "1.4.1",
+ "version": "1.5.0",
"publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
},