437 lines
14 KiB
JavaScript
437 lines
14 KiB
JavaScript
"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;
|
||
setCurrentAreas((prevAreas) => {
|
||
const updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||
return AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||
});
|
||
}, [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
|