"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, DistortionEditor: () => DistortionEditor, ImageDistortion: () => ImageDistortion, SHADER_CONFIG: () => SHADER_CONFIG, ShaderManager: () => ShaderManager, ThreeScene: () => ThreeScene, applyEasing: () => applyEasing, useAnimationFrame: () => useAnimationFrame, useDistortionEditor: () => useDistortionEditor }); 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) { console.log("[ThreeScene] setShaderMaterial \uD638\uCD9C\uB428"); console.log("[ThreeScene] vertexShader \uAE38\uC774:", vertexShader.length); console.log("[ThreeScene] fragmentShader \uAE38\uC774:", fragmentShader.length); const geometry = new THREE.PlaneGeometry(2, 2); const material = new THREE.ShaderMaterial({ uniforms: this.uniforms, vertexShader, fragmentShader }); console.log("[ThreeScene] ShaderMaterial \uC0DD\uC131\uB428"); const renderer = this.renderer; const testScene = new THREE.Scene(); const testMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material); testScene.add(testMesh); try { renderer.compile(testScene, this.camera); console.log("[ThreeScene] \uC170\uC774\uB354 \uCEF4\uD30C\uC77C \uC131\uACF5!"); } catch (e) { console.error("[ThreeScene] \uC170\uC774\uB354 \uCEF4\uD30C\uC77C \uC5D0\uB7EC:", e); } if (this.mesh) { this.scene.remove(this.mesh); } this.mesh = new THREE.Mesh(geometry, material); this.scene.add(this.mesh); console.log("[ThreeScene] mesh\uB97C \uC52C\uC5D0 \uCD94\uAC00\uD568"); } /** * 유니폼 값 업데이트 * @param updates 업데이트할 유니폼 값들 */ updateUniforms(updates) { Object.keys(updates).forEach((key) => { const uniformKey = key; this.uniforms[uniformKey].value = updates[uniformKey].value; }); } /** * 씬 렌더링 */ render() { console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh); this.renderer.render(this.scene, this.camera); } /** * 현재 해상도 가져오기 */ getResolution() { return { x: this.uniforms.u_resolution.value.x, y: this.uniforms.u_resolution.value.y }; } /** * 리소스 정리 */ 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) { console.log("[ShaderManager] loadShaders \uC2DC\uC791:", { vertexPath, fragmentPath }); try { console.log("[ShaderManager] fetch \uC2DC\uC791..."); const [vertexResponse, fragmentResponse] = await Promise.all([ fetch(vertexPath), fetch(fragmentPath) ]); console.log("[ShaderManager] fetch \uC644\uB8CC:", { vertexStatus: vertexResponse.status, fragmentStatus: fragmentResponse.status }); 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}`); } console.log("[ShaderManager] text() \uBCC0\uD658 \uC2DC\uC791..."); this.vertexShaderSource = await vertexResponse.text(); this.fragmentShaderSource = await fragmentResponse.text(); console.log("[ShaderManager] \uC170\uC774\uB354 \uB85C\uB4DC \uC644\uB8CC!", { vertexLength: this.vertexShaderSource.length, fragmentLength: this.fragmentShaderSource.length }); return { vertex: this.vertexShaderSource, fragment: this.fragmentShaderSource }; } catch (error) { console.error("[ShaderManager] \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 [imageLoaded, setImageLoaded] = (0, import_react2.useState)(false); const [currentAreas, setCurrentAreas] = (0, import_react2.useState)(areas); (0, import_react2.useEffect)(() => { setCurrentAreas(areas); }, [areas]); (0, import_react2.useEffect)(() => { console.log("[ImageDistortion] useEffect \uC2E4\uD589, containerRef.current:", containerRef.current); if (!containerRef.current) { console.warn("[ImageDistortion] containerRef.current\uAC00 null\uC785\uB2C8\uB2E4. \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC81C\uB300\uB85C \uB9C8\uC6B4\uD2B8\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4."); return; } console.log("[ImageDistortion] \uCD08\uAE30\uD654 \uC2DC\uC791"); const scene = new ThreeScene(containerRef.current); sceneRef.current = scene; const vertPath = vertexShaderPath || "/shaders/distortion.vert.glsl"; const fragPath = fragmentShaderPath || "/shaders/distortion.frag.glsl"; console.log("[ImageDistortion] \uC170\uC774\uB354 \uB85C\uB4DC \uC2DC\uB3C4:", { vertPath, fragPath }); shaderManagerRef.current.loadShaders(vertPath, fragPath).then(({ vertex, fragment }) => { console.log("[ImageDistortion] \uC170\uC774\uB354 \uB85C\uB4DC \uC131\uACF5"); scene.setShaderMaterial(vertex, fragment); setIsReady(true); }).catch((error) => { console.error("[ImageDistortion] \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) { console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2A4\uD0B5:", { imageSrc, isReady }); return; } console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2DC\uC791:", imageSrc); setImageLoaded(false); const loader = new THREE2.TextureLoader(); loader.load( imageSrc, (texture) => { console.log("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC131\uACF5!", { width: texture.image.width, height: texture.image.height }); textureRef.current = texture; setImageLoaded(true); if (sceneRef.current) { sceneRef.current.updateUniforms({ u_texture: { value: texture } }); sceneRef.current.render(); console.log("[ImageDistortion] \uD14D\uC2A4\uCC98 \uC5C5\uB370\uC774\uD2B8 \uBC0F \uB80C\uB354\uB9C1 \uC644\uB8CC"); } }, (progress) => { console.log( "[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB529 \uC911...", Math.round(progress.loaded / progress.total * 100) + "%" ); }, (error) => { console.error("[ImageDistortion] \uC774\uBBF8\uC9C0 \uB85C\uB4DC \uC2E4\uD328:", error); setImageLoaded(false); } ); return () => { if (textureRef.current) { textureRef.current.dispose(); textureRef.current = null; } }; }, [imageSrc, isReady]); (0, import_react2.useEffect)(() => { if (!sceneRef.current || !isReady) return; const resolution = sceneRef.current.getResolution(); 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] = 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, children: !imageLoaded && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { style: { position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", background: "rgba(0, 0, 0, 0.7)", color: "white", padding: "20px", borderRadius: "8px", zIndex: 999 }, children: "\uC774\uBBF8\uC9C0 \uB85C\uB529 \uC911..." } ) } ); }; // src/editor/DistortionEditor.tsx var import_react5 = require("react"); // src/editor/hooks/useDistortionEditor.ts var import_react3 = require("react"); var useDistortionEditor = (initialAreas = []) => { const [state, setState] = (0, import_react3.useState)({ selectedAreaId: initialAreas[0]?.id || null, areas: initialAreas, editMode: "normal", draggingPointIndex: null }); const selectArea = (0, import_react3.useCallback)((areaId) => { setState((prev) => ({ ...prev, selectedAreaId: areaId })); }, []); const addArea = (0, import_react3.useCallback)((area) => { setState((prev) => ({ ...prev, areas: [...prev.areas, area], selectedAreaId: area.id })); }, []); const removeArea = (0, import_react3.useCallback)((areaId) => { setState((prev) => { const newAreas = prev.areas.filter((a) => a.id !== areaId); return { ...prev, areas: newAreas, selectedAreaId: prev.selectedAreaId === areaId ? newAreas[0]?.id || null : prev.selectedAreaId }; }); }, []); const updateArea = (0, import_react3.useCallback)((areaId, updates) => { setState((prev) => ({ ...prev, areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area) })); }, []); const updatePoint = (0, import_react3.useCallback)((areaId, pointIndex, point) => { setState((prev) => ({ ...prev, areas: prev.areas.map((area) => { if (area.id === areaId) { const newPoints = [...area.basePoints]; newPoints[pointIndex] = point; return { ...area, basePoints: newPoints }; } return area; }) })); }, []); const startDragging = (0, import_react3.useCallback)((pointIndex) => { setState((prev) => ({ ...prev, draggingPointIndex: pointIndex })); }, []); const stopDragging = (0, import_react3.useCallback)(() => { setState((prev) => ({ ...prev, draggingPointIndex: null })); }, []); const setEditMode = (0, import_react3.useCallback)((mode) => { setState((prev) => ({ ...prev, editMode: mode })); }, []); const getSelectedArea = (0, import_react3.useCallback)(() => { return state.areas.find((a) => a.id === state.selectedAreaId) || null; }, [state.areas, state.selectedAreaId]); return { state, selectArea, addArea, removeArea, updateArea, updatePoint, startDragging, stopDragging, setEditMode, getSelectedArea }; }; // src/editor/components/EditorCanvas.tsx var import_react4 = require("react"); // src/editor/constants.ts var DEFAULT_EDITOR_CANVAS_STYLE = { // 3단계 원 스타일 (외부 -> 내부) circleLevels: [ { radius: 0.5, opacity: 0.3, lineWidth: 2, color: "rgba(255, 200, 0, 1)", dashPattern: [8, 4] }, { radius: 0.33, opacity: 0.6, lineWidth: 2.5, color: "rgba(255, 200, 0, 1)", dashPattern: [8, 4] }, { radius: 0.167, opacity: 0.9, lineWidth: 3, color: "rgba(255, 200, 0, 1)", dashPattern: [8, 4] } ], // 원 내부 채우기 circleFillColor: "rgba(255, 200, 0, 0.08)", // 중심점 centerPoint: { radius: 5, fillColor: "rgba(255, 200, 0, 1)", strokeColor: "rgba(255, 255, 255, 0.8)", strokeWidth: 2 }, // 포인트 핸들 pointHandle: { size: 16, fillColor: "#00aaff", strokeColor: "white", strokeWidth: 2, labelColor: "#00aaff", labelFontSize: 11 }, // 영역 외곽선 areaOutline: { selectedColor: "#00aaff", unselectedColor: "#888", selectedWidth: 2, unselectedWidth: 1, unselectedDashPattern: [5, 5], selectedFillColor: "rgba(0, 170, 255, 0.08)", // 선택된 영역 배경 (연한 파란색) unselectedFillColor: "rgba(136, 136, 136, 0.03)" // 선택 안된 영역 배경 (연한 회색) } }; // src/editor/components/EditorCanvas.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var EditorCanvas = ({ areas, selectedAreaId, imageSrc, width, height, onUpdatePoint, onUpdateArea, draggingPointIndex, onStartDragging, onStopDragging, style: customStyle }) => { const containerRef = (0, import_react4.useRef)(null); const [canvasSize, setCanvasSize] = (0, import_react4.useState)({ width: 0, height: 0 }); const [isDraggingArea, setIsDraggingArea] = (0, import_react4.useState)(false); const [dragStartPos, setDragStartPos] = (0, import_react4.useState)(null); const editorStyle = (0, import_react4.useMemo)(() => ({ ...DEFAULT_EDITOR_CANVAS_STYLE, ...customStyle, circleLevels: customStyle?.circleLevels || DEFAULT_EDITOR_CANVAS_STYLE.circleLevels, centerPoint: { ...DEFAULT_EDITOR_CANVAS_STYLE.centerPoint, ...customStyle?.centerPoint }, pointHandle: { ...DEFAULT_EDITOR_CANVAS_STYLE.pointHandle, ...customStyle?.pointHandle }, areaOutline: { ...DEFAULT_EDITOR_CANVAS_STYLE.areaOutline, ...customStyle?.areaOutline } }), [customStyle]); (0, import_react4.useEffect)(() => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); setCanvasSize({ width: rect.width, height: rect.height }); }, [width, height]); const selectedArea = areas.find((a) => a.id === selectedAreaId); const isPointInPolygon = (0, import_react4.useCallback)((point, polygon) => { let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x, yi = polygon[i].y; const xj = polygon[j].x, yj = polygon[j].y; const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; }, []); const handleMouseDown = (0, import_react4.useCallback)( (pointIndex) => (e) => { e.preventDefault(); e.stopPropagation(); onStartDragging(pointIndex); }, [onStartDragging] ); const handleCanvasMouseDown = (0, import_react4.useCallback)( (e) => { if (!selectedArea || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = (e.clientY - rect.top) / rect.height; const clickPoint = { x, y }; if (isPointInPolygon(clickPoint, selectedArea.basePoints)) { setIsDraggingArea(true); setDragStartPos(clickPoint); e.preventDefault(); } }, [selectedArea, isPointInPolygon] ); const handleMouseMove = (0, import_react4.useCallback)( (e) => { if (!selectedArea || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = (e.clientY - rect.top) / rect.height; if (draggingPointIndex !== null) { const clampedX = Math.max(0, Math.min(1, x)); const clampedY = Math.max(0, Math.min(1, y)); onUpdatePoint(selectedArea.id, draggingPointIndex, { x: clampedX, y: clampedY }); } else if (isDraggingArea && dragStartPos) { const deltaX = x - dragStartPos.x; const deltaY = y - dragStartPos.y; const newPoints = selectedArea.basePoints.map((point) => ({ x: Math.max(0, Math.min(1, point.x + deltaX)), y: Math.max(0, Math.min(1, point.y + deltaY)) })); onUpdateArea(selectedArea.id, { basePoints: newPoints }); setDragStartPos({ x, y }); } }, [draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea] ); const handleMouseUp = (0, import_react4.useCallback)(() => { if (draggingPointIndex !== null) { onStopDragging(); } if (isDraggingArea) { setIsDraggingArea(false); setDragStartPos(null); } }, [draggingPointIndex, isDraggingArea, onStopDragging]); (0, import_react4.useEffect)(() => { if (draggingPointIndex !== null || isDraggingArea) { window.addEventListener("mouseup", handleMouseUp); return () => window.removeEventListener("mouseup", handleMouseUp); } }, [draggingPointIndex, isDraggingArea, handleMouseUp]); const uvToPixel = (u, v, points, canvasWidth, canvasHeight) => { const [p0, p1, p2, p3] = points; const leftX = p0.x * (1 - u) + p1.x * u; const leftY = p0.y * (1 - u) + p1.y * u; const rightX = p3.x * (1 - u) + p2.x * u; const rightY = p3.y * (1 - u) + p2.y * u; const posX = leftX * (1 - v) + rightX * v; const posY = leftY * (1 - v) + rightY * v; return { x: posX * canvasWidth, y: posY * canvasHeight }; }; const drawDistortionCircle = (0, import_react4.useCallback)((ctx, points, canvasWidth, canvasHeight) => { const segments = 128; const centerU = 0.5; const centerV = 0.5; const circleLevels = editorStyle.circleLevels || []; circleLevels.forEach((level, index) => { const levelPoints = []; for (let i = 0; i <= segments; i++) { const theta = i / segments * 2 * Math.PI; const u = centerU - level.radius * Math.sin(theta); const v = centerV + level.radius * Math.cos(theta); const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight); levelPoints.push(pixelPos); } ctx.beginPath(); ctx.moveTo(levelPoints[0].x, levelPoints[0].y); for (let i = 1; i < levelPoints.length; i++) { ctx.lineTo(levelPoints[i].x, levelPoints[i].y); } ctx.closePath(); const baseColor = level.color || "rgba(255, 200, 0, 1)"; const colorWithOpacity = baseColor.replace(/rgba?\(([^)]+)\)/, (_, rgb) => { const parts = rgb.split(",").map((p) => p.trim()); return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${level.opacity})`; }); ctx.strokeStyle = colorWithOpacity; ctx.lineWidth = level.lineWidth; if (level.dashPattern) { ctx.setLineDash(level.dashPattern); } ctx.stroke(); ctx.setLineDash([]); if (index === 0 && editorStyle.circleFillColor) { ctx.fillStyle = editorStyle.circleFillColor; ctx.fill(); } }); const centerPointStyle = editorStyle.centerPoint || {}; const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight); ctx.beginPath(); ctx.arc(centerPixel.x, centerPixel.y, centerPointStyle.radius || 5, 0, 2 * Math.PI); if (centerPointStyle.fillColor) { ctx.fillStyle = centerPointStyle.fillColor; ctx.fill(); } if (centerPointStyle.strokeColor) { ctx.strokeStyle = centerPointStyle.strokeColor; ctx.lineWidth = centerPointStyle.strokeWidth || 2; ctx.stroke(); } }, [editorStyle]); const getCursorStyle = () => { if (draggingPointIndex !== null) return "grabbing"; if (isDraggingArea) return "grabbing"; return "default"; }; return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "div", { ref: containerRef, className: "editor-canvas", style: { width, height, position: "relative", cursor: getCursorStyle() }, onMouseDown: handleCanvasMouseDown, onMouseMove: handleMouseMove, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "svg", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }, children: areas.map((area) => { const isSelected = area.id === selectedAreaId; const points = area.basePoints; const outlineStyle = editorStyle.areaOutline || {}; return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("g", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "polygon", { points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "), fill: isSelected ? outlineStyle.selectedFillColor || "rgba(0, 170, 255, 0.08)" : outlineStyle.unselectedFillColor || "rgba(136, 136, 136, 0.03)", stroke: isSelected ? outlineStyle.selectedColor || "#00aaff" : outlineStyle.unselectedColor || "#888", strokeWidth: isSelected ? outlineStyle.selectedWidth || 2 : outlineStyle.unselectedWidth || 1, strokeDasharray: isSelected ? "0" : outlineStyle.unselectedDashPattern?.join(",") || "5,5", opacity: isSelected ? 1 : 0.5 } ) }, area.id); }) } ), selectedArea && canvasSize.width > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "canvas", { style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }, width: canvasSize.width, height: canvasSize.height, ref: (canvas) => { if (canvas) { const ctx = canvas.getContext("2d"); if (ctx) { ctx.clearRect(0, 0, canvasSize.width, canvasSize.height); drawDistortionCircle(ctx, selectedArea.basePoints, canvasSize.width, canvasSize.height); } } } } ), selectedArea && selectedArea.basePoints.map((point, index) => { const handleStyle = editorStyle.pointHandle || {}; return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "div", { className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`, style: { position: "absolute", left: `${point.x * 100}%`, top: `${point.y * 100}%`, transform: "translate(-50%, -50%)", width: handleStyle.size || 16, height: handleStyle.size || 16, borderRadius: "50%", backgroundColor: handleStyle.fillColor || "#00aaff", border: `${handleStyle.strokeWidth || 2}px solid ${handleStyle.strokeColor || "white"}`, cursor: "grab", pointerEvents: "auto", boxShadow: "0 2px 4px rgba(0,0,0,0.3)" }, onMouseDown: handleMouseDown(index), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "div", { style: { position: "absolute", top: -24, left: "50%", transform: "translateX(-50%)", fontSize: handleStyle.labelFontSize || 11, color: handleStyle.labelColor || "#00aaff", fontWeight: "bold", textShadow: "1px 1px 2px rgba(0,0,0,0.8)", whiteSpace: "nowrap" }, children: [ "P", index + 1 ] } ) }, index ); }) ] } ); }; // src/editor/components/AreaList.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); var AreaList = ({ areas, selectedAreaId, onSelectArea, onRemoveArea, onAddArea }) => { return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "area-list", children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "area-list-header", children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "button", { onClick: onAddArea, disabled: areas.length >= 8, className: "btn-add", title: areas.length >= 8 ? "\uCD5C\uB300 8\uAC1C \uC601\uC5ED\uAE4C\uC9C0 \uC9C0\uC6D0" : "\uC0C8 \uC601\uC5ED \uCD94\uAC00", children: "+ \uCD94\uAC00" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "area-list-empty", children: "\uC601\uC5ED\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. + \uCD94\uAC00 \uBC84\uD2BC\uC744 \uB20C\uB7EC\uC8FC\uC138\uC694." }) : areas.map((area, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)( "div", { className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`, onClick: () => onSelectArea(area.id), children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "area-item-info", children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "area-item-name", children: [ "\uC601\uC5ED ", index + 1 ] }), /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "area-item-strength", children: [ "\uAC15\uB3C4: ", (area.distortionStrength * 100).toFixed(0), "%" ] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "button", { onClick: (e) => { e.stopPropagation(); onRemoveArea(area.id); }, className: "btn-remove", title: "\uC601\uC5ED \uC0AD\uC81C", children: "\xD7" } ) ] }, area.id )) }) ] }); }; // src/editor/components/ParameterPanel.tsx var import_jsx_runtime4 = require("react/jsx-runtime"); var EASING_OPTIONS = [ { value: "linear", label: "\uC120\uD615 (Linear)" }, { value: "easeIn", label: "\uAC00\uC18D (Ease In)" }, { value: "easeOut", label: "\uAC10\uC18D (Ease Out)" }, { value: "easeInOut", label: "\uAC00\uAC10\uC18D (Ease In Out)" }, { value: "easeInQuad", label: "\uAC00\uC18D\xB2 (Ease In Quad)" }, { value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" } ]; var ParameterPanel = ({ area, onUpdateArea }) => { if (!area) { return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "parameter-panel", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) }); } return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-panel", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [ "\uC65C\uACE1 \uAC15\uB3C4: ", (area.distortionStrength * 100).toFixed(0), "%" ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { type: "range", min: "0", max: "1", step: "0.01", value: area.distortionStrength, onChange: (e) => onUpdateArea({ distortionStrength: parseFloat(e.target.value) }), className: "slider" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [ "\uC9C0\uC18D \uC2DC\uAC04: ", area.movement.duration.toFixed(1), "\uCD08" ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { type: "number", min: "0.1", max: "10", step: "0.1", value: area.movement.duration, onChange: (e) => onUpdateArea({ movement: { ...area.movement, duration: parseFloat(e.target.value) } }), className: "input-number" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "\uC774\uC9D5 \uD568\uC218" }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "select", { value: area.movement.easing, onChange: (e) => onUpdateArea({ movement: { ...area.movement, easing: e.target.value } }), className: "select", children: EASING_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: option.value, children: option.label }, option.value)) } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [ "\uBCA1\uD130 X: ", area.movement.vectorA.x.toFixed(2) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { type: "range", min: "-1", max: "1", step: "0.01", value: area.movement.vectorA.x, onChange: (e) => onUpdateArea({ movement: { ...area.movement, vectorA: { ...area.movement.vectorA, x: parseFloat(e.target.value) } } }), className: "slider" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [ "\uBCA1\uD130 Y: ", area.movement.vectorA.y.toFixed(2) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( "input", { type: "range", min: "-1", max: "1", step: "0.01", value: area.movement.vectorA.y, onChange: (e) => onUpdateArea({ movement: { ...area.movement, vectorA: { ...area.movement.vectorA, y: parseFloat(e.target.value) } } }), className: "slider" } ) ] }), /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }), /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "point-coord", children: [ "P", idx + 1, ": (", point.x.toFixed(3), ", ", point.y.toFixed(3), ")" ] }, idx)) }) ] }) ] }); }; // src/editor/DistortionEditor.tsx var import_jsx_runtime5 = require("react/jsx-runtime"); var DistortionEditor = ({ initialAreas = [], imageSrc, onAreasChange, onSelectedAreaChange, width = 800, height = 600, canvasStyle }) => { const { state, selectArea, addArea, removeArea, updateArea, updatePoint, startDragging, stopDragging, getSelectedArea } = useDistortionEditor(initialAreas); (0, import_react5.useEffect)(() => { onAreasChange?.(state.areas); }, [state.areas, onAreasChange]); (0, import_react5.useEffect)(() => { onSelectedAreaChange?.(state.selectedAreaId); }, [state.selectedAreaId, onSelectedAreaChange]); const handleAddArea = () => { const newArea = { id: `area-${Date.now()}`, basePoints: [ { x: 0.3, y: 0.3 }, { x: 0.7, y: 0.3 }, { x: 0.7, y: 0.7 }, { x: 0.3, y: 0.7 } ], movement: { vectorA: { x: DEFAULT_AREA.VECTOR_A.x, y: DEFAULT_AREA.VECTOR_A.y }, vectorB: { x: DEFAULT_AREA.VECTOR_B.x, y: DEFAULT_AREA.VECTOR_B.y }, duration: DEFAULT_AREA.DURATION, easing: DEFAULT_AREA.EASING }, distortionStrength: DEFAULT_AREA.DISTORTION_STRENGTH, progress: 0, dragVector: { x: 0, y: 0 } }; addArea(newArea); }; const handleUpdateArea = (updates) => { if (state.selectedAreaId) { updateArea(state.selectedAreaId, updates); } }; const selectedArea = getSelectedArea(); return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "distortion-editor", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-main", children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "editor-canvas-container", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( EditorCanvas, { areas: state.areas, selectedAreaId: state.selectedAreaId, imageSrc, width, height, onUpdatePoint: updatePoint, onUpdateArea: updateArea, draggingPointIndex: state.draggingPointIndex, onStartDragging: startDragging, onStopDragging: stopDragging, style: canvasStyle } ) }), /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-sidebar", children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( AreaList, { areas: state.areas, selectedAreaId: state.selectedAreaId, onSelectArea: selectArea, onRemoveArea: removeArea, onAddArea: handleAddArea } ), /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ParameterPanel, { area: selectedArea, onUpdateArea: handleUpdateArea }) ] }) ] }) }); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ANIMATION_CONFIG, AnimationLoop, DEFAULT_AREA, DistortionEditor, ImageDistortion, SHADER_CONFIG, ShaderManager, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor }); //# sourceMappingURL=index.js.map