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