feat(CanvasManager): enhance image layer management and event handling

This commit is contained in:
bighuixiang
2025-06-26 00:37:07 +08:00
parent afa3b69f71
commit 2fcba962d1
16 changed files with 901 additions and 448 deletions

View File

@@ -3,6 +3,7 @@ import { fabric } from "fabric-with-all";
/**
* 笔刷指示器
* 在画笔模式下显示当前笔刷大小的圆圈指示器
* 使用独立的 fabric.StaticCanvas完全不干扰主画布的绘制流程
*/
export class BrushIndicator {
/**
@@ -19,18 +20,129 @@ export class BrushIndicator {
...options,
};
// 指示器圆圈对象
this.indicator = null;
// 创建独立的静态画布
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);
}
}
/**
@@ -74,8 +186,11 @@ export class BrushIndicator {
this.currentSize = size;
// 如果指示器正在显示,更新其大小
if (this.isVisible && this.indicator) {
this._updateIndicatorSize();
if (this.isVisible && this.indicatorCircle) {
this.indicatorCircle.set({
radius: size / 2,
});
this.staticCanvas.renderAll();
}
}
@@ -87,19 +202,15 @@ export class BrushIndicator {
// 更新配置选项中的颜色
if (color) {
this.options.strokeColor = color;
// 将颜色转换为半透明填充色
// this.options.fillColor = this._convertToTransparentFill(color);
}
// 如果指示器正在显示,更新其颜色
if (this.isVisible && this.indicator) {
this.indicator.set({
if (this.isVisible && this.indicatorCircle) {
this.indicatorCircle.set({
stroke: this.options.strokeColor,
fill: this.options.fillColor,
});
// 重新渲染画布
this.canvas.requestRenderAll();
this.staticCanvas.renderAll();
}
}
@@ -113,23 +224,26 @@ export class BrushIndicator {
this.isVisible = true;
// 创建指示器圆圈
this._createIndicator(pointer);
this._createIndicatorCircle();
// 更新位置
this.updatePosition(pointer);
}
/**
* 隐藏指示器
*/
hide() {
if (!this.isVisible || !this.indicator) return;
if (!this.isVisible) return;
this.isVisible = false;
// 从画布移除指示器
this.canvas.remove(this.indicator);
this.indicator = null;
// 重新渲染画布
this.canvas.renderAll();
// 移除指示器圆圈
if (this.indicatorCircle && this.staticCanvas) {
this.staticCanvas.remove(this.indicatorCircle);
this.indicatorCircle = null;
this.staticCanvas.renderAll();
}
}
/**
@@ -137,22 +251,53 @@ export class BrushIndicator {
* @param {Object} pointer 鼠标位置
*/
updatePosition(pointer) {
if (!this.isVisible || !this.indicator) return;
if (!this.isVisible || !this.indicatorCircle) return;
// 转换坐标考虑画布变
// 使用主画布的坐标转
const canvasPointer = this.canvas.getPointer(pointer);
// 批量更新位置属性
this.indicator.set({
// 更新当前位置
this.currentPosition = {
x: canvasPointer.x,
y: canvasPointer.y,
};
// 更新指示器位置
this.indicatorCircle.set({
left: canvasPointer.x,
top: canvasPointer.y,
});
// 更新坐标系
this.indicator.setCoords();
// 重新渲染静态画布
// this.staticCanvas.renderAll();
// 优化渲染
this.staticCanvas.requestRenderAll();
}
// 使用requestRenderAll优化渲染性能
this.canvas.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);
}
/**
@@ -164,7 +309,6 @@ export class BrushIndicator {
// 鼠标进入画布
this._mouseEnterHandler = (e) => {
// 只在画笔相关模式下显示
if (this._shouldShowIndicator()) {
this.show(e.e);
}
@@ -175,75 +319,29 @@ export class BrushIndicator {
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);
// requestIdleCallback(() => {
// this.updatePosition(e.e);
// });
requestAnimationFrame(() => {
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);
}
/**
@@ -253,6 +351,7 @@ export class BrushIndicator {
_unbindEvents() {
if (!this.canvas) return;
// 解绑鼠标事件
if (this._mouseEnterHandler) {
this.canvas.off("mouse:over", this._mouseEnterHandler);
this._mouseEnterHandler = null;
@@ -268,16 +367,7 @@ export class BrushIndicator {
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);
@@ -288,81 +378,11 @@ export class BrushIndicator {
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;
if (this._resizeHandler) {
this.canvas.off("canvas:resized", this._resizeHandler);
this._resizeHandler = null;
}
}
/**
@@ -380,104 +400,35 @@ export class BrushIndicator {
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._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.indicator = null;
this.indicatorCanvas = null;
this.indicatorCircle = 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)";
}
}