435 lines
10 KiB
JavaScript
435 lines
10 KiB
JavaScript
import { fabric } from "fabric-with-all";
|
||
|
||
/**
|
||
* 笔刷指示器
|
||
* 在画笔模式下显示当前笔刷大小的圆圈指示器
|
||
* 使用独立的 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._createStaticCanvas();
|
||
|
||
// 事件处理器
|
||
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 };
|
||
}
|
||
|
||
/**
|
||
* 创建独立的 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;
|
||
|
||
// 同步画布尺寸
|
||
this.staticCanvas.setWidth(this.canvas.width);
|
||
this.staticCanvas.setHeight(this.canvas.height);
|
||
|
||
// 同步缩放
|
||
this.staticCanvas.setZoom(this.canvas.getZoom());
|
||
|
||
// 同步视口变换
|
||
const vpt = this.canvas.viewportTransform;
|
||
if (vpt) {
|
||
this.staticCanvas.setViewportTransform([...vpt]);
|
||
}
|
||
|
||
// 同步背景色(可选,通常保持透明)
|
||
// this.staticCanvas.backgroundColor = 'transparent';
|
||
|
||
// 重新渲染
|
||
this.staticCanvas.renderAll();
|
||
}
|
||
|
||
/**
|
||
* 监听主画布变化
|
||
* @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;
|
||
|
||
// 使用主画布的坐标转换
|
||
const canvasPointer = this.canvas.getPointer(pointer);
|
||
|
||
// 更新当前位置
|
||
this.currentPosition = {
|
||
x: canvasPointer.x,
|
||
y: canvasPointer.y,
|
||
};
|
||
|
||
// 更新指示器位置
|
||
this.indicatorCircle.set({
|
||
left: canvasPointer.x,
|
||
top: canvasPointer.y,
|
||
});
|
||
|
||
// 重新渲染静态画布
|
||
// this.staticCanvas.renderAll();
|
||
// 优化渲染
|
||
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);
|
||
}
|
||
};
|
||
|
||
// 鼠标离开画布
|
||
this._mouseLeaveHandler = () => {
|
||
this.hide();
|
||
};
|
||
|
||
// 鼠标移动
|
||
this._mouseMoveHandler = (e) => {
|
||
if (this._shouldShowIndicator()) {
|
||
if (!this.isVisible) {
|
||
this.show(e.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);
|
||
}
|
||
|
||
/**
|
||
* 解绑事件处理器
|
||
* @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() {
|
||
// 检查画布是否在绘图模式
|
||
if (!this.canvas.isDrawingMode) 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;
|
||
}
|
||
}
|