项目初始化
This commit is contained in:
285
src/core/index.js
Normal file
285
src/core/index.js
Normal file
@@ -0,0 +1,285 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user