import { fabric } from "fabric-with-all"; /** * 笔刷指示器 * 在画笔模式下显示当前笔刷大小的圆圈指示器 */ export class BrushIndicator { /** * 构造函数 * @param {Object} canvas fabric.js画布实例 * @param {Object} options 配置选项 */ constructor(canvas, options = {}) { this.canvas = canvas; this.options = { strokeColor: options.strokeColor || "rgba(0, 0, 0, 0.5)", strokeWidth: options.strokeWidth || 1, fillColor: options.fillColor || "rgba(0, 0, 0, 0.1)", ...options, }; // 指示器圆圈对象 this.indicator = null; // 事件处理器 this._mouseEnterHandler = null; this._mouseLeaveHandler = null; this._mouseMoveHandler = null; // 当前状态 this.isVisible = false; this.currentSize = 10; this.isEnabled = false; } /** * 启用笔刷指示器 * @param {Number} brushSize 笔刷大小 */ enable(brushSize) { if (this.isEnabled) return; this.isEnabled = true; this.currentSize = brushSize; // 绑定事件 this._bindEvents(); } /** * 禁用笔刷指示器 */ disable() { if (!this.isEnabled) return; this.isEnabled = false; // 隐藏指示器 this.hide(); // 重置颜色配置为默认值 this.options.strokeColor = ""; this.options.fillColor = ""; // 解绑事件 this._unbindEvents(); } /** * 更新笔刷大小 * @param {Number} size 新的笔刷大小 */ updateSize(size) { this.currentSize = size; // 如果指示器正在显示,更新其大小 if (this.isVisible && this.indicator) { this._updateIndicatorSize(); } } /** * 更新指示器颜色 * @param {String} color 新的颜色值 */ updateColor(color) { // 更新配置选项中的颜色 if (color) { this.options.strokeColor = color; // 将颜色转换为半透明填充色 // this.options.fillColor = this._convertToTransparentFill(color); } // 如果指示器正在显示,更新其颜色 if (this.isVisible && this.indicator) { this.indicator.set({ stroke: this.options.strokeColor, fill: this.options.fillColor, }); // 重新渲染画布 this.canvas.requestRenderAll(); } } /** * 显示指示器 * @param {Object} pointer 鼠标位置 */ show(pointer) { if (!this.isEnabled || this.isVisible) return; this.isVisible = true; // 创建指示器圆圈 this._createIndicator(pointer); } /** * 隐藏指示器 */ hide() { if (!this.isVisible || !this.indicator) return; this.isVisible = false; // 从画布移除指示器 this.canvas.remove(this.indicator); this.indicator = null; // 重新渲染画布 this.canvas.renderAll(); } /** * 更新指示器位置 * @param {Object} pointer 鼠标位置 */ updatePosition(pointer) { if (!this.isVisible || !this.indicator) return; // 转换坐标考虑画布变换 const canvasPointer = this.canvas.getPointer(pointer); // 批量更新位置属性 this.indicator.set({ left: canvasPointer.x, top: canvasPointer.y, }); // 更新坐标系 this.indicator.setCoords(); // 使用requestRenderAll优化渲染性能 this.canvas.requestRenderAll(); } /** * 绑定事件处理器 * @private */ _bindEvents() { if (!this.canvas) return; // 鼠标进入画布 this._mouseEnterHandler = (e) => { // 只在画笔相关模式下显示 if (this._shouldShowIndicator()) { this.show(e.e); } }; // 鼠标离开画布 this._mouseLeaveHandler = () => { this.hide(); }; // 鼠标在画布上移动 - 修改事件处理逻辑 this._mouseMoveHandler = (e) => { // 如果正在绘图,不处理指示器更新,避免干扰绘图 if (this.canvas._isCurrentlyDrawing) { return; } if (this._shouldShowIndicator()) { if (!this.isVisible) { this.show(e.e); } else { this.updatePosition(e.e); } } else { this.hide(); } }; // 监听绘图开始事件,隐藏指示器并模拟微小移动 this._drawingStartHandler = (e) => { if (this.isVisible) { this.hide(); } // 模拟1px的微小移动来确保笔刷能正常启动绘画 this._simulateTinyMovement(e); }; // 监听绘图结束事件,重新显示指示器 this._drawingEndHandler = () => { // 延迟一点重新显示,确保绘图完全结束 setTimeout(() => { if (this._shouldShowIndicator() && !this.isVisible) { // 重新检查鼠标位置并显示指示器 this._checkAndShowIndicator(); } }, 50); }; // 画布缩放变化处理器 this._zoomHandler = () => { if (this.isVisible && this.indicator) { // 立即更新指示器大小 this._updateIndicatorSize(); } }; // 画布视口变化处理器 this._viewportHandler = () => { if (this.isVisible && this.indicator) { // 视口变化时也需要更新指示器 this._updateIndicatorSize(); } }; // 绑定事件 this.canvas.on("mouse:over", this._mouseEnterHandler); this.canvas.on("mouse:out", this._mouseLeaveHandler); this.canvas.on("mouse:move", this._mouseMoveHandler); // 监听绘图状态变化 this.canvas.on("path:created", this._drawingEndHandler); this.canvas.on("mouse:down", this._drawingStartHandler); this.canvas.on("mouse:up", this._drawingEndHandler); // 监听画布缩放和视口变化 this.canvas.on("after:render", this._zoomHandler); this.canvas.on("canvas:zoomed", this._zoomHandler); this.canvas.on("viewport:changed", this._viewportHandler); } /** * 解绑事件处理器 * @private */ _unbindEvents() { if (!this.canvas) return; if (this._mouseEnterHandler) { this.canvas.off("mouse:over", this._mouseEnterHandler); this._mouseEnterHandler = null; } if (this._mouseLeaveHandler) { this.canvas.off("mouse:out", this._mouseLeaveHandler); this._mouseLeaveHandler = null; } if (this._mouseMoveHandler) { this.canvas.off("mouse:move", this._mouseMoveHandler); this._mouseMoveHandler = null; } // 清理绘图相关事件 if (this._drawingStartHandler) { this.canvas.off("mouse:down", this._drawingStartHandler); this.canvas.off("path:created", this._drawingEndHandler); this.canvas.off("mouse:up", this._drawingEndHandler); this._drawingStartHandler = null; this._drawingEndHandler = null; } // 清理缩放相关事件 if (this._zoomHandler) { this.canvas.off("after:render", this._zoomHandler); this.canvas.off("canvas:zoomed", this._zoomHandler); this._zoomHandler = null; } if (this._viewportHandler) { this.canvas.off("viewport:changed", this._viewportHandler); this._viewportHandler = null; } } /** * 创建指示器圆圈 * @private * @param {Object} pointer 鼠标位置 */ _createIndicator(pointer) { // 转换坐标考虑画布变换 const canvasPointer = this.canvas.getPointer(pointer); // 计算考虑画布缩放的半径 const radius = this._getIndicatorRadius(); // 创建圆圈 this.indicator = new fabric.Circle({ left: canvasPointer.x, top: canvasPointer.y, radius: radius, fill: this.options.fillColor, stroke: this.options.strokeColor, strokeWidth: this.options.strokeWidth / this.canvas.getZoom(), // 线宽不受缩放影响 originX: "center", originY: "center", selectable: false, evented: false, excludeFromExport: true, // 导出时排除 isTemp: true, // 标记为临时对象 pointer: true, // 标记为鼠标指示器 }); // 添加到画布 this.canvas.add(this.indicator); // 确保指示器在最顶层 this.canvas.bringToFront(this.indicator); // 重新渲染画布 this.canvas.renderAll(); } /** * 更新指示器大小 * @private */ _updateIndicatorSize() { if (!this.indicator) return; // 获取当前画布缩放比例 const zoom = this.canvas.getZoom(); const radius = this._getIndicatorRadius(); const strokeWidth = this.options.strokeWidth / zoom; // 批量更新属性,减少重绘次数 this.indicator.set({ radius: radius, strokeWidth: strokeWidth, }); // 标记指示器需要重绘 this.indicator.setCoords(); // 重新渲染画布 this.canvas.requestRenderAll(); } /** * 获取指示器半径(与笔刷实际绘制大小一致) * @private * @returns {Number} 半径值 */ _getIndicatorRadius() { // 指示器大小应该与笔刷实际绘制大小一致 // 笔刷的实际绘制大小不受画布缩放影响,所以指示器也不应该受缩放影响 return this.currentSize / 2; } /** * 判断是否应该显示指示器 * @private * @returns {Boolean} 是否显示 */ _shouldShowIndicator() { // 检查画布是否在绘图模式 if (!this.canvas.isDrawingMode) return false; // 检查是否有笔刷 if (!this.canvas.freeDrawingBrush) return false; return true; } /** * 检查并显示指示器(如果鼠标在画布内) * @private */ _checkAndShowIndicator() { // 这个方法用于在绘图结束后重新显示指示器 // 由于无法直接获取当前鼠标位置,我们依赖下一次鼠标移动事件 } /** * 模拟微小移动 * @private * @param {Object} e 鼠标事件对象 */ _simulateTinyMovement(e) { // 获取当前鼠标位置 const pointer = this.canvas.getPointer(e.e); // 模拟移动1px this.canvas.fire("mouse:move", { ...e, e: { ...e.e, clientX: pointer.x + 1, clientY: pointer.y + 1, pointer: { x: pointer.x + 1, y: pointer.y + 1, }, }, }); // 再模拟移动回原位置 this.canvas.fire("mouse:move", { ...e, e: { ...e.e, clientX: pointer.x, clientY: pointer.y, pointer: { x: pointer.x, y: pointer.y, }, }, }); } /** * 销毁指示器 */ dispose() { this.disable(); this.canvas = null; this.indicator = null; this.options = null; } /** * 将颜色转换为半透明填充色 * @private * @param {String} color 原始颜色值 * @returns {String} 半透明填充色 */ _convertToTransparentFill(color) { // 如果已经是 rgba 格式,直接使用但降低透明度 if (color.startsWith("rgba")) { return color.replace(/[\d\.]+\)$/, "0.1)"); } // 如果是 rgb 格式,转换为 rgba if (color.startsWith("rgb")) { return color.replace("rgb", "rgba").replace(")", ", 0.1)"); } // 如果是十六进制格式,转换为 rgba if (color.startsWith("#")) { const hex = color.slice(1); let r, g, b; if (hex.length === 3) { // 处理 #RGB 格式 r = parseInt(hex[0] + hex[0], 16); g = parseInt(hex[1] + hex[1], 16); b = parseInt(hex[2] + hex[2], 16); } else if (hex.length === 6) { // 处理 #RRGGBB 格式 r = parseInt(hex.slice(0, 2), 16); g = parseInt(hex.slice(2, 4), 16); b = parseInt(hex.slice(4, 6), 16); } else { // 无法解析的格式,返回默认半透明黑色 return "rgba(0, 0, 0, 0.1)"; } return `rgba(${r}, ${g}, ${b}, 0.1)`; } // 处理命名颜色或其他格式,使用默认半透明效果 return "rgba(0, 0, 0, 0.1)"; } }