build: Update compiled distribution files
- Update CJS and ESM bundles with coordinate system fixes - Update type definitions (d.ts, d.mts) - Add editor styles (CSS) - Update shader files - Update source maps
This commit is contained in:
parent
f080693d32
commit
e66b078dd8
11
dist/distortion.frag.glsl
vendored
11
dist/distortion.frag.glsl
vendored
@ -1,8 +1,8 @@
|
|||||||
uniform vec2 u_resolution;
|
uniform vec2 u_resolution;
|
||||||
uniform sampler2D u_texture;
|
uniform sampler2D u_texture;
|
||||||
uniform vec2 u_points[32]; // 최대 8영역 × 4포인트 (정규화된 좌표)
|
uniform vec2 u_points[32]; // 최대 8영역 × 4포인트 (정규화된 좌표 0-1)
|
||||||
uniform int u_numAreas;
|
uniform int u_numAreas;
|
||||||
uniform vec2 u_dragVectors[8]; // 정규화된 좌표
|
uniform vec2 u_dragVectors[8]; // 드래그 벡터 (정규화된 좌표 0-1)
|
||||||
uniform float u_distortionStrengths[8];
|
uniform float u_distortionStrengths[8];
|
||||||
|
|
||||||
varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
@ -72,10 +72,9 @@ void main() {
|
|||||||
|
|
||||||
if (distToCenter < maxUvRadius) {
|
if (distToCenter < maxUvRadius) {
|
||||||
float influence = 1.0 - smoothstep(0.0, maxUvRadius, distToCenter);
|
float influence = 1.0 - smoothstep(0.0, maxUvRadius, distToCenter);
|
||||||
// dragVector도 정규화된 좌표이므로 픽셀로 변환
|
// dragVector는 정규화된 좌표(0-1)이므로 바로 사용 (Flutter와 동일한 결과)
|
||||||
vec2 distortion = (u_dragVectors[i] * u_resolution) * influence * u_distortionStrengths[i];
|
vec2 distortion = u_dragVectors[i] * influence * u_distortionStrengths[i];
|
||||||
// texCoord는 이미 정규화된 좌표이므로 정규화된 왜곡 적용
|
texCoord += distortion;
|
||||||
texCoord += distortion / u_resolution;
|
|
||||||
texCoord = clamp(texCoord, 0.0, 1.0);
|
texCoord = clamp(texCoord, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
found = true;
|
found = true;
|
||||||
|
|||||||
298
dist/index.css
vendored
Normal file
298
dist/index.css
vendored
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
/* src/editor/editor.css */
|
||||||
|
.distortion-editor {
|
||||||
|
font-family:
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
sans-serif;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.editor-main {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.editor-canvas-container {
|
||||||
|
flex: 1;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.editor-canvas {
|
||||||
|
position: relative;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.editor-sidebar {
|
||||||
|
width: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.area-list {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.area-list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.area-list-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.btn-add {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #00aaff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.btn-add:hover:not(:disabled) {
|
||||||
|
background: #0088cc;
|
||||||
|
}
|
||||||
|
.btn-add:disabled {
|
||||||
|
background: #555;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.area-list-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.area-list-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.area-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #383838;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
.area-item:hover {
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
.area-item.selected {
|
||||||
|
background: #2d5a7a;
|
||||||
|
border-color: #00aaff;
|
||||||
|
}
|
||||||
|
.area-item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.area-item-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.area-item-strength {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.btn-remove {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #ff4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.btn-remove:hover {
|
||||||
|
background: #cc0000;
|
||||||
|
}
|
||||||
|
.parameter-panel {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.parameter-panel h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.parameter-panel-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #888;
|
||||||
|
padding: 40px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.parameter-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.parameter-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #444;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #00aaff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.slider::-webkit-slider-thumb:hover {
|
||||||
|
background: #0088cc;
|
||||||
|
}
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #00aaff;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.slider::-moz-range-thumb:hover {
|
||||||
|
background: #0088cc;
|
||||||
|
}
|
||||||
|
.input-number {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: #383838;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.input-number:focus {
|
||||||
|
border-color: #00aaff;
|
||||||
|
}
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background: #383838;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.select:focus {
|
||||||
|
border-color: #00aaff;
|
||||||
|
}
|
||||||
|
.points-display {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.point-coord {
|
||||||
|
padding: 8px;
|
||||||
|
background: #383838;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
.point-handle {
|
||||||
|
z-index: 10;
|
||||||
|
transition: transform 0.1s, box-shadow 0.1s;
|
||||||
|
}
|
||||||
|
.point-handle:hover {
|
||||||
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 170, 255, 0.5);
|
||||||
|
}
|
||||||
|
.point-handle.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
transform: translate(-50%, -50%) scale(1.3);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 170, 255, 0.7);
|
||||||
|
}
|
||||||
|
.area-list-items::-webkit-scrollbar,
|
||||||
|
.parameter-panel::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
.area-list-items::-webkit-scrollbar-track,
|
||||||
|
.parameter-panel::-webkit-scrollbar-track {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.area-list-items::-webkit-scrollbar-thumb,
|
||||||
|
.parameter-panel::-webkit-scrollbar-thumb {
|
||||||
|
background: #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.area-list-items::-webkit-scrollbar-thumb:hover,
|
||||||
|
.parameter-panel::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.editor-main {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.editor-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.area-list,
|
||||||
|
.parameter-panel {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-sidebar {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.points-display {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=index.css.map */
|
||||||
1
dist/index.css.map
vendored
Normal file
1
dist/index.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
59
dist/index.d.mts
vendored
59
dist/index.d.mts
vendored
@ -132,6 +132,56 @@ interface ImageDistortionProps {
|
|||||||
*/
|
*/
|
||||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에디터 편집 모드
|
||||||
|
*/
|
||||||
|
type EditMode = 'normal' | 'point-edit' | 'parameter-edit';
|
||||||
|
/**
|
||||||
|
* 에디터 상태
|
||||||
|
*/
|
||||||
|
interface EditorState {
|
||||||
|
/** 현재 선택된 영역 ID */
|
||||||
|
selectedAreaId: string | null;
|
||||||
|
/** 모든 왜곡 영역 */
|
||||||
|
areas: DistortionArea[];
|
||||||
|
/** 현재 편집 모드 */
|
||||||
|
editMode: EditMode;
|
||||||
|
/** 드래그 중인 포인트 인덱스 (0-3) */
|
||||||
|
draggingPointIndex: number | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 에디터 Props
|
||||||
|
*/
|
||||||
|
interface DistortionEditorProps {
|
||||||
|
/** 초기 영역 배열 */
|
||||||
|
initialAreas?: DistortionArea[];
|
||||||
|
/** 이미지 소스 */
|
||||||
|
imageSrc: string;
|
||||||
|
/** 영역 변경 콜백 */
|
||||||
|
onAreasChange?: (areas: DistortionArea[]) => void;
|
||||||
|
/** 선택된 영역 변경 콜백 */
|
||||||
|
onSelectedAreaChange?: (areaId: string | null) => void;
|
||||||
|
/** 캔버스 너비 */
|
||||||
|
width?: number;
|
||||||
|
/** 캔버스 높이 */
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const DistortionEditor: React.FC<DistortionEditorProps>;
|
||||||
|
|
||||||
|
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
||||||
|
state: EditorState;
|
||||||
|
selectArea: (areaId: string | null) => void;
|
||||||
|
addArea: (area: DistortionArea) => void;
|
||||||
|
removeArea: (areaId: string) => void;
|
||||||
|
updateArea: (areaId: string, updates: Partial<DistortionArea>) => void;
|
||||||
|
updatePoint: (areaId: string, pointIndex: number, point: Point) => void;
|
||||||
|
startDragging: (pointIndex: number) => void;
|
||||||
|
stopDragging: () => void;
|
||||||
|
setEditMode: (mode: EditorState["editMode"]) => void;
|
||||||
|
getSelectedArea: () => DistortionArea | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행도에 이징 함수를 적용
|
* 진행도에 이징 함수를 적용
|
||||||
* @param progress 진행도 (0.0 - 1.0)
|
* @param progress 진행도 (0.0 - 1.0)
|
||||||
@ -214,6 +264,13 @@ declare class ThreeScene {
|
|||||||
* 씬 렌더링
|
* 씬 렌더링
|
||||||
*/
|
*/
|
||||||
render(): void;
|
render(): void;
|
||||||
|
/**
|
||||||
|
* 현재 해상도 가져오기
|
||||||
|
*/
|
||||||
|
getResolution(): {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* 리소스 정리
|
* 리소스 정리
|
||||||
*/
|
*/
|
||||||
@ -272,4 +329,4 @@ declare class AnimationLoop {
|
|||||||
*/
|
*/
|
||||||
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
||||||
|
|
||||||
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, type DistortionMovement, type EasingFunction, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame };
|
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor };
|
||||||
|
|||||||
59
dist/index.d.ts
vendored
59
dist/index.d.ts
vendored
@ -132,6 +132,56 @@ interface ImageDistortionProps {
|
|||||||
*/
|
*/
|
||||||
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
declare const ImageDistortion: React.FC<ImageDistortionProps>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에디터 편집 모드
|
||||||
|
*/
|
||||||
|
type EditMode = 'normal' | 'point-edit' | 'parameter-edit';
|
||||||
|
/**
|
||||||
|
* 에디터 상태
|
||||||
|
*/
|
||||||
|
interface EditorState {
|
||||||
|
/** 현재 선택된 영역 ID */
|
||||||
|
selectedAreaId: string | null;
|
||||||
|
/** 모든 왜곡 영역 */
|
||||||
|
areas: DistortionArea[];
|
||||||
|
/** 현재 편집 모드 */
|
||||||
|
editMode: EditMode;
|
||||||
|
/** 드래그 중인 포인트 인덱스 (0-3) */
|
||||||
|
draggingPointIndex: number | null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 에디터 Props
|
||||||
|
*/
|
||||||
|
interface DistortionEditorProps {
|
||||||
|
/** 초기 영역 배열 */
|
||||||
|
initialAreas?: DistortionArea[];
|
||||||
|
/** 이미지 소스 */
|
||||||
|
imageSrc: string;
|
||||||
|
/** 영역 변경 콜백 */
|
||||||
|
onAreasChange?: (areas: DistortionArea[]) => void;
|
||||||
|
/** 선택된 영역 변경 콜백 */
|
||||||
|
onSelectedAreaChange?: (areaId: string | null) => void;
|
||||||
|
/** 캔버스 너비 */
|
||||||
|
width?: number;
|
||||||
|
/** 캔버스 높이 */
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const DistortionEditor: React.FC<DistortionEditorProps>;
|
||||||
|
|
||||||
|
declare const useDistortionEditor: (initialAreas?: DistortionArea[]) => {
|
||||||
|
state: EditorState;
|
||||||
|
selectArea: (areaId: string | null) => void;
|
||||||
|
addArea: (area: DistortionArea) => void;
|
||||||
|
removeArea: (areaId: string) => void;
|
||||||
|
updateArea: (areaId: string, updates: Partial<DistortionArea>) => void;
|
||||||
|
updatePoint: (areaId: string, pointIndex: number, point: Point) => void;
|
||||||
|
startDragging: (pointIndex: number) => void;
|
||||||
|
stopDragging: () => void;
|
||||||
|
setEditMode: (mode: EditorState["editMode"]) => void;
|
||||||
|
getSelectedArea: () => DistortionArea | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 진행도에 이징 함수를 적용
|
* 진행도에 이징 함수를 적용
|
||||||
* @param progress 진행도 (0.0 - 1.0)
|
* @param progress 진행도 (0.0 - 1.0)
|
||||||
@ -214,6 +264,13 @@ declare class ThreeScene {
|
|||||||
* 씬 렌더링
|
* 씬 렌더링
|
||||||
*/
|
*/
|
||||||
render(): void;
|
render(): void;
|
||||||
|
/**
|
||||||
|
* 현재 해상도 가져오기
|
||||||
|
*/
|
||||||
|
getResolution(): {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* 리소스 정리
|
* 리소스 정리
|
||||||
*/
|
*/
|
||||||
@ -272,4 +329,4 @@ declare class AnimationLoop {
|
|||||||
*/
|
*/
|
||||||
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
declare const useAnimationFrame: (callback: (deltaTime: number) => void, isPlaying?: boolean) => void;
|
||||||
|
|
||||||
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, type DistortionMovement, type EasingFunction, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame };
|
export { ANIMATION_CONFIG, AnimationLoop, type AnimationState, type AnimationTicker, type AreaBounds, DEFAULT_AREA, type DistortionArea, DistortionEditor, type DistortionEditorProps, type DistortionMovement, type EasingFunction, type EditMode, type EditorState, ImageDistortion, type ImageDistortionProps, type Point, SHADER_CONFIG, type ShaderConfig, ShaderManager, type ShaderUniforms, ThreeScene, applyEasing, useAnimationFrame, useDistortionEditor };
|
||||||
|
|||||||
655
dist/index.js
vendored
655
dist/index.js
vendored
@ -33,12 +33,14 @@ __export(index_exports, {
|
|||||||
ANIMATION_CONFIG: () => ANIMATION_CONFIG,
|
ANIMATION_CONFIG: () => ANIMATION_CONFIG,
|
||||||
AnimationLoop: () => AnimationLoop,
|
AnimationLoop: () => AnimationLoop,
|
||||||
DEFAULT_AREA: () => DEFAULT_AREA,
|
DEFAULT_AREA: () => DEFAULT_AREA,
|
||||||
|
DistortionEditor: () => DistortionEditor,
|
||||||
ImageDistortion: () => ImageDistortion,
|
ImageDistortion: () => ImageDistortion,
|
||||||
SHADER_CONFIG: () => SHADER_CONFIG,
|
SHADER_CONFIG: () => SHADER_CONFIG,
|
||||||
ShaderManager: () => ShaderManager,
|
ShaderManager: () => ShaderManager,
|
||||||
ThreeScene: () => ThreeScene,
|
ThreeScene: () => ThreeScene,
|
||||||
applyEasing: () => applyEasing,
|
applyEasing: () => applyEasing,
|
||||||
useAnimationFrame: () => useAnimationFrame
|
useAnimationFrame: () => useAnimationFrame,
|
||||||
|
useDistortionEditor: () => useDistortionEditor
|
||||||
});
|
});
|
||||||
module.exports = __toCommonJS(index_exports);
|
module.exports = __toCommonJS(index_exports);
|
||||||
|
|
||||||
@ -135,6 +137,15 @@ var ThreeScene = class {
|
|||||||
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 현재 해상도 가져오기
|
||||||
|
*/
|
||||||
|
getResolution() {
|
||||||
|
return {
|
||||||
|
x: this.uniforms.u_resolution.value.x,
|
||||||
|
y: this.uniforms.u_resolution.value.y
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 리소스 정리
|
* 리소스 정리
|
||||||
*/
|
*/
|
||||||
@ -426,19 +437,20 @@ var ImageDistortion = ({
|
|||||||
}, [imageSrc, isReady]);
|
}, [imageSrc, isReady]);
|
||||||
(0, import_react2.useEffect)(() => {
|
(0, import_react2.useEffect)(() => {
|
||||||
if (!sceneRef.current || !isReady) return;
|
if (!sceneRef.current || !isReady) return;
|
||||||
|
const resolution = sceneRef.current.getResolution();
|
||||||
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
||||||
currentAreas.forEach((area, areaIndex) => {
|
currentAreas.forEach((area, areaIndex) => {
|
||||||
area.basePoints.forEach((point, pointIndex) => {
|
area.basePoints.forEach((point, pointIndex) => {
|
||||||
const index = (areaIndex * 4 + pointIndex) * 2;
|
const index = (areaIndex * 4 + pointIndex) * 2;
|
||||||
points[index] = point.x;
|
points[index] = point.x;
|
||||||
points[index + 1] = point.y;
|
points[index + 1] = 1 - point.y;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);
|
const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);
|
||||||
currentAreas.forEach((area, index) => {
|
currentAreas.forEach((area, index) => {
|
||||||
const baseIndex = index * 2;
|
const baseIndex = index * 2;
|
||||||
dragVectors[baseIndex] = area.dragVector.x;
|
dragVectors[baseIndex] = area.dragVector.x;
|
||||||
dragVectors[baseIndex + 1] = area.dragVector.y;
|
dragVectors[baseIndex + 1] = -area.dragVector.y;
|
||||||
});
|
});
|
||||||
const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);
|
const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);
|
||||||
currentAreas.forEach((area, index) => {
|
currentAreas.forEach((area, index) => {
|
||||||
@ -491,16 +503,651 @@ var ImageDistortion = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src/editor/DistortionEditor.tsx
|
||||||
|
var import_react5 = require("react");
|
||||||
|
|
||||||
|
// src/editor/hooks/useDistortionEditor.ts
|
||||||
|
var import_react3 = require("react");
|
||||||
|
var useDistortionEditor = (initialAreas = []) => {
|
||||||
|
const [state, setState] = (0, import_react3.useState)({
|
||||||
|
selectedAreaId: initialAreas[0]?.id || null,
|
||||||
|
areas: initialAreas,
|
||||||
|
editMode: "normal",
|
||||||
|
draggingPointIndex: null
|
||||||
|
});
|
||||||
|
const selectArea = (0, import_react3.useCallback)((areaId) => {
|
||||||
|
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
||||||
|
}, []);
|
||||||
|
const addArea = (0, import_react3.useCallback)((area) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
areas: [...prev.areas, area],
|
||||||
|
selectedAreaId: area.id
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
const removeArea = (0, import_react3.useCallback)((areaId) => {
|
||||||
|
setState((prev) => {
|
||||||
|
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
areas: newAreas,
|
||||||
|
selectedAreaId: prev.selectedAreaId === areaId ? newAreas[0]?.id || null : prev.selectedAreaId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const updateArea = (0, import_react3.useCallback)((areaId, updates) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
const updatePoint = (0, import_react3.useCallback)((areaId, pointIndex, point) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
areas: prev.areas.map((area) => {
|
||||||
|
if (area.id === areaId) {
|
||||||
|
const newPoints = [...area.basePoints];
|
||||||
|
newPoints[pointIndex] = point;
|
||||||
|
return { ...area, basePoints: newPoints };
|
||||||
|
}
|
||||||
|
return area;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
const startDragging = (0, import_react3.useCallback)((pointIndex) => {
|
||||||
|
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
|
||||||
|
}, []);
|
||||||
|
const stopDragging = (0, import_react3.useCallback)(() => {
|
||||||
|
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
||||||
|
}, []);
|
||||||
|
const setEditMode = (0, import_react3.useCallback)((mode) => {
|
||||||
|
setState((prev) => ({ ...prev, editMode: mode }));
|
||||||
|
}, []);
|
||||||
|
const getSelectedArea = (0, import_react3.useCallback)(() => {
|
||||||
|
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
|
||||||
|
}, [state.areas, state.selectedAreaId]);
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
selectArea,
|
||||||
|
addArea,
|
||||||
|
removeArea,
|
||||||
|
updateArea,
|
||||||
|
updatePoint,
|
||||||
|
startDragging,
|
||||||
|
stopDragging,
|
||||||
|
setEditMode,
|
||||||
|
getSelectedArea
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/components/EditorCanvas.tsx
|
||||||
|
var import_react4 = require("react");
|
||||||
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
||||||
|
var EditorCanvas = ({
|
||||||
|
areas,
|
||||||
|
selectedAreaId,
|
||||||
|
imageSrc,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onUpdatePoint,
|
||||||
|
onUpdateArea,
|
||||||
|
draggingPointIndex,
|
||||||
|
onStartDragging,
|
||||||
|
onStopDragging
|
||||||
|
}) => {
|
||||||
|
const containerRef = (0, import_react4.useRef)(null);
|
||||||
|
const [canvasSize, setCanvasSize] = (0, import_react4.useState)({ width: 0, height: 0 });
|
||||||
|
const [isDraggingArea, setIsDraggingArea] = (0, import_react4.useState)(false);
|
||||||
|
const [dragStartPos, setDragStartPos] = (0, import_react4.useState)(null);
|
||||||
|
(0, import_react4.useEffect)(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
setCanvasSize({ width: rect.width, height: rect.height });
|
||||||
|
}, [width, height]);
|
||||||
|
const selectedArea = areas.find((a) => a.id === selectedAreaId);
|
||||||
|
const isPointInPolygon = (0, import_react4.useCallback)((point, polygon) => {
|
||||||
|
let inside = false;
|
||||||
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||||
|
const xi = polygon[i].x, yi = polygon[i].y;
|
||||||
|
const xj = polygon[j].x, yj = polygon[j].y;
|
||||||
|
const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
|
||||||
|
if (intersect) inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}, []);
|
||||||
|
const handleMouseDown = (0, import_react4.useCallback)(
|
||||||
|
(pointIndex) => (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onStartDragging(pointIndex);
|
||||||
|
},
|
||||||
|
[onStartDragging]
|
||||||
|
);
|
||||||
|
const handleCanvasMouseDown = (0, import_react4.useCallback)(
|
||||||
|
(e) => {
|
||||||
|
if (!selectedArea || !containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
|
const clickPoint = { x, y };
|
||||||
|
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
|
||||||
|
setIsDraggingArea(true);
|
||||||
|
setDragStartPos(clickPoint);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedArea, isPointInPolygon]
|
||||||
|
);
|
||||||
|
const handleMouseMove = (0, import_react4.useCallback)(
|
||||||
|
(e) => {
|
||||||
|
if (!selectedArea || !containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
|
if (draggingPointIndex !== null) {
|
||||||
|
const clampedX = Math.max(0, Math.min(1, x));
|
||||||
|
const clampedY = Math.max(0, Math.min(1, y));
|
||||||
|
onUpdatePoint(selectedArea.id, draggingPointIndex, { x: clampedX, y: clampedY });
|
||||||
|
} else if (isDraggingArea && dragStartPos) {
|
||||||
|
const deltaX = x - dragStartPos.x;
|
||||||
|
const deltaY = y - dragStartPos.y;
|
||||||
|
const newPoints = selectedArea.basePoints.map((point) => ({
|
||||||
|
x: Math.max(0, Math.min(1, point.x + deltaX)),
|
||||||
|
y: Math.max(0, Math.min(1, point.y + deltaY))
|
||||||
|
}));
|
||||||
|
onUpdateArea(selectedArea.id, { basePoints: newPoints });
|
||||||
|
setDragStartPos({ x, y });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
||||||
|
);
|
||||||
|
const handleMouseUp = (0, import_react4.useCallback)(() => {
|
||||||
|
if (draggingPointIndex !== null) {
|
||||||
|
onStopDragging();
|
||||||
|
}
|
||||||
|
if (isDraggingArea) {
|
||||||
|
setIsDraggingArea(false);
|
||||||
|
setDragStartPos(null);
|
||||||
|
}
|
||||||
|
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
||||||
|
(0, import_react4.useEffect)(() => {
|
||||||
|
if (draggingPointIndex !== null || isDraggingArea) {
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
return () => window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
}, [draggingPointIndex, isDraggingArea, handleMouseUp]);
|
||||||
|
const uvToPixel = (u, v, points, canvasWidth, canvasHeight) => {
|
||||||
|
const [p0, p1, p2, p3] = points;
|
||||||
|
const leftX = p0.x * (1 - u) + p1.x * u;
|
||||||
|
const leftY = p0.y * (1 - u) + p1.y * u;
|
||||||
|
const rightX = p3.x * (1 - u) + p2.x * u;
|
||||||
|
const rightY = p3.y * (1 - u) + p2.y * u;
|
||||||
|
const posX = leftX * (1 - v) + rightX * v;
|
||||||
|
const posY = leftY * (1 - v) + rightY * v;
|
||||||
|
return {
|
||||||
|
x: posX * canvasWidth,
|
||||||
|
y: posY * canvasHeight
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const drawDistortionCircle = (ctx, points, canvasWidth, canvasHeight) => {
|
||||||
|
const segments = 128;
|
||||||
|
const centerU = 0.5;
|
||||||
|
const centerV = 0.5;
|
||||||
|
const maxRadius = 0.5;
|
||||||
|
const circlePoints = [];
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const theta = i / segments * 2 * Math.PI;
|
||||||
|
const u = centerU - maxRadius * Math.sin(theta);
|
||||||
|
const v = centerV + maxRadius * Math.cos(theta);
|
||||||
|
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
|
||||||
|
circlePoints.push(pixelPos);
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(circlePoints[0].x, circlePoints[0].y);
|
||||||
|
for (let i = 1; i < circlePoints.length; i++) {
|
||||||
|
ctx.lineTo(circlePoints[i].x, circlePoints[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.strokeStyle = "rgba(255, 200, 0, 0.9)";
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.setLineDash([8, 4]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillStyle = "rgba(255, 200, 0, 0.12)";
|
||||||
|
ctx.fill();
|
||||||
|
for (const r of [0.25, 0.375]) {
|
||||||
|
const gradientPoints = [];
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const theta = i / segments * 2 * Math.PI;
|
||||||
|
const u = centerU - r * Math.sin(theta);
|
||||||
|
const v = centerV + r * Math.cos(theta);
|
||||||
|
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
|
||||||
|
gradientPoints.push(pixelPos);
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(gradientPoints[0].x, gradientPoints[0].y);
|
||||||
|
for (let i = 1; i < gradientPoints.length; i++) {
|
||||||
|
ctx.lineTo(gradientPoints[i].x, gradientPoints[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
const alpha = r / maxRadius;
|
||||||
|
ctx.strokeStyle = `rgba(255, 220, 100, ${0.3 * alpha})`;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([3, 3]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerPixel.x, centerPixel.y, 5, 0, 2 * Math.PI);
|
||||||
|
ctx.fillStyle = "rgba(255, 200, 0, 1)";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
const getCursorStyle = () => {
|
||||||
|
if (draggingPointIndex !== null) return "grabbing";
|
||||||
|
if (isDraggingArea) return "grabbing";
|
||||||
|
return "default";
|
||||||
|
};
|
||||||
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
className: "editor-canvas",
|
||||||
|
style: { width, height, position: "relative", cursor: getCursorStyle() },
|
||||||
|
onMouseDown: handleCanvasMouseDown,
|
||||||
|
onMouseMove: handleMouseMove,
|
||||||
|
children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ImageDistortion, { imageSrc, areas, width, height }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "none"
|
||||||
|
},
|
||||||
|
children: areas.map((area) => {
|
||||||
|
const isSelected = area.id === selectedAreaId;
|
||||||
|
const points = area.basePoints;
|
||||||
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("g", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
||||||
|
"polygon",
|
||||||
|
{
|
||||||
|
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
|
||||||
|
fill: "none",
|
||||||
|
stroke: isSelected ? "#00aaff" : "#888",
|
||||||
|
strokeWidth: isSelected ? 2 : 1,
|
||||||
|
strokeDasharray: isSelected ? "0" : "5,5",
|
||||||
|
opacity: isSelected ? 1 : 0.5
|
||||||
|
}
|
||||||
|
) }, area.id);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
|
selectedArea && canvasSize.width > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
||||||
|
"canvas",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "none"
|
||||||
|
},
|
||||||
|
width: canvasSize.width,
|
||||||
|
height: canvasSize.height,
|
||||||
|
ref: (canvas) => {
|
||||||
|
if (canvas) {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);
|
||||||
|
drawDistortionCircle(ctx, selectedArea.basePoints, canvasSize.width, canvasSize.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
selectedArea && selectedArea.basePoints.map((point, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
left: `${point.x * 100}%`,
|
||||||
|
top: `${point.y * 100}%`,
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "#00aaff",
|
||||||
|
border: "2px solid white",
|
||||||
|
cursor: "grab",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
|
||||||
|
},
|
||||||
|
onMouseDown: handleMouseDown(index),
|
||||||
|
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
top: -24,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
fontSize: 11,
|
||||||
|
color: "#00aaff",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textShadow: "1px 1px 2px rgba(0,0,0,0.8)",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
"P",
|
||||||
|
index + 1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
index
|
||||||
|
))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/components/AreaList.tsx
|
||||||
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
||||||
|
var AreaList = ({
|
||||||
|
areas,
|
||||||
|
selectedAreaId,
|
||||||
|
onSelectArea,
|
||||||
|
onRemoveArea,
|
||||||
|
onAddArea
|
||||||
|
}) => {
|
||||||
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "area-list", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "area-list-header", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onClick: onAddArea,
|
||||||
|
disabled: areas.length >= 8,
|
||||||
|
className: "btn-add",
|
||||||
|
title: areas.length >= 8 ? "\uCD5C\uB300 8\uAC1C \uC601\uC5ED\uAE4C\uC9C0 \uC9C0\uC6D0" : "\uC0C8 \uC601\uC5ED \uCD94\uAC00",
|
||||||
|
children: "+ \uCD94\uAC00"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "area-list-empty", children: "\uC601\uC5ED\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. + \uCD94\uAC00 \uBC84\uD2BC\uC744 \uB20C\uB7EC\uC8FC\uC138\uC694." }) : areas.map((area, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`,
|
||||||
|
onClick: () => onSelectArea(area.id),
|
||||||
|
children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "area-item-info", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "area-item-name", children: [
|
||||||
|
"\uC601\uC5ED ",
|
||||||
|
index + 1
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "area-item-strength", children: [
|
||||||
|
"\uAC15\uB3C4: ",
|
||||||
|
(area.distortionStrength * 100).toFixed(0),
|
||||||
|
"%"
|
||||||
|
] })
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onClick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemoveArea(area.id);
|
||||||
|
},
|
||||||
|
className: "btn-remove",
|
||||||
|
title: "\uC601\uC5ED \uC0AD\uC81C",
|
||||||
|
children: "\xD7"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
area.id
|
||||||
|
)) })
|
||||||
|
] });
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/components/ParameterPanel.tsx
|
||||||
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
||||||
|
var EASING_OPTIONS = [
|
||||||
|
{ value: "linear", label: "\uC120\uD615 (Linear)" },
|
||||||
|
{ value: "easeIn", label: "\uAC00\uC18D (Ease In)" },
|
||||||
|
{ value: "easeOut", label: "\uAC10\uC18D (Ease Out)" },
|
||||||
|
{ value: "easeInOut", label: "\uAC00\uAC10\uC18D (Ease In Out)" },
|
||||||
|
{ value: "easeInQuad", label: "\uAC00\uC18D\xB2 (Ease In Quad)" },
|
||||||
|
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
|
||||||
|
];
|
||||||
|
var ParameterPanel = ({ area, onUpdateArea }) => {
|
||||||
|
if (!area) {
|
||||||
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "parameter-panel", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) });
|
||||||
|
}
|
||||||
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-panel", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [
|
||||||
|
"\uC65C\uACE1 \uAC15\uB3C4: ",
|
||||||
|
(area.distortionStrength * 100).toFixed(0),
|
||||||
|
"%"
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "range",
|
||||||
|
min: "0",
|
||||||
|
max: "1",
|
||||||
|
step: "0.01",
|
||||||
|
value: area.distortionStrength,
|
||||||
|
onChange: (e) => onUpdateArea({ distortionStrength: parseFloat(e.target.value) }),
|
||||||
|
className: "slider"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [
|
||||||
|
"\uC9C0\uC18D \uC2DC\uAC04: ",
|
||||||
|
area.movement.duration.toFixed(1),
|
||||||
|
"\uCD08"
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "number",
|
||||||
|
min: "0.1",
|
||||||
|
max: "10",
|
||||||
|
step: "0.1",
|
||||||
|
value: area.movement.duration,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: { ...area.movement, duration: parseFloat(e.target.value) }
|
||||||
|
}),
|
||||||
|
className: "input-number"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "\uC774\uC9D5 \uD568\uC218" }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
value: area.movement.easing,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: { ...area.movement, easing: e.target.value }
|
||||||
|
}),
|
||||||
|
className: "select",
|
||||||
|
children: EASING_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: option.value, children: option.label }, option.value))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [
|
||||||
|
"\uBCA1\uD130 X: ",
|
||||||
|
area.movement.vectorA.x.toFixed(2)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "range",
|
||||||
|
min: "-1",
|
||||||
|
max: "1",
|
||||||
|
step: "0.01",
|
||||||
|
value: area.movement.vectorA.x,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: {
|
||||||
|
...area.movement,
|
||||||
|
vectorA: { ...area.movement.vectorA, x: parseFloat(e.target.value) }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
className: "slider"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { children: [
|
||||||
|
"\uBCA1\uD130 Y: ",
|
||||||
|
area.movement.vectorA.y.toFixed(2)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "range",
|
||||||
|
min: "-1",
|
||||||
|
max: "1",
|
||||||
|
step: "0.01",
|
||||||
|
value: area.movement.vectorA.y,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: {
|
||||||
|
...area.movement,
|
||||||
|
vectorA: { ...area.movement.vectorA, y: parseFloat(e.target.value) }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
className: "slider"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "point-coord", children: [
|
||||||
|
"P",
|
||||||
|
idx + 1,
|
||||||
|
": (",
|
||||||
|
point.x.toFixed(3),
|
||||||
|
", ",
|
||||||
|
point.y.toFixed(3),
|
||||||
|
")"
|
||||||
|
] }, idx)) })
|
||||||
|
] })
|
||||||
|
] });
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/DistortionEditor.tsx
|
||||||
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
||||||
|
var DistortionEditor = ({
|
||||||
|
initialAreas = [],
|
||||||
|
imageSrc,
|
||||||
|
onAreasChange,
|
||||||
|
onSelectedAreaChange,
|
||||||
|
width = 800,
|
||||||
|
height = 600
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
selectArea,
|
||||||
|
addArea,
|
||||||
|
removeArea,
|
||||||
|
updateArea,
|
||||||
|
updatePoint,
|
||||||
|
startDragging,
|
||||||
|
stopDragging,
|
||||||
|
getSelectedArea
|
||||||
|
} = useDistortionEditor(initialAreas);
|
||||||
|
(0, import_react5.useEffect)(() => {
|
||||||
|
onAreasChange?.(state.areas);
|
||||||
|
}, [state.areas, onAreasChange]);
|
||||||
|
(0, import_react5.useEffect)(() => {
|
||||||
|
onSelectedAreaChange?.(state.selectedAreaId);
|
||||||
|
}, [state.selectedAreaId, onSelectedAreaChange]);
|
||||||
|
const handleAddArea = () => {
|
||||||
|
const newArea = {
|
||||||
|
id: `area-${Date.now()}`,
|
||||||
|
basePoints: [
|
||||||
|
{ x: 0.3, y: 0.3 },
|
||||||
|
{ x: 0.7, y: 0.3 },
|
||||||
|
{ x: 0.7, y: 0.7 },
|
||||||
|
{ x: 0.3, y: 0.7 }
|
||||||
|
],
|
||||||
|
movement: {
|
||||||
|
vectorA: { x: DEFAULT_AREA.VECTOR_A.x, y: DEFAULT_AREA.VECTOR_A.y },
|
||||||
|
vectorB: { x: DEFAULT_AREA.VECTOR_B.x, y: DEFAULT_AREA.VECTOR_B.y },
|
||||||
|
duration: DEFAULT_AREA.DURATION,
|
||||||
|
easing: DEFAULT_AREA.EASING
|
||||||
|
},
|
||||||
|
distortionStrength: DEFAULT_AREA.DISTORTION_STRENGTH,
|
||||||
|
progress: 0,
|
||||||
|
dragVector: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
addArea(newArea);
|
||||||
|
};
|
||||||
|
const handleUpdateArea = (updates) => {
|
||||||
|
if (state.selectedAreaId) {
|
||||||
|
updateArea(state.selectedAreaId, updates);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const selectedArea = getSelectedArea();
|
||||||
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "distortion-editor", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-main", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "editor-canvas-container", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
||||||
|
EditorCanvas,
|
||||||
|
{
|
||||||
|
areas: state.areas,
|
||||||
|
selectedAreaId: state.selectedAreaId,
|
||||||
|
imageSrc,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onUpdatePoint: updatePoint,
|
||||||
|
onUpdateArea: updateArea,
|
||||||
|
draggingPointIndex: state.draggingPointIndex,
|
||||||
|
onStartDragging: startDragging,
|
||||||
|
onStopDragging: stopDragging
|
||||||
|
}
|
||||||
|
) }),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "editor-sidebar", children: [
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
||||||
|
AreaList,
|
||||||
|
{
|
||||||
|
areas: state.areas,
|
||||||
|
selectedAreaId: state.selectedAreaId,
|
||||||
|
onSelectArea: selectArea,
|
||||||
|
onRemoveArea: removeArea,
|
||||||
|
onAddArea: handleAddArea
|
||||||
|
}
|
||||||
|
),
|
||||||
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ParameterPanel, { area: selectedArea, onUpdateArea: handleUpdateArea })
|
||||||
|
] })
|
||||||
|
] }) });
|
||||||
|
};
|
||||||
// Annotate the CommonJS export names for ESM import in node:
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
0 && (module.exports = {
|
0 && (module.exports = {
|
||||||
ANIMATION_CONFIG,
|
ANIMATION_CONFIG,
|
||||||
AnimationLoop,
|
AnimationLoop,
|
||||||
DEFAULT_AREA,
|
DEFAULT_AREA,
|
||||||
|
DistortionEditor,
|
||||||
ImageDistortion,
|
ImageDistortion,
|
||||||
SHADER_CONFIG,
|
SHADER_CONFIG,
|
||||||
ShaderManager,
|
ShaderManager,
|
||||||
ThreeScene,
|
ThreeScene,
|
||||||
applyEasing,
|
applyEasing,
|
||||||
useAnimationFrame
|
useAnimationFrame,
|
||||||
|
useDistortionEditor
|
||||||
});
|
});
|
||||||
//# sourceMappingURL=index.js.map
|
//# sourceMappingURL=index.js.map
|
||||||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
651
dist/index.mjs
vendored
651
dist/index.mjs
vendored
@ -91,6 +91,15 @@ var ThreeScene = class {
|
|||||||
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
console.log("[ThreeScene] render() \uD638\uCD9C\uB428, mesh:", this.mesh);
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 현재 해상도 가져오기
|
||||||
|
*/
|
||||||
|
getResolution() {
|
||||||
|
return {
|
||||||
|
x: this.uniforms.u_resolution.value.x,
|
||||||
|
y: this.uniforms.u_resolution.value.y
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 리소스 정리
|
* 리소스 정리
|
||||||
*/
|
*/
|
||||||
@ -382,19 +391,20 @@ var ImageDistortion = ({
|
|||||||
}, [imageSrc, isReady]);
|
}, [imageSrc, isReady]);
|
||||||
useEffect2(() => {
|
useEffect2(() => {
|
||||||
if (!sceneRef.current || !isReady) return;
|
if (!sceneRef.current || !isReady) return;
|
||||||
|
const resolution = sceneRef.current.getResolution();
|
||||||
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
const points = new Float32Array(SHADER_CONFIG.MAX_POINTS * 2);
|
||||||
currentAreas.forEach((area, areaIndex) => {
|
currentAreas.forEach((area, areaIndex) => {
|
||||||
area.basePoints.forEach((point, pointIndex) => {
|
area.basePoints.forEach((point, pointIndex) => {
|
||||||
const index = (areaIndex * 4 + pointIndex) * 2;
|
const index = (areaIndex * 4 + pointIndex) * 2;
|
||||||
points[index] = point.x;
|
points[index] = point.x;
|
||||||
points[index + 1] = point.y;
|
points[index + 1] = 1 - point.y;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);
|
const dragVectors = new Float32Array(SHADER_CONFIG.MAX_DRAG_VECTORS * 2);
|
||||||
currentAreas.forEach((area, index) => {
|
currentAreas.forEach((area, index) => {
|
||||||
const baseIndex = index * 2;
|
const baseIndex = index * 2;
|
||||||
dragVectors[baseIndex] = area.dragVector.x;
|
dragVectors[baseIndex] = area.dragVector.x;
|
||||||
dragVectors[baseIndex + 1] = area.dragVector.y;
|
dragVectors[baseIndex + 1] = -area.dragVector.y;
|
||||||
});
|
});
|
||||||
const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);
|
const strengths = new Float32Array(SHADER_CONFIG.MAX_STRENGTHS);
|
||||||
currentAreas.forEach((area, index) => {
|
currentAreas.forEach((area, index) => {
|
||||||
@ -447,15 +457,650 @@ var ImageDistortion = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src/editor/DistortionEditor.tsx
|
||||||
|
import { useEffect as useEffect4 } from "react";
|
||||||
|
|
||||||
|
// src/editor/hooks/useDistortionEditor.ts
|
||||||
|
import { useState as useState2, useCallback as useCallback2 } from "react";
|
||||||
|
var useDistortionEditor = (initialAreas = []) => {
|
||||||
|
const [state, setState] = useState2({
|
||||||
|
selectedAreaId: initialAreas[0]?.id || null,
|
||||||
|
areas: initialAreas,
|
||||||
|
editMode: "normal",
|
||||||
|
draggingPointIndex: null
|
||||||
|
});
|
||||||
|
const selectArea = useCallback2((areaId) => {
|
||||||
|
setState((prev) => ({ ...prev, selectedAreaId: areaId }));
|
||||||
|
}, []);
|
||||||
|
const addArea = useCallback2((area) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
areas: [...prev.areas, area],
|
||||||
|
selectedAreaId: area.id
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
const removeArea = useCallback2((areaId) => {
|
||||||
|
setState((prev) => {
|
||||||
|
const newAreas = prev.areas.filter((a) => a.id !== areaId);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
areas: newAreas,
|
||||||
|
selectedAreaId: prev.selectedAreaId === areaId ? newAreas[0]?.id || null : prev.selectedAreaId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const updateArea = useCallback2((areaId, updates) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
areas: prev.areas.map((area) => area.id === areaId ? { ...area, ...updates } : area)
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
const updatePoint = useCallback2((areaId, pointIndex, point) => {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
areas: prev.areas.map((area) => {
|
||||||
|
if (area.id === areaId) {
|
||||||
|
const newPoints = [...area.basePoints];
|
||||||
|
newPoints[pointIndex] = point;
|
||||||
|
return { ...area, basePoints: newPoints };
|
||||||
|
}
|
||||||
|
return area;
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
const startDragging = useCallback2((pointIndex) => {
|
||||||
|
setState((prev) => ({ ...prev, draggingPointIndex: pointIndex }));
|
||||||
|
}, []);
|
||||||
|
const stopDragging = useCallback2(() => {
|
||||||
|
setState((prev) => ({ ...prev, draggingPointIndex: null }));
|
||||||
|
}, []);
|
||||||
|
const setEditMode = useCallback2((mode) => {
|
||||||
|
setState((prev) => ({ ...prev, editMode: mode }));
|
||||||
|
}, []);
|
||||||
|
const getSelectedArea = useCallback2(() => {
|
||||||
|
return state.areas.find((a) => a.id === state.selectedAreaId) || null;
|
||||||
|
}, [state.areas, state.selectedAreaId]);
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
selectArea,
|
||||||
|
addArea,
|
||||||
|
removeArea,
|
||||||
|
updateArea,
|
||||||
|
updatePoint,
|
||||||
|
startDragging,
|
||||||
|
stopDragging,
|
||||||
|
setEditMode,
|
||||||
|
getSelectedArea
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/components/EditorCanvas.tsx
|
||||||
|
import { useRef as useRef3, useEffect as useEffect3, useState as useState3, useCallback as useCallback3 } from "react";
|
||||||
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
||||||
|
var EditorCanvas = ({
|
||||||
|
areas,
|
||||||
|
selectedAreaId,
|
||||||
|
imageSrc,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onUpdatePoint,
|
||||||
|
onUpdateArea,
|
||||||
|
draggingPointIndex,
|
||||||
|
onStartDragging,
|
||||||
|
onStopDragging
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef3(null);
|
||||||
|
const [canvasSize, setCanvasSize] = useState3({ width: 0, height: 0 });
|
||||||
|
const [isDraggingArea, setIsDraggingArea] = useState3(false);
|
||||||
|
const [dragStartPos, setDragStartPos] = useState3(null);
|
||||||
|
useEffect3(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
setCanvasSize({ width: rect.width, height: rect.height });
|
||||||
|
}, [width, height]);
|
||||||
|
const selectedArea = areas.find((a) => a.id === selectedAreaId);
|
||||||
|
const isPointInPolygon = useCallback3((point, polygon) => {
|
||||||
|
let inside = false;
|
||||||
|
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||||
|
const xi = polygon[i].x, yi = polygon[i].y;
|
||||||
|
const xj = polygon[j].x, yj = polygon[j].y;
|
||||||
|
const intersect = yi > point.y !== yj > point.y && point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi;
|
||||||
|
if (intersect) inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}, []);
|
||||||
|
const handleMouseDown = useCallback3(
|
||||||
|
(pointIndex) => (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onStartDragging(pointIndex);
|
||||||
|
},
|
||||||
|
[onStartDragging]
|
||||||
|
);
|
||||||
|
const handleCanvasMouseDown = useCallback3(
|
||||||
|
(e) => {
|
||||||
|
if (!selectedArea || !containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
|
const clickPoint = { x, y };
|
||||||
|
if (isPointInPolygon(clickPoint, selectedArea.basePoints)) {
|
||||||
|
setIsDraggingArea(true);
|
||||||
|
setDragStartPos(clickPoint);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selectedArea, isPointInPolygon]
|
||||||
|
);
|
||||||
|
const handleMouseMove = useCallback3(
|
||||||
|
(e) => {
|
||||||
|
if (!selectedArea || !containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left) / rect.width;
|
||||||
|
const y = (e.clientY - rect.top) / rect.height;
|
||||||
|
if (draggingPointIndex !== null) {
|
||||||
|
const clampedX = Math.max(0, Math.min(1, x));
|
||||||
|
const clampedY = Math.max(0, Math.min(1, y));
|
||||||
|
onUpdatePoint(selectedArea.id, draggingPointIndex, { x: clampedX, y: clampedY });
|
||||||
|
} else if (isDraggingArea && dragStartPos) {
|
||||||
|
const deltaX = x - dragStartPos.x;
|
||||||
|
const deltaY = y - dragStartPos.y;
|
||||||
|
const newPoints = selectedArea.basePoints.map((point) => ({
|
||||||
|
x: Math.max(0, Math.min(1, point.x + deltaX)),
|
||||||
|
y: Math.max(0, Math.min(1, point.y + deltaY))
|
||||||
|
}));
|
||||||
|
onUpdateArea(selectedArea.id, { basePoints: newPoints });
|
||||||
|
setDragStartPos({ x, y });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[draggingPointIndex, isDraggingArea, dragStartPos, selectedArea, onUpdatePoint, onUpdateArea]
|
||||||
|
);
|
||||||
|
const handleMouseUp = useCallback3(() => {
|
||||||
|
if (draggingPointIndex !== null) {
|
||||||
|
onStopDragging();
|
||||||
|
}
|
||||||
|
if (isDraggingArea) {
|
||||||
|
setIsDraggingArea(false);
|
||||||
|
setDragStartPos(null);
|
||||||
|
}
|
||||||
|
}, [draggingPointIndex, isDraggingArea, onStopDragging]);
|
||||||
|
useEffect3(() => {
|
||||||
|
if (draggingPointIndex !== null || isDraggingArea) {
|
||||||
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
return () => window.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
}
|
||||||
|
}, [draggingPointIndex, isDraggingArea, handleMouseUp]);
|
||||||
|
const uvToPixel = (u, v, points, canvasWidth, canvasHeight) => {
|
||||||
|
const [p0, p1, p2, p3] = points;
|
||||||
|
const leftX = p0.x * (1 - u) + p1.x * u;
|
||||||
|
const leftY = p0.y * (1 - u) + p1.y * u;
|
||||||
|
const rightX = p3.x * (1 - u) + p2.x * u;
|
||||||
|
const rightY = p3.y * (1 - u) + p2.y * u;
|
||||||
|
const posX = leftX * (1 - v) + rightX * v;
|
||||||
|
const posY = leftY * (1 - v) + rightY * v;
|
||||||
|
return {
|
||||||
|
x: posX * canvasWidth,
|
||||||
|
y: posY * canvasHeight
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const drawDistortionCircle = (ctx, points, canvasWidth, canvasHeight) => {
|
||||||
|
const segments = 128;
|
||||||
|
const centerU = 0.5;
|
||||||
|
const centerV = 0.5;
|
||||||
|
const maxRadius = 0.5;
|
||||||
|
const circlePoints = [];
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const theta = i / segments * 2 * Math.PI;
|
||||||
|
const u = centerU - maxRadius * Math.sin(theta);
|
||||||
|
const v = centerV + maxRadius * Math.cos(theta);
|
||||||
|
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
|
||||||
|
circlePoints.push(pixelPos);
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(circlePoints[0].x, circlePoints[0].y);
|
||||||
|
for (let i = 1; i < circlePoints.length; i++) {
|
||||||
|
ctx.lineTo(circlePoints[i].x, circlePoints[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.strokeStyle = "rgba(255, 200, 0, 0.9)";
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.setLineDash([8, 4]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillStyle = "rgba(255, 200, 0, 0.12)";
|
||||||
|
ctx.fill();
|
||||||
|
for (const r of [0.25, 0.375]) {
|
||||||
|
const gradientPoints = [];
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const theta = i / segments * 2 * Math.PI;
|
||||||
|
const u = centerU - r * Math.sin(theta);
|
||||||
|
const v = centerV + r * Math.cos(theta);
|
||||||
|
const pixelPos = uvToPixel(u, v, points, canvasWidth, canvasHeight);
|
||||||
|
gradientPoints.push(pixelPos);
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(gradientPoints[0].x, gradientPoints[0].y);
|
||||||
|
for (let i = 1; i < gradientPoints.length; i++) {
|
||||||
|
ctx.lineTo(gradientPoints[i].x, gradientPoints[i].y);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
const alpha = r / maxRadius;
|
||||||
|
ctx.strokeStyle = `rgba(255, 220, 100, ${0.3 * alpha})`;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([3, 3]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
const centerPixel = uvToPixel(centerU, centerV, points, canvasWidth, canvasHeight);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerPixel.x, centerPixel.y, 5, 0, 2 * Math.PI);
|
||||||
|
ctx.fillStyle = "rgba(255, 200, 0, 1)";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
const getCursorStyle = () => {
|
||||||
|
if (draggingPointIndex !== null) return "grabbing";
|
||||||
|
if (isDraggingArea) return "grabbing";
|
||||||
|
return "default";
|
||||||
|
};
|
||||||
|
return /* @__PURE__ */ jsxs(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
className: "editor-canvas",
|
||||||
|
style: { width, height, position: "relative", cursor: getCursorStyle() },
|
||||||
|
onMouseDown: handleCanvasMouseDown,
|
||||||
|
onMouseMove: handleMouseMove,
|
||||||
|
children: [
|
||||||
|
/* @__PURE__ */ jsx2(ImageDistortion, { imageSrc, areas, width, height }),
|
||||||
|
/* @__PURE__ */ jsx2(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "none"
|
||||||
|
},
|
||||||
|
children: areas.map((area) => {
|
||||||
|
const isSelected = area.id === selectedAreaId;
|
||||||
|
const points = area.basePoints;
|
||||||
|
return /* @__PURE__ */ jsx2("g", { children: /* @__PURE__ */ jsx2(
|
||||||
|
"polygon",
|
||||||
|
{
|
||||||
|
points: points.map((p) => `${p.x * canvasSize.width},${p.y * canvasSize.height}`).join(" "),
|
||||||
|
fill: "none",
|
||||||
|
stroke: isSelected ? "#00aaff" : "#888",
|
||||||
|
strokeWidth: isSelected ? 2 : 1,
|
||||||
|
strokeDasharray: isSelected ? "0" : "5,5",
|
||||||
|
opacity: isSelected ? 1 : 0.5
|
||||||
|
}
|
||||||
|
) }, area.id);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
|
selectedArea && canvasSize.width > 0 && /* @__PURE__ */ jsx2(
|
||||||
|
"canvas",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "none"
|
||||||
|
},
|
||||||
|
width: canvasSize.width,
|
||||||
|
height: canvasSize.height,
|
||||||
|
ref: (canvas) => {
|
||||||
|
if (canvas) {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (ctx) {
|
||||||
|
ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);
|
||||||
|
drawDistortionCircle(ctx, selectedArea.basePoints, canvasSize.width, canvasSize.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
selectedArea && selectedArea.basePoints.map((point, index) => /* @__PURE__ */ jsx2(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: `point-handle ${draggingPointIndex === index ? "dragging" : ""}`,
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
left: `${point.x * 100}%`,
|
||||||
|
top: `${point.y * 100}%`,
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "#00aaff",
|
||||||
|
border: "2px solid white",
|
||||||
|
cursor: "grab",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
|
||||||
|
},
|
||||||
|
onMouseDown: handleMouseDown(index),
|
||||||
|
children: /* @__PURE__ */ jsxs(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
top: -24,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
fontSize: 11,
|
||||||
|
color: "#00aaff",
|
||||||
|
fontWeight: "bold",
|
||||||
|
textShadow: "1px 1px 2px rgba(0,0,0,0.8)",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
"P",
|
||||||
|
index + 1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
index
|
||||||
|
))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/components/AreaList.tsx
|
||||||
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
||||||
|
var AreaList = ({
|
||||||
|
areas,
|
||||||
|
selectedAreaId,
|
||||||
|
onSelectArea,
|
||||||
|
onRemoveArea,
|
||||||
|
onAddArea
|
||||||
|
}) => {
|
||||||
|
return /* @__PURE__ */ jsxs2("div", { className: "area-list", children: [
|
||||||
|
/* @__PURE__ */ jsxs2("div", { className: "area-list-header", children: [
|
||||||
|
/* @__PURE__ */ jsx3("h3", { children: "\uC65C\uACE1 \uC601\uC5ED" }),
|
||||||
|
/* @__PURE__ */ jsx3(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onClick: onAddArea,
|
||||||
|
disabled: areas.length >= 8,
|
||||||
|
className: "btn-add",
|
||||||
|
title: areas.length >= 8 ? "\uCD5C\uB300 8\uAC1C \uC601\uC5ED\uAE4C\uC9C0 \uC9C0\uC6D0" : "\uC0C8 \uC601\uC5ED \uCD94\uAC00",
|
||||||
|
children: "+ \uCD94\uAC00"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsx3("div", { className: "area-list-items", children: areas.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "area-list-empty", children: "\uC601\uC5ED\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. + \uCD94\uAC00 \uBC84\uD2BC\uC744 \uB20C\uB7EC\uC8FC\uC138\uC694." }) : areas.map((area, index) => /* @__PURE__ */ jsxs2(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: `area-item ${selectedAreaId === area.id ? "selected" : ""}`,
|
||||||
|
onClick: () => onSelectArea(area.id),
|
||||||
|
children: [
|
||||||
|
/* @__PURE__ */ jsxs2("div", { className: "area-item-info", children: [
|
||||||
|
/* @__PURE__ */ jsxs2("span", { className: "area-item-name", children: [
|
||||||
|
"\uC601\uC5ED ",
|
||||||
|
index + 1
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsxs2("span", { className: "area-item-strength", children: [
|
||||||
|
"\uAC15\uB3C4: ",
|
||||||
|
(area.distortionStrength * 100).toFixed(0),
|
||||||
|
"%"
|
||||||
|
] })
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsx3(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onClick: (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemoveArea(area.id);
|
||||||
|
},
|
||||||
|
className: "btn-remove",
|
||||||
|
title: "\uC601\uC5ED \uC0AD\uC81C",
|
||||||
|
children: "\xD7"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
area.id
|
||||||
|
)) })
|
||||||
|
] });
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/components/ParameterPanel.tsx
|
||||||
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
||||||
|
var EASING_OPTIONS = [
|
||||||
|
{ value: "linear", label: "\uC120\uD615 (Linear)" },
|
||||||
|
{ value: "easeIn", label: "\uAC00\uC18D (Ease In)" },
|
||||||
|
{ value: "easeOut", label: "\uAC10\uC18D (Ease Out)" },
|
||||||
|
{ value: "easeInOut", label: "\uAC00\uAC10\uC18D (Ease In Out)" },
|
||||||
|
{ value: "easeInQuad", label: "\uAC00\uC18D\xB2 (Ease In Quad)" },
|
||||||
|
{ value: "easeOutQuad", label: "\uAC10\uC18D\xB2 (Ease Out Quad)" }
|
||||||
|
];
|
||||||
|
var ParameterPanel = ({ area, onUpdateArea }) => {
|
||||||
|
if (!area) {
|
||||||
|
return /* @__PURE__ */ jsx4("div", { className: "parameter-panel", children: /* @__PURE__ */ jsx4("div", { className: "parameter-panel-empty", children: "\uC601\uC5ED\uC744 \uC120\uD0DD\uD574\uC8FC\uC138\uC694" }) });
|
||||||
|
}
|
||||||
|
return /* @__PURE__ */ jsxs3("div", { className: "parameter-panel", children: [
|
||||||
|
/* @__PURE__ */ jsx4("h3", { children: "\uD30C\uB77C\uBBF8\uD130 \uD3B8\uC9D1" }),
|
||||||
|
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ jsxs3("label", { children: [
|
||||||
|
"\uC65C\uACE1 \uAC15\uB3C4: ",
|
||||||
|
(area.distortionStrength * 100).toFixed(0),
|
||||||
|
"%"
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsx4(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "range",
|
||||||
|
min: "0",
|
||||||
|
max: "1",
|
||||||
|
step: "0.01",
|
||||||
|
value: area.distortionStrength,
|
||||||
|
onChange: (e) => onUpdateArea({ distortionStrength: parseFloat(e.target.value) }),
|
||||||
|
className: "slider"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ jsxs3("label", { children: [
|
||||||
|
"\uC9C0\uC18D \uC2DC\uAC04: ",
|
||||||
|
area.movement.duration.toFixed(1),
|
||||||
|
"\uCD08"
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsx4(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "number",
|
||||||
|
min: "0.1",
|
||||||
|
max: "10",
|
||||||
|
step: "0.1",
|
||||||
|
value: area.movement.duration,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: { ...area.movement, duration: parseFloat(e.target.value) }
|
||||||
|
}),
|
||||||
|
className: "input-number"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ jsx4("label", { children: "\uC774\uC9D5 \uD568\uC218" }),
|
||||||
|
/* @__PURE__ */ jsx4(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
value: area.movement.easing,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: { ...area.movement, easing: e.target.value }
|
||||||
|
}),
|
||||||
|
className: "select",
|
||||||
|
children: EASING_OPTIONS.map((option) => /* @__PURE__ */ jsx4("option", { value: option.value, children: option.label }, option.value))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ jsxs3("label", { children: [
|
||||||
|
"\uBCA1\uD130 X: ",
|
||||||
|
area.movement.vectorA.x.toFixed(2)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsx4(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "range",
|
||||||
|
min: "-1",
|
||||||
|
max: "1",
|
||||||
|
step: "0.01",
|
||||||
|
value: area.movement.vectorA.x,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: {
|
||||||
|
...area.movement,
|
||||||
|
vectorA: { ...area.movement.vectorA, x: parseFloat(e.target.value) }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
className: "slider"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ jsxs3("label", { children: [
|
||||||
|
"\uBCA1\uD130 Y: ",
|
||||||
|
area.movement.vectorA.y.toFixed(2)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsx4(
|
||||||
|
"input",
|
||||||
|
{
|
||||||
|
type: "range",
|
||||||
|
min: "-1",
|
||||||
|
max: "1",
|
||||||
|
step: "0.01",
|
||||||
|
value: area.movement.vectorA.y,
|
||||||
|
onChange: (e) => onUpdateArea({
|
||||||
|
movement: {
|
||||||
|
...area.movement,
|
||||||
|
vectorA: { ...area.movement.vectorA, y: parseFloat(e.target.value) }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
className: "slider"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
] }),
|
||||||
|
/* @__PURE__ */ jsxs3("div", { className: "parameter-group", children: [
|
||||||
|
/* @__PURE__ */ jsx4("label", { children: "\uD3EC\uC778\uD2B8 \uC88C\uD45C (\uCE94\uBC84\uC2A4\uC5D0\uC11C \uB4DC\uB798\uADF8)" }),
|
||||||
|
/* @__PURE__ */ jsx4("div", { className: "points-display", children: area.basePoints.map((point, idx) => /* @__PURE__ */ jsxs3("div", { className: "point-coord", children: [
|
||||||
|
"P",
|
||||||
|
idx + 1,
|
||||||
|
": (",
|
||||||
|
point.x.toFixed(3),
|
||||||
|
", ",
|
||||||
|
point.y.toFixed(3),
|
||||||
|
")"
|
||||||
|
] }, idx)) })
|
||||||
|
] })
|
||||||
|
] });
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/editor/DistortionEditor.tsx
|
||||||
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
||||||
|
var DistortionEditor = ({
|
||||||
|
initialAreas = [],
|
||||||
|
imageSrc,
|
||||||
|
onAreasChange,
|
||||||
|
onSelectedAreaChange,
|
||||||
|
width = 800,
|
||||||
|
height = 600
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
selectArea,
|
||||||
|
addArea,
|
||||||
|
removeArea,
|
||||||
|
updateArea,
|
||||||
|
updatePoint,
|
||||||
|
startDragging,
|
||||||
|
stopDragging,
|
||||||
|
getSelectedArea
|
||||||
|
} = useDistortionEditor(initialAreas);
|
||||||
|
useEffect4(() => {
|
||||||
|
onAreasChange?.(state.areas);
|
||||||
|
}, [state.areas, onAreasChange]);
|
||||||
|
useEffect4(() => {
|
||||||
|
onSelectedAreaChange?.(state.selectedAreaId);
|
||||||
|
}, [state.selectedAreaId, onSelectedAreaChange]);
|
||||||
|
const handleAddArea = () => {
|
||||||
|
const newArea = {
|
||||||
|
id: `area-${Date.now()}`,
|
||||||
|
basePoints: [
|
||||||
|
{ x: 0.3, y: 0.3 },
|
||||||
|
{ x: 0.7, y: 0.3 },
|
||||||
|
{ x: 0.7, y: 0.7 },
|
||||||
|
{ x: 0.3, y: 0.7 }
|
||||||
|
],
|
||||||
|
movement: {
|
||||||
|
vectorA: { x: DEFAULT_AREA.VECTOR_A.x, y: DEFAULT_AREA.VECTOR_A.y },
|
||||||
|
vectorB: { x: DEFAULT_AREA.VECTOR_B.x, y: DEFAULT_AREA.VECTOR_B.y },
|
||||||
|
duration: DEFAULT_AREA.DURATION,
|
||||||
|
easing: DEFAULT_AREA.EASING
|
||||||
|
},
|
||||||
|
distortionStrength: DEFAULT_AREA.DISTORTION_STRENGTH,
|
||||||
|
progress: 0,
|
||||||
|
dragVector: { x: 0, y: 0 }
|
||||||
|
};
|
||||||
|
addArea(newArea);
|
||||||
|
};
|
||||||
|
const handleUpdateArea = (updates) => {
|
||||||
|
if (state.selectedAreaId) {
|
||||||
|
updateArea(state.selectedAreaId, updates);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const selectedArea = getSelectedArea();
|
||||||
|
return /* @__PURE__ */ jsx5("div", { className: "distortion-editor", children: /* @__PURE__ */ jsxs4("div", { className: "editor-main", children: [
|
||||||
|
/* @__PURE__ */ jsx5("div", { className: "editor-canvas-container", children: /* @__PURE__ */ jsx5(
|
||||||
|
EditorCanvas,
|
||||||
|
{
|
||||||
|
areas: state.areas,
|
||||||
|
selectedAreaId: state.selectedAreaId,
|
||||||
|
imageSrc,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onUpdatePoint: updatePoint,
|
||||||
|
onUpdateArea: updateArea,
|
||||||
|
draggingPointIndex: state.draggingPointIndex,
|
||||||
|
onStartDragging: startDragging,
|
||||||
|
onStopDragging: stopDragging
|
||||||
|
}
|
||||||
|
) }),
|
||||||
|
/* @__PURE__ */ jsxs4("div", { className: "editor-sidebar", children: [
|
||||||
|
/* @__PURE__ */ jsx5(
|
||||||
|
AreaList,
|
||||||
|
{
|
||||||
|
areas: state.areas,
|
||||||
|
selectedAreaId: state.selectedAreaId,
|
||||||
|
onSelectArea: selectArea,
|
||||||
|
onRemoveArea: removeArea,
|
||||||
|
onAddArea: handleAddArea
|
||||||
|
}
|
||||||
|
),
|
||||||
|
/* @__PURE__ */ jsx5(ParameterPanel, { area: selectedArea, onUpdateArea: handleUpdateArea })
|
||||||
|
] })
|
||||||
|
] }) });
|
||||||
|
};
|
||||||
export {
|
export {
|
||||||
ANIMATION_CONFIG,
|
ANIMATION_CONFIG,
|
||||||
AnimationLoop,
|
AnimationLoop,
|
||||||
DEFAULT_AREA,
|
DEFAULT_AREA,
|
||||||
|
DistortionEditor,
|
||||||
ImageDistortion,
|
ImageDistortion,
|
||||||
SHADER_CONFIG,
|
SHADER_CONFIG,
|
||||||
ShaderManager,
|
ShaderManager,
|
||||||
ThreeScene,
|
ThreeScene,
|
||||||
applyEasing,
|
applyEasing,
|
||||||
useAnimationFrame
|
useAnimationFrame,
|
||||||
|
useDistortionEditor
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=index.mjs.map
|
//# sourceMappingURL=index.mjs.map
|
||||||
2
dist/index.mjs.map
vendored
2
dist/index.mjs.map
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user