feat: Add AreaList and ParameterPanel components

- 영역 목록과 파라미터 편집 패널을 추가하여 왜곡 영역 관리를 개선했습니다.
- 각 영역의 강도, 애니메이션 지속 시간, 이징 함수 등을 조절할 수 있습니다.
- 새 영역 추가 및 기존 영역 삭제 기능을 제공합니다.
This commit is contained in:
BaekRyang 2025-11-24 15:16:48 +09:00
parent 6babf68c71
commit 5f6e780b40
13 changed files with 546 additions and 935 deletions

59
dist/index.d.mts vendored
View File

@ -305,27 +305,39 @@ interface EditorCanvasStyle {
/** 영역 외곽선 스타일 */
areaOutline?: AreaOutlineStyle;
}
/**
* Props
*/
interface DistortionEditorProps {
/** 초기 영역 배열 */
initialAreas?: DistortionArea[];
/** 이미지 소스 */
imageSrc: string;
/** 영역 변경 콜백 */
onAreasChange?: (areas: DistortionArea[]) => void;
/** 선택된 영역 변경 콜백 */
onSelectedAreaChange?: (areaId: string | null) => void;
/** 캔버스 너비 */
width?: number;
/** 캔버스 높이 */
height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
canvasStyle?: EditorCanvasStyle;
}
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
interface EditorCanvasProps {
areas: DistortionArea[];
selectedAreaId: string | null;
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;
/** 에디터 캔버스 스타일 커스터마이징 */
style?: EditorCanvasStyle;
/** 에디터 UI 표시 여부 (기본값: true) */
showEditor?: boolean;
}
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 useDistortionEditor: (initialAreas?: DistortionArea[]) => {
state: EditorState;
@ -340,6 +352,11 @@ declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
getSelectedArea: () => DistortionArea | null;
};
/**
*
*/
declare const DEFAULT_EDITOR_CANVAS_STYLE: EditorCanvasStyle;
/**
*
* @param progress (0.0 - 1.0)
@ -569,4 +586,4 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
getInteractingAreaIndices: () => Set<number>;
};
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 };
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 };

59
dist/index.d.ts vendored
View File

@ -305,27 +305,39 @@ interface EditorCanvasStyle {
/** 영역 외곽선 스타일 */
areaOutline?: AreaOutlineStyle;
}
/**
* Props
*/
interface DistortionEditorProps {
/** 초기 영역 배열 */
initialAreas?: DistortionArea[];
/** 이미지 소스 */
imageSrc: string;
/** 영역 변경 콜백 */
onAreasChange?: (areas: DistortionArea[]) => void;
/** 선택된 영역 변경 콜백 */
onSelectedAreaChange?: (areaId: string | null) => void;
/** 캔버스 너비 */
width?: number;
/** 캔버스 높이 */
height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
canvasStyle?: EditorCanvasStyle;
}
declare const DistortionEditor: React$1.FC<DistortionEditorProps>;
interface EditorCanvasProps {
areas: DistortionArea[];
selectedAreaId: string | null;
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;
/** 에디터 캔버스 스타일 커스터마이징 */
style?: EditorCanvasStyle;
/** 에디터 UI 표시 여부 (기본값: true) */
showEditor?: boolean;
}
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 useDistortionEditor: (initialAreas?: DistortionArea[]) => {
state: EditorState;
@ -340,6 +352,11 @@ declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
getSelectedArea: () => DistortionArea | null;
};
/**
*
*/
declare const DEFAULT_EDITOR_CANVAS_STYLE: EditorCanvasStyle;
/**
*
* @param progress (0.0 - 1.0)
@ -569,4 +586,4 @@ declare const useMouseInteraction: (containerRef: React.RefObject<HTMLElement |
getInteractingAreaIndices: () => Set<number>;
};
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 };
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 };

582
dist/index.js vendored
View File

@ -32,9 +32,12 @@ var index_exports = {};
__export(index_exports, {
ANIMATION_CONFIG: () => ANIMATION_CONFIG,
AnimationLoop: () => AnimationLoop,
AreaList: () => AreaList,
DEFAULT_AREA: () => DEFAULT_AREA,
DistortionEditor: () => DistortionEditor,
DEFAULT_EDITOR_CANVAS_STYLE: () => DEFAULT_EDITOR_CANVAS_STYLE,
EditorCanvas: () => EditorCanvas,
ImageDistortion: () => ImageDistortion,
ParameterPanel: () => ParameterPanel,
SHADER_CONFIG: () => SHADER_CONFIG,
ShaderManager: () => ShaderManager,
SpringPhysics: () => SpringPhysics,
@ -986,8 +989,152 @@ var ImageDistortion = ({
);
};
// src/editor/DistortionEditor.tsx
var import_react7 = require("react");
// 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/hooks/useDistortionEditor.ts
var import_react5 = require("react");
@ -1063,9 +1210,66 @@ 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_react6 = require("react");
var import_jsx_runtime2 = require("react/jsx-runtime");
var import_jsx_runtime4 = require("react/jsx-runtime");
var EditorCanvas = ({
areas,
selectedAreaId,
@ -1274,7 +1478,7 @@ var EditorCanvas = ({
if (isDraggingArea) return "grabbing";
return "default";
};
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
"div",
{
ref: containerRef,
@ -1293,8 +1497,8 @@ var EditorCanvas = ({
onTouchStart: showEditor ? handleCanvasDown : void 0,
onTouchMove: showEditor ? handleMove : void 0,
children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"svg",
{
style: {
@ -1309,7 +1513,7 @@ var EditorCanvas = ({
const isSelected = area.id === selectedAreaId;
const points = area.basePoints;
const outlineStyle = editorStyle.areaOutline || {};
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("g", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("g", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"polygon",
{
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
@ -1323,7 +1527,7 @@ var EditorCanvas = ({
})
}
),
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"canvas",
{
style: {
@ -1349,7 +1553,7 @@ var EditorCanvas = ({
),
showEditor && selectedArea && selectedArea.basePoints.map((point, index) => {
const handleStyle = editorStyle.pointHandle || {};
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
"div",
{
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
@ -1369,7 +1573,7 @@ var EditorCanvas = ({
},
onMouseDown: handlePointDown(index),
onTouchStart: handlePointDown(index),
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
"div",
{
style: {
@ -1397,362 +1601,16 @@ 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,
DistortionEditor,
DEFAULT_EDITOR_CANVAS_STYLE,
EditorCanvas,
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,8 +935,152 @@ var ImageDistortion = ({
);
};
// src/editor/DistortionEditor.tsx
import { useEffect as useEffect5, useState as useState5 } from "react";
// src/editor/components/EditorCanvas.tsx
import { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react";
// src/editor/components/AreaList.tsx
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var AreaList = ({
areas,
selectedAreaId,
onSelectArea,
onRemoveArea,
onAddArea
}) => {
return /* @__PURE__ */ jsxs("div", { className: "area-list", children: [
/* @__PURE__ */ jsxs("div", { className: "area-list-header", children: [
/* @__PURE__ */ jsx2("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }),
/* @__PURE__ */ jsx2(
"button",
{
onClick: onAddArea,
disabled: areas.length >= 8,
className: "btn-add",
title: areas.length >= 8 ? "\uCD5C\uB300 8\uAC1C \uC601\uC5ED\uAE4C\uC9C0 \uC9C0\uC6D0" : "\uC0C8 \uC601\uC5ED \uCD94\uAC00",
children: "+ \uCD94\uAC00"
}
)
] }),
/* @__PURE__ */ jsx2("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ jsx2("div", { className: "area-list-empty", children: "\uC601\uC5ED\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. + \uCD94\uAC00 \uBC84\uD2BC\uC744 \uB20C\uB7EC\uC8FC\uC138\uC694." }) : areas.map((area, index) => /* @__PURE__ */ jsxs(
"div",
{
className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`,
onClick: () => onSelectArea(area.id),
children: [
/* @__PURE__ */ jsxs("div", { className: "area-item-info", children: [
/* @__PURE__ */ jsxs("span", { className: "area-item-name", children: [
"\uC601\uC5ED ",
index + 1
] }),
/* @__PURE__ */ jsxs("span", { className: "area-item-strength", children: [
"\uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] })
] }),
/* @__PURE__ */ jsx2(
"button",
{
onClick: (e) => {
e.stopPropagation();
onRemoveArea(area.id);
},
className: "btn-remove",
title: "\uC601\uC5ED \uC0AD\uC81C",
children: "\xD7"
}
)
]
},
area.id
)) })
] });
};
// src/editor/components/ParameterPanel.tsx
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
var EASING_OPTIONS = [
{ value: "linear", label: "\uC120\uD615 (Linear)" },
{ value: "easeIn", label: "\uAC00\uC18D (Ease In)" },
{ value: "easeOut", label: "\uAC10\uC18D (Ease Out)" },
{ value: "easeInOut", label: "\uAC00\uAC10\uC18D (Ease In Out)" },
{ value: "easeInQuad", label: "\uAC00\uC18D\xB2 (Ease In Quad)" },
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
];
var ParameterPanel = ({ area, onUpdateArea }) => {
if (!area) {
return /* @__PURE__ */ jsx3("div", { className: "parameter-panel", children: /* @__PURE__ */ jsx3("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) });
}
return /* @__PURE__ */ jsxs2("div", { className: "parameter-panel", children: [
/* @__PURE__ */ jsx3("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs2("label", { children: [
"\uC65C\uACE1 \uAC15\uB3C4: ",
(area.distortionStrength * 100).toFixed(0),
"%"
] }),
/* @__PURE__ */ jsx3(
"input",
{
type: "range",
min: "0",
max: "1",
step: "0.01",
value: area.distortionStrength,
onChange: (e) => onUpdateArea({ distortionStrength: parseFloat(e.target.value) }),
className: "slider"
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsxs2("label", { children: [
"\uC9C0\uC18D \uC2DC\uAC04: ",
area.movement.duration.toFixed(1),
"\uCD08"
] }),
/* @__PURE__ */ jsx3(
"input",
{
type: "number",
min: "0.1",
max: "10",
step: "0.1",
value: area.movement.duration,
onChange: (e) => onUpdateArea({
movement: { ...area.movement, duration: parseFloat(e.target.value) }
}),
className: "input-number"
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsx3("label", { children: "\uC774\uC9D5 \uD568\uC218" }),
/* @__PURE__ */ jsx3(
"select",
{
value: area.movement.easing,
onChange: (e) => onUpdateArea({
movement: { ...area.movement, easing: e.target.value }
}),
className: "select",
children: EASING_OPTIONS.map((option) => /* @__PURE__ */ jsx3("option", { value: option.value, children: option.label }, option.value))
}
)
] }),
/* @__PURE__ */ jsxs2("div", { className: "parameter-group", children: [
/* @__PURE__ */ jsx3("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
/* @__PURE__ */ jsx3("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ jsxs2("div", { className: "point-coord", children: [
"P",
idx + 1,
": (",
point.x.toFixed(3),
", ",
point.y.toFixed(3),
")"
] }, idx)) })
] })
] });
};
// src/editor/hooks/useDistortionEditor.ts
import { useState as useState3, useCallback as useCallback4 } from "react";
@ -1012,9 +1156,66 @@ 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 { useRef as useRef5, useEffect as useEffect4, useState as useState4, useCallback as useCallback5, useMemo } from "react";
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
var EditorCanvas = ({
areas,
selectedAreaId,
@ -1223,7 +1424,7 @@ var EditorCanvas = ({
if (isDraggingArea) return "grabbing";
return "default";
};
return /* @__PURE__ */ jsxs(
return /* @__PURE__ */ jsxs3(
"div",
{
ref: containerRef,
@ -1242,8 +1443,8 @@ var EditorCanvas = ({
onTouchStart: showEditor ? handleCanvasDown : void 0,
onTouchMove: showEditor ? handleMove : void 0,
children: [
/* @__PURE__ */ jsx2(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ jsx2(
/* @__PURE__ */ jsx4(ImageDistortion, { imageSrc, areas }),
showEditor && /* @__PURE__ */ jsx4(
"svg",
{
style: {
@ -1258,7 +1459,7 @@ var EditorCanvas = ({
const isSelected = area.id === selectedAreaId;
const points = area.basePoints;
const outlineStyle = editorStyle.areaOutline || {};
return /* @__PURE__ */ jsx2("g", { children: /* @__PURE__ */ jsx2(
return /* @__PURE__ */ jsx4("g", { children: /* @__PURE__ */ jsx4(
"polygon",
{
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
@ -1272,7 +1473,7 @@ var EditorCanvas = ({
})
}
),
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ jsx2(
showEditor && selectedArea && canvasSize.width > 0 && /* @__PURE__ */ jsx4(
"canvas",
{
style: {
@ -1298,7 +1499,7 @@ var EditorCanvas = ({
),
showEditor && selectedArea && selectedArea.basePoints.map((point, index) => {
const handleStyle = editorStyle.pointHandle || {};
return /* @__PURE__ */ jsx2(
return /* @__PURE__ */ jsx4(
"div",
{
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
@ -1318,7 +1519,7 @@ var EditorCanvas = ({
},
onMouseDown: handlePointDown(index),
onTouchStart: handlePointDown(index),
children: /* @__PURE__ */ jsxs(
children: /* @__PURE__ */ jsxs3(
"div",
{
style: {
@ -1346,361 +1547,15 @@ 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,
DistortionEditor,
DEFAULT_EDITOR_CANVAS_STYLE,
EditorCanvas,
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.3",
"version": "1.0.5",
"publishConfig": {
"registry": "https://git.bnovalab.com/api/packages/baekryang/npm/"
},

View File

@ -1,112 +0,0 @@
import React, { useEffect } 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,
showEditor = true,
}) => {
const {
state,
selectArea,
addArea,
removeArea,
updateArea,
updatePoint,
startDragging,
stopDragging,
getSelectedArea,
} = useDistortionEditor(initialAreas);
// 영역 변경 시 콜백 호출
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-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';
interface AreaListProps {
export 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';
interface EditorCanvasProps {
export 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';
interface ParameterPanelProps {
export interface ParameterPanelProps {
area: DistortionArea | null;
onUpdateArea: (updates: Partial<DistortionArea>) => void;
}
@ -84,52 +84,6 @@ 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,6 +1,8 @@
export { DistortionEditor } from './DistortionEditor';
export { EditorCanvas } from './components/EditorCanvas';
export { AreaList } from './components/AreaList';
export { ParameterPanel } from './components/ParameterPanel';
export type {
DistortionEditorProps,
EditorState,
EditMode,
EditorCanvasStyle,

View File

@ -2,10 +2,30 @@
export { ImageDistortion } from './components/ImageDistortion';
export type { ImageDistortionProps } from './components/ImageDistortion';
// 에디터 (4점 사각형 + 정확한 UV 좌표계 원형 왜곡 가이드)
export { DistortionEditor } from './editor';
export type { DistortionEditorProps, EditorState, EditMode } from './editor';
export { useDistortionEditor } from './editor';
// 에디터 컴포넌트들 (개별적으로 조합 가능)
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';
// 타입 정의
export type {