init
This commit is contained in:
commit
808ddd99ec
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(npm run build:*)",
|
||||||
|
"Bash(dir:*)",
|
||||||
|
"Bash(tree:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/dist
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
.vscode/*
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 디폴트 무시된 파일
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 에디터 기반 HTTP 클라이언트 요청
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="ASK" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="customHeaders">
|
||||||
|
<set>
|
||||||
|
<option value=""name"" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="uvloop" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8Inspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N803" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/responsive-image-canvas.iml" filepath="$PROJECT_DIR$/.idea/responsive-image-canvas.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/responsive-image-canvas.iml
generated
Normal file
12
.idea/responsive-image-canvas.iml
generated
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
254
README.md
Normal file
254
README.md
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
# Responsive Image Canvas
|
||||||
|
|
||||||
|
GPU 가속 이미지 왜곡 효과를 제공하는 React 컴포넌트 라이브러리입니다. Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 애니메이션을 구현합니다.
|
||||||
|
|
||||||
|
## 특징
|
||||||
|
|
||||||
|
- 🚀 GPU 가속 렌더링 (Three.js + WebGL)
|
||||||
|
- 🎨 최대 8개의 독립적인 왜곡 영역 지원
|
||||||
|
- ⚡ 60fps 실시간 애니메이션
|
||||||
|
- 🎯 정규화된 좌표계 (0.0 - 1.0)
|
||||||
|
- 🔧 TypeScript 완벽 지원
|
||||||
|
- 📦 ESM & CommonJS 모두 지원
|
||||||
|
|
||||||
|
## 설치
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install responsive-image-canvas
|
||||||
|
```
|
||||||
|
|
||||||
|
### Peer Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react react-dom three
|
||||||
|
```
|
||||||
|
|
||||||
|
## 기본 사용법
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ImageDistortion, DistortionArea } from '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 }, // 좌하단
|
||||||
|
],
|
||||||
|
movement: {
|
||||||
|
vectorA: { x: 0.1, y: 0.1 },
|
||||||
|
vectorB: { x: -0.1, y: -0.1 },
|
||||||
|
duration: 2.0,
|
||||||
|
easing: 'easeInOut',
|
||||||
|
},
|
||||||
|
distortionStrength: 0.5,
|
||||||
|
progress: 0,
|
||||||
|
dragVector: { x: 0, y: 0 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '800px', height: '600px' }}>
|
||||||
|
<ImageDistortion
|
||||||
|
imageSrc="/path/to/image.jpg"
|
||||||
|
areas={areas}
|
||||||
|
isPlaying={true}
|
||||||
|
/>
|
||||||
|
</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 (정규화된 좌표)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `DistortionMovement`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DistortionMovement {
|
||||||
|
vectorA: Point; // 시작 벡터
|
||||||
|
vectorB: Point; // 종료 벡터
|
||||||
|
duration: number; // 지속 시간 (초)
|
||||||
|
easing: EasingFunction; // 이징 함수
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `EasingFunction`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type EasingFunction =
|
||||||
|
| 'linear'
|
||||||
|
| 'easeIn'
|
||||||
|
| 'easeOut'
|
||||||
|
| 'easeInOut'
|
||||||
|
| 'easeInQuad'
|
||||||
|
| 'easeOutQuad';
|
||||||
|
```
|
||||||
|
|
||||||
|
## 고급 사용법
|
||||||
|
|
||||||
|
### 영역 동적 추가/제거
|
||||||
|
|
||||||
|
```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';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
publicDir: 'public',
|
||||||
|
// node_modules의 셰이더 파일을 복사
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
assetFileNames: 'assets/[name].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
셰이더 파일을 public 폴더로 복사:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp node_modules/responsive-image-canvas/dist/*.glsl public/shaders/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 성능 최적화
|
||||||
|
|
||||||
|
### 1. 영역 수 제한
|
||||||
|
최대 8개의 영역까지 지원하지만, 성능을 위해 4개 이하를 권장합니다.
|
||||||
|
|
||||||
|
### 2. 이미지 크기 최적화
|
||||||
|
큰 이미지는 성능에 영향을 줄 수 있습니다. 적절한 크기로 리사이징하세요.
|
||||||
|
|
||||||
|
### 3. 애니메이션 일시정지
|
||||||
|
필요하지 않을 때는 `isPlaying={false}`로 설정하세요.
|
||||||
|
|
||||||
|
## 제한사항
|
||||||
|
|
||||||
|
- WebGL을 지원하지 않는 브라우저에서는 동작하지 않습니다
|
||||||
|
- 모바일 환경에서는 성능이 제한될 수 있습니다
|
||||||
|
- 최대 8개의 왜곡 영역만 지원합니다
|
||||||
|
|
||||||
|
## 브라우저 지원
|
||||||
|
|
||||||
|
- Chrome 60+
|
||||||
|
- Firefox 60+
|
||||||
|
- Safari 12+
|
||||||
|
- Edge 79+
|
||||||
|
|
||||||
|
## 라이선스
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## 기여
|
||||||
|
|
||||||
|
이슈와 PR을 환영합니다!
|
||||||
|
|
||||||
|
## 관련 프로젝트
|
||||||
|
|
||||||
|
- [Three.js](https://threejs.org/)
|
||||||
|
- [React Three Fiber](https://github.com/pmndrs/react-three-fiber)
|
||||||
2125
package-lock.json
generated
Normal file
2125
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "responsive-image-canvas",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "React component for interactive image distortion with GPU-accelerated shaders",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||||
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"three": "^0.150.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"@types/react-dom": "^19.2.2",
|
||||||
|
"@types/three": "^0.181.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"three": "^0.181.0",
|
||||||
|
"tsup": "^8.5.0",
|
||||||
|
"typescript": "^5.5.3"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"react",
|
||||||
|
"three.js",
|
||||||
|
"webgl",
|
||||||
|
"shader",
|
||||||
|
"image-distortion",
|
||||||
|
"canvas",
|
||||||
|
"animation"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
180
src/components/ImageDistortion.tsx
Normal file
180
src/components/ImageDistortion.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { DistortionArea } from '../types';
|
||||||
|
import { ThreeScene } from '../engine/ThreeScene';
|
||||||
|
import { ShaderManager } from '../engine/ShaderManager';
|
||||||
|
import { AnimationLoop } from '../engine/AnimationLoop';
|
||||||
|
import { useAnimationFrame } from '../hooks/useAnimationFrame';
|
||||||
|
import { SHADER_CONFIG } from '../utils/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImageDistortion 컴포넌트 Props
|
||||||
|
*/
|
||||||
|
export interface ImageDistortionProps {
|
||||||
|
/** 이미지 소스 URL */
|
||||||
|
imageSrc: string;
|
||||||
|
/** 왜곡 영역 배열 */
|
||||||
|
areas: DistortionArea[];
|
||||||
|
/** 버텍스 셰이더 경로 (선택사항) */
|
||||||
|
vertexShaderPath?: string;
|
||||||
|
/** 프래그먼트 셰이더 경로 (선택사항) */
|
||||||
|
fragmentShaderPath?: string;
|
||||||
|
/** 애니메이션 재생 여부 */
|
||||||
|
isPlaying?: boolean;
|
||||||
|
/** 컨테이너 스타일 */
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
/** 컨테이너 클래스명 */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPU 가속 이미지 왜곡 컴포넌트
|
||||||
|
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
||||||
|
*/
|
||||||
|
export const ImageDistortion: React.FC<ImageDistortionProps> = ({
|
||||||
|
imageSrc,
|
||||||
|
areas,
|
||||||
|
vertexShaderPath,
|
||||||
|
fragmentShaderPath,
|
||||||
|
isPlaying = true,
|
||||||
|
style,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sceneRef = useRef<ThreeScene | null>(null);
|
||||||
|
const shaderManagerRef = useRef<ShaderManager>(new ShaderManager());
|
||||||
|
const textureRef = useRef<THREE.Texture | null>(null);
|
||||||
|
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const [currentAreas, setCurrentAreas] = useState<DistortionArea[]>(areas);
|
||||||
|
|
||||||
|
// 영역 변경 시 상태 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentAreas(areas);
|
||||||
|
}, [areas]);
|
||||||
|
|
||||||
|
// Three.js 씬 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const scene = new ThreeScene(containerRef.current);
|
||||||
|
sceneRef.current = scene;
|
||||||
|
|
||||||
|
// 셰이더 로드
|
||||||
|
const vertPath = vertexShaderPath || '/shaders/distortion.vert.glsl';
|
||||||
|
const fragPath = fragmentShaderPath || '/shaders/distortion.frag.glsl';
|
||||||
|
|
||||||
|
shaderManagerRef.current
|
||||||
|
.loadShaders(vertPath, fragPath)
|
||||||
|
.then(({ vertex, fragment }) => {
|
||||||
|
scene.setShaderMaterial(vertex, fragment);
|
||||||
|
setIsReady(true);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('셰이더 로드 실패:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scene.dispose();
|
||||||
|
if (textureRef.current) {
|
||||||
|
textureRef.current.dispose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [vertexShaderPath, fragmentShaderPath]);
|
||||||
|
|
||||||
|
// 이미지 텍스처 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (!imageSrc || !isReady) return;
|
||||||
|
|
||||||
|
const loader = new THREE.TextureLoader();
|
||||||
|
loader.load(
|
||||||
|
imageSrc,
|
||||||
|
(texture) => {
|
||||||
|
textureRef.current = texture;
|
||||||
|
if (sceneRef.current) {
|
||||||
|
sceneRef.current.updateUniforms({
|
||||||
|
u_texture: { value: texture },
|
||||||
|
});
|
||||||
|
sceneRef.current.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
(error) => {
|
||||||
|
console.error('이미지 로드 실패:', error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (textureRef.current) {
|
||||||
|
textureRef.current.dispose();
|
||||||
|
textureRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [imageSrc, isReady]);
|
||||||
|
|
||||||
|
// 셰이더 유니폼 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sceneRef.current || !isReady) return;
|
||||||
|
|
||||||
|
// 포인트 배열 생성
|
||||||
|
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
||||||
|
currentAreas.forEach((area, areaIndex) => {
|
||||||
|
area.basePoints.forEach((point, pointIndex) => {
|
||||||
|
const index = (areaIndex * 4 + pointIndex) * 2;
|
||||||
|
points[index] = point.x;
|
||||||
|
points[index + 1] = point.y;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 드래그 벡터 배열 생성
|
||||||
|
const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);
|
||||||
|
currentAreas.forEach((area, index) => {
|
||||||
|
const baseIndex = index * 2;
|
||||||
|
dragVectors[baseIndex] = area.dragVector.x;
|
||||||
|
dragVectors[baseIndex + 1] = area.dragVector.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 강도 배열 생성
|
||||||
|
const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);
|
||||||
|
currentAreas.forEach((area, index) => {
|
||||||
|
strengths[index] = area.distortionStrength;
|
||||||
|
});
|
||||||
|
|
||||||
|
sceneRef.current.updateUniforms({
|
||||||
|
u_numAreas: { value: currentAreas.length },
|
||||||
|
u_points: { value: points },
|
||||||
|
u_dragVectors: { value: dragVectors },
|
||||||
|
u_distortionStrengths: { value: strengths },
|
||||||
|
});
|
||||||
|
|
||||||
|
sceneRef.current.render();
|
||||||
|
}, [currentAreas, isReady]);
|
||||||
|
|
||||||
|
// 애니메이션 루프
|
||||||
|
const animationCallback = useCallback((deltaTime: number) => {
|
||||||
|
if (!isReady) return;
|
||||||
|
|
||||||
|
// 진행도 업데이트
|
||||||
|
const updatedAreas = AnimationLoop.updateProgress(currentAreas, deltaTime);
|
||||||
|
|
||||||
|
// 드래그 벡터 업데이트
|
||||||
|
const areasWithVectors = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||||
|
|
||||||
|
setCurrentAreas(areasWithVectors);
|
||||||
|
}, [currentAreas, isReady]);
|
||||||
|
|
||||||
|
useAnimationFrame(animationCallback, isPlaying);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
68
src/engine/AnimationLoop.ts
Normal file
68
src/engine/AnimationLoop.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { DistortionArea, Point } from '../types';
|
||||||
|
import { applyEasing } from '../utils/easing';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 애니메이션 루프 관리 클래스
|
||||||
|
*/
|
||||||
|
export class AnimationLoop {
|
||||||
|
/**
|
||||||
|
* 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트
|
||||||
|
* @param areas 왜곡 영역 배열
|
||||||
|
* @returns 업데이트된 영역 배열
|
||||||
|
*/
|
||||||
|
public static updateAreaDragVectors(
|
||||||
|
areas: DistortionArea[]
|
||||||
|
): DistortionArea[] {
|
||||||
|
return areas.map((area) => {
|
||||||
|
const { progress, movement } = area;
|
||||||
|
|
||||||
|
// 이징 적용
|
||||||
|
const easedProgress = applyEasing(progress, movement.easing);
|
||||||
|
|
||||||
|
// 벡터 간 보간
|
||||||
|
let dragVector: Point;
|
||||||
|
|
||||||
|
if (easedProgress < 0.5) {
|
||||||
|
// 0.0 -> 0.5: 0에서 vectorA로 보간
|
||||||
|
const t = easedProgress * 2;
|
||||||
|
dragVector = {
|
||||||
|
x: movement.vectorA.x * t,
|
||||||
|
y: movement.vectorA.y * t,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 0.5 -> 1.0: vectorA에서 0으로 보간
|
||||||
|
const t = (easedProgress - 0.5) * 2;
|
||||||
|
dragVector = {
|
||||||
|
x: movement.vectorA.x * (1 - t),
|
||||||
|
y: movement.vectorA.y * (1 - t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...area,
|
||||||
|
dragVector,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 영역의 진행도를 델타 타임만큼 업데이트
|
||||||
|
* @param areas 왜곡 영역 배열
|
||||||
|
* @param deltaTime 델타 타임 (초)
|
||||||
|
* @returns 업데이트된 영역 배열
|
||||||
|
*/
|
||||||
|
public static updateProgress(
|
||||||
|
areas: DistortionArea[],
|
||||||
|
deltaTime: number
|
||||||
|
): DistortionArea[] {
|
||||||
|
return areas.map((area) => {
|
||||||
|
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||||
|
newProgress %= 1.0; // 루프
|
||||||
|
|
||||||
|
return {
|
||||||
|
...area,
|
||||||
|
progress: newProgress,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/engine/ShaderManager.ts
Normal file
63
src/engine/ShaderManager.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 셰이더 파일 로딩 및 관리 클래스
|
||||||
|
*/
|
||||||
|
export class ShaderManager {
|
||||||
|
private vertexShaderSource: string | null = null;
|
||||||
|
private fragmentShaderSource: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셰이더 파일들을 비동기로 로드
|
||||||
|
* @param vertexPath 버텍스 셰이더 파일 경로
|
||||||
|
* @param fragmentPath 프래그먼트 셰이더 파일 경로
|
||||||
|
* @returns 로드된 셰이더 소스 코드
|
||||||
|
*/
|
||||||
|
public async loadShaders(
|
||||||
|
vertexPath: string,
|
||||||
|
fragmentPath: string
|
||||||
|
): Promise<{ vertex: string; fragment: string }> {
|
||||||
|
try {
|
||||||
|
const [vertexResponse, fragmentResponse] = await Promise.all([
|
||||||
|
fetch(vertexPath),
|
||||||
|
fetch(fragmentPath),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!vertexResponse.ok) {
|
||||||
|
throw new Error(`버텍스 셰이더 로드 실패: ${vertexResponse.statusText}`);
|
||||||
|
}
|
||||||
|
if (!fragmentResponse.ok) {
|
||||||
|
throw new Error(`프래그먼트 셰이더 로드 실패: ${fragmentResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.vertexShaderSource = await vertexResponse.text();
|
||||||
|
this.fragmentShaderSource = await fragmentResponse.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
vertex: this.vertexShaderSource,
|
||||||
|
fragment: this.fragmentShaderSource,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('셰이더 로드 실패:', error);
|
||||||
|
throw new Error('셰이더 로딩에 실패했습니다');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 버텍스 셰이더 소스 코드 반환
|
||||||
|
*/
|
||||||
|
public getVertexShader(): string {
|
||||||
|
if (!this.vertexShaderSource) {
|
||||||
|
throw new Error('버텍스 셰이더가 로드되지 않았습니다');
|
||||||
|
}
|
||||||
|
return this.vertexShaderSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프래그먼트 셰이더 소스 코드 반환
|
||||||
|
*/
|
||||||
|
public getFragmentShader(): string {
|
||||||
|
if (!this.fragmentShaderSource) {
|
||||||
|
throw new Error('프래그먼트 셰이더가 로드되지 않았습니다');
|
||||||
|
}
|
||||||
|
return this.fragmentShaderSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/engine/ThreeScene.ts
Normal file
111
src/engine/ThreeScene.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
import { ShaderUniforms } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Three.js 씬 관리 클래스
|
||||||
|
*/
|
||||||
|
export class ThreeScene {
|
||||||
|
private scene: THREE.Scene;
|
||||||
|
private camera: THREE.OrthographicCamera;
|
||||||
|
private renderer: THREE.WebGLRenderer;
|
||||||
|
private mesh: THREE.Mesh | null = null;
|
||||||
|
private uniforms: ShaderUniforms;
|
||||||
|
|
||||||
|
constructor(private container: HTMLElement) {
|
||||||
|
// 씬 생성
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
|
||||||
|
// 2D용 직교 카메라 설정
|
||||||
|
this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||||
|
|
||||||
|
// 렌더러 설정
|
||||||
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: true,
|
||||||
|
alpha: false,
|
||||||
|
});
|
||||||
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
this.container.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
// 유니폼 초기화
|
||||||
|
this.uniforms = {
|
||||||
|
u_resolution: { value: new THREE.Vector2() },
|
||||||
|
u_texture: { value: null },
|
||||||
|
u_points: { value: new Float32Array(64) }, // 32포인트 × 2(x,y)
|
||||||
|
u_numAreas: { value: 0 },
|
||||||
|
u_dragVectors: { value: new Float32Array(16) }, // 8벡터 × 2(x,y)
|
||||||
|
u_distortionStrengths: { value: new Float32Array(8) },
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleResize();
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 윈도우 리사이즈 핸들러
|
||||||
|
*/
|
||||||
|
private handleResize = () => {
|
||||||
|
const width = this.container.clientWidth;
|
||||||
|
const height = this.container.clientHeight;
|
||||||
|
|
||||||
|
this.renderer.setSize(width, height);
|
||||||
|
this.uniforms.u_resolution.value.set(width, height);
|
||||||
|
|
||||||
|
if (this.mesh) {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셰이더 머티리얼 설정
|
||||||
|
* @param vertexShader 버텍스 셰이더 소스
|
||||||
|
* @param fragmentShader 프래그먼트 셰이더 소스
|
||||||
|
*/
|
||||||
|
public setShaderMaterial(vertexShader: string, fragmentShader: string) {
|
||||||
|
const geometry = new THREE.PlaneGeometry(2, 2);
|
||||||
|
const material = new THREE.ShaderMaterial({
|
||||||
|
uniforms: this.uniforms,
|
||||||
|
vertexShader,
|
||||||
|
fragmentShader,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.mesh) {
|
||||||
|
this.scene.remove(this.mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mesh = new THREE.Mesh(geometry, material);
|
||||||
|
this.scene.add(this.mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유니폼 값 업데이트
|
||||||
|
* @param updates 업데이트할 유니폼 값들
|
||||||
|
*/
|
||||||
|
public updateUniforms(updates: Partial<ShaderUniforms>) {
|
||||||
|
Object.keys(updates).forEach((key) => {
|
||||||
|
const uniformKey = key as keyof ShaderUniforms;
|
||||||
|
this.uniforms[uniformKey].value = updates[uniformKey]!.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 씬 렌더링
|
||||||
|
*/
|
||||||
|
public render() {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리소스 정리
|
||||||
|
*/
|
||||||
|
public dispose() {
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
this.renderer.dispose();
|
||||||
|
if (this.mesh) {
|
||||||
|
this.mesh.geometry.dispose();
|
||||||
|
(this.mesh.material as THREE.Material).dispose();
|
||||||
|
}
|
||||||
|
if (this.container.contains(this.renderer.domElement)) {
|
||||||
|
this.container.removeChild(this.renderer.domElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/hooks/useAnimationFrame.ts
Normal file
35
src/hooks/useAnimationFrame.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* requestAnimationFrame을 사용한 애니메이션 루프 훅
|
||||||
|
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
||||||
|
* @param isPlaying 애니메이션 재생 여부
|
||||||
|
*/
|
||||||
|
export const useAnimationFrame = (
|
||||||
|
callback: (deltaTime: number) => void,
|
||||||
|
isPlaying: boolean = true
|
||||||
|
) => {
|
||||||
|
const requestRef = useRef<number | undefined>(undefined);
|
||||||
|
const previousTimeRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPlaying) return;
|
||||||
|
|
||||||
|
const animate = (time: number) => {
|
||||||
|
if (previousTimeRef.current !== undefined) {
|
||||||
|
const deltaTime = (time - previousTimeRef.current) / 1000; // 밀리초를 초로 변환
|
||||||
|
callback(deltaTime);
|
||||||
|
}
|
||||||
|
previousTimeRef.current = time;
|
||||||
|
requestRef.current = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
requestRef.current = requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (requestRef.current) {
|
||||||
|
cancelAnimationFrame(requestRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [callback, isPlaying]);
|
||||||
|
};
|
||||||
28
src/index.ts
Normal file
28
src/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 메인 컴포넌트
|
||||||
|
export { ImageDistortion } from './components/ImageDistortion';
|
||||||
|
export type { ImageDistortionProps } from './components/ImageDistortion';
|
||||||
|
|
||||||
|
// 타입 정의
|
||||||
|
export type {
|
||||||
|
Point,
|
||||||
|
EasingFunction,
|
||||||
|
DistortionMovement,
|
||||||
|
DistortionArea,
|
||||||
|
AreaBounds,
|
||||||
|
ShaderUniforms,
|
||||||
|
ShaderConfig,
|
||||||
|
AnimationState,
|
||||||
|
AnimationTicker,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// 유틸리티 함수
|
||||||
|
export { applyEasing } from './utils/easing';
|
||||||
|
export { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants';
|
||||||
|
|
||||||
|
// 엔진 클래스 (고급 사용자용)
|
||||||
|
export { ThreeScene } from './engine/ThreeScene';
|
||||||
|
export { ShaderManager } from './engine/ShaderManager';
|
||||||
|
export { AnimationLoop } from './engine/AnimationLoop';
|
||||||
|
|
||||||
|
// 훅
|
||||||
|
export { useAnimationFrame } from './hooks/useAnimationFrame';
|
||||||
88
src/shaders/distortion.frag.glsl
Normal file
88
src/shaders/distortion.frag.glsl
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform vec2 u_points[32]; // 최대 8영역 × 4포인트
|
||||||
|
uniform int u_numAreas;
|
||||||
|
uniform vec2 u_dragVectors[8];
|
||||||
|
uniform float u_distortionStrengths[8];
|
||||||
|
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
// 사각형 내부의 포인트에 대한 UV 좌표 계산
|
||||||
|
vec2 computeUV(vec2 xy, vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
|
||||||
|
// 경계 상자 체크
|
||||||
|
vec2 minP = min(min(p0, p1), min(p2, p3));
|
||||||
|
vec2 maxP = max(max(p0, p1), max(p2, p3));
|
||||||
|
|
||||||
|
if (xy.x < minP.x || xy.x > maxP.x || xy.y < minP.y || xy.y > maxP.y) {
|
||||||
|
return vec2(-1.0, -1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기 추정값 (정규화된 좌표)
|
||||||
|
vec2 rectSize = maxP - minP;
|
||||||
|
vec2 rectUV = (xy - minP) / rectSize;
|
||||||
|
float u0 = rectUV.x;
|
||||||
|
float v0 = rectUV.y;
|
||||||
|
|
||||||
|
// Newton-Raphson 반복법으로 정확한 UV 계산
|
||||||
|
for (int iter = 0; iter < 3; iter++) {
|
||||||
|
vec2 xy0 = mix(mix(p0, p1, u0), mix(p3, p2, u0), v0);
|
||||||
|
vec2 du_vec = mix(p1 - p0, p2 - p3, v0);
|
||||||
|
vec2 dv_vec = mix(p3 - p0, p2 - p1, u0);
|
||||||
|
|
||||||
|
vec2 dxy = xy - xy0;
|
||||||
|
float det = du_vec.x * dv_vec.y - du_vec.y * dv_vec.x;
|
||||||
|
|
||||||
|
if (abs(det) < 1e-6) break;
|
||||||
|
|
||||||
|
float du = (dv_vec.y * dxy.x - dv_vec.x * dxy.y) / det;
|
||||||
|
float dv = (-du_vec.y * dxy.x + du_vec.x * dxy.y) / det;
|
||||||
|
|
||||||
|
u0 += du;
|
||||||
|
v0 += dv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 포인트가 내부에 있는지 확인
|
||||||
|
if (u0 >= 0.0 && u0 <= 1.0 && v0 >= 0.0 && v0 <= 1.0) {
|
||||||
|
return vec2(u0, v0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec2(-1.0, -1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = vUv;
|
||||||
|
vec2 pixelCoord = vUv * u_resolution;
|
||||||
|
|
||||||
|
// 모든 영역의 왜곡 적용
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
if (i >= u_numAreas) break;
|
||||||
|
|
||||||
|
int baseIndex = i * 4;
|
||||||
|
vec2 p0 = u_points[baseIndex + 0] * u_resolution;
|
||||||
|
vec2 p1 = u_points[baseIndex + 1] * u_resolution;
|
||||||
|
vec2 p2 = u_points[baseIndex + 2] * u_resolution;
|
||||||
|
vec2 p3 = u_points[baseIndex + 3] * u_resolution;
|
||||||
|
|
||||||
|
vec2 areaUV = computeUV(pixelCoord, p0, p1, p2, p3);
|
||||||
|
|
||||||
|
if (areaUV.x >= 0.0) {
|
||||||
|
// 이 영역 내부에 포인트가 있음
|
||||||
|
vec2 center = vec2(0.5, 0.5);
|
||||||
|
float distToCenter = length(areaUV - center);
|
||||||
|
float maxUvRadius = 0.707; // sqrt(0.5^2 + 0.5^2)
|
||||||
|
|
||||||
|
// 부드러운 감쇠
|
||||||
|
float influence = 1.0 - smoothstep(0.0, maxUvRadius, distToCenter);
|
||||||
|
|
||||||
|
// 왜곡 적용
|
||||||
|
vec2 distortion = (u_dragVectors[i] / u_resolution) * influence * u_distortionStrengths[i];
|
||||||
|
uv += distortion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 텍스처 외부 샘플링 방지를 위한 클램핑
|
||||||
|
uv = clamp(uv, 0.0, 1.0);
|
||||||
|
|
||||||
|
// 텍스처 샘플링
|
||||||
|
gl_FragColor = texture2D(u_texture, uv);
|
||||||
|
}
|
||||||
6
src/shaders/distortion.vert.glsl
Normal file
6
src/shaders/distortion.vert.glsl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
27
src/types/animation.ts
Normal file
27
src/types/animation.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 애니메이션 상태
|
||||||
|
*/
|
||||||
|
export interface AnimationState {
|
||||||
|
/** 재생 중 여부 */
|
||||||
|
isPlaying: boolean;
|
||||||
|
/** 현재 시간 (초) */
|
||||||
|
currentTime: number;
|
||||||
|
/** 델타 타임 (프레임 간 시간 차이, 초) */
|
||||||
|
deltaTime: number;
|
||||||
|
/** 현재 FPS */
|
||||||
|
fps: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 애니메이션 틱 컨트롤러
|
||||||
|
*/
|
||||||
|
export interface AnimationTicker {
|
||||||
|
/** 애니메이션 시작 */
|
||||||
|
start: () => void;
|
||||||
|
/** 애니메이션 정지 */
|
||||||
|
stop: () => void;
|
||||||
|
/** 애니메이션 일시정지 */
|
||||||
|
pause: () => void;
|
||||||
|
/** 애니메이션 재개 */
|
||||||
|
resume: () => void;
|
||||||
|
}
|
||||||
60
src/types/area.ts
Normal file
60
src/types/area.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 정규화된 좌표계의 2D 포인트 (0.0 - 1.0)
|
||||||
|
*/
|
||||||
|
export interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 애니메이션 이징 함수 타입
|
||||||
|
*/
|
||||||
|
export type EasingFunction =
|
||||||
|
| 'linear'
|
||||||
|
| 'easeIn'
|
||||||
|
| 'easeOut'
|
||||||
|
| 'easeInOut'
|
||||||
|
| 'easeInQuad'
|
||||||
|
| 'easeOutQuad';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 왜곡 애니메이션 움직임 설정
|
||||||
|
*/
|
||||||
|
export interface DistortionMovement {
|
||||||
|
/** 왜곡 시작 벡터 */
|
||||||
|
vectorA: Point;
|
||||||
|
/** 왜곡 종료 벡터 */
|
||||||
|
vectorB: Point;
|
||||||
|
/** 애니메이션 지속 시간 (초) */
|
||||||
|
duration: number;
|
||||||
|
/** 적용할 이징 함수 */
|
||||||
|
easing: EasingFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사각형 포인트와 애니메이션 설정을 포함하는 왜곡 영역
|
||||||
|
*/
|
||||||
|
export interface DistortionArea {
|
||||||
|
/** 고유 식별자 */
|
||||||
|
id: string;
|
||||||
|
/** 사각형의 네 모서리 포인트 [topLeft, topRight, bottomRight, bottomLeft] */
|
||||||
|
basePoints: [Point, Point, Point, Point];
|
||||||
|
/** 움직임 애니메이션 설정 */
|
||||||
|
movement: DistortionMovement;
|
||||||
|
/** 왜곡 강도 (0.0 - 1.0) */
|
||||||
|
distortionStrength: number;
|
||||||
|
/** 현재 애니메이션 진행도 (0.0 - 1.0) */
|
||||||
|
progress: number;
|
||||||
|
/** 현재 드래그 벡터 (progress로부터 계산됨) */
|
||||||
|
dragVector: Point;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 영역 충돌 감지를 위한 경계 상자
|
||||||
|
*/
|
||||||
|
export interface AreaBounds {
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
3
src/types/index.ts
Normal file
3
src/types/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './area';
|
||||||
|
export * from './shader';
|
||||||
|
export * from './animation';
|
||||||
30
src/types/shader.ts
Normal file
30
src/types/shader.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셰이더 유니폼 변수 타입
|
||||||
|
*/
|
||||||
|
export interface ShaderUniforms {
|
||||||
|
[uniform: string]: THREE.IUniform<any>;
|
||||||
|
/** 화면 해상도 */
|
||||||
|
u_resolution: THREE.IUniform<THREE.Vector2>;
|
||||||
|
/** 이미지 텍스처 */
|
||||||
|
u_texture: THREE.IUniform<THREE.Texture | null>;
|
||||||
|
/** 모든 영역의 포인트 배열 (최대 32개 포인트 = 8영역 × 4포인트) */
|
||||||
|
u_points: THREE.IUniform<Float32Array>;
|
||||||
|
/** 활성 영역 개수 */
|
||||||
|
u_numAreas: THREE.IUniform<number>;
|
||||||
|
/** 각 영역의 드래그 벡터 배열 */
|
||||||
|
u_dragVectors: THREE.IUniform<Float32Array>;
|
||||||
|
/** 각 영역의 왜곡 강도 배열 */
|
||||||
|
u_distortionStrengths: THREE.IUniform<Float32Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 셰이더 설정
|
||||||
|
*/
|
||||||
|
export interface ShaderConfig {
|
||||||
|
/** 최대 영역 개수 */
|
||||||
|
maxAreas: number;
|
||||||
|
/** 최대 포인트 개수 (maxAreas × 4) */
|
||||||
|
maxPoints: number;
|
||||||
|
}
|
||||||
39
src/utils/constants.ts
Normal file
39
src/utils/constants.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 셰이더 관련 설정
|
||||||
|
*/
|
||||||
|
export const SHADER_CONFIG = {
|
||||||
|
/** 최대 영역 개수 */
|
||||||
|
MAX_AREAS: 8,
|
||||||
|
/** 최대 포인트 개수 (8영역 × 4포인트) */
|
||||||
|
MAX_POINTS: 32,
|
||||||
|
/** 최대 드래그 벡터 개수 */
|
||||||
|
MAX_DRAG_VECTORS: 8,
|
||||||
|
/** 최대 강도 배열 크기 */
|
||||||
|
MAX_STRENGTHS: 8,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 애니메이션 관련 설정
|
||||||
|
*/
|
||||||
|
export const ANIMATION_CONFIG = {
|
||||||
|
/** 목표 FPS */
|
||||||
|
TARGET_FPS: 60,
|
||||||
|
/** 델타 타임 (약 16.67ms) */
|
||||||
|
DELTA_TIME: 1 / 60,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 영역 설정값
|
||||||
|
*/
|
||||||
|
export const DEFAULT_AREA = {
|
||||||
|
/** 기본 왜곡 강도 */
|
||||||
|
DISTORTION_STRENGTH: 0.5,
|
||||||
|
/** 기본 애니메이션 지속 시간 (초) */
|
||||||
|
DURATION: 2.0,
|
||||||
|
/** 기본 이징 함수 */
|
||||||
|
EASING: 'easeInOut' as const,
|
||||||
|
/** 기본 벡터 A */
|
||||||
|
VECTOR_A: { x: 0.1, y: 0.1 },
|
||||||
|
/** 기본 벡터 B */
|
||||||
|
VECTOR_B: { x: -0.1, y: -0.1 },
|
||||||
|
} as const;
|
||||||
31
src/utils/easing.ts
Normal file
31
src/utils/easing.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { EasingFunction } from '../types';
|
||||||
|
|
||||||
|
type EasingFunc = (t: number) => number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이징 함수 구현 맵
|
||||||
|
*/
|
||||||
|
const easingFunctions: Record<EasingFunction, EasingFunc> = {
|
||||||
|
linear: (t) => t,
|
||||||
|
|
||||||
|
easeIn: (t) => t * t,
|
||||||
|
easeOut: (t) => t * (2 - t),
|
||||||
|
easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
|
||||||
|
|
||||||
|
easeInQuad: (t) => t * t,
|
||||||
|
easeOutQuad: (t) => t * (2 - t),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 진행도에 이징 함수를 적용
|
||||||
|
* @param progress 진행도 (0.0 - 1.0)
|
||||||
|
* @param easingType 적용할 이징 함수 타입
|
||||||
|
* @returns 이징이 적용된 진행도 (0.0 - 1.0)
|
||||||
|
*/
|
||||||
|
export const applyEasing = (
|
||||||
|
progress: number,
|
||||||
|
easingType: EasingFunction
|
||||||
|
): number => {
|
||||||
|
const clampedProgress = Math.max(0, Math.min(1, progress));
|
||||||
|
return easingFunctions[easingType](clampedProgress);
|
||||||
|
};
|
||||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
14
tsup.config.ts
Normal file
14
tsup.config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['cjs', 'esm'],
|
||||||
|
dts: true,
|
||||||
|
splitting: false,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
external: ['react', 'react-dom', 'three'],
|
||||||
|
// 셰이더 파일을 dist로 복사
|
||||||
|
publicDir: 'src/shaders',
|
||||||
|
outDir: 'dist',
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user