/** * Drawing Module - 독립적인 Canvas 그리기 모듈 * @version 1.0.0 * @author 김보곤 * * 사용법: * const drawer = new DrawingModule({ * container: 'myContainer', * onSave: (imageData) => { console.log('저장된 이미지:', imageData); } * }); * drawer.show(); */ class DrawingModule { constructor(options = {}) { this.options = { container: options.container || 'body', width: options.width || 800, height: options.height || 600, canvasWidth: options.canvasWidth || 320, canvasHeight: options.canvasHeight || 240, onSave: options.onSave || null, onCancel: options.onCancel || null, initialImage: options.initialImage || null, title: options.title || '그리기 도구' }; this.canvas = null; this.ctx = null; this.drawing = false; this.drawMode = 'polyline'; this.drawColor = '#000000'; this.lineWidth = 2; this.eraserRadius = 15; this.isEraser = false; this.textMode = false; this.straightMode = true; this.polyPrev = null; this.startX = 0; this.startY = 0; this.currentPath = []; this.drawingData = []; this.originalImage = null; this.modalElement = null; this.handlers = {}; } // 모달 생성 및 표시 show() { this.createModal(); this.initializeCanvas(); this.attachEventHandlers(); this.modalElement.style.display = 'block'; // 초기 이미지가 있으면 로드 if (this.options.initialImage) { this.loadImage(this.options.initialImage); } } // 모달 HTML 생성 createModal() { // 기존 모달이 있으면 제거 const existingModal = document.getElementById('drawingModuleModal'); if (existingModal) { existingModal.remove(); } const modalHtml = `

${this.options.title}

×
2px
`; // 컨테이너에 모달 추가 const container = typeof this.options.container === 'string' ? document.querySelector(this.options.container) : this.options.container; container.insertAdjacentHTML('beforeend', modalHtml); this.modalElement = document.getElementById('drawingModuleModal'); } // 캔버스 초기화 initializeCanvas() { this.canvas = document.getElementById('dmCanvas'); this.canvas.width = this.options.canvasWidth; this.canvas.height = this.options.canvasHeight; // 캔버스를 포커스 가능하게 만들기 this.canvas.tabIndex = 1; this.canvas.style.outline = 'none'; // 포커스 아웃라인 제거 this.ctx = this.canvas.getContext('2d'); this.ctx.lineWidth = this.lineWidth; this.ctx.strokeStyle = this.drawColor; this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round'; // 흰색 배경 설정 this.ctx.fillStyle = 'white'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } // 이벤트 핸들러 연결 attachEventHandlers() { // 모달 닫기 버튼 const closeBtn = this.modalElement.querySelector('.dm-close'); closeBtn.addEventListener('click', () => this.close()); // 취소 버튼 document.getElementById('dmCancelBtn').addEventListener('click', () => this.close()); // 저장 버튼 document.getElementById('dmSaveBtn').addEventListener('click', () => this.save()); // 초기화 버튼 document.getElementById('dmClearBtn').addEventListener('click', () => this.clear()); // 실행취소 버튼 document.getElementById('dmUndoBtn').addEventListener('click', () => this.undo()); // 그리기 모드 변경 document.getElementById('dmDrawMode').addEventListener('change', (e) => { this.drawMode = e.target.value; // 텍스트 모드 비활성화 this.textMode = false; document.getElementById('dmTextBtn').classList.remove('dm-active'); // 지우개 모드 비활성화 this.isEraser = false; document.getElementById('dmEraserBtn').classList.remove('dm-active'); document.getElementById('dmEraserSizeContainer').style.display = 'none'; this.removeEraserCursor(); this.canvas.style.cursor = 'crosshair'; this.resetDrawingState(); }); // 색상 변경 document.getElementById('dmDrawColor').addEventListener('change', (e) => { this.drawColor = e.target.value; this.ctx.strokeStyle = this.drawColor; }); // 선 굵기 변경 document.getElementById('dmLineWidth').addEventListener('input', (e) => { this.lineWidth = parseInt(e.target.value); this.ctx.lineWidth = this.lineWidth; document.getElementById('dmLineWidthLabel').textContent = `${this.lineWidth}px`; }); // 지우개 버튼 document.getElementById('dmEraserBtn').addEventListener('click', (e) => { e.preventDefault(); // 기본 동작 방지 e.stopPropagation(); // 이벤트 버블링 방지 this.toggleEraser(); }); // 지우개 크기 변경 document.getElementById('dmEraserSize').addEventListener('input', (e) => { this.eraserRadius = parseInt(e.target.value); document.getElementById('dmEraserSizeLabel').textContent = `${this.eraserRadius}px`; // 지우개 모드일 때 커서 크기 업데이트 if (this.isEraser) { const cursor = document.getElementById('dmEraserCursor'); if (cursor) { cursor.style.width = `${this.eraserRadius * 2}px`; cursor.style.height = `${this.eraserRadius * 2}px`; } } }); // 직각 모드 document.getElementById('dmStraightMode').addEventListener('change', (e) => { this.straightMode = e.target.checked; }); // 선 그리기 버튼 document.getElementById('dmLineBtn').addEventListener('click', () => { this.drawMode = 'line'; document.getElementById('dmDrawMode').value = 'line'; // 텍스트 모드 비활성화 this.textMode = false; document.getElementById('dmTextBtn').classList.remove('dm-active'); // 지우개 모드 비활성화 this.isEraser = false; document.getElementById('dmEraserBtn').classList.remove('dm-active'); document.getElementById('dmEraserSizeContainer').style.display = 'none'; this.removeEraserCursor(); this.canvas.style.cursor = 'crosshair'; this.resetDrawingState(); }); // 텍스트 버튼 document.getElementById('dmTextBtn').addEventListener('click', () => { this.textMode = !this.textMode; if (this.textMode) { // 텍스트 모드 활성화 document.getElementById('dmTextBtn').classList.add('dm-active'); this.canvas.style.cursor = 'text'; // 다른 모드 비활성화 this.isEraser = false; this.drawing = false; this.polyPrev = null; document.getElementById('dmEraserBtn').classList.remove('dm-active'); document.getElementById('dmEraserSizeContainer').style.display = 'none'; this.removeEraserCursor(); } else { // 텍스트 모드 비활성화 document.getElementById('dmTextBtn').classList.remove('dm-active'); this.canvas.style.cursor = 'crosshair'; } }); // 캔버스 이벤트 this.attachCanvasEvents(); } // 캔버스 이벤트 처리 attachCanvasEvents() { // 마우스 이벤트 this.canvas.addEventListener('mousedown', (e) => this.handleMouseDown(e)); this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e)); this.canvas.addEventListener('mouseup', (e) => this.handleMouseUp(e)); this.canvas.addEventListener('mouseout', (e) => this.handleMouseOut(e)); // 터치 이벤트 (모바일 지원) this.canvas.addEventListener('touchstart', (e) => this.handleTouchStart(e)); this.canvas.addEventListener('touchmove', (e) => this.handleTouchMove(e)); this.canvas.addEventListener('touchend', (e) => this.handleTouchEnd(e)); // ESC 키 이벤트 (점연결 모드 취소) this.handleEscKey = (e) => { if (e.key === 'Escape' && this.drawMode === 'polyline' && this.polyPrev) { this.cancelPolyline(); } }; document.addEventListener('keydown', this.handleEscKey); } // 마우스 다운 이벤트 handleMouseDown(e) { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // 캔버스 밖에서 클릭한 경우 무시 if (x < 0 || x > this.canvas.width || y < 0 || y > this.canvas.height) { return; } if (this.textMode) { this.addText(x, y); return; // 텍스트 모드는 유지됨 } if (this.isEraser) { this.drawing = true; this.eraseAt(x, y); return; } switch (this.drawMode) { case 'polyline': this.handlePolylineClick(x, y); break; case 'line': this.startLine(x, y); break; case 'free': this.startFreeDraw(x, y); break; } } // 마우스 이동 이벤트 handleMouseMove(e) { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; if (this.isEraser && this.drawing) { this.eraseAt(x, y); return; } if (this.drawing) { switch (this.drawMode) { case 'line': this.previewLine(x, y); break; case 'free': this.continueDraw(x, y); break; } } // polyline 모드에서 미리보기 if (this.drawMode === 'polyline' && this.polyPrev) { this.previewPolyline(x, y); } } // 마우스 업 이벤트 handleMouseUp(e) { if (this.isEraser) { this.drawing = false; this.saveState(); return; } if (this.drawing && this.drawMode === 'line') { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.endLine(x, y); } else if (this.drawing && this.drawMode === 'free') { this.endFreeDraw(); } } // 마우스 아웃 이벤트 handleMouseOut(e) { if (this.drawing && this.drawMode === 'free') { this.endFreeDraw(); } } // 터치 이벤트 (모바일) handleTouchStart(e) { e.preventDefault(); const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY }); this.canvas.dispatchEvent(mouseEvent); } handleTouchMove(e) { e.preventDefault(); const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY }); this.canvas.dispatchEvent(mouseEvent); } handleTouchEnd(e) { e.preventDefault(); const mouseEvent = new MouseEvent('mouseup', {}); this.canvas.dispatchEvent(mouseEvent); } // Polyline 그리기 handlePolylineClick(x, y) { if (!this.polyPrev) { // 첫 번째 점 this.polyPrev = { x, y }; this.currentPath = [{ x, y }]; this.saveState(); // 시작 시점 상태 저장 } else { // 연결선 그리기 let endX = x; let endY = y; if (this.straightMode) { const angle = Math.atan2(y - this.polyPrev.y, x - this.polyPrev.x); const angleDeg = Math.abs(angle * 180 / Math.PI); if (angleDeg < 22.5 || angleDeg > 157.5) { endY = this.polyPrev.y; // 수평선 } else if (angleDeg > 67.5 && angleDeg < 112.5) { endX = this.polyPrev.x; // 수직선 } } this.ctx.beginPath(); this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y); this.ctx.lineTo(endX, endY); this.ctx.stroke(); this.currentPath.push({ x: endX, y: endY }); this.polyPrev = { x: endX, y: endY }; this.saveState(); // 각 선분 그린 후 상태 저장 } } // Polyline 미리보기 previewPolyline(x, y) { // 이전 상태 복원 (잔상 제거) if (this.drawingData.length > 0) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); // 미리보기 선 그리기 let endX = x; let endY = y; if (this.straightMode) { const angle = Math.atan2(y - this.polyPrev.y, x - this.polyPrev.x); const angleDeg = Math.abs(angle * 180 / Math.PI); if (angleDeg < 22.5 || angleDeg > 157.5) { endY = this.polyPrev.y; } else if (angleDeg > 67.5 && angleDeg < 112.5) { endX = this.polyPrev.x; } } this.ctx.save(); this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)'; this.ctx.setLineDash([5, 5]); this.ctx.beginPath(); this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y); this.ctx.lineTo(endX, endY); this.ctx.stroke(); this.ctx.restore(); }; img.src = this.drawingData[this.drawingData.length - 1]; } } // 직선 그리기 startLine(x, y) { this.drawing = true; this.startX = x; this.startY = y; this.saveState(); } previewLine(x, y) { // 이전 상태 복원 (미리보기용) if (this.drawingData.length > 0) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); // 미리보기 선 그리기 let endX = x; let endY = y; if (this.straightMode) { const angle = Math.atan2(y - this.startY, x - this.startX); const angleDeg = Math.abs(angle * 180 / Math.PI); if (angleDeg < 22.5 || angleDeg > 157.5) { endY = this.startY; } else if (angleDeg > 67.5 && angleDeg < 112.5) { endX = this.startX; } } this.ctx.save(); this.ctx.strokeStyle = this.drawColor; this.ctx.lineWidth = this.lineWidth; this.ctx.beginPath(); this.ctx.moveTo(this.startX, this.startY); this.ctx.lineTo(endX, endY); this.ctx.stroke(); this.ctx.restore(); }; img.src = this.drawingData[this.drawingData.length - 1]; } } endLine(x, y) { // 최종 선 그리기 let endX = x; let endY = y; if (this.straightMode) { const angle = Math.atan2(y - this.startY, x - this.startX); const angleDeg = Math.abs(angle * 180 / Math.PI); if (angleDeg < 22.5 || angleDeg > 157.5) { endY = this.startY; } else if (angleDeg > 67.5 && angleDeg < 112.5) { endX = this.startX; } } // 먼저 이전 상태 복원 if (this.drawingData.length > 0) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); // 최종 선 그리기 this.ctx.save(); this.ctx.strokeStyle = this.drawColor; this.ctx.lineWidth = this.lineWidth; this.ctx.beginPath(); this.ctx.moveTo(this.startX, this.startY); this.ctx.lineTo(endX, endY); this.ctx.stroke(); this.ctx.restore(); // 그린 후 상태 저장 this.drawing = false; this.saveState(); }; img.src = this.drawingData[this.drawingData.length - 1]; } else { // 저장된 상태가 없는 경우 직접 그리기 this.ctx.save(); this.ctx.strokeStyle = this.drawColor; this.ctx.lineWidth = this.lineWidth; this.ctx.beginPath(); this.ctx.moveTo(this.startX, this.startY); this.ctx.lineTo(endX, endY); this.ctx.stroke(); this.ctx.restore(); this.drawing = false; this.saveState(); } } // 자유 그리기 startFreeDraw(x, y) { this.drawing = true; this.ctx.beginPath(); this.ctx.moveTo(x, y); } continueDraw(x, y) { this.ctx.lineTo(x, y); this.ctx.stroke(); } endFreeDraw() { this.drawing = false; this.saveState(); } // 지우개 기능 toggleEraser() { this.isEraser = !this.isEraser; const eraserBtn = document.getElementById('dmEraserBtn'); const eraserContainer = document.getElementById('dmEraserSizeContainer'); if (this.isEraser) { eraserBtn.classList.add('dm-active'); eraserContainer.style.display = 'block'; this.canvas.style.cursor = 'none'; // 다른 그리기 모드 비활성화 this.textMode = false; this.drawing = false; this.polyPrev = null; document.getElementById('dmTextBtn').classList.remove('dm-active'); // 지우개 커서 생성 (포커스 관련 문제 방지를 위해 마지막에) this.createEraserCursor(); } else { eraserBtn.classList.remove('dm-active'); eraserContainer.style.display = 'none'; // 텍스트 모드가 아니면 crosshair 커서로 this.canvas.style.cursor = this.textMode ? 'text' : 'crosshair'; this.removeEraserCursor(); } } eraseAt(x, y) { this.ctx.save(); this.ctx.globalCompositeOperation = 'destination-out'; this.ctx.beginPath(); this.ctx.arc(x, y, this.eraserRadius, 0, Math.PI * 2); this.ctx.fill(); this.ctx.restore(); } // 지우개 커서 createEraserCursor() { // 기존 커서가 있으면 먼저 제거 this.removeEraserCursor(); let cursor = document.createElement('div'); cursor.id = 'dmEraserCursor'; cursor.className = 'dm-eraser-cursor'; cursor.style.width = `${this.eraserRadius * 2}px`; cursor.style.height = `${this.eraserRadius * 2}px`; cursor.style.pointerEvents = 'none'; // 커서가 마우스 이벤트를 방해하지 않도록 cursor.style.position = 'absolute'; cursor.style.display = 'none'; cursor.style.zIndex = '999'; // 커서가 충분히 위에 나타나도록 cursor.style.borderRadius = '50%'; // 원형 모양 this.modalElement.querySelector('.dm-canvas-area').appendChild(cursor); // 이벤트 리스너 바인딩 저장 (중복 방지를 위해 기존 것 제거 후 추가) this.eraserCursorMove = (e) => this.updateEraserCursor(e); this.eraserCursorEnter = (e) => { const cursor = document.getElementById('dmEraserCursor'); if (cursor && this.isEraser) { this.updateEraserCursor(e); // 진입 시 위치 업데이트 cursor.style.display = 'block'; } }; this.eraserCursorLeave = () => { const cursor = document.getElementById('dmEraserCursor'); if (cursor) { cursor.style.display = 'none'; } }; this.canvas.addEventListener('mousemove', this.eraserCursorMove, { passive: true }); this.canvas.addEventListener('mouseenter', this.eraserCursorEnter, { passive: true }); this.canvas.addEventListener('mouseleave', this.eraserCursorLeave, { passive: true }); } updateEraserCursor(e) { const cursor = document.getElementById('dmEraserCursor'); if (!cursor || !this.isEraser) return; const canvasRect = this.canvas.getBoundingClientRect(); const canvasAreaRect = this.modalElement.querySelector('.dm-canvas-area').getBoundingClientRect(); // 캔버스 영역 상대 좌표 계산 const canvasX = e.clientX - canvasRect.left; const canvasY = e.clientY - canvasRect.top; // 캔버스 영역 대비 좌표 계산 (커서 위치용) const areaX = e.clientX - canvasAreaRect.left; const areaY = e.clientY - canvasAreaRect.top; // 커서 크기 업데이트 cursor.style.width = `${this.eraserRadius * 2}px`; cursor.style.height = `${this.eraserRadius * 2}px`; // 캔버스 내부에 있을 때만 커서 표시 if (canvasX >= 0 && canvasX <= this.canvas.width && canvasY >= 0 && canvasY <= this.canvas.height) { cursor.style.left = `${areaX - this.eraserRadius}px`; cursor.style.top = `${areaY - this.eraserRadius}px`; cursor.style.display = 'block'; } else { cursor.style.display = 'none'; } } removeEraserCursor() { // 이벤트 리스너 제거 if (this.canvas) { if (this.eraserCursorMove) { this.canvas.removeEventListener('mousemove', this.eraserCursorMove); } if (this.eraserCursorEnter) { this.canvas.removeEventListener('mouseenter', this.eraserCursorEnter); } if (this.eraserCursorLeave) { this.canvas.removeEventListener('mouseleave', this.eraserCursorLeave); } } // 커서 엘리먼트 제거 const cursor = document.getElementById('dmEraserCursor'); if (cursor) { cursor.remove(); } } // 텍스트 추가 addText(x, y) { const text = prompt('텍스트를 입력하세요:'); if (text) { this.ctx.save(); this.ctx.font = '14px Arial'; this.ctx.fillStyle = this.drawColor; this.ctx.fillText(text, x, y); this.ctx.restore(); this.saveState(); } // 텍스트 모드 유지 (사용자가 명시적으로 다른 모드를 선택할 때까지) // this.textMode = false; // 이 줄을 제거하여 텍스트 모드 유지 // document.getElementById('dmTextBtn').classList.remove('dm-active'); // 버튼 활성 상태 유지 this.canvas.style.cursor = 'text'; // 텍스트 커서 유지 } // 상태 저장/복원 saveState() { const imageData = this.canvas.toDataURL(); this.drawingData.push(imageData); if (this.drawingData.length > 20) { this.drawingData.shift(); } } restoreState() { if (this.drawingData.length > 0) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); }; img.src = this.drawingData[this.drawingData.length - 1]; } } redrawCanvas() { if (this.drawingData.length > 0) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); }; img.src = this.drawingData[this.drawingData.length - 1]; } } // 실행취소 undo() { if (this.drawingData.length > 1) { this.drawingData.pop(); this.restoreState(); } else if (this.drawingData.length === 1) { this.drawingData.pop(); this.clear(); } } // 초기화 clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = 'white'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.drawingData = []; this.resetDrawingState(); } // 그리기 상태 초기화 resetDrawingState() { this.drawing = false; this.polyPrev = null; this.currentPath = []; this.isEraser = false; this.textMode = false; document.getElementById('dmEraserBtn').classList.remove('dm-active'); document.getElementById('dmTextBtn').classList.remove('dm-active'); document.getElementById('dmEraserSizeContainer').style.display = 'none'; this.canvas.style.cursor = 'crosshair'; this.removeEraserCursor(); // 잔상 제거를 위한 캔버스 다시 그리기 if (this.drawingData.length > 0) { this.restoreState(); } } // Polyline 취소 (ESC 키) cancelPolyline() { this.polyPrev = null; this.currentPath = []; // 마지막 저장 상태로 복원 if (this.drawingData.length > 0) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); }; img.src = this.drawingData[this.drawingData.length - 1]; } else { // 저장된 상태가 없으면 캔버스 초기화 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = 'white'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } console.log('Polyline 그리기 취소됨'); } // 이미지 로드 loadImage(imageSrc) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 캔버스 크기에 맞게 이미지 조정 const scale = Math.min( this.canvas.width / img.width, this.canvas.height / img.height ); const width = img.width * scale; const height = img.height * scale; const x = (this.canvas.width - width) / 2; const y = (this.canvas.height - height) / 2; this.ctx.drawImage(img, x, y, width, height); this.originalImage = this.canvas.toDataURL(); this.saveState(); }; img.src = imageSrc; } // 저장 save() { // JPG로 저장하기 위해 흰색 배경이 있는 임시 캔버스 생성 const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.canvas.width; tempCanvas.height = this.canvas.height; const tempCtx = tempCanvas.getContext('2d'); // 흰색 배경 먼저 그리기 tempCtx.fillStyle = '#FFFFFF'; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); // 기존 캔버스 내용을 위에 그리기 tempCtx.drawImage(this.canvas, 0, 0); // JPG 형식으로 데이터 생성 (품질 0.9) const imageData = tempCanvas.toDataURL('image/jpeg', 0.9); if (this.options.onSave) { this.options.onSave({ imageData: imageData, timestamp: new Date().toISOString(), width: this.canvas.width, height: this.canvas.height, format: 'jpeg' }); } this.close(); } // 닫기 close() { if (this.options.onCancel && !this.saved) { this.options.onCancel(); } // ESC 키 이벤트 리스너 제거 if (this.handleEscKey) { document.removeEventListener('keydown', this.handleEscKey); } this.removeEraserCursor(); this.modalElement.remove(); this.modalElement = null; this.canvas = null; this.ctx = null; } // 이미지 데이터 가져오기 (JPG 형식) getImageData(format = 'jpeg', quality = 0.9) { if (format === 'jpeg') { // JPG의 경우 흰색 배경 추가 const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.canvas.width; tempCanvas.height = this.canvas.height; const tempCtx = tempCanvas.getContext('2d'); // 흰색 배경 먼저 그리기 tempCtx.fillStyle = '#FFFFFF'; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); // 기존 캔버스 내용을 위에 그리기 tempCtx.drawImage(this.canvas, 0, 0); return tempCanvas.toDataURL('image/jpeg', quality); } else { // PNG는 기존 방식 return this.canvas.toDataURL('image/png'); } } // 이미지 다운로드 (기본 JPG) download(filename = 'drawing.jpg', format = 'jpeg', quality = 0.9) { const link = document.createElement('a'); link.download = filename; if (format === 'jpeg') { // JPG의 경우 흰색 배경 추가 const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.canvas.width; tempCanvas.height = this.canvas.height; const tempCtx = tempCanvas.getContext('2d'); // 흰색 배경 먼저 그리기 tempCtx.fillStyle = '#FFFFFF'; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); // 기존 캔버스 내용을 위에 그리기 tempCtx.drawImage(this.canvas, 0, 0); link.href = tempCanvas.toDataURL('image/jpeg', quality); } else { // PNG는 기존 방식 link.href = this.canvas.toDataURL('image/png'); } link.click(); } } // 전역 네임스페이스에 등록 window.DrawingModule = DrawingModule;