init dist
This commit is contained in:
parent
808ddd99ec
commit
6ddae08d86
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
/tmp
|
||||
/out-tsc
|
||||
/dist
|
||||
|
||||
/node_modules
|
||||
npm-debug.log*
|
||||
|
||||
88
dist/distortion.frag.glsl
vendored
Normal file
88
dist/distortion.frag.glsl
vendored
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
dist/distortion.vert.glsl
vendored
Normal file
6
dist/distortion.vert.glsl
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
275
dist/index.d.mts
vendored
Normal file
275
dist/index.d.mts
vendored
Normal file
@ -0,0 +1,275 @@
|
||||
import React from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
/**
|
||||
* 정규화된 좌표계의 2D 포인트 (0.0 - 1.0)
|
||||
*/
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
/**
|
||||
* 애니메이션 이징 함수 타입
|
||||
*/
|
||||
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad';
|
||||
/**
|
||||
* 왜곡 애니메이션 움직임 설정
|
||||
*/
|
||||
interface DistortionMovement {
|
||||
/** 왜곡 시작 벡터 */
|
||||
vectorA: Point;
|
||||
/** 왜곡 종료 벡터 */
|
||||
vectorB: Point;
|
||||
/** 애니메이션 지속 시간 (초) */
|
||||
duration: number;
|
||||
/** 적용할 이징 함수 */
|
||||
easing: EasingFunction;
|
||||
}
|
||||
/**
|
||||
* 사각형 포인트와 애니메이션 설정을 포함하는 왜곡 영역
|
||||
*/
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* 영역 충돌 감지를 위한 경계 상자
|
||||
*/
|
||||
interface AreaBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 셰이더 유니폼 변수 타입
|
||||
*/
|
||||
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>;
|
||||
}
|
||||
/**
|
||||
* 셰이더 설정
|
||||
*/
|
||||
interface ShaderConfig {
|
||||
/** 최대 영역 개수 */
|
||||
maxAreas: number;
|
||||
/** 최대 포인트 개수 (maxAreas × 4) */
|
||||
maxPoints: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 애니메이션 상태
|
||||
*/
|
||||
interface AnimationState {
|
||||
/** 재생 중 여부 */
|
||||
isPlaying: boolean;
|
||||
/** 현재 시간 (초) */
|
||||
currentTime: number;
|
||||
/** 델타 타임 (프레임 간 시간 차이, 초) */
|
||||
deltaTime: number;
|
||||
/** 현재 FPS */
|
||||
fps: number;
|
||||
}
|
||||
/**
|
||||
* 애니메이션 틱 컨트롤러
|
||||
*/
|
||||
interface AnimationTicker {
|
||||
/** 애니메이션 시작 */
|
||||
start: () => void;
|
||||
/** 애니메이션 정지 */
|
||||
stop: () => void;
|
||||
/** 애니메이션 일시정지 */
|
||||
pause: () => void;
|
||||
/** 애니메이션 재개 */
|
||||
resume: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageDistortion 컴포넌트 Props
|
||||
*/
|
||||
interface ImageDistortionProps {
|
||||
/** 이미지 소스 URL */
|
||||
imageSrc: string;
|
||||
/** 왜곡 영역 배열 */
|
||||
areas: DistortionArea[];
|
||||
/** 버텍스 셰이더 경로 (선택사항) */
|
||||
vertexShaderPath?: string;
|
||||
/** 프래그먼트 셰이더 경로 (선택사항) */
|
||||
fragmentShaderPath?: string;
|
||||
/** 애니메이션 재생 여부 */
|
||||
isPlaying?: boolean;
|
||||
/** 컨테이너 스타일 */
|
||||
style?: React.CSSProperties;
|
||||
/** 컨테이너 클래스명 */
|
||||
className?: string;
|
||||
}
|
||||
/**
|
||||
* GPU 가속 이미지 왜곡 컴포넌트
|
||||
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
||||
*/
|
||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
||||
|
||||
/**
|
||||
* 진행도에 이징 함수를 적용
|
||||
* @param progress 진행도 (0.0 - 1.0)
|
||||
* @param easingType 적용할 이징 함수 타입
|
||||
* @returns 이징이 적용된 진행도 (0.0 - 1.0)
|
||||
*/
|
||||
declare const applyEasing: (progress: number, easingType: EasingFunction) => number;
|
||||
|
||||
/**
|
||||
* 셰이더 관련 설정
|
||||
*/
|
||||
declare const SHADER_CONFIG: {
|
||||
/** 최대 영역 개수 */
|
||||
readonly MAX_AREAS: 8;
|
||||
/** 최대 포인트 개수 (8영역 × 4포인트) */
|
||||
readonly MAX_POINTS: 32;
|
||||
/** 최대 드래그 벡터 개수 */
|
||||
readonly MAX_DRAG_VECTORS: 8;
|
||||
/** 최대 강도 배열 크기 */
|
||||
readonly MAX_STRENGTHS: 8;
|
||||
};
|
||||
/**
|
||||
* 애니메이션 관련 설정
|
||||
*/
|
||||
declare const ANIMATION_CONFIG: {
|
||||
/** 목표 FPS */
|
||||
readonly TARGET_FPS: 60;
|
||||
/** 델타 타임 (약 16.67ms) */
|
||||
readonly DELTA_TIME: number;
|
||||
};
|
||||
/**
|
||||
* 기본 영역 설정값
|
||||
*/
|
||||
declare const DEFAULT_AREA: {
|
||||
/** 기본 왜곡 강도 */
|
||||
readonly DISTORTION_STRENGTH: 0.5;
|
||||
/** 기본 애니메이션 지속 시간 (초) */
|
||||
readonly DURATION: 2;
|
||||
/** 기본 이징 함수 */
|
||||
readonly EASING: "easeInOut";
|
||||
/** 기본 벡터 A */
|
||||
readonly VECTOR_A: {
|
||||
readonly x: 0.1;
|
||||
readonly y: 0.1;
|
||||
};
|
||||
/** 기본 벡터 B */
|
||||
readonly VECTOR_B: {
|
||||
readonly x: -0.1;
|
||||
readonly y: -0.1;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Three.js 씬 관리 클래스
|
||||
*/
|
||||
declare class ThreeScene {
|
||||
private container;
|
||||
private scene;
|
||||
private camera;
|
||||
private renderer;
|
||||
private mesh;
|
||||
private uniforms;
|
||||
constructor(container: HTMLElement);
|
||||
/**
|
||||
* 윈도우 리사이즈 핸들러
|
||||
*/
|
||||
private handleResize;
|
||||
/**
|
||||
* 셰이더 머티리얼 설정
|
||||
* @param vertexShader 버텍스 셰이더 소스
|
||||
* @param fragmentShader 프래그먼트 셰이더 소스
|
||||
*/
|
||||
setShaderMaterial(vertexShader: string, fragmentShader: string): void;
|
||||
/**
|
||||
* 유니폼 값 업데이트
|
||||
* @param updates 업데이트할 유니폼 값들
|
||||
*/
|
||||
updateUniforms(updates: Partial<ShaderUniforms>): void;
|
||||
/**
|
||||
* 씬 렌더링
|
||||
*/
|
||||
render(): void;
|
||||
/**
|
||||
* 리소스 정리
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 셰이더 파일 로딩 및 관리 클래스
|
||||
*/
|
||||
declare class ShaderManager {
|
||||
private vertexShaderSource;
|
||||
private fragmentShaderSource;
|
||||
/**
|
||||
* 셰이더 파일들을 비동기로 로드
|
||||
* @param vertexPath 버텍스 셰이더 파일 경로
|
||||
* @param fragmentPath 프래그먼트 셰이더 파일 경로
|
||||
* @returns 로드된 셰이더 소스 코드
|
||||
*/
|
||||
loadShaders(vertexPath: string, fragmentPath: string): Promise<{
|
||||
vertex: string;
|
||||
fragment: string;
|
||||
}>;
|
||||
/**
|
||||
* 버텍스 셰이더 소스 코드 반환
|
||||
*/
|
||||
getVertexShader(): string;
|
||||
/**
|
||||
* 프래그먼트 셰이더 소스 코드 반환
|
||||
*/
|
||||
getFragmentShader(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 애니메이션 루프 관리 클래스
|
||||
*/
|
||||
declare class AnimationLoop {
|
||||
/**
|
||||
* 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트
|
||||
* @param areas 왜곡 영역 배열
|
||||
* @returns 업데이트된 영역 배열
|
||||
*/
|
||||
static updateAreaDragVectors(areas: DistortionArea[]): DistortionArea[];
|
||||
/**
|
||||
* 모든 영역의 진행도를 델타 타임만큼 업데이트
|
||||
* @param areas 왜곡 영역 배열
|
||||
* @param deltaTime 델타 타임 (초)
|
||||
* @returns 업데이트된 영역 배열
|
||||
*/
|
||||
static updateProgress(areas: DistortionArea[], deltaTime: number): DistortionArea[];
|
||||
}
|
||||
|
||||
/**
|
||||
* requestAnimationFrame을 사용한 애니메이션 루프 훅
|
||||
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
||||
* @param isPlaying 애니메이션 재생 여부
|
||||
*/
|
||||
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
||||
|
||||
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, type DistortionMovement, type EasingFunction, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame };
|
||||
275
dist/index.d.ts
vendored
Normal file
275
dist/index.d.ts
vendored
Normal file
@ -0,0 +1,275 @@
|
||||
import React from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
/**
|
||||
* 정규화된 좌표계의 2D 포인트 (0.0 - 1.0)
|
||||
*/
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
/**
|
||||
* 애니메이션 이징 함수 타입
|
||||
*/
|
||||
type EasingFunction = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'easeInQuad' | 'easeOutQuad';
|
||||
/**
|
||||
* 왜곡 애니메이션 움직임 설정
|
||||
*/
|
||||
interface DistortionMovement {
|
||||
/** 왜곡 시작 벡터 */
|
||||
vectorA: Point;
|
||||
/** 왜곡 종료 벡터 */
|
||||
vectorB: Point;
|
||||
/** 애니메이션 지속 시간 (초) */
|
||||
duration: number;
|
||||
/** 적용할 이징 함수 */
|
||||
easing: EasingFunction;
|
||||
}
|
||||
/**
|
||||
* 사각형 포인트와 애니메이션 설정을 포함하는 왜곡 영역
|
||||
*/
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* 영역 충돌 감지를 위한 경계 상자
|
||||
*/
|
||||
interface AreaBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 셰이더 유니폼 변수 타입
|
||||
*/
|
||||
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>;
|
||||
}
|
||||
/**
|
||||
* 셰이더 설정
|
||||
*/
|
||||
interface ShaderConfig {
|
||||
/** 최대 영역 개수 */
|
||||
maxAreas: number;
|
||||
/** 최대 포인트 개수 (maxAreas × 4) */
|
||||
maxPoints: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 애니메이션 상태
|
||||
*/
|
||||
interface AnimationState {
|
||||
/** 재생 중 여부 */
|
||||
isPlaying: boolean;
|
||||
/** 현재 시간 (초) */
|
||||
currentTime: number;
|
||||
/** 델타 타임 (프레임 간 시간 차이, 초) */
|
||||
deltaTime: number;
|
||||
/** 현재 FPS */
|
||||
fps: number;
|
||||
}
|
||||
/**
|
||||
* 애니메이션 틱 컨트롤러
|
||||
*/
|
||||
interface AnimationTicker {
|
||||
/** 애니메이션 시작 */
|
||||
start: () => void;
|
||||
/** 애니메이션 정지 */
|
||||
stop: () => void;
|
||||
/** 애니메이션 일시정지 */
|
||||
pause: () => void;
|
||||
/** 애니메이션 재개 */
|
||||
resume: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageDistortion 컴포넌트 Props
|
||||
*/
|
||||
interface ImageDistortionProps {
|
||||
/** 이미지 소스 URL */
|
||||
imageSrc: string;
|
||||
/** 왜곡 영역 배열 */
|
||||
areas: DistortionArea[];
|
||||
/** 버텍스 셰이더 경로 (선택사항) */
|
||||
vertexShaderPath?: string;
|
||||
/** 프래그먼트 셰이더 경로 (선택사항) */
|
||||
fragmentShaderPath?: string;
|
||||
/** 애니메이션 재생 여부 */
|
||||
isPlaying?: boolean;
|
||||
/** 컨테이너 스타일 */
|
||||
style?: React.CSSProperties;
|
||||
/** 컨테이너 클래스명 */
|
||||
className?: string;
|
||||
}
|
||||
/**
|
||||
* GPU 가속 이미지 왜곡 컴포넌트
|
||||
* Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.
|
||||
*/
|
||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
||||
|
||||
/**
|
||||
* 진행도에 이징 함수를 적용
|
||||
* @param progress 진행도 (0.0 - 1.0)
|
||||
* @param easingType 적용할 이징 함수 타입
|
||||
* @returns 이징이 적용된 진행도 (0.0 - 1.0)
|
||||
*/
|
||||
declare const applyEasing: (progress: number, easingType: EasingFunction) => number;
|
||||
|
||||
/**
|
||||
* 셰이더 관련 설정
|
||||
*/
|
||||
declare const SHADER_CONFIG: {
|
||||
/** 최대 영역 개수 */
|
||||
readonly MAX_AREAS: 8;
|
||||
/** 최대 포인트 개수 (8영역 × 4포인트) */
|
||||
readonly MAX_POINTS: 32;
|
||||
/** 최대 드래그 벡터 개수 */
|
||||
readonly MAX_DRAG_VECTORS: 8;
|
||||
/** 최대 강도 배열 크기 */
|
||||
readonly MAX_STRENGTHS: 8;
|
||||
};
|
||||
/**
|
||||
* 애니메이션 관련 설정
|
||||
*/
|
||||
declare const ANIMATION_CONFIG: {
|
||||
/** 목표 FPS */
|
||||
readonly TARGET_FPS: 60;
|
||||
/** 델타 타임 (약 16.67ms) */
|
||||
readonly DELTA_TIME: number;
|
||||
};
|
||||
/**
|
||||
* 기본 영역 설정값
|
||||
*/
|
||||
declare const DEFAULT_AREA: {
|
||||
/** 기본 왜곡 강도 */
|
||||
readonly DISTORTION_STRENGTH: 0.5;
|
||||
/** 기본 애니메이션 지속 시간 (초) */
|
||||
readonly DURATION: 2;
|
||||
/** 기본 이징 함수 */
|
||||
readonly EASING: "easeInOut";
|
||||
/** 기본 벡터 A */
|
||||
readonly VECTOR_A: {
|
||||
readonly x: 0.1;
|
||||
readonly y: 0.1;
|
||||
};
|
||||
/** 기본 벡터 B */
|
||||
readonly VECTOR_B: {
|
||||
readonly x: -0.1;
|
||||
readonly y: -0.1;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Three.js 씬 관리 클래스
|
||||
*/
|
||||
declare class ThreeScene {
|
||||
private container;
|
||||
private scene;
|
||||
private camera;
|
||||
private renderer;
|
||||
private mesh;
|
||||
private uniforms;
|
||||
constructor(container: HTMLElement);
|
||||
/**
|
||||
* 윈도우 리사이즈 핸들러
|
||||
*/
|
||||
private handleResize;
|
||||
/**
|
||||
* 셰이더 머티리얼 설정
|
||||
* @param vertexShader 버텍스 셰이더 소스
|
||||
* @param fragmentShader 프래그먼트 셰이더 소스
|
||||
*/
|
||||
setShaderMaterial(vertexShader: string, fragmentShader: string): void;
|
||||
/**
|
||||
* 유니폼 값 업데이트
|
||||
* @param updates 업데이트할 유니폼 값들
|
||||
*/
|
||||
updateUniforms(updates: Partial<ShaderUniforms>): void;
|
||||
/**
|
||||
* 씬 렌더링
|
||||
*/
|
||||
render(): void;
|
||||
/**
|
||||
* 리소스 정리
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 셰이더 파일 로딩 및 관리 클래스
|
||||
*/
|
||||
declare class ShaderManager {
|
||||
private vertexShaderSource;
|
||||
private fragmentShaderSource;
|
||||
/**
|
||||
* 셰이더 파일들을 비동기로 로드
|
||||
* @param vertexPath 버텍스 셰이더 파일 경로
|
||||
* @param fragmentPath 프래그먼트 셰이더 파일 경로
|
||||
* @returns 로드된 셰이더 소스 코드
|
||||
*/
|
||||
loadShaders(vertexPath: string, fragmentPath: string): Promise<{
|
||||
vertex: string;
|
||||
fragment: string;
|
||||
}>;
|
||||
/**
|
||||
* 버텍스 셰이더 소스 코드 반환
|
||||
*/
|
||||
getVertexShader(): string;
|
||||
/**
|
||||
* 프래그먼트 셰이더 소스 코드 반환
|
||||
*/
|
||||
getFragmentShader(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 애니메이션 루프 관리 클래스
|
||||
*/
|
||||
declare class AnimationLoop {
|
||||
/**
|
||||
* 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트
|
||||
* @param areas 왜곡 영역 배열
|
||||
* @returns 업데이트된 영역 배열
|
||||
*/
|
||||
static updateAreaDragVectors(areas: DistortionArea[]): DistortionArea[];
|
||||
/**
|
||||
* 모든 영역의 진행도를 델타 타임만큼 업데이트
|
||||
* @param areas 왜곡 영역 배열
|
||||
* @param deltaTime 델타 타임 (초)
|
||||
* @returns 업데이트된 영역 배열
|
||||
*/
|
||||
static updateProgress(areas: DistortionArea[], deltaTime: number): DistortionArea[];
|
||||
}
|
||||
|
||||
/**
|
||||
* requestAnimationFrame을 사용한 애니메이션 루프 훅
|
||||
* @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)
|
||||
* @param isPlaying 애니메이션 재생 여부
|
||||
*/
|
||||
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
||||
|
||||
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, type DistortionMovement, type EasingFunction, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame };
|
||||
436
dist/index.js
vendored
Normal file
436
dist/index.js
vendored
Normal file
@ -0,0 +1,436 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
ANIMATION_CONFIG: () => ANIMATION_CONFIG,
|
||||
AnimationLoop: () => AnimationLoop,
|
||||
DEFAULT_AREA: () => DEFAULT_AREA,
|
||||
ImageDistortion: () => ImageDistortion,
|
||||
SHADER_CONFIG: () => SHADER_CONFIG,
|
||||
ShaderManager: () => ShaderManager,
|
||||
ThreeScene: () => ThreeScene,
|
||||
applyEasing: () => applyEasing,
|
||||
useAnimationFrame: () => useAnimationFrame
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/components/ImageDistortion.tsx
|
||||
var import_react2 = require("react");
|
||||
var THREE2 = __toESM(require("three"));
|
||||
|
||||
// src/engine/ThreeScene.ts
|
||||
var THREE = __toESM(require("three"));
|
||||
var ThreeScene = class {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.mesh = null;
|
||||
/**
|
||||
* 윈도우 리사이즈 핸들러
|
||||
*/
|
||||
this.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();
|
||||
}
|
||||
};
|
||||
this.scene = new THREE.Scene();
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* 셰이더 머티리얼 설정
|
||||
* @param vertexShader 버텍스 셰이더 소스
|
||||
* @param fragmentShader 프래그먼트 셰이더 소스
|
||||
*/
|
||||
setShaderMaterial(vertexShader, fragmentShader) {
|
||||
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 업데이트할 유니폼 값들
|
||||
*/
|
||||
updateUniforms(updates) {
|
||||
Object.keys(updates).forEach((key) => {
|
||||
const uniformKey = key;
|
||||
this.uniforms[uniformKey].value = updates[uniformKey].value;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 씬 렌더링
|
||||
*/
|
||||
render() {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
/**
|
||||
* 리소스 정리
|
||||
*/
|
||||
dispose() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
this.renderer.dispose();
|
||||
if (this.mesh) {
|
||||
this.mesh.geometry.dispose();
|
||||
this.mesh.material.dispose();
|
||||
}
|
||||
if (this.container.contains(this.renderer.domElement)) {
|
||||
this.container.removeChild(this.renderer.domElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/engine/ShaderManager.ts
|
||||
var ShaderManager = class {
|
||||
constructor() {
|
||||
this.vertexShaderSource = null;
|
||||
this.fragmentShaderSource = null;
|
||||
}
|
||||
/**
|
||||
* 셰이더 파일들을 비동기로 로드
|
||||
* @param vertexPath 버텍스 셰이더 파일 경로
|
||||
* @param fragmentPath 프래그먼트 셰이더 파일 경로
|
||||
* @returns 로드된 셰이더 소스 코드
|
||||
*/
|
||||
async loadShaders(vertexPath, fragmentPath) {
|
||||
try {
|
||||
const [vertexResponse, fragmentResponse] = await Promise.all([
|
||||
fetch(vertexPath),
|
||||
fetch(fragmentPath)
|
||||
]);
|
||||
if (!vertexResponse.ok) {
|
||||
throw new Error(`\uBC84\uD14D\uC2A4 \uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328: ${vertexResponse.statusText}`);
|
||||
}
|
||||
if (!fragmentResponse.ok) {
|
||||
throw new Error(`\uD504\uB798\uADF8\uBA3C\uD2B8 \uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328: ${fragmentResponse.statusText}`);
|
||||
}
|
||||
this.vertexShaderSource = await vertexResponse.text();
|
||||
this.fragmentShaderSource = await fragmentResponse.text();
|
||||
return {
|
||||
vertex: this.vertexShaderSource,
|
||||
fragment: this.fragmentShaderSource
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("\uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328:", error);
|
||||
throw new Error("\uC170\uC774\uB354 \uB85C\uB529\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 버텍스 셰이더 소스 코드 반환
|
||||
*/
|
||||
getVertexShader() {
|
||||
if (!this.vertexShaderSource) {
|
||||
throw new Error("\uBC84\uD14D\uC2A4 \uC170\uC774\uB354\uAC00 \uB85C\uB4DC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
|
||||
}
|
||||
return this.vertexShaderSource;
|
||||
}
|
||||
/**
|
||||
* 프래그먼트 셰이더 소스 코드 반환
|
||||
*/
|
||||
getFragmentShader() {
|
||||
if (!this.fragmentShaderSource) {
|
||||
throw new Error("\uD504\uB798\uADF8\uBA3C\uD2B8 \uC170\uC774\uB354\uAC00 \uB85C\uB4DC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
|
||||
}
|
||||
return this.fragmentShaderSource;
|
||||
}
|
||||
};
|
||||
|
||||
// src/utils/easing.ts
|
||||
var easingFunctions = {
|
||||
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)
|
||||
};
|
||||
var applyEasing = (progress, easingType) => {
|
||||
const clampedProgress = Math.max(0, Math.min(1, progress));
|
||||
return easingFunctions[easingType](clampedProgress);
|
||||
};
|
||||
|
||||
// src/engine/AnimationLoop.ts
|
||||
var AnimationLoop = class {
|
||||
/**
|
||||
* 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트
|
||||
* @param areas 왜곡 영역 배열
|
||||
* @returns 업데이트된 영역 배열
|
||||
*/
|
||||
static updateAreaDragVectors(areas) {
|
||||
return areas.map((area) => {
|
||||
const { progress, movement } = area;
|
||||
const easedProgress = applyEasing(progress, movement.easing);
|
||||
let dragVector;
|
||||
if (easedProgress < 0.5) {
|
||||
const t = easedProgress * 2;
|
||||
dragVector = {
|
||||
x: movement.vectorA.x * t,
|
||||
y: movement.vectorA.y * t
|
||||
};
|
||||
} else {
|
||||
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 업데이트된 영역 배열
|
||||
*/
|
||||
static updateProgress(areas, deltaTime) {
|
||||
return areas.map((area) => {
|
||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||
newProgress %= 1;
|
||||
return {
|
||||
...area,
|
||||
progress: newProgress
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/hooks/useAnimationFrame.ts
|
||||
var import_react = require("react");
|
||||
var useAnimationFrame = (callback, isPlaying = true) => {
|
||||
const requestRef = (0, import_react.useRef)(void 0);
|
||||
const previousTimeRef = (0, import_react.useRef)(void 0);
|
||||
(0, import_react.useEffect)(() => {
|
||||
if (!isPlaying) return;
|
||||
const animate = (time) => {
|
||||
if (previousTimeRef.current !== void 0) {
|
||||
const deltaTime = (time - previousTimeRef.current) / 1e3;
|
||||
callback(deltaTime);
|
||||
}
|
||||
previousTimeRef.current = time;
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
return () => {
|
||||
if (requestRef.current) {
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
}
|
||||
};
|
||||
}, [callback, isPlaying]);
|
||||
};
|
||||
|
||||
// src/utils/constants.ts
|
||||
var SHADER_CONFIG = {
|
||||
/** 최대 영역 개수 */
|
||||
MAX_AREAS: 8,
|
||||
/** 최대 포인트 개수 (8영역 × 4포인트) */
|
||||
MAX_POINTS: 32,
|
||||
/** 최대 드래그 벡터 개수 */
|
||||
MAX_DRAG_VECTORS: 8,
|
||||
/** 최대 강도 배열 크기 */
|
||||
MAX_STRENGTHS: 8
|
||||
};
|
||||
var ANIMATION_CONFIG = {
|
||||
/** 목표 FPS */
|
||||
TARGET_FPS: 60,
|
||||
/** 델타 타임 (약 16.67ms) */
|
||||
DELTA_TIME: 1 / 60
|
||||
};
|
||||
var DEFAULT_AREA = {
|
||||
/** 기본 왜곡 강도 */
|
||||
DISTORTION_STRENGTH: 0.5,
|
||||
/** 기본 애니메이션 지속 시간 (초) */
|
||||
DURATION: 2,
|
||||
/** 기본 이징 함수 */
|
||||
EASING: "easeInOut",
|
||||
/** 기본 벡터 A */
|
||||
VECTOR_A: { x: 0.1, y: 0.1 },
|
||||
/** 기본 벡터 B */
|
||||
VECTOR_B: { x: -0.1, y: -0.1 }
|
||||
};
|
||||
|
||||
// src/components/ImageDistortion.tsx
|
||||
var import_jsx_runtime = require("react/jsx-runtime");
|
||||
var ImageDistortion = ({
|
||||
imageSrc,
|
||||
areas,
|
||||
vertexShaderPath,
|
||||
fragmentShaderPath,
|
||||
isPlaying = true,
|
||||
style,
|
||||
className
|
||||
}) => {
|
||||
const containerRef = (0, import_react2.useRef)(null);
|
||||
const sceneRef = (0, import_react2.useRef)(null);
|
||||
const shaderManagerRef = (0, import_react2.useRef)(new ShaderManager());
|
||||
const textureRef = (0, import_react2.useRef)(null);
|
||||
const [isReady, setIsReady] = (0, import_react2.useState)(false);
|
||||
const [currentAreas, setCurrentAreas] = (0, import_react2.useState)(areas);
|
||||
(0, import_react2.useEffect)(() => {
|
||||
setCurrentAreas(areas);
|
||||
}, [areas]);
|
||||
(0, import_react2.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("\uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328:", error);
|
||||
});
|
||||
return () => {
|
||||
scene.dispose();
|
||||
if (textureRef.current) {
|
||||
textureRef.current.dispose();
|
||||
}
|
||||
};
|
||||
}, [vertexShaderPath, fragmentShaderPath]);
|
||||
(0, import_react2.useEffect)(() => {
|
||||
if (!imageSrc || !isReady) return;
|
||||
const loader = new THREE2.TextureLoader();
|
||||
loader.load(
|
||||
imageSrc,
|
||||
(texture) => {
|
||||
textureRef.current = texture;
|
||||
if (sceneRef.current) {
|
||||
sceneRef.current.updateUniforms({
|
||||
u_texture: { value: texture }
|
||||
});
|
||||
sceneRef.current.render();
|
||||
}
|
||||
},
|
||||
void 0,
|
||||
(error) => {
|
||||
console.error("\uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2E4\uD328:", error);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
if (textureRef.current) {
|
||||
textureRef.current.dispose();
|
||||
textureRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [imageSrc, isReady]);
|
||||
(0, import_react2.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 = (0, import_react2.useCallback)((deltaTime) => {
|
||||
if (!isReady) return;
|
||||
const updatedAreas = AnimationLoop.updateProgress(currentAreas, deltaTime);
|
||||
const areasWithVectors = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
setCurrentAreas(areasWithVectors);
|
||||
}, [currentAreas, isReady]);
|
||||
useAnimationFrame(animationCallback, isPlaying);
|
||||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
"div",
|
||||
{
|
||||
ref: containerRef,
|
||||
style: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
...style
|
||||
},
|
||||
className
|
||||
}
|
||||
);
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
ANIMATION_CONFIG,
|
||||
AnimationLoop,
|
||||
DEFAULT_AREA,
|
||||
ImageDistortion,
|
||||
SHADER_CONFIG,
|
||||
ShaderManager,
|
||||
ThreeScene,
|
||||
applyEasing,
|
||||
useAnimationFrame
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/index.js.map
vendored
Normal file
1
dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
391
dist/index.mjs
vendored
Normal file
391
dist/index.mjs
vendored
Normal file
@ -0,0 +1,391 @@
|
||||
// src/components/ImageDistortion.tsx
|
||||
import { useEffect as useEffect2, useRef as useRef2, useState, useCallback } from "react";
|
||||
import * as THREE2 from "three";
|
||||
|
||||
// src/engine/ThreeScene.ts
|
||||
import * as THREE from "three";
|
||||
var ThreeScene = class {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.mesh = null;
|
||||
/**
|
||||
* 윈도우 리사이즈 핸들러
|
||||
*/
|
||||
this.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();
|
||||
}
|
||||
};
|
||||
this.scene = new THREE.Scene();
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* 셰이더 머티리얼 설정
|
||||
* @param vertexShader 버텍스 셰이더 소스
|
||||
* @param fragmentShader 프래그먼트 셰이더 소스
|
||||
*/
|
||||
setShaderMaterial(vertexShader, fragmentShader) {
|
||||
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 업데이트할 유니폼 값들
|
||||
*/
|
||||
updateUniforms(updates) {
|
||||
Object.keys(updates).forEach((key) => {
|
||||
const uniformKey = key;
|
||||
this.uniforms[uniformKey].value = updates[uniformKey].value;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 씬 렌더링
|
||||
*/
|
||||
render() {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
/**
|
||||
* 리소스 정리
|
||||
*/
|
||||
dispose() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
this.renderer.dispose();
|
||||
if (this.mesh) {
|
||||
this.mesh.geometry.dispose();
|
||||
this.mesh.material.dispose();
|
||||
}
|
||||
if (this.container.contains(this.renderer.domElement)) {
|
||||
this.container.removeChild(this.renderer.domElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/engine/ShaderManager.ts
|
||||
var ShaderManager = class {
|
||||
constructor() {
|
||||
this.vertexShaderSource = null;
|
||||
this.fragmentShaderSource = null;
|
||||
}
|
||||
/**
|
||||
* 셰이더 파일들을 비동기로 로드
|
||||
* @param vertexPath 버텍스 셰이더 파일 경로
|
||||
* @param fragmentPath 프래그먼트 셰이더 파일 경로
|
||||
* @returns 로드된 셰이더 소스 코드
|
||||
*/
|
||||
async loadShaders(vertexPath, fragmentPath) {
|
||||
try {
|
||||
const [vertexResponse, fragmentResponse] = await Promise.all([
|
||||
fetch(vertexPath),
|
||||
fetch(fragmentPath)
|
||||
]);
|
||||
if (!vertexResponse.ok) {
|
||||
throw new Error(`\uBC84\uD14D\uC2A4 \uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328: ${vertexResponse.statusText}`);
|
||||
}
|
||||
if (!fragmentResponse.ok) {
|
||||
throw new Error(`\uD504\uB798\uADF8\uBA3C\uD2B8 \uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328: ${fragmentResponse.statusText}`);
|
||||
}
|
||||
this.vertexShaderSource = await vertexResponse.text();
|
||||
this.fragmentShaderSource = await fragmentResponse.text();
|
||||
return {
|
||||
vertex: this.vertexShaderSource,
|
||||
fragment: this.fragmentShaderSource
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("\uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328:", error);
|
||||
throw new Error("\uC170\uC774\uB354 \uB85C\uB529\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 버텍스 셰이더 소스 코드 반환
|
||||
*/
|
||||
getVertexShader() {
|
||||
if (!this.vertexShaderSource) {
|
||||
throw new Error("\uBC84\uD14D\uC2A4 \uC170\uC774\uB354\uAC00 \uB85C\uB4DC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
|
||||
}
|
||||
return this.vertexShaderSource;
|
||||
}
|
||||
/**
|
||||
* 프래그먼트 셰이더 소스 코드 반환
|
||||
*/
|
||||
getFragmentShader() {
|
||||
if (!this.fragmentShaderSource) {
|
||||
throw new Error("\uD504\uB798\uADF8\uBA3C\uD2B8 \uC170\uC774\uB354\uAC00 \uB85C\uB4DC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
|
||||
}
|
||||
return this.fragmentShaderSource;
|
||||
}
|
||||
};
|
||||
|
||||
// src/utils/easing.ts
|
||||
var easingFunctions = {
|
||||
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)
|
||||
};
|
||||
var applyEasing = (progress, easingType) => {
|
||||
const clampedProgress = Math.max(0, Math.min(1, progress));
|
||||
return easingFunctions[easingType](clampedProgress);
|
||||
};
|
||||
|
||||
// src/engine/AnimationLoop.ts
|
||||
var AnimationLoop = class {
|
||||
/**
|
||||
* 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트
|
||||
* @param areas 왜곡 영역 배열
|
||||
* @returns 업데이트된 영역 배열
|
||||
*/
|
||||
static updateAreaDragVectors(areas) {
|
||||
return areas.map((area) => {
|
||||
const { progress, movement } = area;
|
||||
const easedProgress = applyEasing(progress, movement.easing);
|
||||
let dragVector;
|
||||
if (easedProgress < 0.5) {
|
||||
const t = easedProgress * 2;
|
||||
dragVector = {
|
||||
x: movement.vectorA.x * t,
|
||||
y: movement.vectorA.y * t
|
||||
};
|
||||
} else {
|
||||
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 업데이트된 영역 배열
|
||||
*/
|
||||
static updateProgress(areas, deltaTime) {
|
||||
return areas.map((area) => {
|
||||
let newProgress = area.progress + deltaTime / area.movement.duration;
|
||||
newProgress %= 1;
|
||||
return {
|
||||
...area,
|
||||
progress: newProgress
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/hooks/useAnimationFrame.ts
|
||||
import { useEffect, useRef } from "react";
|
||||
var useAnimationFrame = (callback, isPlaying = true) => {
|
||||
const requestRef = useRef(void 0);
|
||||
const previousTimeRef = useRef(void 0);
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
const animate = (time) => {
|
||||
if (previousTimeRef.current !== void 0) {
|
||||
const deltaTime = (time - previousTimeRef.current) / 1e3;
|
||||
callback(deltaTime);
|
||||
}
|
||||
previousTimeRef.current = time;
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
return () => {
|
||||
if (requestRef.current) {
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
}
|
||||
};
|
||||
}, [callback, isPlaying]);
|
||||
};
|
||||
|
||||
// src/utils/constants.ts
|
||||
var SHADER_CONFIG = {
|
||||
/** 최대 영역 개수 */
|
||||
MAX_AREAS: 8,
|
||||
/** 최대 포인트 개수 (8영역 × 4포인트) */
|
||||
MAX_POINTS: 32,
|
||||
/** 최대 드래그 벡터 개수 */
|
||||
MAX_DRAG_VECTORS: 8,
|
||||
/** 최대 강도 배열 크기 */
|
||||
MAX_STRENGTHS: 8
|
||||
};
|
||||
var ANIMATION_CONFIG = {
|
||||
/** 목표 FPS */
|
||||
TARGET_FPS: 60,
|
||||
/** 델타 타임 (약 16.67ms) */
|
||||
DELTA_TIME: 1 / 60
|
||||
};
|
||||
var DEFAULT_AREA = {
|
||||
/** 기본 왜곡 강도 */
|
||||
DISTORTION_STRENGTH: 0.5,
|
||||
/** 기본 애니메이션 지속 시간 (초) */
|
||||
DURATION: 2,
|
||||
/** 기본 이징 함수 */
|
||||
EASING: "easeInOut",
|
||||
/** 기본 벡터 A */
|
||||
VECTOR_A: { x: 0.1, y: 0.1 },
|
||||
/** 기본 벡터 B */
|
||||
VECTOR_B: { x: -0.1, y: -0.1 }
|
||||
};
|
||||
|
||||
// src/components/ImageDistortion.tsx
|
||||
import { jsx } from "react/jsx-runtime";
|
||||
var ImageDistortion = ({
|
||||
imageSrc,
|
||||
areas,
|
||||
vertexShaderPath,
|
||||
fragmentShaderPath,
|
||||
isPlaying = true,
|
||||
style,
|
||||
className
|
||||
}) => {
|
||||
const containerRef = useRef2(null);
|
||||
const sceneRef = useRef2(null);
|
||||
const shaderManagerRef = useRef2(new ShaderManager());
|
||||
const textureRef = useRef2(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [currentAreas, setCurrentAreas] = useState(areas);
|
||||
useEffect2(() => {
|
||||
setCurrentAreas(areas);
|
||||
}, [areas]);
|
||||
useEffect2(() => {
|
||||
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("\uC170\uC774\uB354 \uB85C\uB4DC \uC2E4\uD328:", error);
|
||||
});
|
||||
return () => {
|
||||
scene.dispose();
|
||||
if (textureRef.current) {
|
||||
textureRef.current.dispose();
|
||||
}
|
||||
};
|
||||
}, [vertexShaderPath, fragmentShaderPath]);
|
||||
useEffect2(() => {
|
||||
if (!imageSrc || !isReady) return;
|
||||
const loader = new THREE2.TextureLoader();
|
||||
loader.load(
|
||||
imageSrc,
|
||||
(texture) => {
|
||||
textureRef.current = texture;
|
||||
if (sceneRef.current) {
|
||||
sceneRef.current.updateUniforms({
|
||||
u_texture: { value: texture }
|
||||
});
|
||||
sceneRef.current.render();
|
||||
}
|
||||
},
|
||||
void 0,
|
||||
(error) => {
|
||||
console.error("\uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2E4\uD328:", error);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
if (textureRef.current) {
|
||||
textureRef.current.dispose();
|
||||
textureRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [imageSrc, isReady]);
|
||||
useEffect2(() => {
|
||||
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) => {
|
||||
if (!isReady) return;
|
||||
const updatedAreas = AnimationLoop.updateProgress(currentAreas, deltaTime);
|
||||
const areasWithVectors = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||||
setCurrentAreas(areasWithVectors);
|
||||
}, [currentAreas, isReady]);
|
||||
useAnimationFrame(animationCallback, isPlaying);
|
||||
return /* @__PURE__ */ jsx(
|
||||
"div",
|
||||
{
|
||||
ref: containerRef,
|
||||
style: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
...style
|
||||
},
|
||||
className
|
||||
}
|
||||
);
|
||||
};
|
||||
export {
|
||||
ANIMATION_CONFIG,
|
||||
AnimationLoop,
|
||||
DEFAULT_AREA,
|
||||
ImageDistortion,
|
||||
SHADER_CONFIG,
|
||||
ShaderManager,
|
||||
ThreeScene,
|
||||
applyEasing,
|
||||
useAnimationFrame
|
||||
};
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
1
dist/index.mjs.map
vendored
Normal file
1
dist/index.mjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user