Files
FiDA_Front/src/components/Canvas/DepthCanvas/manager/BrushIndicator.js

523 lines
12 KiB
JavaScript
Raw Normal View History

2026-03-11 15:34:56 +08:00
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);
2026-03-16 11:38:58 +08:00
this._observeCanvasChanges();
2026-03-11 15:34:56 +08:00
}
/**
* 解绑事件处理器
* @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;
}
}