export default class DrawingTool { constructor({ container, fileInput, drawBtn, lineBtn, textBtn, eraserBtn, saveBtn, clearBtn, modeSelect, colorPicker, straightToggle, eraserSize, eraserSizeLabel }) { // DOM elements this.container = container; this.fileInput = fileInput; // for saving this.drawBtn = drawBtn; this.lineBtn = lineBtn; this.textBtn = textBtn; this.eraserBtn = eraserBtn; this.saveBtn = saveBtn; this.clearBtn = clearBtn; this.modeSelect = modeSelect; this.colorPicker = colorPicker; this.straightToggle = straightToggle; this.eraserSize = eraserSize; this.eraserSizeLabel = eraserSizeLabel; // internal state this.canvas = null; this.ctx = null; this.drawingData = []; this.currentPath = []; this.isDrawing = false; this.drawMode = 'polyline'; this.drawColor = colorPicker.value || '#000'; this.eraserRadius = parseInt(eraserSize.value, 10) || 15; this.polyPrev = null; this.previewLine = null; // bind handlers this._onMouseDown = this._onMouseDown.bind(this); this._onMouseMove = this._onMouseMove.bind(this); this._onMouseUp = this._onMouseUp.bind(this); this._onMouseOut = this._onMouseOut.bind(this); this._onCanvasClick = this._onCanvasClick.bind(this); this._onKeyDown = this._onKeyDown.bind(this); } init() { // UI controls this.drawBtn.addEventListener('click', () => this._startDrawing()); this.lineBtn.addEventListener('click', () => this._setMode('polyline', this.lineBtn)); this.textBtn.addEventListener('click', () => this._setMode('text', this.textBtn)); this.eraserBtn.addEventListener('click',() => this._setMode('eraser', this.eraserBtn)); this.modeSelect.addEventListener('change',() => this._setMode(this.modeSelect.value)); this.colorPicker.addEventListener('input',() => this.drawColor = this.colorPicker.value); this.straightToggle.addEventListener('change', () => {}); this.eraserSize.addEventListener('input', () => { this.eraserRadius = parseInt(this.eraserSize.value, 10); this.eraserSizeLabel.textContent = this.eraserSize.value + 'px'; }); // action buttons this.clearBtn.addEventListener('click', () => this._clearCanvas()); this.saveBtn.addEventListener('click', () => this._saveDrawing()); } _startDrawing() { // reset mode to polyline this._setMode('polyline', this.lineBtn); if (this.canvas) return; // create overlay canvas this.canvas = document.createElement('canvas'); this.canvas.classList.add('drawing-canvas'); this.canvas.width = this.container.clientWidth; this.canvas.height = this.container.clientHeight; this.container.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); this.ctx.lineWidth = 2; // draw background image or white const img = this.container.querySelector('img'); if (img && img.complete) { this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height); } else if (!img) { this.ctx.fillStyle = '#fff'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } // bind events this.canvas.addEventListener('mousedown', this._onMouseDown); this.canvas.addEventListener('mousemove', this._onMouseMove); this.canvas.addEventListener('mouseup', this._onMouseUp); this.canvas.addEventListener('mouseout', this._onMouseOut); this.canvas.addEventListener('click', this._onCanvasClick); document.addEventListener('keydown', this._onKeyDown); // show controls [this.lineBtn, this.textBtn, this.eraserBtn, this.modeSelect, this.colorPicker, this.straightToggle, this.saveBtn, this.clearBtn, this.eraserSize, this.eraserSizeLabel] .forEach(el => el.style.display = 'inline-block'); } _setMode(mode, activeBtn) { this.drawMode = mode; this.modeSelect.value = mode; [this.lineBtn, this.textBtn, this.eraserBtn].forEach(btn => btn.classList.remove('active')); if (activeBtn) activeBtn.classList.add('active'); } _onMouseDown(e) { if (this.drawMode === 'polyline') return; this.isDrawing = true; const {x,y} = this._getXY(e); if (this.drawMode === 'free') { this.ctx.beginPath(); this.ctx.moveTo(x,y); this.currentPath = [{x,y}]; } else { this.startX = x; this.startY = y; } } _onMouseMove(e) { const {x,y} = this._getXY(e); if (this.drawMode === 'polyline' && this.polyPrev) { this._drawPreviewLine(x,y); } if (!this.isDrawing) return; if (this.drawMode === 'eraser') { this.ctx.save(); this.ctx.globalCompositeOperation = 'destination-out'; this.ctx.beginPath(); this.ctx.arc(x,y,this.eraserRadius,0,2*Math.PI); this.ctx.fill(); this.ctx.restore(); } else if (this.drawMode === 'free') { this.ctx.strokeStyle = this.drawColor; this.ctx.lineTo(x,y); this.ctx.stroke(); this.currentPath.push({x,y}); } } _onMouseUp(e) { if (!this.isDrawing) return; const {x,y} = this._getXY(e); if (this.drawMode === 'line') { this.ctx.beginPath(); this.ctx.moveTo(this.startX, this.startY); this.ctx.lineTo(x,y); this.ctx.strokeStyle = this.drawColor; this.ctx.stroke(); this._saveData('line', {startX:this.startX, startY:this.startY, endX:x, endY:y}); } else if (this.drawMode === 'free' && this.currentPath.length >1) { this._saveData('free', {path: [...this.currentPath]}); } this.isDrawing = false; this.currentPath = []; } _onMouseOut() { this.isDrawing = false; if (this.drawMode==='polyline' && this.previewLine) { this._redrawAll(); this.previewLine = null; } } _onCanvasClick(e) { if (this.drawMode === 'text') return; // text input handled separately if (this.drawMode !== 'polyline') return; const {x:rawX,y:rawY} = this._getXY(e); let endX=rawX, endY=rawY; if (this.straightToggle.checked && this.polyPrev) { const dx=rawX-this.polyPrev.x, dy=rawY-this.polyPrev.y; if (Math.abs(dx)>Math.abs(dy)) endY=this.polyPrev.y; else endX=this.polyPrev.x; } if (!this.polyPrev) { this.polyPrev={x:endX,y:endY}; return; } this._redrawAll(); this.ctx.beginPath(); this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y); this.ctx.lineTo(endX,endY); this.ctx.strokeStyle = this.drawColor; this.ctx.stroke(); this._saveData('polyline', {startX:this.polyPrev.x, startY:this.polyPrev.y, endX, endY}); this.polyPrev={x:endX,y:endY}; } _onKeyDown(e) { if (e.key==='Escape' && this.drawMode==='polyline') { this.polyPrev = null; this._redrawAll(); this.previewLine = null; } } _getXY(e) { const r = this.canvas.getBoundingClientRect(); return {x: e.clientX - r.left, y: e.clientY - r.top}; } _drawPreviewLine(x,y) { this._redrawAll(); this.ctx.save(); this.ctx.strokeStyle = this.drawColor; this.ctx.globalAlpha = 0.3; this.ctx.setLineDash([5,5]); this.ctx.beginPath(); this.ctx.moveTo(this.polyPrev.x, this.polyPrev.y); this.ctx.lineTo(x,y); this.ctx.stroke(); this.ctx.restore(); this.previewLine={startX:this.polyPrev.x,startY:this.polyPrev.y,endX:x,endY:y}; } _saveData(type, data) { this.drawingData.push({type, data, color:this.drawColor, lineWidth:this.ctx.lineWidth}); } _redrawAll() { // clear then draw background this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height); const img = this.container.querySelector('img'); if (img && img.complete) this.ctx.drawImage(img,0,0,this.canvas.width,this.canvas.height); else { this.ctx.fillStyle='#fff'; this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height); } // redraw shapes for (const item of this.drawingData) { this.ctx.save(); this.ctx.strokeStyle = item.color; this.ctx.lineWidth = item.lineWidth; this.ctx.setLineDash([]); if (item.type==='line' || item.type==='polyline') { this.ctx.beginPath(); this.ctx.moveTo(item.data.startX, item.data.startY); this.ctx.lineTo(item.data.endX, item.data.endY); this.ctx.stroke(); } else if (item.type==='free') { const p0 = item.data.path[0]; this.ctx.beginPath(); this.ctx.moveTo(p0.x,p0.y); item.data.path.slice(1).forEach(p=> this.ctx.lineTo(p.x,p.y)); this.ctx.stroke(); } this.ctx.restore(); } } _clearCanvas() { if (!this.canvas) return; this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height); this._redrawAll(); this.drawingData=[]; this.polyPrev=null; this.previewLine=null; } _saveDrawing() { if (!this.canvas) return; this._redrawAll(); this.canvas.toBlob(blob=>{ const file = new File([blob],'drawing.png',{type:'image/png'}); const dt = new DataTransfer(); dt.items.add(file); if (this.fileInput) this.fileInput.files = dt.files; }); } }