// src/components/ImageDistortion.tsx import { useEffect as useEffect3, useRef as useRef4, useState as useState2, useCallback as useCallback3 } 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) { 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() { 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), easeInCubic: (t) => t * t * t, easeOutCubic: (t) => 1 - Math.pow(1 - t, 3) }; var applyEasing = (progress, easingType) => { const clampedProgress = Math.max(0, Math.min(1, progress)); return easingFunctions[easingType](clampedProgress); }; // src/utils/motionPresets.ts var presetRegistry = /* @__PURE__ */ new Map(); var rotationPresets = /* @__PURE__ */ new Set(["rotate-cw", "rotate-ccw"]); var BUILT_IN_PRESETS = { "none": () => ({ x: 0, y: 0 }), "horizontal": (strength) => ({ x: strength, y: 0 }), "vertical": (strength) => ({ x: 0, y: strength }), "rotate-cw": (strength) => ({ x: strength, y: 0 }), "rotate-ccw": (strength) => ({ x: -strength, y: 0 }), "pulse": (strength) => ({ x: strength, y: strength }), "diagonal-1": (strength) => ({ x: strength * 0.707, y: strength * 0.707 }), "diagonal-2": (strength) => ({ x: strength * 0.707, y: -strength * 0.707 }) }; Object.entries(BUILT_IN_PRESETS).forEach(([name, definition]) => { presetRegistry.set(name, definition); }); function registerMotionPreset(name, definition, options) { presetRegistry.set(name, definition); if (options?.isRotation) { rotationPresets.add(name); } else { rotationPresets.delete(name); } } function registerMotionPresets(presets, rotationPresetNames) { Object.entries(presets).forEach(([name, definition]) => { presetRegistry.set(name, definition); }); rotationPresetNames?.forEach((name) => rotationPresets.add(name)); } function unregisterMotionPreset(name) { rotationPresets.delete(name); return presetRegistry.delete(name); } function getRegisteredPresets() { return Array.from(presetRegistry.keys()); } function hasPreset(name) { return presetRegistry.has(name); } function resetToBuiltInPresets() { presetRegistry.clear(); rotationPresets.clear(); Object.entries(BUILT_IN_PRESETS).forEach(([name, definition]) => { presetRegistry.set(name, definition); }); rotationPresets.add("rotate-cw"); rotationPresets.add("rotate-ccw"); } function presetToVector(preset, strength = 0.1) { const definition = presetRegistry.get(preset); if (definition) { return definition(strength); } console.warn(`Unknown motion preset: "${preset}". Falling back to "none".`); return { x: 0, y: 0 }; } function isRotationPreset(preset) { if (!preset) return false; return rotationPresets.has(preset); } // src/engine/AnimationLoop.ts var AnimationLoop = class { /** * 영역들의 드래그 벡터를 현재 진행도에 따라 업데이트 * @param areas 왜곡 영역 배열 * @returns 업데이트된 영역 배열 */ static updateAreaDragVectors(areas) { return areas.map((area) => { const { progress, movement } = area; if (movement.duration <= 0 || movement.preset === "none") { return { ...area, dragVector: { x: 0, y: 0 } }; } let baseVector; if (movement.preset) { const strength = movement.strength ?? 0.1; baseVector = presetToVector(movement.preset, strength); } else { baseVector = movement.vectorA; } const easedProgress = applyEasing(progress, movement.easing); let dragVector; if (movement.preset && isRotationPreset(movement.preset)) { const angle = easedProgress * Math.PI * 2; const radius = Math.sqrt(baseVector.x * baseVector.x + baseVector.y * baseVector.y); const direction = movement.preset === "rotate-cw" ? 1 : -1; dragVector = { x: Math.cos(angle * direction) * radius, y: Math.sin(angle * direction) * radius }; } else { const oscillation = Math.sin(easedProgress * Math.PI * 2); dragVector = { x: baseVector.x * oscillation, y: baseVector.y * oscillation }; } return { ...area, dragVector }; }); } /** * 모든 영역의 진행도를 델타 타임만큼 업데이트 * @param areas 왜곡 영역 배열 * @param deltaTime 델타 타임 (초) * @returns 업데이트된 영역 배열 */ static updateProgress(areas, deltaTime) { return areas.map((area) => { if (area.movement.duration <= 0) { return 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/hooks/useMouseInteraction.ts import { useRef as useRef3, useCallback as useCallback2, useState } from "react"; // src/hooks/useMouseVelocity.ts import { useRef as useRef2, useCallback, useEffect as useEffect2 } from "react"; var useMouseVelocity = (containerRef) => { const mouseStateRef = useRef2({ position: null, prevPosition: null, velocity: { x: 0, y: 0 }, acceleration: { x: 0, y: 0 }, isHovering: false, isDragging: false }); const lastUpdateTimeRef = useRef2(Date.now()); const prevVelocityRef = useRef2({ x: 0, y: 0 }); const toNormalized = useCallback((clientX, clientY) => { if (!containerRef.current) return null; const rect = containerRef.current.getBoundingClientRect(); return { x: (clientX - rect.left) / rect.width, y: (clientY - rect.top) / rect.height }; }, [containerRef]); const updatePosition = useCallback((clientX, clientY) => { const now = Date.now(); const deltaTime = (now - lastUpdateTimeRef.current) / 1e3; lastUpdateTimeRef.current = now; const normalizedPos = toNormalized(clientX, clientY); if (!normalizedPos) return; const state = mouseStateRef.current; const prevPos = state.position; let velocity = { x: 0, y: 0 }; if (prevPos && deltaTime > 0) { velocity = { x: (normalizedPos.x - prevPos.x) / deltaTime, y: (normalizedPos.y - prevPos.y) / deltaTime }; } const prevVel = prevVelocityRef.current; let acceleration = { x: 0, y: 0 }; if (deltaTime > 0) { acceleration = { x: (velocity.x - prevVel.x) / deltaTime, y: (velocity.y - prevVel.y) / deltaTime }; } mouseStateRef.current = { position: normalizedPos, prevPosition: prevPos, velocity, acceleration, isHovering: true, isDragging: state.isDragging }; prevVelocityRef.current = velocity; }, [toNormalized]); const handleMouseMove = useCallback((e) => { updatePosition(e.clientX, e.clientY); }, [updatePosition]); const handleMouseEnter = useCallback(() => { mouseStateRef.current.isHovering = true; }, []); const handleMouseLeave = useCallback(() => { mouseStateRef.current = { position: null, prevPosition: null, velocity: { x: 0, y: 0 }, acceleration: { x: 0, y: 0 }, isHovering: false, isDragging: false }; prevVelocityRef.current = { x: 0, y: 0 }; }, []); const handleMouseDown = useCallback(() => { mouseStateRef.current.isDragging = true; }, []); const handleMouseUp = useCallback(() => { mouseStateRef.current.isDragging = false; }, []); const handleTouchMove = useCallback((e) => { e.preventDefault(); if (e.touches.length > 0) { const touch = e.touches[0]; updatePosition(touch.clientX, touch.clientY); } }, [updatePosition]); const handleTouchStart = useCallback((e) => { e.preventDefault(); mouseStateRef.current.isDragging = true; mouseStateRef.current.isHovering = true; if (e.touches.length > 0) { const touch = e.touches[0]; updatePosition(touch.clientX, touch.clientY); } }, [updatePosition]); const handleTouchEnd = useCallback(() => { mouseStateRef.current.isDragging = false; mouseStateRef.current.isHovering = false; mouseStateRef.current.position = null; mouseStateRef.current.prevPosition = null; mouseStateRef.current.velocity = { x: 0, y: 0 }; mouseStateRef.current.acceleration = { x: 0, y: 0 }; prevVelocityRef.current = { x: 0, y: 0 }; }, []); useEffect2(() => { const container = containerRef.current; if (!container) return; container.addEventListener("mousemove", handleMouseMove); container.addEventListener("mouseenter", handleMouseEnter); container.addEventListener("mouseleave", handleMouseLeave); container.addEventListener("mousedown", handleMouseDown); window.addEventListener("mouseup", handleMouseUp); container.addEventListener("touchmove", handleTouchMove, { passive: false }); container.addEventListener("touchstart", handleTouchStart, { passive: false }); container.addEventListener("touchend", handleTouchEnd); container.addEventListener("touchcancel", handleTouchEnd); return () => { container.removeEventListener("mousemove", handleMouseMove); container.removeEventListener("mouseenter", handleMouseEnter); container.removeEventListener("mouseleave", handleMouseLeave); container.removeEventListener("mousedown", handleMouseDown); window.removeEventListener("mouseup", handleMouseUp); container.removeEventListener("touchmove", handleTouchMove); container.removeEventListener("touchstart", handleTouchStart); container.removeEventListener("touchend", handleTouchEnd); container.removeEventListener("touchcancel", handleTouchEnd); }; }, [containerRef, handleMouseMove, handleMouseEnter, handleMouseLeave, handleMouseDown, handleMouseUp, handleTouchMove, handleTouchStart, handleTouchEnd]); const getState = useCallback(() => { return { ...mouseStateRef.current }; }, []); return { getState }; }; // src/engine/SpringPhysics.ts var SpringPhysics = class { constructor(config) { this.config = config; this.state = { displacement: { x: 0, y: 0 }, velocity: { x: 0, y: 0 }, target: { x: 0, y: 0 } }; } /** * 물리 파라미터 업데이트 */ setConfig(config) { this.config = { ...this.config, ...config }; } /** * 목표 위치 설정 (마우스 속도 기반) */ setTarget(velocity, velocityMultiplier = 1) { this.state.target = { x: velocity.x * velocityMultiplier, y: velocity.y * velocityMultiplier }; } /** * 초기 속도 설정 (드래그 방향과 속도를 즉시 반영) * 드래그 방향으로 즉시 튕기는 효과 */ setInitialVelocity(velocity, multiplier = 1) { this.state.velocity = { x: velocity.x * multiplier, y: velocity.y * multiplier }; this.state.target = { x: 0, y: 0 }; } /** * 스프링 물리 업데이트 (Hooke's Law + Damping) * F = -k * x - c * v * a = F / m * v += a * dt * x += v * dt */ update(deltaTime) { const { stiffness, damping, mass } = this.config; const { displacement, velocity, target } = this.state; const dx = displacement.x - target.x; const dy = displacement.y - target.y; const springForceX = -stiffness * dx; const springForceY = -stiffness * dy; const dampingForceX = -damping * velocity.x; const dampingForceY = -damping * velocity.y; const totalForceX = springForceX + dampingForceX; const totalForceY = springForceY + dampingForceY; const accelerationX = totalForceX / mass; const accelerationY = totalForceY / mass; const newVelocityX = velocity.x + accelerationX * deltaTime; const newVelocityY = velocity.y + accelerationY * deltaTime; const newDisplacementX = displacement.x + newVelocityX * deltaTime; const newDisplacementY = displacement.y + newVelocityY * deltaTime; this.state = { displacement: { x: newDisplacementX, y: newDisplacementY }, velocity: { x: newVelocityX, y: newVelocityY }, target }; const isNearlyZero = (val) => Math.abs(val) < 1e-4; if (isNearlyZero(this.state.displacement.x) && isNearlyZero(this.state.displacement.y) && isNearlyZero(this.state.velocity.x) && isNearlyZero(this.state.velocity.y)) { this.reset(); } return this.state.displacement; } /** * 즉시 충격 적용 (마우스 가속도 기반) */ applyImpulse(acceleration, multiplier = 1) { this.state.velocity.x += acceleration.x * multiplier; this.state.velocity.y += acceleration.y * multiplier; } /** * 현재 변위 가져오기 */ getDisplacement() { return { ...this.state.displacement }; } /** * 현재 속도 가져오기 */ getVelocity() { return { ...this.state.velocity }; } /** * 상태 리셋 */ reset() { this.state = { displacement: { x: 0, y: 0 }, velocity: { x: 0, y: 0 }, target: { x: 0, y: 0 } }; } /** * 마우스가 멈췄을 때 목표를 0으로 설정 (평형 상태로 복귀) */ returnToEquilibrium() { this.state.target = { x: 0, y: 0 }; } }; // src/hooks/useMouseInteraction.ts var isPointInPolygon = (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; }; var useMouseInteraction = (containerRef, config) => { const { getState } = useMouseVelocity(containerRef); const [interactingAreaIndices, setInteractingAreaIndices] = useState(/* @__PURE__ */ new Set()); const springPhysicsMapRef = useRef3(/* @__PURE__ */ new Map()); const getSpringPhysics = useCallback2((areaIndex, area) => { if (!springPhysicsMapRef.current.has(areaIndex)) { const physicsConfig = area?.physics || config.physics; springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(physicsConfig)); } return springPhysicsMapRef.current.get(areaIndex); }, [config.physics]); const updateInteraction = useCallback2((areas, deltaTime) => { if (!config.enabled) return areas; areas.forEach((area, index) => { if (area.physics && springPhysicsMapRef.current.has(index)) { const spring = springPhysicsMapRef.current.get(index); spring.setConfig(area.physics); } }); const mouseState = getState(); if (mouseState.isDragging && mouseState.position) { const currentlyInAreas = /* @__PURE__ */ new Set(); for (let i = 0; i < areas.length; i++) { if (isPointInPolygon(mouseState.position, areas[i].basePoints)) { currentlyInAreas.add(i); if (!interactingAreaIndices.has(i)) { getSpringPhysics(i, areas[i]).reset(); } } } interactingAreaIndices.forEach((areaIndex) => { if (!currentlyInAreas.has(areaIndex)) { getSpringPhysics(areaIndex, areas[areaIndex]).returnToEquilibrium(); } }); setInteractingAreaIndices(currentlyInAreas); const velocityMult = config.velocityMultiplier || 1; const velocityMag = Math.sqrt( mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 ); const minVel = config.minVelocity || 0.05; const maxVel = config.maxVelocity || 5; let clampedVelocity = mouseState.velocity; if (velocityMag > maxVel) { const scale = maxVel / velocityMag; clampedVelocity = { x: mouseState.velocity.x * scale, y: mouseState.velocity.y * scale }; } currentlyInAreas.forEach((areaIndex) => { const spring = getSpringPhysics(areaIndex, areas[areaIndex]); if (velocityMag >= minVel) { spring.setTarget(clampedVelocity, velocityMult); } else { spring.returnToEquilibrium(); } }); } else { if (interactingAreaIndices.size > 0) { const velocityMult = config.velocityMultiplier || 1; const maxVel = config.maxVelocity || 5; const velocityMag = Math.sqrt( mouseState.velocity.x ** 2 + mouseState.velocity.y ** 2 ); let clampedVelocity = mouseState.velocity; if (velocityMag > maxVel) { const scale = maxVel / velocityMag; clampedVelocity = { x: mouseState.velocity.x * scale, y: mouseState.velocity.y * scale }; } interactingAreaIndices.forEach((areaIndex) => { const spring = getSpringPhysics(areaIndex, areas[areaIndex]); spring.setInitialVelocity(clampedVelocity, velocityMult); }); setInteractingAreaIndices(/* @__PURE__ */ new Set()); } } return areas.map((area, index) => { const spring = springPhysicsMapRef.current.get(index); if (!spring) return area; const springVelocity = spring.getVelocity(); const springDisplacement = spring.getDisplacement(); const isSpringActive = Math.sqrt(springVelocity.x ** 2 + springVelocity.y ** 2) > 1e-3 || Math.sqrt(springDisplacement.x ** 2 + springDisplacement.y ** 2) > 1e-3; if (!interactingAreaIndices.has(index) && !isSpringActive) { return area; } const displacement = spring.update(deltaTime); const displacementMag = Math.sqrt(displacement.x ** 2 + displacement.y ** 2); if (displacementMag < 1e-3) { return area; } return { ...area, dragVector: { x: area.dragVector.x - displacement.x, y: area.dragVector.y - displacement.y } }; }); }, [config, getState, interactingAreaIndices, getSpringPhysics]); const updateConfig = useCallback2((newConfig) => { const physicsConfig = newConfig.physics; if (physicsConfig) { springPhysicsMapRef.current.forEach((spring) => { spring.setConfig(physicsConfig); }); } }, []); const reset = useCallback2(() => { springPhysicsMapRef.current.forEach((spring) => { spring.reset(); }); setInteractingAreaIndices(/* @__PURE__ */ new Set()); }, []); const isDragging = useCallback2(() => { const mouseState = getState(); return mouseState.isDragging; }, [getState]); const getInteractingAreaIndices = useCallback2(() => { return interactingAreaIndices; }, [interactingAreaIndices]); return { updateInteraction, updateConfig, reset, isDragging, getInteractingAreaIndices }; }; // 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, mouseInteraction }) => { const containerRef = useRef4(null); const sceneRef = useRef4(null); const shaderManagerRef = useRef4(new ShaderManager()); const textureRef = useRef4(null); const [isReady, setIsReady] = useState2(false); const [imageLoaded, setImageLoaded] = useState2(false); const [currentAreas, setCurrentAreas] = useState2(areas); const mouseInteractionHook = useMouseInteraction( containerRef, mouseInteraction || { enabled: false, physics: { stiffness: 100, damping: 10, mass: 1, influenceRadius: 0.2, maxStrength: 1 } } ); useEffect3(() => { setCurrentAreas(areas); }, [areas]); useEffect3(() => { if (mouseInteraction) { mouseInteractionHook.updateConfig(mouseInteraction); } }, [mouseInteraction, mouseInteractionHook]); useEffect3(() => { 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]); useEffect3(() => { 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]); useEffect3(() => { 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 = useCallback3((deltaTime) => { if (!isReady) return; setCurrentAreas((prevAreas) => { const interactingIndices = mouseInteractionHook.getInteractingAreaIndices?.() || /* @__PURE__ */ new Set(); let updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime); updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas); if (interactingIndices.size > 0) { updatedAreas = updatedAreas.map((area, index) => { if (interactingIndices.has(index)) { return { ...area, dragVector: { x: 0, y: 0 } }; } return area; }); } if (mouseInteraction?.enabled) { updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime); } return updatedAreas; }); }, [isReady, mouseInteraction, mouseInteractionHook]); useAnimationFrame(animationCallback, isPlaying || mouseInteraction?.enabled || false); return /* @__PURE__ */ jsx( "div", { ref: containerRef, style: { width: "100%", height: "100%", position: "relative", ...style }, className, children: !imageLoaded && /* @__PURE__ */ 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/components/EditorCanvas.tsx import { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react"; // src/editor/components/AreaList.tsx import { jsx as jsx2, jsxs } from "react/jsx-runtime"; var AreaList = ({ areas, selectedAreaId, onSelectArea, onRemoveArea, onAddArea }) => { return /* @__PURE__ */ jsxs("div", { className: "area-list", children: [ /* @__PURE__ */ jsxs("div", { className: "area-list-header", children: [ /* @__PURE__ */ jsx2("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }), /* @__PURE__ */ jsx2( "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__ */ jsx2("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ jsx2("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__ */ jsxs( "div", { className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`, onClick: () => onSelectArea(area.id), children: [ /* @__PURE__ */ jsxs("div", { className: "area-item-info", children: [ /* @__PURE__ */ jsxs("span", { className: "area-item-name", children: [ "\uC601\uC5ED ", index + 1 ] }), /* @__PURE__ */ jsxs("span", { className: "area-item-strength", children: [ "\uAC15\uB3C4: ", (area.distortionStrength * 100).toFixed(0), "%" ] }) ] }), /* @__PURE__ */ jsx2( "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 import { jsx as jsx3, jsxs as jsxs2 } from "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__ */ jsx3("div", { className: "parameter-panel", children: /* @__PURE__ */ jsx3("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) }); } return /* @__PURE__ */ jsxs2("div", { className: "parameter-panel", children: [ /* @__PURE__ */ jsx3("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }), /* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [ /* @__PURE__ */ jsxs2("label", { children: [ "\uC65C\uACE1 \uAC15\uB3C4: ", (area.distortionStrength * 100).toFixed(0), "%" ] }), /* @__PURE__ */ jsx3( "input", { type: "range", min: "0", max: "1", step: "0.01", value: area.distortionStrength, onChange: (e) => onUpdateArea({ distortionStrength: parseFloat(e.target.value) }), className: "slider" } ) ] }), /* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [ /* @__PURE__ */ jsxs2("label", { children: [ "\uC9C0\uC18D \uC2DC\uAC04: ", area.movement.duration.toFixed(1), "\uCD08" ] }), /* @__PURE__ */ jsx3( "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__ */ jsxs2("div", { className: "parameter-group", children: [ /* @__PURE__ */ jsx3("label", { children: "\uC774\uC9D5 \uD568\uC218" }), /* @__PURE__ */ jsx3( "select", { value: area.movement.easing, onChange: (e) => onUpdateArea({ movement: { ...area.movement, easing: e.target.value } }), className: "select", children: EASING_OPTIONS.map((option) => /* @__PURE__ */ jsx3("option", { value: option.value, children: option.label }, option.value)) } ) ] }), /* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [ /* @__PURE__ */ jsx3("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }), /* @__PURE__ */ jsx3("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ jsxs2("div", { className: "point-coord", children: [ "P", idx + 1, ": (", point.x.toFixed(3), ", ", point.y.toFixed(3), ")" ] }, idx)) }) ] }) ] }); }; // src/editor/hooks/useDistortionEditor.ts import { useState as useState3, useCallback as useCallback4 } from "react"; var useDistortionEditor = (initialAreas = []) => { const [state, setState] = useState3({ selectedAreaId: initialAreas[0]?.id || null, areas: initialAreas, editMode: "normal", draggingPointIndex: null }); const selectArea = useCallback4((areaId) => { setState((prev) => ({ ...prev, selectedAreaId: areaId })); }, []); const addArea = useCallback4((area) => { setState((prev) => ({ ...prev, areas: [...prev.areas, area], selectedAreaId: area.id })); }, []); const removeArea = useCallback4((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 = useCallback4((areaId, updates) => { setState((prev) => ({ ...prev, areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area) })); }, []); const updatePoint = useCallback4((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 = useCallback4((pointIndex) => { setState((prev) => ({ ...prev, draggingPointIndex: pointIndex })); }, []); const stopDragging = useCallback4(() => { setState((prev) => ({ ...prev, draggingPointIndex: null })); }, []); const setEditMode = useCallback4((mode) => { setState((prev) => ({ ...prev, editMode: mode })); }, []); const getSelectedArea = useCallback4(() => { 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/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 import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime"; var EditorCanvas = ({ areas, selectedAreaId, imageSrc, width, height, onUpdatePoint, onUpdateArea, draggingPointIndex, onStartDragging, onStopDragging, style: customStyle, showEditor = true }) => { const containerRef = useRef5(null); const [canvasSize, setCanvasSize] = useState4({ width: 0, height: 0 }); const [isDraggingArea, setIsDraggingArea] = useState4(false); const [dragStartPos, setDragStartPos] = useState4(null); const editorStyle = 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]); useEffect4(() => { 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 isPointInPolygon2 = useCallback5((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 handlePointDown = useCallback5( (pointIndex) => (e) => { e.preventDefault(); e.stopPropagation(); onStartDragging(pointIndex); }, [onStartDragging] ); const handleCanvasDown = useCallback5( (e) => { if (!showEditor || !selectedArea || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); let clientX, clientY; if ("touches" in e) { if (e.touches.length === 0) return; clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const x = (clientX - rect.left) / rect.width; const y = (clientY - rect.top) / rect.height; const clickPoint = { x, y }; if (isPointInPolygon2(clickPoint, selectedArea.basePoints)) { setIsDraggingArea(true); setDragStartPos(clickPoint); e.preventDefault(); } }, [showEditor, selectedArea, isPointInPolygon2] ); const handleMove = useCallback5( (e) => { if (!showEditor || !selectedArea || !containerRef.current) return; if ("touches" in e && (draggingPointIndex !== null || isDraggingArea)) { e.preventDefault(); } const rect = containerRef.current.getBoundingClientRect(); let clientX, clientY; if ("touches" in e) { if (e.touches.length === 0) return; clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const x = (clientX - rect.left) / rect.width; const y = (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 }); } }, [showEditor, draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea] ); const handleUp = useCallback5(() => { if (draggingPointIndex !== null) { onStopDragging(); } if (isDraggingArea) { setIsDraggingArea(false); setDragStartPos(null); } }, [draggingPointIndex, isDraggingArea, onStopDragging]); useEffect4(() => { if (draggingPointIndex !== null || isDraggingArea) { window.addEventListener("mouseup", handleUp); window.addEventListener("touchend", handleUp); window.addEventListener("touchcancel", handleUp); return () => { window.removeEventListener("mouseup", handleUp); window.removeEventListener("touchend", handleUp); window.removeEventListener("touchcancel", handleUp); }; } }, [draggingPointIndex, isDraggingArea, handleUp]); 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 = useCallback5((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__ */ jsxs3( "div", { ref: containerRef, className: "editor-canvas", style: { width, height, position: "relative", cursor: showEditor ? getCursorStyle() : "default", pointerEvents: showEditor ? "auto" : "none", touchAction: "none" // 터치 시 모든 브라우저 동작 비활성화 (스크롤, 줌 등) }, onMouseDown: showEditor ? handleCanvasDown : void 0, onMouseMove: showEditor ? handleMove : void 0, onTouchStart: showEditor ? handleCanvasDown : void 0, onTouchMove: showEditor ? handleMove : void 0, children: [ /* @__PURE__ */ jsx4(ImageDistortion, { imageSrc, areas }), showEditor && /* @__PURE__ */ jsx4( "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__ */ jsx4("g", { children: /* @__PURE__ */ jsx4( "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); }) } ), showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ jsx4( "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); } } } } ), showEditor && selectedArea && selectedArea.basePoints.map((point, index) => { const handleStyle = editorStyle.pointHandle || {}; return /* @__PURE__ */ jsx4( "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: handlePointDown(index), onTouchStart: handlePointDown(index), children: /* @__PURE__ */ jsxs3( "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 ); }) ] } ); }; export { ANIMATION_CONFIG, AnimationLoop, AreaList, DEFAULT_AREA, DEFAULT_EDITOR_CANVAS_STYLE, EditorCanvas, ImageDistortion, ParameterPanel, SHADER_CONFIG, ShaderManager, SpringPhysics, ThreeScene, applyEasing, getRegisteredPresets, hasPreset, isRotationPreset, presetToVector, registerMotionPreset, registerMotionPresets, resetToBuiltInPresets, unregisterMotionPreset, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity }; //# sourceMappingURL=index.mjs.map