From 6ddae08d86da4a998d58914c88ae2a7a3235fb17 Mon Sep 17 00:00:00 2001 From: BaekRyang Date: Tue, 4 Nov 2025 10:17:17 +0900 Subject: [PATCH] init dist --- .gitignore | 1 - dist/distortion.frag.glsl | 88 ++++++++ dist/distortion.vert.glsl | 6 + dist/index.d.mts | 275 ++++++++++++++++++++++++ dist/index.d.ts | 275 ++++++++++++++++++++++++ dist/index.js | 436 ++++++++++++++++++++++++++++++++++++++ dist/index.js.map | 1 + dist/index.mjs | 391 ++++++++++++++++++++++++++++++++++ dist/index.mjs.map | 1 + 9 files changed, 1473 insertions(+), 1 deletion(-) create mode 100644 dist/distortion.frag.glsl create mode 100644 dist/distortion.vert.glsl create mode 100644 dist/index.d.mts create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 100644 dist/index.mjs create mode 100644 dist/index.mjs.map diff --git a/.gitignore b/.gitignore index 278f2c4..613fa96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ /tmp /out-tsc -/dist /node_modules npm-debug.log* diff --git a/dist/distortion.frag.glsl b/dist/distortion.frag.glsl new file mode 100644 index 0000000..f05b2e8 --- /dev/null +++ b/dist/distortion.frag.glsl @@ -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); +} \ No newline at end of file diff --git a/dist/distortion.vert.glsl b/dist/distortion.vert.glsl new file mode 100644 index 0000000..72652c6 --- /dev/null +++ b/dist/distortion.vert.glsl @@ -0,0 +1,6 @@ +varying vec2 vUv; + +void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); +} \ No newline at end of file diff --git a/dist/index.d.mts b/dist/index.d.mts new file mode 100644 index 0000000..ca95915 --- /dev/null +++ b/dist/index.d.mts @@ -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; + /** 화면 해상도 */ + u_resolution: THREE.IUniform; + /** 이미지 텍스처 */ + u_texture: THREE.IUniform; + /** 모든 영역의 포인트 배열 (최대 32개 포인트 = 8영역 × 4포인트) */ + u_points: THREE.IUniform; + /** 활성 영역 개수 */ + u_numAreas: THREE.IUniform; + /** 각 영역의 드래그 벡터 배열 */ + u_dragVectors: THREE.IUniform; + /** 각 영역의 왜곡 강도 배열 */ + u_distortionStrengths: THREE.IUniform; +} +/** + * 셰이더 설정 + */ +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; + +/** + * 진행도에 이징 함수를 적용 + * @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): 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 }; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..ca95915 --- /dev/null +++ b/dist/index.d.ts @@ -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; + /** 화면 해상도 */ + u_resolution: THREE.IUniform; + /** 이미지 텍스처 */ + u_texture: THREE.IUniform; + /** 모든 영역의 포인트 배열 (최대 32개 포인트 = 8영역 × 4포인트) */ + u_points: THREE.IUniform; + /** 활성 영역 개수 */ + u_numAreas: THREE.IUniform; + /** 각 영역의 드래그 벡터 배열 */ + u_dragVectors: THREE.IUniform; + /** 각 영역의 왜곡 강도 배열 */ + u_distortionStrengths: THREE.IUniform; +} +/** + * 셰이더 설정 + */ +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; + +/** + * 진행도에 이징 함수를 적용 + * @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): 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 }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..2df2a1a --- /dev/null +++ b/dist/index.js @@ -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 \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..2a99bdd --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/index.ts","../src/components/ImageDistortion.tsx","../src/engine/ThreeScene.ts","../src/engine/ShaderManager.ts","../src/utils/easing.ts","../src/engine/AnimationLoop.ts","../src/hooks/useAnimationFrame.ts","../src/utils/constants.ts"],"sourcesContent":["// 메인 컴포넌트\nexport { ImageDistortion } from './components/ImageDistortion';\nexport type { ImageDistortionProps } from './components/ImageDistortion';\n\n// 타입 정의\nexport type {\n Point,\n EasingFunction,\n DistortionMovement,\n DistortionArea,\n AreaBounds,\n ShaderUniforms,\n ShaderConfig,\n AnimationState,\n AnimationTicker,\n} from './types';\n\n// 유틸리티 함수\nexport { applyEasing } from './utils/easing';\nexport { SHADER_CONFIG, ANIMATION_CONFIG, DEFAULT_AREA } from './utils/constants';\n\n// 엔진 클래스 (고급 사용자용)\nexport { ThreeScene } from './engine/ThreeScene';\nexport { ShaderManager } from './engine/ShaderManager';\nexport { AnimationLoop } from './engine/AnimationLoop';\n\n// 훅\nexport { useAnimationFrame } from './hooks/useAnimationFrame';","import React, { useEffect, useRef, useState, useCallback } from 'react';\nimport * as THREE from 'three';\nimport { DistortionArea } from '../types';\nimport { ThreeScene } from '../engine/ThreeScene';\nimport { ShaderManager } from '../engine/ShaderManager';\nimport { AnimationLoop } from '../engine/AnimationLoop';\nimport { useAnimationFrame } from '../hooks/useAnimationFrame';\nimport { SHADER_CONFIG } from '../utils/constants';\n\n/**\n * ImageDistortion 컴포넌트 Props\n */\nexport interface ImageDistortionProps {\n /** 이미지 소스 URL */\n imageSrc: string;\n /** 왜곡 영역 배열 */\n areas: DistortionArea[];\n /** 버텍스 셰이더 경로 (선택사항) */\n vertexShaderPath?: string;\n /** 프래그먼트 셰이더 경로 (선택사항) */\n fragmentShaderPath?: string;\n /** 애니메이션 재생 여부 */\n isPlaying?: boolean;\n /** 컨테이너 스타일 */\n style?: React.CSSProperties;\n /** 컨테이너 클래스명 */\n className?: string;\n}\n\n/**\n * GPU 가속 이미지 왜곡 컴포넌트\n * Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.\n */\nexport const ImageDistortion: React.FC = ({\n imageSrc,\n areas,\n vertexShaderPath,\n fragmentShaderPath,\n isPlaying = true,\n style,\n className,\n}) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const shaderManagerRef = useRef(new ShaderManager());\n const textureRef = useRef(null);\n\n const [isReady, setIsReady] = useState(false);\n const [currentAreas, setCurrentAreas] = useState(areas);\n\n // 영역 변경 시 상태 업데이트\n useEffect(() => {\n setCurrentAreas(areas);\n }, [areas]);\n\n // Three.js 씬 초기화\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new ThreeScene(containerRef.current);\n sceneRef.current = scene;\n\n // 셰이더 로드\n const vertPath = vertexShaderPath || '/shaders/distortion.vert.glsl';\n const fragPath = fragmentShaderPath || '/shaders/distortion.frag.glsl';\n\n shaderManagerRef.current\n .loadShaders(vertPath, fragPath)\n .then(({ vertex, fragment }) => {\n scene.setShaderMaterial(vertex, fragment);\n setIsReady(true);\n })\n .catch((error) => {\n console.error('셰이더 로드 실패:', error);\n });\n\n return () => {\n scene.dispose();\n if (textureRef.current) {\n textureRef.current.dispose();\n }\n };\n }, [vertexShaderPath, fragmentShaderPath]);\n\n // 이미지 텍스처 로드\n useEffect(() => {\n if (!imageSrc || !isReady) return;\n\n const loader = new THREE.TextureLoader();\n loader.load(\n imageSrc,\n (texture) => {\n textureRef.current = texture;\n if (sceneRef.current) {\n sceneRef.current.updateUniforms({\n u_texture: { value: texture },\n });\n sceneRef.current.render();\n }\n },\n undefined,\n (error) => {\n console.error('이미지 로드 실패:', error);\n }\n );\n\n return () => {\n if (textureRef.current) {\n textureRef.current.dispose();\n textureRef.current = null;\n }\n };\n }, [imageSrc, isReady]);\n\n // 셰이더 유니폼 업데이트\n useEffect(() => {\n if (!sceneRef.current || !isReady) return;\n\n // 포인트 배열 생성\n const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);\n currentAreas.forEach((area, areaIndex) => {\n area.basePoints.forEach((point, pointIndex) => {\n const index = (areaIndex * 4 + pointIndex) * 2;\n points[index] = point.x;\n points[index + 1] = point.y;\n });\n });\n\n // 드래그 벡터 배열 생성\n const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);\n currentAreas.forEach((area, index) => {\n const baseIndex = index * 2;\n dragVectors[baseIndex] = area.dragVector.x;\n dragVectors[baseIndex + 1] = area.dragVector.y;\n });\n\n // 강도 배열 생성\n const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);\n currentAreas.forEach((area, index) => {\n strengths[index] = area.distortionStrength;\n });\n\n sceneRef.current.updateUniforms({\n u_numAreas: { value: currentAreas.length },\n u_points: { value: points },\n u_dragVectors: { value: dragVectors },\n u_distortionStrengths: { value: strengths },\n });\n\n sceneRef.current.render();\n }, [currentAreas, isReady]);\n\n // 애니메이션 루프\n const animationCallback = useCallback((deltaTime: number) => {\n if (!isReady) return;\n\n // 진행도 업데이트\n const updatedAreas = AnimationLoop.updateProgress(currentAreas, deltaTime);\n\n // 드래그 벡터 업데이트\n const areasWithVectors = AnimationLoop.updateAreaDragVectors(updatedAreas);\n\n setCurrentAreas(areasWithVectors);\n }, [currentAreas, isReady]);\n\n useAnimationFrame(animationCallback, isPlaying);\n\n return (\n \n );\n};","import * as THREE from 'three';\nimport { ShaderUniforms } from '@/types';\n\n/**\n * Three.js 씬 관리 클래스\n */\nexport class ThreeScene {\n private scene: THREE.Scene;\n private camera: THREE.OrthographicCamera;\n private renderer: THREE.WebGLRenderer;\n private mesh: THREE.Mesh | null = null;\n private uniforms: ShaderUniforms;\n\n constructor(private container: HTMLElement) {\n // 씬 생성\n this.scene = new THREE.Scene();\n\n // 2D용 직교 카메라 설정\n this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n // 렌더러 설정\n this.renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: false,\n });\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.container.appendChild(this.renderer.domElement);\n\n // 유니폼 초기화\n this.uniforms = {\n u_resolution: { value: new THREE.Vector2() },\n u_texture: { value: null },\n u_points: { value: new Float32Array(64) }, // 32포인트 × 2(x,y)\n u_numAreas: { value: 0 },\n u_dragVectors: { value: new Float32Array(16) }, // 8벡터 × 2(x,y)\n u_distortionStrengths: { value: new Float32Array(8) },\n };\n\n this.handleResize();\n window.addEventListener('resize', this.handleResize);\n }\n\n /**\n * 윈도우 리사이즈 핸들러\n */\n private handleResize = () => {\n const width = this.container.clientWidth;\n const height = this.container.clientHeight;\n\n this.renderer.setSize(width, height);\n this.uniforms.u_resolution.value.set(width, height);\n\n if (this.mesh) {\n this.render();\n }\n };\n\n /**\n * 셰이더 머티리얼 설정\n * @param vertexShader 버텍스 셰이더 소스\n * @param fragmentShader 프래그먼트 셰이더 소스\n */\n public setShaderMaterial(vertexShader: string, fragmentShader: string) {\n const geometry = new THREE.PlaneGeometry(2, 2);\n const material = new THREE.ShaderMaterial({\n uniforms: this.uniforms,\n vertexShader,\n fragmentShader,\n });\n\n if (this.mesh) {\n this.scene.remove(this.mesh);\n }\n\n this.mesh = new THREE.Mesh(geometry, material);\n this.scene.add(this.mesh);\n }\n\n /**\n * 유니폼 값 업데이트\n * @param updates 업데이트할 유니폼 값들\n */\n public updateUniforms(updates: Partial) {\n Object.keys(updates).forEach((key) => {\n const uniformKey = key as keyof ShaderUniforms;\n this.uniforms[uniformKey].value = updates[uniformKey]!.value;\n });\n }\n\n /**\n * 씬 렌더링\n */\n public render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n /**\n * 리소스 정리\n */\n public dispose() {\n window.removeEventListener('resize', this.handleResize);\n this.renderer.dispose();\n if (this.mesh) {\n this.mesh.geometry.dispose();\n (this.mesh.material as THREE.Material).dispose();\n }\n if (this.container.contains(this.renderer.domElement)) {\n this.container.removeChild(this.renderer.domElement);\n }\n }\n}","/**\n * 셰이더 파일 로딩 및 관리 클래스\n */\nexport class ShaderManager {\n private vertexShaderSource: string | null = null;\n private fragmentShaderSource: string | null = null;\n\n /**\n * 셰이더 파일들을 비동기로 로드\n * @param vertexPath 버텍스 셰이더 파일 경로\n * @param fragmentPath 프래그먼트 셰이더 파일 경로\n * @returns 로드된 셰이더 소스 코드\n */\n public async loadShaders(\n vertexPath: string,\n fragmentPath: string\n ): Promise<{ vertex: string; fragment: string }> {\n try {\n const [vertexResponse, fragmentResponse] = await Promise.all([\n fetch(vertexPath),\n fetch(fragmentPath),\n ]);\n\n if (!vertexResponse.ok) {\n throw new Error(`버텍스 셰이더 로드 실패: ${vertexResponse.statusText}`);\n }\n if (!fragmentResponse.ok) {\n throw new Error(`프래그먼트 셰이더 로드 실패: ${fragmentResponse.statusText}`);\n }\n\n this.vertexShaderSource = await vertexResponse.text();\n this.fragmentShaderSource = await fragmentResponse.text();\n\n return {\n vertex: this.vertexShaderSource,\n fragment: this.fragmentShaderSource,\n };\n } catch (error) {\n console.error('셰이더 로드 실패:', error);\n throw new Error('셰이더 로딩에 실패했습니다');\n }\n }\n\n /**\n * 버텍스 셰이더 소스 코드 반환\n */\n public getVertexShader(): string {\n if (!this.vertexShaderSource) {\n throw new Error('버텍스 셰이더가 로드되지 않았습니다');\n }\n return this.vertexShaderSource;\n }\n\n /**\n * 프래그먼트 셰이더 소스 코드 반환\n */\n public getFragmentShader(): string {\n if (!this.fragmentShaderSource) {\n throw new Error('프래그먼트 셰이더가 로드되지 않았습니다');\n }\n return this.fragmentShaderSource;\n }\n}","import { EasingFunction } from '../types';\n\ntype EasingFunc = (t: number) => number;\n\n/**\n * 이징 함수 구현 맵\n */\nconst easingFunctions: Record = {\n linear: (t) => t,\n\n easeIn: (t) => t * t,\n easeOut: (t) => t * (2 - t),\n easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),\n\n easeInQuad: (t) => t * t,\n easeOutQuad: (t) => t * (2 - t),\n};\n\n/**\n * 진행도에 이징 함수를 적용\n * @param progress 진행도 (0.0 - 1.0)\n * @param easingType 적용할 이징 함수 타입\n * @returns 이징이 적용된 진행도 (0.0 - 1.0)\n */\nexport const applyEasing = (\n progress: number,\n easingType: EasingFunction\n): number => {\n const clampedProgress = Math.max(0, Math.min(1, progress));\n return easingFunctions[easingType](clampedProgress);\n};","import { DistortionArea, Point } from '../types';\nimport { applyEasing } from '../utils/easing';\n\n/**\n * 애니메이션 루프 관리 클래스\n */\nexport class AnimationLoop {\n /**\n * 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트\n * @param areas 왜곡 영역 배열\n * @returns 업데이트된 영역 배열\n */\n public static updateAreaDragVectors(\n areas: DistortionArea[]\n ): DistortionArea[] {\n return areas.map((area) => {\n const { progress, movement } = area;\n\n // 이징 적용\n const easedProgress = applyEasing(progress, movement.easing);\n\n // 벡터 간 보간\n let dragVector: Point;\n\n if (easedProgress < 0.5) {\n // 0.0 -> 0.5: 0에서 vectorA로 보간\n const t = easedProgress * 2;\n dragVector = {\n x: movement.vectorA.x * t,\n y: movement.vectorA.y * t,\n };\n } else {\n // 0.5 -> 1.0: vectorA에서 0으로 보간\n const t = (easedProgress - 0.5) * 2;\n dragVector = {\n x: movement.vectorA.x * (1 - t),\n y: movement.vectorA.y * (1 - t),\n };\n }\n\n return {\n ...area,\n dragVector,\n };\n });\n }\n\n /**\n * 모든 영역의 진행도를 델타 타임만큼 업데이트\n * @param areas 왜곡 영역 배열\n * @param deltaTime 델타 타임 (초)\n * @returns 업데이트된 영역 배열\n */\n public static updateProgress(\n areas: DistortionArea[],\n deltaTime: number\n ): DistortionArea[] {\n return areas.map((area) => {\n let newProgress = area.progress + deltaTime / area.movement.duration;\n newProgress %= 1.0; // 루프\n\n return {\n ...area,\n progress: newProgress,\n };\n });\n }\n}","import { useEffect, useRef } from 'react';\n\n/**\n * requestAnimationFrame을 사용한 애니메이션 루프 훅\n * @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)\n * @param isPlaying 애니메이션 재생 여부\n */\nexport const useAnimationFrame = (\n callback: (deltaTime: number) => void,\n isPlaying: boolean = true\n) => {\n const requestRef = useRef(undefined);\n const previousTimeRef = useRef(undefined);\n\n useEffect(() => {\n if (!isPlaying) return;\n\n const animate = (time: number) => {\n if (previousTimeRef.current !== undefined) {\n const deltaTime = (time - previousTimeRef.current) / 1000; // 밀리초를 초로 변환\n callback(deltaTime);\n }\n previousTimeRef.current = time;\n requestRef.current = requestAnimationFrame(animate);\n };\n\n requestRef.current = requestAnimationFrame(animate);\n\n return () => {\n if (requestRef.current) {\n cancelAnimationFrame(requestRef.current);\n }\n };\n }, [callback, isPlaying]);\n};","/**\n * 셰이더 관련 설정\n */\nexport const SHADER_CONFIG = {\n /** 최대 영역 개수 */\n MAX_AREAS: 8,\n /** 최대 포인트 개수 (8영역 × 4포인트) */\n MAX_POINTS: 32,\n /** 최대 드래그 벡터 개수 */\n MAX_DRAG_VECTORS: 8,\n /** 최대 강도 배열 크기 */\n MAX_STRENGTHS: 8,\n} as const;\n\n/**\n * 애니메이션 관련 설정\n */\nexport const ANIMATION_CONFIG = {\n /** 목표 FPS */\n TARGET_FPS: 60,\n /** 델타 타임 (약 16.67ms) */\n DELTA_TIME: 1 / 60,\n} as const;\n\n/**\n * 기본 영역 설정값\n */\nexport const DEFAULT_AREA = {\n /** 기본 왜곡 강도 */\n DISTORTION_STRENGTH: 0.5,\n /** 기본 애니메이션 지속 시간 (초) */\n DURATION: 2.0,\n /** 기본 이징 함수 */\n EASING: 'easeInOut' as const,\n /** 기본 벡터 A */\n VECTOR_A: { x: 0.1, y: 0.1 },\n /** 기본 벡터 B */\n VECTOR_B: { x: -0.1, y: -0.1 },\n} as const;"],"mappings":"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","names":["import_react","THREE"]} \ No newline at end of file diff --git a/dist/index.mjs b/dist/index.mjs new file mode 100644 index 0000000..7ba02f8 --- /dev/null +++ b/dist/index.mjs @@ -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 \ No newline at end of file diff --git a/dist/index.mjs.map b/dist/index.mjs.map new file mode 100644 index 0000000..f9a0d14 --- /dev/null +++ b/dist/index.mjs.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/components/ImageDistortion.tsx","../src/engine/ThreeScene.ts","../src/engine/ShaderManager.ts","../src/utils/easing.ts","../src/engine/AnimationLoop.ts","../src/hooks/useAnimationFrame.ts","../src/utils/constants.ts"],"sourcesContent":["import React, { useEffect, useRef, useState, useCallback } from 'react';\nimport * as THREE from 'three';\nimport { DistortionArea } from '../types';\nimport { ThreeScene } from '../engine/ThreeScene';\nimport { ShaderManager } from '../engine/ShaderManager';\nimport { AnimationLoop } from '../engine/AnimationLoop';\nimport { useAnimationFrame } from '../hooks/useAnimationFrame';\nimport { SHADER_CONFIG } from '../utils/constants';\n\n/**\n * ImageDistortion 컴포넌트 Props\n */\nexport interface ImageDistortionProps {\n /** 이미지 소스 URL */\n imageSrc: string;\n /** 왜곡 영역 배열 */\n areas: DistortionArea[];\n /** 버텍스 셰이더 경로 (선택사항) */\n vertexShaderPath?: string;\n /** 프래그먼트 셰이더 경로 (선택사항) */\n fragmentShaderPath?: string;\n /** 애니메이션 재생 여부 */\n isPlaying?: boolean;\n /** 컨테이너 스타일 */\n style?: React.CSSProperties;\n /** 컨테이너 클래스명 */\n className?: string;\n}\n\n/**\n * GPU 가속 이미지 왜곡 컴포넌트\n * Three.js와 GLSL 셰이더를 사용하여 실시간 이미지 왜곡 효과를 제공합니다.\n */\nexport const ImageDistortion: React.FC = ({\n imageSrc,\n areas,\n vertexShaderPath,\n fragmentShaderPath,\n isPlaying = true,\n style,\n className,\n}) => {\n const containerRef = useRef(null);\n const sceneRef = useRef(null);\n const shaderManagerRef = useRef(new ShaderManager());\n const textureRef = useRef(null);\n\n const [isReady, setIsReady] = useState(false);\n const [currentAreas, setCurrentAreas] = useState(areas);\n\n // 영역 변경 시 상태 업데이트\n useEffect(() => {\n setCurrentAreas(areas);\n }, [areas]);\n\n // Three.js 씬 초기화\n useEffect(() => {\n if (!containerRef.current) return;\n\n const scene = new ThreeScene(containerRef.current);\n sceneRef.current = scene;\n\n // 셰이더 로드\n const vertPath = vertexShaderPath || '/shaders/distortion.vert.glsl';\n const fragPath = fragmentShaderPath || '/shaders/distortion.frag.glsl';\n\n shaderManagerRef.current\n .loadShaders(vertPath, fragPath)\n .then(({ vertex, fragment }) => {\n scene.setShaderMaterial(vertex, fragment);\n setIsReady(true);\n })\n .catch((error) => {\n console.error('셰이더 로드 실패:', error);\n });\n\n return () => {\n scene.dispose();\n if (textureRef.current) {\n textureRef.current.dispose();\n }\n };\n }, [vertexShaderPath, fragmentShaderPath]);\n\n // 이미지 텍스처 로드\n useEffect(() => {\n if (!imageSrc || !isReady) return;\n\n const loader = new THREE.TextureLoader();\n loader.load(\n imageSrc,\n (texture) => {\n textureRef.current = texture;\n if (sceneRef.current) {\n sceneRef.current.updateUniforms({\n u_texture: { value: texture },\n });\n sceneRef.current.render();\n }\n },\n undefined,\n (error) => {\n console.error('이미지 로드 실패:', error);\n }\n );\n\n return () => {\n if (textureRef.current) {\n textureRef.current.dispose();\n textureRef.current = null;\n }\n };\n }, [imageSrc, isReady]);\n\n // 셰이더 유니폼 업데이트\n useEffect(() => {\n if (!sceneRef.current || !isReady) return;\n\n // 포인트 배열 생성\n const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);\n currentAreas.forEach((area, areaIndex) => {\n area.basePoints.forEach((point, pointIndex) => {\n const index = (areaIndex * 4 + pointIndex) * 2;\n points[index] = point.x;\n points[index + 1] = point.y;\n });\n });\n\n // 드래그 벡터 배열 생성\n const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);\n currentAreas.forEach((area, index) => {\n const baseIndex = index * 2;\n dragVectors[baseIndex] = area.dragVector.x;\n dragVectors[baseIndex + 1] = area.dragVector.y;\n });\n\n // 강도 배열 생성\n const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);\n currentAreas.forEach((area, index) => {\n strengths[index] = area.distortionStrength;\n });\n\n sceneRef.current.updateUniforms({\n u_numAreas: { value: currentAreas.length },\n u_points: { value: points },\n u_dragVectors: { value: dragVectors },\n u_distortionStrengths: { value: strengths },\n });\n\n sceneRef.current.render();\n }, [currentAreas, isReady]);\n\n // 애니메이션 루프\n const animationCallback = useCallback((deltaTime: number) => {\n if (!isReady) return;\n\n // 진행도 업데이트\n const updatedAreas = AnimationLoop.updateProgress(currentAreas, deltaTime);\n\n // 드래그 벡터 업데이트\n const areasWithVectors = AnimationLoop.updateAreaDragVectors(updatedAreas);\n\n setCurrentAreas(areasWithVectors);\n }, [currentAreas, isReady]);\n\n useAnimationFrame(animationCallback, isPlaying);\n\n return (\n \n );\n};","import * as THREE from 'three';\nimport { ShaderUniforms } from '@/types';\n\n/**\n * Three.js 씬 관리 클래스\n */\nexport class ThreeScene {\n private scene: THREE.Scene;\n private camera: THREE.OrthographicCamera;\n private renderer: THREE.WebGLRenderer;\n private mesh: THREE.Mesh | null = null;\n private uniforms: ShaderUniforms;\n\n constructor(private container: HTMLElement) {\n // 씬 생성\n this.scene = new THREE.Scene();\n\n // 2D용 직교 카메라 설정\n this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\n // 렌더러 설정\n this.renderer = new THREE.WebGLRenderer({\n antialias: true,\n alpha: false,\n });\n this.renderer.setPixelRatio(window.devicePixelRatio);\n this.container.appendChild(this.renderer.domElement);\n\n // 유니폼 초기화\n this.uniforms = {\n u_resolution: { value: new THREE.Vector2() },\n u_texture: { value: null },\n u_points: { value: new Float32Array(64) }, // 32포인트 × 2(x,y)\n u_numAreas: { value: 0 },\n u_dragVectors: { value: new Float32Array(16) }, // 8벡터 × 2(x,y)\n u_distortionStrengths: { value: new Float32Array(8) },\n };\n\n this.handleResize();\n window.addEventListener('resize', this.handleResize);\n }\n\n /**\n * 윈도우 리사이즈 핸들러\n */\n private handleResize = () => {\n const width = this.container.clientWidth;\n const height = this.container.clientHeight;\n\n this.renderer.setSize(width, height);\n this.uniforms.u_resolution.value.set(width, height);\n\n if (this.mesh) {\n this.render();\n }\n };\n\n /**\n * 셰이더 머티리얼 설정\n * @param vertexShader 버텍스 셰이더 소스\n * @param fragmentShader 프래그먼트 셰이더 소스\n */\n public setShaderMaterial(vertexShader: string, fragmentShader: string) {\n const geometry = new THREE.PlaneGeometry(2, 2);\n const material = new THREE.ShaderMaterial({\n uniforms: this.uniforms,\n vertexShader,\n fragmentShader,\n });\n\n if (this.mesh) {\n this.scene.remove(this.mesh);\n }\n\n this.mesh = new THREE.Mesh(geometry, material);\n this.scene.add(this.mesh);\n }\n\n /**\n * 유니폼 값 업데이트\n * @param updates 업데이트할 유니폼 값들\n */\n public updateUniforms(updates: Partial) {\n Object.keys(updates).forEach((key) => {\n const uniformKey = key as keyof ShaderUniforms;\n this.uniforms[uniformKey].value = updates[uniformKey]!.value;\n });\n }\n\n /**\n * 씬 렌더링\n */\n public render() {\n this.renderer.render(this.scene, this.camera);\n }\n\n /**\n * 리소스 정리\n */\n public dispose() {\n window.removeEventListener('resize', this.handleResize);\n this.renderer.dispose();\n if (this.mesh) {\n this.mesh.geometry.dispose();\n (this.mesh.material as THREE.Material).dispose();\n }\n if (this.container.contains(this.renderer.domElement)) {\n this.container.removeChild(this.renderer.domElement);\n }\n }\n}","/**\n * 셰이더 파일 로딩 및 관리 클래스\n */\nexport class ShaderManager {\n private vertexShaderSource: string | null = null;\n private fragmentShaderSource: string | null = null;\n\n /**\n * 셰이더 파일들을 비동기로 로드\n * @param vertexPath 버텍스 셰이더 파일 경로\n * @param fragmentPath 프래그먼트 셰이더 파일 경로\n * @returns 로드된 셰이더 소스 코드\n */\n public async loadShaders(\n vertexPath: string,\n fragmentPath: string\n ): Promise<{ vertex: string; fragment: string }> {\n try {\n const [vertexResponse, fragmentResponse] = await Promise.all([\n fetch(vertexPath),\n fetch(fragmentPath),\n ]);\n\n if (!vertexResponse.ok) {\n throw new Error(`버텍스 셰이더 로드 실패: ${vertexResponse.statusText}`);\n }\n if (!fragmentResponse.ok) {\n throw new Error(`프래그먼트 셰이더 로드 실패: ${fragmentResponse.statusText}`);\n }\n\n this.vertexShaderSource = await vertexResponse.text();\n this.fragmentShaderSource = await fragmentResponse.text();\n\n return {\n vertex: this.vertexShaderSource,\n fragment: this.fragmentShaderSource,\n };\n } catch (error) {\n console.error('셰이더 로드 실패:', error);\n throw new Error('셰이더 로딩에 실패했습니다');\n }\n }\n\n /**\n * 버텍스 셰이더 소스 코드 반환\n */\n public getVertexShader(): string {\n if (!this.vertexShaderSource) {\n throw new Error('버텍스 셰이더가 로드되지 않았습니다');\n }\n return this.vertexShaderSource;\n }\n\n /**\n * 프래그먼트 셰이더 소스 코드 반환\n */\n public getFragmentShader(): string {\n if (!this.fragmentShaderSource) {\n throw new Error('프래그먼트 셰이더가 로드되지 않았습니다');\n }\n return this.fragmentShaderSource;\n }\n}","import { EasingFunction } from '../types';\n\ntype EasingFunc = (t: number) => number;\n\n/**\n * 이징 함수 구현 맵\n */\nconst easingFunctions: Record = {\n linear: (t) => t,\n\n easeIn: (t) => t * t,\n easeOut: (t) => t * (2 - t),\n easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),\n\n easeInQuad: (t) => t * t,\n easeOutQuad: (t) => t * (2 - t),\n};\n\n/**\n * 진행도에 이징 함수를 적용\n * @param progress 진행도 (0.0 - 1.0)\n * @param easingType 적용할 이징 함수 타입\n * @returns 이징이 적용된 진행도 (0.0 - 1.0)\n */\nexport const applyEasing = (\n progress: number,\n easingType: EasingFunction\n): number => {\n const clampedProgress = Math.max(0, Math.min(1, progress));\n return easingFunctions[easingType](clampedProgress);\n};","import { DistortionArea, Point } from '../types';\nimport { applyEasing } from '../utils/easing';\n\n/**\n * 애니메이션 루프 관리 클래스\n */\nexport class AnimationLoop {\n /**\n * 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트\n * @param areas 왜곡 영역 배열\n * @returns 업데이트된 영역 배열\n */\n public static updateAreaDragVectors(\n areas: DistortionArea[]\n ): DistortionArea[] {\n return areas.map((area) => {\n const { progress, movement } = area;\n\n // 이징 적용\n const easedProgress = applyEasing(progress, movement.easing);\n\n // 벡터 간 보간\n let dragVector: Point;\n\n if (easedProgress < 0.5) {\n // 0.0 -> 0.5: 0에서 vectorA로 보간\n const t = easedProgress * 2;\n dragVector = {\n x: movement.vectorA.x * t,\n y: movement.vectorA.y * t,\n };\n } else {\n // 0.5 -> 1.0: vectorA에서 0으로 보간\n const t = (easedProgress - 0.5) * 2;\n dragVector = {\n x: movement.vectorA.x * (1 - t),\n y: movement.vectorA.y * (1 - t),\n };\n }\n\n return {\n ...area,\n dragVector,\n };\n });\n }\n\n /**\n * 모든 영역의 진행도를 델타 타임만큼 업데이트\n * @param areas 왜곡 영역 배열\n * @param deltaTime 델타 타임 (초)\n * @returns 업데이트된 영역 배열\n */\n public static updateProgress(\n areas: DistortionArea[],\n deltaTime: number\n ): DistortionArea[] {\n return areas.map((area) => {\n let newProgress = area.progress + deltaTime / area.movement.duration;\n newProgress %= 1.0; // 루프\n\n return {\n ...area,\n progress: newProgress,\n };\n });\n }\n}","import { useEffect, useRef } from 'react';\n\n/**\n * requestAnimationFrame을 사용한 애니메이션 루프 훅\n * @param callback 매 프레임마다 호출될 콜백 (deltaTime을 인자로 받음)\n * @param isPlaying 애니메이션 재생 여부\n */\nexport const useAnimationFrame = (\n callback: (deltaTime: number) => void,\n isPlaying: boolean = true\n) => {\n const requestRef = useRef(undefined);\n const previousTimeRef = useRef(undefined);\n\n useEffect(() => {\n if (!isPlaying) return;\n\n const animate = (time: number) => {\n if (previousTimeRef.current !== undefined) {\n const deltaTime = (time - previousTimeRef.current) / 1000; // 밀리초를 초로 변환\n callback(deltaTime);\n }\n previousTimeRef.current = time;\n requestRef.current = requestAnimationFrame(animate);\n };\n\n requestRef.current = requestAnimationFrame(animate);\n\n return () => {\n if (requestRef.current) {\n cancelAnimationFrame(requestRef.current);\n }\n };\n }, [callback, isPlaying]);\n};","/**\n * 셰이더 관련 설정\n */\nexport const SHADER_CONFIG = {\n /** 최대 영역 개수 */\n MAX_AREAS: 8,\n /** 최대 포인트 개수 (8영역 × 4포인트) */\n MAX_POINTS: 32,\n /** 최대 드래그 벡터 개수 */\n MAX_DRAG_VECTORS: 8,\n /** 최대 강도 배열 크기 */\n MAX_STRENGTHS: 8,\n} as const;\n\n/**\n * 애니메이션 관련 설정\n */\nexport const ANIMATION_CONFIG = {\n /** 목표 FPS */\n TARGET_FPS: 60,\n /** 델타 타임 (약 16.67ms) */\n DELTA_TIME: 1 / 60,\n} as const;\n\n/**\n * 기본 영역 설정값\n */\nexport const DEFAULT_AREA = {\n /** 기본 왜곡 강도 */\n DISTORTION_STRENGTH: 0.5,\n /** 기본 애니메이션 지속 시간 (초) */\n DURATION: 2.0,\n /** 기본 이징 함수 */\n EASING: 'easeInOut' as const,\n /** 기본 벡터 A */\n VECTOR_A: { x: 0.1, y: 0.1 },\n /** 기본 벡터 B */\n VECTOR_B: { x: -0.1, y: -0.1 },\n} as const;"],"mappings":"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","names":["useEffect","useRef","THREE","useRef","useEffect"]} \ No newline at end of file