build: Update compiled distribution files

- Update CJS and ESM bundles with coordinate system fixes
- Update type definitions (d.ts, d.mts)
- Add editor styles (CSS)
- Update shader files
- Update source maps
This commit is contained in:
BaekRyang 2025-11-05 11:20:53 +09:00
parent f080693d32
commit e66b078dd8
9 changed files with 1721 additions and 17 deletions

View File

@ -1,8 +1,8 @@
uniform vec2 u_resolution; uniform vec2 u_resolution;
uniform sampler2D u_texture; uniform sampler2D u_texture;
uniform vec2 u_points[32]; // 최대 8영역 × 4포인트 (정규화된 좌표) uniform vec2 u_points[32]; // 최대 8영역 × 4포인트 (정규화된 좌표 0-1)
uniform int u_numAreas; uniform int u_numAreas;
uniform vec2 u_dragVectors[8]; // 정규화된 좌표 uniform vec2 u_dragVectors[8]; // 드래그 벡터 (정규화된 좌표 0-1)
uniform float u_distortionStrengths[8]; uniform float u_distortionStrengths[8];
varying vec2 vUv; varying vec2 vUv;
@ -72,10 +72,9 @@ void main() {
if (distToCenter < maxUvRadius) { if (distToCenter < maxUvRadius) {
float influence = 1.0 - smoothstep(0.0, maxUvRadius, distToCenter); float influence = 1.0 - smoothstep(0.0, maxUvRadius, distToCenter);
// dragVector도 정규화된 좌표이므로 픽셀로 변환 // dragVector는 정규화된 좌표(0-1)이므로 바로 사용 (Flutter와 동일한 결과)
vec2 distortion = (u_dragVectors[i] * u_resolution) * influence * u_distortionStrengths[i]; vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
// texCoord는 이미 정규화된 좌표이므로 정규화된 왜곡 적용 texCoord += distortion;
texCoord += distortion / u_resolution;
texCoord = clamp(texCoord, 0.0, 1.0); texCoord = clamp(texCoord, 0.0, 1.0);
} }
found = true; found = true;

298
dist/index.css vendored Normal file
View File

@ -0,0 +1,298 @@
/* src/editor/editor.css */
.distortion-editor {
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
background: #1e1e1e;
color: #e0e0e0;
min-height: 100vh;
padding: 20px;
}
.editor-main {
display: flex;
gap: 20px;
max-width: 1600px;
margin: 0 auto;
}
.editor-canvas-container {
flex: 1;
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.editor-canvas {
position: relative;
background: #000;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.editor-sidebar {
width: 320px;
display: flex;
flex-direction: column;
gap: 20px;
}
.area-list {
background: #2a2a2a;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.area-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.area-list-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #fff;
}
.btn-add {
padding: 6px 12px;
background: #00aaff;
color: white;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-add:hover:not(:disabled) {
background: #0088cc;
}
.btn-add:disabled {
background: #555;
cursor: not-allowed;
opacity: 0.5;
}
.area-list-items {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 300px;
overflow-y: auto;
}
.area-list-empty {
text-align: center;
color: #888;
padding: 20px;
font-size: 13px;
}
.area-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: #383838;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.area-item:hover {
background: #404040;
}
.area-item.selected {
background: #2d5a7a;
border-color: #00aaff;
}
.area-item-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.area-item-name {
font-size: 14px;
font-weight: 500;
color: #fff;
}
.area-item-strength {
font-size: 12px;
color: #aaa;
}
.btn-remove {
width: 24px;
height: 24px;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
font-size: 18px;
line-height: 1;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-remove:hover {
background: #cc0000;
}
.parameter-panel {
background: #2a2a2a;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
flex: 1;
overflow-y: auto;
}
.parameter-panel h3 {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #fff;
}
.parameter-panel-empty {
text-align: center;
color: #888;
padding: 40px 20px;
font-size: 13px;
}
.parameter-group {
margin-bottom: 20px;
}
.parameter-group label {
display: block;
font-size: 13px;
font-weight: 500;
color: #ccc;
margin-bottom: 8px;
}
.slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: #444;
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #00aaff;
cursor: pointer;
transition: background 0.2s;
}
.slider::-webkit-slider-thumb:hover {
background: #0088cc;
}
.slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #00aaff;
cursor: pointer;
border: none;
transition: background 0.2s;
}
.slider::-moz-range-thumb:hover {
background: #0088cc;
}
.input-number {
width: 100%;
padding: 8px;
background: #383838;
border: 1px solid #555;
border-radius: 4px;
color: #fff;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.input-number:focus {
border-color: #00aaff;
}
.select {
width: 100%;
padding: 8px;
background: #383838;
border: 1px solid #555;
border-radius: 4px;
color: #fff;
font-size: 14px;
outline: none;
cursor: pointer;
transition: border-color 0.2s;
}
.select:focus {
border-color: #00aaff;
}
.points-display {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
.point-coord {
padding: 8px;
background: #383838;
border-radius: 4px;
font-size: 11px;
font-family: "Courier New", monospace;
color: #aaa;
}
.point-handle {
z-index: 10;
transition: transform 0.1s, box-shadow 0.1s;
}
.point-handle:hover {
transform: translate(-50%, -50%) scale(1.2);
box-shadow: 0 4px 8px rgba(0, 170, 255, 0.5);
}
.point-handle.dragging {
cursor: grabbing;
transform: translate(-50%, -50%) scale(1.3);
box-shadow: 0 6px 12px rgba(0, 170, 255, 0.7);
}
.area-list-items::-webkit-scrollbar,
.parameter-panel::-webkit-scrollbar {
width: 8px;
}
.area-list-items::-webkit-scrollbar-track,
.parameter-panel::-webkit-scrollbar-track {
background: #1e1e1e;
border-radius: 4px;
}
.area-list-items::-webkit-scrollbar-thumb,
.parameter-panel::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
.area-list-items::-webkit-scrollbar-thumb:hover,
.parameter-panel::-webkit-scrollbar-thumb:hover {
background: #666;
}
@media (max-width: 1200px) {
.editor-main {
flex-direction: column;
}
.editor-sidebar {
width: 100%;
flex-direction: row;
}
.area-list,
.parameter-panel {
flex: 1;
}
}
@media (max-width: 768px) {
.editor-sidebar {
flex-direction: column;
}
.points-display {
grid-template-columns: 1fr;
}
}
/*# sourceMappingURL=index.css.map */

1
dist/index.css.map vendored Normal file

File diff suppressed because one or more lines are too long

59
dist/index.d.mts vendored
View File

@ -132,6 +132,56 @@ interface ImageDistortionProps {
*/ */
declare const ImageDistortion: React.FC<ImageDistortionProps>; declare const ImageDistortion: React.FC<ImageDistortionProps>;
/**
*
*/
type EditMode = 'normal' | 'point-edit' | 'parameter-edit';
/**
*
*/
interface EditorState {
/** 현재 선택된 영역 ID */
selectedAreaId: string | null;
/** 모든 왜곡 영역 */
areas: DistortionArea[];
/** 현재 편집 모드 */
editMode: EditMode;
/** 드래그 중인 포인트 인덱스 (0-3) */
draggingPointIndex: number | null;
}
/**
* Props
*/
interface DistortionEditorProps {
/** 초기 영역 배열 */
initialAreas?: DistortionArea[];
/** 이미지 소스 */
imageSrc: string;
/** 영역 변경 콜백 */
onAreasChange?: (areas: DistortionArea[]) => void;
/** 선택된 영역 변경 콜백 */
onSelectedAreaChange?: (areaId: string | null) => void;
/** 캔버스 너비 */
width?: number;
/** 캔버스 높이 */
height?: number;
}
declare const DistortionEditor: React.FC<DistortionEditorProps>;
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
state: EditorState;
selectArea: (areaId: string | null) => void;
addArea: (area: DistortionArea) => void;
removeArea: (areaId: string) => void;
updateArea: (areaId: string, updates: Partial<DistortionArea>) => void;
updatePoint: (areaId: string, pointIndex: number, point: Point) => void;
startDragging: (pointIndex: number) => void;
stopDragging: () => void;
setEditMode: (mode: EditorState["editMode"]) => void;
getSelectedArea: () => DistortionArea | null;
};
/** /**
* *
* @param progress (0.0 - 1.0) * @param progress (0.0 - 1.0)
@ -214,6 +264,13 @@ declare class ThreeScene {
* *
*/ */
render(): void; render(): void;
/**
*
*/
getResolution(): {
x: number;
y: number;
};
/** /**
* *
*/ */
@ -272,4 +329,4 @@ declare class AnimationLoop {
*/ */
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void; declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, type DistortionMovement, type EasingFunction, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame }; export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor };

59
dist/index.d.ts vendored
View File

@ -132,6 +132,56 @@ interface ImageDistortionProps {
*/ */
declare const ImageDistortion: React.FC<ImageDistortionProps>; declare const ImageDistortion: React.FC<ImageDistortionProps>;
/**
*
*/
type EditMode = 'normal' | 'point-edit' | 'parameter-edit';
/**
*
*/
interface EditorState {
/** 현재 선택된 영역 ID */
selectedAreaId: string | null;
/** 모든 왜곡 영역 */
areas: DistortionArea[];
/** 현재 편집 모드 */
editMode: EditMode;
/** 드래그 중인 포인트 인덱스 (0-3) */
draggingPointIndex: number | null;
}
/**
* Props
*/
interface DistortionEditorProps {
/** 초기 영역 배열 */
initialAreas?: DistortionArea[];
/** 이미지 소스 */
imageSrc: string;
/** 영역 변경 콜백 */
onAreasChange?: (areas: DistortionArea[]) => void;
/** 선택된 영역 변경 콜백 */
onSelectedAreaChange?: (areaId: string | null) => void;
/** 캔버스 너비 */
width?: number;
/** 캔버스 높이 */
height?: number;
}
declare const DistortionEditor: React.FC<DistortionEditorProps>;
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
state: EditorState;
selectArea: (areaId: string | null) => void;
addArea: (area: DistortionArea) => void;
removeArea: (areaId: string) => void;
updateArea: (areaId: string, updates: Partial<DistortionArea>) => void;
updatePoint: (areaId: string, pointIndex: number, point: Point) => void;
startDragging: (pointIndex: number) => void;
stopDragging: () => void;
setEditMode: (mode: EditorState["editMode"]) => void;
getSelectedArea: () => DistortionArea | null;
};
/** /**
* *
* @param progress (0.0 - 1.0) * @param progress (0.0 - 1.0)
@ -214,6 +264,13 @@ declare class ThreeScene {
* *
*/ */
render(): void; render(): void;
/**
*
*/
getResolution(): {
x: number;
y: number;
};
/** /**
* *
*/ */
@ -272,4 +329,4 @@ declare class AnimationLoop {
*/ */
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void; declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, type DistortionMovement, type EasingFunction, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame }; export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor };

