Files
sam-kd/js/drawLib.js

273 lines
9.5 KiB
JavaScript
Raw Normal View History

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; // <input type="file"> 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;
});
}
}