// imageEditor.js export function openImageEditor(initialSrc) { return new Promise((resolve, reject) => { const dialog = document.getElementById('editorDialog'); const canvasEl = document.getElementById('c'); const applyBtn = document.getElementById('applyBtn'); const closeBtn = document.getElementById('closeEditorBtn'); // 1회성 초기화 if (!openImageEditor._initialized) { const canvas = new fabric.Canvas(canvasEl, { selection: false }); openImageEditor._canvas = canvas; // 상태 변수 let mode = 'polyline'; let selectMode = false; let isLine = false; let lineObj = null; let polyPoints = []; let previewLine = null; let isPreview = true; let isRight = true; let currentColor = '#000000'; // 브러시 설정 const freeBrush = canvas.freeDrawingBrush; freeBrush.width = 2; freeBrush.color = currentColor; const eraserBrush = new fabric.PencilBrush(canvas); eraserBrush.width = +document.getElementById('eraserRange').value; eraserBrush.color = '#ffffff'; function updateEraserCursor(r) { const svg = ``; canvas.freeDrawingCursor = `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${r/2} ${r/2}, auto`; } updateEraserCursor(eraserBrush.width); // 버튼 강조 function highlight() { document.querySelectorAll('.toolbar-btn').forEach(btn => btn.classList.remove('active')); switch (mode) { case 'polyline': document.getElementById('polyBtn').classList.add('active'); break; case 'free': document.getElementById('freeBtn').classList.add('active'); break; case 'line': document.getElementById('lineBtn').classList.add('active'); break; case 'text': document.getElementById('textBtn').classList.add('active'); break; case 'eraser': document.getElementById('eraserBtn').classList.add('active'); break; case 'select': document.getElementById('selectBtn').classList.add('active'); break; } } // 모드 전환 function setMode(m) { if (mode === 'polyline' && m !== 'polyline') { if (previewLine) { canvas.remove(previewLine); previewLine = null; } polyPoints = []; } mode = m; canvas.isDrawingMode = false; if (m === 'free') { canvas.isDrawingMode = true; canvas.freeDrawingBrush = freeBrush; } if (m === 'eraser') { canvas.isDrawingMode = true; canvas.freeDrawingBrush = eraserBrush; } selectMode = (m === 'select'); canvas.selection = selectMode; canvas.upperCanvasEl.style.cursor = (m === 'eraser') ? canvas.freeDrawingCursor : 'crosshair'; canvas.getObjects().forEach(o => { if (o === canvas.backgroundImage) return; o.selectable = selectMode; o.evented = selectMode; }); highlight(); } setMode('polyline'); // 툴바 버튼 이벤트 document.getElementById('polyBtn').addEventListener('click', () => setMode('polyline')); document.getElementById('freeBtn').addEventListener('click', () => setMode('free')); document.getElementById('lineBtn').addEventListener('click', () => setMode('line')); document.getElementById('textBtn').addEventListener('click', () => setMode('text')); document.getElementById('eraserBtn').addEventListener('click', () => setMode('eraser')); document.getElementById('selectBtn').addEventListener('click', () => setMode(document.getElementById('selectBtn').classList.contains('active') ? 'free' : 'select') ); document.getElementById('clearBtn').addEventListener('click', () => { canvas.clear(); fabric.Image.fromURL(openImageEditor._lastSrc, img => { img.selectable = false; canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); }); polyPoints = []; if (previewLine) { canvas.remove(previewLine); previewLine = null; } canvas.upperCanvasEl.style.cursor = 'crosshair'; setMode('polyline'); }); // 색상 선택 document.querySelectorAll('#colorPicker .color-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('#colorPicker .color-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentColor = btn.getAttribute('data-color'); freeBrush.color = currentColor; }); }); // 지우개 크기 document.getElementById('eraserRange').addEventListener('input', e => { const r = +e.target.value; document.getElementById('eraserSizeLabel').innerText = r; eraserBrush.width = r; updateEraserCursor(r); if (mode === 'eraser') canvas.upperCanvasEl.style.cursor = canvas.freeDrawingCursor; }); // 직각 고정 document.getElementById('rightAngle').addEventListener('change', e => { isRight = e.target.checked; }); // 키보드 단축키 document.addEventListener('keydown', e => { if (e.key.toLowerCase() === 'l') setMode('line'); if (e.key === 'Escape' && mode === 'polyline') { isPreview = false; if (previewLine) { canvas.remove(previewLine); previewLine = null; canvas.requestRenderAll(); } } if (e.key === 'Escape' && mode === 'text') { document.querySelectorAll('.canvas-text-input').forEach(el => el.remove()); } if (e.key === 'Delete' && selectMode) { canvas.getActiveObjects().forEach(o => canvas.remove(o)); canvas.discardActiveObject(); canvas.requestRenderAll(); } }); // 마우스 이벤트 canvas.on('mouse:down', opt => { const p = canvas.getPointer(opt.e); if (mode === 'line') { isLine = true; lineObj = new fabric.Line([p.x, p.y, p.x, p.y], { stroke: currentColor, strokeWidth: 2, originX: 'center', originY: 'center', selectable: selectMode }); canvas.add(lineObj); } else if (mode === 'polyline') { isPreview = true; let x = p.x, y = p.y; if (isRight && polyPoints.length) { const prev = polyPoints[polyPoints.length - 1]; const dx = Math.abs(x - prev.x), dy = Math.abs(y - prev.y); if (dx > dy) y = prev.y; else x = prev.x; } if (polyPoints.length) { const prev = polyPoints[polyPoints.length - 1]; const seg = new fabric.Line([prev.x, prev.y, x, y], { stroke: currentColor, strokeWidth: 2, originX: 'center', originY: 'center', selectable: selectMode }); canvas.add(seg); } polyPoints.push({ x, y }); if (previewLine) { canvas.remove(previewLine); previewLine = null; } } else if (mode === 'text') { document.querySelectorAll('.canvas-text-input').forEach(el => el.remove()); const ta = document.createElement('textarea'); ta.className = 'canvas-text-input'; ta.style.left = `${p.x}px`; ta.style.top = `${p.y}px`; ta.rows = 1; ta.cols = 20; ta.style.fontSize = '14px'; document.getElementById('editorBody').appendChild(ta); setTimeout(() => ta.focus(), 0); ta.addEventListener('keydown', ev => { if (ev.key === 'Enter') { ev.preventDefault(); const txt = ta.value.trim(); if (txt) { const t = new fabric.Text(txt, { left: p.x, top: p.y, fontSize: 14, fill: currentColor }); canvas.add(t); } ta.remove(); } else if (ev.key === 'Escape') ta.remove(); }); } }); canvas.on('mouse:move', opt => { const p = canvas.getPointer(opt.e); if (mode === 'line' && isLine) { let x2 = p.x, y2 = p.y; if (isRight) { const dx = Math.abs(x2 - lineObj.x1), dy = Math.abs(y2 - lineObj.y1); if (dx > dy) y2 = lineObj.y1; else x2 = lineObj.x1; } lineObj.set({ x2, y2 }); canvas.requestRenderAll(); } else if (mode === 'polyline' && polyPoints.length && isPreview) { const L = polyPoints[polyPoints.length - 1]; let x2 = p.x, y2 = p.y; if (isRight) { const dx = Math.abs(x2 - L.x), dy = Math.abs(y2 - L.y); if (dx > dy) y2 = L.y; else x2 = L.x; } if (previewLine) { previewLine.set({ x1: L.x, y1: L.y, x2, y2 }); } else { previewLine = new fabric.Line([L.x, L.y, x2, y2], { stroke: 'gray', strokeWidth: 1, strokeDashArray: [5,5], selectable: false }); canvas.add(previewLine); } canvas.requestRenderAll(); } }); canvas.on('mouse:up', () => { if (mode === 'line') { isLine = false; lineObj = null; } }); // path 생성 시 스타일 canvas.on('path:created', e => { e.path.selectable = selectMode; e.path.stroke = (mode === 'eraser') ? '#ffffff' : currentColor; }); openImageEditor._initialized = true; } // 이미지 로드 및 dialog 표시 openImageEditor._lastSrc = initialSrc; fabric.Image.fromURL(initialSrc, img => { img.selectable = false; openImageEditor._canvas.setBackgroundImage(img, openImageEditor._canvas.renderAll.bind(openImageEditor._canvas)); dialog.showModal(); }); // 적용 버튼 applyBtn.addEventListener('click', () => { const dataURL = openImageEditor._canvas.toDataURL({ format: 'png' }); resolve(dataURL); dialog.close(); }); // 닫기 버튼 closeBtn.addEventListener('click', () => { reject(new Error('User cancelled')); dialog.close(); }); // ESC로 닫기 방지 dialog.addEventListener('cancel', e => e.preventDefault()); }); }