From 77f44141a178f1d5bf512f21c9caac7d06321af9 Mon Sep 17 00:00:00 2001 From: BaekRyang Date: Fri, 13 Mar 2026 14:32:33 +0900 Subject: [PATCH] Update documentation and bump version to 1.5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md에 마우스 인터랙션 및 파티클 이펙트 설명 추가 - 에디터 컴포넌트 및 관련 훅(useDistortionEditor 등) 문서화 - 설치 가이드 및 피어 디펜던시 정보 업데이트 - 패키지 버전을 1.5.0으로 상향 조정 - .claude 로컬 설정의 허용된 Bash 명령어 목록 업데이트 --- .claude/settings.local.json | 3 +- README.md | 907 +++++++++++++++++++++++++++++------- package.json | 2 +- 3 files changed, 735 insertions(+), 177 deletions(-) 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/" },