- useMouseInteraction 훅에 isDragging 함수를 추가했습니다. - ImageDistortion 컴포넌트의 애니메이션 콜백에서 마우스 드래그 상태를 감지하여, 드래그 중일 때는 자동 애니메이션의 dragVector를 0으로 설정하도록 로직을 개선했습니다.
1754 lines
61 KiB
JavaScript
1754 lines
61 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,
|
||
DistortionEditor: () => DistortionEditor,
|
||
ImageDistortion: () => ImageDistortion,
|
||
SHADER_CONFIG: () => SHADER_CONFIG,
|
||
ShaderManager: () => ShaderManager,
|
||
SpringPhysics: () => SpringPhysics,
|
||
ThreeScene: () => ThreeScene,
|
||
applyEasing: () => applyEasing,
|
||
isRotationPreset: () => isRotationPreset,
|
||
presetToVector: () => presetToVector,
|
||
useAnimationFrame: () => useAnimationFrame,
|
||
useDistortionEditor: () => useDistortionEditor,
|
||
useMouseInteraction: () => useMouseInteraction,
|
||
useMouseVelocity: () => useMouseVelocity
|
||
});
|
||
module.exports = __toCommonJS(index_exports);
|
||
|
||
// src/components/ImageDistortion.tsx
|
||
var import_react4 = 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() {
|
||
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/utils/motionPresets.ts
|
||
function presetToVector(preset, strength = 0.1) {
|
||
switch (preset) {
|
||
case "none":
|
||
return { x: 0, y: 0 };
|
||
case "horizontal":
|
||
return { x: strength, y: 0 };
|
||
case "vertical":
|
||
return { x: 0, y: strength };
|
||
case "rotate-cw":
|
||
return { x: strength, y: 0 };
|
||
case "rotate-ccw":
|
||
return { x: -strength, y: 0 };
|
||
case "pulse":
|
||
return { x: strength, y: strength };
|
||
case "diagonal-1":
|
||
return { x: strength * 0.707, y: strength * 0.707 };
|
||
// √2/2 ≈ 0.707
|
||
case "diagonal-2":
|
||
return { x: strength * 0.707, y: -strength * 0.707 };
|
||
default:
|
||
return { x: 0, y: 0 };
|
||
}
|
||
}
|
||
function isRotationPreset(preset) {
|
||
return preset === "rotate-cw" || preset === "rotate-ccw";
|
||
}
|
||
|
||
// 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 {
|
||
if (easedProgress < 0.5) {
|
||
const t = easedProgress * 2;
|
||
dragVector = {
|
||
x: baseVector.x * t,
|
||
y: baseVector.y * t
|
||
};
|
||
} else {
|
||
const t = (easedProgress - 0.5) * 2;
|
||
dragVector = {
|
||
x: baseVector.x * (1 - t),
|
||
y: baseVector.y * (1 - t)
|
||
};
|
||
}
|
||
}
|
||
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
|
||
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/hooks/useMouseInteraction.ts
|
||
var import_react3 = require("react");
|
||
|
||
// src/hooks/useMouseVelocity.ts
|
||
var import_react2 = require("react");
|
||
var useMouseVelocity = (containerRef) => {
|
||
const mouseStateRef = (0, import_react2.useRef)({
|
||
position: null,
|
||
prevPosition: null,
|
||
velocity: { x: 0, y: 0 },
|
||
acceleration: { x: 0, y: 0 },
|
||
isHovering: false,
|
||
isDragging: false
|
||
});
|
||
const lastUpdateTimeRef = (0, import_react2.useRef)(Date.now());
|
||
const prevVelocityRef = (0, import_react2.useRef)({ x: 0, y: 0 });
|
||
const toNormalized = (0, import_react2.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 = (0, import_react2.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 = (0, import_react2.useCallback)((e) => {
|
||
updatePosition(e.clientX, e.clientY);
|
||
}, [updatePosition]);
|
||
const handleMouseEnter = (0, import_react2.useCallback)(() => {
|
||
mouseStateRef.current.isHovering = true;
|
||
}, []);
|
||
const handleMouseLeave = (0, import_react2.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 = (0, import_react2.useCallback)(() => {
|
||
mouseStateRef.current.isDragging = true;
|
||
}, []);
|
||
const handleMouseUp = (0, import_react2.useCallback)(() => {
|
||
mouseStateRef.current.isDragging = false;
|
||
}, []);
|
||
const handleTouchMove = (0, import_react2.useCallback)((e) => {
|
||
e.preventDefault();
|
||
if (e.touches.length > 0) {
|
||
const touch = e.touches[0];
|
||
updatePosition(touch.clientX, touch.clientY);
|
||
}
|
||
}, [updatePosition]);
|
||
const handleTouchStart = (0, import_react2.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 = (0, import_react2.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 };
|
||
}, []);
|
||
(0, import_react2.useEffect)(() => {
|
||
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 = (0, import_react2.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] = (0, import_react3.useState)(/* @__PURE__ */ new Set());
|
||
const springPhysicsMapRef = (0, import_react3.useRef)(/* @__PURE__ */ new Map());
|
||
const getSpringPhysics = (0, import_react3.useCallback)((areaIndex) => {
|
||
if (!springPhysicsMapRef.current.has(areaIndex)) {
|
||
springPhysicsMapRef.current.set(areaIndex, new SpringPhysics(config.physics));
|
||
}
|
||
return springPhysicsMapRef.current.get(areaIndex);
|
||
}, [config.physics]);
|
||
const updateInteraction = (0, import_react3.useCallback)((areas, deltaTime) => {
|
||
if (!config.enabled) return areas;
|
||
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).reset();
|
||
}
|
||
}
|
||
}
|
||
interactingAreaIndices.forEach((areaIndex) => {
|
||
if (!currentlyInAreas.has(areaIndex)) {
|
||
getSpringPhysics(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);
|
||
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);
|
||
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 = (0, import_react3.useCallback)((newConfig) => {
|
||
const physicsConfig = newConfig.physics;
|
||
if (physicsConfig) {
|
||
springPhysicsMapRef.current.forEach((spring) => {
|
||
spring.setConfig(physicsConfig);
|
||
});
|
||
}
|
||
}, []);
|
||
const reset = (0, import_react3.useCallback)(() => {
|
||
springPhysicsMapRef.current.forEach((spring) => {
|
||
spring.reset();
|
||
});
|
||
setInteractingAreaIndices(/* @__PURE__ */ new Set());
|
||
}, []);
|
||
const isDragging = (0, import_react3.useCallback)(() => {
|
||
const mouseState = getState();
|
||
return mouseState.isDragging;
|
||
}, [getState]);
|
||
return {
|
||
updateInteraction,
|
||
updateConfig,
|
||
reset,
|
||
isDragging
|
||
};
|
||
};
|
||
|
||
// 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,
|
||
mouseInteraction
|
||
}) => {
|
||
const containerRef = (0, import_react4.useRef)(null);
|
||
const sceneRef = (0, import_react4.useRef)(null);
|
||
const shaderManagerRef = (0, import_react4.useRef)(new ShaderManager());
|
||
const textureRef = (0, import_react4.useRef)(null);
|
||
const [isReady, setIsReady] = (0, import_react4.useState)(false);
|
||
const [imageLoaded, setImageLoaded] = (0, import_react4.useState)(false);
|
||
const [currentAreas, setCurrentAreas] = (0, import_react4.useState)(areas);
|
||
const mouseInteractionHook = useMouseInteraction(
|
||
containerRef,
|
||
mouseInteraction || {
|
||
enabled: false,
|
||
physics: {
|
||
stiffness: 100,
|
||
damping: 10,
|
||
mass: 1,
|
||
influenceRadius: 0.2,
|
||
maxStrength: 1
|
||
}
|
||
}
|
||
);
|
||
(0, import_react4.useEffect)(() => {
|
||
setCurrentAreas(areas);
|
||
}, [areas]);
|
||
(0, import_react4.useEffect)(() => {
|
||
if (mouseInteraction) {
|
||
mouseInteractionHook.updateConfig(mouseInteraction);
|
||
}
|
||
}, [mouseInteraction, mouseInteractionHook]);
|
||
(0, import_react4.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_react4.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_react4.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_react4.useCallback)((deltaTime) => {
|
||
if (!isReady) return;
|
||
setCurrentAreas((prevAreas) => {
|
||
const isDragging = mouseInteractionHook.isDragging?.();
|
||
let updatedAreas = prevAreas;
|
||
if (!isDragging) {
|
||
updatedAreas = AnimationLoop.updateProgress(prevAreas, deltaTime);
|
||
updatedAreas = AnimationLoop.updateAreaDragVectors(updatedAreas);
|
||
} else {
|
||
updatedAreas = prevAreas.map((area) => ({
|
||
...area,
|
||
dragVector: { x: 0, y: 0 }
|
||
}));
|
||
}
|
||
if (mouseInteraction?.enabled) {
|
||
updatedAreas = mouseInteractionHook.updateInteraction(updatedAreas, deltaTime);
|
||
}
|
||
return updatedAreas;
|
||
});
|
||
}, [isReady, mouseInteraction, mouseInteractionHook]);
|
||
useAnimationFrame(animationCallback, isPlaying || mouseInteraction?.enabled || false);
|
||
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_react7 = require("react");
|
||
|
||
// src/editor/hooks/useDistortionEditor.ts
|
||
var import_react5 = require("react");
|
||
var useDistortionEditor = (initialAreas = []) => {
|
||
const [state, setState] = (0, import_react5.useState)({
|
||
selectedAreaId: initialAreas[0]?.id || null,
|
||
areas: initialAreas,
|
||
editMode: "normal",
|
||
draggingPointIndex: null
|
||
});
|
||
const selectArea = (0, import_react5.useCallback)((areaId) => {
|
||
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
||
}, []);
|
||
const addArea = (0, import_react5.useCallback)((area) => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
areas: [...prev.areas, area],
|
||
selectedAreaId: area.id
|
||
}));
|
||
}, []);
|
||
const removeArea = (0, import_react5.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_react5.useCallback)((areaId, updates) => {
|
||
setState((prev) => ({
|
||
...prev,
|
||
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
|
||
}));
|
||
}, []);
|
||
const updatePoint = (0, import_react5.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_react5.useCallback)((pointIndex) => {
|
||
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
|
||
}, []);
|
||
const stopDragging = (0, import_react5.useCallback)(() => {
|
||
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
||
}, []);
|
||
const setEditMode = (0, import_react5.useCallback)((mode) => {
|
||
setState((prev) => ({ ...prev, editMode: mode }));
|
||
}, []);
|
||
const getSelectedArea = (0, import_react5.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_react6 = require("react");
|
||
var import_jsx_runtime2 = require("react/jsx-runtime");
|
||
var EditorCanvas = ({
|
||
areas,
|
||
selectedAreaId,
|
||
imageSrc,
|
||
width,
|
||
height,
|
||
onUpdatePoint,
|
||
onUpdateArea,
|
||
draggingPointIndex,
|
||
onStartDragging,
|
||
onStopDragging,
|
||
style: customStyle,
|
||
showEditor = true
|
||
}) => {
|
||
const containerRef = (0, import_react6.useRef)(null);
|
||
const [canvasSize, setCanvasSize] = (0, import_react6.useState)({ width: 0, height: 0 });
|
||
const [isDraggingArea, setIsDraggingArea] = (0, import_react6.useState)(false);
|
||
const [dragStartPos, setDragStartPos] = (0, import_react6.useState)(null);
|
||
const editorStyle = (0, import_react6.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_react6.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 isPointInPolygon2 = (0, import_react6.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 handlePointDown = (0, import_react6.useCallback)(
|
||
(pointIndex) => (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
onStartDragging(pointIndex);
|
||
},
|
||
[onStartDragging]
|
||
);
|
||
const handleCanvasDown = (0, import_react6.useCallback)(
|
||
(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 = (0, import_react6.useCallback)(
|
||
(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 = (0, import_react6.useCallback)(() => {
|
||
if (draggingPointIndex !== null) {
|
||
onStopDragging();
|
||
}
|
||
if (isDraggingArea) {
|
||
setIsDraggingArea(false);
|
||
setDragStartPos(null);
|
||
}
|
||
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
||
(0, import_react6.useEffect)(() => {
|
||
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 = (0, import_react6.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: 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__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas }),
|
||
showEditor && /* @__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);
|
||
})
|
||
}
|
||
),
|
||
showEditor && 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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
),
|
||
showEditor && 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: handlePointDown(index),
|
||
onTouchStart: handlePointDown(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);
|
||
const [showEditor, setShowEditor] = (0, import_react7.useState)(true);
|
||
(0, import_react7.useEffect)(() => {
|
||
onAreasChange?.(state.areas);
|
||
}, [state.areas, onAreasChange]);
|
||
(0, import_react7.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.jsxs)("div", { className: "distortion-editor", children: [
|
||
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "editor-toolbar", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
||
"button",
|
||
{
|
||
className: `editor-toggle-btn ${showEditor ? "active" : ""}`,
|
||
onClick: () => setShowEditor(!showEditor),
|
||
title: showEditor ? "\uC5D0\uB514\uD130 \uC228\uAE30\uAE30 (\uC65C\uACE1 \uD6A8\uACFC\uB9CC \uBCF4\uAE30)" : "\uC5D0\uB514\uD130 \uD45C\uC2DC",
|
||
children: showEditor ? "\u{1F441}\uFE0F \uC5D0\uB514\uD130 \uC228\uAE30\uAE30" : "\u270F\uFE0F \uC5D0\uB514\uD130 \uD45C\uC2DC"
|
||
}
|
||
) }),
|
||
/* @__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,
|
||
showEditor
|
||
}
|
||
) }),
|
||
/* @__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 })
|
||
] })
|
||
] })
|
||
] });
|
||
};
|
||
|
||
// 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)"
|
||
// 선택 안된 영역 배경 (연한 회색)
|
||
}
|
||
};
|
||
// Annotate the CommonJS export names for ESM import in node:
|
||
0 && (module.exports = {
|
||
ANIMATION_CONFIG,
|
||
AnimationLoop,
|
||
DEFAULT_AREA,
|
||
DistortionEditor,
|
||
ImageDistortion,
|
||
SHADER_CONFIG,
|
||
ShaderManager,
|
||
SpringPhysics,
|
||
ThreeScene,
|
||
applyEasing,
|
||
isRotationPreset,
|
||
presetToVector,
|
||
useAnimationFrame,
|
||
useDistortionEditor,
|
||
useMouseInteraction,
|
||
useMouseVelocity
|
||
});
|
||
//# sourceMappingURL=index.js.map
|