Update documentation and bump version to 1.5.0

- README.md에 마우스 인터랙션 및 파티클 이펙트 설명 추가
- 에디터 컴포넌트 및 관련 훅(useDistortionEditor 등) 문서화
- 설치 가이드 및 피어 디펜던시 정보 업데이트
- 패키지 버전을 1.5.0으로 상향 조정
- .claude 로컬 설정의 허용된 Bash 명령어 목록 업데이트
This commit is contained in:
BaekRyang 2026-03-13 14:32:33 +09:00
parent 672dd80b9d
commit 77f44141a1
3 changed files with 735 additions and 177 deletions

View File

@ -15,7 +15,8 @@
"Bash(find:*)", "Bash(find:*)",
"Bash(nul)", "Bash(nul)",
"Bash(cd:*)", "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": [], "deny": [],
"ask": [] "ask": []

907
README.md
View File

@ -1,20 +1,23 @@
# Responsive Image Canvas # Responsive Image Canvas
GPU 가속 이미지 왜곡 효과를 제공하는 React 컴포넌트 라이브러리입니다. Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 애니메이션을 구현합니다. GPU 가속 이미지 왜곡 효과를 제공하는 React 컴포넌트 라이브러리입니다.
Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 애니메이션, 마우스/터치 인터랙션, 파티클 이펙트를 구현합니다.
## 특징 ## 특징
- 🚀 GPU 가속 렌더링 (Three.js + WebGL) - GPU 가속 렌더링 (Three.js + WebGL)
- 🎨 최대 8개의 독립적인 왜곡 영역 지원 - 최대 8개의 독립적인 왜곡 영역 지원
- ⚡ 60fps 실시간 애니메이션 - 스프링 물리 기반 마우스/터치 인터랙션
- 🎯 정규화된 좌표계 (0.0 - 1.0) - 이모지 & 스프라이트 시트 파티클 이펙트
- 🔧 TypeScript 완벽 지원 - 모션 프리셋 & 커스텀 이징 함수
- 📦 ESM & CommonJS 모두 지원 - 렌즈 왜곡 (볼록/오목) 효과
- 영역 편집을 위한 에디터 컴포넌트
- TypeScript & ESM/CJS 지원
## 설치 ## 설치
```bash ```bash
npm install responsive-image-canvas npm install @baekryang/responsive-image-canvas
``` ```
### Peer Dependencies ### Peer Dependencies
@ -23,25 +26,38 @@ npm install responsive-image-canvas
npm install react react-dom three 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 ```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[] = [ const areas: DistortionArea[] = [
{ {
id: 'area-1', id: 'area-1',
basePoints: [ basePoints: [
{ x: 0.2, y: 0.2 }, // 좌상단 { x: 0.3, y: 0.3 }, // 좌상단
{ x: 0.4, y: 0.2 }, // 우상단 { x: 0.7, y: 0.3 }, // 우상단
{ x: 0.4, y: 0.4 }, // 우하단 { x: 0.7, y: 0.7 }, // 우하단
{ x: 0.2, y: 0.4 }, // 좌하단 { x: 0.3, y: 0.7 }, // 좌하단
], ],
movement: { movement: {
vectorA: { x: 0.1, y: 0.1 }, preset: 'horizontal',
vectorB: { x: -0.1, y: -0.1 }, vectorA: { x: 0.1, y: 0 },
vectorB: { x: -0.1, y: 0 },
duration: 2.0, duration: 2.0,
easing: 'easeInOut', easing: 'easeInOut',
strength: 0.15,
}, },
distortionStrength: 0.5, distortionStrength: 0.5,
progress: 0, progress: 0,
@ -51,204 +67,745 @@ const areas: DistortionArea[] = [
function App() { function App() {
return ( return (
<div style={{ width: '800px', height: '600px' }}> <ImageDistortion
<ImageDistortion imageSrc="/image.jpg"
imageSrc="/path/to/image.jpg" areas={areas}
areas={areas} style={{ width: '100%', height: '100%' }}
isPlaying={true} />
/> );
}
```
> **좌표계**: 모든 좌표는 **정규화 좌표(0.0 ~ 1.0)** 를 사용합니다. `(0, 0)`은 이미지 좌상단, `(1, 1)`은 우하단입니다.
---
## 컴포넌트
### `<ImageDistortion />`
이미지 왜곡 및 인터랙션 렌더링을 담당하는 메인 컴포넌트입니다.
```tsx
<ImageDistortion
imageSrc="/image.jpg"
areas={areas}
mouseInteraction={mouseConfig}
spriteEffectAreas={spriteEffectAreas}
style={{ width: '100%', height: '100%' }}
/>
```
| Prop | 타입 | 필수 | 설명 |
|------|------|:----:|------|
| `imageSrc` | `string` | O | 이미지 URL |
| `areas` | `DistortionArea[]` | O | 왜곡 영역 배열 |
| `mouseInteraction` | `MouseInteractionConfig` | | 마우스/터치 인터랙션 설정 |
| `spriteEffectAreas` | `SpriteEffectArea[]` | | 파티클 이펙트 영역 |
| `isPlaying` | `boolean` | | 애니메이션 재생 여부 (기본: `true`) |
| `style` | `CSSProperties` | | 컨테이너 스타일 |
| `className` | `string` | | 컨테이너 클래스 |
### `<EditorCanvas />`
영역 편집을 위한 시각적 에디터 오버레이입니다. 꼭짓점 드래그, 영역 선택 등을 제공합니다.
```tsx
<EditorCanvas
areas={areas}
selectedAreaId={selectedId}
imageSrc="/image.jpg"
width={imageWidth}
height={imageHeight}
onUpdatePoint={(areaId, pointIndex, point) => { /* ... */ }}
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[]` | | 파티클 이펙트 영역 |
### `<AreaList />`, `<ParameterPanel />`
영역 목록 관리 및 파라미터 편집을 위한 보조 에디터 컴포넌트입니다.
---
## 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<DistortionArea>) => 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<MouseInteractionConfig>) => void
reset, // () => void
isDragging, // () => boolean
getInteractingAreaIndices, // () => Set<number>
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,
};
<ImageDistortion
imageSrc="/image.jpg"
areas={areas}
mouseInteraction={mouseConfig}
/>
```
### 물리 프리셋 예시
```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)
},
},
],
},
];
<ImageDistortion
imageSrc="/image.jpg"
areas={areas}
spriteEffectAreas={spriteEffectAreas}
/>
```
### 지속 방출 (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 (
<div style={{ position: 'relative', width: imageWidth, height: imageHeight }}>
{isEditing ? (
<EditorCanvas
areas={state.areas}
selectedAreaId={state.selectedAreaId}
imageSrc={imageSrc}
width={imageWidth}
height={imageHeight}
onUpdatePoint={updatePoint}
onUpdateArea={(id, updates) => updateArea(id, updates)}
draggingPointIndex={state.draggingPointIndex}
onStartDragging={startDragging}
onStopDragging={stopDragging}
onSelectArea={selectArea}
/>
) : (
<ImageDistortion
imageSrc={imageSrc}
areas={state.areas}
mouseInteraction={mouseConfig}
style={{ width: '100%', height: '100%' }}
/>
)}
</div> </div>
); );
} }
``` ```
## 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 ```typescript
interface Point { interface Point {
x: number; // 0.0 - 1.0 (정규화된 좌표) x: number; // 0.0 ~ 1.0
y: 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 ```typescript
interface DistortionMovement { interface SpringPhysicsConfig {
vectorA: Point; // 시작 벡터 stiffness: number;
vectorB: Point; // 종료 벡터 damping: number;
duration: number; // 지속 시간 (초) mass: number;
easing: EasingFunction; // 이징 함수 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 ```typescript
type EasingFunction = type EasingFunction =
| 'linear' | 'linear'
| 'easeIn' | 'easeIn' | 'easeOut' | 'easeInOut'
| 'easeOut' | 'easeInQuad' | 'easeOutQuad'
| 'easeInOut' | 'easeInCubic' | 'easeOutCubic';
| 'easeInQuad'
| 'easeOutQuad'; 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<DistortionArea[]>([]);
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 (
<div>
<button onClick={addArea}>영역 추가</button>
<ImageDistortion
imageSrc="/image.jpg"
areas={areas}
/>
</div>
);
}
```
### 유틸리티 함수 사용
```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 ```typescript
// vite.config.ts import { DEFAULT_AREA, SHADER_CONFIG, ANIMATION_CONFIG } from '@baekryang/responsive-image-canvas';
import { defineConfig } from 'vite';
export default defineConfig({ DEFAULT_AREA.DISTORTION_STRENGTH // 0.5
publicDir: 'public', DEFAULT_AREA.DURATION // 2.0
// node_modules의 셰이더 파일을 복사 DEFAULT_AREA.EASING // 'easeInOut'
build: { DEFAULT_AREA.LENS_STRENGTH // 0
rollupOptions: { DEFAULT_AREA.SNAP_STEPS // 0
output: {
assetFileNames: 'assets/[name].[ext]', 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을 지원하지 않는 브라우저에서는 동작하지 않습니다 - WebGL을 지원하지 않는 브라우저에서는 동작하지 않습니다
- 모바일 환경에서는 성능이 제한될 수 있습니다
- 최대 8개의 왜곡 영역만 지원합니다 - 최대 8개의 왜곡 영역만 지원합니다
- `emoji``spriteUrl`은 하나만 사용 가능합니다 (둘 다 없으면 텍스처 로드 실패)
## 브라우저 지원
- Chrome 60+
- Firefox 60+
- Safari 12+
- Edge 79+
## 라이선스 ## 라이선스
MIT MIT
## 기여
이슈와 PR을 환영합니다!
## 관련 프로젝트
- [Three.js](https://threejs.org/)
- [React Three Fiber](https://github.com/pmndrs/react-three-fiber)

View File

@ -1,6 +1,6 @@
{ {
"name": "@baekryang/responsive-image-canvas", "name": "@baekryang/responsive-image-canvas",
"version": "1.4.1", "version": "1.5.0",
"publishConfig": { "publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/" "registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
}, },