feat: Add canvas style customization

- 왜곡 영역 원 레벨 스타일, 중심점, 포인트 핸들, 영역 외곽선 등
  캔버스 스타일을 커스터마이징할 수 있도록 `EditorCanvasStyle` 타입을
  추가했습니다.
- `DistortionEditorProps`에 `canvasStyle` prop을 추가하여
  외부에서 캔버스 스타일을 전달받을 수 있도록 했습니다.
- `EditorCanvas` 컴포넌트에서 `useMemo`를 사용하여
  기본 스타일과 사용자 정의 스타일을 병합하고, 이를 렌더링에
  반영하도록 수정했습니다.
This commit is contained in:
BaekRyang 2025-11-05 11:48:05 +09:00
parent d621d5b691
commit 0c3c0b606e
10 changed files with 713 additions and 279 deletions

81
dist/index.d.mts vendored
View File

@ -149,6 +149,85 @@ interface EditorState {
/** 드래그 중인 포인트 인덱스 (0-3) */ /** 드래그 중인 포인트 인덱스 (0-3) */
draggingPointIndex: number | null; draggingPointIndex: number | null;
} }
/**
*
*/
interface CircleLevelStyle {
/** 반지름 (0.0 - 1.0, UV 좌표) */
radius: number;
/** 투명도 (0.0 - 1.0) */
opacity: number;
/** 선 두께 (픽셀) */
lineWidth: number;
/** 선 색상 (CSS color) */
color?: string;
/** 대시 패턴 [dash, gap] */
dashPattern?: [number, number];
}
/**
*
*/
interface CenterPointStyle {
/** 반지름 (픽셀) */
radius?: number;
/** 채우기 색상 */
fillColor?: string;
/** 테두리 색상 */
strokeColor?: string;
/** 테두리 두께 */
strokeWidth?: number;
}
/**
*
*/
interface PointHandleStyle {
/** 핸들 크기 (픽셀) */
size?: number;
/** 채우기 색상 */
fillColor?: string;
/** 테두리 색상 */
strokeColor?: string;
/** 테두리 두께 */
strokeWidth?: number;
/** 레이블 색상 */
labelColor?: string;
/** 레이블 폰트 크기 */
labelFontSize?: number;
}
/**
*
*/
interface AreaOutlineStyle {
/** 선택된 영역 색상 */
selectedColor?: string;
/** 선택되지 않은 영역 색상 */
unselectedColor?: string;
/** 선택된 영역 선 두께 */
selectedWidth?: number;
/** 선택되지 않은 영역 선 두께 */
unselectedWidth?: number;
/** 선택되지 않은 영역 대시 패턴 */
unselectedDashPattern?: [number, number];
/** 선택된 영역 배경 채우기 색상 */
selectedFillColor?: string;
/** 선택되지 않은 영역 배경 채우기 색상 */
unselectedFillColor?: string;
}
/**
*
*/
interface EditorCanvasStyle {
/** 왜곡 영역 원 레벨 스타일 배열 (외부 -> 내부 순) */
circleLevels?: CircleLevelStyle[];
/** 왜곡 영역 내부 채우기 색상 */
circleFillColor?: string;
/** 중심점 스타일 */
centerPoint?: CenterPointStyle;
/** 포인트 핸들 스타일 */
pointHandle?: PointHandleStyle;
/** 영역 외곽선 스타일 */
areaOutline?: AreaOutlineStyle;
}
/** /**
* Props * Props
*/ */
@ -165,6 +244,8 @@ interface DistortionEditorProps {
width?: number; width?: number;
/** 캔버스 높이 */ /** 캔버스 높이 */
height?: number; height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
canvasStyle?: EditorCanvasStyle;
} }
declare const DistortionEditor: React.FC<DistortionEditorProps>; declare const DistortionEditor: React.FC<DistortionEditorProps>;

81
dist/index.d.ts vendored
View File

@ -149,6 +149,85 @@ interface EditorState {
/** 드래그 중인 포인트 인덱스 (0-3) */ /** 드래그 중인 포인트 인덱스 (0-3) */
draggingPointIndex: number | null; draggingPointIndex: number | null;
} }
/**
*
*/
interface CircleLevelStyle {
/** 반지름 (0.0 - 1.0, UV 좌표) */
radius: number;
/** 투명도 (0.0 - 1.0) */
opacity: number;
/** 선 두께 (픽셀) */
lineWidth: number;
/** 선 색상 (CSS color) */
color?: string;
/** 대시 패턴 [dash, gap] */
dashPattern?: [number, number];
}
/**
*
*/
interface CenterPointStyle {
/** 반지름 (픽셀) */
radius?: number;
/** 채우기 색상 */
fillColor?: string;
/** 테두리 색상 */
strokeColor?: string;
/** 테두리 두께 */
strokeWidth?: number;
}
/**
*
*/
interface PointHandleStyle {
/** 핸들 크기 (픽셀) */
size?: number;
/** 채우기 색상 */
fillColor?: string;
/** 테두리 색상 */
strokeColor?: string;
/** 테두리 두께 */
strokeWidth?: number;
/** 레이블 색상 */
labelColor?: string;
/** 레이블 폰트 크기 */
labelFontSize?: number;
}
/**
*
*/
interface AreaOutlineStyle {
/** 선택된 영역 색상 */
selectedColor?: string;
/** 선택되지 않은 영역 색상 */
unselectedColor?: string;
/** 선택된 영역 선 두께 */
selectedWidth?: number;
/** 선택되지 않은 영역 선 두께 */
unselectedWidth?: number;
/** 선택되지 않은 영역 대시 패턴 */
unselectedDashPattern?: [number, number];
/** 선택된 영역 배경 채우기 색상 */
selectedFillColor?: string;
/** 선택되지 않은 영역 배경 채우기 색상 */
unselectedFillColor?: string;
}
/**
*
*/
interface EditorCanvasStyle {
/** 왜곡 영역 원 레벨 스타일 배열 (외부 -> 내부 순) */
circleLevels?: CircleLevelStyle[];
/** 왜곡 영역 내부 채우기 색상 */
circleFillColor?: string;
/** 중심점 스타일 */
centerPoint?: CenterPointStyle;
/** 포인트 핸들 스타일 */
pointHandle?: PointHandleStyle;
/** 영역 외곽선 스타일 */
areaOutline?: AreaOutlineStyle;
}
/** /**
* Props * Props
*/ */
@ -165,6 +244,8 @@ interface DistortionEditorProps {
width?: number; width?: number;
/** 캔버스 높이 */ /** 캔버스 높이 */
height?: number; height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
canvasStyle?: EditorCanvasStyle;
} }
declare const DistortionEditor: React.FC<DistortionEditorProps>; declare const DistortionEditor: React.FC<DistortionEditorProps>;

