合并画布代码
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import initAligningGuidelines, {
|
||||
initCenteringGuidelines,
|
||||
} from "../utils/helperLine";
|
||||
@@ -14,10 +14,15 @@ import { createCanvas } from "../utils/canvasFactory";
|
||||
import { CanvasEventManager } from "./events/CanvasEventManager";
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
import { RedGreenModeManager } from "./RedGreenModeManager";
|
||||
import { EraserStateManager } from "./EraserStateManager";
|
||||
import { deepClone, optimizeCanvasRendering } from "../utils/helper";
|
||||
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
|
||||
import { isFunction } from "lodash-es";
|
||||
import {
|
||||
ChangeFixedImageCommand,
|
||||
AddImageToLayerCommand,
|
||||
} from "../commands/ObjectLayerCommands";
|
||||
restoreObjectLayerAssociations,
|
||||
simplifyLayers,
|
||||
validateLayerAssociations,
|
||||
} from "../utils/layerUtils";
|
||||
|
||||
export class CanvasManager {
|
||||
constructor(canvasElement, options) {
|
||||
@@ -34,6 +39,7 @@ export class CanvasManager {
|
||||
this.canvasHeight = options.canvasHeight || this.height; // 画布高度
|
||||
this.canvasColor = options.canvasColor || "#ffffff"; // 画布背景颜色
|
||||
this.enabledRedGreenMode = options.enabledRedGreenMode || false; // 是否启用红绿图模式
|
||||
this.eraserStateManager = null; // 橡皮擦状态管理器引用
|
||||
// 初始化画布
|
||||
this.initializeCanvas();
|
||||
}
|
||||
@@ -65,14 +71,6 @@ export class CanvasManager {
|
||||
layers: this.layers,
|
||||
});
|
||||
|
||||
// 初始化红绿图模式管理器
|
||||
this.redGreenModeManager = new RedGreenModeManager({
|
||||
canvas: this.canvas,
|
||||
layerManager: null, // 稍后设置
|
||||
toolManager: null, // 稍后设置
|
||||
commandManager: null, // 稍后设置
|
||||
});
|
||||
|
||||
// 设置画布辅助线
|
||||
initAligningGuidelines(this.canvas);
|
||||
|
||||
@@ -90,7 +88,7 @@ export class CanvasManager {
|
||||
*/
|
||||
_initCanvasEvents() {
|
||||
// 添加笔刷图像转换处理回调
|
||||
this.canvas.onBrushImageConverted = (fabricImage) => {
|
||||
this.canvas.onBrushImageConverted = async (fabricImage) => {
|
||||
// 如果图层管理器存在,将图像合并到当前活动图层
|
||||
if (this.layerManager) {
|
||||
// 获取当前活动图层
|
||||
@@ -107,21 +105,49 @@ export class CanvasManager {
|
||||
});
|
||||
|
||||
// 执行高保真合并操作
|
||||
this.eventManager?.mergeLayerObjectsForPerformance?.({
|
||||
await this.eventManager?.mergeLayerObjectsForPerformance?.({
|
||||
fabricImage,
|
||||
activeLayer,
|
||||
});
|
||||
|
||||
// 返回false表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
||||
return false;
|
||||
// 返回true表示不要自动添加到画布,因为我们已经通过图层管理器处理了
|
||||
return true;
|
||||
} else {
|
||||
console.warn("没有活动图层,使用默认行为添加图像");
|
||||
}
|
||||
}
|
||||
|
||||
// 返回true表示使用默认行为(直接添加到画布)
|
||||
return true;
|
||||
// 返回false表示使用默认行为(直接添加到画布)
|
||||
return false;
|
||||
};
|
||||
|
||||
this.eraserStateManager = new EraserStateManager(
|
||||
this.canvas,
|
||||
this.layerManager
|
||||
);
|
||||
|
||||
// 监听擦除开始事件
|
||||
this.canvas.on("erasing:start", () => {
|
||||
console.log("开始擦除");
|
||||
this.eraserStateManager.startErasing();
|
||||
});
|
||||
|
||||
// 监听擦除结束事件
|
||||
this.canvas.on("erasing:end", async (e) => {
|
||||
console.log("擦除完成", e.targets);
|
||||
// 可以在这里保存状态到命令管理器
|
||||
const affectedObjects = e.targets || [];
|
||||
const command = this.eraserStateManager.endErasing(affectedObjects);
|
||||
if (command && this.commandManager) {
|
||||
await this.commandManager?.executeCommand?.(command);
|
||||
} else {
|
||||
await command?.execute?.(); // 如果没有命令管理器,直接执行命令
|
||||
}
|
||||
|
||||
// 更新交互性
|
||||
command &&
|
||||
(await this.layerManager?.updateLayersObjectsInteractivity?.());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +194,10 @@ export class CanvasManager {
|
||||
if (this.redGreenModeManager) {
|
||||
this.redGreenModeManager.layerManager = this.layerManager;
|
||||
}
|
||||
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager.setLayerManager(this.layerManager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,7 +350,8 @@ export class CanvasManager {
|
||||
|
||||
/**
|
||||
* 居中所有画布元素
|
||||
* 计算所有对象的边界框,然后将它们整体居中显示
|
||||
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
|
||||
* 这样可以保持对象间的相对位置关系不变
|
||||
*/
|
||||
centerAllObjects() {
|
||||
if (!this.canvas) return;
|
||||
@@ -333,45 +364,56 @@ export class CanvasManager {
|
||||
(obj) => obj.visible !== false && !obj.excludeFromExport
|
||||
);
|
||||
|
||||
// 如果只有背景层或没有可见对象,只居中背景层
|
||||
if (
|
||||
visibleObjects.length === 0 ||
|
||||
(visibleObjects.length === 1 && visibleObjects[0].isBackground)
|
||||
) {
|
||||
// 尝试居中背景层
|
||||
this.centerBackgroundLayer(this.width, this.height);
|
||||
return;
|
||||
}
|
||||
// 如果没有可见对象,直接返回
|
||||
if (visibleObjects.length === 0) return;
|
||||
|
||||
// 单独处理背景层
|
||||
// 获取背景对象
|
||||
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
|
||||
const contentObjects = backgroundObject
|
||||
? visibleObjects.filter((obj) => obj !== backgroundObject)
|
||||
: visibleObjects;
|
||||
|
||||
// 如果只有背景层,居中背景层
|
||||
if (contentObjects.length === 0 && backgroundObject) {
|
||||
this.centerBackgroundLayer(this.width, this.height);
|
||||
// 如果只有背景层或没有背景层,使用原有逻辑
|
||||
if (!backgroundObject) {
|
||||
console.warn("未找到背景层,使用默认居中逻辑");
|
||||
// 如果只有一个对象且可能是背景,直接居中
|
||||
if (visibleObjects.length === 1) {
|
||||
const obj = visibleObjects[0];
|
||||
obj.set({
|
||||
left: this.width / 2,
|
||||
top: this.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
obj.setCoords();
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算内容对象的边界
|
||||
const bounds = this._calculateObjectsBounds(contentObjects);
|
||||
|
||||
// 计算所有对象的中心点
|
||||
const objectsCenterX = bounds.left + bounds.width / 2;
|
||||
const objectsCenterY = bounds.top + bounds.height / 2;
|
||||
// 记录背景层居中前的位置
|
||||
const backgroundOldLeft = backgroundObject.left;
|
||||
const backgroundOldTop = backgroundObject.top;
|
||||
|
||||
// 计算画布中心点
|
||||
const canvasCenterX = this.width / 2;
|
||||
const canvasCenterY = this.height / 2;
|
||||
|
||||
// 计算需要移动的距离
|
||||
const deltaX = canvasCenterX - objectsCenterX;
|
||||
const deltaY = canvasCenterY - objectsCenterY;
|
||||
// 设置背景层居中
|
||||
backgroundObject.set({
|
||||
left: canvasCenterX,
|
||||
top: canvasCenterY,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
});
|
||||
|
||||
// 移动所有对象,包括背景层
|
||||
visibleObjects.forEach((obj) => {
|
||||
// 计算背景层的偏移量
|
||||
const deltaX = backgroundObject.left - backgroundOldLeft;
|
||||
const deltaY = backgroundObject.top - backgroundOldTop;
|
||||
|
||||
// 将相同的偏移量应用到所有其他对象上
|
||||
const otherObjects = visibleObjects.filter(
|
||||
(obj) => obj !== backgroundObject
|
||||
);
|
||||
|
||||
otherObjects.forEach((obj) => {
|
||||
obj.set({
|
||||
left: obj.left + deltaX,
|
||||
top: obj.top + deltaY,
|
||||
@@ -456,6 +498,7 @@ export class CanvasManager {
|
||||
// 如果需要裁剪背景层以外的内容,则更新蒙层位置
|
||||
// 创建或更新蒙层
|
||||
CanvasConfig.isCropBackground &&
|
||||
!this.enabledRedGreenMode &&
|
||||
this.createOrUpdateMask(backgroundLayerObject);
|
||||
return true;
|
||||
}
|
||||
@@ -480,12 +523,13 @@ export class CanvasManager {
|
||||
|
||||
// 创建蒙层 - 使用透明矩形作为裁剪区域
|
||||
this.maskLayer = new fabric.Rect({
|
||||
id: "canvasMaskLayer",
|
||||
width: bgWidth,
|
||||
height: bgHeight,
|
||||
left: left,
|
||||
top: top,
|
||||
fill: "transparent",
|
||||
stroke: "#cccccc",
|
||||
stroke: "transparent",
|
||||
strokeWidth: 1,
|
||||
strokeDashArray: [5, 5],
|
||||
selectable: false,
|
||||
@@ -504,20 +548,12 @@ export class CanvasManager {
|
||||
this.canvas.clipPath = new fabric.Rect({
|
||||
width: bgWidth,
|
||||
height: bgHeight,
|
||||
left: 0,
|
||||
top: 0,
|
||||
left: left,
|
||||
top: top,
|
||||
originX: backgroundLayerObject.originX || "left",
|
||||
originY: backgroundLayerObject.originY || "top",
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
// 设置蒙层位置
|
||||
this.canvas.clipPath.set({
|
||||
left: left,
|
||||
top: top,
|
||||
});
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
getBackgroundLayer() {
|
||||
if (!this.canvas) return null;
|
||||
@@ -655,6 +691,39 @@ export class CanvasManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 更改固定图层的图片
|
||||
* @param {String} imageUrl 新的图片URL
|
||||
* @param {Object} options 选项
|
||||
* @param {String} options.targetLayerType 目标图层类型(background/fixed)
|
||||
* @param {Boolean} options.undoable 是否可撤销,默认不可撤销
|
||||
* @return {Object} 执行结果
|
||||
* */
|
||||
async changeFixedImage(imageUrl, options = {}) {
|
||||
if (!this.layerManager) {
|
||||
console.error("图层管理器未设置,无法更改固定图层图片");
|
||||
return;
|
||||
}
|
||||
const command = new ChangeFixedImageCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
imageUrl: imageUrl,
|
||||
targetLayerType: options.targetLayerType || "fixed", // background/fixed
|
||||
});
|
||||
|
||||
command.undoable =
|
||||
options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
|
||||
|
||||
return (
|
||||
(await command?.execute?.()) || {
|
||||
success: false,
|
||||
layerId: null,
|
||||
imageUrl: null,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出图片
|
||||
* @param {Object} options 导出选项
|
||||
@@ -663,6 +732,7 @@ export class CanvasManager {
|
||||
* @param {String} options.layerId 导出具体图层ID
|
||||
* @param {Array} options.layerIdArray 导出多个图层ID数组
|
||||
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
|
||||
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
|
||||
* @returns {String} 导出的图片数据URL
|
||||
*/
|
||||
exportImage(options = {}) {
|
||||
@@ -672,7 +742,39 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
try {
|
||||
return this.exportManager.exportImage(options);
|
||||
// 自动设置红绿图模式相关参数
|
||||
const enhancedOptions = {
|
||||
...options,
|
||||
// 如果没有明确指定,则根据当前模式自动设置
|
||||
restoreOpacityInRedGreen:
|
||||
options.restoreOpacityInRedGreen !== undefined
|
||||
? options.restoreOpacityInRedGreen
|
||||
: true, // 默认在红绿图模式下恢复透明度
|
||||
};
|
||||
|
||||
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
|
||||
if (
|
||||
this.enabledRedGreenMode &&
|
||||
!options.layerId &&
|
||||
(!options.layerIdArray || options.layerIdArray.length === 0)
|
||||
) {
|
||||
console.log("检测到红绿图模式,自动包含所有普通图层进行导出");
|
||||
|
||||
// 获取所有非背景、非固定的普通图层ID
|
||||
const normalLayerIds =
|
||||
this.layers?.value
|
||||
?.filter(
|
||||
(layer) => !layer.isBackground && !layer.isFixed && layer.visible
|
||||
)
|
||||
?.map((layer) => layer.id) || [];
|
||||
|
||||
if (normalLayerIds.length > 0) {
|
||||
enhancedOptions.layerIdArray = normalLayerIds;
|
||||
console.log("红绿图模式导出图层:", normalLayerIds);
|
||||
}
|
||||
}
|
||||
|
||||
return this.exportManager.exportImage(enhancedOptions);
|
||||
} catch (error) {
|
||||
console.error("CanvasManager导出图片失败:", error);
|
||||
throw error;
|
||||
@@ -709,129 +811,129 @@ export class CanvasManager {
|
||||
}
|
||||
|
||||
getJSON() {
|
||||
// 简化图层数据,在loadJSON时要根据id恢复引用
|
||||
let tempLayers = this.layers ? this.layers.value : [];
|
||||
// // 简化图层数据,在loadJSON时要根据id恢复引用
|
||||
// let tempLayers = this.layers ? this.layers.value : [];
|
||||
// // 创建对象ID映射表,用于快速查找
|
||||
// tempLayers = tempLayers.map((layer) => {
|
||||
// const newLayer = { ...layer };
|
||||
|
||||
// 为所有fabric对象生成ID(如果没有的话)
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (!obj.id) {
|
||||
obj.id = `obj_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
|
||||
}
|
||||
});
|
||||
// // 处理fabricObjects数组
|
||||
// if (Array.isArray(layer.fabricObjects)) {
|
||||
// newLayer.fabricObjects = layer.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// 创建对象ID映射表,用于快速查找
|
||||
const objectIdMap = new Map();
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.id) {
|
||||
objectIdMap.set(obj, obj.id);
|
||||
}
|
||||
});
|
||||
// // 确保对象有ID
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
tempLayers = tempLayers.map((layer) => {
|
||||
const newLayer = { ...layer };
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object", // 保存类型信息用于调试
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newLayer.fabricObjects = [];
|
||||
// }
|
||||
|
||||
// 处理fabricObjects数组
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
newLayer.fabricObjects = layer.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
// if (layer.clippingMask) {
|
||||
// layer.clippingMask = {
|
||||
// id: layer.clippingMask.id,
|
||||
// };
|
||||
// }
|
||||
|
||||
// 确保对象有ID
|
||||
if (!item.id) {
|
||||
item.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
// // 处理单个fabricObject
|
||||
// if (layer.fabricObject) {
|
||||
// if (!layer.fabricObject.id) {
|
||||
// layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
// newLayer.fabricObject = {
|
||||
// id: layer.fabricObject.id,
|
||||
// type: layer.fabricObject.type || "object",
|
||||
// };
|
||||
// } else {
|
||||
// newLayer.fabricObject = null;
|
||||
// }
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type || "object", // 保存类型信息用于调试
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
} else {
|
||||
newLayer.fabricObjects = [];
|
||||
}
|
||||
// // 处理子图层
|
||||
// if (Array.isArray(layer.children)) {
|
||||
// newLayer.children = layer.children.map((cItem) => {
|
||||
// const newChild = { ...cItem };
|
||||
|
||||
// 处理单个fabricObject
|
||||
if (layer.fabricObject) {
|
||||
if (!layer.fabricObject.id) {
|
||||
layer.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
newLayer.fabricObject = {
|
||||
id: layer.fabricObject.id,
|
||||
type: layer.fabricObject.type || "object",
|
||||
};
|
||||
} else {
|
||||
newLayer.fabricObject = null;
|
||||
}
|
||||
// // 处理子图层的fabricObjects
|
||||
// if (Array.isArray(cItem.fabricObjects)) {
|
||||
// newChild.fabricObjects = cItem.fabricObjects
|
||||
// .map((item) => {
|
||||
// if (!item) return null;
|
||||
|
||||
// 处理子图层
|
||||
if (Array.isArray(layer.children)) {
|
||||
newLayer.children = layer.children.map((cItem) => {
|
||||
const newChild = { ...cItem };
|
||||
// if (!item.id) {
|
||||
// item.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
|
||||
// 处理子图层的fabricObjects
|
||||
if (Array.isArray(cItem.fabricObjects)) {
|
||||
newChild.fabricObjects = cItem.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item) return null;
|
||||
// return {
|
||||
// id: item.id,
|
||||
// type: item.type || "object",
|
||||
// };
|
||||
// })
|
||||
// .filter((item) => item !== null);
|
||||
// } else {
|
||||
// newChild.fabricObjects = [];
|
||||
// }
|
||||
|
||||
if (!item.id) {
|
||||
item.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
// // 处理子图层的fabricObject
|
||||
// if (cItem.fabricObject) {
|
||||
// if (!cItem.fabricObject.id) {
|
||||
// cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
// Math.random() * 10000
|
||||
// )}`;
|
||||
// }
|
||||
// newChild.fabricObject = {
|
||||
// id: cItem.fabricObject.id,
|
||||
// type: cItem.fabricObject.type || "object",
|
||||
// };
|
||||
// } else {
|
||||
// newChild.fabricObject = null;
|
||||
// }
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type || "object",
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
} else {
|
||||
newChild.fabricObjects = [];
|
||||
}
|
||||
|
||||
// 处理子图层的fabricObject
|
||||
if (cItem.fabricObject) {
|
||||
if (!cItem.fabricObject.id) {
|
||||
cItem.fabricObject.id = `obj_${Date.now()}_${Math.floor(
|
||||
Math.random() * 10000
|
||||
)}`;
|
||||
}
|
||||
newChild.fabricObject = {
|
||||
id: cItem.fabricObject.id,
|
||||
type: cItem.fabricObject.type || "object",
|
||||
};
|
||||
} else {
|
||||
newChild.fabricObject = null;
|
||||
}
|
||||
|
||||
return newChild;
|
||||
});
|
||||
} else {
|
||||
newLayer.children = [];
|
||||
}
|
||||
|
||||
return newLayer;
|
||||
});
|
||||
// return newChild;
|
||||
// });
|
||||
// } else {
|
||||
// newLayer.children = [];
|
||||
// }
|
||||
|
||||
// return newLayer;
|
||||
// });
|
||||
try {
|
||||
console.log(
|
||||
"获取画布JSON数据...",
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
);
|
||||
return JSON.stringify({
|
||||
canvas: this.canvas.toJSON([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"isBackground",
|
||||
"isLocked",
|
||||
"isVisible",
|
||||
"isFixed",
|
||||
"parentId",
|
||||
"excludeFromExport",
|
||||
"eraser",
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]),
|
||||
layers: tempLayers,
|
||||
layers: JSON.stringify(
|
||||
simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
), // 简化图层数据
|
||||
version: "1.0", // 添加版本信息
|
||||
timestamp: new Date().toISOString(), // 添加时间戳
|
||||
canvasWidth: this.canvasWidth.value,
|
||||
@@ -845,7 +947,7 @@ export class CanvasManager {
|
||||
}
|
||||
}
|
||||
|
||||
loadJSON(json) {
|
||||
loadJSON(json, calllBack) {
|
||||
console.log("加载画布JSON数据:", json);
|
||||
|
||||
// 确保传入的json是字符串格式
|
||||
@@ -859,7 +961,7 @@ export class CanvasManager {
|
||||
const parsedJson = JSON.parse(json);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tempLayers = parsedJson?.layers || [];
|
||||
const tempLayers = JSON.parse(parsedJson?.layers) || [];
|
||||
const canvasData = parsedJson?.canvas;
|
||||
|
||||
if (!tempLayers) {
|
||||
@@ -872,9 +974,11 @@ export class CanvasManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
||||
this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
||||
this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
||||
this.layers.value = tempLayers;
|
||||
|
||||
// this.canvasWidth.value = parsedJson.canvasWidth || this.width;
|
||||
// this.canvasHeight.value = parsedJson.canvasHeight || this.height;
|
||||
// this.canvasColor.value = parsedJson.canvasColor || this.backgroundColor;
|
||||
|
||||
console.log("是否检测到红绿图模式内容:", this.enabledRedGreenMode);
|
||||
|
||||
@@ -885,107 +989,57 @@ export class CanvasManager {
|
||||
this.canvas.clear();
|
||||
|
||||
// 加载画布数据
|
||||
this.canvas.loadFromJSON(canvasData, () => {
|
||||
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
||||
try {
|
||||
// 重置画布数据
|
||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||
this.canvas.loadFromJSON(canvasData, async () => {
|
||||
await optimizeCanvasRendering(this.canvas, async () => {
|
||||
this.backgroundColor = parsedJson.backgroundColor || "#ffffff";
|
||||
try {
|
||||
// 重置画布数据
|
||||
this.setCanvasSize(this.canvas.width, this.canvas.height);
|
||||
|
||||
// 创建对象ID映射表,用于快速查找
|
||||
const objectIdMap = new Map();
|
||||
const canvasObjects = this.canvas.getObjects();
|
||||
// 重新构建对象关系
|
||||
restoreObjectLayerAssociations(
|
||||
this.layers.value,
|
||||
this.canvas.getObjects()
|
||||
);
|
||||
|
||||
canvasObjects.forEach((obj) => {
|
||||
if (obj.id) {
|
||||
objectIdMap.set(obj.id, obj);
|
||||
}
|
||||
});
|
||||
// 验证图层关联关系 - 稳定后可以注释
|
||||
const isValidate = validateLayerAssociations(
|
||||
this.layers.value,
|
||||
this.canvas.getObjects()
|
||||
);
|
||||
|
||||
// 辅助函数:根据ID查找对象
|
||||
const findObjectById = (id) => {
|
||||
if (!id) return null;
|
||||
return objectIdMap.get(id) || null;
|
||||
};
|
||||
console.log("图层关联验证结果:", isValidate);
|
||||
|
||||
// 恢复图层数据
|
||||
this.layers.value = tempLayers.map((layer) => {
|
||||
const restoredLayer = { ...layer };
|
||||
this.canvas.activeLayerId.value =
|
||||
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
|
||||
|
||||
// 恢复fabricObjects数组
|
||||
if (Array.isArray(layer.fabricObjects)) {
|
||||
restoredLayer.fabricObjects = layer.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item || !item.id) return null;
|
||||
return findObjectById(item.id);
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
} else {
|
||||
restoredLayer.fabricObjects = [];
|
||||
}
|
||||
// // 如果检测到红绿图模式内容,进行缩放调整
|
||||
// if (this.enabledRedGreenMode) {
|
||||
// this._rescaleRedGreenModeContent();
|
||||
// }
|
||||
|
||||
// 恢复单个fabricObject
|
||||
if (layer.fabricObject && layer.fabricObject.id) {
|
||||
restoredLayer.fabricObject = findObjectById(
|
||||
layer.fabricObject.id
|
||||
);
|
||||
} else {
|
||||
restoredLayer.fabricObject = null;
|
||||
}
|
||||
// 重载代码后支持回调中操作一些内容
|
||||
await calllBack?.();
|
||||
|
||||
// 恢复子图层
|
||||
if (Array.isArray(layer.children)) {
|
||||
restoredLayer.children = layer.children.map((cItem) => {
|
||||
const restoredChild = { ...cItem };
|
||||
// 确保所有对象的交互性正确设置
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.(
|
||||
false
|
||||
);
|
||||
console.log(this.layerManager.layers.value);
|
||||
debugger;
|
||||
|
||||
// 恢复子图层的fabricObjects
|
||||
if (Array.isArray(cItem.fabricObjects)) {
|
||||
restoredChild.fabricObjects = cItem.fabricObjects
|
||||
.map((item) => {
|
||||
if (!item || !item.id) return null;
|
||||
return findObjectById(item.id);
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
} else {
|
||||
restoredChild.fabricObjects = [];
|
||||
}
|
||||
// 更新所有缩略图
|
||||
setTimeout(() => {
|
||||
this.updateAllThumbnails();
|
||||
}, 100);
|
||||
|
||||
// 恢复子图层的fabricObject
|
||||
if (cItem.fabricObject && cItem.fabricObject.id) {
|
||||
restoredChild.fabricObject = findObjectById(
|
||||
cItem.fabricObject.id
|
||||
);
|
||||
} else {
|
||||
restoredChild.fabricObject = null;
|
||||
}
|
||||
|
||||
return restoredChild;
|
||||
});
|
||||
} else {
|
||||
restoredLayer.children = [];
|
||||
}
|
||||
|
||||
return restoredLayer;
|
||||
});
|
||||
|
||||
this.canvas.activeLayerId.value =
|
||||
parsedJson?.activeLayerId || this.layers.value[0]?.id || null;
|
||||
|
||||
// 如果检测到红绿图模式内容,进行缩放调整
|
||||
if (this.enabledRedGreenMode) {
|
||||
this._rescaleRedGreenModeContent();
|
||||
console.log("画布JSON数据加载完成");
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("恢复图层数据失败:", error);
|
||||
reject(new Error("恢复图层数据失败: " + error.message));
|
||||
}
|
||||
|
||||
// 更新所有缩略图
|
||||
setTimeout(() => {
|
||||
this.updateAllThumbnails();
|
||||
}, 100);
|
||||
|
||||
console.log("画布JSON数据加载完成");
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error("恢复图层数据失败:", error);
|
||||
reject(new Error("恢复图层数据失败: " + error.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { EraserCommand } from "../commands/EraserCommand";
|
||||
|
||||
/**
|
||||
* 橡皮擦状态管理器
|
||||
* 用于管理橡皮擦操作的状态快照
|
||||
*/
|
||||
export class EraserStateManager {
|
||||
constructor(canvas, layerManager) {
|
||||
this.canvas = canvas;
|
||||
this.layerManager = layerManager;
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
|
||||
setLayerManager(layerManager) {
|
||||
this.layerManager = layerManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始橡皮擦操作 - 捕获初始状态
|
||||
*/
|
||||
startErasing() {
|
||||
console.log("橡皮擦操作开始 - 捕获状态快照");
|
||||
this.currentSnapshot = this._captureCanvasSnapshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束橡皮擦操作 - 创建命令
|
||||
* @param {Array} affectedObjects 受影响的对象
|
||||
* @returns {EraserCommand|null} 创建的橡皮擦命令
|
||||
*/
|
||||
endErasing(affectedObjects = []) {
|
||||
if (!this.currentSnapshot) {
|
||||
console.warn("没有初始状态快照,无法创建橡皮擦命令");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!affectedObjects || affectedObjects.length === 0) {
|
||||
console.log("没有对象被擦除,不创建命令");
|
||||
this.currentSnapshot = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`橡皮擦操作结束 - 影响了 ${affectedObjects.length} 个对象`);
|
||||
|
||||
// 捕获擦除后的状态
|
||||
const afterSnapshot = this._captureCanvasSnapshot();
|
||||
|
||||
// 创建橡皮擦命令
|
||||
const command = new EraserCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
affectedObjects: affectedObjects,
|
||||
beforeSnapshot: this.currentSnapshot,
|
||||
afterSnapshot: afterSnapshot,
|
||||
});
|
||||
|
||||
// 重置状态
|
||||
this.currentSnapshot = null;
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获画布状态快照
|
||||
* @returns {Object} 画布状态快照
|
||||
* @private
|
||||
*/
|
||||
_captureCanvasSnapshot() {
|
||||
try {
|
||||
return this.canvas.toJSON([
|
||||
"id",
|
||||
"type",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"isBackground",
|
||||
"isLocked",
|
||||
"isVisible",
|
||||
"isFixed",
|
||||
"parentId",
|
||||
"eraser",
|
||||
"eraserable",
|
||||
"erasable",
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("捕获画布状态快照失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前擦除操作
|
||||
*/
|
||||
cancelErasing() {
|
||||
this.currentSnapshot = null;
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* 液化对象引用管理器
|
||||
* 专门处理液化操作中的对象引用管理,避免引用丢失问题
|
||||
*/
|
||||
export class LiquifyReferenceManager {
|
||||
constructor() {
|
||||
// 对象引用池
|
||||
this.objectRefs = new Map();
|
||||
// 状态快照池
|
||||
this.stateSnapshots = new Map();
|
||||
// ImageData缓存池
|
||||
this.imageDataCache = new Map();
|
||||
// 事件监听器备份
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册对象到引用管理器
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {String} objectId 对象唯一ID
|
||||
* @returns {String} 引用ID
|
||||
*/
|
||||
registerObject(fabricObject, objectId) {
|
||||
const refId = objectId || this._generateRefId();
|
||||
|
||||
// 保存对象引用
|
||||
this.objectRefs.set(refId, fabricObject);
|
||||
|
||||
// 备份事件监听器
|
||||
this._backupEventListeners(refId, fabricObject);
|
||||
|
||||
// 创建初始状态快照
|
||||
this._createStateSnapshot(refId, fabricObject);
|
||||
|
||||
console.log(`📝 对象已注册到引用管理器: ${refId}`);
|
||||
return refId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象引用
|
||||
* @param {String} refId 引用ID
|
||||
* @returns {Object|null} Fabric对象
|
||||
*/
|
||||
getObjectRef(refId) {
|
||||
return this.objectRefs.get(refId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对象的图像数据,保持引用不变
|
||||
* @param {String} refId 引用ID
|
||||
* @param {ImageData} newImageData 新的图像数据
|
||||
* @returns {Promise<Boolean>} 更新结果
|
||||
*/
|
||||
async updateObjectImageData(refId, newImageData) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
if (!fabricObject || !newImageData) {
|
||||
throw new Error(`无法更新对象图像数据: 对象不存在或数据无效 (${refId})`);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = newImageData.width;
|
||||
tempCanvas.height = newImageData.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
tempCtx.putImageData(newImageData, 0, 0);
|
||||
|
||||
// 创建新的图像元素
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 保存当前状态
|
||||
const currentState = this._captureObjectState(fabricObject);
|
||||
|
||||
// 更新图像源
|
||||
if (fabricObject.setElement) {
|
||||
fabricObject.setElement(img);
|
||||
} else if (fabricObject._element) {
|
||||
fabricObject._element = img;
|
||||
fabricObject._originalElement = img;
|
||||
}
|
||||
|
||||
// 恢复非图像属性
|
||||
this._restoreObjectState(fabricObject, currentState);
|
||||
|
||||
// 标记需要重新渲染
|
||||
fabricObject.dirty = true;
|
||||
fabricObject.setCoords();
|
||||
|
||||
// 更新缓存
|
||||
this._updateImageDataCache(refId, newImageData);
|
||||
|
||||
console.log(`✅ 对象图像数据更新成功: ${refId}`);
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
console.error("更新对象状态失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error("加载图像数据失败"));
|
||||
};
|
||||
|
||||
img.src = tempCanvas.toDataURL();
|
||||
} catch (error) {
|
||||
console.error("创建临时canvas失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对象状态快照
|
||||
* @param {String} refId 引用ID
|
||||
* @param {String} snapshotId 快照ID
|
||||
* @returns {String} 快照ID
|
||||
*/
|
||||
createSnapshot(refId, snapshotId = null) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
if (!fabricObject) {
|
||||
throw new Error(`无法创建快照: 对象不存在 (${refId})`);
|
||||
}
|
||||
|
||||
const snapId = snapshotId || `${refId}_${Date.now()}`;
|
||||
const snapshot = this._createStateSnapshot(snapId, fabricObject);
|
||||
|
||||
console.log(`📸 已创建对象快照: ${snapId}`);
|
||||
return snapId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象到指定快照状态
|
||||
* @param {String} refId 引用ID
|
||||
* @param {String} snapshotId 快照ID
|
||||
* @returns {Promise<Boolean>} 恢复结果
|
||||
*/
|
||||
async restoreFromSnapshot(refId, snapshotId) {
|
||||
const fabricObject = this.objectRefs.get(refId);
|
||||
const snapshot = this.stateSnapshots.get(snapshotId);
|
||||
|
||||
if (!fabricObject || !snapshot) {
|
||||
throw new Error(
|
||||
`无法恢复快照: 对象或快照不存在 (${refId}, ${snapshotId})`
|
||||
);
|
||||
}
|
||||
|
||||
// 恢复图像数据
|
||||
if (snapshot.imageData) {
|
||||
await this.updateObjectImageData(refId, snapshot.imageData);
|
||||
}
|
||||
|
||||
// 恢复对象属性
|
||||
if (snapshot.properties) {
|
||||
this._restoreObjectState(fabricObject, snapshot.properties);
|
||||
}
|
||||
|
||||
// 恢复事件监听器
|
||||
if (snapshot.eventListeners) {
|
||||
this._restoreEventListeners(refId, fabricObject, snapshot.eventListeners);
|
||||
}
|
||||
|
||||
console.log(`🔄 对象快照恢复成功: ${refId} -> ${snapshotId}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新多个对象
|
||||
* @param {Array} updates 更新列表 [{refId, imageData}, ...]
|
||||
* @returns {Promise<Array>} 更新结果
|
||||
*/
|
||||
async batchUpdate(updates) {
|
||||
const results = [];
|
||||
|
||||
for (const update of updates) {
|
||||
try {
|
||||
const result = await this.updateObjectImageData(
|
||||
update.refId,
|
||||
update.imageData
|
||||
);
|
||||
results.push({ refId: update.refId, success: true, result });
|
||||
} catch (error) {
|
||||
console.error(`批量更新失败 ${update.refId}:`, error);
|
||||
results.push({
|
||||
refId: update.refId,
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理不再使用的引用
|
||||
* @param {String} refId 引用ID
|
||||
*/
|
||||
cleanup(refId) {
|
||||
this.objectRefs.delete(refId);
|
||||
this.stateSnapshots.delete(refId);
|
||||
this.imageDataCache.delete(refId);
|
||||
this.eventListeners.delete(refId);
|
||||
|
||||
console.log(`🗑️ 已清理对象引用: ${refId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有引用
|
||||
*/
|
||||
cleanupAll() {
|
||||
this.objectRefs.clear();
|
||||
this.stateSnapshots.clear();
|
||||
this.imageDataCache.clear();
|
||||
this.eventListeners.clear();
|
||||
|
||||
console.log("🗑️ 已清理所有对象引用");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存使用统计
|
||||
* @returns {Object} 统计信息
|
||||
*/
|
||||
getMemoryStats() {
|
||||
return {
|
||||
objectRefs: this.objectRefs.size,
|
||||
stateSnapshots: this.stateSnapshots.size,
|
||||
imageDataCache: this.imageDataCache.size,
|
||||
eventListeners: this.eventListeners.size,
|
||||
totalMemoryUsage: this._calculateMemoryUsage(),
|
||||
};
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
/**
|
||||
* 生成引用ID
|
||||
* @returns {String} 引用ID
|
||||
* @private
|
||||
*/
|
||||
_generateRefId() {
|
||||
return `ref_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份事件监听器
|
||||
* @param {String} refId 引用ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @private
|
||||
*/
|
||||
_backupEventListeners(refId, fabricObject) {
|
||||
const listeners = {};
|
||||
|
||||
// 备份常见的事件监听器
|
||||
const eventTypes = [
|
||||
"mousedown",
|
||||
"mouseup",
|
||||
"mousemove",
|
||||
"mouseout",
|
||||
"mouseover",
|
||||
];
|
||||
|
||||
eventTypes.forEach((eventType) => {
|
||||
if (
|
||||
fabricObject.__eventListeners &&
|
||||
fabricObject.__eventListeners[eventType]
|
||||
) {
|
||||
listeners[eventType] = [...fabricObject.__eventListeners[eventType]];
|
||||
}
|
||||
});
|
||||
|
||||
this.eventListeners.set(refId, listeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复事件监听器
|
||||
* @param {String} refId 引用ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {Object} listeners 监听器备份
|
||||
* @private
|
||||
*/
|
||||
_restoreEventListeners(refId, fabricObject, listeners) {
|
||||
Object.keys(listeners).forEach((eventType) => {
|
||||
if (listeners[eventType] && listeners[eventType].length > 0) {
|
||||
listeners[eventType].forEach((listener) => {
|
||||
fabricObject.on(eventType, listener);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建状态快照
|
||||
* @param {String} snapId 快照ID
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {Object} 快照数据
|
||||
* @private
|
||||
*/
|
||||
_createStateSnapshot(snapId, fabricObject) {
|
||||
const snapshot = {
|
||||
timestamp: Date.now(),
|
||||
properties: this._captureObjectState(fabricObject),
|
||||
imageData: this._captureImageData(fabricObject),
|
||||
eventListeners: this.eventListeners.get(snapId.split("_")[0]) || {},
|
||||
};
|
||||
|
||||
this.stateSnapshots.set(snapId, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获对象状态
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {Object} 对象状态
|
||||
* @private
|
||||
*/
|
||||
_captureObjectState(fabricObject) {
|
||||
return {
|
||||
left: fabricObject.left,
|
||||
top: fabricObject.top,
|
||||
scaleX: fabricObject.scaleX,
|
||||
scaleY: fabricObject.scaleY,
|
||||
angle: fabricObject.angle,
|
||||
flipX: fabricObject.flipX,
|
||||
flipY: fabricObject.flipY,
|
||||
opacity: fabricObject.opacity,
|
||||
visible: fabricObject.visible,
|
||||
selectable: fabricObject.selectable,
|
||||
evented: fabricObject.evented,
|
||||
id: fabricObject.id,
|
||||
layerId: fabricObject.layerId,
|
||||
customData: fabricObject.customData ? { ...fabricObject.customData } : {},
|
||||
filters: fabricObject.filters ? [...fabricObject.filters] : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象状态
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @param {Object} state 状态数据
|
||||
* @private
|
||||
*/
|
||||
_restoreObjectState(fabricObject, state) {
|
||||
if (!state) return;
|
||||
|
||||
fabricObject.set({
|
||||
left: state.left,
|
||||
top: state.top,
|
||||
scaleX: state.scaleX,
|
||||
scaleY: state.scaleY,
|
||||
angle: state.angle,
|
||||
flipX: state.flipX,
|
||||
flipY: state.flipY,
|
||||
opacity: state.opacity,
|
||||
visible: state.visible,
|
||||
selectable: state.selectable,
|
||||
evented: state.evented,
|
||||
});
|
||||
|
||||
// 恢复自定义属性
|
||||
if (state.customData) {
|
||||
fabricObject.customData = { ...state.customData };
|
||||
}
|
||||
|
||||
// 恢复滤镜
|
||||
if (state.filters && state.filters.length > 0) {
|
||||
fabricObject.filters = [...state.filters];
|
||||
fabricObject.applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获图像数据
|
||||
* @param {Object} fabricObject Fabric对象
|
||||
* @returns {ImageData|null} 图像数据
|
||||
* @private
|
||||
*/
|
||||
_captureImageData(fabricObject) {
|
||||
try {
|
||||
if (
|
||||
fabricObject._element &&
|
||||
fabricObject._element.width &&
|
||||
fabricObject._element.height
|
||||
) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = fabricObject._element.width;
|
||||
canvas.height = fabricObject._element.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(fabricObject._element, 0, 0);
|
||||
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("无法捕获图像数据:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图像数据缓存
|
||||
* @param {String} refId 引用ID
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
_updateImageDataCache(refId, imageData) {
|
||||
this.imageDataCache.set(refId, {
|
||||
data: imageData,
|
||||
timestamp: Date.now(),
|
||||
width: imageData.width,
|
||||
height: imageData.height,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算内存使用量(近似值)
|
||||
* @returns {Number} 内存使用量(字节)
|
||||
* @private
|
||||
*/
|
||||
_calculateMemoryUsage() {
|
||||
let totalBytes = 0;
|
||||
|
||||
// 计算ImageData缓存大小
|
||||
this.imageDataCache.forEach((cache) => {
|
||||
totalBytes += cache.width * cache.height * 4; // RGBA = 4 bytes per pixel
|
||||
});
|
||||
|
||||
return totalBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单例引用管理器
|
||||
*/
|
||||
let liquifyReferenceManagerInstance = null;
|
||||
|
||||
export function getLiquifyReferenceManager() {
|
||||
if (!liquifyReferenceManagerInstance) {
|
||||
liquifyReferenceManagerInstance = new LiquifyReferenceManager();
|
||||
}
|
||||
return liquifyReferenceManagerInstance;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
||||
import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js";
|
||||
|
||||
@@ -35,7 +35,6 @@ export class RedGreenModeManager {
|
||||
* @param {String} options.redGreenImageUrl 红绿图URL
|
||||
* @param {Number} options.normalLayerOpacity 普通图层透明度 (0-1)
|
||||
* @param {Function} options.onImageGenerated 图片生成回调
|
||||
* @param {Boolean} options.useBatchMode 是否使用批量模式 (默认true,减少闪烁)
|
||||
* @returns {Promise<boolean>} 是否初始化成功
|
||||
*/
|
||||
async initialize(options = {}) {
|
||||
@@ -70,13 +69,8 @@ export class RedGreenModeManager {
|
||||
throw new Error("缺少必需的图片URL参数");
|
||||
}
|
||||
|
||||
// 使用批量模式或传统模式
|
||||
const useBatchMode = options.useBatchMode !== false; // 默认为true
|
||||
|
||||
let initCommand;
|
||||
|
||||
// 使用新的批量初始化命令,减少页面闪烁
|
||||
initCommand = new BatchInitializeRedGreenModeCommand({
|
||||
const initCommand = new BatchInitializeRedGreenModeCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
toolManager: this.toolManager,
|
||||
@@ -94,10 +88,11 @@ export class RedGreenModeManager {
|
||||
await initCommand.execute();
|
||||
}
|
||||
|
||||
this.registerRedGreenMouseUpEvent();
|
||||
// 标记为已初始化
|
||||
this.isInitialized = true;
|
||||
|
||||
this.registerRedGreenMouseUpEvent();
|
||||
|
||||
// 启用图层管理器的红绿图模式
|
||||
if (
|
||||
this.layerManager &&
|
||||
@@ -106,6 +101,9 @@ export class RedGreenModeManager {
|
||||
this.layerManager.enableRedGreenMode();
|
||||
}
|
||||
|
||||
// 更新交互性
|
||||
await this.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
|
||||
// 重置工具管理器状态
|
||||
// 默认红色笔刷
|
||||
if (this.toolManager) {
|
||||
@@ -116,7 +114,6 @@ export class RedGreenModeManager {
|
||||
衣服底图: this.clothingImageUrl,
|
||||
红绿图: this.redGreenImageUrl,
|
||||
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
|
||||
批量模式: useBatchMode ? "已启用" : "已禁用",
|
||||
画布背景: "白色",
|
||||
});
|
||||
|
||||
@@ -131,17 +128,17 @@ export class RedGreenModeManager {
|
||||
// 注册鼠标抬起事件
|
||||
registerRedGreenMouseUpEvent() {
|
||||
this.canvas.on("mouse:up", (event) => {
|
||||
if (!this.isInitialized) {
|
||||
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 可以在这里添加更多逻辑,比如生成图片或更新状态
|
||||
if (this.onImageGenerated) {
|
||||
const imageData = this.canvasManager.exportImage();
|
||||
console.log("生成红绿图图片数据:", imageData);
|
||||
this.onImageGenerated(imageData);
|
||||
}
|
||||
requestAnimationFrame(async () => {
|
||||
if (!this.isInitialized) {
|
||||
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
||||
return;
|
||||
}
|
||||
if (this.onImageGenerated) {
|
||||
const imageData = await this.canvasManager.exportImage();
|
||||
this.onImageGenerated(imageData);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ import { BrushManager } from "./brushes/brushManager";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import CanvasConfig from "../config/canvasConfig";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import {
|
||||
InitLiquifyToolCommand,
|
||||
RasterizeForLiquifyCommand,
|
||||
} from "../commands/LiquifyCommands";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { InitLiquifyToolCommand } from "../commands/LiquifyCommands";
|
||||
import { RasterizeLayerCommand } from "../commands/GroupCommands";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { h } from "vue";
|
||||
|
||||
/**
|
||||
* 工具管理器
|
||||
@@ -378,6 +378,8 @@ export class ToolManager {
|
||||
previousTool: this.activeTool.value,
|
||||
});
|
||||
|
||||
command.undoable = options.undoable !== undefined ? options.undoable : true;
|
||||
|
||||
// 执行命令
|
||||
this.commandManager.execute(command, { ...options });
|
||||
}
|
||||
@@ -690,18 +692,9 @@ export class ToolManager {
|
||||
}
|
||||
} else if (checkResult.needsRasterization) {
|
||||
// 需要栅格化 (多个对象或组)
|
||||
// 询问用户是否要栅格化
|
||||
if (
|
||||
confirm(
|
||||
checkResult.isGroup
|
||||
? "组对象需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
: "当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
)
|
||||
) {
|
||||
// 用户确认栅格化,执行栅格化操作
|
||||
this._rasterizeLayerForLiquify(activeLayerId);
|
||||
return; // 栅格化后会重新调用液化功能,这里直接返回
|
||||
}
|
||||
// 使用Modal询问用户是否要栅格化
|
||||
this._showRasterizeConfirmModal(checkResult.isGroup, activeLayerId);
|
||||
return; // 等待用户确认,不继续执行
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,51 +709,42 @@ export class ToolManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并准备液化操作
|
||||
* 显示栅格化确认Modal对话框
|
||||
* @param {Boolean} isGroup 是否为组对象
|
||||
* @param {String} layerId 图层ID
|
||||
* @private
|
||||
*/
|
||||
_checkAndPrepareForLiquify(layerId) {
|
||||
// 确保存在液化管理器
|
||||
const liquifyManager = this.canvasManager?.liquifyManager;
|
||||
if (!liquifyManager) {
|
||||
console.error("液化管理器未初始化");
|
||||
return;
|
||||
}
|
||||
_showRasterizeConfirmModal(isGroup, layerId) {
|
||||
const title = "栅格化图层";
|
||||
const content = "需要先栅格化才能进行液化操作,是否立即栅格化?";
|
||||
|
||||
// 检查图层是否适合液化
|
||||
const checkResult = liquifyManager.checkLayerForLiquify(layerId);
|
||||
|
||||
if (checkResult.isEmpty) {
|
||||
// 空图层
|
||||
alert("当前图层为空,无法进行液化操作");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkResult.isGroup) {
|
||||
// 询问是否栅格化组
|
||||
if (confirm("组对象需要栅格化才能进行液化操作,是否立即栅格化?")) {
|
||||
Modal.confirm({
|
||||
title,
|
||||
content,
|
||||
okText: "确定栅格化",
|
||||
cancelText: "取消",
|
||||
centered: true,
|
||||
icon: h("span", { style: "color: #faad14;" }, "⚠️"),
|
||||
onOk: () => {
|
||||
// 用户确认栅格化,执行栅格化操作
|
||||
this._rasterizeLayerForLiquify(layerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkResult.needsRasterization) {
|
||||
// 询问是否栅格化图层
|
||||
if (
|
||||
confirm(
|
||||
"当前图层含有多个对象,需要先栅格化才能进行液化操作,是否立即栅格化?"
|
||||
)
|
||||
) {
|
||||
this._rasterizeLayerForLiquify(layerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果图层可以直接液化(单个图像对象)
|
||||
if (checkResult.valid) {
|
||||
this._startLiquify(layerId);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log("用户取消了栅格化操作");
|
||||
// 用户取消,触发液化面板显示事件但不能液化
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("showLiquifyPanel", {
|
||||
detail: {
|
||||
activeLayerId: layerId,
|
||||
layerStatus: { needsRasterization: true, isGroup },
|
||||
canLiquify: false,
|
||||
targetObject: null,
|
||||
originalImageData: null,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -772,25 +756,52 @@ export class ToolManager {
|
||||
if (!this.commandManager || !this.layerManager) return;
|
||||
|
||||
try {
|
||||
// 导入液化相关命令
|
||||
// 显示加载Modal
|
||||
const loadingModal = Modal.info({
|
||||
title: "正在栅格化",
|
||||
content: "正在栅格化图层,请稍候...",
|
||||
okButtonProps: { style: { display: "none" } },
|
||||
centered: true,
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
});
|
||||
|
||||
// 创建栅格化命令
|
||||
const rasterizeCommand = new RasterizeForLiquifyCommand({
|
||||
const rasterizeCommand = new RasterizeLayerCommand({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
layerId: layerId,
|
||||
layers: this.layerManager.layers,
|
||||
activeLayerId: this.layerManager.activeLayerId,
|
||||
});
|
||||
|
||||
// 执行命令
|
||||
const result = await this.commandManager.execute(rasterizeCommand);
|
||||
|
||||
// 关闭加载Modal
|
||||
loadingModal.destroy();
|
||||
|
||||
if (result) {
|
||||
// 栅格化成功,启动液化
|
||||
message.success("图层已成功栅格化,可以进行液化操作");
|
||||
this._startLiquify(layerId);
|
||||
} else {
|
||||
// 栅格化失败
|
||||
Modal.error({
|
||||
title: "栅格化失败",
|
||||
content: "栅格化失败,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("栅格化图层失败:", error);
|
||||
alert("栅格化失败,无法进行液化操作");
|
||||
Modal.error({
|
||||
title: "栅格化错误",
|
||||
content: `栅格化失败:${error.message}`,
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +814,12 @@ export class ToolManager {
|
||||
// 获取图层信息
|
||||
const layer = this.layerManager.getLayerById(layerId);
|
||||
if (!layer) {
|
||||
console.error("图层不存在");
|
||||
Modal.error({
|
||||
title: "图层错误",
|
||||
content: "图层不存在",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -812,14 +828,24 @@ export class ToolManager {
|
||||
if (layer.isBackground) {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (!layer.fabricObject) {
|
||||
console.error("背景图层为空");
|
||||
Modal.warning({
|
||||
title: "背景图层为空",
|
||||
content: "背景图层为空,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
targetObject = layer.fabricObject;
|
||||
} else {
|
||||
// 普通图层使用 fabricObjects (复数)
|
||||
if (!layer.fabricObjects || layer.fabricObjects.length === 0) {
|
||||
console.error("图层为空");
|
||||
Modal.warning({
|
||||
title: "图层为空",
|
||||
content: "图层为空,无法进行液化操作",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
targetObject = layer.fabricObjects[0];
|
||||
@@ -828,11 +854,26 @@ export class ToolManager {
|
||||
// 确保liquifyManager可用
|
||||
const liquifyManager = this.canvasManager?.liquifyManager;
|
||||
if (!liquifyManager) {
|
||||
console.error("液化管理器未初始化");
|
||||
Modal.error({
|
||||
title: "液化管理器错误",
|
||||
content: "液化管理器未初始化",
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示准备中的Modal
|
||||
const preparingModal = Modal.info({
|
||||
title: "准备液化环境",
|
||||
content: "正在准备液化环境,请稍候...",
|
||||
okButtonProps: { style: { display: "none" } },
|
||||
centered: true,
|
||||
closable: false,
|
||||
maskClosable: false,
|
||||
});
|
||||
|
||||
// 准备液化环境
|
||||
liquifyManager.initialize({
|
||||
canvas: this.canvas,
|
||||
@@ -855,6 +896,9 @@ export class ToolManager {
|
||||
// 执行初始化命令
|
||||
await this.commandManager.execute(initCommand);
|
||||
|
||||
// 关闭准备Modal
|
||||
preparingModal.destroy();
|
||||
|
||||
// 触发液化面板显示事件
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("showLiquifyPanel", {
|
||||
@@ -867,7 +911,12 @@ export class ToolManager {
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("启动液化工具失败:", error);
|
||||
alert("启动液化工具失败:" + error.message);
|
||||
Modal.error({
|
||||
title: "液化工具启动失败",
|
||||
content: `启动液化工具失败:${error.message}`,
|
||||
okText: "确定",
|
||||
centered: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -577,6 +577,49 @@ export class TexturePresetManager {
|
||||
cacheSize: this.textureCache.size,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证纹理文件
|
||||
* @param {File} file 要验证的文件
|
||||
* @returns {Boolean} 是否为有效的纹理文件
|
||||
*/
|
||||
validateTextureFile(file) {
|
||||
if (!file) {
|
||||
console.warn("文件不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith("image/")) {
|
||||
console.warn("文件类型无效,必须是图片文件");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小(限制为 10MB)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
console.warn("文件大小超过限制(10MB)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查支持的图片格式
|
||||
const supportedTypes = [
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
];
|
||||
|
||||
if (!supportedTypes.includes(file.type)) {
|
||||
console.warn("不支持的图片格式");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import "./fabric.brushes.js";
|
||||
import { BrushStore } from "../../store/BrushStore";
|
||||
import { brushRegistry } from "./BrushRegistry";
|
||||
|
||||
@@ -16,7 +17,7 @@ import { MarkerBrush } from "./types/MarkerBrush";
|
||||
import { CustomPenBrush } from "./types/CustomPenBrush";
|
||||
import { RibbonBrush } from "./types/RibbonBrush";
|
||||
import { ShadedBrush } from "./types/ShadedBrush";
|
||||
import { SketchyBrush } from "./types/SketchyBrush";
|
||||
// import { SketchyBrush } from "./types/SketchyBrush";
|
||||
import { SpraypaintBrush } from "./types/SpraypaintBrush";
|
||||
|
||||
/**
|
||||
@@ -42,6 +43,11 @@ export class BrushManager {
|
||||
|
||||
// 初始化笔刷注册
|
||||
this._registerDefaultBrushes();
|
||||
|
||||
// 初始化橡皮擦状态管理器
|
||||
this.eraserStateManager = null;
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = []; // 当前擦除会话中被影响的对象
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,64 +57,112 @@ export class BrushManager {
|
||||
_registerDefaultBrushes() {
|
||||
// 注册铅笔笔刷
|
||||
brushRegistry.register("pencil", PencilBrush, {
|
||||
name: "铅笔",
|
||||
name: "Pencil",
|
||||
description: "基础铅笔工具,适合精细线条绘制",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册材质笔刷
|
||||
brushRegistry.register("texture", TextureBrush);
|
||||
brushRegistry.register("texture", TextureBrush, {
|
||||
name: "Texture",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册集成的笔刷类型
|
||||
brushRegistry.register("crayon", CrayonBrush);
|
||||
brushRegistry.register("fur", FurBrush);
|
||||
brushRegistry.register("ink", InkBrush);
|
||||
brushRegistry.register("longfur", LongfurBrush);
|
||||
brushRegistry.register("writing", WritingBrush);
|
||||
brushRegistry.register("marker", MarkerBrush);
|
||||
brushRegistry.register("pen", CustomPenBrush);
|
||||
brushRegistry.register("ribbon", RibbonBrush);
|
||||
brushRegistry.register("shaded", ShadedBrush);
|
||||
brushRegistry.register("sketchy", SketchyBrush);
|
||||
brushRegistry.register("spraypaint", SpraypaintBrush);
|
||||
brushRegistry.register("crayon", CrayonBrush, {
|
||||
name: "Crayon",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("fur", FurBrush, {
|
||||
name: "Texture",
|
||||
description: "使用纹理图片作为笔刷,支持缩放和透明度",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("ink", InkBrush, {
|
||||
name: "Ink",
|
||||
description: "墨水笔刷,适合书写和绘图",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("", LongfurBrush, {
|
||||
name: "Longfur",
|
||||
description: "长毛发笔刷,适合绘制动物毛皮、草或头发",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("writing", WritingBrush, {
|
||||
name: "Writing",
|
||||
description: "书法笔刷,模拟中国传统书法毛笔效果,具有笔锋和墨色变化",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("marker", MarkerBrush, {
|
||||
name: "Marker",
|
||||
description: "马克笔笔刷,适合粗线条和填充",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("pen", CustomPenBrush, {
|
||||
name: "Pen",
|
||||
description: "自定义钢笔笔刷,适合书写和绘图",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("ribbon", RibbonBrush, {
|
||||
name: "Ribbon",
|
||||
description: "丝带笔刷,适合创建流动的丝带效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
brushRegistry.register("shaded", ShadedBrush, {
|
||||
name: "Shaded",
|
||||
description: "阴影笔刷,适合创建渐变和阴影效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
// brushRegistry.register("sketchy", SketchyBrush);
|
||||
brushRegistry.register("spraypaint", SpraypaintBrush, {
|
||||
name: "Spraypaint",
|
||||
description: "喷漆笔刷,模拟喷漆效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
|
||||
// 注册喷枪笔刷
|
||||
brushRegistry.register(
|
||||
"spray",
|
||||
class SprayBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "spray",
|
||||
name: "喷枪",
|
||||
description: "模拟喷枪效果,创建散点效果",
|
||||
category: "基础笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册喷枪笔刷
|
||||
// brushRegistry.register(
|
||||
// "spray",
|
||||
// class SprayBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "spray",
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// category: "基础笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.SprayBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
return this.brush;
|
||||
}
|
||||
// create() {
|
||||
// this.brush = new fabric.SprayBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
if (options.density !== undefined) {
|
||||
brush.density = options.density;
|
||||
}
|
||||
// if (options.density !== undefined) {
|
||||
// brush.density = options.density;
|
||||
// }
|
||||
|
||||
if (options.randomOpacity !== undefined) {
|
||||
brush.randomOpacity = options.randomOpacity;
|
||||
}
|
||||
// if (options.randomOpacity !== undefined) {
|
||||
// brush.randomOpacity = options.randomOpacity;
|
||||
// }
|
||||
|
||||
if (options.dotWidth !== undefined) {
|
||||
brush.dotWidth = options.dotWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
// if (options.dotWidth !== undefined) {
|
||||
// brush.dotWidth = options.dotWidth;
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// }
|
||||
// );
|
||||
// 注册橡皮擦笔刷
|
||||
brushRegistry.register(
|
||||
"eraser",
|
||||
@@ -187,87 +241,87 @@ export class BrushManager {
|
||||
}
|
||||
);
|
||||
|
||||
// 注册水彩笔刷
|
||||
brushRegistry.register(
|
||||
"watercolor",
|
||||
class WatercolorBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "watercolor",
|
||||
name: "水彩",
|
||||
description: "模拟水彩效果,带有流动感和透明感",
|
||||
category: "特效笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册水彩笔刷
|
||||
// brushRegistry.register(
|
||||
// "watercolor",
|
||||
// class WatercolorBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "watercolor",
|
||||
// name: "水彩",
|
||||
// description: "模拟水彩效果,带有流动感和透明感",
|
||||
// category: "特效笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
// 创建一个自定义的PencilBrush来模拟水彩效果
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
// create() {
|
||||
// // 创建一个自定义的PencilBrush来模拟水彩效果
|
||||
// this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
|
||||
// 水彩效果特有的属性
|
||||
this.brush.globalCompositeOperation = "multiply";
|
||||
this.brush.shadow = new fabric.Shadow({
|
||||
color: this.options.color || "#000",
|
||||
blur: 5,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
});
|
||||
// // 水彩效果特有的属性
|
||||
// this.brush.globalCompositeOperation = "multiply";
|
||||
// this.brush.shadow = new fabric.Shadow({
|
||||
// color: this.options.color || "#000",
|
||||
// blur: 5,
|
||||
// offsetX: 0,
|
||||
// offsetY: 0,
|
||||
// });
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
// 水彩笔刷特有的配置
|
||||
brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
|
||||
}
|
||||
}
|
||||
);
|
||||
// // 水彩笔刷特有的配置
|
||||
// brush.opacity = Math.min(0.5, options.opacity || 0.3); // 默认透明度30%
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// 注册粉笔笔刷
|
||||
brushRegistry.register(
|
||||
"chalk",
|
||||
class ChalkBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "chalk",
|
||||
name: "粉笔",
|
||||
description: "模拟粉笔效果,有颗粒感和不连续性",
|
||||
category: "特效笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
// // 注册粉笔笔刷
|
||||
// brushRegistry.register(
|
||||
// "chalk",
|
||||
// class ChalkBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "chalk",
|
||||
// name: "粉笔",
|
||||
// description: "模拟粉笔效果,有颗粒感和不连续性",
|
||||
// category: "特效笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
|
||||
create() {
|
||||
this.brush = new fabric.PencilBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
// create() {
|
||||
// this.brush = new fabric.PencilBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
|
||||
// 自定义绘画方法来模拟粉笔效果
|
||||
const originalOnMouseMove = this.brush.onMouseMove;
|
||||
this.brush.onMouseMove = function (pointer, options) {
|
||||
// 随机调整坐标位置,增加粉笔质感
|
||||
const jitter = 2;
|
||||
pointer.x += (Math.random() - 0.5) * jitter;
|
||||
pointer.y += (Math.random() - 0.5) * jitter;
|
||||
// // 自定义绘画方法来模拟粉笔效果
|
||||
// const originalOnMouseMove = this.brush.onMouseMove;
|
||||
// this.brush.onMouseMove = function (pointer, options) {
|
||||
// // 随机调整坐标位置,增加粉笔质感
|
||||
// const jitter = 2;
|
||||
// pointer.x += (Math.random() - 0.5) * jitter;
|
||||
// pointer.y += (Math.random() - 0.5) * jitter;
|
||||
|
||||
// 调用原始方法
|
||||
originalOnMouseMove.call(this, pointer, options);
|
||||
};
|
||||
// // 调用原始方法
|
||||
// originalOnMouseMove.call(this, pointer, options);
|
||||
// };
|
||||
|
||||
return this.brush;
|
||||
}
|
||||
// return this.brush;
|
||||
// }
|
||||
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
|
||||
// 粉笔特有的设置
|
||||
brush.strokeDashArray = [5, 5]; // 虚线效果
|
||||
}
|
||||
}
|
||||
);
|
||||
// // 粉笔特有的设置
|
||||
// brush.strokeDashArray = [5, 5]; // 虚线效果
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,6 +625,148 @@ export class BrushManager {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置依赖管理器
|
||||
*/
|
||||
setManagers({ layerManager, commandManager }) {
|
||||
this.layerManager = layerManager;
|
||||
this.commandManager = commandManager;
|
||||
|
||||
// 初始化橡皮擦状态管理器
|
||||
if (this.canvas && this.layerManager) {
|
||||
this.eraserStateManager = new EraserStateManager(
|
||||
this.canvas,
|
||||
this.layerManager
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建橡皮擦
|
||||
* @param {Object} options 橡皮擦选项
|
||||
*/
|
||||
createEraser(options = {}) {
|
||||
if (!this.canvas) {
|
||||
console.error("画布未初始化");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接使用 fabric-with-erasing 库提供的 EraserBrush
|
||||
this.brush = new fabric.EraserBrush(this.canvas);
|
||||
|
||||
// 应用配置
|
||||
this.configure(this.brush, {
|
||||
width: this.brushSize.value,
|
||||
color: this.brushColor.value,
|
||||
opacity: this.brushOpacity.value,
|
||||
inverted: options.inverted || false,
|
||||
...options,
|
||||
});
|
||||
|
||||
// 设置画布为绘图模式
|
||||
this.canvas.isDrawingMode = true;
|
||||
this.canvas.freeDrawingBrush = this.brush;
|
||||
|
||||
// 绑定橡皮擦事件处理器
|
||||
// this._bindEraserEvents();
|
||||
|
||||
console.log("橡皮擦创建成功");
|
||||
return this.brush;
|
||||
} catch (error) {
|
||||
console.error("创建橡皮擦失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定橡皮擦事件处理器
|
||||
* @private
|
||||
*/
|
||||
_bindEraserEvents() {
|
||||
if (!this.canvas || !this.eraserStateManager) return;
|
||||
|
||||
// 监听擦除开始事件
|
||||
this.canvas.on("erasing:start", this._handleErasingStart.bind(this));
|
||||
|
||||
// 监听擦除结束事件
|
||||
this.canvas.on("erasing:end", this._handleErasingEnd.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑橡皮擦事件处理器
|
||||
* @private
|
||||
*/
|
||||
_unbindEraserEvents() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
this.canvas.off("erasing:start", this._handleErasingStart.bind(this));
|
||||
this.canvas.off("erasing:end", this._handleErasingEnd.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理擦除开始事件
|
||||
* @param {Object} e 事件对象
|
||||
* @private
|
||||
*/
|
||||
_handleErasingStart(e) {
|
||||
console.log("橡皮擦开始擦除");
|
||||
|
||||
if (!this.eraserStateManager) return;
|
||||
|
||||
// 标记擦除状态
|
||||
this.isErasingActive = true;
|
||||
this.currentErasedObjects = [];
|
||||
|
||||
// 捕获擦除前的状态
|
||||
this.eraserStateManager.startErasing();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理擦除结束事件
|
||||
* @param {Object} e 事件对象
|
||||
* @private
|
||||
*/
|
||||
_handleErasingEnd(e) {
|
||||
console.log("橡皮擦擦除结束", e);
|
||||
|
||||
if (!this.eraserStateManager || !this.isErasingActive) return;
|
||||
|
||||
// 获取被擦除的对象
|
||||
const affectedObjects = e.targets || [];
|
||||
|
||||
// 创建橡皮擦命令
|
||||
const eraserCommand = this.eraserStateManager.endErasing(affectedObjects);
|
||||
|
||||
// 如果有有效的命令且有命令管理器,执行命令
|
||||
if (eraserCommand && this.commandManager) {
|
||||
// 注意:不需要调用 execute(),因为擦除操作已经完成
|
||||
// 只需要将命令添加到历史记录中以支持撤销
|
||||
this.commandManager.addToHistory(eraserCommand);
|
||||
console.log("橡皮擦操作已添加到命令历史");
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前擦除操作
|
||||
*/
|
||||
cancelErasing() {
|
||||
if (!this.isErasingActive) return;
|
||||
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager.cancelErasing();
|
||||
}
|
||||
|
||||
this.isErasingActive = false;
|
||||
this.currentErasedObjects = [];
|
||||
|
||||
console.log("当前擦除操作已取消");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前笔刷类型
|
||||
* @returns {String} 当前笔刷类型ID
|
||||
@@ -646,7 +842,6 @@ export class BrushManager {
|
||||
|
||||
return this.canvas.freeDrawingBrush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建橡皮擦
|
||||
* @returns {Object} 橡皮擦笔刷
|
||||
@@ -703,9 +898,20 @@ export class BrushManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁资源
|
||||
* 销毁笔刷管理器
|
||||
*/
|
||||
dispose() {
|
||||
// 解绑事件
|
||||
// this._unbindEraserEvents();
|
||||
|
||||
// 取消进行中的擦除操作
|
||||
this.cancelErasing();
|
||||
|
||||
// 清理状态管理器
|
||||
if (this.eraserStateManager) {
|
||||
this.eraserStateManager = null;
|
||||
}
|
||||
|
||||
// 销毁当前笔刷
|
||||
if (this.activeBrush) {
|
||||
this.activeBrush.destroy();
|
||||
|
||||
1748
src/component/Canvas/CanvasEditor/managers/brushes/fabric.brushes.js
Normal file
1748
src/component/Canvas/CanvasEditor/managers/brushes/fabric.brushes.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { BaseBrush } from "../BaseBrush";
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import texturePresetManager from "../TexturePresetManager";
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,72 @@ export class TextureBrush extends BaseBrush {
|
||||
// 创建fabric原生纹理笔刷
|
||||
this.brush = new fabric.PatternBrush(this.canvas);
|
||||
|
||||
// 配置笔刷
|
||||
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
|
||||
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(
|
||||
this.brush
|
||||
);
|
||||
const self = this; // 保存外部this引用
|
||||
|
||||
this.brush._finalizeAndAddPath = function () {
|
||||
console.log("TextureBrush: _finalizeAndAddPath called");
|
||||
const ctx = this.canvas.contextTop;
|
||||
ctx.closePath();
|
||||
|
||||
// 应用点简化(如果PatternBrush支持)
|
||||
if (this.decimate && this._points) {
|
||||
this._points = this.decimatePoints(this._points, this.decimate);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"TextureBrush: points count =",
|
||||
this._points ? this._points.length : 0
|
||||
);
|
||||
|
||||
// 检查是否有有效的路径数据
|
||||
if (!this._points || this._points.length < 2) {
|
||||
// 如果点数不足,直接请求重新渲染
|
||||
console.log("TextureBrush: Not enough points, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const pathData = this.convertPointsToSVGPath(this._points);
|
||||
|
||||
const isEmpty = self._isEmptySVGPath(pathData);
|
||||
console.log("TextureBrush: isEmpty =", isEmpty);
|
||||
|
||||
if (isEmpty) {
|
||||
// 如果路径为空,直接请求重新渲染
|
||||
console.log("TextureBrush: Path is empty, skipping");
|
||||
this.canvas.requestRenderAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// 先触发事件,模拟原生行为
|
||||
const path = this.createPath(pathData);
|
||||
this.canvas.fire("before:path:created", { path: path });
|
||||
|
||||
console.log("TextureBrush: Calling convertToImg");
|
||||
|
||||
// 调用 convertToImg 方法将绘制内容转换为图片
|
||||
if (typeof this.convertToImg === "function") {
|
||||
this.convertToImg();
|
||||
console.log("TextureBrush: convertToImg called successfully");
|
||||
} else {
|
||||
console.warn(
|
||||
"convertToImg method not found, falling back to original behavior"
|
||||
);
|
||||
// 如果没有convertToImg方法,回退到原始行为
|
||||
this.canvas.add(path);
|
||||
this.canvas.fire("path:created", { path: path });
|
||||
this.canvas.clearContext(this.canvas.contextTop);
|
||||
}
|
||||
|
||||
// 重置阴影
|
||||
this._resetShadow();
|
||||
};
|
||||
|
||||
// 配置笔刷基本属性
|
||||
this.configure(this.brush, this.options);
|
||||
|
||||
// 如果有选中的材质,则设置纹理
|
||||
@@ -221,6 +286,11 @@ export class TextureBrush extends BaseBrush {
|
||||
canvasTexture.width = width;
|
||||
canvasTexture.height = height;
|
||||
|
||||
// 应用透明度设置
|
||||
if (this.textureOpacity < 1) {
|
||||
ctx.globalAlpha = this.textureOpacity;
|
||||
}
|
||||
|
||||
// 绘制前应用旋转
|
||||
if (this.textureAngle !== 0) {
|
||||
ctx.save();
|
||||
@@ -233,26 +303,16 @@ export class TextureBrush extends BaseBrush {
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
}
|
||||
|
||||
// 应用透明度
|
||||
if (this.textureOpacity < 1) {
|
||||
ctx.globalAlpha = this.textureOpacity;
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
// 缓存处理后的纹理图像
|
||||
this._processedTextureCanvas = canvasTexture;
|
||||
|
||||
// 创建Pattern对象
|
||||
const pattern = new fabric.Pattern({
|
||||
source: canvasTexture,
|
||||
repeat: this.textureRepeat,
|
||||
});
|
||||
// 使用fabric.js 5.x正确的API方式设置PatternBrush
|
||||
// 直接设置source属性,这是PatternBrush的标准用法
|
||||
this.brush.source = canvasTexture;
|
||||
|
||||
// 设置笔刷源纹理
|
||||
if (typeof this.brush.setSource === "function") {
|
||||
this.brush.setSource(pattern);
|
||||
} else if (typeof this.brush.source === "object") {
|
||||
this.brush.source = pattern;
|
||||
} else if (typeof this.brush.pattern === "object") {
|
||||
this.brush.pattern = pattern;
|
||||
// 通知画布重绘
|
||||
if (this.canvas && this.canvas.requestRenderAll) {
|
||||
this.canvas.requestRenderAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -852,4 +912,37 @@ export class TextureBrush extends BaseBrush {
|
||||
getTextureStats() {
|
||||
return texturePresetManager.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 SVG 路径是否为空
|
||||
* @private
|
||||
* @param {Array} pathData SVG 路径数据
|
||||
* @returns {Boolean} 是否为空路径
|
||||
*/
|
||||
_isEmptySVGPath(pathData) {
|
||||
if (!pathData || pathData.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查路径是否只包含移动命令或者是一个点
|
||||
let hasDrawing = false;
|
||||
let moveCount = 0;
|
||||
|
||||
for (let i = 0; i < pathData.length; i++) {
|
||||
const command = pathData[i];
|
||||
if (command[0] === "M") {
|
||||
moveCount++;
|
||||
} else if (
|
||||
command[0] === "L" ||
|
||||
command[0] === "Q" ||
|
||||
command[0] === "C"
|
||||
) {
|
||||
hasDrawing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果只有移动命令且超过1个,或者没有绘制命令,则认为是空路径
|
||||
return !hasDrawing || (moveCount > 0 && pathData.length <= moveCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ export class CanvasEventManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并图层中的对象为图像以提高性能
|
||||
* 合并图层中的对象为组以提高性能
|
||||
* @param {Object} options 合并选项
|
||||
* @param {fabric.Image} options.fabricImage 新的图像对象
|
||||
* @param {Object} options.activeLayer 当前活动图层
|
||||
@@ -594,7 +594,6 @@ export class CanvasEventManager {
|
||||
console.warn("合并对象失败:没有活动图层");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证是否需要合并
|
||||
const hasExistingObjects =
|
||||
Array.isArray(activeLayer.fabricObjects) &&
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
|
||||
import { LiquifyCPUManager } from "./LiquifyCPUManager";
|
||||
import { LayerType } from "../../utils/layerHelper";
|
||||
|
||||
export class EnhancedLiquifyManager {
|
||||
/**
|
||||
@@ -313,16 +314,40 @@ export class EnhancedLiquifyManager {
|
||||
if (param in this.params) {
|
||||
this.params[param] = value;
|
||||
|
||||
// 同步更新当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
// 同步更新当前渲染器 - 关键修复:确保参数正确传递
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.setParam === "function"
|
||||
) {
|
||||
console.log(`EnhancedLiquifyManager 设置参数: ${param}=${value}`);
|
||||
this.activeRenderer.setParam(param, value);
|
||||
} else {
|
||||
console.warn(
|
||||
`EnhancedLiquifyManager: 无法设置参数 ${param},渲染器未就绪`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(`EnhancedLiquifyManager: 无效参数 ${param}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("EnhancedLiquifyManager 批量设置参数:", params);
|
||||
|
||||
if (params && typeof params === "object") {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.setParam(key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -348,6 +373,36 @@ export class EnhancedLiquifyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.startDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.startDeformation(x, y);
|
||||
}
|
||||
console.log(
|
||||
`开始液化操作,渲染模式=${this.renderMode}, 初始点: (${x}, ${y})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.endDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.endDeformation();
|
||||
}
|
||||
console.log(`结束液化操作,渲染模式=${this.renderMode}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用液化变形
|
||||
* @param {Object} target 目标对象
|
||||
@@ -468,7 +523,9 @@ export class EnhancedLiquifyManager {
|
||||
// 注意:这里不自动切换,因为可能会导致中途渲染结果不一致
|
||||
}
|
||||
}
|
||||
|
||||
setRealtimeUpdater(realtimeUpdater) {
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
}
|
||||
/**
|
||||
* 重置液化操作
|
||||
* @returns {ImageData} 重置后的图像数据
|
||||
@@ -519,7 +576,7 @@ export class EnhancedLiquifyManager {
|
||||
|
||||
// 检查图层是否为空
|
||||
let objectsToCheck = [];
|
||||
if (layer.isBackground || layer.type === "background") {
|
||||
if (layer.isBackground || layer.type === "background" || layer.isFixed) {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (layer.fabricObject) {
|
||||
objectsToCheck = [layer.fabricObject];
|
||||
@@ -548,7 +605,10 @@ export class EnhancedLiquifyManager {
|
||||
objectsToCheck[0].type === "rasterized-layer");
|
||||
|
||||
// 检查是否为组
|
||||
const isGroup = objectsToCheck.some((obj) => obj.type === "group");
|
||||
const isGroup =
|
||||
objectsToCheck.some((obj) => obj.type === "group") ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
layer.children?.length > 0;
|
||||
|
||||
// 如果不是单一图像,需要栅格化
|
||||
const needsRasterization = !isImage || isGroup;
|
||||
@@ -572,26 +632,34 @@ export class EnhancedLiquifyManager {
|
||||
async _getImageData(fabricObject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
// 创建临时canvas - 关键修复:使用原始图像尺寸,不考虑fabric对象的缩放
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = fabricObject.width * fabricObject.scaleX;
|
||||
tempCanvas.height = fabricObject.height * fabricObject.scaleY;
|
||||
// 使用图像的原始尺寸,而不是缩放后的尺寸
|
||||
tempCanvas.width = fabricObject.width;
|
||||
tempCanvas.height = fabricObject.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 如果对象有图像元素
|
||||
if (fabricObject._element) {
|
||||
// 绘制原始尺寸的图像
|
||||
tempCtx.drawImage(
|
||||
fabricObject._element,
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
} else if (fabricObject.getSrc) {
|
||||
// 通过URL创建图像
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
tempCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
@@ -615,6 +683,13 @@ export class EnhancedLiquifyManager {
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
`获取图像数据: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +
|
||||
`对象缩放=(${fabricObject.scaleX}, ${fabricObject.scaleY}), ` +
|
||||
`图像数据尺寸=${imageData.width}x${imageData.height}`
|
||||
);
|
||||
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* 混合液化管理器 - 根据模式智能选择算法
|
||||
*/
|
||||
export class HybridLiquifyManager {
|
||||
constructor(options = {}) {
|
||||
this.config = {
|
||||
gridSize: options.gridSize || 16,
|
||||
maxStrength: options.maxStrength || 200,
|
||||
smoothingIterations: options.smoothingIterations || 1,
|
||||
relaxFactor: options.relaxFactor || 0.1,
|
||||
};
|
||||
|
||||
this.params = {
|
||||
size: 80,
|
||||
pressure: 0.8,
|
||||
distortion: 0,
|
||||
power: 0.8,
|
||||
};
|
||||
|
||||
this.modes = {
|
||||
PUSH: "push",
|
||||
CLOCKWISE: "clockwise",
|
||||
COUNTERCLOCKWISE: "counterclockwise",
|
||||
PINCH: "pinch",
|
||||
EXPAND: "expand",
|
||||
CRYSTAL: "crystal",
|
||||
EDGE: "edge",
|
||||
RECONSTRUCT: "reconstruct",
|
||||
};
|
||||
|
||||
// 定义哪些模式使用像素算法
|
||||
this.pixelModes = new Set([
|
||||
this.modes.CLOCKWISE,
|
||||
this.modes.COUNTERCLOCKWISE,
|
||||
this.modes.CRYSTAL,
|
||||
this.modes.EDGE,
|
||||
]);
|
||||
|
||||
// 定义哪些模式使用网格算法
|
||||
this.meshModes = new Set([
|
||||
this.modes.PUSH,
|
||||
this.modes.PINCH,
|
||||
this.modes.EXPAND,
|
||||
this.modes.RECONSTRUCT,
|
||||
]);
|
||||
|
||||
this.currentMode = this.modes.PUSH;
|
||||
this.originalImageData = null;
|
||||
this.currentImageData = null;
|
||||
this.mesh = null;
|
||||
this.initialized = false;
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
|
||||
// 持续按压相关状态
|
||||
this.pressStartTime = 0;
|
||||
this.pressDuration = 0;
|
||||
this.accumulatedRotation = 0;
|
||||
this.accumulatedScale = 0;
|
||||
this.isHolding = false;
|
||||
this.continuousTimer = null;
|
||||
|
||||
// 鼠标状态
|
||||
this.initialMouseX = 0;
|
||||
this.initialMouseY = 0;
|
||||
this.currentMouseX = 0;
|
||||
this.currentMouseY = 0;
|
||||
this.isDragging = false;
|
||||
this.dragDistance = 0;
|
||||
}
|
||||
|
||||
// ...existing initialization methods...
|
||||
|
||||
/**
|
||||
* 应用液化变形 - 智能选择算法
|
||||
*/
|
||||
applyDeformation(x, y) {
|
||||
if (!this.initialized || !this.originalImageData) {
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
// 更新鼠标位置
|
||||
this.currentMouseX = x;
|
||||
this.currentMouseY = y;
|
||||
|
||||
const { size, pressure, power } = this.params;
|
||||
const radius = size;
|
||||
const strength = pressure * power;
|
||||
|
||||
// 根据模式选择算法
|
||||
if (this.pixelModes.has(this.currentMode)) {
|
||||
return this._applyPixelDeformation(x, y, radius, strength);
|
||||
} else if (this.meshModes.has(this.currentMode)) {
|
||||
return this._applyMeshDeformation(x, y, radius, strength);
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级液化算法 - 适用于旋转、水晶、边缘模式
|
||||
*/
|
||||
_applyPixelDeformation(centerX, centerY, radius, strength) {
|
||||
const data = this.currentImageData.data;
|
||||
const width = this.currentImageData.width;
|
||||
const height = this.currentImageData.height;
|
||||
const tempData = new Uint8ClampedArray(data);
|
||||
|
||||
const processRadius = Math.min(radius, Math.min(width, height) / 2);
|
||||
|
||||
// 计算影响区域边界
|
||||
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + processRadius));
|
||||
const minY = Math.max(0, Math.floor(centerY - processRadius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + processRadius));
|
||||
|
||||
switch (this.currentMode) {
|
||||
case this.modes.CLOCKWISE:
|
||||
case this.modes.COUNTERCLOCKWISE:
|
||||
this._applyPixelRotation(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength,
|
||||
this.currentMode === this.modes.CLOCKWISE
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.CRYSTAL:
|
||||
this._applyPixelCrystal(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.EDGE:
|
||||
this._applyPixelEdge(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级旋转算法
|
||||
*/
|
||||
_applyPixelRotation(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength,
|
||||
clockwise
|
||||
) {
|
||||
// 计算旋转角度
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
|
||||
const baseRotationSpeed = 0.015;
|
||||
const rotationAngle =
|
||||
(clockwise ? 1 : -1) *
|
||||
baseRotationSpeed *
|
||||
strength *
|
||||
(1.0 + timeFactor * 0.3);
|
||||
|
||||
this.accumulatedRotation += rotationAngle;
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
// 距离衰减:内圈快,外圈慢
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = Math.pow(1 - normalizedDistance, 2); // 二次衰减
|
||||
|
||||
// 计算旋转后的源位置
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const newAngle = angle + this.accumulatedRotation * falloff;
|
||||
|
||||
const sourceX = centerX + Math.cos(newAngle) * distance;
|
||||
const sourceY = centerY + Math.sin(newAngle) * distance;
|
||||
|
||||
// 双线性插值采样
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级水晶效果
|
||||
*/
|
||||
_applyPixelCrystal(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
|
||||
const distortionStrength = strength * (1.0 + timeFactor * 0.5);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const crystalRadius = normalizedDistance;
|
||||
|
||||
// 多层波浪扭曲
|
||||
const wave1 = Math.sin(angle * 8 + this.pressDuration * 0.005) * 0.6;
|
||||
const wave2 = Math.cos(angle * 12 + this.pressDuration * 0.003) * 0.4;
|
||||
const waveAngle =
|
||||
angle + (wave1 + wave2) * distortionStrength * falloff;
|
||||
|
||||
const radialMod =
|
||||
1 +
|
||||
Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) *
|
||||
0.3;
|
||||
const modDistance = distance * radialMod;
|
||||
|
||||
const sourceX = centerX + Math.cos(waveAngle) * modDistance;
|
||||
const sourceY = centerY + Math.sin(waveAngle) * modDistance;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
const factor = falloff * distortionStrength * 0.7;
|
||||
|
||||
// 混合原始颜色和扭曲颜色
|
||||
const originalIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = Math.round(
|
||||
srcData[originalIdx] * (1 - factor) + color[0] * factor
|
||||
);
|
||||
dstData[targetIdx + 1] = Math.round(
|
||||
srcData[originalIdx + 1] * (1 - factor) + color[1] * factor
|
||||
);
|
||||
dstData[targetIdx + 2] = Math.round(
|
||||
srcData[originalIdx + 2] * (1 - factor) + color[2] * factor
|
||||
);
|
||||
dstData[targetIdx + 3] = srcData[originalIdx + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级边缘效果
|
||||
*/
|
||||
_applyPixelEdge(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
|
||||
const edgeStrength = strength * (1.0 + timeFactor * 0.4);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const edgeRadius = normalizedDistance;
|
||||
|
||||
const edgeWave =
|
||||
Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) *
|
||||
Math.cos(angle * 6 + this.pressDuration * 0.002);
|
||||
const perpAngle = angle + Math.PI / 2;
|
||||
|
||||
const edgeFactor = edgeWave * falloff * edgeStrength * 0.5;
|
||||
const offsetX = Math.cos(perpAngle) * edgeFactor;
|
||||
const offsetY = Math.sin(perpAngle) * edgeFactor;
|
||||
|
||||
const sourceX = x + offsetX;
|
||||
const sourceY = y + offsetY;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 双线性插值采样
|
||||
*/
|
||||
_bilinearSample(data, width, height, x, y) {
|
||||
if (x < 0 || x >= width - 1 || y < 0 || y >= height - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x1 = Math.floor(x);
|
||||
const y1 = Math.floor(y);
|
||||
const x2 = x1 + 1;
|
||||
const y2 = y1 + 1;
|
||||
|
||||
const fx = x - x1;
|
||||
const fy = y - y1;
|
||||
|
||||
const getPixel = (px, py) => {
|
||||
const idx = (py * width + px) * 4;
|
||||
return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]];
|
||||
};
|
||||
|
||||
const p1 = getPixel(x1, y1);
|
||||
const p2 = getPixel(x2, y1);
|
||||
const p3 = getPixel(x1, y2);
|
||||
const p4 = getPixel(x2, y2);
|
||||
|
||||
return [
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[0] +
|
||||
fx * (1 - fy) * p2[0] +
|
||||
(1 - fx) * fy * p3[0] +
|
||||
fx * fy * p4[0]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[1] +
|
||||
fx * (1 - fy) * p2[1] +
|
||||
(1 - fx) * fy * p3[1] +
|
||||
fx * fy * p4[1]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[2] +
|
||||
fx * (1 - fy) * p2[2] +
|
||||
(1 - fx) * fy * p3[2] +
|
||||
fx * fy * p4[2]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[3] +
|
||||
fx * (1 - fy) * p2[3] +
|
||||
(1 - fx) * fy * p3[3] +
|
||||
fx * fy * p4[3]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格液化算法 - 适用于推拉、捏合、展开模式
|
||||
*/
|
||||
_applyMeshDeformation(x, y, radius, strength) {
|
||||
if (!this.mesh) return this.currentImageData;
|
||||
|
||||
// 使用现有的网格算法处理推拉、捏合、展开
|
||||
const mode = this.currentMode;
|
||||
const { distortion } = this.params;
|
||||
|
||||
this._applyDeformation(x, y, radius, strength, mode, distortion);
|
||||
|
||||
if (this.config.smoothingIterations > 0) {
|
||||
this._smoothMesh();
|
||||
}
|
||||
|
||||
return this._applyMeshToImage();
|
||||
}
|
||||
|
||||
// ...existing mesh methods...
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ export class LiquifyManager {
|
||||
meshResolution: options.meshResolution || 64,
|
||||
// 根据环境选择合适的渲染模式
|
||||
forceCPU: true, // 默认不强制使用CPU
|
||||
forceWebGL: false, // 优先使用WebGL模式
|
||||
forceWebGL: true, // 优先使用WebGL模式
|
||||
webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL
|
||||
layerManager: options.layerManager || null,
|
||||
canvas: options.canvas || null,
|
||||
@@ -87,9 +87,19 @@ export class LiquifyManager {
|
||||
* @param {Number} value 参数值
|
||||
*/
|
||||
setParam(param, value) {
|
||||
console.log(`LiquifyManager 设置参数: ${param}=${value}`);
|
||||
return this.enhancedManager.setParam(param, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("LiquifyManager 批量设置参数:", params);
|
||||
return this.enhancedManager.setParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -120,21 +130,14 @@ export class LiquifyManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保设置正确的模式和参数
|
||||
if (mode) {
|
||||
this.enhancedManager.setMode(mode);
|
||||
}
|
||||
console.log(
|
||||
`LiquifyManager.applyLiquify: 模式=${mode}, 坐标=(${x}, ${y}), 参数=`,
|
||||
params
|
||||
);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.enhancedManager.setParam(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 应用液化变形
|
||||
console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
|
||||
try {
|
||||
// 直接调用EnhancedLiquifyManager的applyLiquify方法
|
||||
// 避免重复设置参数,让EnhancedLiquifyManager处理参数设置
|
||||
const resultData = await this.enhancedManager.applyLiquify(
|
||||
targetObject,
|
||||
mode,
|
||||
@@ -146,6 +149,13 @@ export class LiquifyManager {
|
||||
// 确保返回结果数据
|
||||
if (!resultData) {
|
||||
console.warn("液化变形没有返回结果数据");
|
||||
} else {
|
||||
console.log(
|
||||
"✅ 液化变形成功,返回图像数据尺寸:",
|
||||
resultData.width,
|
||||
"x",
|
||||
resultData.height
|
||||
);
|
||||
}
|
||||
|
||||
return resultData;
|
||||
@@ -180,6 +190,22 @@ export class LiquifyManager {
|
||||
return this.enhancedManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
return this.enhancedManager.startLiquifyOperation(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
return this.enhancedManager.endLiquifyOperation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 液化实时更新器
|
||||
* 负责高效地更新液化效果到画布上,避免频繁创建fabric对象导致的性能问题
|
||||
*/
|
||||
export class LiquifyRealTimeUpdater {
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvas = canvas;
|
||||
this.targetObject = null;
|
||||
this.isUpdating = false;
|
||||
this.updateQueue = [];
|
||||
this.lastUpdateTime = 0;
|
||||
this.currImage = options.currImage || { value: null };
|
||||
|
||||
// 配置选项
|
||||
this.config = {
|
||||
throttleTime: options.throttleTime || 16, // 60fps
|
||||
maxQueueSize: options.maxQueueSize || 5,
|
||||
useDirectUpdate: options.useDirectUpdate !== false, // 默认启用直接更新
|
||||
imageQuality: options.imageQuality || 1.0, // 图像质量 (0.1-1.0)
|
||||
skipRenderDuringDrag: options.skipRenderDuringDrag || false, // 拖拽时跳过渲染
|
||||
};
|
||||
|
||||
// 临时canvas用于快速渲染
|
||||
this.tempCanvas = document.createElement("canvas");
|
||||
this.tempCtx = this.tempCanvas.getContext("2d");
|
||||
|
||||
// 高质量canvas用于最终输出
|
||||
this.highQualityCanvas = document.createElement("canvas");
|
||||
this.highQualityCtx = this.highQualityCanvas.getContext("2d");
|
||||
|
||||
// 当前缓存的图像数据
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
this.renderingScheduled = false;
|
||||
|
||||
// 优化Canvas画布渲染设置
|
||||
this.canvas.renderOnAddRemove = false; // 禁用自动渲染
|
||||
this.canvas.skipOffscreen = true; // 跳过离屏元素渲染
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置目标对象
|
||||
* @param {Object} fabricObject fabric图像对象
|
||||
*/
|
||||
setTargetObject(fabricObject) {
|
||||
this.targetObject = fabricObject;
|
||||
if (fabricObject && fabricObject._element) {
|
||||
// 设置临时canvas尺寸
|
||||
this.tempCanvas.width = fabricObject.width;
|
||||
this.tempCanvas.height = fabricObject.height;
|
||||
|
||||
// 设置高质量canvas尺寸
|
||||
this.highQualityCanvas.width = fabricObject.width;
|
||||
this.highQualityCanvas.height = fabricObject.height;
|
||||
|
||||
// 配置高质量渲染上下文
|
||||
this.highQualityCtx.imageSmoothingEnabled = true;
|
||||
this.highQualityCtx.imageSmoothingQuality = "high";
|
||||
|
||||
// 配置临时canvas上下文(快速渲染)
|
||||
this.tempCtx.imageSmoothingEnabled = false; // 快速模式关闭平滑
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时更新图像数据到画布
|
||||
* @param {ImageData} imageData 新的图像数据
|
||||
* @param {Boolean} isDrawing 是否正在绘制(拖拽过程中)
|
||||
* @returns {Promise} 更新完成的Promise
|
||||
*/
|
||||
async updateImage(imageData, isDrawing = false) {
|
||||
if (!this.targetObject || !imageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 节流控制
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdateTime < this.config.throttleTime && isDrawing) {
|
||||
// 在绘制过程中进行节流,缓存最新的图像数据
|
||||
this.pendingImageData = imageData;
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdateTime = now;
|
||||
|
||||
if (isDrawing && this.config.useDirectUpdate) {
|
||||
// 拖拽过程中使用快速更新(降低质量以提高性能)
|
||||
this._fastUpdate(imageData);
|
||||
} else {
|
||||
// 拖拽结束后使用完整更新(最高质量)
|
||||
await this._fullUpdate(imageData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能图像质量更新
|
||||
* 根据图像尺寸和设备性能动态调整质量
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @param {Boolean} isDrawing 是否正在绘制
|
||||
* @private
|
||||
*/
|
||||
_getOptimalQuality(imageData, isDrawing) {
|
||||
const pixelCount = imageData.width * imageData.height;
|
||||
|
||||
if (isDrawing) {
|
||||
// 拖拽时根据图像大小调整质量
|
||||
if (pixelCount > 1000000) {
|
||||
// 大于1M像素
|
||||
return 0.7;
|
||||
} else if (pixelCount > 500000) {
|
||||
// 大于500K像素
|
||||
return 0.8;
|
||||
} else {
|
||||
return 0.9;
|
||||
}
|
||||
} else {
|
||||
// 拖拽结束时始终使用最高质量
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速更新 - 直接修改现有对象的图像源
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
_fastUpdate(imageData) {
|
||||
if (!this.targetObject || !this.targetObject._element) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 将ImageData渲染到临时canvas(快速模式)
|
||||
this.tempCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 获取智能质量设置
|
||||
const quality = this._getOptimalQuality(imageData, true);
|
||||
|
||||
// 直接更新fabric对象的图像源(使用PNG格式保持质量)
|
||||
const targetElement = this.targetObject._element;
|
||||
|
||||
// 方案1: 直接设置src属性(最高性能)
|
||||
const dataURL = this.tempCanvas.toDataURL("image/png", quality);
|
||||
|
||||
if (targetElement.src !== dataURL) {
|
||||
targetElement.src = dataURL;
|
||||
|
||||
// 关键优化:直接设置fabric对象为脏状态,但不立即渲染
|
||||
// this.targetObject.dirty = false; // 标记为不需要立即渲染
|
||||
// this.canvas.renderOnAddRemove = true; // 恢复自动渲染
|
||||
// this.renderingScheduled = false; // 重置渲染调度状态
|
||||
this?.scheduleRender?.(); // 调度一次渲染
|
||||
// 使用requestAnimationFrame进行批量渲染优化
|
||||
// if (!this.renderingScheduled && !this.config.skipRenderDuringDrag) {
|
||||
// this.renderingScheduled = true;
|
||||
// requestIdleCallback(() => {
|
||||
// this.canvas.renderAll();
|
||||
// this.renderingScheduled = false;
|
||||
// });
|
||||
// }
|
||||
} else {
|
||||
console.warn(
|
||||
"=================快速更新液化效果时,图像数据未变化,跳过更新"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("快速更新液化效果失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
getImageData(imageData) {
|
||||
// 使用高质量canvas进行最终渲染
|
||||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 生成高质量DataURL(PNG格式,最大质量)
|
||||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整更新 - 创建新的fabric对象
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
async _fullUpdate(imageData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 使用高质量canvas进行最终渲染
|
||||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 生成高质量DataURL(PNG格式,最大质量)
|
||||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||||
|
||||
// 如果DataURL没有变化,跳过更新
|
||||
if (this.cachedDataURL === dataURL) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedDataURL = dataURL;
|
||||
|
||||
// 创建新的fabric图像对象,保持最高质量
|
||||
fabric.Image.fromURL(
|
||||
dataURL,
|
||||
(newImg) => {
|
||||
try {
|
||||
if (!this.targetObject) {
|
||||
console.warn("目标对象为空,跳过更新");
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存原对象信息用于智能查找
|
||||
const originalObjId = this.targetObject.id;
|
||||
const originalObjLayerId = this.targetObject.layerId;
|
||||
|
||||
// 保留原对象的所有变换属性
|
||||
const originalObj = this.targetObject;
|
||||
newImg.set({
|
||||
left: originalObj.left,
|
||||
top: originalObj.top,
|
||||
scaleX: originalObj.scaleX,
|
||||
scaleY: originalObj.scaleY,
|
||||
angle: originalObj.angle,
|
||||
flipX: originalObj.flipX,
|
||||
flipY: originalObj.flipY,
|
||||
opacity: originalObj.opacity,
|
||||
originX: originalObj.originX,
|
||||
originY: originalObj.originY,
|
||||
id: originalObj.id,
|
||||
name: originalObj.name,
|
||||
layerId: originalObj.layerId,
|
||||
selected: false,
|
||||
evented: originalObj.evented,
|
||||
});
|
||||
|
||||
// 临时禁用画布自动渲染
|
||||
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
|
||||
// 智能查找和替换canvas上的对象
|
||||
const allObjects = this.canvas.getObjects();
|
||||
let targetIndex = allObjects.indexOf(originalObj);
|
||||
|
||||
// 如果直接查找失败,尝试通过ID查找
|
||||
if (targetIndex === -1 && originalObjId) {
|
||||
targetIndex = allObjects.findIndex(
|
||||
(obj) => obj.id === originalObjId
|
||||
);
|
||||
if (targetIndex !== -1) {
|
||||
console.log(`通过ID找到目标对象: ${originalObjId}`);
|
||||
// 更新目标对象引用
|
||||
this.targetObject = allObjects[targetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果通过ID查找仍然失败,尝试通过图层ID查找
|
||||
if (targetIndex === -1 && originalObjLayerId) {
|
||||
targetIndex = allObjects.findIndex(
|
||||
(obj) => obj.layerId === originalObjLayerId
|
||||
);
|
||||
if (targetIndex !== -1) {
|
||||
console.log(`通过图层ID找到目标对象: ${originalObjLayerId}`);
|
||||
// 更新目标对象引用
|
||||
this.targetObject = allObjects[targetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
// 找到目标对象,执行替换
|
||||
this.canvas.remove(this.targetObject);
|
||||
this.canvas.insertAt(newImg, targetIndex);
|
||||
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
|
||||
// 更新目标对象引用
|
||||
this.targetObject = newImg;
|
||||
|
||||
// 一次性重新渲染画布
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log(`✅ 液化对象更新成功,位置: ${targetIndex}`);
|
||||
resolve(newImg);
|
||||
} else {
|
||||
// 如果在画布中找不到对象,可能对象已被移除或引用已更新
|
||||
console.warn(
|
||||
"在画布中找不到目标对象,可能已被其他操作移除或替换"
|
||||
);
|
||||
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
|
||||
// 尝试添加新对象到画布末尾
|
||||
this.canvas.add(newImg);
|
||||
this.targetObject = newImg;
|
||||
this.canvas.renderAll();
|
||||
|
||||
console.log("🔄 已将新对象添加到画布末尾");
|
||||
resolve(newImg);
|
||||
}
|
||||
} catch (error) {
|
||||
// 恢复自动渲染设置
|
||||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||||
console.error("更新fabric对象时出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
); // 确保跨域支持
|
||||
} catch (error) {
|
||||
console.error("完整更新过程出错:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理待处理的图像数据
|
||||
* 在拖拽结束后调用,处理可能积压的更新
|
||||
*/
|
||||
async processPendingUpdates() {
|
||||
if (this.pendingImageData && !this.isUpdating) {
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
await this._fullUpdate(this.pendingImageData);
|
||||
this.pendingImageData = null;
|
||||
} catch (error) {
|
||||
console.error("处理待处理更新失败:", error);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this.targetObject = null;
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
this.updateQueue.length = 0;
|
||||
|
||||
// 清理临时canvas
|
||||
if (this.tempCanvas) {
|
||||
this.tempCanvas.width = 0;
|
||||
this.tempCanvas.height = 0;
|
||||
this.tempCanvas = null;
|
||||
this.tempCtx = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
* @returns {Object} 当前的fabric对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.targetObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制进行完整更新
|
||||
* @param {ImageData} imageData 图像数据
|
||||
*/
|
||||
async forceFullUpdate(imageData) {
|
||||
return this._fullUpdate(imageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用拖拽模式 - 暂停渲染以提高性能
|
||||
*/
|
||||
enableDragMode() {
|
||||
this.config.skipRenderDuringDrag = true;
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
console.log("🚀 启用拖拽优化模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用拖拽模式 - 恢复正常渲染
|
||||
*/
|
||||
disableDragMode() {
|
||||
this.config.skipRenderDuringDrag = false;
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
|
||||
// 执行一次完整渲染
|
||||
this.canvas.renderAll();
|
||||
console.log("✅ 恢复正常渲染模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.targetObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图像质量
|
||||
* @param {Number} quality 质量值 (0.1-1.0)
|
||||
*/
|
||||
setImageQuality(quality) {
|
||||
this.config.imageQuality = Math.max(0.1, Math.min(1.0, quality));
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化的批量渲染方法
|
||||
*/
|
||||
scheduleRender() {
|
||||
if (!this.renderingScheduled) {
|
||||
this.renderingScheduled = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.canvas.renderAll();
|
||||
this.renderingScheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
// 恢复canvas设置
|
||||
this.canvas.renderOnAddRemove = true;
|
||||
|
||||
// 清理缓存
|
||||
this.cachedDataURL = null;
|
||||
this.pendingImageData = null;
|
||||
|
||||
// 清理canvas
|
||||
if (this.tempCanvas) {
|
||||
this.tempCanvas.width = 0;
|
||||
this.tempCanvas.height = 0;
|
||||
}
|
||||
|
||||
if (this.highQualityCanvas) {
|
||||
this.highQualityCanvas.width = 0;
|
||||
this.highQualityCanvas.height = 0;
|
||||
}
|
||||
|
||||
console.log("🧹 液化实时更新器资源已清理");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 液化面板状态管理器
|
||||
* 负责管理液化操作的状态、性能优化和用户反馈
|
||||
*/
|
||||
export class LiquifyStateManager {
|
||||
constructor(canvas, realtimeUpdater) {
|
||||
this.canvas = canvas;
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
|
||||
// 状态管理
|
||||
this.isOperating = false;
|
||||
this.isDragging = false;
|
||||
this.operationCount = 0;
|
||||
this.startTime = null;
|
||||
|
||||
// 性能监控
|
||||
this.performanceMetrics = {
|
||||
totalOperations: 0,
|
||||
totalTime: 0,
|
||||
averageTime: 0,
|
||||
maxTime: 0,
|
||||
minTime: Infinity,
|
||||
lastOperationTime: 0,
|
||||
};
|
||||
|
||||
// 用户反馈
|
||||
this.feedbackEnabled = true;
|
||||
this.cursorCache = new Map();
|
||||
|
||||
// 设备性能检测
|
||||
this.devicePerformance = this._detectDevicePerformance();
|
||||
|
||||
console.log(
|
||||
"🎯 液化状态管理器已初始化,设备性能等级:",
|
||||
this.devicePerformance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作
|
||||
*/
|
||||
startOperation() {
|
||||
if (this.isOperating) return;
|
||||
|
||||
this.isOperating = true;
|
||||
this.startTime = performance.now();
|
||||
|
||||
// 根据设备性能调整设置
|
||||
this._adjustPerformanceSettings();
|
||||
|
||||
// 显示操作反馈
|
||||
this._showOperationFeedback();
|
||||
|
||||
console.log("🚀 开始液化操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始拖拽
|
||||
*/
|
||||
startDrag() {
|
||||
if (this.isDragging) return;
|
||||
|
||||
this.isDragging = true;
|
||||
|
||||
// 优化拖拽性能
|
||||
this.realtimeUpdater?.enableDragMode();
|
||||
|
||||
// 更新鼠标样式
|
||||
this._updateCursor("liquifying");
|
||||
|
||||
// 禁用不必要的画布功能
|
||||
this._disableCanvasFeatures();
|
||||
|
||||
console.log("🖱️ 开始拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束拖拽
|
||||
*/
|
||||
async endDrag() {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
// 恢复鼠标样式
|
||||
this._updateCursor("default");
|
||||
|
||||
// 恢复画布功能
|
||||
this._enableCanvasFeatures();
|
||||
|
||||
// 处理待处理的更新
|
||||
if (this.realtimeUpdater) {
|
||||
try {
|
||||
await this.realtimeUpdater.processPendingUpdates();
|
||||
} catch (error) {
|
||||
console.error("处理待处理更新失败:", error);
|
||||
} finally {
|
||||
this.isDragging = false;
|
||||
// 恢复正常模式
|
||||
this.realtimeUpdater?.disableDragMode();
|
||||
|
||||
// 结束液化操作 添加结果到命令中 更新当前激活图层对象
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ 结束拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endOperation() {
|
||||
if (!this.isOperating) return;
|
||||
|
||||
const operationTime = performance.now() - this.startTime;
|
||||
this._updatePerformanceMetrics(operationTime);
|
||||
|
||||
this.isOperating = false;
|
||||
this.operationCount++;
|
||||
|
||||
// 隐藏操作反馈
|
||||
this._hideOperationFeedback();
|
||||
|
||||
console.log(`⏱️ 液化操作完成,耗时: ${operationTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录单次变形操作
|
||||
*/
|
||||
recordDeformation(operationTime) {
|
||||
this.performanceMetrics.totalOperations++;
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime /
|
||||
this.performanceMetrics.totalOperations;
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.lastOperationTime = operationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作性能指标
|
||||
* @param {Object} metrics 性能指标对象
|
||||
*/
|
||||
recordOperationMetrics(metrics) {
|
||||
const {
|
||||
operationTime,
|
||||
operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
} = metrics;
|
||||
|
||||
// 记录基础性能数据
|
||||
this.recordDeformation(operationTime);
|
||||
|
||||
// 记录详细操作信息
|
||||
this.performanceMetrics.lastOperation = {
|
||||
type: operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 根据性能数据动态调整设置
|
||||
this._adaptivePerformanceOptimization(operationTime);
|
||||
|
||||
console.log(
|
||||
`📊 记录性能指标: ${operationType}/${mode}, 耗时: ${operationTime.toFixed(
|
||||
2
|
||||
)}ms, 渲染模式: ${renderMode}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应性能优化
|
||||
* @param {Number} operationTime 操作耗时
|
||||
* @private
|
||||
*/
|
||||
_adaptivePerformanceOptimization(operationTime) {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
// 如果操作耗时过长,动态降低质量或增加节流时间
|
||||
if (operationTime > 50 && this.devicePerformance !== "high") {
|
||||
// 降低图像质量
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality > 0.7) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.max(0.7, currentQuality - 0.1)
|
||||
);
|
||||
console.log("⚡ 自动降低图像质量以提升性能");
|
||||
}
|
||||
|
||||
// 增加节流时间
|
||||
if (this.realtimeUpdater.config.throttleTime < 33) {
|
||||
this.realtimeUpdater.config.throttleTime = Math.min(
|
||||
33,
|
||||
this.realtimeUpdater.config.throttleTime + 8
|
||||
);
|
||||
console.log("⏱️ 自动增加节流时间以提升性能");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果性能很好,可以适当提高质量
|
||||
if (operationTime < 20 && this.devicePerformance === "high") {
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality < 1.0) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.min(1.0, currentQuality + 0.05)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
*/
|
||||
getPerformanceReport() {
|
||||
return {
|
||||
...this.performanceMetrics,
|
||||
devicePerformance: this.devicePerformance,
|
||||
fps: this._calculateFPS(),
|
||||
recommendations: this._generateRecommendations(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户反馈
|
||||
*/
|
||||
setFeedbackEnabled(enabled) {
|
||||
this.feedbackEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this._enableCanvasFeatures();
|
||||
this._updateCursor("default");
|
||||
this.cursorCache.clear();
|
||||
|
||||
console.log("🧹 液化状态管理器已清理");
|
||||
}
|
||||
|
||||
// === 私有方法 ===
|
||||
|
||||
/**
|
||||
* 检测设备性能
|
||||
*/
|
||||
_detectDevicePerformance() {
|
||||
// 检测硬件并发数
|
||||
const cores = navigator.hardwareConcurrency || 4;
|
||||
|
||||
// 检测内存
|
||||
const memory = navigator.deviceMemory || 4;
|
||||
|
||||
// 检测连接类型
|
||||
const connection = navigator.connection;
|
||||
const effectiveType = connection?.effectiveType || "4g";
|
||||
|
||||
// 简单的性能评分算法
|
||||
let score = 0;
|
||||
score += cores * 10;
|
||||
score += memory * 5;
|
||||
|
||||
if (effectiveType === "4g") score += 10;
|
||||
else if (effectiveType === "3g") score += 5;
|
||||
|
||||
// 性能等级
|
||||
if (score >= 70) return "high";
|
||||
if (score >= 40) return "medium";
|
||||
return "low";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备性能调整设置
|
||||
*/
|
||||
_adjustPerformanceSettings() {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
switch (this.devicePerformance) {
|
||||
case "high":
|
||||
this.realtimeUpdater.setImageQuality(1.0);
|
||||
this.realtimeUpdater.config.throttleTime = 8; // 120fps
|
||||
break;
|
||||
case "medium":
|
||||
this.realtimeUpdater.setImageQuality(0.9);
|
||||
this.realtimeUpdater.config.throttleTime = 16; // 60fps
|
||||
break;
|
||||
case "low":
|
||||
this.realtimeUpdater.setImageQuality(0.8);
|
||||
this.realtimeUpdater.config.throttleTime = 33; // 30fps
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示操作反馈
|
||||
*/
|
||||
_showOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
// 添加视觉反馈(可以是加载动画、进度条等)
|
||||
document.body.style.cursor = "wait";
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏操作反馈
|
||||
*/
|
||||
_hideOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
document.body.style.cursor = "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新鼠标样式
|
||||
*/
|
||||
_updateCursor(type) {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
const cursors = {
|
||||
default: "default",
|
||||
liquifying: "crosshair",
|
||||
wait: "wait",
|
||||
"not-allowed": "not-allowed",
|
||||
};
|
||||
|
||||
const cursor = cursors[type] || "default";
|
||||
|
||||
if (this.canvas && this.canvas.upperCanvasEl) {
|
||||
this.canvas.upperCanvasEl.style.cursor = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用画布功能以提高性能
|
||||
*/
|
||||
_disableCanvasFeatures() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 保存原始设置
|
||||
this._originalSettings = {
|
||||
renderOnAddRemove: this.canvas.renderOnAddRemove,
|
||||
skipOffscreen: this.canvas.skipOffscreen,
|
||||
enableRetinaScaling: this.canvas.enableRetinaScaling,
|
||||
};
|
||||
|
||||
// 应用性能优化设置
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
this.canvas.skipOffscreen = true;
|
||||
|
||||
// 低性能设备关闭高分辨率支持
|
||||
if (this.devicePerformance === "low") {
|
||||
this.canvas.enableRetinaScaling = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复画布功能
|
||||
*/
|
||||
_enableCanvasFeatures() {
|
||||
if (!this.canvas || !this._originalSettings) return;
|
||||
|
||||
// 恢复原始设置
|
||||
this.canvas.renderOnAddRemove = this._originalSettings.renderOnAddRemove;
|
||||
this.canvas.skipOffscreen = this._originalSettings.skipOffscreen;
|
||||
this.canvas.enableRetinaScaling =
|
||||
this._originalSettings.enableRetinaScaling;
|
||||
|
||||
this._originalSettings = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新性能指标
|
||||
*/
|
||||
_updatePerformanceMetrics(operationTime) {
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime / (this.operationCount + 1);
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算FPS
|
||||
*/
|
||||
_calculateFPS() {
|
||||
if (this.performanceMetrics.averageTime === 0) return 0;
|
||||
return Math.round(1000 / this.performanceMetrics.averageTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成性能建议
|
||||
*/
|
||||
_generateRecommendations() {
|
||||
const recommendations = [];
|
||||
|
||||
if (this.performanceMetrics.averageTime > 50) {
|
||||
recommendations.push("操作响应较慢,建议降低图像尺寸或关闭高质量模式");
|
||||
}
|
||||
|
||||
if (this.devicePerformance === "low") {
|
||||
recommendations.push("检测到低性能设备,已自动启用性能优化模式");
|
||||
}
|
||||
|
||||
const fps = this._calculateFPS();
|
||||
if (fps < 30) {
|
||||
recommendations.push("帧率较低,建议减少同时进行的操作或降低液化强度");
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* 小地图管理器类
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import { fabric } from "fabric-with-all";
|
||||
import { fabric } from "fabric-with-all";
|
||||
import { generateId } from "../../utils/helper";
|
||||
import { OperationType } from "../../utils/layerHelper";
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user