responsive-image-canvas/src/editor/hooks/useDistortionEditor.ts
BaekRyang 63e7bac3c7 feat: Add interactive distortion area editor
- Add EditorCanvas component with visual distortion area editing
  - Point-in-polygon detection for area selection
  - Individual point dragging with visual handles
  - Entire area dragging by clicking inside polygon
  - UV-space distortion circle visualization
- Add AreaList component for managing multiple distortion areas
- Add ParameterPanel for editing distortion properties
  - Base points (normalized coordinates)
  - Drag vectors and distortion strength
  - Animation duration and easing
- Add DistortionEditor main component with sidebar layout
- Add useDistortionEditor hook for state management
- Add editor types and interfaces

사각형 내부를 클릭하여 전체 영역을 드래그할 수 있는 기능 포함
2025-11-05 11:20:20 +09:00

96 lines
2.5 KiB
TypeScript

import { useState, useCallback } from 'react';
import { DistortionArea, Point } from '../../types/area';
import { EditorState } from '../types';
export const useDistortionEditor = (initialAreas: DistortionArea[] = []) => {
const [state, setState] = useState<EditorState>({
selectedAreaId: initialAreas[0]?.id || null,
areas: initialAreas,
editMode: 'normal',
draggingPointIndex: null,
});
/** 영역 선택 */
const selectArea = useCallback((areaId: string | null) => {
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
}, []);
/** 영역 추가 */
const addArea = useCallback((area: DistortionArea) => {
setState((prev) => ({
...prev,
areas: [...prev.areas, area],
selectedAreaId: area.id,
}));
}, []);
/** 영역 삭제 */
const removeArea = useCallback((areaId: string) => {
setState((prev) => {
const newAreas = prev.areas.filter((a) => a.id !== areaId);
return {
...prev,
areas: newAreas,
selectedAreaId:
prev.selectedAreaId === areaId ? newAreas[0]?.id || null : prev.selectedAreaId,
};
});
}, []);
/** 영역 업데이트 */
const updateArea = useCallback((areaId: string, updates: Partial<DistortionArea>) => {
setState((prev) => ({
...prev,
areas: prev.areas.map((area) => (area.id === areaId ? { ...area, ...updates } : area)),
}));
}, []);
/** 포인트 업데이트 */
const updatePoint = useCallback((areaId: string, pointIndex: number, point: Point) => {
setState((prev) => ({
...prev,
areas: prev.areas.map((area) => {
if (area.id === areaId) {
const newPoints = [...area.basePoints] as [Point, Point, Point, Point];
newPoints[pointIndex] = point;
return { ...area, basePoints: newPoints };
}
return area;
}),
}));
}, []);
/** 드래그 시작 */
const startDragging = useCallback((pointIndex: number) => {
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
}, []);
/** 드래그 종료 */
const stopDragging = useCallback(() => {
setState((prev) => ({ ...prev, draggingPointIndex: null }));
}, []);
/** 편집 모드 변경 */
const setEditMode = useCallback((mode: EditorState['editMode']) => {
setState((prev) => ({ ...prev, editMode: mode }));
}, []);
/** 선택된 영역 가져오기 */
const getSelectedArea = useCallback(() => {
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
}, [state.areas, state.selectedAreaId]);
return {
state,
selectArea,
addArea,
removeArea,
updateArea,
updatePoint,
startDragging,
stopDragging,
setEditMode,
getSelectedArea,
};
};