188
dist/index.js vendored
View File

@ -583,6 +583,66 @@ var useDistortionEditor = (initialAreas = []) => {
// src/editor/components/EditorCanvas.tsx // src/editor/components/EditorCanvas.tsx
var import_react4 = require("react"); var import_react4 = require("react");
// 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_runtime2 = require("react/jsx-runtime"); var import_jsx_runtime2 = require("react/jsx-runtime");
var EditorCanvas = ({ var EditorCanvas = ({
areas, areas,
@ -594,12 +654,30 @@ var EditorCanvas = ({
onUpdateArea, onUpdateArea,
draggingPointIndex, draggingPointIndex,
onStartDragging, onStartDragging,
onStopDragging onStopDragging,
style: customStyle
}) => { }) => {
const containerRef = (0, import_react4.useRef)(null); const containerRef = (0, import_react4.useRef)(null);
const [canvasSize, setCanvasSize] = (0, import_react4.useState)({ width: 0, height: 0 }); const [canvasSize, setCanvasSize] = (0, import_react4.useState)({ width: 0, height: 0 });
const [isDraggingArea, setIsDraggingArea] = (0, import_react4.useState)(false); const [isDraggingArea, setIsDraggingArea] = (0, import_react4.useState)(false);
const [dragStartPos, setDragStartPos] = (0, import_react4.useState)(null); const [dragStartPos, setDragStartPos] = (0, import_react4.useState)(null);
const editorStyle = (0, import_react4.useMemo)(() => ({
...DEFAULT_EDITOR_CANVAS_STYLE,
...customStyle,
circleLevels: customStyle?.circleLevels || DEFAULT_EDITOR_CANVAS_STYLE.circleLevels,
centerPoint: {
...DEFAULT_EDITOR_CANVAS_STYLE.centerPoint,
...customStyle?.centerPoint
},
pointHandle: {
...DEFAULT_EDITOR_CANVAS_STYLE.pointHandle,
...customStyle?.pointHandle
},
areaOutline: {
...DEFAULT_EDITOR_CANVAS_STYLE.areaOutline,
...customStyle?.areaOutline
}
}), [customStyle]);
(0, import_react4.useEffect)(() => { (0, import_react4.useEffect)(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
@ -690,63 +768,57 @@ var EditorCanvas = ({
y: posY * canvasHeight y: posY * canvasHeight
}; };
}; };
const drawDistortionCircle = (ctx, points, canvasWidth, canvasHeight) => { const drawDistortionCircle = (0, import_react4.useCallback)((ctx, points, canvasWidth, canvasHeight) => {
const segments = 128; const segments = 128;
const centerU = 0.5; const centerU = 0.5;
const centerV = 0.5; const centerV = 0.5;
const maxRadius = 0.5; const circleLevels = editorStyle.circleLevels || [];
const circlePoints = []; circleLevels.forEach((level, index) => {
const levelPoints = [];
for (let i = 0; i <= segments; i++) { for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI; const theta = i / segments * 2 * Math.PI;
const u = centerU - maxRadius * Math.sin(theta); const u = centerU - level.radius * Math.sin(theta);
const v = centerV + maxRadius * Math.cos(theta); const v = centerV + level.radius * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight); const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
circlePoints.push(pixelPos); levelPoints.push(pixelPos);
} }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(circlePoints[0].x, circlePoints[0].y); ctx.moveTo(levelPoints[0].x, levelPoints[0].y);
for (let i = 1; i < circlePoints.length; i++) { for (let i = 1; i < levelPoints.length; i++) {
ctx.lineTo(circlePoints[i].x, circlePoints[i].y); ctx.lineTo(levelPoints[i].x, levelPoints[i].y);
} }
ctx.closePath(); ctx.closePath();
ctx.strokeStyle = "rgba(255, 200, 0, 0.9)"; const baseColor = level.color || "rgba(255, 200, 0, 1)";
ctx.lineWidth = 3; const colorWithOpacity = baseColor.replace(/rgba?\(([^)]+)\)/, (_, rgb) => {
ctx.setLineDash([8, 4]); const parts = rgb.split(",").map((p) => p.trim());
return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${level.opacity})`;
});
ctx.strokeStyle = colorWithOpacity;
ctx.lineWidth = level.lineWidth;
if (level.dashPattern) {
ctx.setLineDash(level.dashPattern);
}
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]); ctx.setLineDash([]);
ctx.fillStyle = "rgba(255, 200, 0, 0.12)"; if (index === 0 && editorStyle.circleFillColor) {
ctx.fillStyle = editorStyle.circleFillColor;
ctx.fill(); ctx.fill();
for (const r of [0.25, 0.375]) {
const gradientPoints = [];
for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI;
const u = centerU - r * Math.sin(theta);
const v = centerV + r * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
gradientPoints.push(pixelPos);
}
ctx.beginPath();
ctx.moveTo(gradientPoints[0].x, gradientPoints[0].y);
for (let i = 1; i < gradientPoints.length; i++) {
ctx.lineTo(gradientPoints[i].x, gradientPoints[i].y);
}
ctx.closePath();
const alpha = r / maxRadius;
ctx.strokeStyle = `rgba(255, 220, 100, ${0.3 * alpha})`;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.stroke();
ctx.setLineDash([]);
} }
});
const centerPointStyle = editorStyle.centerPoint || {};
const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight); const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight);
ctx.beginPath(); ctx.beginPath();
ctx.arc(centerPixel.x, centerPixel.y, 5, 0, 2 * Math.PI); ctx.arc(centerPixel.x, centerPixel.y, centerPointStyle.radius || 5, 0, 2 * Math.PI);
ctx.fillStyle = "rgba(255, 200, 0, 1)"; if (centerPointStyle.fillColor) {
ctx.fillStyle = centerPointStyle.fillColor;
ctx.fill(); ctx.fill();
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)"; }
ctx.lineWidth = 2; if (centerPointStyle.strokeColor) {
ctx.strokeStyle = centerPointStyle.strokeColor;
ctx.lineWidth = centerPointStyle.strokeWidth || 2;
ctx.stroke(); ctx.stroke();
}; }
}, [editorStyle]);
const getCursorStyle = () => { const getCursorStyle = () => {
if (draggingPointIndex !== null) return "grabbing"; if (draggingPointIndex !== null) return "grabbing";
if (isDraggingArea) return "grabbing"; if (isDraggingArea) return "grabbing";
@ -761,7 +833,7 @@ var EditorCanvas = ({
onMouseDown: handleCanvasMouseDown, onMouseDown: handleCanvasMouseDown,
onMouseMove: handleMouseMove, onMouseMove: handleMouseMove,
children: [ children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas, width, height }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)( /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"svg", "svg",
{ {
@ -776,14 +848,15 @@ var EditorCanvas = ({
children: areas.map((area) => { children: areas.map((area) => {
const isSelected = area.id === selectedAreaId; const isSelected = area.id === selectedAreaId;
const points = area.basePoints; 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_runtime2.jsx)("g", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"polygon", "polygon",
{ {
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "), points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
fill: "none", fill: isSelected ? outlineStyle.selectedFillColor || "rgba(0, 170, 255, 0.08)" : outlineStyle.unselectedFillColor || "rgba(136, 136, 136, 0.03)",
stroke: isSelected ? "#00aaff" : "#888", stroke: isSelected ? outlineStyle.selectedColor || "#00aaff" : outlineStyle.unselectedColor || "#888",
strokeWidth: isSelected ? 2 : 1, strokeWidth: isSelected ? outlineStyle.selectedWidth || 2 : outlineStyle.unselectedWidth || 1,
strokeDasharray: isSelected ? "0" : "5,5", strokeDasharray: isSelected ? "0" : outlineStyle.unselectedDashPattern?.join(",") || "5,5",
opacity: isSelected ? 1 : 0.5 opacity: isSelected ? 1 : 0.5
} }
) }, area.id); ) }, area.id);
@ -814,7 +887,9 @@ var EditorCanvas = ({
} }
} }
), ),
selectedArea && selectedArea.basePoints.map((point, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( selectedArea && selectedArea.basePoints.map((point, index) => {
const handleStyle = editorStyle.pointHandle || {};
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"div", "div",
{ {
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`, className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
@ -823,11 +898,11 @@ var EditorCanvas = ({
left: `${point.x * 100}%`, left: `${point.x * 100}%`,
top: `${point.y * 100}%`, top: `${point.y * 100}%`,
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
width: 16, width: handleStyle.size || 16,
height: 16, height: handleStyle.size || 16,
borderRadius: "50%", borderRadius: "50%",
backgroundColor: "#00aaff", backgroundColor: handleStyle.fillColor || "#00aaff",
border: "2px solid white", border: `${handleStyle.strokeWidth || 2}px solid ${handleStyle.strokeColor || "white"}`,
cursor: "grab", cursor: "grab",
pointerEvents: "auto", pointerEvents: "auto",
boxShadow: "0 2px 4px rgba(0,0,0,0.3)" boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
@ -841,8 +916,8 @@ var EditorCanvas = ({
top: -24, top: -24,
left: "50%", left: "50%",
transform: "translateX(-50%)", transform: "translateX(-50%)",
fontSize: 11, fontSize: handleStyle.labelFontSize || 11,
color: "#00aaff", color: handleStyle.labelColor || "#00aaff",
fontWeight: "bold", fontWeight: "bold",
textShadow: "1px 1px 2px rgba(0,0,0,0.8)", textShadow: "1px 1px 2px rgba(0,0,0,0.8)",
whiteSpace: "nowrap" whiteSpace: "nowrap"
@ -855,7 +930,8 @@ var EditorCanvas = ({
) )
}, },
index index
)) );
})
] ]
} }
); );
@ -1059,7 +1135,8 @@ var DistortionEditor = ({
onAreasChange, onAreasChange,
onSelectedAreaChange, onSelectedAreaChange,
width = 800, width = 800,
height = 600 height = 600,
canvasStyle
}) => { }) => {
const { const {
state, state,
@ -1118,7 +1195,8 @@ var DistortionEditor = ({
onUpdateArea: updateArea, onUpdateArea: updateArea,
draggingPointIndex: state.draggingPointIndex, draggingPointIndex: state.draggingPointIndex,
onStartDragging: startDragging, onStartDragging: startDragging,
onStopDragging: stopDragging onStopDragging: stopDragging,
style: canvasStyle
} }
) }), ) }),
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-sidebar", children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-sidebar", children: [

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

190
dist/index.mjs vendored
View File

@ -536,7 +536,67 @@ var useDistortionEditor = (initialAreas = []) => {
}; };
// src/editor/components/EditorCanvas.tsx // src/editor/components/EditorCanvas.tsx
import { useRef as useRef3, useEffect as useEffect3, useState as useState3, useCallback as useCallback3 } from "react"; import { useRef as useRef3, useEffect as useEffect3, useState as useState3, useCallback as useCallback3, useMemo } from "react";
// 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 jsx2, jsxs } from "react/jsx-runtime"; import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var EditorCanvas = ({ var EditorCanvas = ({
areas, areas,
@ -548,12 +608,30 @@ var EditorCanvas = ({
onUpdateArea, onUpdateArea,
draggingPointIndex, draggingPointIndex,
onStartDragging, onStartDragging,
onStopDragging onStopDragging,
style: customStyle
}) => { }) => {
const containerRef = useRef3(null); const containerRef = useRef3(null);
const [canvasSize, setCanvasSize] = useState3({ width: 0, height: 0 }); const [canvasSize, setCanvasSize] = useState3({ width: 0, height: 0 });
const [isDraggingArea, setIsDraggingArea] = useState3(false); const [isDraggingArea, setIsDraggingArea] = useState3(false);
const [dragStartPos, setDragStartPos] = useState3(null); const [dragStartPos, setDragStartPos] = useState3(null);
const editorStyle = useMemo(() => ({
...DEFAULT_EDITOR_CANVAS_STYLE,
...customStyle,
circleLevels: customStyle?.circleLevels || DEFAULT_EDITOR_CANVAS_STYLE.circleLevels,
centerPoint: {
...DEFAULT_EDITOR_CANVAS_STYLE.centerPoint,
...customStyle?.centerPoint
},
pointHandle: {
...DEFAULT_EDITOR_CANVAS_STYLE.pointHandle,
...customStyle?.pointHandle
},
areaOutline: {
...DEFAULT_EDITOR_CANVAS_STYLE.areaOutline,
...customStyle?.areaOutline
}
}), [customStyle]);
useEffect3(() => { useEffect3(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect(); const rect = containerRef.current.getBoundingClientRect();
@ -644,63 +722,57 @@ var EditorCanvas = ({
y: posY * canvasHeight y: posY * canvasHeight
}; };
}; };
const drawDistortionCircle = (ctx, points, canvasWidth, canvasHeight) => { const drawDistortionCircle = useCallback3((ctx, points, canvasWidth, canvasHeight) => {
const segments = 128; const segments = 128;
const centerU = 0.5; const centerU = 0.5;
const centerV = 0.5; const centerV = 0.5;
const maxRadius = 0.5; const circleLevels = editorStyle.circleLevels || [];
const circlePoints = []; circleLevels.forEach((level, index) => {
const levelPoints = [];
for (let i = 0; i <= segments; i++) { for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI; const theta = i / segments * 2 * Math.PI;
const u = centerU - maxRadius * Math.sin(theta); const u = centerU - level.radius * Math.sin(theta);
const v = centerV + maxRadius * Math.cos(theta); const v = centerV + level.radius * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight); const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
circlePoints.push(pixelPos); levelPoints.push(pixelPos);
} }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(circlePoints[0].x, circlePoints[0].y); ctx.moveTo(levelPoints[0].x, levelPoints[0].y);
for (let i = 1; i < circlePoints.length; i++) { for (let i = 1; i < levelPoints.length; i++) {
ctx.lineTo(circlePoints[i].x, circlePoints[i].y); ctx.lineTo(levelPoints[i].x, levelPoints[i].y);
} }
ctx.closePath(); ctx.closePath();
ctx.strokeStyle = "rgba(255, 200, 0, 0.9)"; const baseColor = level.color || "rgba(255, 200, 0, 1)";
ctx.lineWidth = 3; const colorWithOpacity = baseColor.replace(/rgba?\(([^)]+)\)/, (_, rgb) => {
ctx.setLineDash([8, 4]); const parts = rgb.split(",").map((p) => p.trim());
return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${level.opacity})`;
});
ctx.strokeStyle = colorWithOpacity;
ctx.lineWidth = level.lineWidth;
if (level.dashPattern) {
ctx.setLineDash(level.dashPattern);
}
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]); ctx.setLineDash([]);
ctx.fillStyle = "rgba(255, 200, 0, 0.12)"; if (index === 0 && editorStyle.circleFillColor) {
ctx.fillStyle = editorStyle.circleFillColor;
ctx.fill(); ctx.fill();
for (const r of [0.25, 0.375]) {
const gradientPoints = [];
for (let i = 0; i <= segments; i++) {
const theta = i / segments * 2 * Math.PI;
const u = centerU - r * Math.sin(theta);
const v = centerV + r * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
gradientPoints.push(pixelPos);
}
ctx.beginPath();
ctx.moveTo(gradientPoints[0].x, gradientPoints[0].y);
for (let i = 1; i < gradientPoints.length; i++) {
ctx.lineTo(gradientPoints[i].x, gradientPoints[i].y);
}
ctx.closePath();
const alpha = r / maxRadius;
ctx.strokeStyle = `rgba(255, 220, 100, ${0.3 * alpha})`;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.stroke();
ctx.setLineDash([]);
} }
});
const centerPointStyle = editorStyle.centerPoint || {};
const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight); const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight);
ctx.beginPath(); ctx.beginPath();
ctx.arc(centerPixel.x, centerPixel.y, 5, 0, 2 * Math.PI); ctx.arc(centerPixel.x, centerPixel.y, centerPointStyle.radius || 5, 0, 2 * Math.PI);
ctx.fillStyle = "rgba(255, 200, 0, 1)"; if (centerPointStyle.fillColor) {
ctx.fillStyle = centerPointStyle.fillColor;
ctx.fill(); ctx.fill();
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)"; }
ctx.lineWidth = 2; if (centerPointStyle.strokeColor) {
ctx.strokeStyle = centerPointStyle.strokeColor;
ctx.lineWidth = centerPointStyle.strokeWidth || 2;
ctx.stroke(); ctx.stroke();
}; }
}, [editorStyle]);
const getCursorStyle = () => { const getCursorStyle = () => {
if (draggingPointIndex !== null) return "grabbing"; if (draggingPointIndex !== null) return "grabbing";
if (isDraggingArea) return "grabbing"; if (isDraggingArea) return "grabbing";
@ -715,7 +787,7 @@ var EditorCanvas = ({
onMouseDown: handleCanvasMouseDown, onMouseDown: handleCanvasMouseDown,
onMouseMove: handleMouseMove, onMouseMove: handleMouseMove,
children: [ children: [
/* @__PURE__ */ jsx2(ImageDistortion, { imageSrc, areas, width, height }), /* @__PURE__ */ jsx2(ImageDistortion, { imageSrc, areas }),
/* @__PURE__ */ jsx2( /* @__PURE__ */ jsx2(
"svg", "svg",
{ {
@ -730,14 +802,15 @@ var EditorCanvas = ({
children: areas.map((area) => { children: areas.map((area) => {
const isSelected = area.id === selectedAreaId; const isSelected = area.id === selectedAreaId;
const points = area.basePoints; const points = area.basePoints;
const outlineStyle = editorStyle.areaOutline || {};
return /* @__PURE__ */ jsx2("g", { children: /* @__PURE__ */ jsx2( return /* @__PURE__ */ jsx2("g", { children: /* @__PURE__ */ jsx2(
"polygon", "polygon",
{ {
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "), points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
fill: "none", fill: isSelected ? outlineStyle.selectedFillColor || "rgba(0, 170, 255, 0.08)" : outlineStyle.unselectedFillColor || "rgba(136, 136, 136, 0.03)",
stroke: isSelected ? "#00aaff" : "#888", stroke: isSelected ? outlineStyle.selectedColor || "#00aaff" : outlineStyle.unselectedColor || "#888",
strokeWidth: isSelected ? 2 : 1, strokeWidth: isSelected ? outlineStyle.selectedWidth || 2 : outlineStyle.unselectedWidth || 1,
strokeDasharray: isSelected ? "0" : "5,5", strokeDasharray: isSelected ? "0" : outlineStyle.unselectedDashPattern?.join(",") || "5,5",
opacity: isSelected ? 1 : 0.5 opacity: isSelected ? 1 : 0.5
} }
) }, area.id); ) }, area.id);
@ -768,7 +841,9 @@ var EditorCanvas = ({
} }
} }
), ),
selectedArea && selectedArea.basePoints.map((point, index) => /* @__PURE__ */ jsx2( selectedArea && selectedArea.basePoints.map((point, index) => {
const handleStyle = editorStyle.pointHandle || {};
return /* @__PURE__ */ jsx2(
"div", "div",
{ {
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`, className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
@ -777,11 +852,11 @@ var EditorCanvas = ({
left: `${point.x * 100}%`, left: `${point.x * 100}%`,
top: `${point.y * 100}%`, top: `${point.y * 100}%`,
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
width: 16, width: handleStyle.size || 16,
height: 16, height: handleStyle.size || 16,
borderRadius: "50%", borderRadius: "50%",
backgroundColor: "#00aaff", backgroundColor: handleStyle.fillColor || "#00aaff",
border: "2px solid white", border: `${handleStyle.strokeWidth || 2}px solid ${handleStyle.strokeColor || "white"}`,
cursor: "grab", cursor: "grab",
pointerEvents: "auto", pointerEvents: "auto",
boxShadow: "0 2px 4px rgba(0,0,0,0.3)" boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
@ -795,8 +870,8 @@ var EditorCanvas = ({
top: -24, top: -24,
left: "50%", left: "50%",
transform: "translateX(-50%)", transform: "translateX(-50%)",
fontSize: 11, fontSize: handleStyle.labelFontSize || 11,
color: "#00aaff", color: handleStyle.labelColor || "#00aaff",
fontWeight: "bold", fontWeight: "bold",
textShadow: "1px 1px 2px rgba(0,0,0,0.8)", textShadow: "1px 1px 2px rgba(0,0,0,0.8)",
whiteSpace: "nowrap" whiteSpace: "nowrap"
@ -809,7 +884,8 @@ var EditorCanvas = ({
) )
}, },
index index
)) );
})
] ]
} }
); );
@ -1013,7 +1089,8 @@ var DistortionEditor = ({
onAreasChange, onAreasChange,
onSelectedAreaChange, onSelectedAreaChange,
width = 800, width = 800,
height = 600 height = 600,
canvasStyle
}) => { }) => {
const { const {
state, state,
@ -1072,7 +1149,8 @@ var DistortionEditor = ({
onUpdateArea: updateArea, onUpdateArea: updateArea,
draggingPointIndex: state.draggingPointIndex, draggingPointIndex: state.draggingPointIndex,
onStartDragging: startDragging, onStartDragging: startDragging,
onStopDragging: stopDragging onStopDragging: stopDragging,
style: canvasStyle
} }
) }), ) }),
/* @__PURE__ */ jsxs4("div", { className: "editor-sidebar", children: [ /* @__PURE__ */ jsxs4("div", { className: "editor-sidebar", children: [

2
dist/index.mjs.map vendored

File diff suppressed because one or more lines are too long

View File

@ -14,6 +14,7 @@ export const DistortionEditor: React.FC<DistortionEditorProps> = ({
onSelectedAreaChange, onSelectedAreaChange,
width = 800, width = 800,
height = 600, height = 600,
canvasStyle,
}) => { }) => {
const { const {
state, state,
@ -85,6 +86,7 @@ export const DistortionEditor: React.FC<DistortionEditorProps> = ({
draggingPointIndex={state.draggingPointIndex} draggingPointIndex={state.draggingPointIndex}
onStartDragging={startDragging} onStartDragging={startDragging}
onStopDragging={stopDragging} onStopDragging={stopDragging}
style={canvasStyle}
/> />
</div> </div>

View File

@ -1,6 +1,8 @@
import React, {useRef, useEffect, useState, useCallback} from 'react'; import React, {useRef, useEffect, useState, useCallback, useMemo} from 'react';
import {DistortionArea, Point} from '../../types/area'; import {DistortionArea, Point} from '../../types/area';
import {ImageDistortion} from '../../components/ImageDistortion'; import {ImageDistortion} from '../../components/ImageDistortion';
import {EditorCanvasStyle} from '../types';
import {DEFAULT_EDITOR_CANVAS_STYLE} from '../constants';
interface EditorCanvasProps { interface EditorCanvasProps {
areas: DistortionArea[]; areas: DistortionArea[];
@ -13,6 +15,8 @@ interface EditorCanvasProps {
draggingPointIndex: number | null; draggingPointIndex: number | null;
onStartDragging: (pointIndex: number) => void; onStartDragging: (pointIndex: number) => void;
onStopDragging: () => void; onStopDragging: () => void;
/** 에디터 캔버스 스타일 커스터마이징 */
style?: EditorCanvasStyle;
} }
export const EditorCanvas: React.FC<EditorCanvasProps> = ({ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
@ -26,12 +30,32 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
draggingPointIndex, draggingPointIndex,
onStartDragging, onStartDragging,
onStopDragging, onStopDragging,
style: customStyle,
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [canvasSize, setCanvasSize] = useState({width: 0, height: 0}); const [canvasSize, setCanvasSize] = useState({width: 0, height: 0});
const [isDraggingArea, setIsDraggingArea] = useState(false); const [isDraggingArea, setIsDraggingArea] = useState(false);
const [dragStartPos, setDragStartPos] = useState<Point | null>(null); const [dragStartPos, setDragStartPos] = useState<Point | null>(null);
// 스타일 병합 (커스텀 스타일 우선)
const editorStyle = useMemo(() => ({
...DEFAULT_EDITOR_CANVAS_STYLE,
...customStyle,
circleLevels: customStyle?.circleLevels || DEFAULT_EDITOR_CANVAS_STYLE.circleLevels,
centerPoint: {
...DEFAULT_EDITOR_CANVAS_STYLE.centerPoint,
...customStyle?.centerPoint,
},
pointHandle: {
...DEFAULT_EDITOR_CANVAS_STYLE.pointHandle,
...customStyle?.pointHandle,
},
areaOutline: {
...DEFAULT_EDITOR_CANVAS_STYLE.areaOutline,
...customStyle?.areaOutline,
},
}), [customStyle]);
// 컨테이너 크기 측정 // 컨테이너 크기 측정
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
@ -167,7 +191,7 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
}; };
// UV 좌표계의 원을 정확히 그리기 (찌그러진 원 형태) // UV 좌표계의 원을 정확히 그리기 (찌그러진 원 형태)
const drawDistortionCircle = ( const drawDistortionCircle = useCallback((
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
points: [Point, Point, Point, Point], points: [Point, Point, Point, Point],
canvasWidth: number, canvasWidth: number,
@ -176,74 +200,64 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
const segments = 128; // 원을 128개 세그먼트로 촘촘히 분할 const segments = 128; // 원을 128개 세그먼트로 촘촘히 분할
const centerU = 0.5; const centerU = 0.5;
const centerV = 0.5; const centerV = 0.5;
const maxRadius = 0.5; // UV 좌표계에서 최대 반지름 0.5 (셰이더의 maxUvRadius)
// 원 위의 점들을 UV 좌표로 샘플링 후 픽셀 좌표로 변환 const circleLevels = editorStyle.circleLevels || [];
// 4가지 조합을 모두 테스트 (사용자가 이미지에서 P1-P3 대각선으로 늘렸을 때 왜곡도 같은 방향이어야 함)
const circlePoints: { x: number; y: number }[] = []; // 원 레벨별로 그리기 (외부 -> 내부 순)
circleLevels.forEach((level, index) => {
const levelPoints: { x: number; y: number }[] = [];
for (let i = 0; i <= segments; i++) { for (let i = 0; i <= segments; i++) {
const theta = (i / segments) * 2 * Math.PI; const theta = (i / segments) * 2 * Math.PI;
const u = centerU - level.radius * Math.sin(theta);
// 테스트: u=-sin, v=cos (-90도 회전, P1-P3 방향에 맞춤) const v = centerV + level.radius * Math.cos(theta);
const u = centerU - maxRadius * Math.sin(theta);
const v = centerV + maxRadius * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight); const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
circlePoints.push(pixelPos); levelPoints.push(pixelPos);
} }
// 찌그러진 원 그리기 (실제 왜곡 적용 경계)
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(circlePoints[0].x, circlePoints[0].y); ctx.moveTo(levelPoints[0].x, levelPoints[0].y);
for (let i = 1; i < circlePoints.length; i++) { for (let i = 1; i < levelPoints.length; i++) {
ctx.lineTo(circlePoints[i].x, circlePoints[i].y); ctx.lineTo(levelPoints[i].x, levelPoints[i].y);
} }
ctx.closePath(); ctx.closePath();
ctx.strokeStyle = 'rgba(255, 200, 0, 0.9)';
ctx.lineWidth = 3; // 원 테두리
ctx.setLineDash([8, 4]); const baseColor = level.color || 'rgba(255, 200, 0, 1)';
// baseColor에서 RGB 추출하고 opacity 적용
const colorWithOpacity = baseColor.replace(/rgba?\(([^)]+)\)/, (_, rgb) => {
const parts = rgb.split(',').map((p: string) => p.trim());
return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${level.opacity})`;
});
ctx.strokeStyle = colorWithOpacity;
ctx.lineWidth = level.lineWidth;
if (level.dashPattern) {
ctx.setLineDash(level.dashPattern);
}
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]); ctx.setLineDash([]);
// 내부를 반투명하게 채우기 // 가장 외부 원만 내부 채우기
ctx.fillStyle = 'rgba(255, 200, 0, 0.12)'; if (index === 0 && editorStyle.circleFillColor) {
ctx.fillStyle = editorStyle.circleFillColor;
ctx.fill(); ctx.fill();
// 영향력 그라디언트를 나타내는 추가 원들 (0.25, 0.375 반지름)
for (const r of [0.25, 0.375]) {
const gradientPoints: { x: number; y: number }[] = [];
for (let i = 0; i <= segments; i++) {
const theta = (i / segments) * 2 * Math.PI;
const u = centerU - r * Math.sin(theta);
const v = centerV + r * Math.cos(theta);
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
gradientPoints.push(pixelPos);
}
ctx.beginPath();
ctx.moveTo(gradientPoints[0].x, gradientPoints[0].y);
for (let i = 1; i < gradientPoints.length; i++) {
ctx.lineTo(gradientPoints[i].x, gradientPoints[i].y);
}
ctx.closePath();
const alpha = r / maxRadius;
ctx.strokeStyle = `rgba(255, 220, 100, ${0.3 * alpha})`;
ctx.lineWidth = 1;
ctx.setLineDash([3, 3]);
ctx.stroke();
ctx.setLineDash([]);
} }
});
// 중심점 표시 // 중심점 표시
const centerPointStyle = editorStyle.centerPoint || {};
const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight); const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight);
ctx.beginPath(); ctx.beginPath();
ctx.arc(centerPixel.x, centerPixel.y, 5, 0, 2 * Math.PI); ctx.arc(centerPixel.x, centerPixel.y, centerPointStyle.radius || 5, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(255, 200, 0, 1)'; if (centerPointStyle.fillColor) {
ctx.fillStyle = centerPointStyle.fillColor;
ctx.fill(); ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; }
ctx.lineWidth = 2; if (centerPointStyle.strokeColor) {
ctx.strokeStyle = centerPointStyle.strokeColor;
ctx.lineWidth = centerPointStyle.strokeWidth || 2;
ctx.stroke(); ctx.stroke();
}; }
}, [editorStyle]);
// 커서 스타일 결정 // 커서 스타일 결정
const getCursorStyle = () => { const getCursorStyle = () => {
@ -261,7 +275,7 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
> >
{/* ImageDistortion 컴포넌트 */} {/* ImageDistortion 컴포넌트 */}
<ImageDistortion imageSrc={imageSrc} areas={areas} width={width} height={height}/> <ImageDistortion imageSrc={imageSrc} areas={areas}/>
{/* 오버레이 SVG */} {/* 오버레이 SVG */}
<svg <svg
@ -278,17 +292,18 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
{areas.map((area) => { {areas.map((area) => {
const isSelected = area.id === selectedAreaId; const isSelected = area.id === selectedAreaId;
const points = area.basePoints; const points = area.basePoints;
const outlineStyle = editorStyle.areaOutline || {};
return ( return (
<g key={area.id}> <g key={area.id}>
{/* 사각형 경계선 */} {/* 사각형 배경 및 경계선 */}
<polygon <polygon
points={points points={points
.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`) .map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`)
.join(' ')} .join(' ')}
fill="none" fill={isSelected ? (outlineStyle.selectedFillColor || 'rgba(0, 170, 255, 0.08)') : (outlineStyle.unselectedFillColor || 'rgba(136, 136, 136, 0.03)')}
stroke={isSelected ? '#00aaff' : '#888'} stroke={isSelected ? (outlineStyle.selectedColor || '#00aaff') : (outlineStyle.unselectedColor || '#888')}
strokeWidth={isSelected ? 2 : 1} strokeWidth={isSelected ? (outlineStyle.selectedWidth || 2) : (outlineStyle.unselectedWidth || 1)}
strokeDasharray={isSelected ? '0' : '5,5'} strokeDasharray={isSelected ? '0' : (outlineStyle.unselectedDashPattern?.join(',') || '5,5')}
opacity={isSelected ? 1 : 0.5} opacity={isSelected ? 1 : 0.5}
/> />
</g> </g>
@ -323,7 +338,9 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
{/* 선택된 영역의 포인트 핸들 */} {/* 선택된 영역의 포인트 핸들 */}
{selectedArea && {selectedArea &&
selectedArea.basePoints.map((point, index) => ( selectedArea.basePoints.map((point, index) => {
const handleStyle = editorStyle.pointHandle || {};
return (
<div <div
key={index} key={index}
className={`point-handle ${draggingPointIndex === index ? 'dragging' : ''}`} className={`point-handle ${draggingPointIndex === index ? 'dragging' : ''}`}
@ -332,11 +349,11 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
left: `${point.x * 100}%`, left: `${point.x * 100}%`,
top: `${point.y * 100}%`, top: `${point.y * 100}%`,
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 16, width: handleStyle.size || 16,
height: 16, height: handleStyle.size || 16,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: '#00aaff', backgroundColor: handleStyle.fillColor || '#00aaff',
border: '2px solid white', border: `${handleStyle.strokeWidth || 2}px solid ${handleStyle.strokeColor || 'white'}`,
cursor: 'grab', cursor: 'grab',
pointerEvents: 'auto', pointerEvents: 'auto',
boxShadow: '0 2px 4px rgba(0,0,0,0.3)', boxShadow: '0 2px 4px rgba(0,0,0,0.3)',
@ -349,8 +366,8 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
top: -24, top: -24,
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
fontSize: 11, fontSize: handleStyle.labelFontSize || 11,
color: '#00aaff', color: handleStyle.labelColor || '#00aaff',
fontWeight: 'bold', fontWeight: 'bold',
textShadow: '1px 1px 2px rgba(0,0,0,0.8)', textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -359,7 +376,8 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
P{index + 1} P{index + 1}
</div> </div>
</div> </div>
))} );
})}
</div> </div>
); );
}; };

View File

@ -1,4 +1,14 @@
export { DistortionEditor } from './DistortionEditor'; export { DistortionEditor } from './DistortionEditor';
export type { DistortionEditorProps, EditorState, EditMode } from './types'; export type {
DistortionEditorProps,
EditorState,
EditMode,
EditorCanvasStyle,
CircleLevelStyle,
CenterPointStyle,
PointHandleStyle,
AreaOutlineStyle,
} from './types';
export { useDistortionEditor } from './hooks/useDistortionEditor'; export { useDistortionEditor } from './hooks/useDistortionEditor';
export { DEFAULT_EDITOR_CANVAS_STYLE } from './constants';
import './editor.css'; import './editor.css';

View File

@ -32,6 +32,90 @@ export interface PointHandle {
label: string; label: string;
} }
/**
*
*/
export interface CircleLevelStyle {
/** 반지름 (0.0 - 1.0, UV 좌표) */
radius: number;
/** 투명도 (0.0 - 1.0) */
opacity: number;
/** 선 두께 (픽셀) */
lineWidth: number;
/** 선 색상 (CSS color) */
color?: string;
/** 대시 패턴 [dash, gap] */
dashPattern?: [number, number];
}
/**
*
*/
export interface CenterPointStyle {
/** 반지름 (픽셀) */
radius?: number;
/** 채우기 색상 */
fillColor?: string;
/** 테두리 색상 */
strokeColor?: string;
/** 테두리 두께 */
strokeWidth?: number;
}
/**
*
*/
export interface PointHandleStyle {
/** 핸들 크기 (픽셀) */
size?: number;
/** 채우기 색상 */
fillColor?: string;
/** 테두리 색상 */
strokeColor?: string;
/** 테두리 두께 */
strokeWidth?: number;
/** 레이블 색상 */
labelColor?: string;
/** 레이블 폰트 크기 */
labelFontSize?: number;
}
/**
*
*/
export interface AreaOutlineStyle {
/** 선택된 영역 색상 */
selectedColor?: string;
/** 선택되지 않은 영역 색상 */
unselectedColor?: string;
/** 선택된 영역 선 두께 */
selectedWidth?: number;
/** 선택되지 않은 영역 선 두께 */
unselectedWidth?: number;
/** 선택되지 않은 영역 대시 패턴 */
unselectedDashPattern?: [number, number];
/** 선택된 영역 배경 채우기 색상 */
selectedFillColor?: string;
/** 선택되지 않은 영역 배경 채우기 색상 */
unselectedFillColor?: string;
}
/**
*
*/
export interface EditorCanvasStyle {
/** 왜곡 영역 원 레벨 스타일 배열 (외부 -> 내부 순) */
circleLevels?: CircleLevelStyle[];
/** 왜곡 영역 내부 채우기 색상 */
circleFillColor?: string;
/** 중심점 스타일 */
centerPoint?: CenterPointStyle;
/** 포인트 핸들 스타일 */
pointHandle?: PointHandleStyle;
/** 영역 외곽선 스타일 */
areaOutline?: AreaOutlineStyle;
}
/** /**
* Props * Props
*/ */
@ -48,4 +132,6 @@ export interface DistortionEditorProps {
width?: number; width?: number;
/** 캔버스 높이 */ /** 캔버스 높이 */
height?: number; height?: number;
/** 에디터 캔버스 스타일 커스터마이징 */
canvasStyle?: EditorCanvasStyle;
} }