import EventEmitter from "@/utils/emitter"; /** * Canvas绘图类,支持多种图形绘制和多人同步 * 使用百分比坐标系统确保跨设备一致性 */ class Canvas extends EventEmitter { constructor(canvasId) { super(); this.canvas = document.getElementById(canvasId); if (!this.canvas) { throw new Error(`Canvas element with id ${canvasId} not found`); } this.ctx = this.canvas.getContext('2d'); this.shapes = []; // 所有已绘制形状 this.currentShape = null; // 当前正在绘制的形状 this.isDrawing = false; this.drawingTool = 'pencil'; this.pathOptimizationEnabled = true; this.optimizationThreshold = 0.005; this.currentColor = '#ffcc00'; this.currentThickness = 2; this.resize(); // 绑定事件 this.handleMouseDown = this.handleMouseDown.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseUp = this.handleMouseUp.bind(this); this.handleMouseLeave = this.handleMouseLeave.bind(this); this.canvas.addEventListener('mousedown', this.handleMouseDown); this.canvas.addEventListener('mousemove', this.handleMouseMove); this.canvas.addEventListener('mouseup', this.handleMouseUp); this.canvas.addEventListener('mouseleave', this.handleMouseLeave); window.addEventListener('resize', () => this.resize()); } resize() { const parent = this.canvas.parentElement; if (!parent) return; const containerWidth = parent.offsetWidth; const containerHeight = parent.offsetHeight; let width = containerWidth; let height = Math.floor((width * 9) / 16); if (height > containerHeight) { height = containerHeight; width = Math.floor((height * 16) / 9); } if (this.canvas.width === width && this.canvas.height === height) return; this.canvas.width = width; this.canvas.height = height; this.canvas.style.width = width + "px"; this.canvas.style.height = height + "px"; this.render(); } setDrawingTool(tool) { this.drawingTool = tool; } setColor(color) { this.currentColor = color; } setThickness(size) { this.currentThickness = size; } setPathOptimization(enabled) { this.pathOptimizationEnabled = enabled; } setOptimizationThreshold(threshold) { this.optimizationThreshold = threshold; } getShapes() { return this.shapes; } setShapes(shapes) { this.shapes = shapes; this.render(); } getMouseCoordinates(e) { const rect = this.canvas.getBoundingClientRect(); return { x: (e.clientX - rect.left) / this.canvas.width, y: (e.clientY - rect.top) / this.canvas.height }; } handleMouseDown(e) { this.isDrawing = true; const coords = this.getMouseCoordinates(e); if (this.drawingTool === 'pencil') { this.currentShape = { type: 'pencil', data: { color: this.currentColor, path: [coords], thickness: this.currentThickness } }; } else if (this.drawingTool === 'line') { this.currentShape = { type: 'line', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness } }; } else if (this.drawingTool === 'rectangle') { this.currentShape = { type: 'rectangle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } }; } else if (this.drawingTool === 'circle') { this.currentShape = { type: 'circle', data: { color: this.currentColor, start: coords, end: coords, thickness: this.currentThickness, fill: false } }; } else if (this.drawingTool === 'eraser') { this.currentShape = { type: 'eraser', data: { color: '#ffffff', start: coords, end: coords, thickness: 3 } }; } this.emit('drawingStart', this.currentShape); } handleMouseMove(e) { if (!this.isDrawing || !this.currentShape) return; const coords = this.getMouseCoordinates(e); if (this.drawingTool === 'pencil') { this.currentShape.data.path.push(coords); } else { this.currentShape.data.end = coords; } this.render(); this.emit('drawingUpdate', this.currentShape); } handleMouseUp(e) { if (!this.isDrawing || !this.currentShape) return; this.isDrawing = false; const coords = this.getMouseCoordinates(e); if (this.drawingTool === 'pencil' && this.pathOptimizationEnabled && this.currentShape.data.path.length > 10) { this.currentShape.data.path = this.optimizePath(this.currentShape.data.path); } else { this.currentShape.data.end = coords; } this.shapes.push({ ...this.currentShape }); this.emit('drawingEnd', this.currentShape); this.currentShape = null; this.render(); } handleMouseLeave(e) { if (this.isDrawing) this.handleMouseUp(e); } optimizePath(path) { if (path.length < 3) return path; const optimizedPath = [path[0]]; for (let i = 1; i < path.length - 1;) { let a = 1; while (i + a < path.length && this.calculateDistance(path[i], path[i + a]) < this.optimizationThreshold) a++; optimizedPath.push(path[i]); i += a; } optimizedPath.push(path[path.length - 1]); return optimizedPath; } calculateDistance(p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); } addShape(shapeData) { if (Array.isArray(shapeData)) this.shapes.push(...shapeData); else this.shapes.push(shapeData); this.render(); } clearCanvas() { this.shapes = []; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.emit('clear'); } exportToDataURL(type = 'image/png', quality = 1) { return this.canvas.toDataURL(type, quality); } render() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 绘制历史形状 this.shapes.forEach(shape => this.drawShape(shape)); // 绘制当前正在绘制的形状 if (this.currentShape) this.drawShape(this.currentShape); // 画画布边框 this.ctx.save(); this.ctx.strokeStyle = "#000"; this.ctx.lineWidth = 2; this.ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.restore(); } drawShape(shape) { switch (shape.type) { case 'pencil': this.drawPencil(shape.data); break; case 'line': this.drawLine(shape.data); break; case 'rectangle': this.drawRectangle(shape.data); break; case 'circle': this.drawCircle(shape.data); break; case 'eraser': this.drawEraser(shape.data, shape); break; } } drawPencil(p) { if (p.path.length < 2) return; const path = p.path.map(pt => ({ x: pt.x * this.canvas.width, y: pt.y * this.canvas.height })); this.ctx.beginPath(); this.ctx.strokeStyle = p.color; this.ctx.lineWidth = p.thickness; this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round'; this.drawSmoothCurve(this.ctx, path); this.ctx.stroke(); } drawSmoothCurve(ctx, path) { if (path.length < 3) { ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) ctx.lineTo(path[i].x, path[i].y); return; } ctx.moveTo(path[0].x, path[0].y); const threshold = 5; for (let i = 1; i < path.length - 2;) { let a = 1; while (i + a < path.length - 2 && Math.sqrt(Math.pow(path[i].x - path[i + a].x, 2) + Math.pow(path[i].y - path[i + a].y, 2)) < threshold) a++; const xc = (path[i].x + path[i + a].x) / 2; const yc = (path[i].y + path[i + a].y) / 2; ctx.quadraticCurveTo(path[i].x, path[i].y, xc, yc); i += a; } ctx.lineTo(path[path.length - 1].x, path[path.length - 1].y); } drawLine(l) { const sx = l.start.x * this.canvas.width, sy = l.start.y * this.canvas.height; const ex = l.end.x * this.canvas.width, ey = l.end.y * this.canvas.height; this.ctx.beginPath(); this.ctx.moveTo(sx, sy); this.ctx.lineTo(ex, ey); this.ctx.strokeStyle = l.color; this.ctx.lineWidth = l.thickness; this.ctx.stroke(); } drawRectangle(r) { const sx = r.start.x * this.canvas.width, sy = r.start.y * this.canvas.height; const ex = r.end.x * this.canvas.width, ey = r.end.y * this.canvas.height; const w = ex - sx, h = ey - sy; this.ctx.beginPath(); this.ctx.rect(sx, sy, w, h); if (r.fill) { this.ctx.fillStyle = r.color; this.ctx.fill(); } else { this.ctx.strokeStyle = r.color; this.ctx.lineWidth = r.thickness; this.ctx.stroke(); } } drawCircle(c) { const sx = c.start.x * this.canvas.width, sy = c.start.y * this.canvas.height; const ex = c.end.x * this.canvas.width, ey = c.end.y * this.canvas.height; const cx = (sx + ex) / 2, cy = (sy + ey) / 2; const rx = Math.abs(ex - sx) / 2, ry = Math.abs(ey - sy) / 2; this.ctx.beginPath(); this.ctx.ellipse(cx, cy, rx, ry, 0, 0, 2 * Math.PI); if (c.fill) { this.ctx.fillStyle = c.color; this.ctx.fill(); } else { this.ctx.strokeStyle = c.color; this.ctx.lineWidth = c.thickness; this.ctx.stroke(); } } drawEraser(e, shapeObj) { const sx = e.start.x * this.canvas.width, sy = e.start.y * this.canvas.height; const ex = e.end.x * this.canvas.width, ey = e.end.y * this.canvas.height; const w = Math.abs(ex - sx), h = Math.abs(ey - sy); const x = Math.min(sx, ex), y = Math.min(sy, ey); this.ctx.beginPath(); this.ctx.rect(x, y, w, h); this.ctx.fillStyle = 'rgba(255,255,255)'; this.ctx.fill(); // 仅当前正在绘制的橡皮擦显示边框 if (this.currentShape && shapeObj === this.currentShape) { this.ctx.save(); this.ctx.strokeStyle = '#000'; this.ctx.lineWidth = 1; this.ctx.strokeRect(x, y, w, h); this.ctx.restore(); } } destroy() { this.canvas.removeEventListener('mousedown', this.handleMouseDown); this.canvas.removeEventListener('mousemove', this.handleMouseMove); this.canvas.removeEventListener('mouseup', this.handleMouseUp); this.canvas.removeEventListener('mouseleave', this.handleMouseLeave); window.removeEventListener('resize', this.resize); } } export default Canvas;