import { fabric } from "fabric-with-all"; import { OperationType } from "../tools/layerHelper"; /** * 笔刷指示器 * 在画笔模式下显示当前笔刷大小的圆圈指示器 * 使用独立的 fabric.StaticCanvas,完全不干扰主画布的绘制流程 */ 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.indicatorCanvas = null; this.staticCanvas = null; this.indicatorCircle = null; // 事件处理器 this._mouseEnterHandler = null; this._mouseLeaveHandler = null; this._mouseMoveHandler = null; this._zoomHandler = null; this._viewportHandler = null; // 当前状态 this.isVisible = false; this.currentSize = 10; this.isEnabled = false; this.currentPosition = { x: 0, y: 0 }; // 缓存画布状态,用于优化性能 this._lastCanvasState = { width: null, height: null, zoom: null, viewportTransform: null, }; this._createStaticCanvas(); } /** * 创建独立的 fabric.StaticCanvas 画布层 * @private */ _createStaticCanvas() { if (!this.canvas.wrapperEl) return; // 创建独立的 canvas 元素 this.indicatorCanvas = document.createElement("canvas"); this.indicatorCanvas.className = "brush-indicator-canvas"; // 设置样式,使其覆盖在主画布上 Object.assign(this.indicatorCanvas.style, { position: "absolute", top: "0", left: "0", pointerEvents: "none", // 不阻挡鼠标事件 zIndex: "10", // 确保在最上层 width: "100%", height: "100%", }); // 将指示器画布添加到 fabric.js 画布容器中 this.canvas.wrapperEl.appendChild(this.indicatorCanvas); // 创建 fabric.StaticCanvas 实例 this.staticCanvas = new fabric.StaticCanvas(this.indicatorCanvas, { enableRetinaScaling: this.canvas.enableRetinaScaling, allowTouchScrolling: false, selection: false, skipTargetFind: true, preserveObjectStacking: true, }); // 同步画布尺寸和变换 this._syncCanvasProperties(); // 监听主画布变化 this._observeCanvasChanges(); } /** * 同步画布属性(尺寸、缩放、视口变换等) * @private */ _syncCanvasProperties() { if (!this.staticCanvas || !this.canvas) return; // 检查是否为笔刷或橡皮擦模式,非相关模式直接返回 const isBrushMode = this.canvas.isDrawingMode && this.canvas.freeDrawingBrush; const isEraserMode = this.canvas.isDrawingMode && this.canvas.freeDrawingBrush && this.canvas.freeDrawingBrush.type === "eraser"; const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式 if ([isBrushMode, isEraserMode, isLiquifyMode].every(v => !v)) return; let hasChanges = false; // 检查画布尺寸是否变化 const currentWidth = this.canvas.width; const currentHeight = this.canvas.height; if ( currentWidth !== this._lastCanvasState.width || currentHeight !== this._lastCanvasState.height ) { this.staticCanvas.setWidth(currentWidth); this.staticCanvas.setHeight(currentHeight); this._lastCanvasState.width = currentWidth; this._lastCanvasState.height = currentHeight; hasChanges = true; } // 检查缩放比例是否变化 const currentZoom = this.canvas.getZoom(); if (Math.abs(currentZoom - (this._lastCanvasState.zoom || 0)) > 0.001) { this.staticCanvas.setZoom(currentZoom); this._lastCanvasState.zoom = currentZoom; hasChanges = true; } // 检查视口变换是否变化 const currentVpt = this.canvas.viewportTransform; if ( currentVpt && !this._areViewportTransformsEqual( currentVpt, this._lastCanvasState.viewportTransform ) ) { this.staticCanvas.setViewportTransform([...currentVpt]); this._lastCanvasState.viewportTransform = [...currentVpt]; hasChanges = true; } // 只有在有变化时才重新渲染 if (hasChanges) { this.staticCanvas.renderAll(); } } /** * 比较两个视口变换矩阵是否相等 * @private * @param {Array} vpt1 视口变换矩阵1 * @param {Array} vpt2 视口变换矩阵2 * @returns {Boolean} 是否相等 */ _areViewportTransformsEqual(vpt1, vpt2) { if (!vpt1 || !vpt2) return false; if (vpt1.length !== vpt2.length) return false; const tolerance = 0.001; for (let i = 0; i < vpt1.length; i++) { if (Math.abs(vpt1[i] - vpt2[i]) > tolerance) { return false; } } return true; } /** * 监听主画布变化 * @private */ _observeCanvasChanges() { if (!this.canvas) return; // 监听缩放变化 this._zoomHandler = () => { this._syncCanvasProperties(); }; // 监听视口变化 this._viewportHandler = () => { this._syncCanvasProperties(); }; // 监听画布尺寸变化 this._resizeHandler = () => { this._syncCanvasProperties(); }; // 绑定事件 this.canvas.on("after:render", this._zoomHandler); this.canvas.on("canvas:zoomed", this._zoomHandler); this.canvas.on("viewport:changed", this._viewportHandler); this.canvas.on("canvas:resized", this._resizeHandler); // 使用 ResizeObserver 监听 DOM 尺寸变化 if (window.ResizeObserver && this.canvas.upperCanvasEl) { this.resizeObserver = new ResizeObserver(() => { this._syncCanvasProperties(); }); this.resizeObserver.observe(this.canvas.upperCanvasEl); } } /** * 启用笔刷指示器 * @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.indicatorCircle) { this.indicatorCircle.set({ radius: size / 2, }); this.staticCanvas.renderAll(); } } /** * 更新指示器颜色 * @param {String} color 新的颜色值 */ updateColor(color) { // 更新配置选项中的颜色 if (color) { this.options.strokeColor = color; } // 如果指示器正在显示,更新其颜色 if (this.isVisible && this.indicatorCircle) { this.indicatorCircle.set({ stroke: this.options.strokeColor, fill: this.options.fillColor, }); this.staticCanvas.renderAll(); } } /** * 显示指示器 * @param {Object} pointer 鼠标位置 */ show(pointer) { if (!this.isEnabled || this.isVisible) return; this.isVisible = true; // 创建指示器圆圈 this._createIndicatorCircle(); // 更新位置 this.updatePosition(pointer); } /** * 隐藏指示器 */ hide() { if (!this.isVisible) return; this.isVisible = false; // 移除指示器圆圈 if (this.indicatorCircle && this.staticCanvas) { this.staticCanvas.remove(this.indicatorCircle); this.indicatorCircle = null; this.staticCanvas.renderAll(); } } /** * 更新指示器位置 * @param {Object} pointer 鼠标位置 */ updatePosition(pointer) { if (!this.isVisible || !this.indicatorCircle) return; let canvasPointer; // 如果是原生事件(如 e.e),优先用 clientX/clientY 转换 if ( pointer && pointer.clientX !== undefined && pointer.clientY !== undefined ) { // 获取主画布的包裹元素位置 const rect = this.canvas.upperCanvasEl.getBoundingClientRect(); const x = pointer.clientX - rect.left; const y = pointer.clientY - rect.top; // 逆变换到画布坐标 canvasPointer = fabric.util.transformPoint( new fabric.Point(x, y), fabric.util.invertTransform(this.canvas.viewportTransform) ); } else { // 兼容 fabric 的 getPointer canvasPointer = this.canvas.getPointer(pointer); } // 更新当前位置 this.currentPosition = { x: canvasPointer.x, y: canvasPointer.y, }; // 更新指示器位置 this.indicatorCircle.set({ left: canvasPointer.x, top: canvasPointer.y, }); // 优化渲染 this.staticCanvas.requestRenderAll(); } /** * 创建指示器圆圈对象 * @private */ _createIndicatorCircle() { if (this.indicatorCircle || !this.staticCanvas) return; // 创建圆圈对象 this.indicatorCircle = new fabric.Circle({ radius: this.currentSize / 2, fill: this.options.fillColor, stroke: this.options.strokeColor, strokeWidth: this.options.strokeWidth, originX: "center", originY: "center", selectable: false, evented: false, excludeFromExport: true, isTemp: true, pointer: true, }); // 添加到静态画布 this.staticCanvas.add(this.indicatorCircle); } /** * 绑定事件处理器 * @private */ _bindEvents() { if (!this.canvas) return; // 鼠标进入画布 this._mouseEnterHandler = (e) => { if (this._shouldShowIndicator()) { this.show(e.e); const currentVpt = this.canvas.viewportTransform; this.staticCanvas.setViewportTransform([...currentVpt]); this.staticCanvas.renderAll(); } }; // 鼠标离开画布 this._mouseLeaveHandler = () => { this.hide(); }; // 鼠标移动 this._mouseMoveHandler = (e) => { if (this._shouldShowIndicator()) { if (!this.isVisible) { // this.show(e.e); this._mouseEnterHandler && this._mouseEnterHandler(e) } else { // requestIdleCallback(() => { // this.updatePosition(e.e); // }); requestAnimationFrame(() => { this.updatePosition(e.e); }); } } else { this.hide(); } }; // 绑定事件 this.canvas.on("mouse:over", this._mouseEnterHandler); this.canvas.on("mouse:out", this._mouseLeaveHandler); this.canvas.on("mouse:move", this._mouseMoveHandler); this._observeCanvasChanges(); } /** * 解绑事件处理器 * @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._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; } if (this._resizeHandler) { this.canvas.off("canvas:resized", this._resizeHandler); this._resizeHandler = null; } } /** * 判断是否应该显示指示器 * @private * @returns {Boolean} 是否显示 */ _shouldShowIndicator() { const isDrawingMode = this.canvas.isDrawingMode;// 检查画布是否在绘图模式 const isLiquifyMode = this.canvas.toolId === OperationType.LIQUIFY;// 检查是否在液化模式 // console.log(`笔刷指示器\n绘图模式:${isDrawingMode}\n液化模式:${isLiquifyMode}`) // 检查画布是否在绘图模式OR液化模式 if ([isDrawingMode, isLiquifyMode].every(v => !v)) return false; // 检查是否有笔刷 if (!this.canvas.freeDrawingBrush) return false; return true; } /** * 销毁指示器 */ dispose() { this.disable(); // 解绑画布变化事件 this._unbindEvents(); // 停止监听尺寸变化 if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } // 销毁静态画布 if (this.staticCanvas) { this.staticCanvas.dispose(); this.staticCanvas = null; } // 移除指示器画布 if (this.indicatorCanvas && this.indicatorCanvas.parentNode) { this.indicatorCanvas.parentNode.removeChild(this.indicatorCanvas); } this.canvas = null; this.indicatorCanvas = null; this.indicatorCircle = null; this.options = null; } }