Update documentation and bump version to 1.5.0
- README.md에 마우스 인터랙션 및 파티클 이펙트 설명 추가 - 에디터 컴포넌트 및 관련 훅(useDistortionEditor 등) 문서화 - 설치 가이드 및 피어 디펜던시 정보 업데이트 - 패키지 버전을 1.5.0으로 상향 조정 - .claude 로컬 설정의 허용된 Bash 명령어 목록 업데이트
This commit is contained in:
parent
672dd80b9d
commit
77f44141a1
@ -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": []
|
||||
|
||||
907
README.md
907
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 (
|
||||
<div style={{ width: '800px', height: '600px' }}>
|
||||
<ImageDistortion
|
||||
imageSrc="/path/to/image.jpg"
|
||||
areas={areas}
|
||||
isPlaying={true}
|
||||
/>
|
||||
<ImageDistortion
|
||||
imageSrc="/image.jpg"
|
||||
areas={areas}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> **좌표계**: 모든 좌표는 **정규화 좌표(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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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<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
|
||||
// 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
|
||||
@ -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/"
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user