655
dist/index.js vendored
View File

@ -33,12 +33,14 @@ __export(index_exports, {
ANIMATION_CONFIG: () => ANIMATION_CONFIG, ANIMATION_CONFIG: () => ANIMATION_CONFIG,
AnimationLoop: () => AnimationLoop, AnimationLoop: () => AnimationLoop,
DEFAULT_AREA: () => DEFAULT_AREA, DEFAULT_AREA: () => DEFAULT_AREA,
DistortionEditor: () => DistortionEditor,
ImageDistortion: () => ImageDistortion, ImageDistortion: () => ImageDistortion,
SHADER_CONFIG: () => SHADER_CONFIG, SHADER_CONFIG: () => SHADER_CONFIG,
ShaderManager: () => ShaderManager, ShaderManager: () => ShaderManager,
ThreeScene: () => ThreeScene, ThreeScene: () => ThreeScene,
applyEasing: () => applyEasing, applyEasing: () => applyEasing,
useAnimationFrame: () => useAnimationFrame useAnimationFrame: () => useAnimationFrame,
useDistortionEditor: () => useDistortionEditor
}); });
module.exports = __toCommonJS(index_exports); module.exports = __toCommonJS(index_exports);
@ -135,6 +137,15 @@ var ThreeScene = class {
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh); console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
} }
/**
* 현재 해상도 가져오기
*/
getResolution() {
return {
x: this.uniforms.u_resolution.value.x,
y: this.uniforms.u_resolution.value.y
};
}
/** /**
* 리소스 정리 * 리소스 정리
*/ */
@ -426,19 +437,20 @@ var ImageDistortion = ({
}, [imageSrc, isReady]); }, [imageSrc, isReady]);
(0, import_react2.useEffect)(() => { (0, import_react2.useEffect)(() => {
if (!sceneRef.current || !isReady) return; if (!sceneRef.current || !isReady) return;
const resolution = sceneRef.current.getResolution();
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2); const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
currentAreas.forEach((area, areaIndex) => { currentAreas.forEach((area, areaIndex) => {
area.basePoints.forEach((point, pointIndex) => { area.basePoints.forEach((point, pointIndex) => {
const index = (areaIndex * 4 + pointIndex) * 2; const index = (areaIndex * 4 + pointIndex) * 2;
points[index] = point.x; points[index] = point.x;
points[index + 1] = point.y; points[index + 1] = 1 - point.y;
}); });
}); });
const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2); const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);
currentAreas.forEach((area, index) => { currentAreas.forEach((area, index) => {
const baseIndex = index * 2; const baseIndex = index * 2;
dragVectors[baseIndex] = area.dragVector.x; dragVectors[baseIndex] = area.dragVector.x;
dragVectors[baseIndex + 1] = area.dragVector.y; dragVectors[baseIndex + 1] = -area.dragVector.y;
}); });
const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS); const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);
currentAreas.forEach((area, index) => { currentAreas.forEach((area, index) => {
@ -491,16 +503,651 @@ var ImageDistortion = ({
} }
); );
}; };
// src/editor/DistortionEditor.tsx
var import_react5 = require("react");
// src/editor/hooks/useDistortionEditor.ts
var import_react3 = require("react");
var useDistortionEditor = (initialAreas = []) => {
const [state, setState] = (0, import_react3.useState)({
selectedAreaId: initialAreas[0]?.id || null,
areas: initialAreas,
editMode: "normal",
draggingPointIndex: null
});
const selectArea = (0, import_react3.useCallback)((areaId) => {
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
}, []);
const addArea = (0, import_react3.useCallback)((area) => {
setState((prev) => ({
...prev,
areas: [...prev.areas, area],
selectedAreaId: area.id
}));
}, []);
const removeArea = (0, import_react3.useCallback)((areaId) => {
setState((prev) => {
const newAreas = prev.areas.filter((a) => a.id !== areaId);
return {
...prev,
areas: newAreas,
selectedAreaId: prev.selectedAreaId === areaId ? newAreas[0]?.id || null : prev.selectedAreaId
};
});
}, []);
const updateArea = (0, import_react3.useCallback)((areaId, updates) => {
setState((prev) => ({
...prev,
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
}));
}, []);
const updatePoint = (0, import_react3.useCallback)((areaId, pointIndex, point) => {
setState((prev) => ({
...prev,
areas: prev.areas.map((area) => {
if (area.id === areaId) {
const newPoints = [...area.basePoints];
newPoints[pointIndex] = point;
return { ...area, basePoints: newPoints };
}
return area;
})
}));
}, []);
const startDragging = (0, import_react3.useCallback)((pointIndex) => {
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
}, []);
const stopDragging = (0, import_react3.useCallback)(() => {
setState((prev) => ({ ...prev, draggingPointIndex: null }));
}, []);
const setEditMode = (0, import_react3.useCallback)((mode) => {
setState((prev) => ({ ...prev, editMode: mode }));
}, []);
const getSelectedArea = (0, import_react3.useCallback)(() => {
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
}, [state.areas, state.selectedAreaId]);
return {
state,
selectArea,
addArea,
removeArea,
updateArea,
updatePoint,
startDragging,
stopDragging,
setEditMode,
getSelectedArea
};
};
// src/editor/components/EditorCanvas.tsx
var import_react4 = require("react");
var import_jsx_runtime2 = require("react/jsx-runtime");
var EditorCanvas = ({
areas,
selectedAreaId,
imageSrc,
width,
height,
onUpdatePoint,
onUpdateArea,
draggingPointIndex,
onStartDragging,
onStopDragging
}) => {
const containerRef = (0, import_react4.useRef)(null);
const [canvasSize, setCanvasSize] = (0, import_react4.useState)({ width: 0, height: 0 });
const [isDraggingArea, setIsDraggingArea] = (0, import_react4.useState)(false);
const [dragStartPos, setDragStartPos] = (0, import_react4.useState)(null);
(0, import_react4.useEffect)(() => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
setCanvasSize({ width: rect.width, height: rect.height });
}, [width, height]);
const selectedArea = areas.find((a) => a.id === selectedAreaId);
const isPointInPolygon = (0, import_react4.useCallback)((point, polygon) => {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x, yi = polygon[i].y;
const xj = polygon[j].x, yj = polygon[j].y;
const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}, []);
const handleMouseDown = (0, import_react4.useCallback)(
(pointIndex) => (e) => {
e.preventDefault();
e.stopPropagation();
onStartDragging(pointIndex);
},
[onStartDragging]
);
const handleCanvasMouseDown = (0, import_react4.useCallback)(
(e) => {
if (!selectedArea || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
const clickPoint = { x, y };
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
setIsDraggingArea(true);
setDragStartPos(clickPoint);
e.preventDefault();
}
},
[selectedArea, isPointInPolygon]
);
const handleMouseMove = (0, import_react4.useCallback)(
(e) => {
if (!selectedArea || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
if (draggingPointIndex !== null) {
const clampedX = Math.max(0, Math.min(1, x));
const clampedY = Math.max(0, Math.min(1, y));
onUpdatePoint(selectedArea.id, draggingPointIndex, { x: clampedX, y: clampedY });
} else if (isDraggingArea && dragStartPos) {
const deltaX = x - dragStartPos.x;
const deltaY = y - dragStartPos.y;
const newPoints = selectedArea.basePoints.map((point) => ({
x: Math.max(0, Math.min(1, point.x + deltaX)),
y: Math.max(0, Math.min(1, point.y + deltaY))
}));
onUpdateArea(selectedArea.id, { basePoints: newPoints });
setDragStartPos({ x, y });
}
},
[draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
);
const handleMouseUp = (0, import_react4.useCallback)(() => {
if (draggingPointIndex !== null) {
onStopDragging();
}
if (isDraggingArea) {
setIsDraggingArea(false);
setDragStartPos(null);
}
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
(0, import_react4.useEffect)(() => {
if (draggingPointIndex !== null || isDraggingArea) {
window.addEventListener("mouseup", handleMouseUp);
return () => window.removeEventListener("mouseup", handleMouseUp);
}
}, [draggingPointIndex, isDraggingArea, handleMouseUp]);
const uvToPixel = (u, v, points, canvasWidth, canvasHeight) => {
const [p0, p1, p2, p3] = points;
const leftX = p0.x * (1 - u) + p1.x * u;
const leftY = p0.y * (1 - u) + p1.y * u;
const rightX = p3.x * (1 - u) + p2.x * u;
const rightY = p3.y * (1 - u) + p2.y * u;
const posX = leftX * (1 - v) + rightX * v;
const posY = leftY * (1 - v) + rightY * v;
return {
x: posX * canvasWidth,
y: posY * canvasHeight
};
};
const drawDistortionCircle = (ctx, points, canvasWidth, canvasHeight) => {
const segments = 128;
const centerU = 0.5;
const centerV = 0.5;
const maxRadius = 0.5;
const circlePoints = [];
for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI;
const u = centerU - maxRadius * Math.sin(theta);
const v = centerV + maxRadius * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
circlePoints.push(pixelPos);
}
ctx.beginPath();
ctx.moveTo(circlePoints[0].x, circlePoints[0].y);
for (let i = 1; i < circlePoints.length; i++) {
ctx.lineTo(circlePoints[i].x, circlePoints[i].y);
}
ctx.closePath();
ctx.strokeStyle = "rgba(255, 200, 0, 0.9)";
ctx.lineWidth = 3;
ctx.setLineDash([8, 4]);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = "rgba(255, 200, 0, 0.12)";
ctx.fill();
for (const r of [0.25, 0.375]) {
const gradientPoints = [];
for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI;
const u = centerU - r * Math.sin(theta);
const v = centerV + r * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
gradientPoints.push(pixelPos);
}
ctx.beginPath();
ctx.moveTo(gradientPoints[0].x, gradientPoints[0].y);
for (let i = 1; i < gradientPoints.length; i++) {
ctx.lineTo(gradientPoints[i].x, gradientPoints[i].y);
}
ctx.closePath();
const alpha = r / maxRadius;
ctx.strokeStyle = `rgba(255, 220, 100, ${0.3 * alpha})`;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.stroke();
ctx.setLineDash([]);
}
const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight);
ctx.beginPath();
ctx.arc(centerPixel.x, centerPixel.y, 5, 0, 2 * Math.PI);
ctx.fillStyle = "rgba(255, 200, 0, 1)";
ctx.fill();
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
ctx.lineWidth = 2;
ctx.stroke();
};
const getCursorStyle = () => {
if (draggingPointIndex !== null) return "grabbing";
if (isDraggingArea) return "grabbing";
return "default";
};
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
"div",
{
ref: containerRef,
className: "editor-canvas",
style: { width, height, position: "relative", cursor: getCursorStyle() },
onMouseDown: handleCanvasMouseDown,
onMouseMove: handleMouseMove,
children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas, width, height }),
/* @__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;
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: "none",
stroke: isSelected ? "#00aaff" : "#888",
strokeWidth: isSelected ? 2 : 1,
strokeDasharray: isSelected ? "0" : "5,5",
opacity: isSelected ? 1 : 0.5
}
) }, area.id);
})
}
),
selectedArea && canvasSize.width > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"canvas",
{
style: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
pointerEvents: "none"
},
width: canvasSize.width,
height: canvasSize.height,
ref: (canvas) => {
if (canvas) {
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);
drawDistortionCircle(ctx, selectedArea.basePoints, canvasSize.width, canvasSize.height);
}
}
}
}
),
selectedArea && selectedArea.basePoints.map((point, index) => /* @__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: 16,
height: 16,
borderRadius: "50%",
backgroundColor: "#00aaff",
border: "2px solid white",
cursor: "grab",
pointerEvents: "auto",
boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
},
onMouseDown: handleMouseDown(index),
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
"div",
{
style: {
position: "absolute",
top: -24,
left: "50%",
transform: "translateX(-50%)",
fontSize: 11,
color: "#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
}) => {
const {
state,
selectArea,
addArea,
removeArea,
updateArea,
updatePoint,
startDragging,
stopDragging,
getSelectedArea
} = useDistortionEditor(initialAreas);
(0, import_react5.useEffect)(() => {
onAreasChange?.(state.areas);
}, [state.areas, onAreasChange]);
(0, import_react5.useEffect)(() => {
onSelectedAreaChange?.(state.selectedAreaId);
}, [state.selectedAreaId, onSelectedAreaChange]);
const handleAddArea = () => {
const newArea = {
id: `area-${Date.now()}`,
basePoints: [
{ x: 0.3, y: 0.3 },
{ x: 0.7, y: 0.3 },
{ x: 0.7, y: 0.7 },
{ x: 0.3, y: 0.7 }
],
movement: {
vectorA: { x: DEFAULT_AREA.VECTOR_A.x, y: DEFAULT_AREA.VECTOR_A.y },
vectorB: { x: DEFAULT_AREA.VECTOR_B.x, y: DEFAULT_AREA.VECTOR_B.y },
duration: DEFAULT_AREA.DURATION,
easing: DEFAULT_AREA.EASING
},
distortionStrength: DEFAULT_AREA.DISTORTION_STRENGTH,
progress: 0,
dragVector: { x: 0, y: 0 }
};
addArea(newArea);
};
const handleUpdateArea = (updates) => {
if (state.selectedAreaId) {
updateArea(state.selectedAreaId, updates);
}
};
const selectedArea = getSelectedArea();
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "distortion-editor", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-main", children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "editor-canvas-container", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
EditorCanvas,
{
areas: state.areas,
selectedAreaId: state.selectedAreaId,
imageSrc,
width,
height,
onUpdatePoint: updatePoint,
onUpdateArea: updateArea,
draggingPointIndex: state.draggingPointIndex,
onStartDragging: startDragging,
onStopDragging: stopDragging
}
) }),
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-sidebar", children: [
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
AreaList,
{
areas: state.areas,
selectedAreaId: state.selectedAreaId,
onSelectArea: selectArea,
onRemoveArea: removeArea,
onAddArea: handleAddArea
}
),
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ParameterPanel, { area: selectedArea, onUpdateArea: handleUpdateArea })
] })
] }) });
};
// Annotate the CommonJS export names for ESM import in node: // Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = { 0 && (module.exports = {
ANIMATION_CONFIG, ANIMATION_CONFIG,
AnimationLoop, AnimationLoop,
DEFAULT_AREA, DEFAULT_AREA,
DistortionEditor,
ImageDistortion, ImageDistortion,
SHADER_CONFIG, SHADER_CONFIG,
ShaderManager, ShaderManager,
ThreeScene, ThreeScene,
applyEasing, applyEasing,
useAnimationFrame useAnimationFrame,
useDistortionEditor
}); });
//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

651
dist/index.mjs vendored
View File

@ -91,6 +91,15 @@ var ThreeScene = class {
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh); console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
} }
/**
* 현재 해상도 가져오기
*/
getResolution() {
return {
x: this.uniforms.u_resolution.value.x,
y: this.uniforms.u_resolution.value.y
};
}
/** /**
* 리소스 정리 * 리소스 정리
*/ */
@ -382,19 +391,20 @@ var ImageDistortion = ({
}, [imageSrc, isReady]); }, [imageSrc, isReady]);
useEffect2(() => { useEffect2(() => {
if (!sceneRef.current || !isReady) return; if (!sceneRef.current || !isReady) return;
const resolution = sceneRef.current.getResolution();
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2); const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
currentAreas.forEach((area, areaIndex) => { currentAreas.forEach((area, areaIndex) => {
area.basePoints.forEach((point, pointIndex) => { area.basePoints.forEach((point, pointIndex) => {
const index = (areaIndex * 4 + pointIndex) * 2; const index = (areaIndex * 4 + pointIndex) * 2;
points[index] = point.x; points[index] = point.x;
points[index + 1] = point.y; points[index + 1] = 1 - point.y;
}); });
}); });
const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2); const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);
currentAreas.forEach((area, index) => { currentAreas.forEach((area, index) => {
const baseIndex = index * 2; const baseIndex = index * 2;
dragVectors[baseIndex] = area.dragVector.x; dragVectors[baseIndex] = area.dragVector.x;
dragVectors[baseIndex + 1] = area.dragVector.y; dragVectors[baseIndex + 1] = -area.dragVector.y;
}); });
const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS); const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);
currentAreas.forEach((area, index) => { currentAreas.forEach((area, index) => {
@ -447,15 +457,650 @@ var ImageDistortion = ({
} }
); );
}; };
// src/editor/DistortionEditor.tsx
import { useEffect as useEffect4 } from "react";
// src/editor/hooks/useDistortionEditor.ts
import { useState as useState2, useCallback as useCallback2 } from "react";
var useDistortionEditor = (initialAreas = []) => {
const [state, setState] = useState2({
selectedAreaId: initialAreas[0]?.id || null,
areas: initialAreas,
editMode: "normal",
draggingPointIndex: null
});
const selectArea = useCallback2((areaId) => {
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
}, []);
const addArea = useCallback2((area) => {
setState((prev) => ({
...prev,
areas: [...prev.areas, area],
selectedAreaId: area.id
}));
}, []);
const removeArea = useCallback2((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 = useCallback2((areaId, updates) => {
setState((prev) => ({
...prev,
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
}));
}, []);
const updatePoint = useCallback2((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 = useCallback2((pointIndex) => {
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
}, []);
const stopDragging = useCallback2(() => {
setState((prev) => ({ ...prev, draggingPointIndex: null }));
}, []);
const setEditMode = useCallback2((mode) => {
setState((prev) => ({ ...prev, editMode: mode }));
}, []);
const getSelectedArea = useCallback2(() => {
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
import { useRef as useRef3, useEffect as useEffect3, useState as useState3, useCallback as useCallback3 } from "react";
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var EditorCanvas = ({
areas,
selectedAreaId,
imageSrc,
width,
height,
onUpdatePoint,
onUpdateArea,
draggingPointIndex,
onStartDragging,
onStopDragging
}) => {
const containerRef = useRef3(null);
const [canvasSize, setCanvasSize] = useState3({ width: 0, height: 0 });
const [isDraggingArea, setIsDraggingArea] = useState3(false);
const [dragStartPos, setDragStartPos] = useState3(null);
useEffect3(() => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
setCanvasSize({ width: rect.width, height: rect.height });
}, [width, height]);
const selectedArea = areas.find((a) => a.id === selectedAreaId);
const isPointInPolygon = useCallback3((point, polygon) => {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x, yi = polygon[i].y;
const xj = polygon[j].x, yj = polygon[j].y;
const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}, []);
const handleMouseDown = useCallback3(
(pointIndex) => (e) => {
e.preventDefault();
e.stopPropagation();
onStartDragging(pointIndex);
},
[onStartDragging]
);
const handleCanvasMouseDown = useCallback3(
(e) => {
if (!selectedArea || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
const clickPoint = { x, y };
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
setIsDraggingArea(true);
setDragStartPos(clickPoint);
e.preventDefault();
}
},
[selectedArea, isPointInPolygon]
);
const handleMouseMove = useCallback3(
(e) => {
if (!selectedArea || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
if (draggingPointIndex !== null) {
const clampedX = Math.max(0, Math.min(1, x));
const clampedY = Math.max(0, Math.min(1, y));
onUpdatePoint(selectedArea.id, draggingPointIndex, { x: clampedX, y: clampedY });
} else if (isDraggingArea && dragStartPos) {
const deltaX = x - dragStartPos.x;
const deltaY = y - dragStartPos.y;
const newPoints = selectedArea.basePoints.map((point) => ({
x: Math.max(0, Math.min(1, point.x + deltaX)),
y: Math.max(0, Math.min(1, point.y + deltaY))
}));
onUpdateArea(selectedArea.id, { basePoints: newPoints });
setDragStartPos({ x, y });
}
},
[draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
);
const handleMouseUp = useCallback3(() => {
if (draggingPointIndex !== null) {
onStopDragging();
}
if (isDraggingArea) {
setIsDraggingArea(false);
setDragStartPos(null);
}
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
useEffect3(() => {
if (draggingPointIndex !== null || isDraggingArea) {
window.addEventListener("mouseup", handleMouseUp);
return () => window.removeEventListener("mouseup", handleMouseUp);
}
}, [draggingPointIndex, isDraggingArea, handleMouseUp]);
const uvToPixel = (u, v, points, canvasWidth, canvasHeight) => {
const [p0, p1, p2, p3] = points;
const leftX = p0.x * (1 - u) + p1.x * u;
const leftY = p0.y * (1 - u) + p1.y * u;
const rightX = p3.x * (1 - u) + p2.x * u;
const rightY = p3.y * (1 - u) + p2.y * u;
const posX = leftX * (1 - v) + rightX * v;
const posY = leftY * (1 - v) + rightY * v;
return {
x: posX * canvasWidth,
y: posY * canvasHeight
};
};
const drawDistortionCircle = (ctx, points, canvasWidth, canvasHeight) => {
const segments = 128;
const centerU = 0.5;
const centerV = 0.5;
const maxRadius = 0.5;
const circlePoints = [];
for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI;
const u = centerU - maxRadius * Math.sin(theta);
const v = centerV + maxRadius * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
circlePoints.push(pixelPos);
}
ctx.beginPath();
ctx.moveTo(circlePoints[0].x, circlePoints[0].y);
for (let i = 1; i < circlePoints.length; i++) {
ctx.lineTo(circlePoints[i].x, circlePoints[i].y);
}
ctx.closePath();
ctx.strokeStyle = "rgba(255, 200, 0, 0.9)";
ctx.lineWidth = 3;
ctx.setLineDash([8, 4]);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = "rgba(255, 200, 0, 0.12)";
ctx.fill();
for (const r of [0.25, 0.375]) {
const gradientPoints = [];
for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI;
const u = centerU - r * Math.sin(theta);
const v = centerV + r * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
gradientPoints.push(pixelPos);
}
ctx.beginPath();
ctx.moveTo(gradientPoints[0].x, gradientPoints[0].y);
for (let i = 1; i < gradientPoints.length; i++) {
ctx.lineTo(gradientPoints[i].x, gradientPoints[i].y);
}
ctx.closePath();
const alpha = r / maxRadius;
ctx.strokeStyle = `rgba(255, 220, 100, ${0.3 * alpha})`;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.stroke();
ctx.setLineDash([]);
}
const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight);
ctx.beginPath();
ctx.arc(centerPixel.x, centerPixel.y, 5, 0, 2 * Math.PI);
ctx.fillStyle = "rgba(255, 200, 0, 1)";
ctx.fill();
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
ctx.lineWidth = 2;
ctx.stroke();
};
const getCursorStyle = () => {
if (draggingPointIndex !== null) return "grabbing";
if (isDraggingArea) return "grabbing";
return "default";
};
return /* @__PURE__ */ jsxs(
"div",
{
ref: containerRef,
className: "editor-canvas",
style: { width, height, position: "relative", cursor: getCursorStyle() },
onMouseDown: handleCanvasMouseDown,
onMouseMove: handleMouseMove,
children: [
/* @__PURE__ */ jsx2(ImageDistortion, { imageSrc, areas, width, height }),
/* @__PURE__ */ jsx2(
"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;
return /* @__PURE__ */ jsx2("g", { children: /* @__PURE__ */ jsx2(
"polygon",
{
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
fill: "none",
stroke: isSelected ? "#00aaff" : "#888",
strokeWidth: isSelected ? 2 : 1,
strokeDasharray: isSelected ? "0" : "5,5",
opacity: isSelected ? 1 : 0.5
}
) }, area.id);
})
}
),
selectedArea && canvasSize.width > 0 && /* @__PURE__ */ jsx2(
"canvas",
{
style: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
pointerEvents: "none"
},
width: canvasSize.width,
height: canvasSize.height,
ref: (canvas) => {
if (canvas) {
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);
drawDistortionCircle(ctx, selectedArea.basePoints, canvasSize.width, canvasSize.height);
}
}
}
}
),
selectedArea && selectedArea.basePoints.map((point, index) => /* @__PURE__ */ jsx2(
"div",
{
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
style: {
position: "absolute",
left: `${point.x * 100}%`,
top: `${point.y * 100}%`,
transform: "translate(-50%, -50%)",
width: 16,
height: 16,
borderRadius: "50%",
backgroundColor: "#00aaff",
border: "2px solid white",
cursor: "grab",
pointerEvents: "auto",
boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
},
onMouseDown: handleMouseDown(index),
children: /* @__PURE__ */ jsxs(
"div",
{
style: {
position: "absolute",
top: -24,
left: "50%",
transform: "translateX(-50%)",
fontSize: 11,
color: "#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
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
var AreaList = ({
areas,
selectedAreaId,
onSelectArea,
onRemoveArea,
onAddArea
}) => {
return /* @__PURE__ */ jsxs2("div", { className: "area-list", children: [
/* @__PURE__ */ jsxs2("div", { className: "area-list-header", children: [
/* @__PURE__ */ jsx3("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }),
/* @__PURE__ */ jsx3(
"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__ */ jsx3("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ jsx3("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__ */ jsxs2(
"div",
{
className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`,
onClick: () => onSelectArea(area.id),
children: [
/* @__PURE__ */ jsxs2("div", { className: "area-item-info", children: [
/* @__PURE__ */ jsxs2("span", { className: "area-item-name", children: [
"\uC601\uC5ED ",
index + 1
] }),
/* @__PURE__ */ jsxs2("span", { className: "area-item-strength", children: [
"\uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] })
] }),
/* @__PURE__ */ jsx3(
"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 jsx4, jsxs as jsxs3 } 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__ */ jsx4("div", { className: "parameter-panel", children: /* @__PURE__ */ jsx4("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) });
}
return /* @__PURE__ */ jsxs3("div", { className: "parameter-panel", children: [
/* @__PURE__ */ jsx4("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }),
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs3("label", { children: [
"\uC65C\uACE1 \uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] }),
/* @__PURE__ */ jsx4(
"input",
{
type: "range",
min: "0",
max: "1",
step: "0.01",
value: area.distortionStrength,
onChange: (e) => onUpdateArea({ distortionStrength: parseFloat(e.target.value) }),
className: "slider"
}
)
] }),
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs3("label", { children: [
"\uC9C0\uC18D \uC2DC\uAC04: ",
area.movement.duration.toFixed(1),
"\uCD08"
] }),
/* @__PURE__ */ jsx4(
"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__ */ jsxs3("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsx4("label", { children: "\uC774\uC9D5 \uD568\uC218" }),
/* @__PURE__ */ jsx4(
"select",
{
value: area.movement.easing,
onChange: (e) => onUpdateArea({
movement: { ...area.movement, easing: e.target.value }
}),
className: "select",
children: EASING_OPTIONS.map((option) => /* @__PURE__ */ jsx4("option", { value: option.value, children: option.label }, option.value))
}
)
] }),
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs3("label", { children: [
"\uBCA1\uD130 X: ",
area.movement.vectorA.x.toFixed(2)
] }),
/* @__PURE__ */ jsx4(
"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__ */ jsxs3("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs3("label", { children: [
"\uBCA1\uD130 Y: ",
area.movement.vectorA.y.toFixed(2)
] }),
/* @__PURE__ */ jsx4(
"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__ */ jsxs3("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsx4("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
/* @__PURE__ */ jsx4("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ jsxs3("div", { className: "point-coord", children: [
"P",
idx + 1,
": (",
point.x.toFixed(3),
", ",
point.y.toFixed(3),
")"
] }, idx)) })
] })
] });
};
// src/editor/DistortionEditor.tsx
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
var DistortionEditor = ({
initialAreas = [],
imageSrc,
onAreasChange,
onSelectedAreaChange,
width = 800,
height = 600
}) => {
const {
state,
selectArea,
addArea,
removeArea,
updateArea,
updatePoint,
startDragging,
stopDragging,
getSelectedArea
} = useDistortionEditor(initialAreas);
useEffect4(() => {
onAreasChange?.(state.areas);
}, [state.areas, onAreasChange]);
useEffect4(() => {
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__ */ jsx5("div", { className: "distortion-editor", children: /* @__PURE__ */ jsxs4("div", { className: "editor-main", children: [
/* @__PURE__ */ jsx5("div", { className: "editor-canvas-container", children: /* @__PURE__ */ jsx5(
EditorCanvas,
{
areas: state.areas,
selectedAreaId: state.selectedAreaId,
imageSrc,
width,
height,
onUpdatePoint: updatePoint,
onUpdateArea: updateArea,
draggingPointIndex: state.draggingPointIndex,
onStartDragging: startDragging,
onStopDragging: stopDragging
}
) }),
/* @__PURE__ */ jsxs4("div", { className: "editor-sidebar", children: [
/* @__PURE__ */ jsx5(
AreaList,
{
areas: state.areas,
selectedAreaId: state.selectedAreaId,
onSelectArea: selectArea,
onRemoveArea: removeArea,
onAddArea: handleAddArea
}
),
/* @__PURE__ */ jsx5(ParameterPanel, { area: selectedArea, onUpdateArea: handleUpdateArea })
] })
] }) });
};
export { export {
ANIMATION_CONFIG, ANIMATION_CONFIG,
AnimationLoop, AnimationLoop,
DEFAULT_AREA, DEFAULT_AREA,
DistortionEditor,
ImageDistortion, ImageDistortion,
SHADER_CONFIG, SHADER_CONFIG,
ShaderManager, ShaderManager,
ThreeScene, ThreeScene,
applyEasing, applyEasing,
useAnimationFrame useAnimationFrame,
useDistortionEditor
}; };
//# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map

2
dist/index.mjs.map vendored

File diff suppressed because one or more lines are too long