Compare commits

..

No commits in common. "5f6e780b40a4824dc07620aede0372812f30f5d4" and "e08f34caab8684e0b866fc6f63e9efb3d98889e4" have entirely different histories.

14 changed files with 942 additions and 542 deletions

53
dist/index.d.mts vendored
View File

@ -305,39 +305,27 @@ interface EditorCanvasStyle {
/** 영역 외곽선 스타일 */
areaOutline?: AreaOutlineStyle;
}
interface EditorCanvasProps {
areas: DistortionArea[];
selectedAreaId: string | null;
/**
* Props
*/
interface DistortionEditorProps {
/** 초기 영역 배열 */
initialAreas?: DistortionArea[];
/** 이미지 소스 */
imageSrc: string;
width: number;
height: number;
onUpdatePoint: (areaId: string, pointIndex: number, point: Point) => void;
onUpdateArea: (areaId: string, updates: Partial<DistortionArea>) => void;
draggingPointIndex: number | null;
onStartDragging: (pointIndex: number) => void;
onStopDragging: () => void;
/** 영역 변경 콜백 */
onAreasChange?: (areas: DistortionArea[]) => void;
/** 선택된 영역 변경 콜백 */
onSelectedAreaChange?: (areaId: string | null) => void;
/** 캔버스 너비 */
width?: number;
/** 캔버스 높이 */
height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
style?: EditorCanvasStyle;
/** 에디터 UI 표시 여부 (기본값: true) */
showEditor?: boolean;
canvasStyle?: EditorCanvasStyle;
}
declare const EditorCanvas: React$1.FC<EditorCanvasProps>;
interface AreaListProps {
areas: DistortionArea[];
selectedAreaId: string | null;
onSelectArea: (areaId: string) => void;
onRemoveArea: (areaId: string) => void;
onAddArea: () => void;
}
declare const AreaList: React$1.FC<AreaListProps>;
interface ParameterPanelProps {
area: DistortionArea | null;
onUpdateArea: (updates: Partial<DistortionArea>) => void;
}
declare const ParameterPanel: React$1.FC<ParameterPanelProps>;
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
state: EditorState;
@ -352,11 +340,6 @@ declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
getSelectedArea: () => DistortionArea | null;
};
/**
*
*/
declare const DEFAULT_EDITOR_CANVAS_STYLE: EditorCanvasStyle;
/**
*
* @param progress (0.0 - 1.0)
@ -586,4 +569,4 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
getInteractingAreaIndices: () => Set<number>;
};
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, AreaList, type AreaListProps, type AreaOutlineStyle, type CenterPointStyle, type CircleLevelStyle, DEFAULT_AREA, DEFAULT_EDITOR_CANVAS_STYLE, type DistortionArea, type DistortionMovement, type EasingFunction, type EditMode, EditorCanvas, type EditorCanvasProps, type EditorCanvasStyle, type EditorState, ImageDistortion, type ImageDistortionProps, type MotionPreset, type MouseInteractionConfig, type MouseState, ParameterPanel, type ParameterPanelProps, type Point, type PointHandleStyle, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, isRotationPreset, presetToVector, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };
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 MotionPreset, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, isRotationPreset, presetToVector, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };

53
dist/index.d.ts vendored
View File

@ -305,39 +305,27 @@ interface EditorCanvasStyle {
/** 영역 외곽선 스타일 */
areaOutline?: AreaOutlineStyle;
}
interface EditorCanvasProps {
areas: DistortionArea[];
selectedAreaId: string | null;
/**
* Props
*/
interface DistortionEditorProps {
/** 초기 영역 배열 */
initialAreas?: DistortionArea[];
/** 이미지 소스 */
imageSrc: string;
width: number;
height: number;
onUpdatePoint: (areaId: string, pointIndex: number, point: Point) => void;
onUpdateArea: (areaId: string, updates: Partial<DistortionArea>) => void;
draggingPointIndex: number | null;
onStartDragging: (pointIndex: number) => void;
onStopDragging: () => void;
/** 영역 변경 콜백 */
onAreasChange?: (areas: DistortionArea[]) => void;
/** 선택된 영역 변경 콜백 */
onSelectedAreaChange?: (areaId: string | null) => void;
/** 캔버스 너비 */
width?: number;
/** 캔버스 높이 */
height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
style?: EditorCanvasStyle;
/** 에디터 UI 표시 여부 (기본값: true) */
showEditor?: boolean;
canvasStyle?: EditorCanvasStyle;
}
declare const EditorCanvas: React$1.FC<EditorCanvasProps>;
interface AreaListProps {
areas: DistortionArea[];
selectedAreaId: string | null;
onSelectArea: (areaId: string) => void;
onRemoveArea: (areaId: string) => void;
onAddArea: () => void;
}
declare const AreaList: React$1.FC<AreaListProps>;
interface ParameterPanelProps {
area: DistortionArea | null;
onUpdateArea: (updates: Partial<DistortionArea>) => void;
}
declare const ParameterPanel: React$1.FC<ParameterPanelProps>;
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
state: EditorState;
@ -352,11 +340,6 @@ declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
getSelectedArea: () => DistortionArea | null;
};
/**
*
*/
declare const DEFAULT_EDITOR_CANVAS_STYLE: EditorCanvasStyle;
/**
*
* @param progress (0.0 - 1.0)
@ -586,4 +569,4 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
getInteractingAreaIndices: () => Set<number>;
};
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, AreaList, type AreaListProps, type AreaOutlineStyle, type CenterPointStyle, type CircleLevelStyle, DEFAULT_AREA, DEFAULT_EDITOR_CANVAS_STYLE, type DistortionArea, type DistortionMovement, type EasingFunction, type EditMode, EditorCanvas, type EditorCanvasProps, type EditorCanvasStyle, type EditorState, ImageDistortion, type ImageDistortionProps, type MotionPreset, type MouseInteractionConfig, type MouseState, ParameterPanel, type ParameterPanelProps, type Point, type PointHandleStyle, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, isRotationPreset, presetToVector, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };
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 MotionPreset, type MouseInteractionConfig, type MouseState, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, SpringPhysics, type SpringPhysicsConfig, type SpringState, ThreeScene, applyEasing, isRotationPreset, presetToVector, useAnimationFrame, useDistortionEditor, useMouseInteraction, useMouseVelocity };

582
dist/index.js vendored
View File

@ -32,12 +32,9 @@ var index_exports = {};
__export(index_exports, {
ANIMATION_CONFIG: () => ANIMATION_CONFIG,
AnimationLoop: () => AnimationLoop,
AreaList: () => AreaList,
DEFAULT_AREA: () => DEFAULT_AREA,
DEFAULT_EDITOR_CANVAS_STYLE: () => DEFAULT_EDITOR_CANVAS_STYLE,
EditorCanvas: () => EditorCanvas,
DistortionEditor: () => DistortionEditor,
ImageDistortion: () => ImageDistortion,
ParameterPanel: () => ParameterPanel,
SHADER_CONFIG: () => SHADER_CONFIG,
ShaderManager: () => ShaderManager,
SpringPhysics: () => SpringPhysics,
@ -989,152 +986,8 @@ var ImageDistortion = ({
);
};
// src/editor/components/EditorCanvas.tsx
var import_react6 = require("react");
// src/editor/components/AreaList.tsx
var import_jsx_runtime2 = require("react/jsx-runtime");
var AreaList = ({
areas,
selectedAreaId,
onSelectArea,
onRemoveArea,
onAddArea
}) => {
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "area-list", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "area-list-header", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }),
/* @__PURE__ */ (0, import_jsx_runtime2.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_runtime2.jsx)("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.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_runtime2.jsxs)(
"div",
{
className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`,
onClick: () => onSelectArea(area.id),
children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "area-item-info", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "area-item-name", children: [
"\uC601\uC5ED ",
index + 1
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "area-item-strength", children: [
"\uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] })
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.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_runtime3 = 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_runtime3.jsx)("div", { className: "parameter-panel", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) });
}
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "parameter-panel", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "parameter-group", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { children: [
"\uC65C\uACE1 \uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.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_runtime3.jsxs)("div", { className: "parameter-group", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { children: [
"\uC9C0\uC18D \uC2DC\uAC04: ",
area.movement.duration.toFixed(1),
"\uCD08"
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.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_runtime3.jsxs)("div", { className: "parameter-group", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { children: "\uC774\uC9D5 \uD568\uC218" }),
/* @__PURE__ */ (0, import_jsx_runtime3.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_runtime3.jsx)("option", { value: option.value, children: option.label }, option.value))
}
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "parameter-group", children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "point-coord", children: [
"P",
idx + 1,
": (",
point.x.toFixed(3),
", ",
point.y.toFixed(3),
")"
] }, idx)) })
] })
] });
};
// src/editor/DistortionEditor.tsx
var import_react7 = require("react");
// src/editor/hooks/useDistortionEditor.ts
var import_react5 = require("react");
@ -1210,66 +1063,9 @@ var useDistortionEditor = (initialAreas = []) => {
};
};
// src/editor/constants.ts
var DEFAULT_EDITOR_CANVAS_STYLE = {
// 3단계 원 스타일 (외부 -> 내부)
circleLevels: [
{
radius: 0.5,
opacity: 0.3,
lineWidth: 2,
color: "rgba(255, 200, 0, 1)",
dashPattern: [8, 4]
},
{
radius: 0.33,
opacity: 0.6,
lineWidth: 2.5,
color: "rgba(255, 200, 0, 1)",
dashPattern: [8, 4]
},
{
radius: 0.167,
opacity: 0.9,
lineWidth: 3,
color: "rgba(255, 200, 0, 1)",
dashPattern: [8, 4]
}
],
// 원 내부 채우기
circleFillColor: "rgba(255, 200, 0, 0.08)",
// 중심점
centerPoint: {
radius: 5,
fillColor: "rgba(255, 200, 0, 1)",
strokeColor: "rgba(255, 255, 255, 0.8)",
strokeWidth: 2
},
// 포인트 핸들
pointHandle: {
size: 16,
fillColor: "#00aaff",
strokeColor: "white",
strokeWidth: 2,
labelColor: "#00aaff",
labelFontSize: 11
},
// 영역 외곽선
areaOutline: {
selectedColor: "#00aaff",
unselectedColor: "#888",
selectedWidth: 2,
unselectedWidth: 1,
unselectedDashPattern: [5, 5],
selectedFillColor: "rgba(0, 170, 255, 0.08)",
// 선택된 영역 배경 (연한 파란색)
unselectedFillColor: "rgba(136, 136, 136, 0.03)"
// 선택 안된 영역 배경 (연한 회색)
}
};
// src/editor/components/EditorCanvas.tsx
var import_jsx_runtime4 = require("react/jsx-runtime");
var import_react6 = require("react");
var import_jsx_runtime2 = require("react/jsx-runtime");
var EditorCanvas = ({
areas,
selectedAreaId,
@ -1478,7 +1274,7 @@ var EditorCanvas = ({
if (isDraggingArea) return "grabbing";
return "default";
};
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
"div",
{
ref: containerRef,
@ -1497,8 +1293,8 @@ var EditorCanvas = ({
onTouchStart: showEditor ? handleCanvasDown : void 0,
onTouchMove: showEditor ? handleMove : void 0,
children: [
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"svg",
{
style: {
@ -1513,7 +1309,7 @@ var EditorCanvas = ({
const isSelected = area.id === selectedAreaId;
const points = area.basePoints;
const outlineStyle = editorStyle.areaOutline || {};
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("g", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
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(" "),
@ -1527,7 +1323,7 @@ var EditorCanvas = ({
})
}
),
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"canvas",
{
style: {
@ -1553,7 +1349,7 @@ var EditorCanvas = ({
),
showEditor && selectedArea && selectedArea.basePoints.map((point, index) => {
const handleStyle = editorStyle.pointHandle || {};
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div",
{
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
@ -1573,7 +1369,7 @@ var EditorCanvas = ({
},
onMouseDown: handlePointDown(index),
onTouchStart: handlePointDown(index),
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
"div",
{
style: {
@ -1601,16 +1397,362 @@ var EditorCanvas = ({
}
);
};
// 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,
AreaList,
DEFAULT_AREA,
DEFAULT_EDITOR_CANVAS_STYLE,
EditorCanvas,
DistortionEditor,
ImageDistortion,
ParameterPanel,
SHADER_CONFIG,
ShaderManager,
SpringPhysics,

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

577
dist/index.mjs vendored
View File

@ -935,152 +935,8 @@ var ImageDistortion = ({
);
};
// src/editor/components/EditorCanvas.tsx
import { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react";
// src/editor/components/AreaList.tsx
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var AreaList = ({
areas,
selectedAreaId,
onSelectArea,
onRemoveArea,
onAddArea
}) => {
return /* @__PURE__ */ jsxs("div", { className: "area-list", children: [
/* @__PURE__ */ jsxs("div", { className: "area-list-header", children: [
/* @__PURE__ */ jsx2("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }),
/* @__PURE__ */ jsx2(
"button",
{
onClick: onAddArea,
disabled: areas.length >= 8,
className: "btn-add",
title: areas.length >= 8 ? "\uCD5C\uB300 8\uAC1C \uC601\uC5ED\uAE4C\uC9C0 \uC9C0\uC6D0" : "\uC0C8 \uC601\uC5ED \uCD94\uAC00",
children: "+ \uCD94\uAC00"
}
)
] }),
/* @__PURE__ */ jsx2("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "area-list-empty", children: "\uC601\uC5ED\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. + \uCD94\uAC00 \uBC84\uD2BC\uC744 \uB20C\uB7EC\uC8FC\uC138\uC694." }) : areas.map((area, index) => /* @__PURE__ */ jsxs(
"div",
{
className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`,
onClick: () => onSelectArea(area.id),
children: [
/* @__PURE__ */ jsxs("div", { className: "area-item-info", children: [
/* @__PURE__ */ jsxs("span", { className: "area-item-name", children: [
"\uC601\uC5ED ",
index + 1
] }),
/* @__PURE__ */ jsxs("span", { className: "area-item-strength", children: [
"\uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] })
] }),
/* @__PURE__ */ jsx2(
"button",
{
onClick: (e) => {
e.stopPropagation();
onRemoveArea(area.id);
},
className: "btn-remove",
title: "\uC601\uC5ED \uC0AD\uC81C",
children: "\xD7"
}
)
]
},
area.id
)) })
] });
};
// src/editor/components/ParameterPanel.tsx
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
var EASING_OPTIONS = [
{ value: "linear", label: "\uC120\uD615 (Linear)" },
{ value: "easeIn", label: "\uAC00\uC18D (Ease In)" },
{ value: "easeOut", label: "\uAC10\uC18D (Ease Out)" },
{ value: "easeInOut", label: "\uAC00\uAC10\uC18D (Ease In Out)" },
{ value: "easeInQuad", label: "\uAC00\uC18D\xB2 (Ease In Quad)" },
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
];
var ParameterPanel = ({ area, onUpdateArea }) => {
if (!area) {
return /* @__PURE__ */ jsx3("div", { className: "parameter-panel", children: /* @__PURE__ */ jsx3("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) });
}
return /* @__PURE__ */ jsxs2("div", { className: "parameter-panel", children: [
/* @__PURE__ */ jsx3("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs2("label", { children: [
"\uC65C\uACE1 \uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] }),
/* @__PURE__ */ jsx3(
"input",
{
type: "range",
min: "0",
max: "1",
step: "0.01",
value: area.distortionStrength,
onChange: (e) => onUpdateArea({ distortionStrength: parseFloat(e.target.value) }),
className: "slider"
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs2("label", { children: [
"\uC9C0\uC18D \uC2DC\uAC04: ",
area.movement.duration.toFixed(1),
"\uCD08"
] }),
/* @__PURE__ */ jsx3(
"input",
{
type: "number",
min: "0.1",
max: "10",
step: "0.1",
value: area.movement.duration,
onChange: (e) => onUpdateArea({
movement: { ...area.movement, duration: parseFloat(e.target.value) }
}),
className: "input-number"
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsx3("label", { children: "\uC774\uC9D5 \uD568\uC218" }),
/* @__PURE__ */ jsx3(
"select",
{
value: area.movement.easing,
onChange: (e) => onUpdateArea({
movement: { ...area.movement, easing: e.target.value }
}),
className: "select",
children: EASING_OPTIONS.map((option) => /* @__PURE__ */ jsx3("option", { value: option.value, children: option.label }, option.value))
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsx3("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
/* @__PURE__ */ jsx3("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ jsxs2("div", { className: "point-coord", children: [
"P",
idx + 1,
": (",
point.x.toFixed(3),
", ",
point.y.toFixed(3),
")"
] }, idx)) })
] })
] });
};
// src/editor/DistortionEditor.tsx
import { useEffect as useEffect5, useState as useState5 } from "react";
// src/editor/hooks/useDistortionEditor.ts
import { useState as useState3, useCallback as useCallback4 } from "react";
@ -1156,66 +1012,9 @@ var useDistortionEditor = (initialAreas = []) => {
};
};
// src/editor/constants.ts
var DEFAULT_EDITOR_CANVAS_STYLE = {
// 3단계 원 스타일 (외부 -> 내부)
circleLevels: [
{
radius: 0.5,
opacity: 0.3,
lineWidth: 2,
color: "rgba(255, 200, 0, 1)",
dashPattern: [8, 4]
},
{
radius: 0.33,
opacity: 0.6,
lineWidth: 2.5,
color: "rgba(255, 200, 0, 1)",
dashPattern: [8, 4]
},
{
radius: 0.167,
opacity: 0.9,
lineWidth: 3,
color: "rgba(255, 200, 0, 1)",
dashPattern: [8, 4]
}
],
// 원 내부 채우기
circleFillColor: "rgba(255, 200, 0, 0.08)",
// 중심점
centerPoint: {
radius: 5,
fillColor: "rgba(255, 200, 0, 1)",
strokeColor: "rgba(255, 255, 255, 0.8)",
strokeWidth: 2
},
// 포인트 핸들
pointHandle: {
size: 16,
fillColor: "#00aaff",
strokeColor: "white",
strokeWidth: 2,
labelColor: "#00aaff",
labelFontSize: 11
},
// 영역 외곽선
areaOutline: {
selectedColor: "#00aaff",
unselectedColor: "#888",
selectedWidth: 2,
unselectedWidth: 1,
unselectedDashPattern: [5, 5],
selectedFillColor: "rgba(0, 170, 255, 0.08)",
// 선택된 영역 배경 (연한 파란색)
unselectedFillColor: "rgba(136, 136, 136, 0.03)"
// 선택 안된 영역 배경 (연한 회색)
}
};
// src/editor/components/EditorCanvas.tsx
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
import { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react";
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var EditorCanvas = ({
areas,
selectedAreaId,
@ -1424,7 +1223,7 @@ var EditorCanvas = ({
if (isDraggingArea) return "grabbing";
return "default";
};
return /* @__PURE__ */ jsxs3(
return /* @__PURE__ */ jsxs(
"div",
{
ref: containerRef,
@ -1443,8 +1242,8 @@ var EditorCanvas = ({
onTouchStart: showEditor ? handleCanvasDown : void 0,
onTouchMove: showEditor ? handleMove : void 0,
children: [
/* @__PURE__ */ jsx4(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ jsx4(
/* @__PURE__ */ jsx2(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ jsx2(
"svg",
{
style: {
@ -1459,7 +1258,7 @@ var EditorCanvas = ({
const isSelected = area.id === selectedAreaId;
const points = area.basePoints;
const outlineStyle = editorStyle.areaOutline || {};
return /* @__PURE__ */ jsx4("g", { children: /* @__PURE__ */ jsx4(
return /* @__PURE__ */ jsx2("g", { children: /* @__PURE__ */ jsx2(
"polygon",
{
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
@ -1473,7 +1272,7 @@ var EditorCanvas = ({
})
}
),
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ jsx4(
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ jsx2(
"canvas",
{
style: {
@ -1499,7 +1298,7 @@ var EditorCanvas = ({
),
showEditor && selectedArea && selectedArea.basePoints.map((point, index) => {
const handleStyle = editorStyle.pointHandle || {};
return /* @__PURE__ */ jsx4(
return /* @__PURE__ */ jsx2(
"div",
{
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
@ -1519,7 +1318,7 @@ var EditorCanvas = ({
},
onMouseDown: handlePointDown(index),
onTouchStart: handlePointDown(index),
children: /* @__PURE__ */ jsxs3(
children: /* @__PURE__ */ jsxs(
"div",
{
style: {
@ -1547,15 +1346,361 @@ var EditorCanvas = ({
}
);
};
// 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,
canvasStyle
}) => {
const {
state,
selectArea,
addArea,
removeArea,
updateArea,
updatePoint,
startDragging,
stopDragging,
getSelectedArea
} = useDistortionEditor(initialAreas);
const [showEditor, setShowEditor] = useState5(true);
useEffect5(() => {
onAreasChange?.(state.areas);
}, [state.areas, onAreasChange]);
useEffect5(() => {
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__ */ jsxs4("div", { className: "distortion-editor", children: [
/* @__PURE__ */ jsx5("div", { className: "editor-toolbar", children: /* @__PURE__ */ jsx5(
"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__ */ 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,
style: canvasStyle,
showEditor
}
) }),
/* @__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 })
] })
] })
] });
};
// 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)"
// 선택 안된 영역 배경 (연한 회색)
}
};
export {
ANIMATION_CONFIG,
AnimationLoop,
AreaList,
DEFAULT_AREA,
DEFAULT_EDITOR_CANVAS_STYLE,
EditorCanvas,
DistortionEditor,
ImageDistortion,
ParameterPanel,
SHADER_CONFIG,
ShaderManager,
SpringPhysics,

2
dist/index.mjs.map vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "@baekryang/responsive-image-canvas",
"version": "1.0.5",
"version": "1.0.1",
"publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
},

View File

@ -0,0 +1,125 @@
import React, { useEffect, useState } from 'react';
import { DistortionArea } from '../types/area';
import { DistortionEditorProps } from './types';
import { useDistortionEditor } from './hooks/useDistortionEditor';
import { EditorCanvas } from './components/EditorCanvas';
import { AreaList } from './components/AreaList';
import { ParameterPanel } from './components/ParameterPanel';
import { DEFAULT_AREA } from '../utils/constants';
export const DistortionEditor: React.FC<DistortionEditorProps> = ({
initialAreas = [],
imageSrc,
onAreasChange,
onSelectedAreaChange,
width = 800,
height = 600,
canvasStyle,
}) => {
const {
state,
selectArea,
addArea,
removeArea,
updateArea,
updatePoint,
startDragging,
stopDragging,
getSelectedArea,
} = useDistortionEditor(initialAreas);
// 에디터 모드 토글 상태
const [showEditor, setShowEditor] = useState(true);
// 영역 변경 시 콜백 호출
useEffect(() => {
onAreasChange?.(state.areas);
}, [state.areas, onAreasChange]);
// 선택된 영역 변경 시 콜백 호출
useEffect(() => {
onSelectedAreaChange?.(state.selectedAreaId);
}, [state.selectedAreaId, onSelectedAreaChange]);
// 새 영역 추가 핸들러
const handleAddArea = () => {
const newArea: DistortionArea = {
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 as any,
},
distortionStrength: DEFAULT_AREA.DISTORTION_STRENGTH,
progress: 0,
dragVector: { x: 0, y: 0 },
};
addArea(newArea);
};
// 파라미터 업데이트 핸들러
const handleUpdateArea = (updates: Partial<DistortionArea>) => {
if (state.selectedAreaId) {
updateArea(state.selectedAreaId, updates);
}
};
const selectedArea = getSelectedArea();
return (
<div className="distortion-editor">
{/* 에디터 모드 토글 버튼 */}
<div className="editor-toolbar">
<button
className={`editor-toggle-btn ${showEditor ? 'active' : ''}`}
onClick={() => setShowEditor(!showEditor)}
title={showEditor ? '에디터 숨기기 (왜곡 효과만 보기)' : '에디터 표시'}
>
{showEditor ? '👁️ 에디터 숨기기' : '✏️ 에디터 표시'}
</button>
</div>
<div className="editor-main">
{/* 왼쪽: 캔버스 */}
<div className="editor-canvas-container">
<EditorCanvas
areas={state.areas}
selectedAreaId={state.selectedAreaId}
imageSrc={imageSrc}
width={width}
height={height}
onUpdatePoint={updatePoint}
onUpdateArea={updateArea}
draggingPointIndex={state.draggingPointIndex}
onStartDragging={startDragging}
onStopDragging={stopDragging}
style={canvasStyle}
showEditor={showEditor}
/>
</div>
{/* 오른쪽: 사이드바 */}
<div className="editor-sidebar">
{/* 영역 목록 */}
<AreaList
areas={state.areas}
selectedAreaId={state.selectedAreaId}
onSelectArea={selectArea}
onRemoveArea={removeArea}
onAddArea={handleAddArea}
/>
{/* 파라미터 패널 */}
<ParameterPanel area={selectedArea} onUpdateArea={handleUpdateArea} />
</div>
</div>
</div>
);
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { DistortionArea } from '../../types/area';
export interface AreaListProps {
interface AreaListProps {
areas: DistortionArea[];
selectedAreaId: string | null;
onSelectArea: (areaId: string) => void;

View File

@ -4,7 +4,7 @@ import {ImageDistortion} from '@/components/ImageDistortion';
import {EditorCanvasStyle} from '../types';
import {DEFAULT_EDITOR_CANVAS_STYLE} from '@/editor';
export interface EditorCanvasProps {
interface EditorCanvasProps {
areas: DistortionArea[];
selectedAreaId: string | null;
imageSrc: string;

View File

@ -1,7 +1,7 @@
import React from 'react';
import { DistortionArea, EasingFunction } from '../../types/area';
export interface ParameterPanelProps {
interface ParameterPanelProps {
area: DistortionArea | null;
onUpdateArea: (updates: Partial<DistortionArea>) => void;
}
@ -84,6 +84,52 @@ export const ParameterPanel: React.FC<ParameterPanelProps> = ({ area, onUpdateAr
</select>
</div>
{/* 벡터 A (X) */}
<div className="parameter-group">
<label>
X: {area.movement.vectorA.x.toFixed(2)}
</label>
<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"
/>
</div>
{/* 벡터 A (Y) */}
<div className="parameter-group">
<label>
Y: {area.movement.vectorA.y.toFixed(2)}
</label>
<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"
/>
</div>
{/* 포인트 좌표 (읽기 전용 표시) */}
<div className="parameter-group">
<label> ( )</label>

View File

@ -1,8 +1,6 @@
export { EditorCanvas } from './components/EditorCanvas';
export { AreaList } from './components/AreaList';
export { ParameterPanel } from './components/ParameterPanel';
export { DistortionEditor } from './DistortionEditor';
export type {
DistortionEditorProps,
EditorState,
EditMode,
EditorCanvasStyle,

View File

@ -134,6 +134,4 @@ export interface DistortionEditorProps {
height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
canvasStyle?: EditorCanvasStyle;
/** 에디터 표시 여부 (외부에서 제어) */
showEditor?: boolean;
}

View File

@ -2,30 +2,10 @@
export { ImageDistortion } from './components/ImageDistortion';
export type { ImageDistortionProps } from './components/ImageDistortion';
// 에디터 컴포넌트들 (개별적으로 조합 가능)
export { EditorCanvas } from './editor/components/EditorCanvas';
export type { EditorCanvasProps } from './editor/components/EditorCanvas';
export { AreaList } from './editor/components/AreaList';
export type { AreaListProps } from './editor/components/AreaList';
export { ParameterPanel } from './editor/components/ParameterPanel';
export type { ParameterPanelProps } from './editor/components/ParameterPanel';
// 에디터 상태 관리 훅
export { useDistortionEditor } from './editor/hooks/useDistortionEditor';
// 에디터 타입 및 스타일
export type {
EditorState,
EditMode,
EditorCanvasStyle,
CircleLevelStyle,
CenterPointStyle,
PointHandleStyle,
AreaOutlineStyle,
} from './editor/types';
// 에디터 기본 스타일 상수
export { DEFAULT_EDITOR_CANVAS_STYLE } from './editor/constants';
// 에디터 (4점 사각형 + 정확한 UV 좌표계 원형 왜곡 가이드)
export { DistortionEditor } from './editor';
export type { DistortionEditorProps, EditorState, EditMode } from './editor';
export { useDistortionEditor } from './editor';
// 타입 정의
export type {