接入画布

This commit is contained in:
X1627315083
2025-06-09 10:25:54 +08:00
parent 87a08f5f8f
commit c266967f16
157 changed files with 43833 additions and 1571 deletions

View File

@@ -0,0 +1,589 @@
import { Command } from "./Command";
//import { fabric } from "fabric-with-all";
/**
* 创建背景图层命令
*/
export class CreateBackgroundLayerCommand extends Command {
constructor(options) {
super({
name: "创建背景图层",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.backgroundLayer = options.backgroundLayer;
this.canvasManager = options.canvasManager;
this.historyManager = options.historyManager;
this.beforeLayers = [...this.layers.value]; // 备份原图层列表
}
execute() {
// 检查是否已经存在背景图层
const existingBgLayer = this.layers.value.find(
(layer) => layer.isBackground
);
if (existingBgLayer) {
console.warn("已存在背景层,不重复创建");
return existingBgLayer.id;
}
// 创建背景矩形对象
const bgObject = this._createBackgroundObject();
// 将背景对象添加到图层中
this.backgroundLayer.fabricObject = bgObject;
// 添加图层到最底部
this.layers.value.push(this.backgroundLayer);
// 添加到画布
this.canvas.add(bgObject);
// 渲染画布
this.canvas.renderAll();
return this.backgroundLayer.id;
}
undo() {
// 从图层列表中删除背景图层
const bgLayerIndex = this.layers.value.findIndex(
(layer) => layer.isBackground
);
if (bgLayerIndex !== -1) {
this.layers.value.splice(bgLayerIndex, 1);
}
// 从画布中移除背景对象
if (this.backgroundLayer.fabricObject) {
this.canvas.remove(this.backgroundLayer.fabricObject);
}
// 渲染画布
this.canvas.renderAll();
}
/**
* 创建背景矩形对象
* @returns {Object} fabric.js 矩形对象
* @private
*/
_createBackgroundObject() {
// 计算画布尺寸
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
// 确保背景色为白色,如果没有设置或者是透明的话
const backgroundColor =
this.backgroundLayer.backgroundColor &&
this.backgroundLayer.backgroundColor !== "transparent"
? this.backgroundLayer.backgroundColor
: "#ffffff";
const rect = new fabric.Rect({
left: canvasWidth / 2,
top: canvasHeight / 2,
width: this.backgroundLayer.canvasWidth,
height: this.backgroundLayer.canvasHeight,
fill: backgroundColor,
selectable: false,
evented: false,
hoverCursor: "default",
id: `bg_object_${this.backgroundLayer.id}`,
layerId: this.backgroundLayer.id,
layerName: this.backgroundLayer.name,
originX: "center",
originY: "center",
isBackground: true, // 标记为背景对象
});
return rect;
}
getInfo() {
return {
name: this.name,
layerId: this.backgroundLayer.id,
layerName: this.backgroundLayer.name,
width: this.backgroundLayer.canvasWidth,
height: this.backgroundLayer.canvasHeight,
backgroundColor: this.backgroundLayer.backgroundColor,
};
}
}
/**
* 更新背景属性命令(如背景颜色)
*/
export class UpdateBackgroundCommand extends Command {
constructor(options) {
super({
name: "更新背景属性",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.backgroundColor = options.backgroundColor;
this.historyManager = options.historyManager;
// 查找背景图层
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
this.oldBackgroundColor = this.bgLayer
? this.bgLayer.backgroundColor
: "#ffffff";
}
execute() {
if (!this.bgLayer) {
console.error("未找到背景图层");
return false;
}
// 更新背景图层属性
this.bgLayer.backgroundColor = this.backgroundColor;
// 更新背景对象属性
if (this.bgLayer.fabricObject) {
this.bgLayer.fabricObject.set("fill", this.backgroundColor);
this.canvas.renderAll();
}
return true;
}
undo() {
if (!this.bgLayer) {
return false;
}
// 恢复背景图层属性
this.bgLayer.backgroundColor = this.oldBackgroundColor;
// 恢复背景对象属性
if (this.bgLayer.fabricObject) {
this.bgLayer.fabricObject.set("fill", this.oldBackgroundColor);
this.canvas.renderAll();
}
return true;
}
getInfo() {
return {
name: this.name,
layerId: this.bgLayer?.id,
oldColor: this.oldBackgroundColor,
newColor: this.backgroundColor,
};
}
}
/**
* 调整画布和背景大小命令
*/
export class BackgroundSizeCommand extends Command {
constructor(options) {
super({
name: "调整背景大小",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.canvasManager = options.canvasManager;
this.newWidth = options.newWidth;
this.newHeight = options.newHeight;
this.historyManager = options.historyManager;
// 记录原尺寸
this.oldWidth = this.canvas.width;
this.oldHeight = this.canvas.height;
// 查找背景图层
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
}
execute() {
// 调整画布大小
this.canvas.setWidth(this.newWidth);
this.canvas.setHeight(this.newHeight);
// 如果使用 CanvasManager通知它画布大小变化
if (
this.canvasManager &&
typeof this.canvasManager.updateCanvasSize === "function"
) {
this.canvasManager.updateCanvasSize(this.newWidth, this.newHeight);
}
// 调整背景对象大小
if (this.bgLayer && this.bgLayer.fabricObject) {
// 保持原有的背景颜色,如果没有设置则使用白色
const currentFill =
this.bgLayer.fabricObject.fill ||
this.bgLayer.backgroundColor ||
"#ffffff";
this.bgLayer.fabricObject.set({
width: this.newWidth,
height: this.newHeight,
fill: currentFill, // 保持原有颜色
});
// 更新图层记录的尺寸
this.bgLayer.canvasWidth = this.newWidth;
this.bgLayer.canvasHeight = this.newHeight;
}
// 渲染画布
this.canvas.renderAll();
return true;
}
undo() {
// 恢复画布大小
this.canvas.setWidth(this.oldWidth);
this.canvas.setHeight(this.oldHeight);
// 如果使用 CanvasManager通知它画布大小恢复
if (
this.canvasManager &&
typeof this.canvasManager.updateCanvasSize === "function"
) {
this.canvasManager.updateCanvasSize(this.oldWidth, this.oldHeight);
}
// 恢复背景对象大小
if (this.bgLayer && this.bgLayer.fabricObject) {
this.bgLayer.fabricObject.set({
width: this.oldWidth,
height: this.oldHeight,
});
// 恢复图层记录的尺寸
this.bgLayer.canvasWidth = this.oldWidth;
this.bgLayer.canvasHeight = this.oldHeight;
}
// 渲染画布
this.canvas.renderAll();
return true;
}
getInfo() {
return {
name: this.name,
oldWidth: this.oldWidth,
oldHeight: this.oldHeight,
newWidth: this.newWidth,
newHeight: this.newHeight,
};
}
}
/**
* 调整背景大小并等比缩放所有其他元素的命令
*/
export class BackgroundSizeWithScaleCommand extends Command {
constructor(options) {
super({
name: "调整背景大小并缩放元素",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.canvasManager = options.canvasManager;
this.newWidth = options.newWidth;
this.newHeight = options.newHeight;
this.historyManager = options.historyManager;
// 缩放策略:'uniform' | 'fill' | 'fit' | 'stretch'
this.scaleStrategy = options.scaleStrategy || "uniform";
// 记录原尺寸
this.oldWidth = this.canvas.width;
this.oldHeight = this.canvas.height;
// 查找背景图层
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
// 计算缩放比例
const scaleXRatio = this.newWidth / this.oldWidth;
const scaleYRatio = this.newHeight / this.oldHeight;
// 根据策略计算缩放比例和偏移
this._calculateScaleAndOffset(scaleXRatio, scaleYRatio);
// 存储所有非背景对象的原始状态
this.objectStates = [];
this._saveOriginalStates();
}
/**
* 保存所有非背景对象的原始状态
* @private
*/
_saveOriginalStates() {
this.canvas.getObjects().forEach((obj) => {
if (!obj.isBackground) {
// 检查对象是否已经有原始状态记录
if (!obj._originalState) {
// 第一次记录原始状态
obj._originalState = {
left: obj.left,
top: obj.top,
scaleX: obj.scaleX || 1,
scaleY: obj.scaleY || 1,
width: obj.width,
height: obj.height,
// 记录基准画布尺寸
baseCanvasWidth: this.oldWidth,
baseCanvasHeight: this.oldHeight,
};
}
this.objectStates.push({
obj: obj,
// 使用原始状态而不是当前状态
left: obj._originalState.left,
top: obj._originalState.top,
scaleX: obj._originalState.scaleX,
scaleY: obj._originalState.scaleY,
width: obj._originalState.width,
height: obj._originalState.height,
});
}
});
}
/**
* 根据缩放策略计算缩放比例和偏移量
* @param {number} scaleXRatio X轴缩放比例
* @param {number} scaleYRatio Y轴缩放比例
* @private
*/
_calculateScaleAndOffset(scaleXRatio, scaleYRatio) {
switch (this.scaleStrategy) {
case "uniform":
// 统一缩放:使用平均值,保持相对比例的同时允许适度的形变
this.uniformScale = Math.sqrt(scaleXRatio * scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
break;
case "fit":
// 适应模式:使用较小值,确保所有内容都在画布内,可能有留白
this.uniformScale = Math.min(scaleXRatio, scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
break;
case "fill":
// 填充模式:使用较大值,填满画布,可能有部分内容被裁切
this.uniformScale = Math.max(scaleXRatio, scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
break;
case "stretch":
// 拉伸模式:不保持宽高比,完全适应新尺寸
this.scaleX = scaleXRatio;
this.scaleY = scaleYRatio;
this.offsetX = 0;
this.offsetY = 0;
break;
default:
// 默认使用uniform模式
this.uniformScale = Math.sqrt(scaleXRatio * scaleYRatio);
this.offsetX = (this.newWidth - this.oldWidth * this.uniformScale) / 2;
this.offsetY =
(this.newHeight - this.oldHeight * this.uniformScale) / 2;
}
}
execute() {
// 调整画布大小
this.canvas.setWidth(this.newWidth);
this.canvas.setHeight(this.newHeight);
// 如果使用 CanvasManager通知它画布大小变化
if (
this.canvasManager &&
typeof this.canvasManager.updateCanvasSize === "function"
) {
this.canvasManager.updateCanvasSize(this.newWidth, this.newHeight);
}
// 调整背景对象大小和位置
if (this.bgLayer && this.bgLayer.fabricObject) {
// 保持原有的背景颜色,如果没有设置则使用白色
const currentFill =
this.bgLayer.fabricObject.fill ||
this.bgLayer.backgroundColor ||
"#ffffff";
this.bgLayer.fabricObject.set({
width: this.newWidth,
height: this.newHeight,
left: this.newWidth / 2,
top: this.newHeight / 2,
fill: currentFill, // 保持原有颜色
});
// 更新图层记录的尺寸
this.bgLayer.canvasWidth = this.newWidth;
this.bgLayer.canvasHeight = this.newHeight;
}
// 计算基于原始画布的缩放比例
const baseScaleX =
this.newWidth /
this.objectStates[0]?.obj._originalState?.baseCanvasWidth ||
this.newWidth / this.oldWidth;
const baseScaleY =
this.newHeight /
this.objectStates[0]?.obj._originalState?.baseCanvasHeight ||
this.newHeight / this.oldHeight;
// 根据策略缩放所有非背景对象
this.objectStates.forEach((state) => {
const obj = state.obj;
if (this.scaleStrategy === "stretch") {
// 拉伸模式使用不同的X和Y缩放比例
obj.set({
left: state.left * baseScaleX,
top: state.top * baseScaleY,
scaleX: state.scaleX * baseScaleX,
scaleY: state.scaleY * baseScaleY,
});
} else {
// 其他模式:计算基于原始状态的统一缩放比例
const baseUniformScale = Math.sqrt(baseScaleX * baseScaleY);
const baseOffsetX =
(this.newWidth -
(obj._originalState?.baseCanvasWidth || this.oldWidth) *
baseUniformScale) /
2;
const baseOffsetY =
(this.newHeight -
(obj._originalState?.baseCanvasHeight || this.oldHeight) *
baseUniformScale) /
2;
obj.set({
left: state.left * baseUniformScale + baseOffsetX,
top: state.top * baseUniformScale + baseOffsetY,
scaleX: state.scaleX * baseUniformScale,
scaleY: state.scaleY * baseUniformScale,
});
}
obj.setCoords();
});
// 渲染画布
this.canvas.renderAll();
return true;
}
undo() {
// 恢复画布大小
this.canvas.setWidth(this.oldWidth);
this.canvas.setHeight(this.oldHeight);
// 如果使用 CanvasManager通知它画布大小恢复
if (
this.canvasManager &&
typeof this.canvasManager.updateCanvasSize === "function"
) {
this.canvasManager.updateCanvasSize(this.oldWidth, this.oldHeight);
}
// 恢复背景对象大小和位置
if (this.bgLayer && this.bgLayer.fabricObject) {
this.bgLayer.fabricObject.set({
width: this.oldWidth,
height: this.oldHeight,
left: this.oldWidth / 2,
top: this.oldHeight / 2,
});
// 恢复图层记录的尺寸
this.bgLayer.canvasWidth = this.oldWidth;
this.bgLayer.canvasHeight = this.oldHeight;
}
// 恢复所有非背景对象的当前状态(而不是原始状态,因为可能有其他操作)
this.objectStates.forEach((state) => {
// 计算恢复到之前画布尺寸时的状态
const obj = state.obj;
const originalState = obj._originalState;
if (originalState) {
const baseScaleX = this.oldWidth / originalState.baseCanvasWidth;
const baseScaleY = this.oldHeight / originalState.baseCanvasHeight;
const baseUniformScale = Math.sqrt(baseScaleX * baseScaleY);
const baseOffsetX =
(this.oldWidth - originalState.baseCanvasWidth * baseUniformScale) /
2;
const baseOffsetY =
(this.oldHeight - originalState.baseCanvasHeight * baseUniformScale) /
2;
obj.set({
left: originalState.left * baseUniformScale + baseOffsetX,
top: originalState.top * baseUniformScale + baseOffsetY,
scaleX: originalState.scaleX * baseUniformScale,
scaleY: originalState.scaleY * baseUniformScale,
});
} else {
// 降级到原来的逻辑
obj.set({
left: state.left,
top: state.top,
scaleX: state.scaleX,
scaleY: state.scaleY,
});
}
obj.setCoords();
});
// 渲染画布
this.canvas.renderAll();
return true;
}
getInfo() {
const info = {
name: this.name,
oldWidth: this.oldWidth,
oldHeight: this.oldHeight,
newWidth: this.newWidth,
newHeight: this.newHeight,
scaleStrategy: this.scaleStrategy,
objectCount: this.objectStates.length,
};
if (this.scaleStrategy === "stretch") {
info.scaleX = this.scaleX;
info.scaleY = this.scaleY;
} else {
info.uniformScale = this.uniformScale;
info.offsetX = this.offsetX;
info.offsetY = this.offsetY;
}
return info;
}
}

View File

@@ -0,0 +1,698 @@
import { Command } from "./Command";
import { BrushStore } from "../store/BrushStore";
/**
* 笔刷属性设置基类命令
* 所有笔刷属性设置命令的基类
*/
class BaseBrushCommand extends Command {
constructor(options = {}) {
super(options);
this.brushStore = options.brushStore || BrushStore;
}
}
/**
* 笔刷大小设置命令
*/
export class BrushSizeCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {Number} options.size 要设置的笔刷大小
* @param {Number} options.previousSize 之前的笔刷大小(可选)
* @param {Object} options.brushStore BrushStore实例可选
*/
constructor(options = {}) {
super({
...options,
name: `设置笔刷大小: ${options.size}`,
description: `将笔刷大小从 ${options.previousSize || "?"} 设为 ${
options.size
}`,
});
this.size = options.size;
this.previousSize = options.previousSize || this.brushStore.state.size;
}
execute() {
// 记录当前大小用于撤销
if (this.previousSize === null) {
this.previousSize = this.brushStore.state.size;
}
// 执行设置
this.brushStore.setBrushSize(this.size);
return this.size;
}
undo() {
this.brushStore.setBrushSize(this.previousSize);
return this.previousSize;
}
}
/**
* 笔刷颜色设置命令
*/
export class BrushColorCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {String} options.color 要设置的颜色
* @param {String} options.previousColor 之前的颜色(可选)
* @param {Object} options.brushStore BrushStore实例可选
*/
constructor(options = {}) {
super({
...options,
name: `设置笔刷颜色: ${options.color}`,
description: `将笔刷颜色从 ${options.previousColor || "?"} 设为 ${
options.color
}`,
});
this.color = options.color;
this.previousColor = options.previousColor || this.brushStore.state.color;
}
execute() {
// 记录当前颜色用于撤销
if (this.previousColor === null) {
this.previousColor = this.brushStore.state.color;
}
// 执行设置
this.brushStore.setBrushColor(this.color);
return this.color;
}
undo() {
this.brushStore.setBrushColor(this.previousColor);
return this.previousColor;
}
}
/**
* 笔刷透明度设置命令
*/
export class BrushOpacityCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {Number} options.opacity 要设置的透明度 (0-1)
* @param {Number} options.previousOpacity 之前的透明度(可选)
* @param {Object} options.brushStore BrushStore实例可选
*/
constructor(options = {}) {
super({
...options,
name: `设置笔刷透明度: ${options.opacity}`,
description: `将笔刷透明度从 ${options.previousOpacity || "?"} 设为 ${
options.opacity
}`,
});
this.opacity = options.opacity;
this.previousOpacity =
options.previousOpacity || this.brushStore.state.opacity;
}
execute() {
// 记录当前透明度用于撤销
if (this.previousOpacity === null) {
this.previousOpacity = this.brushStore.state.opacity;
}
// 执行设置
this.brushStore.setBrushOpacity(this.opacity);
return this.opacity;
}
undo() {
this.brushStore.setBrushOpacity(this.previousOpacity);
return this.previousOpacity;
}
}
/**
* 笔刷类型设置命令
*/
export class BrushTypeCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {String} options.brushType 要设置的笔刷类型
* @param {String} options.previousType 之前的笔刷类型(可选)
* @param {Object} options.brushStore BrushStore实例可选
*/
constructor(options = {}) {
super({
...options,
name: `设置笔刷类型: ${options.brushType}`,
description: `将笔刷类型从 ${options.previousType || "?"} 设为 ${
options.brushType
}`,
});
this.brushType = options.brushType;
this.previousType = options.previousType || this.brushStore.state.type;
this.brushManager = options.brushManager;
}
execute() {
// 记录当前类型用于撤销
if (this.previousType === null) {
this.previousType = this.brushStore.state.type;
}
// 执行设置
// this.brushStore.setBrushType(this.brushType);
this.brushManager.setBrushType(this.brushType);
return this.brushType;
}
undo() {
// this.brushStore.setBrushType(this.previousType);
this.brushManager.setBrushType(this.previousType);
return this.previousType;
}
}
/**
* 材质设置命令
*/
export class TextureCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {Boolean} options.enabled 是否启用材质
* @param {String} options.path 材质路径
* @param {Number} options.scale 材质缩放
* @param {Object} options.previous 之前的材质设置(可选)
* @param {Object} options.brushStore BrushStore实例可选
*/
constructor(options = {}) {
super({
...options,
name: options.enabled ? "启用笔刷材质" : "禁用笔刷材质",
description: options.enabled
? `启用材质: ${options.path || "[默认]"}`
: "禁用笔刷材质",
});
this.enabled = options.enabled;
this.path = options.path;
this.scale = options.scale;
// 保存之前状态用于撤销
this.previous = options.previous || {
enabled: this.brushStore.state.textureEnabled,
path: this.brushStore.state.texturePath,
scale: this.brushStore.state.textureScale,
};
}
execute() {
// 记录当前状态用于撤销
if (!this.previous) {
this.previous = {
enabled: this.brushStore.state.textureEnabled,
path: this.brushStore.state.texturePath,
scale: this.brushStore.state.textureScale,
};
}
// 执行设置
this.brushStore.setTextureEnabled(this.enabled);
if (this.path) {
this.brushStore.setTexturePath(this.path);
}
if (this.scale !== undefined) {
this.brushStore.setTextureScale(this.scale);
}
return {
enabled: this.enabled,
path: this.path,
scale: this.scale,
};
}
undo() {
if (!this.previous) return null;
this.brushStore.setTextureEnabled(this.previous.enabled);
this.brushStore.setTexturePath(this.previous.path);
this.brushStore.setTextureScale(this.previous.scale);
return this.previous;
}
}
/**
* 预设应用命令
*/
export class BrushPresetCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {Number|Object} options.preset 预设索引或预设对象
* @param {Object} options.previousState 之前的状态(可选)
* @param {Object} options.brushStore BrushStore实例可选
*/
constructor(options = {}) {
const presetName =
typeof options.preset === "object"
? options.preset.name
: `预设 ${options.preset}`;
super({
...options,
name: `应用笔刷预设: ${presetName}`,
description: `应用预设: ${presetName}`,
});
this.preset = options.preset;
// 保存之前状态用于撤销
this.previousState = options.previousState || {
size: this.brushStore.state.size,
color: this.brushStore.state.color,
opacity: this.brushStore.state.opacity,
type: this.brushStore.state.type,
textureEnabled: this.brushStore.state.textureEnabled,
textureScale: this.brushStore.state.textureScale,
texturePath: this.brushStore.state.texturePath,
};
}
execute() {
// 记录当前状态用于撤销
if (!this.previousState) {
this.previousState = {
size: this.brushStore.state.size,
color: this.brushStore.state.color,
opacity: this.brushStore.state.opacity,
type: this.brushStore.state.type,
textureEnabled: this.brushStore.state.textureEnabled,
textureScale: this.brushStore.state.textureScale,
texturePath: this.brushStore.state.texturePath,
};
}
// 应用预设
if (typeof this.preset === "number") {
this.brushStore.applyPreset(this.preset);
} else if (typeof this.preset === "object") {
// 应用自定义预设对象
if (this.preset.size !== undefined)
this.brushStore.setBrushSize(this.preset.size);
if (this.preset.color !== undefined)
this.brushStore.setBrushColor(this.preset.color);
if (this.preset.opacity !== undefined)
this.brushStore.setBrushOpacity(this.preset.opacity);
if (this.preset.type !== undefined)
this.brushStore.setBrushType(this.preset.type);
if (this.preset.textureEnabled !== undefined) {
this.brushStore.setTextureEnabled(this.preset.textureEnabled);
}
if (this.preset.texturePath !== undefined) {
this.brushStore.setTexturePath(this.preset.texturePath);
}
if (this.preset.textureScale !== undefined) {
this.brushStore.setTextureScale(this.preset.textureScale);
}
}
return true;
}
undo() {
if (!this.previousState) return false;
// 恢复之前的状态
this.brushStore.setBrushSize(this.previousState.size);
this.brushStore.setBrushColor(this.previousState.color);
this.brushStore.setBrushOpacity(this.previousState.opacity);
this.brushStore.setBrushType(this.previousState.type);
this.brushStore.setTextureEnabled(this.previousState.textureEnabled);
this.brushStore.setTextureScale(this.previousState.textureScale);
this.brushStore.setTexturePath(this.previousState.texturePath);
return true;
}
}
/**
* 笔刷属性命令
* 用于修改笔刷的任意属性,包括特殊属性
*/
export class BrushPropertyCommand extends Command {
/**
* 构造函数
* @param {Object} options 命令选项
* @param {String} options.propertyId 属性ID
* @param {any} options.value 新的属性值
* @param {Object} options.brushStore BrushStore实例
*/
constructor(options) {
super({
name: "笔刷属性更改",
description: `更改笔刷属性 ${options.propertyId}`,
options,
});
this.propertyId = options.propertyId;
this.newValue = options.value;
this.oldValue = null;
this.brushStore = options.brushStore || BrushStore;
}
/**
* 执行命令
* @returns {Boolean} 是否执行成功
*/
execute() {
if (!this.brushStore) {
console.error("BrushStore不可用");
return false;
}
// 保存旧值用于撤销
this.oldValue = this.brushStore.getPropertyValue(this.propertyId);
// 更新属性值
this.brushStore.updatePropertyValue(this.propertyId, this.newValue);
return true;
}
/**
* 撤销命令
* @param {Object} context 命令上下文
* @returns {Boolean} 是否撤销成功
*/
undo() {
if (!this.brushStore || this.oldValue === null) {
return false;
}
// 恢复旧值
this.brushStore.updatePropertyValue(this.propertyId, this.oldValue);
return true;
}
}
/**
* 材质选择命令
*/
export class TextureSelectionCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {String} options.textureId 要设置的材质ID
* @param {String} options.previousTextureId 之前的材质ID可选
* @param {Object} options.brushManager 笔刷管理器实例
*/
constructor(options = {}) {
super({
...options,
name: `切换纹理材质`,
description: `切换到材质: ${options.textureId}`,
});
this.textureId = options.textureId;
this.previousTextureId = options.previousTextureId;
this.brushManager = options.brushManager;
}
execute() {
// 记录当前材质用于撤销
const currentBrush = this.brushManager.activeBrush;
if (currentBrush && currentBrush.getCurrentTexture) {
const currentTexture = currentBrush.getCurrentTexture();
this.previousTextureId = currentTexture ? currentTexture.id : null;
}
// 确保当前是材质笔刷
if (this.brushManager.getCurrentBrushType() !== "texture") {
this.brushManager.setBrushType("texture");
}
// 设置材质
const activeBrush = this.brushManager.activeBrush;
if (activeBrush && activeBrush.setTextureById) {
activeBrush.setTextureById(this.textureId);
}
return this.textureId;
}
undo() {
if (!this.previousTextureId) return false;
// 确保当前是材质笔刷
if (this.brushManager.getCurrentBrushType() !== "texture") {
this.brushManager.setBrushType("texture");
}
// 恢复之前的材质
const activeBrush = this.brushManager.activeBrush;
if (activeBrush && activeBrush.setTextureById) {
activeBrush.setTextureById(this.previousTextureId);
}
return this.previousTextureId;
}
}
/**
* 材质属性设置命令
*/
export class TexturePropertyCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {String} options.property 要设置的属性名称 (scale, rotation, offsetX, offsetY等)
* @param {any} options.value 要设置的属性值
* @param {any} options.previousValue 之前的属性值(可选)
* @param {Object} options.brushManager 笔刷管理器实例
*/
constructor(options = {}) {
super({
...options,
name: `设置材质属性: ${options.property}`,
description: `将材质${options.property}${
options.previousValue || "?"
} 设为 ${options.value}`,
});
this.property = options.property;
this.value = options.value;
this.previousValue = options.previousValue;
this.brushManager = options.brushManager;
}
execute() {
// 确保当前是材质笔刷
if (this.brushManager.getCurrentBrushType() !== "texture") {
this.brushManager.setBrushType("texture");
}
const activeBrush = this.brushManager.activeBrush;
if (!activeBrush || !activeBrush.setTextureProperty) {
return false;
}
// 记录当前值用于撤销
if (this.previousValue === undefined) {
this.previousValue = activeBrush.getTextureProperty(this.property);
}
// 设置新值
activeBrush.setTextureProperty(this.property, this.value);
return this.value;
}
undo() {
if (this.previousValue === undefined) return false;
// 确保当前是材质笔刷
if (this.brushManager.getCurrentBrushType() !== "texture") {
this.brushManager.setBrushType("texture");
}
const activeBrush = this.brushManager.activeBrush;
if (activeBrush && activeBrush.setTextureProperty) {
activeBrush.setTextureProperty(this.property, this.previousValue);
}
return this.previousValue;
}
}
/**
* 材质预设应用命令
*/
export class TexturePresetCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {String|Object} options.preset 预设ID或预设对象
* @param {Object} options.previousState 之前的材质状态(可选)
* @param {Object} options.brushManager 笔刷管理器实例
*/
constructor(options = {}) {
const presetName =
typeof options.preset === "object" ? options.preset.name : options.preset;
super({
...options,
name: `应用材质预设: ${presetName}`,
description: `应用材质预设: ${presetName}`,
});
this.preset = options.preset;
this.previousState = options.previousState;
this.brushManager = options.brushManager;
}
execute() {
// 确保当前是材质笔刷
if (this.brushManager.getCurrentBrushType() !== "texture") {
this.brushManager.setBrushType("texture");
}
const activeBrush = this.brushManager.activeBrush;
if (!activeBrush || !activeBrush.applyTexturePreset) {
return false;
}
// 记录当前状态用于撤销
if (!this.previousState) {
this.previousState = activeBrush.getCurrentTextureState();
}
// 应用预设
activeBrush.applyTexturePreset(this.preset);
return true;
}
undo() {
if (!this.previousState) return false;
// 确保当前是材质笔刷
if (this.brushManager.getCurrentBrushType() !== "texture") {
this.brushManager.setBrushType("texture");
}
const activeBrush = this.brushManager.activeBrush;
if (activeBrush && activeBrush.restoreTextureState) {
activeBrush.restoreTextureState(this.previousState);
}
return true;
}
}
/**
* 纹理上传命令
*/
export class TextureUploadCommand extends BaseBrushCommand {
/**
* @param {Object} options 命令选项
* @param {File} options.file 要上传的纹理文件
* @param {String} options.name 纹理名称(可选)
* @param {String} options.category 纹理分类(可选)
* @param {Object} options.texturePresetManager 纹理预设管理器实例
* @param {Object} options.brushManager 笔刷管理器实例
*/
constructor(options = {}) {
super({
...options,
name: `上传纹理: ${options.name || options.file?.name || '未知'}`,
description: `上传自定义纹理文件`,
});
this.file = options.file;
this.name = options.name || options.file?.name?.replace(/\.[^/.]+$/, "") || "自定义纹理";
this.category = options.category || "自定义材质";
this.texturePresetManager = options.texturePresetManager;
this.brushManager = options.brushManager;
this.uploadedTextureId = null;
}
async execute() {
if (!this.file || !this.texturePresetManager) {
throw new Error('缺少必要的文件或纹理预设管理器');
}
try {
// 创建文件 data URL
const dataUrl = await this._fileToDataUrl(this.file);
// 添加到纹理预设管理器
this.uploadedTextureId = this.texturePresetManager.addCustomTexture({
name: this.name,
category: this.category,
file: this.file,
dataUrl: dataUrl,
preview: dataUrl,
});
// 如果是纹理笔刷,自动应用新上传的纹理
if (this.brushManager) {
if (this.brushManager.getCurrentBrushType() !== "texture") {
this.brushManager.setBrushType("texture");
}
const activeBrush = this.brushManager.activeBrush;
if (activeBrush && activeBrush.updateProperty) {
activeBrush.updateProperty("textureSelector", this.uploadedTextureId);
}
}
return {
textureId: this.uploadedTextureId,
dataUrl: dataUrl,
name: this.name
};
} catch (error) {
console.error('纹理上传失败:', error);
throw new Error(`纹理上传失败: ${error.message}`);
}
}
undo() {
if (!this.uploadedTextureId || !this.texturePresetManager) {
return false;
}
// 从纹理预设管理器中移除上传的纹理
try {
this.texturePresetManager.removeCustomTexture(this.uploadedTextureId);
return true;
} catch (error) {
console.error('撤销纹理上传失败:', error);
return false;
}
}
/**
* 将文件转换为 data URL
* @private
* @param {File} file
* @returns {Promise<string>}
*/
_fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = (error) => {
reject(error);
};
reader.readAsDataURL(file);
});
}
}

View File

@@ -0,0 +1,296 @@
/**
* 基础命令类
* 所有命令都应该继承这个类
*/
export class Command {
constructor(options = {}) {
this.name = options.name || "未命名命令";
this.description = options.description || "";
this.undoable = options.undoable !== false; // 默认可撤销
this.timestamp = Date.now();
}
/**
* 执行命令
* @returns {*} 执行结果可以是Promise
*/
execute() {
throw new Error("子类必须实现execute方法");
}
/**
* 撤销命令
* @returns {*} 撤销结果可以是Promise
*/
undo() {
if (!this.undoable) {
throw new Error("此命令不支持撤销");
}
throw new Error("可撤销命令必须实现undo方法");
}
/**
* 获取命令信息
*/
getInfo() {
return {
name: this.name,
description: this.description,
undoable: this.undoable,
timestamp: this.timestamp,
};
}
}
/**
* 复合命令类
* 用于批量执行多个命令,替代事务系统
*/
export class CompositeCommand extends Command {
constructor(commands = [], options = {}) {
super({
name: options.name || "复合命令",
description: options.description || "批量执行多个命令",
...options,
});
this.commands = Array.isArray(commands) ? commands : [];
this.executedCommands = []; // 记录已执行的命令,用于撤销
this.isExecuting = false;
}
/**
* 添加子命令
*/
addCommand(command) {
if (!command || typeof command.execute !== "function") {
throw new Error("无效的命令对象");
}
this.commands.push(command);
return this;
}
/**
* 批量添加子命令
*/
addCommands(commands) {
if (Array.isArray(commands)) {
commands.forEach((cmd) => this.addCommand(cmd));
}
return this;
}
/**
* 执行所有子命令(串行执行)
*/
async execute() {
if (this.isExecuting) {
throw new Error("复合命令正在执行中");
}
this.isExecuting = true;
this.executedCommands = [];
try {
const results = [];
// 串行执行所有子命令
for (const command of this.commands) {
try {
console.log(`📦 复合命令执行子命令: ${command.constructor.name}`);
const result = command.execute();
// 如果是异步命令,等待完成
const finalResult = this._isPromise(result) ? await result : result;
results.push(finalResult);
this.executedCommands.push(command);
console.log(`✅ 子命令执行成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令执行失败: ${command.constructor.name}`,
error
);
// 执行失败时,撤销已执行的命令
await this._rollbackExecutedCommands();
throw new Error(`复合命令执行失败:${error.message}`);
}
}
this.isExecuting = false;
console.log(`✅ 复合命令执行完成,共执行 ${results.length} 个子命令`);
return results;
} catch (error) {
this.isExecuting = false;
throw error;
}
}
/**
* 撤销所有已执行的子命令(逆序撤销)
*/
async undo() {
if (this.isExecuting) {
throw new Error("复合命令正在执行中,无法撤销");
}
if (this.executedCommands.length === 0) {
console.warn("没有已执行的子命令需要撤销");
return true;
}
console.log(
`↩️ 开始撤销复合命令,共 ${this.executedCommands.length} 个子命令`
);
try {
// 逆序撤销已执行的命令
const commands = [...this.executedCommands].reverse();
const results = [];
for (const command of commands) {
if (typeof command.undo === "function") {
try {
console.log(`↩️ 撤销子命令: ${command.constructor.name}`);
const result = command.undo();
// 如果是异步撤销,等待完成
const finalResult = this._isPromise(result) ? await result : result;
results.push(finalResult);
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令撤销失败: ${command.constructor.name}`,
error
);
// 撤销失败不中断整个撤销过程,但要记录错误
}
} else {
console.warn(`⚠️ 子命令不支持撤销: ${command.constructor.name}`);
}
}
this.executedCommands = [];
console.log(`✅ 复合命令撤销完成`);
return results;
} catch (error) {
console.error("❌ 复合命令撤销过程中发生错误:", error);
throw error;
}
}
/**
* 回滚已执行的命令(内部使用)
* @private
*/
async _rollbackExecutedCommands() {
console.log(`🔄 开始回滚已执行的 ${this.executedCommands.length} 个子命令`);
const commands = [...this.executedCommands].reverse();
for (const command of commands) {
if (typeof command.undo === "function") {
try {
console.log(`🔄 回滚子命令: ${command.constructor.name}`);
const result = command.undo();
if (this._isPromise(result)) {
await result;
}
console.log(`✅ 子命令回滚成功: ${command.constructor.name}`);
} catch (error) {
console.error(
`❌ 子命令回滚失败: ${command.constructor.name}`,
error
);
// 回滚失败不中断整个回滚过程
}
}
}
this.executedCommands = [];
console.log(`✅ 回滚完成`);
}
/**
* 检查返回值是否为Promise
* @private
*/
_isPromise(value) {
return (
value &&
typeof value === "object" &&
typeof value.then === "function" &&
typeof value.catch === "function"
);
}
/**
* 获取复合命令信息
*/
getInfo() {
return {
...super.getInfo(),
commandCount: this.commands.length,
executedCount: this.executedCommands.length,
isExecuting: this.isExecuting,
subCommands: this.commands.map((cmd) =>
cmd.getInfo ? cmd.getInfo() : cmd.constructor.name
),
};
}
}
/**
* 函数命令包装器
* 将普通函数包装为命令对象
*/
export class FunctionCommand extends Command {
constructor(executeFn, undoFn = null, options = {}) {
super({
name: options.name || "函数命令",
undoable: typeof undoFn === "function",
...options,
});
if (typeof executeFn !== "function") {
throw new Error("执行函数不能为空");
}
this.executeFn = executeFn;
this.undoFn = undoFn;
this.executeResult = null; // 保存执行结果用于撤销
}
async execute() {
const result = this.executeFn();
this.executeResult = this._isPromise(result) ? await result : result;
return this.executeResult;
}
async undo() {
if (!this.undoFn) {
throw new Error("此函数命令不支持撤销");
}
const result = this.undoFn(this.executeResult);
return this._isPromise(result) ? await result : result;
}
/**
* 检查返回值是否为Promise
* @private
*/
_isPromise(value) {
return (
value &&
typeof value === "object" &&
typeof value.then === "function" &&
typeof value.catch === "function"
);
}
}

View File

@@ -0,0 +1,491 @@
import { CompositeCommand } from "./Command.js";
import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js";
import { ToolCommand } from "./ToolCommands.js";
import { ClearSelectionCommand } from "./SelectionCommands.js";
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
//import { fabric } from "fabric-with-all";
/**
* 套索抠图命令
* 实现将选区内容抠图到新图层的功能
*/
export class LassoCutoutCommand extends CompositeCommand {
constructor(options = {}) {
super([], {
name: "套索抠图",
description: "将选区抠图到新图层",
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.selectionManager = options.selectionManager;
this.toolManager = options.toolManager;
this.sourceLayerId = options.sourceLayerId;
this.newLayerName = options.newLayerName || "抠图";
this.newLayerId = null;
this.cutoutImageUrl = null;
this.fabricImage = null;
this.executedCommands = [];
// 高清截图选项
this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用
this.baseResolutionScale = options.baseResolutionScale || 4; // 基础分辨率倍数提高到4倍获得更清晰的图像
}
async execute() {
if (!this.canvas || !this.layerManager || !this.selectionManager) {
console.error("无法执行套索抠图:参数无效");
return false;
}
try {
this.executedCommands = [];
// 获取选区
const selectionObject = this.selectionManager.getSelectionObject();
if (!selectionObject) {
console.error("无法执行套索抠图:当前没有选区");
return false;
}
// 确定源图层
const sourceLayer = this.layerManager.getActiveLayer();
if (!sourceLayer || sourceLayer.fabricObjects.length === 0) {
console.error("无法执行套索抠图:源图层无效");
return false;
}
// 获取选区边界信息用于后续定位
const selectionBounds = selectionObject.getBoundingRect(true, true);
// 执行在当前画布上的抠图操作
this.cutoutImageUrl = await this._performCutout(
sourceLayer,
selectionObject
);
if (!this.cutoutImageUrl) {
console.error("抠图失败");
return false;
}
// 创建fabric图像对象传递选区边界信息
this.fabricImage = await this._createFabricImage(
this.cutoutImageUrl,
selectionBounds
);
if (!this.fabricImage) {
console.error("创建图像对象失败");
return false;
}
// 1. 创建图像图层命令
const createImageLayerCmd = new CreateImageLayerCommand({
layerManager: this.layerManager,
fabricImage: this.fabricImage,
toolManager: this.toolManager,
layerName: this.newLayerName,
});
// 执行创建图像图层命令
const result = await createImageLayerCmd.execute();
this.newLayerId = createImageLayerCmd.newLayerId;
this.executedCommands.push(createImageLayerCmd);
// 2. 清除选区命令
const clearSelectionCmd = new ClearSelectionCommand({
canvas: this.canvas,
selectionManager: this.selectionManager,
});
// 执行清除选区命令
await clearSelectionCmd.execute();
this.executedCommands.push(clearSelectionCmd);
console.log(`套索抠图完成新图层ID: ${this.newLayerId}`);
return {
newLayerId: this.newLayerId,
cutoutImageUrl: this.cutoutImageUrl,
};
} catch (error) {
console.error("套索抠图过程中出错:", error);
// 如果已经创建了新图层,需要进行清理
if (this.newLayerId) {
try {
await this.layerManager.removeLayer(this.newLayerId);
} catch (cleanupError) {
console.warn("清理新图层失败:", cleanupError);
}
}
throw error;
}
}
async undo() {
try {
// 逆序撤销所有已执行的命令
for (let i = this.executedCommands.length - 1; i >= 0; i--) {
const command = this.executedCommands[i];
if (command && typeof command.undo === "function") {
await command.undo();
}
}
this.executedCommands = [];
this.newLayerId = null;
this.cutoutImageUrl = null;
this.fabricImage = null;
return true;
} catch (error) {
console.error("撤销套索抠图失败:", error);
return false;
}
}
/**
* 在当前画布上执行抠图操作
* @param {Object} sourceLayer 源图层
* @param {Object} selectionObject 选区对象
* @returns {String} 抠图结果的DataURL
* @private
*/
async _performCutout(sourceLayer, selectionObject) {
try {
console.log("=== 开始在当前画布执行抠图 ===");
// 获取选区边界
const selectionBounds = selectionObject.getBoundingRect(true, true);
console.log(
`选区边界: left=${selectionBounds.left}, top=${selectionBounds.top}, width=${selectionBounds.width}, height=${selectionBounds.height}`
);
// 保存画布当前状态
const originalActiveObject = this.canvas.getActiveObject();
const originalSelection = this.canvas.selection;
// 临时禁用画布选择
this.canvas.selection = false;
this.canvas.discardActiveObject();
let tempGroup = null;
let originalObjects = [];
try {
// 收集源图层中的可见对象
const visibleObjects = sourceLayer.fabricObjects.filter(
(obj) => obj.visible
);
console.log(`源图层可见对象数量: ${visibleObjects.length}`);
if (visibleObjects.length === 0) {
throw new Error("源图层没有可见对象");
}
// 如果只有一个对象且已经是组,直接使用
if (visibleObjects.length === 1 && visibleObjects[0].type === "group") {
tempGroup = visibleObjects[0];
console.log("使用现有组对象");
} else {
// 创建临时组
console.log("创建临时组...");
// 记录原始对象的位置和状态,用于后续恢复
originalObjects = visibleObjects.map((obj) => ({
object: obj,
originalLeft: obj.left,
originalTop: obj.top,
originalAngle: obj.angle,
originalScaleX: obj.scaleX,
originalScaleY: obj.scaleY,
}));
// 不需要从画布移除原对象,直接创建组
// 克隆对象来创建组,避免影响原对象
const clonedObjects = [];
for (const obj of visibleObjects) {
const cloned = await this._cloneObject(obj);
clonedObjects.push(cloned);
}
// 创建组
tempGroup = new fabric.Group(clonedObjects, {
selectable: false,
evented: false,
});
// 添加组到画布
this.canvas.add(tempGroup);
}
// 设置选区为裁剪路径
const clipPath = await this._cloneObject(selectionObject);
clipPath.set({
fill: "",
stroke: "",
absolutePositioned: true,
originX: "left",
originY: "top",
});
// 应用裁剪路径到组
tempGroup.set({
clipPath: clipPath,
});
this.canvas.renderAll();
// 计算渲染区域
const renderBounds = {
left: selectionBounds.left,
top: selectionBounds.top,
width: selectionBounds.width,
height: selectionBounds.height,
};
// 设置高分辨率倍数,用于提高图像清晰度
let highResolutionScale = 1;
if (this.highResolutionEnabled) {
// 结合设备像素比和配置的基础倍数,确保在所有设备上都有最佳效果
const devicePixelRatio = window.devicePixelRatio || 1;
// 使用更激进的缩放策略,确保高清晰度
highResolutionScale = Math.max(
this.baseResolutionScale,
devicePixelRatio * 2
);
console.log(
`设备像素比: ${devicePixelRatio}, 基础倍数: ${this.baseResolutionScale}, 最终放大倍数: ${highResolutionScale}`
);
} else {
console.log("高分辨率渲染已禁用使用1x倍数");
}
// 创建用于导出的高分辨率canvas
const exportCanvas = document.createElement("canvas");
const exportCtx = exportCanvas.getContext("2d", {
alpha: true,
willReadFrequently: false,
colorSpace: "srgb",
});
// 设置canvas的实际像素尺寸放大倍数
const actualWidth = Math.round(
renderBounds.width * highResolutionScale
);
const actualHeight = Math.round(
renderBounds.height * highResolutionScale
);
exportCanvas.width = actualWidth;
exportCanvas.height = actualHeight;
// 设置canvas的显示尺寸CSS尺寸保持与选区一致
exportCanvas.style.width = renderBounds.width + "px";
exportCanvas.style.height = renderBounds.height + "px";
// 启用最高质量渲染设置
exportCtx.imageSmoothingEnabled = true;
exportCtx.imageSmoothingQuality = "high";
// 设置文本渲染质量
if (exportCtx.textRenderingOptimization) {
exportCtx.textRenderingOptimization = "optimizeQuality";
}
// 设置线条和图形的渲染质量
exportCtx.lineCap = "round";
exportCtx.lineJoin = "round";
exportCtx.miterLimit = 10;
// 设置画布背景为透明
exportCtx.clearRect(0, 0, actualWidth, actualHeight);
// 获取画布当前的变换矩阵
const vpt = this.canvas.viewportTransform;
const zoom = this.canvas.getZoom();
// 保存当前变换状态
exportCtx.save();
// 应用高分辨率缩放
exportCtx.scale(highResolutionScale, highResolutionScale);
// 应用偏移,只渲染选区部分
exportCtx.translate(-renderBounds.left, -renderBounds.top);
// 如果画布有缩放和平移,需要应用相应变换
if (zoom !== 1 || vpt[4] !== 0 || vpt[5] !== 0) {
exportCtx.transform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
}
// 渲染被裁剪的组
tempGroup.render(exportCtx);
exportCtx.restore();
// 获取结果 - 使用最高质量设置
const dataUrl = exportCanvas.toDataURL("image/png", 1.0);
console.log(
`抠图完成,选区尺寸: ${renderBounds.width}x${renderBounds.height}, 实际渲染尺寸: ${actualWidth}x${actualHeight}, 放大倍数: ${highResolutionScale}x`
);
return dataUrl;
} finally {
// 清理和恢复
if (tempGroup) {
// 移除裁剪路径
tempGroup.set({ clipPath: null });
// 如果是我们创建的临时组,需要移除它
if (originalObjects.length > 0) {
this.canvas.remove(tempGroup);
// 恢复原始对象的状态(位置等信息保持不变)
originalObjects.forEach(
({
object,
originalLeft,
originalTop,
originalAngle,
originalScaleX,
originalScaleY,
}) => {
// 确保对象仍然在画布上且状态正确
if (!this.canvas.getObjects().includes(object)) {
this.canvas.add(object);
}
// 恢复原始变换状态(如果需要的话)
object.set({
left: originalLeft,
top: originalTop,
angle: originalAngle,
scaleX: originalScaleX,
scaleY: originalScaleY,
});
object.setCoords();
}
);
}
}
// 恢复画布状态
this.canvas.selection = originalSelection;
if (originalActiveObject) {
this.canvas.setActiveObject(originalActiveObject);
}
this.canvas.renderAll();
}
} catch (error) {
console.error("在当前画布执行抠图失败:", error);
return null;
}
}
/**
* 从DataURL创建fabric图像对象
* @param {String} dataUrl 图像DataURL
* @param {Object} selectionBounds 选区边界信息
* @returns {fabric.Image} fabric图像对象
* @private
*/
async _createFabricImage(dataUrl, selectionBounds) {
return new Promise((resolve, reject) => {
fabric.Image.fromURL(
dataUrl,
(img) => {
if (!img) {
reject(new Error("无法从DataURL创建图像"));
return;
}
// 计算画布中心位置
const canvasCenter = this.canvas.getCenter();
// 如果有选区边界信息,使用选区的原始位置和尺寸
let targetLeft = canvasCenter.left;
let targetTop = canvasCenter.top;
let targetWidth = img.width;
let targetHeight = img.height;
if (selectionBounds) {
// 使用选区的原始位置
targetLeft = selectionBounds.left + selectionBounds.width / 2;
targetTop = selectionBounds.top + selectionBounds.height / 2;
// 确保图像显示尺寸与选区尺寸一致
targetWidth = selectionBounds.width;
targetHeight = selectionBounds.height;
console.log(
`设置图像位置: left=${targetLeft}, top=${targetTop}, 尺寸: ${targetWidth}x${targetHeight}`
);
}
// 计算缩放比例以匹配目标尺寸
const scaleX = targetWidth / img.width;
const scaleY = targetHeight / img.height;
// 设置图像属性
img.set({
left: targetLeft,
top: targetTop,
scaleX: scaleX,
scaleY: scaleY,
originX: "center",
originY: "center",
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
cornerStyle: "circle",
cornerColor: "#007aff",
cornerSize: 10,
transparentCorners: false,
borderColor: "#007aff",
borderScaleFactor: 2,
// 优化图像渲染质量
objectCaching: false, // 禁用缓存以确保最佳质量
statefullCache: true,
noScaleCache: false,
});
// 更新坐标
img.setCoords();
resolve(img);
},
{
crossOrigin: "anonymous",
// 确保图像以最高质量加载
quality: 1.0,
}
);
});
}
/**
* 克隆fabric对象
* @param {Object} obj fabric对象
* @returns {Object} 克隆的对象
* @private
*/
async _cloneObject(obj) {
return new Promise((resolve, reject) => {
if (!obj) {
reject(new Error("对象无效,无法克隆"));
return;
}
try {
obj.clone((cloned) => {
resolve(cloned);
});
} catch (error) {
reject(error);
}
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,578 @@
import { Command, FunctionCommand } from "./Command";
/**
* 液化命令基类
* 所有液化相关命令的基类
*/
export class LiquifyCommand extends Command {
/**
* 创建液化命令
* @param {Object} options 配置选项
* @param {Object} options.canvas Fabric.js画布实例
* @param {Object} options.layerManager 图层管理器实例
* @param {Object} options.liquifyManager 液化管理器实例
* @param {String} options.mode 液化模式
* @param {Object} options.params 液化参数
* @param {Object} options.targetObject 目标对象
* @param {ImageData} options.originalData 原始图像数据
* @param {ImageData} options.resultData 变形后图像数据
*/
constructor(options) {
super({
name: options.name || `液化操作: ${options.mode || "未知模式"}`,
description:
options.description ||
`使用${options.mode || "未知模式"}模式进行液化操作`,
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.liquifyManager = options.liquifyManager;
this.mode = options.mode;
this.params = options.params || {};
this.targetObject = options.targetObject;
this.targetLayerId = options.targetLayerId;
this.originalData = options.originalData; // 操作前的图像数据
this.resultData = options.resultData; // 操作后的图像数据
this.savedState = null;
}
/**
* 执行液化操作
* @returns {Promise} 执行结果
*/
async execute() {
if (!this.canvas || !this.targetObject) {
throw new Error("液化命令缺少必要的画布或目标对象");
}
if (!this.resultData) {
// 如果没有预先计算的结果数据,现场执行变形
this.resultData = await this.liquifyManager.applyLiquify(
this.targetObject,
this.mode,
this.params
);
}
// 保存执行前的状态
this.savedState = await this._saveObjectState();
// 更新画布上的对象
await this._updateObjectWithResult();
// 刷新Canvas
this.canvas.renderAll();
return this.resultData;
}
/**
* 撤销液化操作
* @returns {Promise} 撤销结果
*/
async undo() {
if (!this.canvas || !this.targetObject || !this.savedState) {
throw new Error("无法撤销:缺少必要的状态信息");
}
// 恢复对象到原始状态
await this._restoreObjectState();
// 刷新Canvas
this.canvas.renderAll();
return true;
}
/**
* 保存对象状态
* @private
*/
async _saveObjectState() {
if (!this.targetObject) return null;
// 对于图像对象我们需要保存src和元数据
const state = {
src: this.targetObject.getSrc ? this.targetObject.getSrc() : null,
element: this.targetObject._element
? this.targetObject._element.cloneNode(true)
: null,
filters: this.targetObject.filters ? [...this.targetObject.filters] : [],
originalData: this.originalData,
targetLayerId: this.targetLayerId,
};
return state;
}
/**
* 恢复对象状态
* @private
*/
async _restoreObjectState() {
if (!this.targetObject || !this.savedState) return false;
// 获取当前图层对象
const layer = this.layerManager.getLayerById(this.savedState.targetLayerId);
if (!layer) return false;
// 恢复原始图像
if (this.savedState.element && this.targetObject.setElement) {
this.targetObject.setElement(this.savedState.element);
// 恢复滤镜
if (this.savedState.filters) {
this.targetObject.filters = [...this.savedState.filters];
this.targetObject.applyFilters();
}
}
return true;
}
/**
* 使用变形结果更新对象
* @private
*/
async _updateObjectWithResult() {
if (!this.targetObject || !this.resultData) return false;
// 创建临时canvas来渲染结果数据
const tempCanvas = document.createElement("canvas");
tempCanvas.width = this.resultData.width;
tempCanvas.height = this.resultData.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.putImageData(this.resultData, 0, 0);
console.log("临时Canvas创建成功 _updateObjectWithResult", this.resultData);
// 更新Fabric图像
await new Promise((resolve) => {
fabric.Image.fromURL(tempCanvas.toDataURL(), (img) => {
// 保留原对象的属性
img.set({
left: this.targetObject.left,
top: this.targetObject.top,
scaleX: this.targetObject.scaleX,
scaleY: this.targetObject.scaleY,
angle: this.targetObject.angle,
flipX: this.targetObject.flipX,
flipY: this.targetObject.flipY,
opacity: this.targetObject.opacity,
});
// 替换Canvas上的对象
const index = this.canvas.getObjects().indexOf(this.targetObject);
if (index !== -1) {
this.canvas.remove(this.targetObject);
this.canvas.insertAt(img, index);
this.targetObject = img;
}
// 确保图层引用更新
const layer = this.layerManager.getLayerById(this.targetLayerId);
if (layer) {
if (
layer.type === "background" &&
layer.fabricObject === this.targetObject
) {
layer.fabricObject = img;
} else if (layer.fabricObjects) {
const objIndex = layer.fabricObjects.indexOf(this.targetObject);
if (objIndex !== -1) {
layer.fabricObjects[objIndex] = img;
}
}
}
resolve();
});
});
return true;
}
}
/**
* 图层栅格化命令
* 用于将复杂图层栅格化为单一图像,以便进行液化操作
*/
export class RasterizeForLiquifyCommand extends Command {
/**
* 创建栅格化命令
* @param {Object} options 配置选项
* @param {Object} options.canvas Fabric.js画布实例
* @param {Object} options.layerManager 图层管理器实例
* @param {String} options.layerId 需要栅格化的图层ID
*/
constructor(options) {
super({
name: options.name || "栅格化图层",
description: options.description || "将图层栅格化为单一图像以便液化操作",
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.layerId = options.layerId;
this.originalLayer = null;
this.rasterizedImageObj = null;
}
/**
* 执行栅格化操作
* @returns {Promise<Object>} 栅格化后的图像对象
*/
async execute() {
if (!this.canvas || !this.layerManager || !this.layerId) {
throw new Error("栅格化命令缺少必要参数");
}
// 保存原始图层信息
this.originalLayer = this.layerManager.getLayerById(this.layerId);
if (!this.originalLayer) {
throw new Error(`图层ID不存在: ${this.layerId}`);
}
// 栅格化图层
const rasterizedImage = await this.layerManager.rasterizeLayer(
this.layerId
);
if (!rasterizedImage) {
throw new Error("栅格化图层失败");
}
this.rasterizedImageObj = rasterizedImage;
return rasterizedImage;
}
/**
* 撤销栅格化操作
* 注意:完整撤销栅格化是复杂的,这里提供近似还原
* @returns {Promise<boolean>} 撤销结果
*/
async undo() {
if (!this.canvas || !this.layerManager || !this.originalLayer) {
throw new Error("无法撤销:缺少必要的状态信息");
}
// 恢复图层为原始状态是复杂的这里可能需要与LayerManager协作
// 这个实现可能需要根据实际的LayerManager功能来调整
const restored = await this.layerManager.restoreLayerFromBackup(
this.layerId
);
if (!restored) {
console.warn("无法完全还原栅格化前的图层状态");
}
return true;
}
}
/**
* 液化工具初始化命令
* 用于初始化液化工具的状态,不执行实际操作
*/
export class InitLiquifyToolCommand extends Command {
constructor(options) {
super({
name: "初始化液化工具",
description: "准备液化工具工作环境",
undoable: false, // 这个命令不需要撤销
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.liquifyManager = options.liquifyManager;
this.toolManager = options.toolManager;
}
/**
* 执行初始化
*/
execute() {
if (this.liquifyManager) {
this.liquifyManager.initialize({
canvas: this.canvas,
layerManager: this.layerManager,
});
}
// 通知各管理器进入液化模式
this.toolManager?.notifyObservers("LIQUIFY");
return true;
}
undo() {
// 不需要撤销初始化
return true;
}
}
/**
* 液化操作命令 - 针对单次变形操作
* 用于实现可撤销的单次液化变形
*/
export class LiquifyDeformCommand extends LiquifyCommand {
/**
* 创建液化变形命令
* @param {Object} options 配置选项
* @param {Number} options.x 变形中心X坐标
* @param {Number} options.y 变形中心Y坐标
* @param {ImageData} options.beforeData 变形前的图像数据
* @param {ImageData} options.afterData 变形后的图像数据
*/
constructor(options) {
super({
...options,
name: `液化变形: ${options.mode || "未知模式"}`,
description: `在(${options.x}, ${options.y})应用${
options.mode || "未知模式"
}变形`,
});
this.x = options.x;
this.y = options.y;
this.beforeData = options.beforeData;
this.afterData = options.afterData;
}
async execute() {
if (!this.afterData) {
// 如果没有预计算的结果,实时计算
await this.liquifyManager.prepareForLiquify(this.targetObject);
this.afterData = await this.liquifyManager.applyLiquify(
this.targetObject,
this.mode,
this.params,
this.x,
this.y
);
}
// 保存当前状态
this.savedState = await this._saveObjectState();
// 应用变形结果
await this._updateObjectWithImageData(this.afterData);
this.canvas.renderAll();
return this.afterData;
}
async undo() {
if (!this.beforeData) {
throw new Error("无法撤销:缺少变形前的数据");
}
// 恢复到变形前的状态
await this._updateObjectWithImageData(this.beforeData);
this.canvas.renderAll();
return true;
}
/**
* 使用图像数据更新对象
* @param {ImageData} imageData 图像数据
* @private
*/
async _updateObjectWithImageData(imageData) {
return new Promise((resolve) => {
// 创建临时canvas
const tempCanvas = document.createElement("canvas");
tempCanvas.width = imageData.width;
tempCanvas.height = imageData.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.putImageData(imageData, 0, 0);
// 从canvas创建新的fabric图像
fabric.Image.fromURL(tempCanvas.toDataURL(), (img) => {
// 保留原对象的变换属性
img.set({
left: this.targetObject.left,
top: this.targetObject.top,
scaleX: this.targetObject.scaleX,
scaleY: this.targetObject.scaleY,
angle: this.targetObject.angle,
flipX: this.targetObject.flipX,
flipY: this.targetObject.flipY,
opacity: this.targetObject.opacity,
});
// 替换canvas上的对象
const index = this.canvas.getObjects().indexOf(this.targetObject);
if (index !== -1) {
this.canvas.remove(this.targetObject);
this.canvas.insertAt(img, index);
this.targetObject = img;
// 更新图层引用
const layer = this.layerManager.getLayerById(this.targetLayerId);
if (layer) {
if (
layer.type === "background" &&
(layer.fabricObject === this.savedState?.originalObject ||
layer.fabricObject === this.targetObject)
) {
layer.fabricObject = img;
} else if (layer.fabricObjects) {
const objIndex = layer.fabricObjects.findIndex(
(obj) =>
obj === this.savedState?.originalObject ||
obj === this.targetObject
);
if (objIndex !== -1) {
layer.fabricObjects[objIndex] = img;
}
}
}
}
resolve();
});
});
}
}
/**
* 复合液化命令 - 用于组合多个液化操作
* 支持一次撤销多个相关的液化变形
*/
export class CompositeLiquifyCommand extends Command {
/**
* 创建复合液化命令
* @param {Object} options 配置选项
* @param {Array} options.commands 子命令列表
* @param {String} options.name 命令名称
*/
constructor(options) {
super({
name: options.name || "液化操作组合",
description:
options.description || `包含${options.commands?.length || 0}个液化操作`,
});
this.commands = options.commands || [];
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.liquifyManager = options.liquifyManager;
}
/**
* 添加子命令
* @param {Command} command 要添加的命令
*/
addCommand(command) {
this.commands.push(command);
}
async execute() {
const results = [];
for (const command of this.commands) {
try {
const result = await command.execute();
results.push(result);
} catch (error) {
// 如果有命令失败,尝试回滚已执行的命令
console.error("复合液化命令执行失败:", error);
await this.undo();
throw error;
}
}
return results;
}
async undo() {
// 逆序撤销所有子命令
const errors = [];
for (let i = this.commands.length - 1; i >= 0; i--) {
try {
await this.commands[i].undo();
} catch (error) {
console.error(`撤销子命令${i}失败:`, error);
errors.push(error);
}
}
if (errors.length > 0) {
throw new Error(`复合命令撤销部分失败: ${errors.length}个错误`);
}
return true;
}
/**
* 检查命令是否可以执行
* @returns {Boolean} 是否可执行
*/
canExecute() {
return (
this.commands.length > 0 &&
this.commands.every((cmd) => (cmd.canExecute ? cmd.canExecute() : true))
);
}
}
/**
* 液化重置命令 - 将图像恢复到原始状态
*/
export class LiquifyResetCommand extends LiquifyCommand {
constructor(options) {
super({
...options,
name: "重置液化",
description: "将图像恢复到液化前的原始状态",
});
}
async execute() {
if (!this.liquifyManager || !this.targetObject) {
throw new Error("无法重置:缺少必要的管理器或目标对象");
}
// 保存当前状态
this.savedState = await this._saveObjectState();
// 重置液化管理器
const resetData = this.liquifyManager.reset();
if (!resetData) {
throw new Error("重置失败:没有原始数据");
}
// 应用重置结果
await this._updateObjectWithResult();
this.canvas.renderAll();
return resetData;
}
}
/**
* 辅助函数:创建液化重置命令
* @param {Object} options 配置选项
* @returns {LiquifyResetCommand} 重置命令实例
*/
export function createLiquifyResetCommand(options) {
return new LiquifyResetCommand(options);
}
/**
* 辅助函数:创建液化变形命令
* @param {Object} options 配置选项
* @returns {LiquifyDeformCommand} 变形命令实例
*/
export function createLiquifyDeformCommand(options) {
return new LiquifyDeformCommand(options);
}
/**
* 辅助函数:创建复合液化命令
* @param {Object} options 配置选项
* @returns {CompositeLiquifyCommand} 复合命令实例
*/
export function createCompositeLiquifyCommand(options) {
return new CompositeLiquifyCommand(options);
}

View File

@@ -0,0 +1,103 @@
/**
* 查询类命令示例 - 不需要撤销
*/
export class GetCanvasInfoCommand {
constructor(options) {
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.undoable = false; // 明确标记为不可撤销
}
execute() {
return {
width: this.canvas.getWidth(),
height: this.canvas.getHeight(),
zoom: this.canvas.getZoom(),
layers: this.layerManager?.getLayers()?.length || 0,
objects: this.canvas.getObjects().length,
};
}
// 查询类命令不需要实现 undo 方法
}
/**
* 导出类命令示例 - 不需要撤销
*/
export class ExportCanvasCommand {
constructor(options) {
this.canvas = options.canvas;
this.format = options.format || "png";
this.quality = options.quality || 1;
this.undoable = false;
}
execute() {
return this.canvas.toDataURL(`image/${this.format}`, this.quality);
}
}
/**
* 验证类命令示例 - 不需要撤销
*/
export class ValidateCanvasCommand {
constructor(options) {
this.canvas = options.canvas;
this.undoable = false;
}
execute() {
const objects = this.canvas.getObjects();
const errors = [];
objects.forEach((obj, index) => {
if (!obj.left || !obj.top) {
errors.push(`对象 ${index} 位置无效`);
}
if (obj.width <= 0 || obj.height <= 0) {
errors.push(`对象 ${index} 尺寸无效`);
}
});
return {
isValid: errors.length === 0,
errors,
objectCount: objects.length,
};
}
}
/**
* 系统清理命令示例 - 不可逆操作,不需要撤销
*/
export class CleanupTempDataCommand {
constructor(options) {
this.canvas = options.canvas;
this.undoable = false;
}
execute() {
// 清理临时数据
const cleaned = [];
// 移除无效对象
const objects = this.canvas.getObjects();
objects.forEach((obj, index) => {
if (obj._isTemp || obj._invalid) {
this.canvas.remove(obj);
cleaned.push(`临时对象 ${index}`);
}
});
// 清理缓存
if (this.canvas._clearCache) {
this.canvas._clearCache();
cleaned.push("画布缓存");
}
return {
cleaned,
count: cleaned.length,
};
}
}

View File

@@ -0,0 +1,942 @@
import { OperationType } from "../utils/layerHelper";
import { Command } from "./Command";
import { generateId } from "../utils/helper";
/**
* 设置活动图层命令
*/
export class SetActiveLayerCommand extends Command {
constructor(options) {
super({
name: "设置活动图层",
saveState: false,
});
this.layers = options.layers;
this.canvas = options.canvas;
this.activeLayerId = options.activeLayerId;
this.layerId = options.layerId;
this.oldActiveLayerId = this.activeLayerId.value;
this.layerManager = options.layerManager;
this.oldActiveObjects = [];
this.newLayer = null;
this.editorMode = options.editorMode;
}
execute() {
this.newLayer = this.layers.value.find(
(layer) => layer.id === this.layerId
);
if (!this.newLayer) {
console.error(`图层 ${this.layerId} 不存在`);
return false;
}
// 如果是背景层,不设置为活动图层
if (this.newLayer.isBackground) {
console.warn("背景层不能设为活动图层");
return false;
}
// 如果图层已锁定,不设置为活动图层
if (this.newLayer.locked) {
console.warn("锁定图层不能设为活动图层");
return false;
}
this.oldActiveObjects = this.canvas.getActiveObjects();
// 设置为活动图层
this.activeLayerId.value = this.layerId;
// 如果在选择模式下,取消所有选择
if (this.editorMode === OperationType.SELECT && this.canvas) {
this.canvas.discardActiveObject();
// 设置为新的图层下的对象为激活,但需要确保对象存在于画布上
if (
this.newLayer.fabricObjects &&
this.newLayer.fabricObjects.length > 0
) {
const canvasObjects = this.canvas.getObjects();
const validObjects = this.newLayer.fabricObjects.filter(
(obj) => obj && canvasObjects.includes(obj)
);
if (validObjects.length > 0) {
if (validObjects.length === 1) {
// 只有一个对象时直接设置
this.canvas.setActiveObject(validObjects[0]);
} else {
// 多个对象时创建活动选择组
const activeSelection = new fabric.ActiveSelection(validObjects, {
canvas: this.canvas,
});
this.canvas.setActiveObject(activeSelection);
}
}
}
this.canvas.renderAll();
}
return true;
}
undo() {
// 恢复原活动图层ID
this.activeLayerId.value = this.oldActiveLayerId;
// 如果在选择模式下,恢复取消的所有选择
if (this.editorMode === OperationType.SELECT && this.canvas) {
this.canvas.discardActiveObject();
// 修复:确保对象存在于画布上才激活
if (this.oldActiveObjects && this.oldActiveObjects.length > 0) {
const canvasObjects = this.canvas.getObjects();
const validObjects = this.oldActiveObjects.filter(
(obj) => obj && canvasObjects.includes(obj)
);
if (validObjects.length > 0) {
if (validObjects.length > 1) {
// 如果有多个对象,需要创建一个活动选择组
const activeSelection = new fabric.ActiveSelection(validObjects, {
canvas: this.canvas,
});
this.canvas.setActiveObject(activeSelection);
} else {
// 只有一个对象时直接设置
this.canvas.setActiveObject(validObjects[0]);
}
}
}
this.canvas.renderAll();
}
}
getInfo() {
return {
name: this.name,
layerId: this.layerId,
oldActiveLayerId: this.oldActiveLayerId,
};
}
}
/**
* 添加对象到图层命令
*/
export class AddObjectToLayerCommand extends Command {
constructor(options) {
super({
name: "添加对象到图层",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.layerId = options.layerId;
this.fabricObject = options.fabricObject;
// 保存对象原始状态和ID
this.objectId =
this.fabricObject.id ||
`obj_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
this.originalObjectState = this.fabricObject.toObject([
"id",
"layerId",
"layerName",
]);
}
execute() {
// 查找目标图层
const layer = this.layers.value.find((l) => l.id === this.layerId);
if (!layer) {
console.error(`图层 ${this.layerId} 不存在`);
return null;
}
// 如果是背景层,不允许添加对象
if (layer.isBackground) {
console.warn("不能向背景层添加对象");
return null;
}
// 为对象生成唯一ID
this.fabricObject.id = this.objectId;
// 设置对象与图层的关联
this.fabricObject.layerId = this.layerId;
this.fabricObject.layerName = layer.name;
// 设置对象可操作可选择
this.fabricObject.selectable = true;
this.fabricObject.evented = true;
// 将对象添加到画布
this.canvas.add(this.fabricObject);
// 将对象添加到图层的fabricObjects数组
layer.fabricObjects = layer.fabricObjects || [];
layer.fabricObjects.push(this.fabricObject);
this.canvas.discardActiveObject();
// 确保对象确实存在于画布上才激活
const canvasObjects = this.canvas.getObjects();
const validObjects = layer.fabricObjects.filter(
(obj) => obj && canvasObjects.includes(obj)
);
if (validObjects.length > 0) {
if (validObjects.length === 1) {
// 只有一个对象时直接设置
this.canvas.setActiveObject(validObjects[0]);
} else {
// 多个对象时创建活动选择组
const activeSelection = new fabric.ActiveSelection(validObjects, {
canvas: this.canvas,
});
this.canvas.setActiveObject(activeSelection);
}
}
// 更新画布
this.canvas.renderAll();
return this.fabricObject;
}
undo() {
// 查找图层
const layer = this.layers.value.find((l) => l.id === this.layerId);
if (!layer) {
return false;
}
// 从图层的fabricObjects数组中移除对象
if (layer.fabricObjects) {
layer.fabricObjects = layer.fabricObjects.filter(
(obj) => obj.id !== this.objectId
);
}
// 从画布移除对象
const object = this.canvas
.getObjects()
.find((obj) => obj.id === this.objectId);
if (object) {
// 先丢弃活动对象,避免控制点渲染错误
this.canvas.discardActiveObject();
this.canvas.remove(object);
// 更新画布
this.canvas.renderAll();
}
return true;
}
getInfo() {
return {
name: this.name,
layerId: this.layerId,
objectId: this.objectId,
};
}
}
/**
* 从图层中移除对象命令
*/
export class RemoveObjectFromLayerCommand extends Command {
constructor(options) {
super({
name: "从图层中移除对象",
saveState: true,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.objectId = options.objectId;
// 查找对象和图层
this.object =
typeof options.objectOrId === "object"
? options.objectOrId
: this.canvas.getObjects().find((obj) => obj.id === this.objectId);
if (this.object) {
this.layerId = this.object.layerId;
this.objectData = this.object.toObject(["id", "layerId", "layerName"]);
}
}
execute() {
if (!this.object) {
console.error(`对象 ${this.objectId} 不存在`);
return false;
}
if (!this.layerId) {
console.error(`对象 ${this.objectId} 未关联到任何图层`);
return false;
}
// 查找图层
const layer = this.layers.value.find((l) => l.id === this.layerId);
if (!layer) {
console.error(`图层 ${this.layerId} 不存在`);
return false;
}
// 从画布移除对象
this.canvas.remove(this.object);
// 从图层的fabricObjects数组移除对象
if (layer.fabricObjects) {
layer.fabricObjects = layer.fabricObjects.filter(
(obj) => obj.id !== this.objectId
);
}
// 更新画布
this.canvas.renderAll();
return true;
}
undo() {
if (!this.objectData || !this.layerId) {
return false;
}
// 查找图层
const layer = this.layers.value.find((l) => l.id === this.layerId);
if (!layer) {
return false;
}
// 恢复对象到画布
fabric.util.enlivenObjects([this.objectData], (objects) => {
const restoredObject = objects[0];
// 将对象添加到画布
this.canvas.add(restoredObject);
// 将对象添加回图层
layer.fabricObjects = layer.fabricObjects || [];
layer.fabricObjects.push(restoredObject);
// 更新画布
this.canvas.renderAll();
});
return true;
}
getInfo() {
return {
name: this.name,
objectId: this.objectId,
layerId: this.layerId,
};
}
}
/**
* 更换固定图层图像命令
* 专门用于更换固定图层(如背景图层)的图像
*/
export class ChangeFixedImageCommand extends Command {
constructor(options = {}) {
super();
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.imageUrl = options.imageUrl;
this.targetLayerType = options.targetLayerType || "background"; // 'background', 'fixed', etc.
this.position = options.position || { x: 0, y: 0 };
this.scale = options.scale || { x: 1, y: 1 };
this.preserveTransform = options.preserveTransform !== false; // 默认保留变换
// 用于回滚的状态
this.previousImage = null;
this.previousTransform = null;
this.targetLayer = null;
this.isExecuted = false;
// 错误处理
this.maxRetries = options.maxRetries || 3;
this.retryCount = 0;
this.timeoutMs = options.timeoutMs || 10000;
}
async execute() {
try {
this.validateInputs();
// 查找目标图层
this.targetLayer = this.findTargetLayer();
if (!this.targetLayer) {
throw new Error(`找不到目标图层类型: ${this.targetLayerType}`);
}
// 保存当前状态用于回滚
await this.saveCurrentState();
// 加载新图像
const newImage = await this.loadImageWithRetry();
// 应用图像到图层
await this.applyImageToLayer(newImage);
this.isExecuted = true;
// 触发成功事件
this.emitEvent("image:changed", {
layerId: this.targetLayer.id,
newImageUrl: this.imageUrl,
command: this,
});
return {
success: true,
layerId: this.targetLayer.id,
imageUrl: this.imageUrl,
};
} catch (error) {
console.error("ChangeFixedImageCommand执行失败:", error);
// 如果已经执行了部分操作,尝试回滚
if (this.isExecuted) {
try {
await this.undo();
} catch (rollbackError) {
console.error("回滚失败:", rollbackError);
}
}
throw error;
}
}
async undo() {
if (!this.isExecuted || !this.targetLayer) {
throw new Error("命令未执行或目标图层不存在");
}
try {
if (this.previousImage) {
// 恢复之前的图像
await this.restorePreviousImage();
} else {
// 如果没有之前的图像,移除当前图像
await this.removeCurrentImage();
}
this.isExecuted = false;
// 触发撤销事件
this.emitEvent("image:reverted", {
layerId: this.targetLayer.id,
command: this,
});
return {
success: true,
action: "reverted",
layerId: this.targetLayer.id,
};
} catch (error) {
console.error("ChangeFixedImageCommand撤销失败:", error);
throw error;
}
}
validateInputs() {
if (!this.canvas) throw new Error("Canvas实例是必需的");
if (!this.layerManager) throw new Error("LayerManager实例是必需的");
if (!this.imageUrl) throw new Error("图像URL是必需的");
// 验证URL格式
try {
new URL(this.imageUrl);
} catch {
throw new Error("无效的图像URL格式");
}
}
findTargetLayer() {
const layers = this.layerManager.layers?.value || [];
switch (this.targetLayerType) {
case "background":
return layers.find((layer) => layer.isBackground);
case "fixed":
return layers.find((layer) => layer.isFixed);
default:
return layers.find((layer) => layer.type === this.targetLayerType);
}
}
async saveCurrentState() {
if (!this.targetLayer.fabricObject) return;
const currentObj = this.targetLayer.fabricObject;
// 保存当前图像URL如果存在
this.previousImage = {
url: currentObj.getSrc ? currentObj.getSrc() : null,
element: currentObj._element ? currentObj._element.cloneNode() : null,
};
// 保存变换状态
this.previousTransform = {
left: currentObj.left,
top: currentObj.top,
scaleX: currentObj.scaleX,
scaleY: currentObj.scaleY,
angle: currentObj.angle,
flipX: currentObj.flipX,
flipY: currentObj.flipY,
opacity: currentObj.opacity,
};
}
async loadImageWithRetry() {
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await this.loadImage();
} catch (error) {
this.retryCount = attempt;
if (attempt === this.maxRetries) {
throw new Error(
`图像加载失败,已重试${this.maxRetries}次: ${error.message}`
);
}
// 指数退避重试
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
console.warn(
`图像加载重试 ${attempt + 1}/${this.maxRetries}:`,
error.message
);
}
}
}
loadImage() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`)
);
}, this.timeoutMs);
fabric.Image.fromURL(
this.imageUrl,
(img) => {
clearTimeout(timeout);
if (!img || !img.getElement()) {
reject(new Error("图像加载失败或无效"));
return;
}
resolve(img);
},
{
crossOrigin: "anonymous",
}
);
});
}
async applyImageToLayer(newImage) {
const currentObj = this.targetLayer.fabricObject;
// 设置基本属性
newImage.set({
id: currentObj?.id || generateId(),
layerId: this.targetLayer.id,
layerName: this.targetLayer.name,
isBackground: this.targetLayer.isBackground,
isFixed: this.targetLayer.isFixed,
});
// 应用位置和变换
if (this.preserveTransform && this.previousTransform) {
newImage.set(this.previousTransform);
} else {
newImage.set({
left: this.position.x,
top: this.position.y,
scaleX: this.scale.x,
scaleY: this.scale.y,
});
}
// 移除旧对象(如果存在)
if (currentObj) {
this.canvas.remove(currentObj);
}
// 添加新图像
this.canvas.add(newImage);
newImage.setCoords();
// 更新图层引用
this.targetLayer.fabricObject = newImage;
// 更新图层管理器
this.layerManager.updateLayerObject(this.targetLayer.id, newImage);
// 重新渲染画布
this.canvas.renderAll();
}
async restorePreviousImage() {
if (!this.previousImage.url) return;
const restoredImage = await this.loadImageFromUrl(this.previousImage.url);
// 恢复之前的变换
if (this.previousTransform) {
restoredImage.set(this.previousTransform);
}
// 设置图层属性
restoredImage.set({
id: this.targetLayer.fabricObject?.id || generateId(),
layerId: this.targetLayer.id,
layerName: this.targetLayer.name,
isBackground: this.targetLayer.isBackground,
isFixed: this.targetLayer.isFixed,
});
// 替换当前对象
if (this.targetLayer.fabricObject) {
this.canvas.remove(this.targetLayer.fabricObject);
}
this.canvas.add(restoredImage);
restoredImage.setCoords();
// 更新引用
this.targetLayer.fabricObject = restoredImage;
this.layerManager.updateLayerObject(this.targetLayer.id, restoredImage);
this.canvas.renderAll();
}
async removeCurrentImage() {
if (this.targetLayer.fabricObject) {
this.canvas.remove(this.targetLayer.fabricObject);
this.targetLayer.fabricObject = null;
this.layerManager.updateLayerObject(this.targetLayer.id, null);
this.canvas.renderAll();
}
}
loadImageFromUrl(url) {
return new Promise((resolve, reject) => {
fabric.Image.fromURL(
url,
(img) => {
if (!img || !img.getElement()) {
reject(new Error("恢复图像加载失败"));
return;
}
resolve(img);
},
{ crossOrigin: "anonymous" }
);
});
}
emitEvent(eventName, data) {
if (this.canvas && this.canvas.fire) {
this.canvas.fire(eventName, data);
}
}
// 获取命令信息用于调试
getCommandInfo() {
return {
type: "ChangeFixedImageCommand",
targetLayerType: this.targetLayerType,
imageUrl: this.imageUrl,
isExecuted: this.isExecuted,
retryCount: this.retryCount,
targetLayerId: this.targetLayer?.id,
preserveTransform: this.preserveTransform,
};
}
}
/**
* 向图层添加图像命令
* 用于向指定图层添加新的图像对象
*/
export class AddImageToLayerCommand extends Command {
constructor(options = {}) {
super();
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.imageUrl = options.imageUrl;
this.layerId = options.layerId;
this.position = options.position || { x: 100, y: 100 };
this.scale = options.scale || { x: 1, y: 1 };
this.zIndex = options.zIndex || null; // 可选的层级控制
// 用于回滚的状态
this.addedObject = null;
this.targetLayer = null;
this.isExecuted = false;
// 错误处理
this.maxRetries = options.maxRetries || 3;
this.retryCount = 0;
this.timeoutMs = options.timeoutMs || 10000;
}
async execute() {
try {
this.validateInputs();
// 查找目标图层
this.targetLayer = this.findTargetLayer();
if (!this.targetLayer) {
throw new Error(`找不到目标图层: ${this.layerId}`);
}
// 检查图层是否可编辑
this.validateLayerEditability();
// 加载新图像
const newImage = await this.loadImageWithRetry();
// 添加图像到图层
await this.addImageToLayer(newImage);
this.isExecuted = true;
// 触发成功事件
this.emitEvent("image:added", {
layerId: this.layerId,
objectId: this.addedObject.id,
imageUrl: this.imageUrl,
command: this,
});
return {
success: true,
layerId: this.layerId,
objectId: this.addedObject.id,
imageUrl: this.imageUrl,
};
} catch (error) {
console.error("AddImageToLayerCommand执行失败:", error);
// 如果已经添加了对象,尝试移除
if (this.addedObject) {
try {
await this.undo();
} catch (rollbackError) {
console.error("回滚失败:", rollbackError);
}
}
throw error;
}
}
async undo() {
if (!this.isExecuted || !this.addedObject) {
throw new Error("命令未执行或没有添加的对象");
}
try {
// 移除添加的对象
this.canvas.remove(this.addedObject);
// 从图层管理器中移除
this.layerManager.removeObjectFromLayer(
this.addedObject.id,
this.layerId
);
this.isExecuted = false;
// 触发撤销事件
this.emitEvent("image:removed", {
layerId: this.layerId,
objectId: this.addedObject.id,
command: this,
});
// 重新渲染
this.canvas.renderAll();
return {
success: true,
action: "removed",
layerId: this.layerId,
objectId: this.addedObject.id,
};
} catch (error) {
console.error("AddImageToLayerCommand撤销失败:", error);
throw error;
}
}
validateInputs() {
if (!this.canvas) throw new Error("Canvas实例是必需的");
if (!this.layerManager) throw new Error("LayerManager实例是必需的");
if (!this.imageUrl) throw new Error("图像URL是必需的");
if (!this.layerId) throw new Error("图层ID是必需的");
// 验证URL格式
try {
new URL(this.imageUrl);
} catch {
throw new Error("无效的图像URL格式");
}
}
findTargetLayer() {
const layers = this.layerManager.layers?.value || [];
return layers.find((layer) => layer.id === this.layerId);
}
validateLayerEditability() {
if (this.targetLayer.locked) {
throw new Error("目标图层已锁定,无法添加对象");
}
if (!this.targetLayer.visible) {
console.warn("目标图层不可见,添加的对象可能不会显示");
}
}
async loadImageWithRetry() {
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await this.loadImage();
} catch (error) {
this.retryCount = attempt;
if (attempt === this.maxRetries) {
throw new Error(
`图像加载失败,已重试${this.maxRetries}次: ${error.message}`
);
}
// 指数退避重试
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
console.warn(
`图像加载重试 ${attempt + 1}/${this.maxRetries}:`,
error.message
);
}
}
}
loadImage() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(`图像加载超时 (${this.timeoutMs}ms): ${this.imageUrl}`)
);
}, this.timeoutMs);
fabric.Image.fromURL(
this.imageUrl,
(img) => {
clearTimeout(timeout);
if (!img || !img.getElement()) {
reject(new Error("图像加载失败或无效"));
return;
}
resolve(img);
},
{
crossOrigin: "anonymous",
}
);
});
}
async addImageToLayer(newImage) {
// 生成唯一ID
const objectId = generateId();
// 设置图像属性
newImage.set({
id: objectId,
layerId: this.layerId,
layerName: this.targetLayer.name,
left: this.position.x,
top: this.position.y,
scaleX: this.scale.x,
scaleY: this.scale.y,
selectable: true,
evented: true,
});
// 添加到画布
this.canvas.add(newImage);
// 设置层级
if (this.zIndex !== null) {
this.setObjectZIndex(newImage, this.zIndex);
}
newImage.setCoords();
// 保存引用用于回滚
this.addedObject = newImage;
// 添加到图层管理器
this.layerManager.addObjectToLayer(newImage, this.layerId);
// 重新渲染画布
this.canvas.renderAll();
}
setObjectZIndex(object, zIndex) {
if (zIndex === "top") {
object.bringToFront();
} else if (zIndex === "bottom") {
object.sendToBack();
} else if (typeof zIndex === "number") {
object.moveTo(zIndex);
}
}
emitEvent(eventName, data) {
if (this.canvas && this.canvas.fire) {
this.canvas.fire(eventName, data);
}
}
// 获取命令信息用于调试
getCommandInfo() {
return {
type: "AddImageToLayerCommand",
layerId: this.layerId,
imageUrl: this.imageUrl,
position: this.position,
scale: this.scale,
isExecuted: this.isExecuted,
retryCount: this.retryCount,
addedObjectId: this.addedObject?.id,
};
}
}

View File

@@ -0,0 +1,379 @@
import { OperationType } from "../utils/layerHelper.js";
import { Command, CompositeCommand } from "./Command.js";
//import { fabric } from "fabric-with-all";
/**
* 批量初始化红绿图模式命令
* 将衣服底图添加到背景层、红绿图添加到固定图层、调整位置和大小,以及设置画布背景为白色等操作合并到一个命令中
* 减少页面闪烁,一次性渲染完成
*/
export class BatchInitializeRedGreenModeCommand extends Command {
constructor(options = {}) {
super({
name: "批量初始化红绿图模式",
description: "一次性完成红绿图模式的所有初始化操作",
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.toolManager = options.toolManager;
this.clothingImageUrl = options.clothingImageUrl;
this.redGreenImageUrl = options.redGreenImageUrl;
this.onImageGenerated = options.onImageGenerated;
this.normalLayerOpacity = options.normalLayerOpacity || 0.4;
// 存储原始状态以便撤销
this.originalCanvasBackground = null;
this.originalBackgroundObject = null;
this.originalFixedObjects = null;
this.originalNormalObjects = null;
this.originalNormalOpacities = new Map();
this.originalToolState = null;
// 存储加载的图片对象
this.clothingImage = null;
this.redGreenImage = null;
}
async execute() {
try {
// 禁用画布渲染以避免闪烁
this.canvas.renderOnAddRemove = false;
// 1. 设置画布背景为白色
this.originalCanvasBackground = this.canvas.backgroundColor;
this.canvas.setBackgroundColor('#ffffff', () => {});
// 2. 查找图层结构
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
if (!backgroundLayer || !fixedLayer || normalLayers.length === 0) {
throw new Error("缺少必要的图层结构");
}
const normalLayer = normalLayers[0]; // 使用第一个普通图层
// 3. 保存原始状态
this.originalBackgroundObject = backgroundLayer.fabricObject ? {
...backgroundLayer.fabricObject.toObject(),
ref: backgroundLayer.fabricObject
} : null;
this.originalFixedObjects = fixedLayer.fabricObject
? [fixedLayer.fabricObject]
: [];
this.originalNormalObjects = normalLayer.fabricObjects
? [...normalLayer.fabricObjects]
: [];
// 保存普通图层透明度
normalLayers.forEach((layer) => {
this.originalNormalOpacities.set(layer.id, layer.opacity || 1);
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
this.originalNormalOpacities.set(
`${layer.id}_${obj.id || "unknown"}`,
obj.opacity || 1
);
});
}
});
// 保存工具状态
if (this.toolManager) {
this.originalToolState = {
currentTool: this.toolManager.getCurrentTool(),
isRedGreenMode: this.toolManager.isRedGreenMode,
};
}
// 4. 确保背景图层大小正确
await this._setupBackgroundLayer(backgroundLayer);
// 5. 并行加载两个图片
const [clothingImg, redGreenImg] = await Promise.all([
this._loadImage(this.clothingImageUrl),
this._loadImage(this.redGreenImageUrl)
]);
// 6. 设置衣服底图到固定图层
await this._setupClothingImage(clothingImg, fixedLayer);
// 7. 设置红绿图到普通图层,位置和大小与衣服底图一致
await this._setupRedGreenImage(redGreenImg, normalLayer, this.clothingImage);
// 8. 设置普通图层透明度
this._setupNormalLayerOpacity(normalLayers);
// 9. 配置工具管理器
this._setupToolManager();
// 10. 重新启用渲染并执行一次性渲染
this.canvas.renderOnAddRemove = true;
this.canvas.renderAll();
console.log("批量红绿图模式初始化完成", {
衣服底图: this.clothingImageUrl,
红绿图: this.redGreenImageUrl,
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
画布背景: "白色",
});
return true;
} catch (error) {
// 恢复渲染
this.canvas.renderOnAddRemove = true;
console.error("批量红绿图模式初始化失败:", error);
throw error;
}
}
async undo() {
try {
// 禁用渲染
this.canvas.renderOnAddRemove = false;
// 1. 恢复画布背景
if (this.originalCanvasBackground !== null) {
this.canvas.setBackgroundColor(this.originalCanvasBackground, () => {});
}
// 2. 恢复图层对象
const layers = this.layerManager.layers?.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
// 移除当前添加的对象
if (this.clothingImage) {
this.canvas.remove(this.clothingImage);
}
if (this.redGreenImage) {
this.canvas.remove(this.redGreenImage);
}
// 恢复背景图层
if (backgroundLayer && this.originalBackgroundObject) {
if (this.originalBackgroundObject.ref) {
backgroundLayer.fabricObject = this.originalBackgroundObject.ref;
}
}
// 恢复固定图层
if (fixedLayer) {
fixedLayer.fabricObject = this.originalFixedObjects.length > 0
? this.originalFixedObjects[0]
: null;
if (fixedLayer.fabricObject) {
this.canvas.add(fixedLayer.fabricObject);
}
}
// 恢复普通图层
if (normalLayers.length > 0) {
const normalLayer = normalLayers[0];
normalLayer.fabricObjects = [...this.originalNormalObjects];
this.originalNormalObjects.forEach((obj) => {
this.canvas.add(obj);
});
}
// 3. 恢复透明度
normalLayers.forEach((layer) => {
if (this.originalNormalOpacities.has(layer.id)) {
layer.opacity = this.originalNormalOpacities.get(layer.id);
}
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
const key = `${layer.id}_${obj.id || "unknown"}`;
if (this.originalNormalOpacities.has(key)) {
obj.opacity = this.originalNormalOpacities.get(key);
}
});
}
});
// 4. 恢复工具状态
if (this.toolManager && this.originalToolState) {
this.toolManager.isRedGreenMode = this.originalToolState.isRedGreenMode;
if (this.originalToolState.currentTool) {
this.toolManager.setTool(this.originalToolState.currentTool);
}
}
// 5. 重新启用渲染
this.canvas.renderOnAddRemove = true;
this.canvas.renderAll();
return true;
} catch (error) {
this.canvas.renderOnAddRemove = true;
console.error("撤销批量红绿图模式初始化失败:", error);
return false;
}
}
/**
* 设置背景图层
*/
async _setupBackgroundLayer(backgroundLayer) {
let backgroundObject = backgroundLayer.fabricObject;
if (!backgroundObject) {
// 创建白色背景矩形
backgroundObject = new fabric.Rect({
left: 0,
top: 0,
width: this.canvas.width,
height: this.canvas.height,
fill: "#ffffff",
selectable: false,
evented: false,
isBackground: true,
layerId: backgroundLayer.id,
layerName: backgroundLayer.name,
});
this.canvas.add(backgroundObject);
this.canvas.sendToBack(backgroundObject);
backgroundLayer.fabricObject = backgroundObject;
} else {
// 更新现有背景对象大小
backgroundObject.set({
width: this.canvas.width,
height: this.canvas.height,
left: 0,
top: 0,
fill: "#ffffff", // 确保背景是白色
});
}
}
/**
* 加载图片
*/
async _loadImage(imageUrl) {
return new Promise((resolve, reject) => {
fabric.Image.fromURL(
imageUrl,
(img) => {
if (!img) {
reject(new Error(`无法加载图片: ${imageUrl}`));
return;
}
resolve(img);
},
{ crossOrigin: "anonymous" }
);
});
}
/**
* 设置衣服底图
*/
async _setupClothingImage(img, fixedLayer) {
// 计算图片缩放,保持上下留边距
const margin = 50;
const maxWidth = this.canvas.width - margin * 2;
const maxHeight = this.canvas.height - margin * 2;
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
img.set({
scaleX: scale,
scaleY: scale,
left: this.canvas.width / 2,
top: this.canvas.height / 2,
originX: "center",
originY: "center",
selectable: false,
evented: false,
layerId: fixedLayer.id,
layerName: fixedLayer.name,
});
// 清除固定图层原有内容
if (fixedLayer.fabricObject) {
this.canvas.remove(fixedLayer.fabricObject);
}
// 添加到画布和固定图层
this.canvas.add(img);
fixedLayer.fabricObject = img;
this.clothingImage = img;
}
/**
* 设置红绿图
*/
async _setupRedGreenImage(img, normalLayer, clothingImage) {
if (!clothingImage) {
throw new Error("衣服底图未加载,无法设置红绿图位置");
}
// 使用与衣服底图完全相同的属性
img.set({
scaleX: clothingImage.scaleX,
scaleY: clothingImage.scaleY,
left: clothingImage.left,
top: clothingImage.top,
originX: clothingImage.originX,
originY: clothingImage.originY,
selectable: false,
evented: false,
layerId: normalLayer.id,
layerName: normalLayer.name,
});
// 清除普通图层原有内容
if (normalLayer.fabricObjects) {
normalLayer.fabricObjects.forEach((obj) => {
this.canvas.remove(obj);
});
}
// 添加到画布和普通图层
this.canvas.add(img);
normalLayer.fabricObjects = [img];
this.redGreenImage = img;
}
/**
* 设置普通图层透明度
*/
_setupNormalLayerOpacity(normalLayers) {
normalLayers.forEach((layer) => {
// 设置图层透明度
layer.opacity = this.normalLayerOpacity;
// 更新图层中所有对象的透明度
if (layer.fabricObjects) {
layer.fabricObjects.forEach((obj) => {
obj.opacity = this.normalLayerOpacity;
});
}
});
}
/**
* 设置工具管理器
*/
_setupToolManager() {
if (this.toolManager) {
// 设置红绿图模式
this.toolManager.isRedGreenMode = true;
// 切换到红色笔刷工具
this.toolManager.setTool(OperationType.RED_BRUSH);
}
}
}

View File

@@ -0,0 +1,578 @@
import { Command, CompositeCommand } from "./Command.js";
//import { fabric } from "fabric-with-all";
import { createLayer, LayerType } from "../utils/layerHelper.js";
/**
* 创建选区命令
*/
export class CreateSelectionCommand extends Command {
constructor(options = {}) {
super({
name: options.name || "创建选区",
description: "在画布上创建选区",
saveState: false,
});
this.canvas = options.canvas;
this.selectionManager = options.selectionManager;
this.selectionObject = options.selectionObject;
this.selectionType = options.selectionType || "rectangle";
}
async execute() {
if (!this.canvas || !this.selectionManager || !this.selectionObject) {
console.error("无法创建选区:参数无效");
return false;
}
// 将选择对象添加到选区管理器
this.selectionManager.setSelectionObject(this.selectionObject);
return true;
}
async undo() {
if (!this.selectionManager) return false;
this.selectionManager.clearSelection();
return true;
}
}
/**
* 反转选区命令
*/
export class InvertSelectionCommand extends Command {
constructor(options = {}) {
super({
name: "反转选区",
description: "反转当前选区",
saveState: false,
});
this.canvas = options.canvas;
this.selectionManager = options.selectionManager;
this.originalSelection = options.selectionManager
? options.selectionManager.getSelectionPath()
: null;
}
async execute() {
if (!this.canvas || !this.selectionManager) {
console.error("无法反转选区:参数无效");
return false;
}
// 保存原始选区
if (!this.originalSelection) {
this.originalSelection = this.selectionManager.getSelectionPath();
}
// 反转选区
const result = await this.selectionManager.invertSelection();
return result;
}
async undo() {
if (!this.selectionManager || !this.originalSelection) return false;
// 恢复原始选区
this.selectionManager.setSelectionFromPath(this.originalSelection);
return true;
}
}
/**
* 添加到选区命令
*/
export class AddToSelectionCommand extends Command {
constructor(options = {}) {
super({
name: "添加到选区",
description: "将新的选区添加到现有选区",
saveState: false,
});
this.canvas = options.canvas;
this.selectionManager = options.selectionManager;
this.newSelection = options.newSelection;
this.originalSelection = options.selectionManager
? options.selectionManager.getSelectionPath()
: null;
}
async execute() {
if (!this.canvas || !this.selectionManager || !this.newSelection) {
console.error("无法添加到选区:参数无效");
return false;
}
// 保存原始选区
if (!this.originalSelection) {
this.originalSelection = this.selectionManager.getSelectionPath();
}
// 添加到选区
const result = await this.selectionManager.addToSelection(
this.newSelection
);
return result;
}
async undo() {
if (!this.selectionManager || !this.originalSelection) return false;
// 恢复原始选区
this.selectionManager.setSelectionFromPath(this.originalSelection);
return true;
}
}
/**
* 从选区中移除命令
*/
export class RemoveFromSelectionCommand extends Command {
constructor(options = {}) {
super({
name: "从选区中移除",
description: "从现有选区中移除指定区域",
saveState: false,
});
this.canvas = options.canvas;
this.selectionManager = options.selectionManager;
this.removeSelection = options.removeSelection;
this.originalSelection = options.selectionManager
? options.selectionManager.getSelectionPath()
: null;
}
async execute() {
if (!this.canvas || !this.selectionManager || !this.removeSelection) {
console.error("无法从选区中移除:参数无效");
return false;
}
// 保存原始选区
if (!this.originalSelection) {
this.originalSelection = this.selectionManager.getSelectionPath();
}
// 从选区中移除
const result = await this.selectionManager.removeFromSelection(
this.removeSelection
);
return result;
}
async undo() {
if (!this.selectionManager || !this.originalSelection) return false;
// 恢复原始选区
this.selectionManager.setSelectionFromPath(this.originalSelection);
return true;
}
}
/**
* 清除选区命令
*/
export class ClearSelectionCommand extends Command {
constructor(options = {}) {
super({
name: "清除选区",
description: "清除当前选区",
saveState: false,
});
this.selectionManager = options.selectionManager;
this.originalSelection = options.selectionManager
? options.selectionManager.getSelectionPath()
: null;
}
async execute() {
if (!this.selectionManager) {
console.error("无法清除选区:参数无效");
return false;
}
// 保存原始选区
if (!this.originalSelection) {
this.originalSelection = this.selectionManager.getSelectionPath();
}
// 清除选区
this.selectionManager.clearSelection();
return true;
}
async undo() {
if (!this.selectionManager || !this.originalSelection) return false;
// 恢复原始选区
this.selectionManager.setSelectionFromPath(this.originalSelection);
return true;
}
}
/**
* 羽化选区命令
*/
export class FeatherSelectionCommand extends Command {
constructor(options = {}) {
super({
name: "羽化选区",
description: "对当前选区应用羽化效果",
saveState: false,
});
this.selectionManager = options.selectionManager;
this.featherAmount = options.featherAmount || 5;
this.originalSelection = options.selectionManager
? options.selectionManager.getSelectionPath()
: null;
this.originalFeatherAmount = options.selectionManager
? options.selectionManager.getFeatherAmount()
: 0;
}
async execute() {
if (!this.selectionManager) {
console.error("无法羽化选区:参数无效");
return false;
}
// 保存原始选区和羽化值
if (!this.originalSelection) {
this.originalSelection = this.selectionManager.getSelectionPath();
this.originalFeatherAmount = this.selectionManager.getFeatherAmount();
}
// 应用羽化
const result = await this.selectionManager.featherSelection(
this.featherAmount
);
return result;
}
async undo() {
if (!this.selectionManager || !this.originalSelection) return false;
// 恢复原始选区和羽化值
this.selectionManager.setSelectionFromPath(this.originalSelection);
this.selectionManager.setFeatherAmount(this.originalFeatherAmount);
return true;
}
}
/**
* 填充选区命令
*/
export class FillSelectionCommand extends Command {
constructor(options = {}) {
super({
name: "填充选区",
description: "使用指定颜色填充当前选区",
saveState: false,
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.selectionManager = options.selectionManager;
this.color = options.color || "#000000";
this.targetLayerId = options.targetLayerId;
this.createdObjectIds = [];
}
async execute() {
if (!this.canvas || !this.layerManager || !this.selectionManager) {
console.error("无法填充选区:参数无效");
return false;
}
// 获取选区路径
const selectionPath = this.selectionManager.getSelectionObject();
if (!selectionPath) {
console.error("无法填充选区:当前没有选区");
return false;
}
// 确定目标图层
const layerId = this.targetLayerId || this.layerManager.getActiveLayerId();
if (!layerId) {
console.error("无法填充选区:没有活动图层");
return false;
}
// 创建填充对象
const fillObject = new fabric.Path(selectionPath.path, {
fill: this.color,
stroke: null,
opacity: 1,
id: `fill_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
layerId: layerId,
selectable: false,
});
// 应用羽化效果(如果有)
const featherAmount = this.selectionManager.getFeatherAmount();
if (featherAmount > 0) {
fillObject.shadow = new fabric.Shadow({
color: this.color,
blur: featherAmount,
offsetX: 0,
offsetY: 0,
});
}
// 添加到图层
this.layerManager.addObjectToLayer(layerId, fillObject);
this.createdObjectIds.push(fillObject.id);
// 清空选区
this.selectionManager.clearSelection();
return true;
}
async undo() {
if (!this.layerManager || this.createdObjectIds.length === 0) return false;
// 移除创建的填充对象
for (const id of this.createdObjectIds) {
const layerObj = this._findObjectInLayers(id);
if (layerObj) {
this.layerManager.removeObjectFromLayer(layerObj.layerId, id);
}
}
return true;
}
_findObjectInLayers(objectId) {
if (!this.layerManager) return null;
const layers = this.layerManager.layers.value;
for (const layer of layers) {
if (layer.fabricObjects) {
const obj = layer.fabricObjects.find((obj) => obj.id === objectId);
if (obj) return { object: obj, layerId: layer.id };
}
}
return null;
}
}
/**
* 复制选区内容到新图层命令
*/
export class CopySelectionToNewLayerCommand extends CompositeCommand {
constructor(options = {}) {
super([], {
name: "复制选区到新图层",
description: "将选区中的内容复制到新图层",
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.selectionManager = options.selectionManager;
this.sourceLayerId = options.sourceLayerId;
this.newLayerName = options.newLayerName || "选区复制";
this.newLayerId = null;
this.copiedObjectIds = [];
}
async execute() {
if (!this.canvas || !this.layerManager || !this.selectionManager) {
console.error("无法复制选区:参数无效");
return false;
}
try {
// 获取选区
const selectionObject = this.selectionManager.getSelectionObject();
if (!selectionObject) {
console.error("无法复制选区:当前没有选区");
return false;
}
// 确定源图层
const sourceId =
this.sourceLayerId || this.layerManager.getActiveLayerId();
const sourceLayer = this.layerManager.getLayerById(sourceId);
if (!sourceLayer || !sourceLayer.fabricObjects) {
console.error("无法复制选区:源图层无效或为空");
return false;
}
// 创建新图层
this.newLayerId = await this.layerManager.createLayer(
this.newLayerName,
LayerType.EMPTY
);
// 获取选区内的对象
const objectsToCopy = sourceLayer.fabricObjects.filter((obj) => {
return this.selectionManager.isObjectInSelection(obj);
});
if (objectsToCopy.length === 0) {
console.warn("选区内没有对象可复制");
return true; // 仍然返回成功,因为已创建了新图层
}
// 复制对象到新图层
for (const obj of objectsToCopy) {
// 克隆对象
const clonedObj = await this._cloneObject(obj);
// 设置新的ID和图层ID
const newId = `copy_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
clonedObj.id = newId;
clonedObj.layerId = this.newLayerId;
// 添加到新图层
await this.layerManager.addObjectToLayer(this.newLayerId, clonedObj);
this.copiedObjectIds.push(newId);
}
// 设置新图层为活动图层
this.layerManager.setActiveLayer(this.newLayerId);
// 清空选区
this.selectionManager.clearSelection();
return {
newLayerId: this.newLayerId,
copiedCount: this.copiedObjectIds.length,
};
} catch (error) {
console.error("复制选区过程中出错:", error);
// 如果已经创建了新图层,需要进行清理
if (this.newLayerId) {
try {
await this.layerManager.removeLayer(this.newLayerId);
} catch (cleanupError) {
console.warn("清理新图层失败:", cleanupError);
}
}
throw error;
}
}
async _cloneObject(obj) {
return new Promise((resolve, reject) => {
if (!obj) {
reject(new Error("对象无效,无法克隆"));
return;
}
try {
obj.clone((cloned) => {
resolve(cloned);
});
} catch (error) {
reject(error);
}
});
}
}
/**
* 从选区中删除内容命令
*/
export class ClearSelectionContentCommand extends Command {
constructor(options = {}) {
super({
name: "清除选区内容",
description: "删除选区中的内容",
saveState: false,
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.selectionManager = options.selectionManager;
this.targetLayerId = options.targetLayerId;
this.removedObjects = [];
}
async execute() {
if (!this.canvas || !this.layerManager || !this.selectionManager) {
console.error("无法清除选区内容:参数无效");
return false;
}
// 获取选区
const selectionObject = this.selectionManager.getSelectionObject();
if (!selectionObject) {
console.error("无法清除选区内容:当前没有选区");
return false;
}
// 确定目标图层
const layerId = this.targetLayerId || this.layerManager.getActiveLayerId();
const layer = this.layerManager.getLayerById(layerId);
if (!layer || !layer.fabricObjects) {
console.error("无法清除选区内容:目标图层无效或为空");
return false;
}
// 找到选区内的对象
const objectsToRemove = layer.fabricObjects.filter((obj) => {
return this.selectionManager.isObjectInSelection(obj);
});
if (objectsToRemove.length === 0) {
console.warn("选区内没有对象需要清除");
return true;
}
// 备份被删除的对象
this.removedObjects = objectsToRemove.map((obj) => ({
object: this._cloneObjectSync(obj),
layerId: layerId,
}));
// 从图层中移除对象
for (const obj of objectsToRemove) {
this.layerManager.removeObjectFromLayer(layerId, obj.id);
}
return { removedCount: objectsToRemove.length };
}
async undo() {
if (!this.layerManager || this.removedObjects.length === 0) return false;
// 恢复被删除的对象
for (const item of this.removedObjects) {
if (item.object && item.layerId) {
const clonedObj = await this._cloneObject(item.object);
this.layerManager.addObjectToLayer(item.layerId, clonedObj);
}
}
return true;
}
_cloneObjectSync(obj) {
// 这是一个简单的深拷贝,不适用于所有场景
// 在实际应用中应该使用fabric.js的clone方法
if (!obj) return null;
return JSON.parse(JSON.stringify(obj));
}
async _cloneObject(obj) {
return new Promise((resolve, reject) => {
if (!obj) {
reject(new Error("对象无效,无法克隆"));
return;
}
try {
if (typeof obj.clone === "function") {
obj.clone((cloned) => {
resolve(cloned);
});
} else {
// 如果对象没有clone方法(可能是因为它已经是序列化后的对象)
// 在实际代码中需要适当处理这种情况
resolve(Object.assign({}, obj));
}
} catch (error) {
reject(error);
}
});
}
}
// 导入套索抠图命令
export { LassoCutoutCommand } from "./LassoCutoutCommand.js";

View File

@@ -0,0 +1,134 @@
import { Command } from "./Command";
/**
* 对象变换命令
* 轻量级命令,只记录对象的变换属性变化(位置、缩放、旋转)
* 不保存整个对象或画布状态,只关注变换属性
*/
export class TransformCommand extends Command {
constructor(options) {
super({
name: options.name || "对象变换",
description: options.description || "移动、缩放或旋转对象",
saveState: false, // 自己管理状态,避免递归
});
this.canvas = options.canvas;
this.objectId = options.objectId;
this.initialState = options.initialState || null;
this.finalState = options.finalState || null;
this.objectType = options.objectType || "object";
}
/**
* 执行命令
* 如果是首次执行,记录初始和最终状态
* 如果是重做,应用最终状态
*/
execute() {
if (!this.finalState) {
console.warn("没有最终状态可应用");
return false;
}
// 查找目标对象
const targetObject = this._findObject(this.objectId);
if (!targetObject) {
console.warn(`未找到ID为 ${this.objectId} 的对象`);
return false;
}
// 应用最终变换状态
this._applyTransform(targetObject, this.finalState);
// 触发画布更新
this.canvas.renderAll();
return true;
}
/**
* 撤销命令
* 应用初始状态
*/
undo() {
if (!this.initialState) {
console.warn("没有初始状态可恢复");
return false;
}
// 查找目标对象
const targetObject = this._findObject(this.objectId);
if (!targetObject) {
console.warn(`未找到ID为 ${this.objectId} 的对象`);
return false;
}
// 应用初始变换状态
this._applyTransform(targetObject, this.initialState);
// 触发画布更新
this.canvas.renderAll();
return true;
}
/**
* 查找对象
* @private
*/
_findObject(objectId) {
if (!this.canvas) return null;
return this.canvas.getObjects().find((obj) => obj.id === objectId);
}
/**
* 应用变换状态到对象
* @private
*/
_applyTransform(object, transformState) {
if (!object || !transformState) return;
// 应用变换属性,只设置真正变化的值
Object.entries(transformState).forEach(([key, value]) => {
object.set(key, value);
});
// 确保对象更新
object.setCoords();
}
/**
* 获取命令信息
*/
getInfo() {
return {
name: this.name,
description: this.description,
objectId: this.objectId,
objectType: this.objectType,
changedProps: this.finalState ? Object.keys(this.finalState) : [],
};
}
/**
* 捕获对象的变换状态
* @static
*/
static captureTransformState(object) {
if (!object) return null;
// 只捕获变换相关的属性
return {
left: object.left,
top: object.top,
scaleX: object.scaleX,
scaleY: object.scaleY,
angle: object.angle,
flipX: object.flipX,
flipY: object.flipY,
skewX: object.skewX,
skewY: object.skewY,
};
}
}

View File

@@ -0,0 +1,304 @@
import { Command } from "./Command";
/**
* 文本内容命令
* 用于更改文本图层的文本内容
*/
export class TextContentCommand extends Command {
constructor(options) {
super({
name: "修改文本内容",
description: "修改文本图层的文本内容",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.newText = options.newText;
this.oldText = this.textObject.text;
}
execute() {
this.textObject.set("text", this.newText);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set("text", this.oldText);
this.canvas.renderAll();
return true;
}
}
/**
* 文本字体命令
* 用于更改文本图层的字体
*/
export class TextFontCommand extends Command {
constructor(options) {
super({
name: "修改文本字体",
description: "修改文本图层的字体",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.newFont = options.newFont;
this.oldFont = this.textObject.fontFamily;
}
execute() {
this.textObject.set("fontFamily", this.newFont);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set("fontFamily", this.oldFont);
this.canvas.renderAll();
return true;
}
}
/**
* 文本尺寸命令
* 用于更改文本图层的字体大小
*/
export class TextSizeCommand extends Command {
constructor(options) {
super({
name: "修改文本尺寸",
description: "修改文本图层的字体大小",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.newSize = options.newSize;
this.oldSize = this.textObject.fontSize;
}
execute() {
this.textObject.set("fontSize", this.newSize);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set("fontSize", this.oldSize);
this.canvas.renderAll();
return true;
}
}
/**
* 文本颜色命令
* 用于更改文本图层的颜色
*/
export class TextColorCommand extends Command {
constructor(options) {
super({
name: "修改文本颜色",
description: "修改文本图层的颜色",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.newColor = options.newColor;
this.oldColor = this.textObject.fill;
}
execute() {
this.textObject.set("fill", this.newColor);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set("fill", this.oldColor);
this.canvas.renderAll();
return true;
}
}
/**
* 文本对齐方式命令
* 用于更改文本图层的对齐方式
*/
export class TextAlignCommand extends Command {
constructor(options) {
super({
name: "修改文本对齐",
description: "修改文本图层的对齐方式",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.newAlign = options.newAlign;
this.oldAlign = this.textObject.textAlign;
}
execute() {
this.textObject.set("textAlign", this.newAlign);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set("textAlign", this.oldAlign);
this.canvas.renderAll();
return true;
}
}
/**
* 文本样式命令
* 用于更改文本图层的样式(粗体、斜体、下划线等)
*/
export class TextStyleCommand extends Command {
constructor(options) {
super({
name: "修改文本样式",
description: "修改文本图层的样式",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.property = options.property; // 'fontWeight', 'fontStyle', 'underline', 'linethrough', 'overline'
this.newValue = options.newValue;
this.oldValue = this.textObject[this.property];
}
execute() {
this.textObject.set(this.property, this.newValue);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set(this.property, this.oldValue);
this.canvas.renderAll();
return true;
}
}
/**
* 文本间距命令
* 用于更改文本图层的字符间距或行高
*/
export class TextSpacingCommand extends Command {
constructor(options) {
super({
name: "修改文本间距",
description: "修改文本图层的字符间距或行高",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.property = options.property; // 'charSpacing' 或 'lineHeight'
this.newValue = options.newValue;
this.oldValue = this.textObject[this.property];
}
execute() {
this.textObject.set(this.property, this.newValue);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set(this.property, this.oldValue);
this.canvas.renderAll();
return true;
}
}
/**
* 文本背景颜色命令
* 用于更改文本图层的背景颜色
*/
export class TextBackgroundCommand extends Command {
constructor(options) {
super({
name: "修改文本背景",
description: "修改文本图层的背景颜色",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.newColor = options.newColor;
this.oldColor = this.textObject.textBackgroundColor;
}
execute() {
this.textObject.set("textBackgroundColor", this.newColor);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set("textBackgroundColor", this.oldColor);
this.canvas.renderAll();
return true;
}
}
/**
* 文本透明度命令
* 用于更改文本图层的透明度
*/
export class TextOpacityCommand extends Command {
constructor(options) {
super({
name: "修改文本透明度",
description: "修改文本图层的透明度",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.newOpacity = options.newOpacity;
this.oldOpacity = this.textObject.opacity;
}
execute() {
this.textObject.set("opacity", this.newOpacity);
this.canvas.renderAll();
return true;
}
undo() {
this.textObject.set("opacity", this.oldOpacity);
this.canvas.renderAll();
return true;
}
}
/**
* 组合文本编辑命令
* 用于一次性应用多个文本属性更改
*/
export class CompositeTextCommand extends Command {
constructor(options) {
super({
name: "组合文本编辑",
description: "组合多个文本编辑操作",
});
this.canvas = options.canvas;
this.textObject = options.textObject;
this.changes = options.changes; // {property: newValue} 形式的对象
this.oldValues = {};
// 保存所有属性的旧值
for (const property in this.changes) {
if (this.textObject[property] !== undefined) {
this.oldValues[property] = this.textObject[property];
}
}
}
execute() {
for (const property in this.changes) {
this.textObject.set(property, this.changes[property]);
}
this.canvas.renderAll();
return true;
}
undo() {
for (const property in this.oldValues) {
this.textObject.set(property, this.oldValues[property]);
}
this.canvas.renderAll();
return true;
}
}

View File

@@ -0,0 +1,54 @@
import { Command } from "./Command";
/**
* 工具切换命令
* 用于切换编辑器的工具模式(如绘画、选择、橡皮擦等)
*/
export class ToolCommand extends Command {
/**
* 创建一个工具切换命令
* @param {Object} options 配置选项
* @param {Object} options.toolManager 工具管理器实例
* @param {String} options.tool 要设置的工具名称
* @param {String} options.previousTool 先前的工具名称(可选,如果不提供会在执行时记录)
* @param {Boolean} options.saveState 是否保存画布状态默认为false
*/
constructor(options) {
super({
...options,
name: `切换工具: ${options.tool}`,
description: `将工具切换为 ${options.tool}`,
});
this.toolManager = options.toolManager;
this.tool = options.tool;
this.previousTool = options.previousTool || null;
}
/**
* 执行工具切换
* @returns {String} 设置的工具名称
*/
execute() {
if (!this.toolManager) return null;
// 记录当前工具(用于撤销)
if (!this.previousTool) {
this.previousTool = this.toolManager.getCurrentTool();
}
// 切换工具
return this.toolManager.setTool(this.tool);
}
/**
* 撤销工具切换
* @returns {String} 恢复的工具名称
*/
undo() {
if (!this.toolManager || !this.previousTool) return null;
// 恢复到先前工具
return this.toolManager.setTool(this.previousTool);
}
}

View File

@@ -0,0 +1,685 @@
<template>
<transition name="fade">
<div v-if="isVisible" class="brush-control-panel">
<!-- 笔刷大小控制 -->
<VerticalSlider
v-model="brushSize"
:min="1"
:max="100"
:presets="sizePresets"
:memorized-values="memorizedSizes"
:active-threshold="1"
custom-class="size-slider"
:step="1"
v-model:showTooltip="showSizeTooltip"
@slide-start="handleSizeSlideStart"
@slide-end="handleSizeSlideEnd"
@click="showSizeTooltip = true"
>
<template #tooltip-content>
<div class="tooltip-header">
<div class="tooltip-title">Size</div>
<div class="tooltip-close-btn" @click.stop="closeSizeTooltip">
<SvgIcon name="CClose" size="20" />
</div>
</div>
<div class="brush-preview-container">
<div
class="brush-size-preview"
:style="{
width: `${Math.min(100, brushSize)}px`,
height: `${Math.min(100, brushSize)}px`,
backgroundColor: showColorPicker ? brushColor : '#888888',
}"
></div>
</div>
<div class="tooltip-content">
<div class="tooltip-text">{{ Math.round(brushSize) }}px</div>
<div class="tooltip-controls">
<button
v-if="!memorizedSizes.includes(brushSize)"
class="control-btn add"
@click="memorizeSize"
>
+
</button>
<button
class="control-btn remove"
@click="removeMemorizedSize"
v-if="canRemoveSize"
>
-
</button>
</div>
</div>
</template>
</VerticalSlider>
<!-- 颜色选择器 - 仅在特定工具下显示 -->
<div v-if="showColorPicker" class="color-picker-container">
<label for="color-picker" class="current-color-label">
<div
class="current-color"
:style="{ backgroundColor: brushColor }"
></div>
</label>
<input
type="color"
id="color-picker"
class="system-color-picker"
v-model="customColor"
@input="setBrushColor(customColor)"
/>
</div>
<!-- 透明度控制 - 仅在特定工具下显示 -->
<VerticalSlider
v-if="showOpacitySlider"
v-model="brushOpacity"
:min="0"
:max="1"
:presets="opacityPresets"
:memorized-values="memorizedOpacities"
:is-percentage="true"
custom-class="opacity-slider"
:active-threshold="0.01"
:step="0.01"
v-model:showTooltip="showOpacityTooltip"
@slide-start="handleOpacitySlideStart"
@slide-end="handleOpacitySlideEnd"
@click="showOpacityTooltip = true"
>
<template #tooltip-content>
<div class="tooltip-header">
<div class="tooltip-title">Opacity</div>
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
<SvgIcon name="CClose" size="20" />
</div>
</div>
<div class="opacity-preview">
<div class="opacity-checker"></div>
<div
class="opacity-color"
:style="{
backgroundColor: brushColor,
opacity: brushOpacity,
}"
></div>
</div>
<div class="tooltip-content">
<div class="tooltip-text">
{{ Math.round(brushOpacity * 100) }}%
</div>
<div class="tooltip-controls">
<button
class="control-btn add"
v-if="!memorizedOpacities.includes(brushOpacity)"
@click="memorizeOpacity"
>
+
</button>
<button
class="control-btn remove"
@click="removeMemorizedOpacity"
v-if="canRemoveOpacity"
>
-
</button>
</div>
</div>
</template>
</VerticalSlider>
</div>
</transition>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { BrushStore } from "../store/BrushStore";
import { OperationType } from "../utils/layerHelper";
import { inject } from "vue";
import VerticalSlider from "./VerticalSlider.vue";
const props = defineProps({
activeTool: {
type: String,
required: true,
},
});
// 工具管理器和画布管理器
const toolManager = inject("toolManager");
const canvasManager = inject("canvasManager");
// 可见性控制
const isVisible = computed(() => {
return [
OperationType.DRAW,
OperationType.ERASER,
OperationType.RED_BRUSH,
OperationType.GREEN_BRUSH,
].includes(props.activeTool);
});
// 控制颜色选择器的显示
const showColorPicker = computed(() => {
return props.activeTool === OperationType.DRAW;
});
// 控制透明度滑块的显示
const showOpacitySlider = computed(() => {
return props.activeTool === OperationType.DRAW;
});
// 笔刷大小相关
const brushSize = ref(BrushStore.state.size);
const showSizeTooltip = ref(false);
const sizePresets = ref([5, 10, 20, 50]); // 预设大小,便于吸附
const memorizedSizes = ref([]);
const canRemoveSize = computed(() => {
return memorizedSizes.value.includes(brushSize.value);
});
const isSizeSliding = ref(false); // 是否正在滑动大小滑块
// 笔刷颜色相关
const brushColor = ref(BrushStore.state.color);
const customColor = ref(BrushStore.state.color);
// 笔刷透明度相关
const brushOpacity = ref(BrushStore.state.opacity);
const showOpacityTooltip = ref(false);
const opacityPresets = ref([0.1, 0.2, 0.5, 0.8]); // 预设透明度,便于吸附
const memorizedOpacities = ref([]);
const canRemoveOpacity = computed(() => {
return memorizedOpacities.value.includes(brushOpacity.value);
});
const isOpacitySliding = ref(false); // 是否正在滑动透明度滑块
// 添加计时器变量用于控制外部变化引起的提示框自动隐藏
const sizeTooltipTimer = ref(null);
const opacityTooltipTimer = ref(null);
const TOOLTIP_HIDE_DELAY = 1500; // 与VerticalSlider组件保持一致的延迟时间
// 处理滑块开始和结束事件
function handleSizeSlideStart() {
isSizeSliding.value = true;
}
function handleSizeSlideEnd(event) {
isSizeSliding.value = false;
}
function handleOpacitySlideStart() {
isOpacitySliding.value = true;
}
function handleOpacitySlideEnd(event) {
isOpacitySliding.value = false;
}
// 设置笔刷大小
function setBrushSize(size) {
brushSize.value = size;
BrushStore.setBrushSize(size);
// 如果工具管理器存在,立即应用此更改
if (toolManager) {
toolManager.updateBrushSize(size);
}
}
// 设置笔刷颜色
function setBrushColor(color) {
brushColor.value = color;
customColor.value = color;
BrushStore.setBrushColor(color);
// 如果工具管理器存在,立即应用此更改
if (toolManager && props.activeTool === OperationType.DRAW) {
toolManager.updateBrushColor(color);
}
}
// 设置笔刷透明度
function setBrushOpacity(opacity) {
brushOpacity.value = opacity;
BrushStore.setBrushOpacity(opacity);
// 如果工具管理器存在,立即应用此更改
if (toolManager) {
toolManager.updateBrushOpacity(opacity);
}
}
// 添加用于自动隐藏大小提示框的方法
function startSizeTooltipHideTimer() {
// 清除已有的计时器
clearSizeTooltipTimer();
// 创建新计时器
sizeTooltipTimer.value = setTimeout(() => {
showSizeTooltip.value = false;
sizeTooltipTimer.value = null;
}, TOOLTIP_HIDE_DELAY);
}
// 清除大小提示框隐藏计时器
function clearSizeTooltipTimer() {
if (sizeTooltipTimer.value) {
clearTimeout(sizeTooltipTimer.value);
sizeTooltipTimer.value = null;
}
}
// 添加用于自动隐藏透明度提示框的方法
function startOpacityTooltipHideTimer() {
// 清除已有的计时器
clearOpacityTooltipTimer();
// 创建新计时器
opacityTooltipTimer.value = setTimeout(() => {
showOpacityTooltip.value = false;
opacityTooltipTimer.value = null;
}, TOOLTIP_HIDE_DELAY);
}
// 清除透明度提示框隐藏计时器
function clearOpacityTooltipTimer() {
if (opacityTooltipTimer.value) {
clearTimeout(opacityTooltipTimer.value);
opacityTooltipTimer.value = null;
}
}
// 主动关闭提示框
function closeSizeTooltip() {
showSizeTooltip.value = false;
clearSizeTooltipTimer(); // 清除任何存在的计时器
}
function closeOpacityTooltip() {
showOpacityTooltip.value = false;
clearOpacityTooltipTimer(); // 清除任何存在的计时器
}
// 记忆当前笔刷大小
function memorizeSize() {
if (!memorizedSizes.value.includes(brushSize.value)) {
memorizedSizes.value.push(brushSize.value);
// 记忆值限制为最多5个
if (memorizedSizes.value.length > 5) {
memorizedSizes.value.shift();
}
}
}
// 删除当前记忆的笔刷大小
function removeMemorizedSize() {
if (memorizedSizes.value.includes(brushSize.value)) {
const index = memorizedSizes.value.indexOf(brushSize.value);
memorizedSizes.value.splice(index, 1);
}
}
// 记忆当前笔刷透明度
function memorizeOpacity() {
if (!memorizedOpacities.value.includes(brushOpacity.value)) {
memorizedOpacities.value.push(brushOpacity.value);
// 记忆值限制为最多5个
if (memorizedOpacities.value.length > 5) {
memorizedOpacities.value.shift();
}
}
}
// 删除当前记忆的笔刷透明度
function removeMemorizedOpacity() {
if (memorizedOpacities.value.includes(brushOpacity.value)) {
const index = memorizedOpacities.value.indexOf(brushOpacity.value);
memorizedOpacities.value.splice(index, 1);
}
}
// 监听工具的变化
watch(
() => props.activeTool,
(newTool) => {
// 当切换到橡皮擦工具时,可以设置特殊的默认值
if (newTool === OperationType.ERASER) {
// 橡皮擦模式下不需要调整颜色,但可能会调整大小和不透明度
} else if (newTool === OperationType.DRAW) {
// 恢复到绘制模式时可能有特殊设置
}
}
);
// 监听brushSize的变化更新到BrushStore
watch(
() => brushSize.value,
(newSize) => {
setBrushSize(newSize);
}
);
// 监听brushOpacity的变化更新到BrushStore
watch(
() => brushOpacity.value,
(newOpacity) => {
setBrushOpacity(newOpacity);
}
);
// 监听BrushStore中的变化
watch(
() => BrushStore.state.size,
(newSize) => {
if (Math.abs(brushSize.value - newSize) > 0.1) {
brushSize.value = newSize;
// 当外部修改了笔刷大小时,显示提示框
showSizeTooltip.value = true;
// 启动自动隐藏计时器
startSizeTooltipHideTimer();
}
}
);
watch(
() => BrushStore.state.opacity,
(newOpacity) => {
if (Math.abs(brushOpacity.value - newOpacity) > 0.01) {
brushOpacity.value = newOpacity;
// 当外部修改了笔刷透明度时,显示提示框
showOpacityTooltip.value = true;
// 启动自动隐藏计时器
startOpacityTooltipHideTimer();
}
}
);
watch(
() => BrushStore.state.color,
(newColor) => {
if (brushColor.value !== newColor) {
brushColor.value = newColor;
customColor.value = newColor;
}
}
);
onMounted(() => {
// 初始化时从BrushStore获取当前值
brushSize.value = BrushStore.state.size;
brushOpacity.value = BrushStore.state.opacity;
brushColor.value = BrushStore.state.color;
customColor.value = BrushStore.state.color;
});
onBeforeUnmount(() => {
// 组件卸载前清除所有计时器
clearSizeTooltipTimer();
clearOpacityTooltipTimer();
});
// 监听showSizeTooltip和showOpacityTooltip的变化防止两个滑块的提示框同时显示
watch(
() => showSizeTooltip.value,
(newValue) => {
if (newValue && showOpacityTooltip.value) {
// 如果大小提示框显示,则隐藏透明度提示框
showOpacityTooltip.value = false;
}
}
);
watch(
() => showOpacityTooltip.value,
(newValue) => {
if (newValue && showSizeTooltip.value) {
// 如果透明度提示框显示,则隐藏大小提示框
showSizeTooltip.value = false;
}
}
);
</script>
<style scoped lang="less">
.brush-control-panel {
position: absolute;
top: 50%;
left: 15px;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
background: rgba(255, 255, 255, 0.8);
border-radius: 5px;
padding: 15px 3px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
z-index: 8;
backdrop-filter: blur(2px);
color: #333;
user-select: none;
-webkit-user-select: none;
}
// 笔刷大小预览相关样式
.brush-preview-container {
width: 110px;
height: 110px;
display: flex;
justify-content: center;
align-items: center;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
border-radius: 6px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 5px;
}
.brush-size-preview {
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
// 透明度预览相关样式
.opacity-preview {
width: 110px;
height: 110px;
border-radius: 6px;
overflow: hidden;
position: relative;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 5px;
}
.opacity-checker {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
}
.opacity-color {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
// 工具提示内容样式
.tooltip-content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.tooltip-text {
font-size: 14px;
font-weight: 500;
color: #333;
background: #f5f5f5;
padding: 2px 8px;
border-radius: 4px;
min-width: 50px;
text-align: center;
}
.tooltip-controls {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 3px;
}
.control-btn {
width: 24px;
height: 24px;
border: none;
border-radius: 4px;
background: #f0f0f0;
color: #333;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
&:hover {
background: #e0e0e0;
}
&:active {
transform: scale(0.95);
}
&.add {
color: #4caf50;
&:hover {
background: rgba(76, 175, 80, 0.1);
}
}
&.remove {
color: #f44336;
&:hover {
background: rgba(244, 67, 54, 0.1);
}
}
}
// 颜色选择器样式
.color-picker-container {
position: relative;
width: 100%;
display: flex;
justify-content: center;
}
.current-color-label {
cursor: pointer;
}
.current-color {
width: 32px;
height: 32px;
border-radius: 8px;
border: 2px solid #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
cursor: pointer;
&:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.1);
}
}
.system-color-picker {
width: 0;
height: 0;
opacity: 0;
position: absolute;
pointer-events: none;
}
// 工具提示标题和关闭按钮
.tooltip-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.tooltip-title {
font-size: 16px;
color: #333;
font-weight: 600;
}
.tooltip-close-btn {
position: absolute;
right: 3px;
top: 3px;
width: 20px;
height: 20px;
border: none;
background: transparent;
color: #999;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
padding: 0;
margin: 0;
transition: all 0.2s ease;
z-index: 99;
&:hover {
background: rgba(0, 0, 0, 0.05);
color: #333;
}
&:active {
transform: scale(0.9);
}
}
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateX(-10px) translateY(-50%);
}
// 响应式调整
@media (max-height: 600px) {
.brush-control-panel {
transform: translateY(-50%);
}
}
@media (max-width: 768px) {
.brush-control-panel {
left: 10px;
// padding: 12px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,525 @@
<script setup>
import {
inject,
ref,
provide,
onMounted,
computed,
watch,
onUnmounted,
} from "vue";
import { OperationType } from "../utils/layerHelper";
import BrushPanel from "./BrushPanel.vue";
import { BrushStore } from "../store/BrushStore";
// 提供brushStore给子组件
provide("brushStore", BrushStore);
const toolManager = inject("toolManager");
const layerManager = inject("layerManager");
const props = defineProps({
activeTool: String,
canvasWidth: Number,
canvasHeight: Number,
canvasColor: String,
brushSize: Number,
});
const emit = defineEmits([
"update:canvasWidth",
"update:canvasHeight",
"update:canvasColor",
"update:brushSize",
"canvas-size-change",
"canvas-color-change",
]);
// 笔刷面板相关状态
const showBrushPanel = ref(false);
const brushPanelRef = ref(null);
// 计算属性
const shouldShowBrushSettings = computed(() => {
return props.activeTool === OperationType.DRAW;
});
function updateCanvasSize() {
if (!layerManager) {
console.warn("LayerManager 未初始化,无法调整背景层尺寸");
return;
}
// 检查画布上是否有除了背景层的其他元素
const hasOtherElements = layerManager.layers.value.some((layer) => {
if (layer.isBackground) return false;
// 检查普通图层是否有对象
if (layer.fabricObjects && layer.fabricObjects.length > 0) return true;
// 检查固定图层是否有对象
if (layer.isFixed && layer.fabricObjects && layer.fabricObjects.length > 0)
return true;
return false;
});
if (hasOtherElements) {
// 有其他元素时使用等比缩放命令
layerManager.resizeCanvasWithScale(props.canvasWidth, props.canvasHeight);
} else {
// 只有背景层时使用普通调整命令
layerManager.resizeCanvas(props.canvasWidth, props.canvasHeight);
}
emit("canvas-size-change");
}
function updateCanvasColor() {
if (!layerManager) {
console.warn("LayerManager 未初始化,无法更改背景色");
return;
}
// 更新背景层颜色而不是画布颜色
layerManager.updateBackgroundColor(props.canvasColor);
emit("canvas-color-change");
}
// 切换笔刷面板显示状态
function toggleBrushPanel() {
showBrushPanel.value = !showBrushPanel.value;
}
// 处理笔刷大小变化
function handleBrushSizeChange(event) {
const newSize = parseFloat(event.target.value);
emit("update:brushSize", newSize);
}
// 处理笔刷设置变化将BrushStore的数据同步到brushManager
function syncBrushStoreToManager() {
if (!toolManager?.brushManager) return;
const brushManager = toolManager.brushManager;
// 检查画笔是否正在更新中
if (brushManager.isUpdatingBrush) {
console.warn("画笔正在更新中,请稍候...");
// 延迟重试,确保在画笔更新完成后应用最新设置
setTimeout(syncBrushStoreToManager, 100);
return;
}
// 监听BrushStore的变化更新brushManager
const size = BrushStore.state.size;
const color = BrushStore.state.color;
const type = BrushStore.state.type;
const opacity = BrushStore.state.opacity;
const textureEnabled = BrushStore.state.textureEnabled;
const texturePath = BrushStore.state.texturePath;
const textureScale = BrushStore.state.textureScale;
// 将所有更改一次性应用减少updateBrush调用次数
let needsUpdate = false;
if (
brushManager.brushSize &&
typeof brushManager.setBrushSize === "function" &&
brushManager.getBrushSize() !== size
) {
brushManager.brushSize.value = size; // 直接设置值避免触发updateBrush
needsUpdate = true;
}
if (
brushManager.brushColor &&
typeof brushManager.setBrushColor === "function" &&
brushManager.getBrushColor() !== color
) {
brushManager.brushColor.value = color; // 直接设置值避免触发updateBrush
needsUpdate = true;
}
if (
typeof brushManager.setBrushType === "function" &&
brushManager.getCurrentBrushType() !== type
) {
brushManager.setBrushType(type);
needsUpdate = true;
}
if (typeof brushManager.setBrushOpacity === "function") {
brushManager.setBrushOpacity(opacity);
needsUpdate = true;
}
// 同步材质相关设置
if (textureEnabled && texturePath) {
if (typeof brushManager.setTexturePath === "function") {
brushManager.setTexturePath(texturePath);
needsUpdate = true;
}
if (
typeof brushManager.setTextureScale === "function" &&
brushManager.getTextureScale() !== textureScale
) {
brushManager.textureScale.value = textureScale; // 直接设置值避免触发updateBrush
needsUpdate = true;
}
}
// 只在有变化时调用一次updateBrush减少重绘次数
if (needsUpdate && typeof brushManager.updateBrush === "function") {
brushManager.updateBrush();
}
}
// 点击外部时关闭笔刷面板
function handleClickOutside(event) {
if (
showBrushPanel.value &&
brushPanelRef.value &&
!brushPanelRef.value.contains(event.target) &&
!event.target.closest(".brush-selector")
) {
showBrushPanel.value = false;
}
}
onMounted(() => {
// 获取工具管理器和笔刷管理器
const brushManager = toolManager?.brushManager;
// 设置初始的可用笔刷类型
if (brushManager) {
const availableBrushes = brushManager.getBrushTypes();
BrushStore.setAvailableBrushes(availableBrushes);
// 初始化BrushStore与brushManager的数据同步
BrushStore.setBrushSize(brushManager.brushSize?.value || 5);
BrushStore.setBrushColor(brushManager.brushColor?.value || "#000000");
BrushStore.setBrushType(brushManager.getCurrentBrushType() || "pencil");
}
// 添加点击外部关闭面板的事件监听
document.addEventListener("mousedown", handleClickOutside);
// 监听BrushStore的变化同步到brushManager
const unwatch = watch(
() => [
BrushStore.state.size,
BrushStore.state.color,
BrushStore.state.type,
BrushStore.state.opacity,
BrushStore.state.textureEnabled,
BrushStore.state.texturePath,
BrushStore.state.textureScale,
],
syncBrushStoreToManager,
{ deep: true }
);
// 组件卸载时移除事件监听
onUnmounted(() => {
document.removeEventListener("mousedown", handleClickOutside);
unwatch();
});
});
</script>
<template>
<div class="canvas-header">
<span class="canvas-title">Canvas</span>
<!-- 默认设置 -->
<div
v-if="
!activeTool ||
activeTool === OperationType.SELECT ||
activeTool === OperationType.PAN
"
class="canvas-settings"
>
<div class="setting-group">
<span class="setting-label">Width</span>
<input
type="text"
:value="canvasWidth"
class="setting-input"
@input="$emit('update:canvasWidth', Number($event.target.value))"
@change="updateCanvasSize"
/>
</div>
<div class="setting-group">
<span class="setting-label">Height</span>
<input
type="text"
:value="canvasHeight"
class="setting-input"
@input="$emit('update:canvasHeight', Number($event.target.value))"
@change="updateCanvasSize"
/>
</div>
<div class="setting-group">
<span class="setting-label">Color</span>
<div class="color-picker-wrapper">
<input
type="color"
:value="canvasColor"
class="color-picker"
@input="$emit('update:canvasColor', $event.target.value)"
@change="updateCanvasColor"
/>
<span class="color-dropdown">▼</span>
</div>
</div>
</div>
<!-- 绘图工具设置 -->
<div v-if="shouldShowBrushSettings" class="canvas-settings">
<!-- 简化的笔刷控制UI -->
<!-- <div class="setting-group">
<span class="setting-label">大小:</span>
<input
type="range"
:value="BrushStore.state.size"
min="0.5"
max="100"
step="0.5"
class="size-slider"
@input="handleBrushSizeChange"
/>
<span class="size-value">{{ BrushStore.state.size }}px</span>
</div> -->
<div class="setting-group">
<span class="setting-label">笔刷:</span>
<div class="brush-selector" @click="toggleBrushPanel">
<div
class="brush-preview"
:style="{
backgroundColor: BrushStore.state.color,
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
opacity: BrushStore.state.opacity,
}"
></div>
<span class="brush-dropdown">▼</span>
</div>
<!-- 笔刷面板 -->
<div
v-if="showBrushPanel"
class="brush-panel-container"
ref="brushPanelRef"
>
<BrushPanel />
</div>
</div>
<div class="setting-group">
<span class="setting-label">颜色:</span>
<div class="color-picker-wrapper">
<input
type="color"
:value="BrushStore.state.color"
class="color-picker"
@input="BrushStore.setBrushColor($event.target.value)"
/>
<span class="color-dropdown">▼</span>
</div>
</div>
</div>
<!-- 文本工具设置 -->
<div v-if="activeTool === OperationType.TEXT" class="canvas-settings">
<div class="setting-group">
<span class="setting-label">Font:</span>
<select class="font-select">
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
</select>
</div>
<div class="setting-group">
<span class="setting-label">Size:</span>
<input
type="number"
class="setting-input"
value="16"
min="8"
max="72"
/>
</div>
<div class="setting-group">
<span class="setting-label">Color:</span>
<div class="color-picker-wrapper">
<input type="color" class="color-picker" value="#000000" />
<span class="color-dropdown">▼</span>
</div>
</div>
</div>
<!-- 上传工具设置 -->
<div v-if="activeTool === OperationType.UPLOAD" class="canvas-settings">
<div class="setting-group">
<span class="setting-label">Upload Type:</span>
<select class="setting-select">
<option value="image">Image</option>
<option value="vector">Vector Graphics</option>
</select>
</div>
</div>
<!-- 导出设置 -->
<div class="setting-group export-group">
<span class="export-model-select">exportModel.select:</span>
<span class="export-model-dropdown"></span>
</div>
</div>
</template>
<style scoped>
.canvas-header {
display: flex;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #e0e0e0;
user-select: none;
}
.canvas-title {
font-size: 16px;
font-weight: 500;
margin-right: 30px;
display: flex;
align-items: center;
}
.canvas-title::before {
content: "⟳";
margin-right: 5px;
font-size: 14px;
}
.canvas-settings {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.setting-group {
display: flex;
align-items: center;
gap: 5px;
position: relative;
}
.setting-label {
font-size: 14px;
color: #333;
}
.setting-input {
width: 60px;
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.setting-select {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.font-select {
width: 150px;
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.color-picker-wrapper {
position: relative;
display: flex;
align-items: center;
}
.color-picker {
width: 30px;
height: 30px;
border: none;
padding: 0;
background: none;
cursor: pointer;
}
.color-dropdown {
font-size: 10px;
margin-left: 5px;
color: #666;
}
.size-slider {
width: 100px;
cursor: pointer;
}
.size-value {
font-size: 12px;
color: #666;
min-width: 30px;
}
.brush-selector {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 4px;
padding: 5px;
cursor: pointer;
background-color: white;
width: 80px;
justify-content: space-between;
}
.brush-preview {
width: 20px;
height: 2px;
background-color: #000;
border-radius: 1px;
}
.brush-dropdown,
.export-model-dropdown {
font-size: 10px;
margin-left: 5px;
color: #666;
}
.export-model-select {
font-size: 14px;
color: #333;
cursor: pointer;
}
.export-group {
margin-left: auto;
}
/* 笔刷面板 */
.brush-panel-container {
position: absolute;
top: calc(100% + 5px);
left: 0;
z-index: 1000;
width: 600px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
border-radius: 4px;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,506 @@
<script setup>
import { ref, inject, onMounted } from "vue";
import { Skeleton } from "ant-design-vue";
const loading = ref(true);
const shortcuts = ref([]);
const keyboardManager = inject("keyboardManager", null);
const platform = ref({});
// 初始化键盘快捷键信息
onMounted(() => {
// 添加延迟以显示骨架屏效果
setTimeout(() => {
if (keyboardManager) {
// 使用KeyboardManager的平台检测
platform.value = {
isMac: keyboardManager.platform === "mac",
isIOS: keyboardManager.platform === "ios",
isIPad:
keyboardManager.platform === "ios" &&
/iPad/.test(window.navigator.userAgent),
isTouchDevice: keyboardManager.isTouchDevice,
isWindows: keyboardManager.platform === "windows",
isAndroid: keyboardManager.platform === "android",
};
// 使用KeyboardManager的API获取所有快捷键
const managerShortcuts = keyboardManager.getShortcuts();
// 转换为组件所需的格式
shortcuts.value = convertShortcuts(managerShortcuts);
} else {
// 如果没有注入keyboardManager使用默认检测和默认快捷键
platform.value = detectPlatform();
shortcuts.value = generateDefaultShortcuts();
}
loading.value = false;
}, 500);
});
// 转换KeyboardManager返回的快捷键格式为组件需要的格式
function convertShortcuts(managerShortcuts) {
// 转换快捷键列表
const result = [];
// 基本的Action到显示名称的映射
const actionDisplayMap = {
undo: "撤销",
redo: "重做",
delete: "删除选中元素",
selectAll: "全选",
copy: "复制",
paste: "粘贴",
cut: "剪切",
save: "保存",
selectTool: "选择工具",
increaseBrushSize: "增加笔触大小",
decreaseBrushSize: "减小笔触大小",
toggleTempTool: "临时切换工具",
newLayer: "新建图层",
groupLayers: "组合图层",
ungroupLayers: "取消组合",
mergeLayers: "合并图层",
};
// 工具ID到显示名称的映射
const toolDisplayMap = {
select: "选择模式",
draw: "绘画模式",
eraser: "橡皮擦模式",
eyedropper: "吸色工具",
pan: "移动画布",
lasso: "套索工具",
area_custom: "自由选区工具",
wave: "波浪工具",
liquify: "液化工具",
};
// 处理每个快捷键
for (const shortcut of managerShortcuts) {
let actionDisplay = actionDisplayMap[shortcut.action] || shortcut.action;
// 特殊处理工具选择
if (
shortcut.action === "selectTool" &&
shortcut.param &&
toolDisplayMap[shortcut.param]
) {
actionDisplay = toolDisplayMap[shortcut.param];
}
result.push({
action: actionDisplay,
windows: shortcut.key.replace(/cmdOrCtrl\+/g, "Ctrl+"),
mac: shortcut.key.replace(/cmdOrCtrl\+/g, "⌘+"),
touch: shortcut.touch || "触控界面点击对应工具",
displayKey: shortcut.displayKey,
});
}
// 添加一些组件特定的快捷键
result.push({
action: "缩放画布",
windows: "鼠标滚轮",
mac: "鼠标滚轮 或 触控板缩放手势",
touch: "双指捏合",
});
return result;
}
// 检测平台 - 作为备用
function detectPlatform() {
const userAgent = window.navigator.userAgent;
return {
isMac: /Mac/.test(userAgent),
isIOS: /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream,
isIPad: /iPad/.test(userAgent),
isTouchDevice: "ontouchstart" in window || navigator.maxTouchPoints > 0,
isWindows: /Win/.test(userAgent),
isAndroid: /Android/.test(userAgent),
};
}
// 生成默认快捷键描述
function generateDefaultShortcuts() {
return [
{
action: "撤销",
windows: "Ctrl+Z",
mac: "⌘+Z",
touch: "双指向右轻扫",
},
{
action: "重做",
windows: "Ctrl+Y 或 Ctrl+Shift+Z",
mac: "⌘+Shift+Z",
touch: "双指向左轻扫",
},
{
action: "删除选中元素",
windows: "Delete 或 Backspace",
mac: "Delete 或 ⌫",
touch: "长按选中元素后点击删除",
},
{
action: "全选",
windows: "Ctrl+A",
mac: "⌘+A",
touch: "无",
},
{
action: "复制",
windows: "Ctrl+C",
mac: "⌘+C",
touch: "无",
},
{
action: "粘贴",
windows: "Ctrl+V",
mac: "⌘+V",
touch: "无",
},
{
action: "剪切",
windows: "Ctrl+X",
mac: "⌘+X",
touch: "无",
},
{
action: "缩放画布",
windows: "鼠标滚轮",
mac: "鼠标滚轮 或 触控板缩放手势",
touch: "双指捏合",
},
{
action: "移动画布",
windows: "Alt+拖动 或 鼠标中键拖动",
mac: "Option+拖动 或 触控板双指拖动",
touch: "双指拖动",
},
{
action: "绘画模式",
windows: "B",
mac: "B",
touch: "点击画笔工具",
},
{
action: "选择模式",
windows: "M",
mac: "M",
touch: "点击选择工具",
},
{
action: "橡皮擦模式",
windows: "E",
mac: "E",
touch: "点击橡皮擦工具",
},
{
action: "吸色工具",
windows: "I",
mac: "I",
touch: "点击吸色工具",
},
{
action: "增加笔触大小",
windows: "]",
mac: "]",
touch: "拖动笔刷大小滑块",
},
{
action: "减小笔触大小",
windows: "[",
mac: "[",
touch: "拖动笔刷大小滑块",
},
{
action: "增加材质图片大小",
windows: "Shift+]",
mac: "⇧+]",
touch: "拖动材质大小滑块",
},
{
action: "减小材质图片大小",
windows: "Shift+[",
mac: "⇧+[",
touch: "拖动材质大小滑块",
},
{
action: "上传图片",
windows: "Ctrl+O",
mac: "⌘+O",
touch: "点击上传按钮",
},
];
}
// 获取当前平台的快捷键文本
function getShortcutForCurrentPlatform(shortcut) {
if (platform.value.isTouchDevice) {
return shortcut.touch;
} else if (platform.value.isMac) {
return shortcut.displayKey || shortcut.mac;
} else {
return shortcut.displayKey || shortcut.windows;
}
}
// 按分类获取快捷键
function getShortcutsByCategory(category) {
const categoryMap = {
basic: [
"撤销",
"重做",
"全选",
"复制",
"粘贴",
"剪切",
"删除选中元素",
"上传图片",
],
view: ["缩放画布", "移动画布"],
tools: [
"绘画模式",
"选择模式",
"橡皮擦模式",
"吸色工具",
"套索工具",
"自由选区工具",
"波浪工具",
"液化工具",
],
brush: [
"增加笔触大小",
"减小笔触大小",
"增加材质图片大小",
"减小材质图片大小",
],
layer: ["新建图层", "组合图层", "取消组合", "合并图层"],
};
return shortcuts.value.filter((s) =>
categoryMap[category]?.includes(s.action)
);
}
</script>
<template>
<div class="keyboard-shortcut-help">
<h2>键盘快捷键 & 操作指南</h2>
<Skeleton active :loading="loading">
<div class="platform-info">
检测到的平台:
<span v-if="platform.isMac">MacOS</span>
<span v-else-if="platform.isWindows">Windows</span>
<span v-else-if="platform.isIPad">iPad</span>
<span v-else-if="platform.isIOS">iOS</span>
<span v-else-if="platform.isAndroid">Android</span>
<span v-else>其他</span>
<span v-if="platform.isTouchDevice"> (触控设备)</span>
</div>
<div class="shortcuts-category">
<h3>基本操作</h3>
<table class="shortcuts-table">
<thead>
<tr>
<th>操作</th>
<th>快捷键/手势</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('basic')"
:key="item.action"
>
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="shortcuts-category">
<h3>视图操作</h3>
<table class="shortcuts-table">
<thead>
<tr>
<th>操作</th>
<th>快捷键/手势</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('view')"
:key="item.action"
>
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="shortcuts-category">
<h3>工具切换</h3>
<table class="shortcuts-table">
<thead>
<tr>
<th>操作</th>
<th>快捷键/手势</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('tools')"
:key="item.action"
>
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="shortcuts-category">
<h3>笔刷调整</h3>
<table class="shortcuts-table">
<thead>
<tr>
<th>操作</th>
<th>快捷键/手势</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('brush')"
:key="item.action"
>
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="shortcuts-category">
<h3>图层操作</h3>
<table class="shortcuts-table">
<thead>
<tr>
<th>操作</th>
<th>快捷键/手势</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('layer')"
:key="item.action"
>
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
</tbody>
</table>
</div>
<div class="touch-tips" v-if="platform.isTouchDevice">
<h3>触控设备提示</h3>
<ul>
<li>长按图层面板可访问更多选项</li>
<li>双击元素可快速进入编辑模式</li>
<li>双指拖动可平移画布</li>
<li>双指捏合可缩放画布</li>
<li>双指连按可显示元素变换控制点</li>
<li>三指左右滑动可进行撤销/重做操作</li>
</ul>
</div>
</Skeleton>
</div>
</template>
<style scoped>
.keyboard-shortcut-help {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
max-width: 600px;
margin: 0 auto;
}
h2 {
margin-top: 0;
margin-bottom: 16px;
font-size: 18px;
}
h3 {
font-size: 16px;
margin-top: 20px;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #eaeaea;
}
.platform-info {
margin-bottom: 16px;
padding: 8px;
background-color: #f0f9ff;
border-radius: 4px;
font-size: 14px;
}
.shortcuts-category {
margin-bottom: 20px;
}
.shortcuts-table {
width: 100%;
border-collapse: collapse;
}
.shortcuts-table th,
.shortcuts-table td {
border: 1px solid #eaeaea;
padding: 8px 10px;
text-align: left;
}
.shortcuts-table th {
background-color: #f5f5f5;
}
.touch-tips {
margin-top: 20px;
padding: 10px;
background-color: #fffbeb;
border-radius: 4px;
border-left: 3px solid #fbbf24;
}
.touch-tips ul {
margin: 10px 0 0;
padding-left: 20px;
}
.touch-tips li {
margin-bottom: 5px;
}
@media (pointer: coarse) {
.keyboard-shortcut-help {
padding: 15px;
}
.shortcuts-table th,
.shortcuts-table td {
padding: 12px 8px;
font-size: 15px;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
<script setup>
import { ref, onMounted, watch, onBeforeUnmount } from "vue";
const props = defineProps({
minimapManager: Object,
});
const minimapContainerRef = ref(null);
let refreshTimeout = null;
// 强制重绘小地图,添加防抖处理
const forceRefresh = () => {
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
refreshTimeout = setTimeout(() => {
if (props.minimapManager) {
props.minimapManager.refresh();
}
}, 50);
};
onMounted(() => {
if (props.minimapManager && minimapContainerRef.value) {
// 使用新的mount方法挂载小地图
props.minimapManager.mount(minimapContainerRef.value);
// 初始加载后延迟刷新一次,确保内容正确加载
setTimeout(forceRefresh, 200);
}
});
watch(
() => props.minimapManager,
(newVal) => {
if (newVal && minimapContainerRef.value) {
newVal.mount(minimapContainerRef.value);
}
}
);
// 添加resize observer以适应容器大小变化
let resizeObserver = null;
onMounted(() => {
if (window.ResizeObserver) {
// 使用防抖处理resize事件避免过于频繁的刷新
resizeObserver = new ResizeObserver(() => {
forceRefresh();
});
if (minimapContainerRef.value) {
resizeObserver.observe(minimapContainerRef.value.parentElement);
}
}
});
onBeforeUnmount(() => {
if (resizeObserver && minimapContainerRef.value) {
resizeObserver.unobserve(minimapContainerRef.value.parentElement);
resizeObserver.disconnect();
}
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
});
</script>
<template>
<div class="minimap-container">
<div class="minimap-header">
<span>画布小地图</span>
<button class="minimap-refresh" @click="forceRefresh" title="刷新小地图">
</button>
</div>
<div class="minimap-content" ref="minimapContainerRef">
<!-- 不再需要直接提供canvas引用由MinimapManager内部创建 -->
</div>
</div>
</template>
<style scoped>
.minimap-container {
position: absolute;
bottom: 10px;
left: 10px;
width: 200px;
height: 140px;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
overflow: hidden;
z-index: 10;
display: flex;
flex-direction: column;
}
.minimap-header {
padding: 5px 8px;
font-size: 12px;
background-color: #f0f0f0;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.minimap-content {
flex: 1;
overflow: hidden;
position: relative;
}
.minimap-refresh {
cursor: pointer;
background: none;
border: none;
font-size: 14px;
padding: 0 4px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.minimap-refresh:hover {
color: #000;
}
/* 触控设备优化 */
@media (pointer: coarse) {
.minimap-container {
width: 220px;
height: 160px;
}
.minimap-header {
padding: 8px 10px;
font-size: 14px;
}
.minimap-refresh {
font-size: 18px;
padding: 0 6px;
}
}
</style>

View File

@@ -0,0 +1,814 @@
<template>
<transition name="fade">
<div class="selection-toolbar" v-if="visible">
<!-- 顶部选区类型工具栏 -->
<div class="toolbar-section">
<div class="toolbar-header">
<div class="header-title">选区工具</div>
<!-- 移除关闭按钮完全通过工具切换控制显示隐藏 -->
</div>
<div class="tool-types">
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO },
]"
@click="setSelectionType(OperationType.LASSO)"
>
<svg-icon name="CFree" size="26" />
<span>{{ $t("手绘") }}</span>
</div>
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO_RECTANGLE },
]"
@click="setSelectionType(OperationType.LASSO_RECTANGLE)"
>
<svg-icon name="CRectangle" size="32" />
<span>{{ $t("矩形") }}</span>
</div>
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO_ELLIPSE },
]"
@click="setSelectionType(OperationType.LASSO_ELLIPSE)"
>
<svg-icon name="CEllipse" size="30" />
<span>{{ $t("椭圆") }}</span>
</div>
</div>
<!-- 分割线 -->
<div class="toolbar-divider"></div>
<!-- 底部选区操作工具栏 -->
<div class="tool-actions">
<div class="action-btn" @click="copySelectionToNewLayer">
<svg-icon name="CPaste" />
<span class="btn-text">{{ $t("拷贝并粘贴") }}</span>
</div>
<!-- <button
class="action-btn"
@click="addSelection"
:disabled="!hasSelection"
title="添加"
>
<svg-icon name="plus" />
<span class="btn-text">{{ $t("添加") }}</span>
</button>
<button
class="action-btn"
@click="removeSelection"
:disabled="!hasSelection"
title="移除"
>
<svg-icon name="minus" />
<span class="btn-text">{{ $t("移除") }}</span>
</button>
<button
class="action-btn"
@click="invertSelection"
:disabled="!hasSelection"
title="反转"
>
<svg-icon name="flip-horizontal" />
<span class="btn-text">{{ $t("反转") }}</span>
</button> -->
<!-- <button
class="action-btn"
@click="copySelectionToNewLayer"
:disabled="!hasSelection"
title="拷贝并粘贴"
>
<svg-icon name="copy" />
<span class="btn-text">{{ $t("拷贝并粘贴") }}</span>
</button>
<button
class="action-btn"
@click="openFeatherDialog"
:disabled="!hasSelection"
title="羽化"
>
<svg-icon name="feather" />
<span class="btn-text">{{ $t("羽化") }}</span>
</button>
<button
class="action-btn"
@click="fillSelection"
:disabled="!hasSelection"
title="颜色填充"
>
<svg-icon name="fill-color" />
<span class="btn-text">{{ $t("颜色填充") }}</span>
</button>
<button
class="action-btn"
@click="clearSelection"
:disabled="!hasSelection"
title="清除"
>
<svg-icon name="trash" />
<span class="btn-text">{{ $t("清除") }}</span>
</button> -->
</div>
</div>
<!-- 羽化设置弹窗 -->
<div v-if="showFeatherDialog" class="dialog-overlay">
<div class="dialog-container">
<div class="dialog-header">
<h3>{{ $t("羽化") }}</h3>
<button class="close-dialog-btn" @click="cancelFeather">×</button>
</div>
<div class="dialog-content">
<div class="feather-control">
<input
type="range"
min="0"
max="50"
v-model.number="featherAmount"
class="slider-control"
/>
<div class="feather-value">{{ featherAmount }}px</div>
</div>
<div class="dialog-buttons">
<button class="cancel-btn" @click="cancelFeather">
{{ $t("取消") }}
</button>
<button class="confirm-btn" @click="applyFeather">
{{ $t("确认") }}
</button>
</div>
</div>
</div>
</div>
<!-- 颜色选择器 -->
<div v-if="showColorPicker" class="dialog-overlay">
<div class="dialog-container">
<div class="dialog-header">
<h3>{{ $t("选择填充颜色") }}</h3>
<button class="close-dialog-btn" @click="cancelColorPicker">
×
</button>
</div>
<div class="dialog-content">
<input type="color" v-model="fillColor" class="color-picker" />
<div class="dialog-buttons">
<button class="cancel-btn" @click="cancelColorPicker">
{{ $t("取消") }}
</button>
<button class="confirm-btn" @click="confirmColorPicker">
{{ $t("确认") }}
</button>
</div>
</div>
</div>
</div>
</div>
</transition>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import {
CreateSelectionCommand,
InvertSelectionCommand,
ClearSelectionCommand,
FeatherSelectionCommand,
FillSelectionCommand,
CopySelectionToNewLayerCommand,
ClearSelectionContentCommand,
LassoCutoutCommand,
} from "../commands/SelectionCommands";
import { ToolCommand } from "../commands/ToolCommands";
import { OperationType } from "../utils/layerHelper";
const props = defineProps({
canvas: {
type: Object,
required: true,
},
commandManager: {
type: Object,
required: true,
},
selectionManager: {
type: Object,
required: true,
},
layerManager: {
type: Object,
required: true,
},
toolManager: {
type: Object,
required: true,
},
activeTool: {
type: String,
required: false,
default: null,
},
});
// 响应式数据
const visible = ref(false);
const selectionType = ref("rectangle");
const featherAmount = ref(0);
const fillColor = ref("#000000");
const hasSelection = ref(false);
const showFeatherDialog = ref(false);
const showColorPicker = ref(false);
// 国际化函数 (简单实现,可根据需要替换为实际的国际化方案)
const $t = (key) => key;
onMounted(() => {
// 为选区管理器添加监听,以便在选区变化时更新状态
if (props.selectionManager) {
// 在选区管理器中添加选区变化的监听
checkSelectionStatus();
// 设置选区状态变化的回调
props.selectionManager.onSelectionChanged = () => {
checkSelectionStatus();
};
}
});
// 监听 activeTool 变化
watch(
() => props.activeTool,
(newTool) => {
// 当工具为LASSO或AREA类型时显示选区面板
const selectionTools = [
OperationType.LASSO,
OperationType.LASSO_RECTANGLE,
OperationType.LASSO_ELLIPSE,
];
if (selectionTools.includes(newTool)) {
show();
// 根据工具类型设置选区类型
selectionType.value = newTool;
// 更新选区管理器的选区类型
if (props.selectionManager) {
props.selectionManager.setSelectionType(selectionType.value);
props.selectionManager.setupSelectionEvents();
}
} else {
close();
}
},
{ immediate: true }
);
/**
* 显示面板
*/
function show() {
visible.value = true;
checkSelectionStatus();
}
/**
* 关闭面板
*/
function close() {
visible.value = false;
}
/**
* 设置选区类型
*/
function setSelectionType(type) {
selectionType.value = type;
// 通过 ToolManager 切换工具,这会自动通知 SelectionManager
if (props.toolManager) {
props.toolManager.setToolWithCommand(type);
}
// 备用方案:如果没有 toolManager直接更新 selectionManager
else if (props.selectionManager) {
props.selectionManager.setSelectionType(type);
props.selectionManager.setupSelectionEvents();
}
}
/**
* 检查选区状态
*/
function checkSelectionStatus() {
hasSelection.value =
props.selectionManager &&
props.selectionManager.getSelectionObject() !== null;
// 同步羽化值
if (hasSelection.value) {
featherAmount.value = props.selectionManager.getFeatherAmount();
}
}
/**
* 添加选区
*/
function addSelection() {
// TODO: 实现添加选区功能
console.log("添加选区功能尚未实现");
}
/**
* 移除选区
*/
function removeSelection() {
// TODO: 实现移除选区功能
console.log("移除选区功能尚未实现");
}
/**
* 反转选区
*/
function invertSelection() {
if (!hasSelection.value) return;
props.commandManager.execute(
new InvertSelectionCommand({
canvas: props.canvas,
selectionManager: props.selectionManager,
})
);
checkSelectionStatus();
}
/**
* 清除选区
*/
function clearSelection() {
if (!hasSelection.value) return;
props.commandManager.execute(
new ClearSelectionCommand({
selectionManager: props.selectionManager,
})
);
checkSelectionStatus();
}
/**
* 应用羽化效果
*/
function applyFeather() {
if (!hasSelection.value) return;
props.commandManager.execute(
new FeatherSelectionCommand({
selectionManager: props.selectionManager,
featherAmount: featherAmount.value,
})
);
}
/**
* 填充选区
*/
function fillSelection() {
if (!hasSelection.value) return;
showColorPicker.value = true;
}
/**
* 套索抠图到新图层
*/
function copySelectionToNewLayer() {
if (!hasSelection.value) return;
props.commandManager.execute(
new LassoCutoutCommand({
canvas: props.canvas,
layerManager: props.layerManager,
selectionManager: props.selectionManager,
toolManager: props.toolManager,
})
);
checkSelectionStatus();
}
/**
* 清除选区内容
*/
function clearSelectionContent() {
if (!hasSelection.value) return;
props.commandManager.execute(
new ClearSelectionContentCommand({
canvas: props.canvas,
layerManager: props.layerManager,
selectionManager: props.selectionManager,
})
);
checkSelectionStatus();
}
/**
* 打开羽化设置弹窗
*/
function openFeatherDialog() {
showFeatherDialog.value = true;
}
/**
* 取消羽化设置
*/
function cancelFeather() {
showFeatherDialog.value = false;
}
/**
* 确认羽化设置
*/
function confirmFeather() {
applyFeather();
showFeatherDialog.value = false;
}
/**
* 取消颜色选择
*/
function cancelColorPicker() {
showColorPicker.value = false;
}
/**
* 确认颜色选择
*/
function confirmColorPicker() {
if (!hasSelection.value) return;
props.commandManager.execute(
new FillSelectionCommand({
canvas: props.canvas,
layerManager: props.layerManager,
selectionManager: props.selectionManager,
color: fillColor.value,
})
);
checkSelectionStatus();
showColorPicker.value = false;
}
</script>
<style scoped lang="less">
.selection-toolbar {
position: absolute;
bottom: 22px;
left: 20px;
right: 20px;
max-width: min(90vw, 640px);
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border-radius: 8px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
z-index: 1000;
color: #333;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* 平板和手机适配 */
@media screen and (max-width: 768px) {
.selection-toolbar {
bottom: 15px;
left: 15px;
right: 15px;
max-width: calc(100vw - 30px);
border-radius: 6px;
}
}
@media screen and (max-width: 480px) {
.selection-toolbar {
bottom: 10px;
left: 10px;
right: 10px;
max-width: calc(100vw - 20px);
}
}
.selection-toolbar.is-active {
transform: translateY(0);
}
.toolbar-header {
// display: flex;
// justify-content: center;
// align-items: center;
padding: 8px 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background-color: rgba(255, 255, 255, 0.8);
border-radius: 8px 8px 0 0;
}
.header-title {
font-size: 13px;
font-weight: 500;
color: #333;
text-align: left;
}
.header-btn {
background: none;
border: none;
color: #333;
font-size: 12px;
cursor: pointer;
padding: 3px 6px;
border-radius: 3px;
transition: background-color 0.2s ease;
min-width: 32px;
}
.header-btn:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.close-btn {
color: #666;
}
.toolbar-section {
padding: 0 0 10px;
}
.tool-types {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding: 10px;
}
/* 平板适配 */
@media screen and (max-width: 768px) {
.tool-types {
gap: 6px;
padding: 8px;
}
.toolbar-header {
padding: 6px 12px;
border-radius: 6px 6px 0 0;
}
}
/* 手机适配 */
@media screen and (max-width: 480px) {
.tool-types {
gap: 4px;
padding: 6px;
}
.toolbar-header {
padding: 5px 10px;
}
.header-title {
font-size: 12px;
}
}
.tool-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.05);
border: none;
border-radius: 6px;
padding: 10px 5px;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.tool-btn span {
margin-top: 6px;
font-size: 11px;
}
.tool-btn svg {
width: 24px;
height: 24px;
}
.tool-btn:hover {
background-color: rgba(0, 0, 0, 0.08);
}
.tool-btn.active {
background-color: #007aff;
color: white;
}
.toolbar-divider {
height: 1px;
background-color: rgba(0, 0, 0, 0.05);
margin: 0 10px 10px;
}
.tool-actions {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 5px;
padding: 0 10px;
}
/* 平板适配 - 每行4个按钮 */
@media screen and (max-width: 768px) {
.tool-actions {
grid-template-columns: repeat(3, 1fr);
gap: 8px 6px;
padding: 0 8px;
}
}
/* 手机适配 - 每行3个按钮 */
@media screen and (max-width: 480px) {
.tool-actions {
grid-template-columns: repeat(3, 1fr);
gap: 6px 4px;
padding: 0 6px;
}
.header-btn {
font-size: 11px;
padding: 2px 4px;
min-width: 28px;
}
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
background: none;
border: none;
color: #333;
cursor: pointer;
padding: 8px 2px;
}
.action-btn svg {
width: 22px;
height: 22px;
margin-bottom: 8px;
}
.btn-text {
font-size: 10px;
text-align: center;
}
.action-btn:hover {
color: #007aff;
}
.action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 对话框样式 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.dialog-container {
background-color: #ffffff;
border-radius: 12px;
width: 280px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
overflow: hidden;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.dialog-header h3 {
margin: 0;
font-size: 15px;
color: #333;
font-weight: 500;
}
.close-dialog-btn {
background: none;
border: none;
color: #666;
font-size: 18px;
cursor: pointer;
padding: 0;
line-height: 1;
}
.dialog-content {
padding: 15px;
}
.feather-control {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.slider-control {
flex: 1;
height: 4px;
background: rgba(0, 0, 0, 0.1);
border-radius: 2px;
-webkit-appearance: none;
appearance: none;
margin-right: 10px;
}
.slider-control::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #007aff;
cursor: pointer;
}
.feather-value {
font-size: 14px;
color: #333;
min-width: 40px;
text-align: right;
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
.cancel-btn,
.confirm-btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
border: none;
}
.cancel-btn {
background-color: rgba(0, 0, 0, 0.05);
color: #333;
}
.confirm-btn {
background-color: #007aff;
color: white;
}
.color-picker {
width: 100%;
height: 40px;
border: none;
border-radius: 6px;
cursor: pointer;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(30px);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,411 @@
<script setup>
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
import { OperationType } from "../utils/layerHelper";
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue";
const props = defineProps({
activeTool: String,
minimapEnabled: {
type: Boolean,
default: true,
},
isRedGreenMode: {
type: Boolean,
default: false,
},
});
const commandManager = inject("commandManager");
// 撤销/重做按钮状态
const canUndo = ref(false);
const canRedo = ref(false);
// 监听命令管理器状态变化
commandManager.setChangeCallback((info) => {
canUndo.value = info.canUndo;
canRedo.value = info.canRedo;
});
// 撤销/重做操作
const undoFun = () => commandManager.undo();
const redoFun = () => commandManager.redo();
const emit = defineEmits([
"tool-selected",
"trigger-image-upload",
"add-text",
"undo",
"redo",
"toggle-minimap",
"zoom-in",
"zoom-out",
"toggle-red-green-mode",
]);
// 普通模式工具列表
const normalToolsList = ref([
{
id: "undo",
title: "Undo",
action: undo,
icon: { name: "CUndo", size: "20" },
class: "undo-btn",
},
{
id: "redo",
title: "Redo",
action: redo,
icon: { name: "CRedo", size: "20" },
class: "redo-btn",
},
{
id: OperationType.DRAW,
title: "Drawing",
action: () => selectTool(OperationType.DRAW),
icon: { name: "CBrush", size: "24" },
class: "draw-btn",
},
{
id: OperationType.ERASER,
title: "Eraser",
action: () => selectTool(OperationType.ERASER),
icon: { name: "CEraser", size: "22" },
class: "eraser-btn",
},
{
id: OperationType.PAN,
title: "Pan",
action: () => selectTool(OperationType.PAN),
icon: { name: "CHand", size: "28" },
class: "hand-btn",
},
{
id: OperationType.SELECT,
title: "Select",
action: () => selectTool(OperationType.SELECT),
icon: { name: "CSelect", size: "28" },
class: "select-btn",
},
{
id: OperationType.LIQUIFY,
title: "Liquefying",
action: () => selectTool(OperationType.LIQUIFY),
icon: { name: "CLiquefying", size: "32" },
class: "liquify-btn",
},
{
id: OperationType.LASSO,
title: "Lasso",
action: () => selectTool(OperationType.LASSO),
icon: { name: "CLasso", size: "28" },
class: "lasso-btn",
activeList: [
OperationType.LASSO,
OperationType.LASSO_RECTANGLE,
OperationType.AREA_CUSTOM,
OperationType.AREA_RECTANGLE,
],
},
{
id: "zoomIn",
title: "Zoom In",
action: zoomIn,
icon: { name: "CZoomIn", size: "30" },
class: "zoom-in-btn",
},
{
id: "zoomOut",
title: "Zoom Out",
action: zoomOut,
icon: { name: "CZoomOut", size: "26" },
class: "zoom-out-btn",
},
{
id: "upload",
title: "Upload Image",
action: triggerImageUpload,
icon: { name: "CUpload", size: "26" },
class: "upload-btn",
},
{
id: "addText",
title: "Add Text",
action: () => addText(),
icon: { name: "CFont", size: "20" },
class: "text-btn",
},
]);
// 红绿图模式工具列表
const redGreenToolsList = ref([
{
id: "undo",
title: "Undo",
action: undo,
icon: { name: "CUndo", size: "20" },
class: "undo-btn",
},
{
id: "redo",
title: "Redo",
action: redo,
icon: { name: "CRedo", size: "20" },
class: "redo-btn",
},
{
id: OperationType.RED_BRUSH,
title: "Red Brush (R)",
action: () => selectTool(OperationType.RED_BRUSH),
icon: { name: "CBrush", size: "24" },
class: "red-brush-btn",
style: { color: "#FF0000" },
},
{
id: OperationType.GREEN_BRUSH,
title: "Green Brush (G)",
action: () => selectTool(OperationType.GREEN_BRUSH),
icon: { name: "CBrush", size: "24" },
class: "green-brush-btn",
style: { color: "#00AA00" },
},
{
id: OperationType.ERASER,
title: "Eraser (E)",
action: () => selectTool(OperationType.ERASER),
icon: { name: "CEraser", size: "22" },
class: "eraser-btn",
},
{
id: "zoomIn",
title: "Zoom In",
action: zoomIn,
icon: { name: "CZoomIn", size: "30" },
class: "zoom-in-btn",
},
{
id: "zoomOut",
title: "Zoom Out",
action: zoomOut,
icon: { name: "CZoomOut", size: "26" },
class: "zoom-out-btn",
},
]);
// 根据模式选择工具列表
const toolsList = computed(() => {
return props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
});
function selectTool(tool) {
emit("tool-selected", tool);
}
function triggerImageUpload() {
emit("trigger-image-upload");
}
function addText() {
emit("add-text");
}
function undo() {
if (!canUndo.value) return;
undoFun();
emit("undo", {
canUndo: canUndo.value,
canRedo: canRedo.value,
commandManager,
});
}
function redo() {
if (!canRedo.value) return;
emit("redo", {
canUndo: canUndo.value,
canRedo: canRedo.value,
commandManager,
});
redoFun();
}
function toggleMinimap() {
emit("toggle-minimap", !props.minimapEnabled);
}
function zoomIn() {
emit("zoom-in");
}
function zoomOut() {
emit("zoom-out");
}
function toggleRedGreenMode() {
emit("toggle-red-green-mode");
}
// 键盘快捷键处理
function handleKeyDown(event) {
// 在红绿图模式下处理特定快捷键
if (props.isRedGreenMode) {
const key = event.key.toUpperCase();
// 当处于输入状态时不触发快捷键
if (
event.target.tagName === "INPUT" ||
event.target.tagName === "TEXTAREA"
) {
return;
}
switch (key) {
case "R":
selectTool(OperationType.RED_BRUSH);
event.preventDefault();
break;
case "G":
selectTool(OperationType.GREEN_BRUSH);
event.preventDefault();
break;
case "E":
selectTool(OperationType.ERASER);
event.preventDefault();
break;
}
}
}
onMounted(() => {
// 添加键盘事件监听
window.addEventListener("keydown", handleKeyDown);
});
onUnmounted(() => {
// 移除键盘事件监听
window.removeEventListener("keydown", handleKeyDown);
});
</script>
<template>
<div class="tools-sidebar">
<div
v-for="tool in toolsList"
:key="tool.id"
:class="[
'tool-btn',
tool.class,
{
active:
tool.id === activeTool ||
tool.id === activeTool.toLowerCase() ||
tool?.activeList?.includes(activeTool),
disabled:
(tool.id === 'undo' && !canUndo) ||
(tool.id === 'redo' && !canRedo),
},
]"
:style="tool.style"
@click="tool.action"
>
<SvgIcon :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
<div class="tool-tooltip">{{ tool.title }}</div>
</div>
</div>
</template>
<style scoped>
.tools-sidebar {
display: flex;
flex-direction: column;
gap: 10px;
padding: 15px 10px;
border-right: 1px solid #e0e0e0;
background-color: #ffffff;
user-select: none;
}
.tool-btn {
position: relative;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
color: #333;
transition: all 0.2s ease;
}
.tool-btn:hover {
background-color: #f0f0f0;
}
.tool-btn:hover .tool-tooltip {
display: block;
}
.tool-btn.active {
background-color: #e6f7ff;
color: #1890ff;
}
.tool-btn.disabled {
cursor: not-allowed;
color: #e0e0e0;
}
.tool-tooltip {
display: none;
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 4px 8px;
border-radius: 4px;
margin-left: 8px;
white-space: nowrap;
font-size: 12px;
z-index: 10;
}
.tool-tooltip:before {
content: "";
position: absolute;
top: 50%;
right: 100%;
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent rgba(0, 0, 0, 0.7) transparent transparent;
}
.red-green-mode {
background-color: #fff4f4;
}
.mode-indicator {
margin-bottom: 10px;
padding: 8px;
border-radius: 4px;
background-color: #ffcccc;
color: #a33;
font-size: 14px;
text-align: center;
}
.mode-label {
font-weight: bold;
}
.mode-hint {
font-size: 12px;
margin-top: 4px;
}
</style>

View File

@@ -0,0 +1,688 @@
<template>
<div class="vertical-slider-container" :class="customClass">
<div
class="slider-track"
ref="sliderTrack"
@mousedown="startSliding"
@touchstart.prevent="startTouchSliding"
@click="handleClick"
>
<div
class="slider-fill"
:style="{ height: `${displayPercentage}%` }"
></div>
<div
class="slider-thumb"
:style="{
bottom: `${displayPercentage}%`,
transform: `translateX(0) translateY(8px) scale(${thumbScale})`,
}"
></div>
<div
v-for="(preset, index) in presets"
:key="`preset-${index}`"
class="slider-notch"
:class="{ active: isActivePreset(preset) }"
:style="{ bottom: `${calculatePresetPosition(preset)}%` }"
@click.stop="setValue(preset)"
></div>
<div
v-for="(preset, index) in memorizedValues"
:key="`preset-${index}`"
class="slider-notch"
:class="{ active: isActivePreset(preset) }"
:style="{ bottom: `${calculatePresetPosition(preset)}%` }"
@click.stop="setValue(preset)"
></div>
</div>
<!-- 提示框 -->
<transition name="fade">
<div class="slider-tooltip" v-if="tooltipVisible" ref="tooltip">
<slot name="tooltip-content" :value="modelValue"></slot>
</div>
</transition>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
const props = defineProps({
modelValue: {
type: Number,
required: true,
},
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 100,
},
presets: {
type: Array,
default: () => [],
},
memorizedValues: {
type: Array,
default: () => [],
},
snapThreshold: {
type: Number,
default: 5,
},
// 用于判断当前值与预设值是否匹配的阈值
activeThreshold: {
type: Number,
default: 0.05,
},
// 是否将预设值位置按百分比计算
isPercentage: {
type: Boolean,
default: false,
},
customClass: {
type: String,
default: "",
},
// 添加步进属性,控制数值的步长
step: {
type: Number,
default: 1,
},
// 增加外部控制tooltip显示的属性
showTooltip: {
type: Boolean,
default: false,
},
// 是否启用点击外部关闭tooltip
closeOnOutsideClick: {
type: Boolean,
default: true,
},
});
const emit = defineEmits([
"update:modelValue",
"slide-start",
"slide-end",
"click",
"update:showTooltip", // 新增emit事件用于双向绑定tooltip状态
]);
const sliderTrack = ref(null);
const tooltip = ref(null);
const isSliding = ref(false);
const internalShowTooltip = ref(false);
const slideMoved = ref(false); // 用于跟踪是否发生了滑动移动
const hideTooltipTimer = ref(null); // 用于存储隐藏tooltip的计时器
// 在script setup顶部添加新的ref
const isSnapping = ref(false);
const snapLockValue = ref(null);
const snapLockTimeout = ref(null);
const thumbScale = ref(1); // 新增:用于控制滑块的缩放效果
// 定义一个统一的延迟时间常量
const TOOLTIP_HIDE_DELAY = 1500; // 1.5秒,可根据需要调整
// 开始新的隐藏tooltip计时器
function startHideTooltipTimer() {
// 先清除已有的计时器
clearHideTooltipTimer();
// 创建新计时器
hideTooltipTimer.value = setTimeout(() => {
// 只有在用户不在滑动时才隐藏tooltip
if (!isSliding.value) {
updateTooltipVisibility(false);
}
hideTooltipTimer.value = null;
}, TOOLTIP_HIDE_DELAY);
}
// 清除隐藏tooltip的计时器
function clearHideTooltipTimer() {
if (hideTooltipTimer.value) {
clearTimeout(hideTooltipTimer.value);
hideTooltipTimer.value = null;
}
}
// 计算tooltip的可见性优先使用props中的showTooltip否则使用内部状态
const tooltipVisible = computed(() => {
return props.showTooltip !== undefined
? props.showTooltip
: internalShowTooltip.value;
});
// 更新tooltip状态的方法
function updateTooltipVisibility(visible) {
if (props.showTooltip !== undefined) {
// 如果父组件提供了showTooltip属性通过emit更新
emit("update:showTooltip", visible);
} else {
// 否则更新内部状态
internalShowTooltip.value = visible;
}
}
// 计算显示百分比
const displayPercentage = computed(() => {
const range = props.max - props.min;
return ((props.modelValue - props.min) / range) * 100;
});
// 判断是否是活动预设值
function isActivePreset(preset) {
return Math.abs(props.modelValue - preset) < props.activeThreshold;
}
// 计算预设值的位置
function calculatePresetPosition(preset) {
if (props.isPercentage) {
return preset * 100;
} else {
const range = props.max - props.min;
return ((preset - props.min) / range) * 100;
}
}
// 将值舍入到最接近的步进值
function roundToStep(value) {
if (!props.step || props.step <= 0) return value;
const numSteps = Math.round((value - props.min) / props.step);
return props.min + numSteps * props.step;
}
// 鼠标滑动处理
function startSliding(event) {
isSliding.value = true;
slideMoved.value = false; // 重置滑动状态
updateTooltipVisibility(true);
clearHideTooltipTimer(); // 清除任何现有的隐藏计时器
updateValueFromMousePosition(event);
emit("slide-start");
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", stopSliding);
}
function handleMouseMove(event) {
if (isSliding.value) {
slideMoved.value = true; // 标记已经发生了滑动
updateValueFromMousePosition(event);
}
}
function updateValueFromMousePosition(event) {
const rect = sliderTrack.value.getBoundingClientRect();
const trackHeight = rect.height;
const posY = event.clientY - rect.top;
// 计算相对位置并转换为min-max范围的值
let newValue = props.max - (posY / trackHeight) * (props.max - props.min);
// 边界检查
newValue = Math.max(props.min, Math.min(props.max, newValue));
// 应用步进
if (props.step > 0) {
newValue = roundToStep(newValue);
}
// 如果当前处于吸附锁定状态,并且移动不超过锁定阈值,则保持当前值不变
if (isSnapping.value && snapLockValue.value !== null) {
// 增加吸附力度:扩大保持吸附的阈值范围
if (
Math.abs(newValue - snapLockValue.value) <
(props.snapThreshold / trackHeight) * (props.max - props.min) * 1.2
) {
newValue = snapLockValue.value;
} else {
// 移动距离超过阈值,解除吸附锁定
clearSnapLock();
}
} else {
// 检查是否可以进入吸附状态
const snapPercentage =
(props.snapThreshold / trackHeight) * (props.max - props.min);
// 检查是否接近预设值,增加吸附判定范围
for (const preset of props.presets) {
if (Math.abs(newValue - preset) < snapPercentage * 0.4) {
// 增加判定范围
// 进入吸附状态
setSnapLock(preset);
newValue = preset;
break;
}
}
// 检查是否接近记忆值
if (!isSnapping.value) {
for (const memValue of props.memorizedValues) {
if (Math.abs(newValue - memValue) < snapPercentage * 0.4) {
// 增加判定范围
// 进入吸附状态
setSnapLock(memValue);
newValue = memValue;
break;
}
}
}
}
setValue(newValue);
}
// 设置吸附锁定
function setSnapLock(value) {
if (!isSnapping.value) {
isSnapping.value = true;
snapLockValue.value = value;
// 添加视觉反馈:放大滑块
thumbScale.value = 1.3;
// 触感反馈:在支持的设备上触发振动
if (navigator.vibrate) {
navigator.vibrate(15); // 短暂振动15ms
}
// 延长锁定时间,增强吸附感
clearTimeout(snapLockTimeout.value);
snapLockTimeout.value = setTimeout(() => {
// 恢复滑块大小
thumbScale.value = 1;
clearSnapLock();
}, 500); // 500ms的吸附感觉比原来的300ms更强
}
}
// 清除吸附锁定
function clearSnapLock() {
isSnapping.value = false;
snapLockValue.value = null;
clearTimeout(snapLockTimeout.value);
snapLockTimeout.value = null;
thumbScale.value = 1; // 确保滑块恢复正常大小
}
function setValue(value) {
// 应用步进,确保即使通过点击预设值也会遵循步进
if (props.step > 0) {
value = roundToStep(value);
}
emit("update:modelValue", value);
}
function stopSliding() {
// 清除吸附锁定
clearSnapLock();
if (isSliding.value) {
// 如果确实进行了滑动,则发送滑动结束事件
if (slideMoved.value) {
emit("slide-end", { isSlide: true });
// 只有在滑动操作后才启动隐藏计时器
startHideTooltipTimer();
} else {
// 只是点击不是滑动不自动隐藏tooltip
emit("slide-end", { isSlide: false });
}
}
isSliding.value = false;
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", stopSliding);
}
// 处理点击事件
function handleClick(event) {
// 对于纯点击操作发出click事件
if (!slideMoved.value) {
clearHideTooltipTimer(); // 清除任何现有的隐藏计时器
updateTooltipVisibility(true); // 确保tooltip在点击时显示
emit("click");
// 点击不启动隐藏计时器
}
}
// 触摸事件处理
function startTouchSliding(event) {
isSliding.value = true;
slideMoved.value = false; // 重置滑动状态
updateTooltipVisibility(true);
updateValueFromTouchPosition(event);
// 滑动开始的触感反馈
if (navigator.vibrate) {
navigator.vibrate(10); // 更轻微的振动反馈
}
emit("slide-start");
window.addEventListener("touchmove", handleTouchMove, { passive: false });
window.addEventListener("touchend", stopTouchSliding);
}
function handleTouchMove(event) {
if (isSliding.value) {
slideMoved.value = true; // 标记已经发生了滑动
event.preventDefault(); // 阻止页面滚动
updateValueFromTouchPosition(event);
}
}
function updateValueFromTouchPosition(event) {
if (!event.touches || event.touches.length === 0) return;
const touch = event.touches[0];
const rect = sliderTrack.value.getBoundingClientRect();
const trackHeight = rect.height;
const posY = touch.clientY - rect.top;
// 计算相对位置并转换为min-max范围的值
let newValue = props.max - (posY / trackHeight) * (props.max - props.min);
// 边界检查
newValue = Math.max(props.min, Math.min(props.max, newValue));
// 应用步进
if (props.step > 0) {
newValue = roundToStep(newValue);
}
// 如果当前处于吸附锁定状态,并且移动不超过锁定阈值,则保持当前值不变
if (isSnapping.value && snapLockValue.value !== null) {
// 增加吸附力度:扩大保持吸附的阈值范围
if (
Math.abs(newValue - snapLockValue.value) <
(props.snapThreshold / trackHeight) * (props.max - props.min) * 1.5
) {
newValue = snapLockValue.value;
} else {
// 移动距离超过阈值,解除吸附锁定,并提供反馈
clearSnapLock();
if (navigator.vibrate) {
navigator.vibrate(8); // 解除吸附的轻微振动
}
}
} else {
// 检查是否可以进入吸附状态
const snapPercentage =
(props.snapThreshold / trackHeight) * (props.max - props.min);
// 检查是否接近预设值,增加吸附判定范围
for (const preset of props.presets) {
if (Math.abs(newValue - preset) < snapPercentage * 0.4) {
// 增加判定范围
// 进入吸附状态
setSnapLock(preset);
newValue = preset;
break;
}
}
// 检查是否接近记忆值
if (!isSnapping.value) {
for (const memValue of props.memorizedValues) {
if (Math.abs(newValue - memValue) < snapPercentage * 0.4) {
// 增加判定范围
// 进入吸附状态
setSnapLock(memValue);
newValue = memValue;
break;
}
}
}
}
setValue(newValue);
}
function stopTouchSliding() {
// 滑动结束时的反馈
if (slideMoved.value && navigator.vibrate) {
navigator.vibrate(12); // 滑动结束的振动反馈
}
// 清除吸附锁定
clearSnapLock();
if (isSliding.value) {
// 如果确实进行了滑动,则发送滑动结束事件
if (slideMoved.value) {
emit("slide-end", { isSlide: true });
// 只有在滑动操作后才启动隐藏计时器
startHideTooltipTimer();
} else {
// 只是点击不是滑动不自动隐藏tooltip
emit("slide-end", { isSlide: false });
}
}
isSliding.value = false;
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", stopTouchSliding);
}
// 添加点击外部关闭tooltip的处理函数
function handleOutsideClick(event) {
if (!props.closeOnOutsideClick || !tooltipVisible.value) return;
// 如果正在滑动,不处理外部点击事件
if (isSliding.value) return;
// 检查点击是否在slider组件外部
const containerEl = sliderTrack.value?.parentElement;
const tooltipEl = tooltip.value;
if (
containerEl &&
tooltipEl &&
!containerEl.contains(event.target) &&
!tooltipEl.contains(event.target)
) {
updateTooltipVisibility(false);
}
}
onMounted(() => {
// 添加全局点击事件监听
if (props.closeOnOutsideClick) {
// 使用 setTimeout 确保点击事件在其他处理程序之后执行
window.addEventListener("click", (e) =>
setTimeout(() => handleOutsideClick(e), 0)
);
}
});
onBeforeUnmount(() => {
// 清理事件监听器
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", stopSliding);
window.removeEventListener("touchmove", handleTouchMove);
window.removeEventListener("touchend", stopTouchSliding);
window.removeEventListener("click", handleOutsideClick);
// 清除任何剩余的计时器
clearHideTooltipTimer();
clearSnapLock();
});
</script>
<style scoped lang="less">
.vertical-slider-container {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
height: 150px;
position: relative;
// margin-top: 8px;
// margin-bottom: 0px;
}
.slider-track {
position: relative;
width: 32px;
height: 100%;
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
cursor: pointer;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.slider-fill {
position: absolute;
bottom: 0;
width: 100%;
// background: linear-gradient(to top, #2196f3, #64b5f6);
border-radius: 4px;
}
.slider-thumb {
position: absolute;
width: 100%;
height: 16px;
background: #fff;
// border: 1px solid #2196f3;
border-radius: 3px;
cursor: grab;
box-shadow: 0 0px 4px rgba(0, 0, 0, 0.4);
transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275); // 更平滑的动画效果
&:active {
cursor: grabbing;
}
}
// 添加iPad和移动设备的专有样式
@media (pointer: coarse) {
.slider-thumb {
height: 20px; // 在触摸设备上增加滑块尺寸,更容易点击
border-radius: 4px;
box-shadow: 0 0px 6px rgba(0, 0, 0, 0.5); // 更明显的阴影
}
.slider-track {
width: 40px; // 在触摸设备上增加宽度
}
.slider-notch {
width: 60%; // 增加刻度线宽度
height: 3px; // 增加刻度线高度
}
}
// 当设备支持悬停时的效果 (通常是桌面设备)
@media (hover: hover) {
.slider-thumb:hover {
box-shadow: 0 0 5px rgba(33, 150, 243, 0.4);
}
}
.slider-notch {
position: absolute;
left: 0;
width: 50%;
height: 2px;
background: #999;
border-radius: 2px;
transform: translateY(1px) translateX(50%);
transition: all 0.2s ease;
&.active {
background: #2196f3;
}
&:hover {
background: #333;
}
}
.slider-tooltip {
position: absolute;
left: calc(100% + 15px);
background: rgba(255, 255, 255, 0.95);
border-radius: 10px;
padding: 10px;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
min-width: 120px;
z-index: 10;
&::before {
content: "";
position: absolute;
left: -8px;
top: 50%;
transform: translateY(-50%);
border-width: 8px 8px 8px 0;
border-style: solid;
border-color: transparent rgba(255, 255, 255, 0.95) transparent transparent;
}
}
// 自定义滑块颜色
// .size-slider {
// .slider-fill {
// // background: linear-gradient(to top, #2196f3, #64b5f6);
// }
// .slider-thumb {
// border-color: #2196f3;
// }
// .slider-notch.active {
// background: #2196f3;
// }
// }
// .opacity-slider {
// .slider-fill {
// // background: linear-gradient(to top, #ff9800, #ffb74d);
// }
// .slider-thumb {
// border-color: #ff9800;
// }
// .slider-notch.active {
// background: #ff9800;
// }
// }
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateX(-10px);
}
// 响应式调整
@media (max-height: 600px) {
.vertical-slider-container {
height: 120px;
}
}
@media (max-width: 768px) {
.slider-tooltip {
left: calc(100% + 10px);
min-width: 100px;
}
}
</style>

View File

@@ -0,0 +1,32 @@
import { OperationType } from "../utils/layerHelper";
// 画布默认配置
export const canvasConfig = {
width: 1024, // 画布宽度
height: 1024, // 画布高度
backgroundColor: "#ffffff", // 背景颜色
objectCaching: true, // 是否启用对象缓存
enableRetinaScaling: true, // 是否启用视网膜缩放
brushWidth: 5, // 画笔宽度
brushOpacity: 1, // 画笔透明度
brushColor: "#000000", // 画笔颜色
brushType: "pencil", // 画笔类型
brushTypeList: [
{ name: "pencil", text: "铅笔" },
// { name: "eraser", text: "橡皮擦" },
{ name: "brush", text: "画笔" },
{ name: "spray", text: "喷枪" },
{ name: "rectangle", text: "矩形" },
{ name: "circle", text: "圆形" },
], // 画笔类型列表
layerWidth: 250, // 图层宽度
// History settings
maxHistorySteps: 50, // 最大历史记录步数
onlyActiveLayerEditable: false, // 是否只有当前活动图层可编辑
// 默认工具模式
defaultTool: OperationType.DRAW || OperationType.SELECT, // 默认工具 TODO: 默认的地方可以陆续改成这个
isCropBackground: true, // 是否要裁剪背景层以外的内容
};
export default canvasConfig;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,355 @@
/**
* 图片导出管理器
* 负责处理画布的图片导出功能,支持多种导出选项和图层过滤
*/
export class ExportManager {
constructor(canvasManager, layerManager) {
this.canvasManager = canvasManager;
this.layerManager = layerManager;
this.canvas = canvasManager.canvas;
}
/**
* 导出图片
* @param {Object} options 导出选项
* @param {Boolean} options.isContainBg 是否包含背景图层
* @param {Boolean} options.isContainFixed 是否包含固定图层
* @param {String} options.layerId 导出具体图层ID
* @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
* @returns {String} 导出的图片数据URL
*/
exportImage(options = {}) {
const {
isContainBg = false,
isContainFixed = false,
layerId = "",
layerIdArray = [],
expPicType = "png"
} = options;
try {
// 如果指定了具体图层ID导出指定图层
if (layerId) {
return this._exportSpecificLayer(layerId, expPicType);
}
// 如果指定了多个图层ID导出多个图层
if (layerIdArray && layerIdArray.length > 0) {
return this._exportMultipleLayers(layerIdArray, expPicType, isContainBg, isContainFixed);
}
// 默认导出所有可见图层
return this._exportAllLayers(expPicType, isContainBg, isContainFixed);
} catch (error) {
console.error("导出图片失败:", error);
throw new Error(`图片导出失败: ${error.message}`);
}
}
/**
* 导出指定单个图层
* @param {String} layerId 图层ID
* @param {String} expPicType 导出类型
* @returns {String} 图片数据URL
* @private
*/
_exportSpecificLayer(layerId, expPicType) {
if (!this.layerManager) {
throw new Error("图层管理器未初始化");
}
const layer = this._getLayerById(layerId);
if (!layer) {
throw new Error(`未找到ID为 ${layerId} 的图层`);
}
if (!layer.visible) {
console.warn(`图层 ${layer.name} 不可见,将导出空白图片`);
}
// 创建临时画布
const tempCanvas = this._createExportCanvas();
const tempFabricCanvas = this._createTempFabricCanvas(tempCanvas);
try {
// 只添加指定图层的对象
this._addLayerObjectsToCanvas(tempFabricCanvas, layer);
// 渲染并导出
tempFabricCanvas.renderAll();
return this._generateDataURL(tempCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
}
/**
* 导出多个指定图层
* @param {Array} layerIdArray 图层ID数组
* @param {String} expPicType 导出类型
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @returns {String} 图片数据URL
* @private
*/
_exportMultipleLayers(layerIdArray, expPicType, isContainBg, isContainFixed) {
if (!this.layerManager) {
throw new Error("图层管理器未初始化");
}
// 创建临时画布
const tempCanvas = this._createExportCanvas();
const tempFabricCanvas = this._createTempFabricCanvas(tempCanvas);
try {
// 按照图层顺序添加指定的图层
const allLayers = this._getAllLayers();
allLayers.forEach(layer => {
if (!layerIdArray.includes(layer.id)) return;
// 检查图层类型过滤条件
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) return;
if (layer.visible) {
this._addLayerObjectsToCanvas(tempFabricCanvas, layer);
}
});
// 渲染并导出
tempFabricCanvas.renderAll();
return this._generateDataURL(tempCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
}
/**
* 导出所有图层
* @param {String} expPicType 导出类型
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @returns {String} 图片数据URL
* @private
*/
_exportAllLayers(expPicType, isContainBg, isContainFixed) {
// 创建临时画布
const tempCanvas = this._createExportCanvas();
const tempFabricCanvas = this._createTempFabricCanvas(tempCanvas);
try {
// 获取所有图层并按顺序添加
const allLayers = this._getAllLayers();
allLayers.forEach(layer => {
// 检查图层类型过滤条件
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) return;
if (layer.visible) {
this._addLayerObjectsToCanvas(tempFabricCanvas, layer);
}
});
// 渲染并导出
tempFabricCanvas.renderAll();
return this._generateDataURL(tempCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
}
/**
* 判断是否应该包含该图层
* @param {Object} layer 图层对象
* @param {Boolean} isContainBg 是否包含背景图层
* @param {Boolean} isContainFixed 是否包含固定图层
* @returns {Boolean} 是否包含
* @private
*/
_shouldIncludeLayer(layer, isContainBg, isContainFixed) {
// 背景图层处理
if (layer.type === 'background' || layer.isBackground) {
return isContainBg;
}
// 固定图层处理
if (layer.type === 'fixed' || layer.isFixed || layer.locked) {
return isContainFixed;
}
// 其他图层默认包含
return true;
}
/**
* 创建导出用的临时画布
* @returns {HTMLCanvasElement} 临时画布
* @private
*/
_createExportCanvas() {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = this.canvas.width || 800;
tempCanvas.height = this.canvas.height || 600;
return tempCanvas;
}
/**
* 创建临时Fabric画布
* @param {HTMLCanvasElement} tempCanvas 临时画布元素
* @returns {fabric.StaticCanvas} 临时Fabric画布
* @private
*/
_createTempFabricCanvas(tempCanvas) {
const { fabric } = window;
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
width: this.canvas.width || 800,
height: this.canvas.height || 600,
backgroundColor: this.canvas.backgroundColor || 'transparent'
});
// 设置高质量渲染选项
tempFabricCanvas.enableRetinaScaling = true;
tempFabricCanvas.imageSmoothingEnabled = true;
return tempFabricCanvas;
}
/**
* 将图层对象添加到临时画布
* @param {fabric.StaticCanvas} tempCanvas 临时画布
* @param {Object} layer 图层对象
* @private
*/
_addLayerObjectsToCanvas(tempCanvas, layer) {
if (!layer) return;
// 处理背景图层
if (layer.type === 'background' && layer.fabricObject) {
this._cloneAndAddObject(tempCanvas, layer.fabricObject);
return;
}
// 处理普通图层的对象
if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) {
layer.fabricObjects.forEach(obj => {
if (obj && obj.visible !== false) {
this._cloneAndAddObject(tempCanvas, obj);
}
});
}
// 处理单个fabricObject的情况
if (layer.fabricObject && !layer.fabricObjects) {
this._cloneAndAddObject(tempCanvas, layer.fabricObject);
}
// 处理分组图层的子图层
if (layer.children && Array.isArray(layer.children)) {
layer.children.forEach(childLayerId => {
const childLayer = this._getLayerById(childLayerId);
if (childLayer && childLayer.visible) {
this._addLayerObjectsToCanvas(tempCanvas, childLayer);
}
});
}
}
/**
* 克隆并添加对象到临时画布
* @param {fabric.StaticCanvas} tempCanvas 临时画布
* @param {fabric.Object} obj Fabric对象
* @private
*/
_cloneAndAddObject(tempCanvas, obj) {
if (!obj) return;
try {
obj.clone((cloned) => {
if (cloned) {
// 确保克隆对象的属性正确
cloned.set({
selectable: false,
evented: false,
visible: true
});
tempCanvas.add(cloned);
}
}, ['id', 'layerId', 'name']); // 保留自定义属性
} catch (error) {
console.warn("克隆对象失败:", error);
}
}
/**
* 生成数据URL
* @param {HTMLCanvasElement} canvas 画布元素
* @param {String} expPicType 导出类型
* @returns {String} 数据URL
* @private
*/
_generateDataURL(canvas, expPicType) {
const format = expPicType.toLowerCase();
switch (format) {
case 'jpg':
case 'jpeg':
return canvas.toDataURL('image/jpeg', 0.9);
case 'svg':
// SVG导出需要特殊处理这里先返回PNG
console.warn("SVG导出暂未实现返回PNG格式");
return canvas.toDataURL('image/png', 1.0);
case 'png':
default:
return canvas.toDataURL('image/png', 1.0);
}
}
/**
* 清理临时画布资源
* @param {fabric.StaticCanvas} tempFabricCanvas 临时Fabric画布
* @private
*/
_cleanupTempCanvas(tempFabricCanvas) {
if (tempFabricCanvas) {
try {
tempFabricCanvas.dispose();
} catch (error) {
console.warn("清理临时画布失败:", error);
}
}
}
/**
* 获取所有图层
* @returns {Array} 图层数组
* @private
*/
_getAllLayers() {
if (this.layerManager && this.layerManager.layers) {
return this.layerManager.layers.value || [];
}
return [];
}
/**
* 根据ID获取图层
* @param {String} layerId 图层ID
* @returns {Object|null} 图层对象
* @private
*/
_getLayerById(layerId) {
if (this.layerManager && this.layerManager.getLayerById) {
return this.layerManager.getLayerById(layerId);
}
// 备用方法:直接从图层数组中查找
const allLayers = this._getAllLayers();
return allLayers.find(layer => layer.id === layerId) || null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,357 @@
//import { fabric } from "fabric-with-all";
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js";
/**
* 红绿图模式管理器
* 利用已有图层结构,不清除现有图层
*/
export class RedGreenModeManager {
constructor(options = {}) {
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.toolManager = options.toolManager;
this.canvasManager = options.canvasManager;
this.commandManager = options.commandManager;
// 红绿图模式状态
this.isInitialized = false;
this.normalLayerOpacity = 0.4; // 默认40%透明度 (0-1)
// 图片URL
this.clothingImageUrl = null;
this.redGreenImageUrl = null;
// 回调函数
this.onImageGenerated = null;
console.log("RedGreenModeManager 已创建");
}
/**
* 初始化红绿图模式
* @param {Object} options 配置选项
* @param {String} options.clothingImageUrl 衣服底图URL
* @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 = {}) {
try {
// 如果已经初始化,先清理状态
if (this.isInitialized) {
console.log("红绿图模式已初始化,重新初始化...");
}
// 更新配置
this.clothingImageUrl = options.clothingImageUrl;
this.redGreenImageUrl = options.redGreenImageUrl;
this.onImageGenerated = options.onImageGenerated;
// 设置透明度 (支持0-100的百分比值或0-1的小数值)
if (typeof options.normalLayerOpacity === "number") {
if (options.normalLayerOpacity > 1) {
// 如果大于1认为是百分比值(0-100)
this.normalLayerOpacity =
Math.max(0, Math.min(100, options.normalLayerOpacity)) / 100;
} else {
// 如果小于等于1认为是小数值(0-1)
this.normalLayerOpacity = Math.max(
0,
Math.min(1, options.normalLayerOpacity)
);
}
}
// 验证必需参数
if (!this.clothingImageUrl || !this.redGreenImageUrl) {
throw new Error("缺少必需的图片URL参数");
}
// 使用批量模式或传统模式
const useBatchMode = options.useBatchMode !== false; // 默认为true
let initCommand;
// 使用新的批量初始化命令,减少页面闪烁
initCommand = new BatchInitializeRedGreenModeCommand({
canvas: this.canvas,
layerManager: this.layerManager,
toolManager: this.toolManager,
clothingImageUrl: this.clothingImageUrl,
redGreenImageUrl: this.redGreenImageUrl,
normalLayerOpacity: this.normalLayerOpacity,
onImageGenerated: this.onImageGenerated,
});
initCommand.undoable = false; // 不可撤销
// 执行命令
if (this.commandManager) {
await this.commandManager.execute(initCommand);
} else {
await initCommand.execute();
}
this.registerRedGreenMouseUpEvent();
// 标记为已初始化
this.isInitialized = true;
// 启用图层管理器的红绿图模式
if (
this.layerManager &&
typeof this.layerManager.enableRedGreenMode === "function"
) {
this.layerManager.enableRedGreenMode();
}
// 重置工具管理器状态
// 默认红色笔刷
if (this.toolManager) {
this.toolManager.isRedGreenMode = true;
}
console.log("红绿图模式初始化成功", {
衣服底图: this.clothingImageUrl,
红绿图: this.redGreenImageUrl,
普通图层透明度: `${Math.round(this.normalLayerOpacity * 100)}%`,
批量模式: useBatchMode ? "已启用" : "已禁用",
画布背景: "白色",
});
return true;
} catch (error) {
console.error("红绿图模式初始化失败:", error);
this.isInitialized = false;
throw error;
}
}
// 注册鼠标抬起事件
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);
}
});
}
/**
* 检查是否已初始化
* @returns {boolean} 是否已初始化
*/
isReady() {
return this.isInitialized;
}
/**
* 更新普通图层透明度
* @param {Number} opacity 透明度值 (0-100的百分比值或0-1的小数值)
* @returns {boolean} 是否更新成功
*/
updateNormalLayerOpacity(opacity) {
if (!this.isInitialized) {
console.warn("红绿图模式未初始化,无法更新透明度");
return false;
}
try {
// 处理透明度值
let normalizedOpacity;
if (opacity > 1) {
// 如果大于1认为是百分比值(0-100)
normalizedOpacity = Math.max(0, Math.min(100, opacity)) / 100;
} else {
// 如果小于等于1认为是小数值(0-1)
normalizedOpacity = Math.max(0, Math.min(1, opacity));
}
// 创建透明度更新命令
const opacityCommand = new UpdateNormalLayerOpacityCommand({
canvas: this.canvas,
layerManager: this.layerManager,
opacity: normalizedOpacity,
});
// 执行命令
if (this.commandManager) {
this.commandManager.execute(opacityCommand);
} else {
opacityCommand.execute();
}
// 更新内部状态
this.normalLayerOpacity = normalizedOpacity;
console.log(
`普通图层透明度已更新为: ${Math.round(normalizedOpacity * 100)}%`
);
return true;
} catch (error) {
console.error("更新普通图层透明度失败:", error);
return false;
}
}
/**
* 获取当前普通图层透明度
* @param {boolean} asPercentage 是否返回百分比值(0-100),默认返回小数值(0-1)
* @returns {Number} 透明度值
*/
getNormalLayerOpacity(asPercentage = false) {
if (asPercentage) {
return Math.round(this.normalLayerOpacity * 100);
}
return this.normalLayerOpacity;
}
/**
* 重新加载图片
* @param {Object} options 配置选项
* @param {String} options.clothingImageUrl 新的衣服底图URL (可选)
* @param {String} options.redGreenImageUrl 新的红绿图URL (可选)
* @returns {Promise<boolean>} 是否重新加载成功
*/
async reloadImages(options = {}) {
if (!this.isInitialized) {
console.warn("红绿图模式未初始化,无法重新加载图片");
return false;
}
try {
// 更新图片URL
if (options.clothingImageUrl) {
this.clothingImageUrl = options.clothingImageUrl;
}
if (options.redGreenImageUrl) {
this.redGreenImageUrl = options.redGreenImageUrl;
}
// 重新初始化
await this.initialize({
clothingImageUrl: this.clothingImageUrl,
redGreenImageUrl: this.redGreenImageUrl,
normalLayerOpacity: this.normalLayerOpacity,
onImageGenerated: this.onImageGenerated,
});
console.log("图片重新加载成功");
return true;
} catch (error) {
console.error("重新加载图片失败:", error);
return false;
}
}
/**
* 获取当前状态信息
* @returns {Object} 状态信息
*/
getStatus() {
return {
isInitialized: this.isInitialized,
clothingImageUrl: this.clothingImageUrl,
redGreenImageUrl: this.redGreenImageUrl,
normalLayerOpacity: this.normalLayerOpacity,
normalLayerOpacityPercentage: Math.round(this.normalLayerOpacity * 100),
};
}
/**
* 获取图层信息
* @returns {Object} 图层信息
*/
getLayerInfo() {
if (!this.layerManager || !this.layerManager.layers) {
return null;
}
const layers = this.layerManager.layers.value || [];
const backgroundLayer = layers.find((layer) => layer.isBackground);
const fixedLayer = layers.find((layer) => layer.isFixed);
const normalLayers = layers.filter(
(layer) => !layer.isBackground && !layer.isFixed
);
return {
backgroundLayer:
backgroundLayer &&
Object.assign(
{
hasObject: !!backgroundLayer.fabricObject,
},
backgroundLayer
),
fixedLayer:
fixedLayer &&
Object.assign(
{
hasObject: !!fixedLayer.fabricObject,
},
fixedLayer
),
normalLayers: normalLayers.map((layer) => ({
id: layer.id,
name: layer.name,
visible: layer.visible,
opacity: layer.opacity,
objectCount: layer.fabricObjects ? layer.fabricObjects.length : 0,
})),
};
}
/**
* 清理红绿图模式
* 注意:这不会删除图层,只是清理红绿图模式的特定内容
*/
cleanup() {
try {
// 禁用图层管理器的红绿图模式
if (
this.layerManager &&
typeof this.layerManager.disableRedGreenMode === "function"
) {
this.layerManager.disableRedGreenMode();
}
// 重置工具管理器
if (this.toolManager && this.toolManager.isRedGreenMode) {
this.toolManager.isRedGreenMode = false;
}
// 重置状态
this.isInitialized = false;
this.clothingImageUrl = null;
this.redGreenImageUrl = null;
this.onImageGenerated = null;
console.log("红绿图模式已清理");
} catch (error) {
console.error("清理红绿图模式失败:", error);
}
}
/**
* 销毁管理器
*/
dispose() {
this.cleanup();
// 清除引用
this.canvas = null;
this.layerManager = null;
this.toolManager = null;
this.commandManager = null;
console.log("RedGreenModeManager 已销毁");
}
}

View File

@@ -0,0 +1,370 @@
/**
* 缩略图管理器 - 负责生成和缓存图层和元素的预览缩略图
*/
export class ThumbnailManager {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.layers = options.layers || []; // 图层管理器
this.layerThumbSize = options.layerThumbSize || { width: 48, height: 48 };
this.elementThumbSize = options.elementThumbSize || {
width: 36,
height: 36,
};
this.layerThumbnails = new Map(); // 图层缩略图缓存
this.elementThumbnails = new Map(); // 元素缩略图缓存
this.defaultThumbnail =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; // 1x1 透明图
}
/**
* 生成图层缩略图
* @param {Object} layer 图层对象ID
*/
generateLayerThumbnail(layerId) {
// const layer = this?.layers.value?.find((layer) => layer.id === layerId);
// if (!layer) return;
// // 延迟执行避免阻塞UI
// requestAnimationFrame(() => {
// this._generateLayerThumbnailNow(layer);
// });
}
/**
* 立即生成图层缩略图
* @param {Object} layer 图层对象
* @private
*/
_generateLayerThumbnailNow(layer) {
if (
!layer ||
!this.canvas ||
!layer.fabricObjects ||
!layer.fabricObjects?.length
)
return;
try {
let thumbnail = null;
if (!layer.children?.length) {
// 如果是元素图层,直接生成元素缩略图
thumbnail = this._generateThumbnailFromObjects(
layer.fabricObjects,
this.layerThumbSize.width,
this.layerThumbSize.height
);
} else if (layer.type === "group" || layer.children?.length) {
const fabricObjects = layer.children.reduce((pre, next) => {
if (next.fabricObjects.length) {
pre.push(...next.fabricObjects);
}
return pre;
}, []);
// 如果是分组图层,合并所有子对象的缩略图
thumbnail = this._generateThumbnailFromObjects(
fabricObjects,
this.layerThumbSize.width,
this.layerThumbSize.height
);
}
// 保存到缩略图缓存
if (thumbnail) {
this.layerThumbnails.set(layer.id, thumbnail);
} else {
// 如果无法生成缩略图,使用默认缩略图
this.layerThumbnails.set(layer.id, this.defaultThumbnail);
}
} catch (error) {
console.error("生成图层缩略图出错:", error);
}
}
/**
* 生成元素缩略图
* @param {Object} element 元素对象
* @param {Object} fabricObject fabric对象
*/
generateElementThumbnail(element, fabricObject) {
if (!element || !element.id || !fabricObject) return;
// 延迟执行避免阻塞UI
requestAnimationFrame(() => {
try {
const thumbnail = this._generateThumbnailFromObject(
fabricObject,
this.elementThumbSize.width,
this.elementThumbSize.height
);
if (thumbnail) {
this.elementThumbnails.set(element.id, thumbnail);
}
} catch (error) {
console.error("生成元素缩略图出错:", error);
}
});
}
/**
* 从fabric对象生成缩略图
* @param {Object} obj fabric对象
* @param {Number} width 缩略图宽度
* @param {Number} height 缩略图高度
* @returns {String} 缩略图数据URL
* @private
*/
_generateThumbnailFromObject(obj, width, height) {
if (!obj || !this.canvas) return null;
// 保存对象状态
const originalState = {
active: obj.active,
visible: obj.visible,
left: obj.left,
top: obj.top,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
opacity: obj.opacity,
};
// 临时修改对象状态
obj.set({
active: false,
visible: true,
opacity: 1,
});
// 创建临时画布
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext("2d");
// 获取对象边界
const bounds = obj.getBoundingRect();
// 绘制缩略图
try {
// 清空画布
tempCtx.clearRect(0, 0, width, height);
// 计算缩放比例
const scaleFactorX = width / bounds.width;
const scaleFactorY = height / bounds.height;
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
// 居中绘制
const centerX = width / 2;
const centerY = height / 2;
tempCtx.save();
tempCtx.translate(centerX, centerY);
tempCtx.scale(scaleFactor, scaleFactor);
tempCtx.translate(
-bounds.left - bounds.width / 2,
-bounds.top - bounds.height / 2
);
// 绘制对象
obj.render(tempCtx);
tempCtx.restore();
// 转换为数据URL
const dataUrl = tempCanvas.toDataURL("image/png");
// 恢复对象状态
obj.set(originalState);
return dataUrl;
} catch (error) {
console.error("绘制对象缩略图出错:", error);
// 恢复对象状态
obj.set(originalState);
return null;
}
}
/**
* 从多个fabric对象生成组合缩略图
* @param {Array} objects fabric对象数组
* @param {Number} width 缩略图宽度
* @param {Number} height 缩略图高度
* @returns {String} 缩略图数据URL
* @private
*/
_generateThumbnailFromObjects(objects, width, height) {
if (!objects || !objects.length || !this.canvas) return null;
// 创建临时画布
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext("2d");
// 计算所有对象的总边界
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
objects.forEach((obj) => {
if (!obj.visible) return;
const bounds = obj.getBoundingRect();
minX = Math.min(minX, bounds.left);
minY = Math.min(minY, bounds.top);
maxX = Math.max(maxX, bounds.left + bounds.width);
maxY = Math.max(maxY, bounds.top + bounds.height);
});
const groupWidth = maxX - minX;
const groupHeight = maxY - minY;
// 如果没有有效对象返回null
if (groupWidth <= 0 || groupHeight <= 0) return null;
// 保存对象状态
const originalStates = objects.map((obj) => ({
obj,
state: {
active: obj.active,
visible: obj.visible,
opacity: obj.opacity,
},
}));
// 临时修改对象状态
originalStates.forEach((item) => {
item.obj.set({
active: false,
visible: true,
opacity: 1,
});
});
// 绘制缩略图
try {
// 清空画布
tempCtx.clearRect(0, 0, width, height);
// 计算缩放比例
const scaleFactorX = width / groupWidth;
const scaleFactorY = height / groupHeight;
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
// 居中绘制
const centerX = width / 2;
const centerY = height / 2;
tempCtx.save();
tempCtx.translate(centerX, centerY);
tempCtx.scale(scaleFactor, scaleFactor);
tempCtx.translate(-(minX + groupWidth / 2), -(minY + groupHeight / 2));
// 按顺序绘制所有对象
objects.forEach((obj) => {
if (obj.visible) {
obj.render(tempCtx);
}
});
tempCtx.restore();
// 转换为数据URL
const dataUrl = tempCanvas.toDataURL("image/png");
// 恢复对象状态
originalStates.forEach((item) => {
item.obj.set(item.state);
});
return dataUrl;
} catch (error) {
console.error("绘制组合缩略图出错:", error);
// 恢复对象状态
originalStates.forEach((item) => {
item.obj.set(item.state);
});
return null;
}
}
/**
* 批量生成图层缩略图
* @param {Array} layers 图层数组
*/
generateAllLayerThumbnails(layers) {
if (!layers || !Array.isArray(layers)) return;
// 使用requestAnimationFrame批量生成避免阻塞主线程
requestAnimationFrame(() => {
layers.forEach((layer) => {
if (layer && layer.id) {
this._generateLayerThumbnailNow(layer);
}
});
});
}
/**
* 获取图层缩略图
* @param {String} layerId 图层ID
* @returns {String|null} 缩略图URL或null
*/
getLayerThumbnail(layerId) {
if (!layerId) return null;
return this.layerThumbnails.get(layerId) || null;
}
/**
* 获取元素缩略图
* @param {String} elementId 元素ID
* @returns {String|null} 缩略图URL或null
*/
getElementThumbnail(elementId) {
if (!elementId) return null;
return this.elementThumbnails.get(elementId) || null;
}
/**
* 清除图层缩略图
* @param {String} layerId 图层ID
*/
clearLayerThumbnail(layerId) {
if (layerId && this.layerThumbnails.has(layerId)) {
this.layerThumbnails.delete(layerId);
}
}
/**
* 清除元素缩略图
* @param {String} elementId 元素ID
*/
clearElementThumbnail(elementId) {
if (elementId && this.elementThumbnails.has(elementId)) {
this.elementThumbnails.delete(elementId);
}
}
/**
* 清除所有缩略图
*/
clearAllThumbnails() {
this.layerThumbnails.clear();
this.elementThumbnails.clear();
}
/**
* 释放资源
*/
dispose() {
this.clearAllThumbnails();
this.canvas = null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,856 @@
import { gsap } from "gsap";
/**
* 画布动画管理器
* 负责处理画布平移、缩放等动画效果
*/
export class AnimationManager {
/**
* 创建动画管理器
* @param {fabric.Canvas} canvas fabric.js画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
this.canvas = canvas;
this.currentZoom = options.currentZoom || { value: 100 };
// 动画相关属性
this._zoomAnimation = null;
this._panAnimation = null;
this._lastWheelTime = 0;
this._lastWheelProcessTime = 0; // 上次处理wheel事件的时间
this._wheelEvents = [];
// 检测设备类型Mac设备使用更短的节流时间确保响应性
this._isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
this._wheelThrottleTime = this._isMac
? options.wheelThrottleTime || 8 // Mac设备使用更短的节流时间
: options.wheelThrottleTime || 30;
this._accumulatedWheelDelta = 0; // 累积滚轮增量
this._wheelAccumulationTimeout = null; // 滚轮累积超时
// Mac设备使用更短的累积时间窗口确保及时响应
this._wheelAccumulationTime = this._isMac ? 60 : 120; // 滚轮累积时间窗口(毫秒)
// 添加新的状态跟踪变量
this._wasPanning = false; // 是否有平移动画正在进行
this._wasZooming = false; // 是否有缩放动画正在进行
this._combinedAnimation = null; // 组合动画引用
// Mac特有的动画优化变量 - 使用最小防抖机制
if (this._isMac) {
this._lastMacAnimationTime = 0; // 上次Mac动画时间
this._macAnimationCooldown = 2; // 最小的动画冷却时间,确保最大响应性
}
// 初始化GSAP默认配置
gsap.defaults({
ease: options.defaultEase || (this._isMac ? "power2.out" : "power2.out"), // Mac使用简单高效的缓动
duration: options.defaultDuration || (this._isMac ? 0.3 : 0.3), // Mac使用标准持续时间
overwrite: "auto", // 自动覆盖同一对象上的动画
});
}
/**
* 使用 GSAP 实现平滑缩放动画
* @param {Object} point 缩放中心点 {x, y}
* @param {Number} targetZoom 目标缩放值
* @param {Object} options 动画选项
*/
animateZoom(point, targetZoom, options = {}) {
if (!this.canvas) return;
// 限制缩放范围
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20);
// 当前缩放值
const currentZoom = this.canvas.getZoom();
// 如果变化太小,直接应用缩放
if (Math.abs(targetZoom - currentZoom) < 0.01) {
this._applyZoom(point, targetZoom);
return;
}
// 停止任何进行中的缩放动画
if (this._zoomAnimation) {
// 不是直接 kill而是获取当前进度值作为新的起点
const currentProgress = this._zoomAnimation.progress();
const currentZoomValue = this._zoomAnimation.targets()[0].value;
this._zoomAnimation.kill();
this._zoomAnimation = null;
// 从当前过渡中的值开始新动画,而不是从最初的值
const zoomObj = { value: currentZoomValue };
const currentVpt = [...this.canvas.viewportTransform];
// 计算过渡动画持续时间 - 根据当前值到目标值的距离比例
const progressRatio =
Math.abs(targetZoom - currentZoomValue) /
Math.abs(targetZoom - currentZoom);
const duration = options.duration || 0.3 * progressRatio;
// 计算缩放后目标位置需要的修正,保持缩放点不变
const animOptions = {
value: targetZoom,
duration: duration,
ease: options.ease || "power2.out",
onUpdate: () => {
// 更新缩放值显示
this.currentZoom.value = Math.round(zoomObj.value * 100);
// 计算过渡中的变换矩阵
const zoom = zoomObj.value;
const scale = zoom / currentZoomValue;
const currentScaleFactor = scale;
// 应用变换
const vpt = this.canvas.viewportTransform;
vpt[0] = currentVpt[0] * scale;
vpt[3] = currentVpt[3] * scale;
// 应用平移修正以保持缩放点
const adjustX = (1 - currentScaleFactor) * point.x;
const adjustY = (1 - currentScaleFactor) * point.y;
vpt[4] = currentVpt[4] * scale + adjustX;
vpt[5] = currentVpt[5] * scale + adjustY;
this.canvas.renderAll();
},
onComplete: () => {
this._zoomAnimation = null;
// 确保最终状态准确
this._applyZoom(point, targetZoom, true);
},
};
// 启动 GSAP 动画
this._zoomAnimation = gsap.to(zoomObj, animOptions);
return;
}
// 如果没有正在进行的动画,创建新的缩放动画
const zoomObj = { value: currentZoom };
const currentVpt = [...this.canvas.viewportTransform];
// 计算缩放后目标位置需要的修正,保持缩放点不变
const scaleFactor = targetZoom / currentZoom;
const invertedScaleFactor = 1 / scaleFactor;
// 这个数学公式确保缩放点在屏幕上的位置保持不变
const dx = point.x - point.x * invertedScaleFactor;
const dy = point.y - point.y * invertedScaleFactor;
// 创建动画配置
const animOptions = {
value: targetZoom,
duration: options.duration || 0.3,
ease: options.ease || (this._isMac ? "expo.out" : "power2.out"), // Mac使用更平滑的缓动
onUpdate: () => {
// 更新缩放值显示
this.currentZoom.value = Math.round(zoomObj.value * 100);
// 计算过渡中的变换矩阵
const zoom = zoomObj.value;
const scale = zoom / currentZoom;
const currentScaleFactor = scale;
// 应用变换
const vpt = this.canvas.viewportTransform;
vpt[0] = currentVpt[0] * scale;
vpt[3] = currentVpt[3] * scale;
// 应用平移修正以保持缩放点
const adjustX = (1 - currentScaleFactor) * point.x;
const adjustY = (1 - currentScaleFactor) * point.y;
vpt[4] = currentVpt[4] * scale + adjustX;
vpt[5] = currentVpt[5] * scale + adjustY;
this.canvas.renderAll();
},
onComplete: () => {
this._zoomAnimation = null;
// 确保最终状态准确
this._applyZoom(point, targetZoom, true);
},
};
// 启动 GSAP 动画
this._zoomAnimation = gsap.to(zoomObj, animOptions);
}
/**
* 应用缩放(内部使用)
* @private
*/
_applyZoom(point, zoom, skipUpdate = false) {
if (!skipUpdate) {
this.currentZoom.value = Math.round(zoom * 100);
}
this.canvas.zoomToPoint(point, zoom);
}
/**
* 使用 GSAP 实现平滑平移动画
* @param {Object} targetPosition 目标位置 {x, y}
* @param {Object} options 动画选项
*/
animatePan(targetPosition, options = {}) {
if (!this.canvas) return;
// 停止任何进行中的平移动画
if (this._panAnimation) {
this._panAnimation.kill();
}
const currentVpt = [...this.canvas.viewportTransform];
const position = {
x: -currentVpt[4],
y: -currentVpt[5],
};
// 计算平移距离
const dx = targetPosition.x - position.x;
const dy = targetPosition.y - position.y;
// 如果距离太小,直接应用平移
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
this._applyPan(targetPosition.x, targetPosition.y);
return;
}
// 创建动画配置
const animOptions = {
x: targetPosition.x,
y: targetPosition.y,
duration: options.duration || 0.3,
ease: options.ease || (this._isMac ? "circ.out" : "power2.out"), // Mac使用更柔和的缓动
onUpdate: () => {
this._applyPan(position.x, position.y);
},
onComplete: () => {
this._panAnimation = null;
// 确保最终位置准确
this._applyPan(targetPosition.x, targetPosition.y);
},
};
// 启动 GSAP 动画
this._panAnimation = gsap.to(position, animOptions);
}
/**
* 应用平移(内部使用)
* @private
*/
_applyPan(x, y) {
if (!this.canvas) return;
const vpt = this.canvas.viewportTransform;
vpt[4] = -x;
vpt[5] = -y;
this.canvas.renderAll();
}
/**
* 使用动画平移到指定元素
* @param {Object} elementId 元素ID
*/
panToElement(elementId) {
if (!this.canvas) return;
const obj = this.canvas.getObjects().find((obj) => obj.id === elementId);
if (!obj) return;
const zoom = this.canvas.getZoom();
const center = obj.getCenterPoint();
// 计算目标中心位置
const targetX = center.x * zoom - this.canvas.width / 2;
const targetY = center.y * zoom - this.canvas.height / 2;
// 动画平移
this.animatePan(
{ x: targetX, y: targetY },
{
duration: 0.6,
ease: this._isMac ? "back.out(0.3)" : "power3.out", // Mac使用轻微回弹效果
}
);
}
/**
* 重置缩放(带平滑动画)
* @param {Boolean} animated 是否使用动画
*/
async resetZoom(animated = true) {
return new Promise((resolve) => {
if (animated) {
// 停止任何进行中的动画
if (this._zoomAnimation) {
this._zoomAnimation.kill();
}
if (this._panAnimation) {
this._panAnimation.kill();
}
const center = {
x: this.canvas.width / 2,
y: this.canvas.height / 2,
};
// 获取当前变换矩阵
const currentVpt = [...this.canvas.viewportTransform];
const currentZoom = this.canvas.getZoom();
// 创建一个对象来动画整个视图变换
const viewTransform = {
zoom: currentZoom,
panX: currentVpt[4],
panY: currentVpt[5],
};
// 使用GSAP同时动画缩放和平移
gsap.to(viewTransform, {
zoom: 1,
panX: 0,
panY: 0,
duration: 0.5,
ease: this._isMac ? "back.out(0.2)" : "power3.out", // Mac使用轻微回弹效果
onUpdate: () => {
// 更新缩放显示值
this.currentZoom.value = Math.round(viewTransform.zoom * 100);
// 应用新的变换
const vpt = this.canvas.viewportTransform;
vpt[0] = viewTransform.zoom;
vpt[3] = viewTransform.zoom;
vpt[4] = viewTransform.panX;
vpt[5] = viewTransform.panY;
this.canvas.renderAll();
},
onComplete: () => {
// 确保最终状态准确
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
this.currentZoom.value = 100;
this._zoomAnimation = null;
this._panAnimation = null;
resolve();
},
});
} else {
this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
this.currentZoom.value = 100;
resolve();
}
});
}
/**
* 处理鼠标滚轮缩放
* @param {Object} opt 事件对象
*/
handleMouseWheel(opt) {
const now = Date.now();
let delta = opt.e.deltaY;
// 记录事件用于计算速度和惯性
this._wheelEvents.push({
delta: delta,
point: { x: opt.e.offsetX, y: opt.e.offsetY },
time: now,
hasPanAnimation: this._wasPanning,
hasZoomAnimation: this._wasZooming,
});
// 保留最近的事件记录
if (this._wheelEvents.length > 10) {
this._wheelEvents.shift();
}
// 检查是否是第一个事件或者距离上次处理已经过了足够时间
const isFirstEvent = !this._wheelAccumulationTimeout;
const timeSinceLastProcess = now - (this._lastWheelProcessTime || 0);
if (isFirstEvent || timeSinceLastProcess > this._wheelAccumulationTime) {
// 立即处理第一个事件或长时间没有处理的事件,确保响应性
this._processAccumulatedWheel(opt);
this._lastWheelProcessTime = now;
// 清理之前的累积
this._accumulatedWheelDelta = 0;
// 如果有pending的timeout清除它
if (this._wheelAccumulationTimeout) {
clearTimeout(this._wheelAccumulationTimeout);
this._wheelAccumulationTimeout = null;
}
} else {
// 累积后续事件
this._accumulatedWheelDelta += delta;
// 如果正在累积中,清除之前的定时器
if (this._wheelAccumulationTimeout) {
clearTimeout(this._wheelAccumulationTimeout);
}
// 设置新的定时器,处理累积的事件
this._wheelAccumulationTimeout = setTimeout(() => {
this._processAccumulatedWheel(opt);
this._lastWheelProcessTime = Date.now();
// 清理
this._accumulatedWheelDelta = 0;
this._wheelAccumulationTimeout = null;
}, this._wheelThrottleTime);
}
opt.e.preventDefault();
opt.e.stopPropagation();
}
/**
* 处理累积的滚轮事件并应用缩放
* @private
* @param {Object} lastOpt 最后一个滚轮事件
*/
_processAccumulatedWheel(lastOpt) {
if (!this._wheelEvents.length) return;
const now = Date.now();
// Mac设备的轻量防抖检查 - 进一步减少冷却时间,确保响应性
if (
this._isMac &&
now - this._lastMacAnimationTime < this._macAnimationCooldown
) {
// 如果距离上次动画时间太短,只延迟很短时间,不阻塞太久
if (this._wheelAccumulationTimeout) {
clearTimeout(this._wheelAccumulationTimeout);
}
this._wheelAccumulationTimeout = setTimeout(() => {
this._processAccumulatedWheel(lastOpt);
}, Math.min(this._macAnimationCooldown, 3)); // 最多延迟3ms
return;
}
const currentZoom = this.canvas.getZoom();
// 分析滚轮事件模式,计算平均增量、速度和加速度
let sumDelta = 0;
let count = 0;
let earliestTime = now;
let latestTime = 0;
let point = {
x: lastOpt.e.offsetX,
y: lastOpt.e.offsetY,
};
// 判断是否在事件收集期间有平移或缩放动画
let hadPanAnimation = false;
let hadZoomAnimation = false;
// 计算平均增量和速度
this._wheelEvents.forEach((event) => {
sumDelta += event.delta;
count++;
earliestTime = Math.min(earliestTime, event.time);
latestTime = Math.max(latestTime, event.time);
// 使用最后记录的点作为缩放中心
if (event.time > latestTime) {
point = event.point;
}
// 检查是否有动画状态
if (event.hasPanAnimation) hadPanAnimation = true;
if (event.hasZoomAnimation) hadZoomAnimation = true;
});
// 计算平均增量
const avgDelta = sumDelta / count;
// 计算滚动速度 - 基于事件频率和时间跨度
const timeSpan = latestTime - earliestTime + 1; // 避免除以零
const eventsPerSecond = (count / timeSpan) * 1000;
// 速度系数: 速度越快,缩放越敏感
let speedFactor = Math.min(3, Math.max(0.5, eventsPerSecond / 10));
// 计算缩放因子,应用速度系数
// 针对Mac设备优化Mac触控板的deltaY值通常较小需要适度增加敏感度
let zoomFactorBase = 0.999;
if (this._isMac) {
// Mac设备的触控板需要适度的敏感度避免过度反应
zoomFactorBase = 0.995; // 适度降低基数,增加缩放敏感度
// 检测是否为触控板滚动(小幅度、高频次的特征)
const avgAbsDelta = Math.abs(avgDelta);
if (avgAbsDelta < 50 && count > 2) {
// 触控板滚动,适度增加敏感度
speedFactor *= 1.6; // 适度增加敏感度倍数
zoomFactorBase = 0.993; // 进一步调整基数
}
}
const zoomFactor = zoomFactorBase ** (avgDelta * speedFactor);
let targetZoom = currentZoom * zoomFactor;
// 限制缩放范围
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20);
// 根据滚动速度和缩放幅度计算动画持续时间
// 速度快时缩短动画时间,缩放幅度大时延长动画时间
const zoomRatio = Math.abs(targetZoom - currentZoom) / currentZoom;
let duration;
if (this._isMac) {
// Mac设备使用平衡的动画时间控制
if (speedFactor > 2) {
// 快速操作:快速但平滑
duration = Math.min(
0.18,
Math.max(0.08, (zoomRatio * 0.3) / Math.sqrt(speedFactor))
);
} else if (speedFactor > 1.2) {
// 中等速度:标准响应
duration = Math.min(
0.25,
Math.max(0.1, (zoomRatio * 0.4) / Math.sqrt(speedFactor))
);
} else {
// 慢速精确操作:确保平滑
duration = Math.min(
0.3,
Math.max(0.12, (zoomRatio * 0.5) / Math.sqrt(speedFactor))
);
}
} else {
duration = Math.min(
0.5,
Math.max(0.15, (zoomRatio * 0.8) / Math.sqrt(speedFactor))
);
}
// 根据滚动速度选择不同的缓动效果
let easeType;
if (this._isMac) {
// Mac设备使用更简单、性能更好的缓动函数
// 避免复杂的指数和回弹效果,减少计算量
if (speedFactor > 2) {
// 快速滚动:使用简单的缓出效果
easeType = "power2.out";
} else if (speedFactor > 1.2) {
// 中等速度:使用平滑的缓出
easeType = "power1.out";
} else {
// 慢速精确操作:使用线性过渡
easeType = "power1.out";
}
} else {
// 非Mac设备保持原有的缓动
easeType = speedFactor > 1.5 ? "power1.out" : "power2.out";
}
// 根据是否有其他动画正在进行,选择合适的动画方法
if (hadPanAnimation || this._wasPanning) {
// 如果有平移动画,使用组合动画以保持平滑过渡
this.animateCombinedTransform(point, targetZoom, {
duration: duration,
ease: easeType,
});
} else {
// 如果没有其他动画,使用标准缩放动画
this.animateZoom(point, targetZoom, {
duration: duration,
ease: easeType,
});
}
// 更新Mac设备的最后动画时间
if (this._isMac) {
this._lastMacAnimationTime = now;
}
// 清理事件记录
this._wheelEvents = [];
}
/**
* 计算并应用拖动结束后的惯性效果
* @param {Array} positions 拖动过程中记录的位置数组
* @param {Boolean} isTouchDevice 是否是触摸设备
*/
applyInertiaEffect(positions, isTouchDevice) {
if (!positions || positions.length <= 1) return;
const lastPos = positions[positions.length - 1];
const firstPos = positions[0];
const deltaTime = lastPos.time - firstPos.time;
if (deltaTime <= 0) return;
// 计算速度向量 (像素/毫秒)
const velocityX = (lastPos.x - firstPos.x) / deltaTime;
const velocityY = (lastPos.y - firstPos.y) / deltaTime;
const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
// 仅当速度足够大时应用惯性效果
if (speed > 0.2) {
// 计算惯性距离,基于速度和衰减因子
const decayFactor = 300; // 调整此值以改变惯性效果的强度
const inertiaDistanceX = velocityX * decayFactor;
const inertiaDistanceY = velocityY * decayFactor;
// 计算目标位置
const vpt = this.canvas.viewportTransform;
const currentPos = {
x: -vpt[4],
y: -vpt[5],
};
const targetPos = {
x: currentPos.x - inertiaDistanceX,
y: currentPos.y - inertiaDistanceY,
};
// 应用惯性动画,速度越大,动画时间越长
const animationDuration = Math.min(1.2, Math.max(0.6, speed * 2));
// 应用惯性动画
this.animatePan(targetPos, {
duration: animationDuration, // 动态计算持续时间
ease: this._isMac ? "quart.out" : "power3.out", // Mac使用更自然的减速效果
});
}
}
/**
* 平滑过渡停止所有动画
* 用于在需要中断当前动画时提供更自然的过渡,而不是硬性中断
* @param {Object} options 过渡选项
*/
smoothStopAnimations(options = {}) {
const duration = options.duration || 0.15; // 默认短暂过渡时间
// 处理缩放动画
if (this._zoomAnimation) {
const zoomObj = this._zoomAnimation.targets()[0];
const currentZoom = this.canvas.getZoom();
// 创建短暂的过渡动画到当前值
gsap.to(zoomObj, {
value: currentZoom,
duration: duration,
ease: this._isMac ? "circ.out" : "power1.out", // Mac使用更平滑的缓动
onUpdate: () => {
this.currentZoom.value = Math.round(zoomObj.value * 100);
this.canvas.renderAll();
},
onComplete: () => {
if (this._zoomAnimation) {
this._zoomAnimation.kill();
this._zoomAnimation = null;
}
},
});
}
// 处理平移动画
if (this._panAnimation) {
const panObj = this._panAnimation.targets()[0];
const vpt = this.canvas.viewportTransform;
const currentPos = { x: -vpt[4], y: -vpt[5] };
// 创建短暂的过渡动画到当前位置
gsap.to(panObj, {
x: currentPos.x,
y: currentPos.y,
duration: duration,
ease: this._isMac ? "circ.out" : "power1.out", // Mac使用更平滑的缓动
onUpdate: () => {
this._applyPan(panObj.x, panObj.y);
},
onComplete: () => {
if (this._panAnimation) {
this._panAnimation.kill();
this._panAnimation = null;
}
},
});
}
}
/**
* 设置画布交互动画
* 为对象交互添加流畅的动画效果
*/
setupInteractionAnimations() {
if (!this.canvas) return;
// 启用对象旋转的流畅动画
this._setupRotationAnimation();
}
/**
* 设置旋转动画
* @private
*/
_setupRotationAnimation() {
if (!fabric) return;
// 保存原始旋转方法
const originalRotate = fabric.Object.prototype.rotate;
const isMac = this._isMac; // 保存Mac检测结果
// 覆盖旋转方法以添加动画
fabric.Object.prototype.rotate = function (angle) {
const currentAngle = this.angle || 0;
if (Math.abs(angle - currentAngle) > 0.1) {
gsap.to(this, {
angle: angle,
duration: 0.3,
ease: isMac ? "back.out(0.3)" : "power2.out", // Mac使用轻微回弹
onUpdate: () => {
this.canvas && this.canvas.renderAll();
},
});
return this;
}
// 如果角度差异很小,使用原始方法
return originalRotate.call(this, angle);
};
}
/**
* 处理滚轮缩放,同时兼容正在进行的平移动画
* @param {Object} point 缩放中心点
* @param {Number} targetZoom 目标缩放值
* @param {Object} options 动画选项
*/
animateCombinedTransform(point, targetZoom, options = {}) {
if (!this.canvas) return;
// 限制缩放范围
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20);
// 当前状态
const currentZoom = this.canvas.getZoom();
const currentVpt = [...this.canvas.viewportTransform];
const currentPos = { x: -currentVpt[4], y: -currentVpt[5] };
// 如果有正在进行的动画,先停止它们
if (this._combinedAnimation) {
this._combinedAnimation.kill();
this._combinedAnimation = null;
}
if (this._zoomAnimation) {
this._zoomAnimation.kill();
this._zoomAnimation = null;
}
if (this._panAnimation) {
this._panAnimation.kill();
this._panAnimation = null;
}
// 创建一个统一的变换对象来动画
const transform = {
zoom: currentZoom,
panX: currentVpt[4],
panY: currentVpt[5],
progress: 0, // 用于动画进度跟踪
};
// 获取平移目标位置(如果有的话)
let panTarget = { x: currentPos.x, y: currentPos.y };
if (this._wasPanning) {
// 如果之前有平移动画,尝试获取平移的目标位置
const vpt = this.canvas.viewportTransform;
panTarget = {
x: currentPos.x,
y: currentPos.y,
};
}
// 计算新的变换矩阵,同时考虑平移和缩放
const scaleFactor = targetZoom / currentZoom;
// 创建动画
this._combinedAnimation = gsap.to(transform, {
zoom: targetZoom,
progress: 1,
duration: options.duration || 0.3,
ease: options.ease || (this._isMac ? "expo.out" : "power2.out"), // Mac使用更平滑的缓动
onUpdate: () => {
// 计算当前动画阶段的混合变换
const currentScaleFactor = transform.zoom / currentZoom;
// 应用缩放
const vpt = this.canvas.viewportTransform;
vpt[0] = currentVpt[0] * (transform.zoom / currentZoom);
vpt[3] = currentVpt[3] * (transform.zoom / currentZoom);
// 平滑混合平移和缩放调整
const adjustX = (1 - currentScaleFactor) * point.x;
const adjustY = (1 - currentScaleFactor) * point.y;
// 如果存在平移目标,进行插值
if (this._wasPanning) {
const t = transform.progress;
const interpolatedX = currentPos.x * (1 - t) + panTarget.x * t;
const interpolatedY = currentPos.y * (1 - t) + panTarget.y * t;
// 结合缩放和平移的调整
vpt[4] = -interpolatedX * currentScaleFactor + adjustX;
vpt[5] = -interpolatedY * currentScaleFactor + adjustY;
} else {
// 只有缩放,保持中心点
vpt[4] = currentVpt[4] * currentScaleFactor + adjustX;
vpt[5] = currentVpt[5] * currentScaleFactor + adjustY;
}
// 更新缩放值显示
this.currentZoom.value = Math.round(transform.zoom * 100);
this.canvas.renderAll();
},
onComplete: () => {
this._combinedAnimation = null;
this._zoomAnimation = null;
this._panAnimation = null;
this._wasPanning = false;
this._wasZooming = false;
// 确保最终状态准确
this._applyZoom(point, targetZoom, true);
},
});
}
/**
* 清理资源
*/
dispose() {
if (this._zoomAnimation) {
this._zoomAnimation.kill();
this._zoomAnimation = null;
}
if (this._panAnimation) {
this._panAnimation.kill();
this._panAnimation = null;
}
this._wheelEvents = [];
this.canvas = null;
this.currentZoom = null;
}
}

View File

@@ -0,0 +1,234 @@
/**
* 笔刷基类
* 所有笔刷类型应继承此基类并实现必要的方法
*/
export class BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 笔刷配置选项
*/
constructor(canvas, options = {}) {
this.canvas = canvas;
this.options = options;
// 基本属性
this.id = options.id || this.constructor.name;
this.name = options.name || "未命名笔刷";
this.description = options.description || "";
this.icon = options.icon || null;
this.category = options.category || "默认";
// 笔刷实例
this.brush = null;
}
/**
* 创建笔刷实例(必须由子类实现)
* @returns {Object} fabric笔刷实例
*/
create() {
throw new Error("必须由子类实现create方法");
}
/**
* 配置笔刷(必须由子类实现)
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options) {
throw new Error("必须由子类实现configure方法");
}
/**
* 获取笔刷的元数据
* @returns {Object} 笔刷元数据
*/
getMetadata() {
return {
id: this.id,
name: this.name,
description: this.description,
icon: this.icon,
category: this.category,
};
}
/**
* 获取笔刷预览
* @returns {String|null} 预览图URL或null
*/
getPreview() {
return null;
}
/**
* 获取笔刷可配置属性
* 这个方法返回一个对象数组,每个对象描述一个可配置属性
* 每个属性对象包含:
* - id: 属性标识符
* - name: 属性显示名称
* - type: 属性类型(例如:'slider', 'color', 'checkbox', 'select'
* - defaultValue: 默认值
* - min/max/step: 对于slider类型的限制值
* - options: 对于select类型的选项
* - description: 属性描述
* - category: 属性分类
* - order: 显示顺序(越小越靠前)
* - visibleWhen: 函数或对象,定义何时显示该属性
* - dynamicOptions: 函数,返回动态的选项列表
* @returns {Array} 可配置属性描述数组
*/
getConfigurableProperties() {
// 返回基础属性,所有笔刷都有这些属性
return [
{
id: "size",
name: "笔刷大小",
type: "slider",
defaultValue: 5,
min: 0.5,
max: 100,
step: 0.5,
description: "笔刷的大小(像素)",
category: "基本",
order: 10,
},
{
id: "color",
name: "笔刷颜色",
type: "color",
defaultValue: "#000000",
description: "笔刷的颜色",
category: "基本",
order: 20,
},
{
id: "opacity",
name: "不透明度",
type: "slider",
defaultValue: 1,
min: 0.05,
max: 1,
step: 0.01,
description: "笔刷的不透明度",
category: "基本",
order: 30,
},
];
}
/**
* 合并特有属性与基本属性
* 子类应该调用此方法来合并自身特有属性与基类提供的基本属性
* @param {Array} specificProperties 特有属性数组
* @returns {Array} 合并后的属性数组
*/
mergeWithBaseProperties(specificProperties) {
const baseProperties = super.getConfigurableProperties();
// 过滤掉同名属性(子类优先)
const basePropsFiltered = baseProperties.filter(
(baseProp) =>
!specificProperties.some(
(specificProp) => specificProp.id === baseProp.id
)
);
return [...basePropsFiltered, ...specificProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
*/
updateProperty(propId, value) {
// 基础实现,可以被子类覆盖以处理特殊属性
if (propId === "size") {
if (this.brush) {
this.brush.width = value;
return true;
}
} else if (propId === "color") {
if (this.brush) {
this.brush.color = value;
return true;
}
} else if (propId === "opacity") {
if (this.brush) {
this.brush.opacity = value;
return true;
}
}
return false;
}
/**
* 检查属性是否可见
* @param {Object} property 属性对象
* @param {Object} currentValues 当前所有属性的值
* @returns {Boolean} 是否可见
*/
isPropertyVisible(property, currentValues) {
// 如果没有visibleWhen条件则始终显示
if (!property.visibleWhen) {
return true;
}
// 如果visibleWhen是函数则调用函数判断
if (typeof property.visibleWhen === "function") {
return property.visibleWhen(currentValues);
}
// 如果visibleWhen是对象检查条件是否满足
if (typeof property.visibleWhen === "object") {
for (const [key, value] of Object.entries(property.visibleWhen)) {
if (currentValues[key] !== value) {
return false;
}
}
return true;
}
return true;
}
/**
* 获取动态选项
* @param {Object} property 属性对象
* @param {Object} currentValues 当前所有属性的值
* @returns {Array} 选项数组
*/
getDynamicOptions(property, currentValues) {
if (
property.dynamicOptions &&
typeof property.dynamicOptions === "function"
) {
return property.dynamicOptions(currentValues);
}
return property.options || [];
}
/**
* 生命周期方法:笔刷被选中
*/
onSelected() {
// 可由子类覆盖
}
/**
* 生命周期方法:笔刷被取消选中
*/
onDeselected() {
// 可由子类覆盖
}
/**
* 销毁笔刷实例并清理资源
*/
destroy() {
this.brush = null;
}
}

View File

@@ -0,0 +1,201 @@
/**
* 笔刷注册表
* 用于注册、获取和管理所有笔刷
*/
export class BrushRegistry {
constructor() {
// 存储所有注册的笔刷类
this.brushes = new Map();
// 按类别组织的笔刷
this.brushesByCategory = new Map();
// 事件监听器
this.listeners = {
register: [],
unregister: [],
};
}
/**
* 注册一个笔刷
* @param {String} id 笔刷唯一标识
* @param {Class} brushClass 笔刷类需要继承BaseBrush
* @param {Object} metadata 笔刷元数据(可选)
* @returns {Boolean} 是否注册成功
*/
register(id, brushClass, metadata = {}) {
if (this.brushes.has(id)) {
console.warn(`笔刷 ${id} 已存在请使用其他ID`);
return false;
}
// 存储笔刷信息
const brushInfo = {
id,
class: brushClass,
metadata: {
...metadata,
id,
},
};
this.brushes.set(id, brushInfo);
// 添加到分类
const category = metadata.category || "默认";
if (!this.brushesByCategory.has(category)) {
this.brushesByCategory.set(category, []);
}
this.brushesByCategory.get(category).push(brushInfo);
// 触发事件
this._triggerEvent("register", brushInfo);
return true;
}
/**
* 取消注册笔刷
* @param {String} id 笔刷ID
* @returns {Boolean} 是否成功
*/
unregister(id) {
if (!this.brushes.has(id)) {
return false;
}
const brushInfo = this.brushes.get(id);
this.brushes.delete(id);
// 从分类中移除
const category = brushInfo.metadata.category || "默认";
if (this.brushesByCategory.has(category)) {
const brushes = this.brushesByCategory.get(category);
const index = brushes.findIndex((b) => b.id === id);
if (index !== -1) {
brushes.splice(index, 1);
}
// 如果分类为空,删除该分类
if (brushes.length === 0) {
this.brushesByCategory.delete(category);
}
}
// 触发事件
this._triggerEvent("unregister", brushInfo);
return true;
}
/**
* 获取笔刷信息
* @param {String} id 笔刷ID
* @returns {Object|null} 笔刷信息或null
*/
getBrush(id) {
return this.brushes.get(id) || null;
}
/**
* 获取所有笔刷
* @returns {Array} 笔刷信息数组
*/
getAllBrushes() {
return Array.from(this.brushes.values());
}
/**
* 获取指定分类的笔刷
* @param {String} category 分类名称
* @returns {Array} 笔刷信息数组
*/
getBrushesByCategory(category) {
return this.brushesByCategory.get(category) || [];
}
/**
* 获取所有分类
* @returns {Array} 分类名称数组
*/
getCategories() {
return Array.from(this.brushesByCategory.keys());
}
/**
* 创建一个笔刷实例
* @param {String} id 笔刷ID
* @param {Object} canvas 画布实例
* @param {Object} options 配置选项
* @returns {Object|null} 笔刷实例或null
*/
createBrushInstance(id, canvas, options = {}) {
const brushInfo = this.getBrush(id);
if (!brushInfo) {
console.error(`笔刷 ${id} 不存在`);
return null;
}
try {
// 创建笔刷实例
return new brushInfo.class(canvas, {
...options,
id: brushInfo.id,
...brushInfo.metadata,
});
} catch (error) {
console.error(`创建笔刷 ${id} 失败:`, error);
return null;
}
}
/**
* 添加事件监听器
* @param {String} event 事件名称 ('register'|'unregister')
* @param {Function} callback 回调函数
*/
addEventListener(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback);
}
}
/**
* 移除事件监听器
* @param {String} event 事件名称
* @param {Function} callback 回调函数
*/
removeEventListener(event, callback) {
if (this.listeners[event]) {
const index = this.listeners[event].indexOf(callback);
if (index !== -1) {
this.listeners[event].splice(index, 1);
}
}
}
/**
* 触发事件
* @param {String} event 事件名称
* @param {*} data 事件数据
* @private
*/
_triggerEvent(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach((callback) => {
try {
callback(data);
} catch (error) {
console.error(`执行 ${event} 事件监听器出错:`, error);
}
});
}
}
}
// 导出单例实例
export const brushRegistry = new BrushRegistry();
// 默认导出单例
export default brushRegistry;

View File

@@ -0,0 +1,586 @@
/**
* 材质预设管理器
* 负责管理所有材质预设,包括内置预设和用户自定义预设
*/
export class TexturePresetManager {
constructor() {
// 内置材质预设
this.builtInTextures = [];
// 用户自定义材质
this.customTextures = [];
// 材质分类
this.categories = new Map();
// 材质缓存
this.textureCache = new Map();
// 事件监听器
this.listeners = {
textureAdded: [],
textureRemoved: [],
textureUpdated: [],
};
// 初始化内置材质
this._initBuiltInTextures();
}
/**
* 初始化内置材质预设
* @private
*/
_initBuiltInTextures() {
// 基于项目中的texture文件夹内容创建预设
const textureList = [
// 基础纹理
{
id: "texture0",
name: "纸质纹理",
category: "基础纹理",
path: "/src/assets/texture/texture0.webp",
},
{
id: "texture1",
name: "粗糙表面",
category: "基础纹理",
path: "/src/assets/texture/texture1.webp",
},
{
id: "texture2",
name: "细腻纹理",
category: "基础纹理",
path: "/src/assets/texture/texture2.webp",
},
{
id: "texture3",
name: "颗粒质感",
category: "基础纹理",
path: "/src/assets/texture/texture3.webp",
},
{
id: "texture4",
name: "布料纹理",
category: "基础纹理",
path: "/src/assets/texture/texture4.webp",
},
{
id: "texture5",
name: "木质纹理",
category: "自然纹理",
path: "/src/assets/texture/texture5.webp",
},
{
id: "texture6",
name: "石材纹理",
category: "自然纹理",
path: "/src/assets/texture/texture6.webp",
},
{
id: "texture7",
name: "金属质感",
category: "金属纹理",
path: "/src/assets/texture/texture7.webp",
},
{
id: "texture8",
name: "皮革纹理",
category: "自然纹理",
path: "/src/assets/texture/texture8.webp",
},
{
id: "texture9",
name: "水彩纸质",
category: "艺术纹理",
path: "/src/assets/texture/texture9.webp",
},
{
id: "texture10",
name: "画布纹理",
category: "艺术纹理",
path: "/src/assets/texture/texture10.webp",
},
{
id: "texture11",
name: "沙砾质感",
category: "自然纹理",
path: "/src/assets/texture/texture11.webp",
},
{
id: "texture12",
name: "水波纹理",
category: "自然纹理",
path: "/src/assets/texture/texture12.webp",
},
{
id: "texture13",
name: "云朵纹理",
category: "自然纹理",
path: "/src/assets/texture/texture13.webp",
},
{
id: "texture14",
name: "火焰纹理",
category: "特效纹理",
path: "/src/assets/texture/texture14.webp",
},
{
id: "texture15",
name: "烟雾效果",
category: "特效纹理",
path: "/src/assets/texture/texture15.webp",
},
{
id: "texture16",
name: "星空纹理",
category: "特效纹理",
path: "/src/assets/texture/texture16.webp",
},
{
id: "texture17",
name: "大理石纹",
category: "石材纹理",
path: "/src/assets/texture/texture17.webp",
},
{
id: "texture18",
name: "花岗岩纹",
category: "石材纹理",
path: "/src/assets/texture/texture18.webp",
},
{
id: "texture19",
name: "竹纹理",
category: "自然纹理",
path: "/src/assets/texture/texture19.webp",
},
{
id: "texture20",
name: "抽象图案",
category: "艺术纹理",
path: "/src/assets/texture/texture20.webp",
},
];
// 添加内置材质
textureList.forEach((texture) => {
this.builtInTextures.push({
id: texture.id,
name: texture.name,
category: texture.category,
path: texture.path,
type: "builtin",
preview: texture.path, // 使用原图作为预览
description: `内置${texture.category} - ${texture.name}`,
tags: [texture.category.replace("纹理", ""), "内置"],
created: new Date().toISOString(),
// 默认属性
defaultSettings: {
scale: 1,
opacity: 1,
repeat: "repeat",
angle: 0,
},
});
// 添加到分类
if (!this.categories.has(texture.category)) {
this.categories.set(texture.category, []);
}
this.categories.get(texture.category).push(texture.id);
});
}
/**
* 获取所有材质(内置 + 自定义)
* @returns {Array} 材质数组
*/
getAllTextures() {
return [...this.builtInTextures, ...this.customTextures];
}
/**
* 根据ID获取材质
* @param {String} textureId 材质ID
* @returns {Object|null} 材质对象
*/
getTextureById(textureId) {
return (
this.getAllTextures().find((texture) => texture.id === textureId) || null
);
}
/**
* 根据分类获取材质
* @param {String} category 分类名称
* @returns {Array} 材质数组
*/
getTexturesByCategory(category) {
return this.getAllTextures().filter(
(texture) => texture.category === category
);
}
/**
* 获取所有分类
* @returns {Array} 分类名称数组
*/
getCategories() {
const categories = new Set();
this.getAllTextures().forEach((texture) => {
categories.add(texture.category);
});
return Array.from(categories);
}
/**
* 添加自定义材质
* @param {Object} textureData 材质数据
* @returns {String} 材质ID
*/
addCustomTexture(textureData) {
const textureId =
textureData.id ||
`custom_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const texture = {
id: textureId,
name: textureData.name || "自定义材质",
category: textureData.category || "自定义材质",
path: textureData.path || textureData.dataUrl,
type: "custom",
preview: textureData.preview || textureData.path || textureData.dataUrl,
description: textureData.description || "用户自定义材质",
tags: textureData.tags || ["自定义"],
created: new Date().toISOString(),
defaultSettings: {
scale: textureData.scale || 1,
opacity: textureData.opacity || 1,
repeat: textureData.repeat || "repeat",
angle: textureData.angle || 0,
...textureData.defaultSettings,
},
// 保存原始文件信息
file: textureData.file || null,
dataUrl: textureData.dataUrl || null,
};
this.customTextures.push(texture);
// 添加到分类
if (!this.categories.has(texture.category)) {
this.categories.set(texture.category, []);
}
this.categories.get(texture.category).push(textureId);
// 触发事件
this._triggerEvent("textureAdded", texture);
return textureId;
}
/**
* 删除自定义材质
* @param {String} textureId 材质ID
* @returns {Boolean} 是否删除成功
*/
removeCustomTexture(textureId) {
const index = this.customTextures.findIndex(
(texture) => texture.id === textureId
);
if (index === -1) {
return false;
}
const texture = this.customTextures[index];
// 只能删除自定义材质
if (texture.type !== "custom") {
console.warn("不能删除内置材质");
return false;
}
this.customTextures.splice(index, 1);
// 从分类中移除
if (this.categories.has(texture.category)) {
const categoryTextures = this.categories.get(texture.category);
const categoryIndex = categoryTextures.indexOf(textureId);
if (categoryIndex !== -1) {
categoryTextures.splice(categoryIndex, 1);
}
// 如果分类为空且不是内置分类,删除分类
if (categoryTextures.length === 0 && texture.category === "自定义材质") {
this.categories.delete(texture.category);
}
}
// 清除缓存
this.textureCache.delete(textureId);
// 触发事件
this._triggerEvent("textureRemoved", texture);
return true;
}
/**
* 更新材质信息
* @param {String} textureId 材质ID
* @param {Object} updates 更新数据
* @returns {Boolean} 是否更新成功
*/
updateTexture(textureId, updates) {
const texture = this.getTextureById(textureId);
if (!texture || texture.type === "builtin") {
return false;
}
// 更新材质属性
Object.assign(texture, updates);
// 触发事件
this._triggerEvent("textureUpdated", texture);
return true;
}
/**
* 获取材质预览URL
* @param {Object} texture 材质对象
* @returns {String} 预览URL
*/
getTexturePreviewUrl(texture) {
if (!texture) return null;
// 如果有预览图,使用预览图
if (texture.preview) {
return texture.preview;
}
// 否则使用原图
return texture.path || texture.dataUrl;
}
/**
* 加载材质图像
* @param {String} textureId 材质ID
* @returns {Promise<HTMLImageElement>} 图像对象
*/
loadTextureImage(textureId) {
return new Promise((resolve, reject) => {
// 检查缓存
if (this.textureCache.has(textureId)) {
resolve(this.textureCache.get(textureId));
return;
}
const texture = this.getTextureById(textureId);
if (!texture) {
reject(new Error(`材质 ${textureId} 不存在`));
return;
}
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
// 缓存图像
this.textureCache.set(textureId, img);
resolve(img);
};
img.onerror = () => {
reject(new Error(`材质 ${textureId} 加载失败`));
};
img.src = texture.path || texture.dataUrl;
});
}
/**
* 搜索材质
* @param {String} query 搜索关键词
* @returns {Array} 匹配的材质数组
*/
searchTextures(query) {
if (!query) return this.getAllTextures();
const searchTerm = query.toLowerCase();
return this.getAllTextures().filter((texture) => {
return (
texture.name.toLowerCase().includes(searchTerm) ||
texture.category.toLowerCase().includes(searchTerm) ||
texture.description.toLowerCase().includes(searchTerm) ||
texture.tags.some((tag) => tag.toLowerCase().includes(searchTerm))
);
});
}
/**
* 保存自定义材质到本地存储
*/
saveCustomTexturesToStorage() {
try {
const customTexturesData = this.customTextures.map((texture) => ({
...texture,
// 不保存file对象到localStorage
file: null,
}));
localStorage.setItem(
"canvasEditor_customTextures",
JSON.stringify(customTexturesData)
);
} catch (error) {
console.error("保存自定义材质失败:", error);
}
}
/**
* 从本地存储加载自定义材质
*/
loadCustomTexturesFromStorage() {
try {
const stored = localStorage.getItem("canvasEditor_customTextures");
if (stored) {
const customTexturesData = JSON.parse(stored);
this.customTextures = customTexturesData;
// 重建分类索引
this.customTextures.forEach((texture) => {
if (!this.categories.has(texture.category)) {
this.categories.set(texture.category, []);
}
if (!this.categories.get(texture.category).includes(texture.id)) {
this.categories.get(texture.category).push(texture.id);
}
});
}
} catch (error) {
console.error("加载自定义材质失败:", error);
this.customTextures = [];
}
}
/**
* 创建材质预设
* @param {String} name 预设名称
* @param {Object} settings 材质设置
* @returns {String} 预设ID
*/
createTexturePreset(name, settings) {
const presetId = `preset_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
const preset = {
id: presetId,
name: name,
type: "preset",
category: "材质预设",
created: new Date().toISOString(),
settings: {
textureId: settings.textureId,
scale: settings.scale || 1,
opacity: settings.opacity || 1,
repeat: settings.repeat || "repeat",
angle: settings.angle || 0,
brushSize: settings.brushSize || 5,
brushOpacity: settings.brushOpacity || 1,
brushColor: settings.brushColor || "#000000",
},
};
this.customTextures.push(preset);
this._triggerEvent("textureAdded", preset);
return presetId;
}
/**
* 应用材质预设
* @param {String} presetId 预设ID
* @returns {Object|null} 预设设置
*/
applyTexturePreset(presetId) {
const preset = this.getTextureById(presetId);
if (!preset || preset.type !== "preset") {
return null;
}
return preset.settings;
}
/**
* 添加事件监听器
* @param {String} event 事件名称
* @param {Function} callback 回调函数
*/
addEventListener(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback);
}
}
/**
* 移除事件监听器
* @param {String} event 事件名称
* @param {Function} callback 回调函数
*/
removeEventListener(event, callback) {
if (this.listeners[event]) {
const index = this.listeners[event].indexOf(callback);
if (index !== -1) {
this.listeners[event].splice(index, 1);
}
}
}
/**
* 触发事件
* @param {String} event 事件名称
* @param {*} data 事件数据
* @private
*/
_triggerEvent(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach((callback) => {
try {
callback(data);
} catch (error) {
console.error(`执行 ${event} 事件监听器出错:`, error);
}
});
}
}
/**
* 清除所有缓存
*/
clearCache() {
this.textureCache.clear();
}
/**
* 获取统计信息
* @returns {Object} 统计信息
*/
getStats() {
return {
builtInCount: this.builtInTextures.length,
customCount: this.customTextures.length,
totalCount: this.getAllTextures().length,
categoriesCount: this.getCategories().length,
cacheSize: this.textureCache.size,
};
}
}
// 创建单例实例
const texturePresetManager = new TexturePresetManager();
// 导出单例
export default texturePresetManager;

View File

@@ -0,0 +1,720 @@
//import { fabric } from "fabric-with-all";
import { BrushStore } from "../../store/BrushStore";
import { brushRegistry } from "./BrushRegistry";
// 导入基础笔刷类型
import { PencilBrush } from "./types/PencilBrush";
import { TextureBrush } from "./types/TextureBrush";
// 导入集成的笔刷类型
import { CrayonBrush } from "./types/CrayonBrush";
import { FurBrush } from "./types/FurBrush";
import { InkBrush } from "./types/InkBrush";
import { LongfurBrush } from "./types/LongfurBrush";
import { WritingBrush } from "./types/WritingBrush";
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 { SpraypaintBrush } from "./types/SpraypaintBrush";
/**
* 笔刷管理器
* 负责管理和切换不同的笔刷类型
*/
export class BrushManager {
/**
* 构造函数
* @param {Object} options 配置选项
* @param {Object} options.canvas fabric.js画布实例
* @param {Object} options.brushStore 笔刷数据存储(可选)
* @param {Object} options.layerManager 图层管理器实例(可选)
*/
constructor(options = {}) {
this.canvas = options.canvas;
this.brushStore = options.brushStore || BrushStore;
this.layerManager = options.layerManager; // 添加图层管理器引用
// 当前活动笔刷
this.activeBrush = null;
this.activeBrushId = null;
// 初始化笔刷注册
this._registerDefaultBrushes();
}
/**
* 注册默认笔刷
* @private
*/
_registerDefaultBrushes() {
// 注册铅笔笔刷
brushRegistry.register("pencil", PencilBrush, {
name: "铅笔",
description: "基础铅笔工具,适合精细线条绘制",
category: "基础笔刷",
});
// 注册材质笔刷
brushRegistry.register("texture", TextureBrush);
// 注册集成的笔刷类型
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(
"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;
}
configure(brush, options = {}) {
super.configure(brush, options);
if (options.density !== undefined) {
brush.density = options.density;
}
if (options.randomOpacity !== undefined) {
brush.randomOpacity = options.randomOpacity;
}
if (options.dotWidth !== undefined) {
brush.dotWidth = options.dotWidth;
}
}
}
);
// 注册橡皮擦笔刷
brushRegistry.register(
"eraser",
class EraserBrush extends PencilBrush {
constructor(canvas, options = {}) {
super(canvas, {
id: "eraser",
name: "橡皮擦",
description: "擦除已绘制的内容",
category: "工具",
...options,
});
}
create() {
// 直接使用 fabric-with-erasing 库提供的 EraserBrush
this.brush = new fabric.EraserBrush(this.canvas);
this.configure(this.brush, this.options);
// 配置橡皮擦特有属性
this.brush.inverted = this.options.inverted || false; // 是否反向擦除(恢复擦除)
return this.brush;
}
configure(brush, options = {}) {
super.configure(brush, options);
// 橡皮擦特有配置
if (options.inverted !== undefined) {
brush.inverted = options.inverted;
}
}
/**
* 设置反向擦除模式
* @param {Boolean} inverted 是否启用反向擦除(撤销擦除效果)
*/
setInverted(inverted) {
if (this.brush) {
this.brush.inverted = inverted;
}
}
/**
* 获取橡皮擦配置属性
* @returns {Array} 可配置属性数组
*/
getConfigurableProperties() {
return [
...super.getConfigurableProperties(),
{
id: "inverted",
name: "反向擦除",
type: "boolean",
description: "启用时可以恢复已擦除的内容",
defaultValue: false,
min: null,
max: null,
},
];
}
/**
* 更新橡皮擦属性
* @param {String} propId 属性ID
* @param {any} value 属性值
*/
updateProperty(propId, value) {
if (propId === "inverted") {
this.setInverted(value);
} else {
super.updateProperty(propId, value);
}
}
}
);
// 注册水彩笔刷
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);
// 水彩效果特有的属性
this.brush.globalCompositeOperation = "multiply";
this.brush.shadow = new fabric.Shadow({
color: this.options.color || "#000",
blur: 5,
offsetX: 0,
offsetY: 0,
});
return this.brush;
}
configure(brush, options = {}) {
super.configure(brush, options);
// 水彩笔刷特有的配置
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,
});
}
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;
// 调用原始方法
originalOnMouseMove.call(this, pointer, options);
};
return this.brush;
}
configure(brush, options = {}) {
super.configure(brush, options);
// 粉笔特有的设置
brush.strokeDashArray = [5, 5]; // 虚线效果
}
}
);
}
/**
* 获取所有可用笔刷类型
* @returns {Array} 笔刷类型数组包含id、name和description
*/
getBrushTypes() {
// 从注册表获取所有笔刷信息
const brushes = brushRegistry.getAllBrushes();
// 将笔刷信息转换为期望的格式
return brushes.map((brushInfo) => ({
id: brushInfo.id,
name: brushInfo.metadata.name || brushInfo.id,
description: brushInfo.metadata.description || "",
category: brushInfo.metadata.category || "默认",
icon: brushInfo.metadata.icon || null,
}));
}
/**
* 初始化笔刷列表并更新BrushStore
*/
initializeBrushes() {
// 获取所有笔刷
const allBrushes = this.getBrushTypes();
// 更新BrushStore中的可用笔刷列表
this.brushStore.setAvailableBrushes(allBrushes);
// 设置默认笔刷
if (!this.activeBrushId && allBrushes.length > 0) {
this.setBrushType(allBrushes[0].id);
}
}
/**
* 设置笔刷类型
* @param {String} brushId 笔刷ID
* @returns {Object|null} 设置的笔刷实例
*/
setBrushType(brushId) {
// 如果相同笔刷,不做处理
if (this.activeBrushId === brushId) {
return this.activeBrush;
}
// 销毁当前笔刷
if (this.activeBrush) {
// 调用生命周期方法
if (this.activeBrush.onDeselected) {
this.activeBrush.onDeselected();
}
this.activeBrush.destroy();
}
// 创建新笔刷实例
try {
const brushInstance = brushRegistry.createBrushInstance(
brushId,
this.canvas,
{
color: brushId === "eraser" ? this.brushStore.state.color : undefined,
width: this.brushStore.state.size,
opacity: this.brushStore.state.opacity,
// 材质笔刷特有配置
textureEnabled: this.brushStore.state.textureEnabled,
texturePath: this.brushStore.state.texturePath,
textureScale: this.brushStore.state.textureScale,
}
);
if (brushInstance) {
// 创建笔刷
const fabricBrush = brushInstance.create();
// 更新画布的当前笔刷
if (fabricBrush) {
this.canvas.freeDrawingBrush = fabricBrush;
}
// 更新当前笔刷引用
this.activeBrush = brushInstance;
this.activeBrushId = brushId;
// 调用生命周期方法
if (this.activeBrush.onSelected) {
this.activeBrush.onSelected();
}
// 更新Store的笔刷类型
this.brushStore.setBrushType(brushId);
// 更新Store的当前笔刷实例用于动态属性系统
this.brushStore.setCurrentBrushInstance(brushInstance);
return brushInstance;
}
} catch (error) {
console.error(`创建笔刷 ${brushId} 失败:`, error);
}
return null;
}
/**
* 设置笔刷颜色
* @param {String} color 十六进制颜色值
*/
setBrushColor(color) {
if (!this.canvas.freeDrawingBrush) return;
// 更新笔刷颜色
this.canvas.freeDrawingBrush.color = color;
// 更新活动笔刷
if (this.activeBrush) {
this.activeBrush.configure(this.canvas.freeDrawingBrush, { color });
}
// 更新Store
this.brushStore.setBrushColor(color);
}
/**
* 设置笔刷大小
* @param {Number} size 笔刷大小
*/
setBrushSize(size) {
if (!this.canvas.freeDrawingBrush) return;
// 限制大小范围
const brushSize = Math.max(0.1, Math.min(100, size));
// 更新笔刷大小
this.canvas.freeDrawingBrush.width = brushSize;
// 更新活动笔刷
if (this.activeBrush) {
this.activeBrush.configure(this.canvas.freeDrawingBrush, {
width: brushSize,
});
}
// 更新Store
this.brushStore.setBrushSize(brushSize);
return brushSize;
}
/**
* 增加笔刷大小
* @param {Number} amount 增加量
* @returns {Number} 新的笔刷大小
*/
increaseBrushSize(amount = 1) {
const currentSize = this.brushStore.state.size;
return this.setBrushSize(currentSize + amount);
}
/**
* 减少笔刷大小
* @param {Number} amount 减少量
* @returns {Number} 新的笔刷大小
*/
decreaseBrushSize(amount = 1) {
const currentSize = this.brushStore.state.size;
return this.setBrushSize(currentSize - amount);
}
/**
* 增加笔刷透明度
* @param {Number} amount 增加量
* @returns {Number} 新的笔刷大小
*/
increaseBrushOpacity(amount = 0.01) {
const currentSize = this.brushStore.state.opacity;
return this.setBrushOpacity(currentSize + amount);
}
/**
* 减少笔刷大小
* @param {Number} amount 减少量
* @returns {Number} 新的笔刷大小
*/
decreaseBrushOpacity(amount = 0.01) {
const currentSize = this.brushStore.state.opacity;
return this.setBrushOpacity(currentSize - amount);
}
/**
* 设置笔刷透明度
* @param {Number} opacity 透明度 (0-1)
*/
setBrushOpacity(opacity) {
if (!this.canvas.freeDrawingBrush) return;
// 限制透明度范围
const brushOpacity = Math.max(0.05, Math.min(1, opacity));
// 更新笔刷透明度
this.canvas.freeDrawingBrush.opacity = brushOpacity;
// 更新活动笔刷
if (this.activeBrush) {
this.activeBrush.configure(this.canvas.freeDrawingBrush, {
opacity: brushOpacity,
});
}
// 更新Store
this.brushStore.setBrushOpacity(brushOpacity);
return brushOpacity;
}
/**
* 设置材质缩放
* @param {Number} scale 缩放比例
*/
setTextureScale(scale) {
// 限制缩放范围
const textureScale = Math.max(0.1, Math.min(10, scale));
// 更新活动笔刷
if (this.activeBrush && this.activeBrush.setTextureScale) {
this.activeBrush.setTextureScale(textureScale);
}
// 更新Store
this.brushStore.setTextureScale(textureScale);
return textureScale;
}
/**
* 增加材质缩放
* @param {Number} amount 增加量
*/
increaseTextureScale(amount = 0.1) {
const currentScale = this.brushStore.state.textureScale;
return this.setTextureScale(currentScale + amount);
}
/**
* 减少材质缩放
* @param {Number} amount 减少量
*/
decreaseTextureScale(amount = 0.1) {
const currentScale = this.brushStore.state.textureScale;
return this.setTextureScale(currentScale - amount);
}
/**
* 设置材质路径
* @param {String} path 材质图片路径
*/
setTexturePath(path) {
// 更新活动笔刷
if (this.activeBrush && this.activeBrush.setTexturePath) {
this.activeBrush.setTexturePath(path);
}
// 更新Store
this.brushStore.setTexturePath(path);
return path;
}
/**
* 启用/禁用材质
* @param {Boolean} enabled 是否启用
*/
setTextureEnabled(enabled) {
// 更新Store
this.brushStore.setTextureEnabled(enabled);
// 如果启用材质,且当前不是材质笔刷,需要切换
if (enabled && this.activeBrushId !== "texture") {
this.setBrushType("texture");
}
return enabled;
}
/**
* 注册新笔刷
* @param {String} id 笔刷ID
* @param {Class} brushClass 笔刷类
* @param {Object} metadata 笔刷元数据
* @returns {Boolean} 是否注册成功
*/
registerBrush(id, brushClass, metadata = {}) {
const success = brushRegistry.register(id, brushClass, metadata);
if (success) {
// 更新可用笔刷列表
this.initializeBrushes();
}
return success;
}
/**
* 获取当前笔刷类型
* @returns {String} 当前笔刷类型ID
*/
getCurrentBrushType() {
return this.activeBrushId;
}
/**
* 获取当前笔刷大小
* @returns {Number} 当前笔刷大小
*/
getBrushSize() {
return this.brushStore.state.size;
}
/**
* 获取当前笔刷颜色
* @returns {String} 当前笔刷颜色
*/
getBrushColor() {
return this.brushStore.state.color;
}
/**
* 获取当前笔刷透明度
* @returns {Number} 当前笔刷透明度
*/
getBrushOpacity() {
return this.brushStore.state.opacity;
}
/**
* 获取材质缩放
* @returns {Number} 材质缩放比例
*/
getTextureScale() {
return this.brushStore.state.textureScale;
}
/**
* 创建材质笔刷
* 这个方法保留用于向下兼容
* @deprecated 请使用setBrushType('texture')代替
* @returns {Object} 材质笔刷实例
*/
createTextureBrush() {
console.warn('createTextureBrush方法已废弃请使用setBrushType("texture")');
return this.setBrushType("texture");
}
/**
* 更新笔刷
* 根据当前设置应用笔刷属性到画布
*/
updateBrush() {
if (!this.canvas) return;
// 如果有活动的笔刷实例,重新配置它
if (this.activeBrush && this.canvas.freeDrawingBrush) {
this.activeBrush.configure(this.canvas.freeDrawingBrush, {
color: this.brushStore.state.color,
width: this.brushStore.state.size,
opacity: this.brushStore.state.opacity,
});
} else {
// 如果没有活动笔刷,创建一个默认的
this.setBrushType(this.activeBrushId || "pencil");
}
// 更新画布状态
this?.canvas?.renderAll?.();
return this.canvas.freeDrawingBrush;
}
/**
* 创建橡皮擦
* @returns {Object} 橡皮擦笔刷
*/
createEraser() {
return this.setBrushType("eraser");
}
/**
* 创建吸色工具
* @param {Function} callback 选择颜色后的回调函数
*/
createEyedropper(callback) {
// 保存当前状态
const previousBrushId = this.activeBrushId;
// 一次性事件处理程序
const handleMouseDown = (event) => {
const pointer = this.canvas.getPointer(event.e);
const ctx = this.canvas.getContext();
// 获取点击位置的像素
const imageData = ctx.getImageData(pointer.x, pointer.y, 1, 1).data;
// 将RGB转换为十六进制颜色
const color = `#${(
(1 << 24) +
(imageData[0] << 16) +
(imageData[1] << 8) +
imageData[2]
)
.toString(16)
.slice(1)}`;
// 调用回调函数
if (typeof callback === "function") {
callback(color);
}
// 恢复之前的笔刷
this.setBrushType(previousBrushId);
// 移除事件监听器
this.canvas.off("mouse:down", handleMouseDown);
};
// 添加事件监听器
this.canvas.on("mouse:down", handleMouseDown);
// 设置吸色光标
this.canvas.defaultCursor = "crosshair";
console.log("吸色工具已激活,点击画布选择颜色");
}
/**
* 销毁资源
*/
dispose() {
// 销毁当前笔刷
if (this.activeBrush) {
this.activeBrush.destroy();
this.activeBrush = null;
}
this.canvas = null;
}
}
// 导出单例
export default BrushManager;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,244 @@
import { BaseBrush } from "../BaseBrush";
/**
* 蜡笔笔刷
* 模拟蜡笔效果,具有颗粒感和纹理
*/
export class CrayonBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "crayon",
name: "蜡笔",
description: "模拟蜡笔效果,具有颗粒感和纹理",
category: "特效笔刷",
icon: "crayon",
...options,
});
// 蜡笔笔刷特有属性
this._baseWidth = options._baseWidth || 15;
this._size = options._size || 0;
this._sep = options._sep || options._sep === 0 ? options._sep : 3;
this._inkAmount = options._inkAmount || 10;
this.randomness = options.randomness || 0.5; // 随机性
this.texture = options.texture || "default"; // 纹理类型
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生蜡笔笔刷
this.brush = new fabric.CrayonBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
// 更新笔刷相关属性
this._baseWidth = options.width / 2;
this._size = options.width / 2 + this._baseWidth;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 蜡笔笔刷特有属性
if (options._baseWidth !== undefined) {
brush._baseWidth = options._baseWidth;
this._baseWidth = options._baseWidth;
this._size = this.width / 2 + this._baseWidth;
}
if (options._sep !== undefined) {
brush._sep = options._sep;
this._sep = options._sep;
}
if (options._inkAmount !== undefined) {
brush._inkAmount = options._inkAmount;
this._inkAmount = options._inkAmount;
}
}
/**
* 设置颗粒分离度
* @param {Number} sep 分离度值
*/
setSeparation(sep) {
this._sep = Math.max(0.5, Math.min(10, sep));
if (this.brush) {
this.brush._sep = this._sep;
}
return this._sep;
}
/**
* 设置墨量
* @param {Number} amount 墨量值
*/
setInkAmount(amount) {
this._inkAmount = Math.max(1, Math.min(50, amount));
if (this.brush) {
this.brush._inkAmount = this._inkAmount;
}
return this._inkAmount;
}
/**
* 设置随机性
* @param {Number} value 随机性值(0-1)
*/
setRandomness(value) {
this.randomness = Math.max(0, Math.min(1, value));
return this.randomness;
}
/**
* 设置纹理类型
* @param {String} type 纹理类型
*/
setTexture(type) {
this.texture = type;
// 实际应用可能需要更多的实现逻辑
return this.texture;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义蜡笔笔刷特有属性
const crayonProperties = [
{
id: "separation",
name: "颗粒分离度",
type: "slider",
defaultValue: this._sep,
min: 0.5,
max: 10,
step: 0.5,
description: "控制蜡笔颗粒的分离程度",
category: "蜡笔设置",
order: 100,
},
{
id: "inkAmount",
name: "墨量",
type: "slider",
defaultValue: this._inkAmount,
min: 1,
max: 50,
step: 1,
description: "控制蜡笔的颜料量",
category: "蜡笔设置",
order: 110,
},
{
id: "randomness",
name: "随机性",
type: "slider",
defaultValue: this.randomness,
min: 0,
max: 1,
step: 0.05,
description: "控制蜡笔纹理的随机程度",
category: "蜡笔设置",
order: 120,
},
{
id: "texture",
name: "纹理类型",
type: "select",
defaultValue: this.texture,
options: [
{ value: "default", label: "默认" },
{ value: "rough", label: "粗糙" },
{ value: "smooth", label: "平滑" },
],
description: "设置蜡笔的纹理类型",
category: "蜡笔设置",
order: 130,
},
];
// 合并并返回所有属性
return [...baseProperties, ...crayonProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理蜡笔笔刷特有属性
if (propId === "separation") {
this.setSeparation(value);
return true;
} else if (propId === "inkAmount") {
this.setInkAmount(value);
return true;
} else if (propId === "randomness") {
this.setRandomness(value);
return true;
} else if (propId === "texture") {
this.setTexture(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB4PSIxMCIgeT0iMTAiIHdpZHRoPSI4MCIgaGVpZ2h0PSI4MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48cmVjdCB4PSIzMCIgeT0iMzAiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=";
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,196 @@
import { BaseBrush } from "../BaseBrush";
/**
* 钢笔笔刷
* 模拟钢笔效果,具有变化的透明度
*/
export class CustomPenBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "pen",
name: "钢笔",
description: "模拟钢笔效果,具有变化的透明度",
category: "基础笔刷",
icon: "pen",
...options,
});
// 钢笔笔刷特有属性
this._baseWidth = options._baseWidth || 15;
this._lineWidth = options._lineWidth || 2;
this.inkOpacityMin = options.inkOpacityMin || 0.2;
this.inkOpacityMax = options.inkOpacityMax || 0.6;
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生钢笔笔刷
this.brush = new fabric.PenBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
// 更新笔刷相关属性
this._baseWidth = options.width / 2;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 钢笔笔刷特有属性
if (options._baseWidth !== undefined) {
brush._baseWidth = options._baseWidth;
this._baseWidth = options._baseWidth;
}
if (options._lineWidth !== undefined) {
brush._lineWidth = options._lineWidth;
this._lineWidth = options._lineWidth;
}
// 确保线条连接设置正确
brush.canvas.contextTop.lineJoin = "round";
brush.canvas.contextTop.lineCap = "round";
}
/**
* 设置最小墨水透明度
* @param {Number} opacity 透明度值(0-1)
*/
setInkOpacityMin(opacity) {
this.inkOpacityMin = Math.max(0.1, Math.min(0.5, opacity));
return this.inkOpacityMin;
}
/**
* 设置最大墨水透明度
* @param {Number} opacity 透明度值(0-1)
*/
setInkOpacityMax(opacity) {
this.inkOpacityMax = Math.max(0.3, Math.min(1, opacity));
return this.inkOpacityMax;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义钢笔笔刷特有属性
const penProperties = [
{
id: "lineWidth",
name: "线条宽度",
type: "slider",
defaultValue: this._lineWidth,
min: 1,
max: 10,
step: 0.5,
description: "控制钢笔线条的宽度",
category: "钢笔设置",
order: 100,
},
{
id: "inkOpacityMin",
name: "最小墨水透明度",
type: "slider",
defaultValue: this.inkOpacityMin,
min: 0.1,
max: 0.5,
step: 0.05,
description: "控制钢笔墨水最小透明度",
category: "钢笔设置",
order: 110,
},
{
id: "inkOpacityMax",
name: "最大墨水透明度",
type: "slider",
defaultValue: this.inkOpacityMax,
min: 0.3,
max: 1,
step: 0.05,
description: "控制钢笔墨水最大透明度",
category: "钢笔设置",
order: 120,
},
];
// 合并并返回所有属性
return [...baseProperties, ...penProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理钢笔笔刷特有属性
if (propId === "lineWidth") {
this._lineWidth = value;
if (this.brush) {
this.brush._lineWidth = value;
}
return true;
} else if (propId === "inkOpacityMin") {
this.setInkOpacityMin(value);
return true;
} else if (propId === "inkOpacityMax") {
this.setInkOpacityMax(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjAgMjBMODAgODBNMjAgODBMODAgMjAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=";
}
}

View File

@@ -0,0 +1,201 @@
import { BaseBrush } from "../BaseBrush";
/**
* 毛发笔刷
* 创建类似于毛发或草的效果
*/
export class FurBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "fur",
name: "毛发笔刷",
description: "创建类似于毛发或草的纹理效果",
category: "特效笔刷",
icon: "fur",
...options,
});
// 毛发笔刷特有属性
this.furLength = options.furLength || 10;
this.furDensity = options.furDensity || 0.7;
this.furRandomness = options.furRandomness || 0.5;
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生毛发笔刷
this.brush = new fabric.FurBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 这里可以添加对毛发笔刷特有属性的配置
// 由于fabric.FurBrush的原始实现可能没有直接暴露这些属性
// 我们可能需要在onMouseMove等事件中动态调整行为
// 存储特有属性,供后续使用
if (options.furLength !== undefined) {
this.furLength = options.furLength;
}
if (options.furDensity !== undefined) {
this.furDensity = options.furDensity;
}
if (options.furRandomness !== undefined) {
this.furRandomness = options.furRandomness;
}
}
/**
* 设置毛发长度
* @param {Number} length 长度值
*/
setFurLength(length) {
this.furLength = Math.max(1, Math.min(50, length));
return this.furLength;
}
/**
* 设置毛发密度
* @param {Number} density 密度值(0-1)
*/
setFurDensity(density) {
this.furDensity = Math.max(0.1, Math.min(1, density));
return this.furDensity;
}
/**
* 设置毛发随机性
* @param {Number} randomness 随机性值(0-1)
*/
setFurRandomness(randomness) {
this.furRandomness = Math.max(0, Math.min(1, randomness));
return this.furRandomness;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义毛发笔刷特有属性
const furProperties = [
{
id: "furLength",
name: "毛发长度",
type: "slider",
defaultValue: this.furLength,
min: 1,
max: 50,
step: 1,
description: "控制毛发的长度",
category: "毛发设置",
order: 100,
},
{
id: "furDensity",
name: "毛发密度",
type: "slider",
defaultValue: this.furDensity,
min: 0.1,
max: 1,
step: 0.05,
description: "控制毛发的密度",
category: "毛发设置",
order: 110,
},
{
id: "furRandomness",
name: "随机性",
type: "slider",
defaultValue: this.furRandomness,
min: 0,
max: 1,
step: 0.05,
description: "控制毛发的随机分布程度",
category: "毛发设置",
order: 120,
},
];
// 合并并返回所有属性
return [...baseProperties, ...furProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理毛发笔刷特有属性
if (propId === "furLength") {
this.setFurLength(value);
return true;
} else if (propId === "furDensity") {
this.setFurDensity(value);
return true;
} else if (propId === "furRandomness") {
this.setFurRandomness(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMTAgODBMNTAgMjBNMjAgODBMNjAgMjBNMzAgODBMNzAgMjBNNDAgODBMODAgMjBNNTAgODBMOTAgMjAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIi8+PC9zdmc+";
}
}

View File

@@ -0,0 +1,267 @@
import { BaseBrush } from "../BaseBrush";
/**
* 水墨笔刷
* 模拟中国传统水墨画效果
*/
export class InkBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "ink",
name: "水墨笔刷",
description: "模拟中国传统水墨画效果,墨色深浅不一",
category: "特效笔刷",
icon: "ink",
...options,
});
// 水墨笔刷特有属性
this._baseWidth = options._baseWidth || 15;
this._inkAmount = options._inkAmount || 7;
this._range = options._range || 10;
this.splashEnabled =
options.splashEnabled !== undefined ? options.splashEnabled : true;
this.splashSize = options.splashSize || 5;
this.splashDistance = options.splashDistance || 30;
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生水墨笔刷
this.brush = new fabric.InkBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
this._baseWidth = options.width / 2;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 水墨笔刷特有属性
if (options._inkAmount !== undefined) {
brush._inkAmount = options._inkAmount;
this._inkAmount = options._inkAmount;
}
if (options._range !== undefined) {
brush._range = options._range;
this._range = options._range;
}
// 更新溅墨相关配置这需要修改原始InkBrush的drawSplash方法
if (this.splashEnabled !== undefined) {
// 由于原始InkBrush没有直接暴露这个配置我们可能需要覆盖方法
// 这里仅保存配置实际逻辑需要在brush创建后处理
}
}
/**
* 设置墨量
* @param {Number} amount 墨量值
*/
setInkAmount(amount) {
this._inkAmount = Math.max(1, Math.min(20, amount));
if (this.brush) {
this.brush._inkAmount = this._inkAmount;
}
return this._inkAmount;
}
/**
* 设置笔触范围
* @param {Number} range 范围值
*/
setRange(range) {
this._range = Math.max(5, Math.min(50, range));
if (this.brush) {
this.brush._range = this._range;
}
return this._range;
}
/**
* 启用/禁用溅墨效果
* @param {Boolean} enabled 是否启用
*/
setSplashEnabled(enabled) {
this.splashEnabled = enabled;
// 实际应用需要更多的逻辑来支持这个功能
// 由于需要修改fabric.InkBrush的内部行为
return this.splashEnabled;
}
/**
* 设置溅墨大小
* @param {Number} size 大小值
*/
setSplashSize(size) {
this.splashSize = Math.max(1, Math.min(20, size));
return this.splashSize;
}
/**
* 设置溅墨距离
* @param {Number} distance 距离值
*/
setSplashDistance(distance) {
this.splashDistance = Math.max(10, Math.min(100, distance));
return this.splashDistance;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义水墨笔刷特有属性
const inkProperties = [
{
id: "inkAmount",
name: "墨量",
type: "slider",
defaultValue: this._inkAmount,
min: 1,
max: 20,
step: 1,
description: "控制水墨的浓度",
category: "水墨设置",
order: 100,
},
{
id: "range",
name: "笔触范围",
type: "slider",
defaultValue: this._range,
min: 5,
max: 50,
step: 1,
description: "控制水墨扩散的范围",
category: "水墨设置",
order: 110,
},
{
id: "splashEnabled",
name: "溅墨效果",
type: "checkbox",
defaultValue: this.splashEnabled,
description: "是否启用溅墨效果",
category: "水墨设置",
order: 120,
},
{
id: "splashSize",
name: "溅墨大小",
type: "slider",
defaultValue: this.splashSize,
min: 1,
max: 20,
step: 1,
description: "溅墨点的大小",
category: "水墨设置",
order: 130,
visibleWhen: { splashEnabled: true },
},
{
id: "splashDistance",
name: "溅墨距离",
type: "slider",
defaultValue: this.splashDistance,
min: 10,
max: 100,
step: 5,
description: "溅墨可扩散的最大距离",
category: "水墨设置",
order: 140,
visibleWhen: { splashEnabled: true },
},
];
// 合并并返回所有属性
return [...baseProperties, ...inkProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理水墨笔刷特有属性
if (propId === "inkAmount") {
this.setInkAmount(value);
return true;
} else if (propId === "range") {
this.setRange(value);
return true;
} else if (propId === "splashEnabled") {
this.setSplashEnabled(value);
return true;
} else if (propId === "splashSize") {
this.setSplashSize(value);
return true;
} else if (propId === "splashDistance") {
this.setSplashDistance(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjAgODBDNDAgNjAgNjAgNDAgODAgMjAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSI1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiLz48Y2lyY2xlIGN4PSI3MCIgY3k9IjMwIiByPSI1IiBmaWxsPSIjMDAwIi8+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iMyIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjMwIiBjeT0iNzAiIHI9IjYiIGZpbGw9IiMwMDAiLz48L3N2Zz4=";
}
}

View File

@@ -0,0 +1,305 @@
import { BaseBrush } from "../BaseBrush";
/**
* 长毛发笔刷
* 创建类似于长毛、毛皮、草或头发的效果
*/
export class LongfurBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "longfur",
name: "长毛发",
description: "创建流动的长毛发效果,适合绘制动物毛皮、草或头发",
category: "特效笔刷",
icon: "longfur",
...options,
});
// 长毛发笔刷特有属性
this.furLength = options.furLength || 20;
this.furDensity = options.furDensity || 0.7;
this.furFlowFactor = options.furFlowFactor || 0.5;
this.furCurvature = options.furCurvature || 0.3;
this.randomizeDirection =
options.randomizeDirection !== undefined
? options.randomizeDirection
: true;
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生长毛发笔刷
this.brush = new fabric.LongfurBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 长毛发笔刷特有属性
if (options.furLength !== undefined) {
this.furLength = options.furLength;
// 如果原生笔刷支持此属性,则设置
if (brush.furLength !== undefined) {
brush.furLength = this.furLength;
}
}
if (options.furDensity !== undefined) {
this.furDensity = options.furDensity;
// 如果原生笔刷支持此属性,则设置
if (brush.furDensity !== undefined) {
brush.furDensity = this.furDensity;
}
}
if (options.furFlowFactor !== undefined) {
this.furFlowFactor = options.furFlowFactor;
// 如果原生笔刷支持此属性,则设置
if (brush.furFlowFactor !== undefined) {
brush.furFlowFactor = this.furFlowFactor;
}
}
if (options.furCurvature !== undefined) {
this.furCurvature = options.furCurvature;
// 如果原生笔刷支持此属性,则设置
if (brush.furCurvature !== undefined) {
brush.furCurvature = this.furCurvature;
}
}
if (options.randomizeDirection !== undefined) {
this.randomizeDirection = options.randomizeDirection;
// 如果原生笔刷支持此属性,则设置
if (brush.randomizeDirection !== undefined) {
brush.randomizeDirection = this.randomizeDirection;
}
}
}
/**
* 设置毛发长度
* @param {Number} length 长度值
*/
setFurLength(length) {
this.furLength = Math.max(5, Math.min(100, length));
if (this.brush && this.brush.furLength !== undefined) {
this.brush.furLength = this.furLength;
}
return this.furLength;
}
/**
* 设置毛发密度
* @param {Number} density 密度值(0-1)
*/
setFurDensity(density) {
this.furDensity = Math.max(0.1, Math.min(1, density));
if (this.brush && this.brush.furDensity !== undefined) {
this.brush.furDensity = this.furDensity;
}
return this.furDensity;
}
/**
* 设置毛发流动系数
* @param {Number} factor 流动系数(0-1)
*/
setFurFlowFactor(factor) {
this.furFlowFactor = Math.max(0, Math.min(1, factor));
if (this.brush && this.brush.furFlowFactor !== undefined) {
this.brush.furFlowFactor = this.furFlowFactor;
}
return this.furFlowFactor;
}
/**
* 设置毛发弯曲度
* @param {Number} curvature 弯曲度(0-1)
*/
setFurCurvature(curvature) {
this.furCurvature = Math.max(0, Math.min(1, curvature));
if (this.brush && this.brush.furCurvature !== undefined) {
this.brush.furCurvature = this.furCurvature;
}
return this.furCurvature;
}
/**
* 设置是否随机化方向
* @param {Boolean} randomize 是否随机化
*/
setRandomizeDirection(randomize) {
this.randomizeDirection = randomize;
if (this.brush && this.brush.randomizeDirection !== undefined) {
this.brush.randomizeDirection = this.randomizeDirection;
}
return this.randomizeDirection;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义长毛发笔刷特有属性
const longfurProperties = [
{
id: "furLength",
name: "毛发长度",
type: "slider",
defaultValue: this.furLength,
min: 5,
max: 100,
step: 1,
description: "控制毛发的长度",
category: "长毛发设置",
order: 100,
},
{
id: "furDensity",
name: "毛发密度",
type: "slider",
defaultValue: this.furDensity,
min: 0.1,
max: 1,
step: 0.05,
description: "控制毛发的密度",
category: "长毛发设置",
order: 110,
},
{
id: "furFlowFactor",
name: "流动系数",
type: "slider",
defaultValue: this.furFlowFactor,
min: 0,
max: 1,
step: 0.05,
description: "控制毛发的流动感",
category: "长毛发设置",
order: 120,
},
{
id: "furCurvature",
name: "弯曲度",
type: "slider",
defaultValue: this.furCurvature,
min: 0,
max: 1,
step: 0.05,
description: "控制毛发的弯曲程度",
category: "长毛发设置",
order: 130,
},
{
id: "randomizeDirection",
name: "随机方向",
type: "checkbox",
defaultValue: this.randomizeDirection,
description: "是否随机化毛发方向",
category: "长毛发设置",
order: 140,
},
];
// 合并并返回所有属性
return [...baseProperties, ...longfurProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理长毛发笔刷特有属性
if (propId === "furLength") {
this.setFurLength(value);
return true;
} else if (propId === "furDensity") {
this.setFurDensity(value);
return true;
} else if (propId === "furFlowFactor") {
this.setFurFlowFactor(value);
return true;
} else if (propId === "furCurvature") {
this.setFurCurvature(value);
return true;
} else if (propId === "randomizeDirection") {
this.setRandomizeDirection(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjUgNTBDMjUgNTAgNTAgMTAgNTAgNTBDNTAgNTAgNTAgOTAgNzUgNTAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIi8+PGxpbmUgeDE9IjMwIiB5MT0iNDUiIHgyPSIzMCIgeTI9IjEwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMSIvPjxsaW5lIHgxPSI0MCIgeTE9IjQwIiB4Mj0iNDAiIHkyPSI1IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMSIvPjxsaW5lIHgxPSI1MCIgeTE9IjQwIiB4Mj0iNTAiIHkyPSIxMCIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjEiLz48bGluZSB4MT0iNjAiIHkxPSI0MCIgeDI9IjYwIiB5Mj0iNSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjEiLz48bGluZSB4MT0iNzAiIHkxPSI0NSIgeDI9IjcwIiB5Mj0iMTAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PC9zdmc+";
}
}

View File

@@ -0,0 +1,230 @@
import { BaseBrush } from "../BaseBrush";
/**
* 马克笔笔刷
* 模拟马克笔效果,具有半透明平头笔触
*/
export class MarkerBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "marker",
name: "马克笔",
description: "模拟马克笔效果,具有半透明平头笔触",
category: "基础笔刷",
icon: "marker",
...options,
});
// 马克笔特有属性
this._baseWidth = options._baseWidth || 15;
this._lineWidth = options._lineWidth || 2;
this.capStyle = options.capStyle || "round"; // "round" 或 "square"
this.blendMode = options.blendMode || "multiply"; // 混合模式
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生马克笔笔刷
this.brush = new fabric.MarkerBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
// 更新笔刷相关属性
this._baseWidth = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
// 马克笔的透明度默认不要太高
brush.opacity = Math.min(0.8, options.opacity || 0.6);
}
// 马克笔笔刷特有属性
if (options._baseWidth !== undefined) {
brush._baseWidth = options._baseWidth;
this._baseWidth = options._baseWidth;
}
if (options._lineWidth !== undefined) {
brush._lineWidth = options._lineWidth;
this._lineWidth = options._lineWidth;
}
// 笔触样式设置
brush.canvas.contextTop.lineJoin = "round";
brush.canvas.contextTop.lineCap = this.capStyle || "round";
// 马克笔的混合模式设置
if (this.blendMode === "multiply") {
brush.canvas.contextTop.globalCompositeOperation = "multiply";
} else {
brush.canvas.contextTop.globalCompositeOperation = "source-over";
}
}
/**
* 设置笔触线宽
* @param {Number} width 线宽值
*/
setLineWidth(width) {
this._lineWidth = Math.max(1, Math.min(10, width));
if (this.brush) {
this.brush._lineWidth = this._lineWidth;
}
return this._lineWidth;
}
/**
* 设置笔头样式
* @param {String} style 笔头样式 ('round' 或 'square')
*/
setCapStyle(style) {
if (style === "round" || style === "square") {
this.capStyle = style;
if (this.brush && this.brush.canvas) {
this.brush.canvas.contextTop.lineCap = style;
}
}
return this.capStyle;
}
/**
* 设置混合模式
* @param {String} mode 混合模式 ('multiply' 或 'normal')
*/
setBlendMode(mode) {
this.blendMode = mode;
if (this.brush && this.brush.canvas) {
this.brush.canvas.contextTop.globalCompositeOperation =
mode === "multiply" ? "multiply" : "source-over";
}
return this.blendMode;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义马克笔笔刷特有属性
const markerProperties = [
{
id: "lineWidth",
name: "笔触宽度",
type: "slider",
defaultValue: this._lineWidth,
min: 1,
max: 10,
step: 0.5,
description: "控制马克笔笔触的宽度",
category: "马克笔设置",
order: 100,
},
{
id: "capStyle",
name: "笔头样式",
type: "select",
defaultValue: this.capStyle,
options: [
{ value: "round", label: "圆形" },
{ value: "square", label: "方形" },
],
description: "设置马克笔笔头的形状",
category: "马克笔设置",
order: 110,
},
{
id: "blendMode",
name: "混合模式",
type: "select",
defaultValue: this.blendMode,
options: [
{ value: "multiply", label: "正片叠底" },
{ value: "normal", label: "正常" },
],
description: "设置马克笔的颜色混合方式",
category: "马克笔设置",
order: 120,
},
];
// 合并并返回所有属性
return [...baseProperties, ...markerProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理马克笔笔刷特有属性
if (propId === "lineWidth") {
this.setLineWidth(value);
return true;
} else if (propId === "capStyle") {
this.setCapStyle(value);
return true;
} else if (propId === "blendMode") {
this.setBlendMode(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMTAgNTBIOTAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyMCIgc3Ryb2tlLW9wYWNpdHk9IjAuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+";
}
}

View File

@@ -0,0 +1,318 @@
import { BaseBrush } from "../BaseBrush";
/**
* 铅笔笔刷
* fabric原生铅笔笔刷的包装类
*/
export class PencilBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "pencil",
name: "铅笔",
description: "基础铅笔工具,适合精细线条绘制",
category: "基础笔刷",
icon: "pencil",
...options,
});
// 铅笔笔刷特有属性
this.decimate = options.decimate || 0.4;
this.strokeLineCap = options.strokeLineCap || "round";
this.strokeLineJoin = options.strokeLineJoin || "round";
}
/**
* 创建笔刷实例
* @returns {Object} fabric.PencilBrush实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生铅笔笔刷
this.brush = new fabric.PencilBrush(this.canvas);
// 重写 _finalizeAndAddPath 方法,使其调用 convertToImg 而不是创建 Path 对象
const originalFinalizeAndAddPath = this.brush._finalizeAndAddPath.bind(
this.brush
);
const self = this; // 保存外部this引用
this.brush._finalizeAndAddPath = function () {
console.log("PencilBrush: _finalizeAndAddPath called");
const ctx = this.canvas.contextTop;
ctx.closePath();
// 应用点简化
if (this.decimate) {
this._points = this.decimatePoints(this._points, this.decimate);
}
console.log(
"PencilBrush: points count =",
this._points ? this._points.length : 0
);
// 检查是否有有效的路径数据
if (!this._points || this._points.length < 2) {
// 如果点数不足,直接请求重新渲染
console.log("PencilBrush: Not enough points, skipping");
this.canvas.requestRenderAll();
return;
}
const pathData = this.convertPointsToSVGPath(this._points);
const isEmpty = self._isEmptySVGPath(pathData);
console.log("PencilBrush: isEmpty =", isEmpty);
if (isEmpty) {
// 如果路径为空,直接请求重新渲染
console.log("PencilBrush: Path is empty, skipping");
this.canvas.requestRenderAll();
return;
}
// 先触发事件,模拟原生行为
const path = this.createPath(pathData);
this.canvas.fire("before:path:created", { path: path });
console.log("PencilBrush: Calling convertToImg");
// 调用 convertToImg 方法将绘制内容转换为图片
if (typeof this.convertToImg === "function") {
this.convertToImg();
console.log("PencilBrush: 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);
return this.brush;
}
/**
* 检查 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);
}
/**
* 配置笔刷
* @param {Object} brush fabric.PencilBrush实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) {
return;
}
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 特殊属性配置
if (options.decimate !== undefined) {
brush.decimate = options.decimate;
this.decimate = options.decimate;
}
if (options.strokeLineCap !== undefined) {
brush.strokeLineCap = options.strokeLineCap;
this.strokeLineCap = options.strokeLineCap;
}
if (options.strokeLineJoin !== undefined) {
brush.strokeLineJoin = options.strokeLineJoin;
this.strokeLineJoin = options.strokeLineJoin;
}
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义铅笔笔刷特有属性
const pencilProperties = [
{
id: "decimate",
name: "精细度",
type: "slider",
defaultValue: this.decimate,
min: 0,
max: 1,
step: 0.1,
description: "控制笔触路径的简化程度,值越小路径越精细",
category: "铅笔设置",
order: 100,
},
{
id: "strokeLineCap",
name: "线条端点",
type: "select",
defaultValue: this.strokeLineCap,
options: [
{ value: "round", label: "圆形" },
{ value: "butt", label: "平直" },
{ value: "square", label: "方形" },
],
description: "线条端点的形状",
category: "铅笔设置",
order: 110,
},
{
id: "strokeLineJoin",
name: "线条连接",
type: "select",
defaultValue: this.strokeLineJoin,
options: [
{ value: "round", label: "圆角" },
{ value: "bevel", label: "斜角" },
{ value: "miter", label: "尖角" },
],
description: "线条拐角的连接方式",
category: "铅笔设置",
order: 120,
},
{
id: "smoothingEnabled",
name: "启用平滑",
type: "checkbox",
defaultValue: false,
description: "是否对线条进行平滑处理",
category: "铅笔设置",
order: 130,
},
{
id: "smoothingFactor",
name: "平滑程度",
type: "slider",
defaultValue: 0.5,
min: 0,
max: 1,
step: 0.05,
description: "线条平滑的强度",
category: "铅笔设置",
order: 140,
// 只有当smoothingEnabled为true时才显示
visibleWhen: { smoothingEnabled: true },
},
];
// 合并并返回所有属性
return [...baseProperties, ...pencilProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理铅笔特有属性
if (propId === "decimate") {
this.decimate = value;
if (this.brush) {
this.brush.decimate = value;
return true;
}
} else if (propId === "strokeLineCap") {
this.strokeLineCap = value;
if (this.brush) {
this.brush.strokeLineCap = value;
return true;
}
} else if (propId === "strokeLineJoin") {
this.strokeLineJoin = value;
if (this.brush) {
this.brush.strokeLineJoin = value;
return true;
}
} else if (propId === "smoothingEnabled") {
this.smoothingEnabled = value;
// 实现平滑逻辑...
return true;
} else if (propId === "smoothingFactor") {
this.smoothingFactor = value;
// 实现平滑度调整...
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
// 实际项目中可以返回一个实际的预览图URL
return "data:image/svg+xml;base64,..."; // 示例SVG
}
}

View File

@@ -0,0 +1,351 @@
import { BaseBrush } from "../BaseBrush";
/**
* 丝带笔刷
* 创建流畅的飘带状线条
*/
export class RibbonBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "ribbon",
name: "飘带",
description: "创建流畅的飘带状线条,具有动态宽度变化和曲线美感",
category: "特效笔刷",
icon: "ribbon",
...options,
});
// 丝带笔刷特有属性
this.ribbonWidth = options.ribbonWidth || 20;
this.widthVariation = options.widthVariation || 0.5;
this.ribbonSmoothness = options.ribbonSmoothness || 0.7;
this.gradient = options.gradient !== undefined ? options.gradient : true;
this.gradientColors = options.gradientColors || ["#000000", "#555555"];
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生丝带笔刷
this.brush = new fabric.RibbonBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
// 基于主宽度更新丝带宽度
this.ribbonWidth = options.width * 2;
}
if (options.color !== undefined) {
brush.color = options.color;
// 如果启用渐变,更新渐变的第一个颜色
if (this.gradient && this.gradientColors.length > 0) {
this.gradientColors[0] = options.color;
this.updateGradient();
}
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 丝带笔刷特有属性
if (options.ribbonWidth !== undefined) {
this.ribbonWidth = options.ribbonWidth;
// 如果原生笔刷支持此属性
if (brush.ribbonWidth !== undefined) {
brush.ribbonWidth = this.ribbonWidth;
}
}
if (options.widthVariation !== undefined) {
this.widthVariation = options.widthVariation;
// 如果原生笔刷支持此属性
if (brush.widthVariation !== undefined) {
brush.widthVariation = this.widthVariation;
}
}
if (options.ribbonSmoothness !== undefined) {
this.ribbonSmoothness = options.ribbonSmoothness;
// 如果原生笔刷支持此属性
if (brush.ribbonSmoothness !== undefined) {
brush.ribbonSmoothness = this.ribbonSmoothness;
}
}
if (options.gradient !== undefined) {
this.gradient = options.gradient;
this.updateGradient();
}
if (options.gradientColors !== undefined) {
this.gradientColors = options.gradientColors;
this.updateGradient();
}
}
/**
* 更新渐变设置
* @private
*/
updateGradient() {
if (!this.brush || !this.canvas) return;
if (this.gradient && this.gradientColors.length >= 2) {
// 创建渐变对象
const ctx = this.canvas.contextTop;
const gradient = ctx.createLinearGradient(0, 0, this.ribbonWidth, 0);
// 添加渐变色
const colorCount = this.gradientColors.length;
this.gradientColors.forEach((color, index) => {
gradient.addColorStop(index / (colorCount - 1), color);
});
// 如果原生笔刷支持渐变
if (typeof this.brush.setGradient === "function") {
this.brush.setGradient(gradient);
} else if (this.brush.gradient !== undefined) {
this.brush.gradient = gradient;
}
// 如果原生笔刷支持渐变标志
if (this.brush.useGradient !== undefined) {
this.brush.useGradient = true;
}
} else if (this.brush.useGradient !== undefined) {
// 禁用渐变
this.brush.useGradient = false;
}
}
/**
* 设置丝带宽度
* @param {Number} width 宽度值
*/
setRibbonWidth(width) {
this.ribbonWidth = Math.max(5, Math.min(100, width));
if (this.brush && this.brush.ribbonWidth !== undefined) {
this.brush.ribbonWidth = this.ribbonWidth;
}
// 更新渐变(因为宽度变了)
if (this.gradient) {
this.updateGradient();
}
return this.ribbonWidth;
}
/**
* 设置宽度变化率
* @param {Number} variation 变化率(0-1)
*/
setWidthVariation(variation) {
this.widthVariation = Math.max(0, Math.min(1, variation));
if (this.brush && this.brush.widthVariation !== undefined) {
this.brush.widthVariation = this.widthVariation;
}
return this.widthVariation;
}
/**
* 设置丝带平滑度
* @param {Number} smoothness 平滑度值(0-1)
*/
setRibbonSmoothness(smoothness) {
this.ribbonSmoothness = Math.max(0, Math.min(1, smoothness));
if (this.brush && this.brush.ribbonSmoothness !== undefined) {
this.brush.ribbonSmoothness = this.ribbonSmoothness;
}
return this.ribbonSmoothness;
}
/**
* 启用/禁用渐变效果
* @param {Boolean} enabled 是否启用
*/
setGradient(enabled) {
this.gradient = enabled;
this.updateGradient();
return this.gradient;
}
/**
* 设置渐变颜色
* @param {Array} colors 颜色数组
*/
setGradientColors(colors) {
if (Array.isArray(colors) && colors.length >= 2) {
this.gradientColors = colors;
this.updateGradient();
}
return this.gradientColors;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义丝带笔刷特有属性
const ribbonProperties = [
{
id: "ribbonWidth",
name: "飘带宽度",
type: "slider",
defaultValue: this.ribbonWidth,
min: 5,
max: 100,
step: 5,
description: "控制飘带的最大宽度",
category: "飘带设置",
order: 100,
},
{
id: "widthVariation",
name: "宽度变化",
type: "slider",
defaultValue: this.widthVariation,
min: 0,
max: 1,
step: 0.05,
description: "控制飘带宽度的变化程度",
category: "飘带设置",
order: 110,
},
{
id: "ribbonSmoothness",
name: "平滑度",
type: "slider",
defaultValue: this.ribbonSmoothness,
min: 0,
max: 1,
step: 0.05,
description: "控制飘带曲线的平滑程度",
category: "飘带设置",
order: 120,
},
{
id: "gradient",
name: "启用渐变",
type: "checkbox",
defaultValue: this.gradient,
description: "是否启用渐变效果",
category: "飘带设置",
order: 130,
},
{
id: "gradientColor1",
name: "渐变起始颜色",
type: "color",
defaultValue: this.gradientColors[0] || "#000000",
description: "设置渐变的起始颜色",
category: "飘带设置",
order: 140,
visibleWhen: { gradient: true },
},
{
id: "gradientColor2",
name: "渐变结束颜色",
type: "color",
defaultValue: this.gradientColors[1] || "#555555",
description: "设置渐变的结束颜色",
category: "飘带设置",
order: 150,
visibleWhen: { gradient: true },
},
];
// 合并并返回所有属性
return [...baseProperties, ...ribbonProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理丝带笔刷特有属性
if (propId === "ribbonWidth") {
this.setRibbonWidth(value);
return true;
} else if (propId === "widthVariation") {
this.setWidthVariation(value);
return true;
} else if (propId === "ribbonSmoothness") {
this.setRibbonSmoothness(value);
return true;
} else if (propId === "gradient") {
this.setGradient(value);
return true;
} else if (propId === "gradientColor1") {
const colors = [...this.gradientColors];
colors[0] = value;
this.setGradientColors(colors);
return true;
} else if (propId === "gradientColor2") {
const colors = [...this.gradientColors];
colors[1] = value;
this.setGradientColors(colors);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMDAwIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNTU1Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIwIDUwQzMwIDMwIDUwIDMwIDYwIDUwQzcwIDcwIDgwIDcwIDkwIDUwIiBzdHJva2U9InVybCgjZ3JhZCkiIHN0cm9rZS13aWR0aD0iMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==";
}
}

View File

@@ -0,0 +1,362 @@
import { BaseBrush } from "../BaseBrush";
/**
* 阴影笔刷
* 创建带有阴影效果的绘制,有深浅变化
*/
export class ShadedBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "shaded",
name: "阴影笔",
description: "创建带有阴影效果的绘制,适合素描和明暗表现",
category: "绘画笔刷",
icon: "shaded",
...options,
});
// 阴影笔刷特有属性
this.shadowColor = options.shadowColor || "#000000";
this.shadowBlur = options.shadowBlur || 5;
this.shadowOffsetX = options.shadowOffsetX || 2;
this.shadowOffsetY = options.shadowOffsetY || 2;
this.blendMode = options.blendMode || "multiply";
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生阴影笔刷
this.brush = new fabric.ShadedBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 阴影笔刷特有属性
if (options.shadowColor !== undefined) {
this.shadowColor = options.shadowColor;
// 如果原生笔刷支持此属性,则设置
if (brush.shadow) {
brush.shadow.color = this.shadowColor;
} else {
brush.shadow = new fabric.Shadow({
color: this.shadowColor,
blur: this.shadowBlur,
offsetX: this.shadowOffsetX,
offsetY: this.shadowOffsetY,
});
}
}
if (options.shadowBlur !== undefined) {
this.shadowBlur = options.shadowBlur;
// 如果原生笔刷支持此属性,则设置
if (brush.shadow) {
brush.shadow.blur = this.shadowBlur;
} else {
brush.shadow = new fabric.Shadow({
color: this.shadowColor,
blur: this.shadowBlur,
offsetX: this.shadowOffsetX,
offsetY: this.shadowOffsetY,
});
}
}
if (options.shadowOffsetX !== undefined) {
this.shadowOffsetX = options.shadowOffsetX;
// 如果原生笔刷支持此属性,则设置
if (brush.shadow) {
brush.shadow.offsetX = this.shadowOffsetX;
} else {
brush.shadow = new fabric.Shadow({
color: this.shadowColor,
blur: this.shadowBlur,
offsetX: this.shadowOffsetX,
offsetY: this.shadowOffsetY,
});
}
}
if (options.shadowOffsetY !== undefined) {
this.shadowOffsetY = options.shadowOffsetY;
// 如果原生笔刷支持此属性,则设置
if (brush.shadow) {
brush.shadow.offsetY = this.shadowOffsetY;
} else {
brush.shadow = new fabric.Shadow({
color: this.shadowColor,
blur: this.shadowBlur,
offsetX: this.shadowOffsetX,
offsetY: this.shadowOffsetY,
});
}
}
if (options.blendMode !== undefined) {
this.blendMode = options.blendMode;
// 如果原生笔刷支持此属性,则设置
if (brush.globalCompositeOperation !== undefined) {
brush.globalCompositeOperation = this.blendMode;
}
}
}
/**
* 设置阴影颜色
* @param {String} color 颜色值
*/
setShadowColor(color) {
this.shadowColor = color;
if (this.brush && this.brush.shadow) {
this.brush.shadow.color = this.shadowColor;
}
return this.shadowColor;
}
/**
* 设置阴影模糊值
* @param {Number} blur 模糊值
*/
setShadowBlur(blur) {
this.shadowBlur = Math.max(0, Math.min(50, blur));
if (this.brush && this.brush.shadow) {
this.brush.shadow.blur = this.shadowBlur;
}
return this.shadowBlur;
}
/**
* 设置阴影X偏移
* @param {Number} offset X偏移值
*/
setShadowOffsetX(offset) {
this.shadowOffsetX = Math.max(-20, Math.min(20, offset));
if (this.brush && this.brush.shadow) {
this.brush.shadow.offsetX = this.shadowOffsetX;
}
return this.shadowOffsetX;
}
/**
* 设置阴影Y偏移
* @param {Number} offset Y偏移值
*/
setShadowOffsetY(offset) {
this.shadowOffsetY = Math.max(-20, Math.min(20, offset));
if (this.brush && this.brush.shadow) {
this.brush.shadow.offsetY = this.shadowOffsetY;
}
return this.shadowOffsetY;
}
/**
* 设置混合模式
* @param {String} mode 混合模式
*/
setBlendMode(mode) {
const validModes = [
"normal",
"multiply",
"screen",
"overlay",
"darken",
"lighten",
"color-dodge",
"color-burn",
"hard-light",
"soft-light",
"difference",
"exclusion",
"hue",
"saturation",
"color",
"luminosity",
];
if (validModes.includes(mode)) {
this.blendMode = mode;
if (this.brush && this.brush.globalCompositeOperation !== undefined) {
this.brush.globalCompositeOperation = this.blendMode;
}
}
return this.blendMode;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义阴影笔刷特有属性
const shadedProperties = [
{
id: "shadowColor",
name: "阴影颜色",
type: "color",
defaultValue: this.shadowColor,
description: "设置阴影的颜色",
category: "阴影设置",
order: 100,
},
{
id: "shadowBlur",
name: "阴影模糊",
type: "slider",
defaultValue: this.shadowBlur,
min: 0,
max: 50,
step: 1,
description: "控制阴影的模糊程度",
category: "阴影设置",
order: 110,
},
{
id: "shadowOffsetX",
name: "阴影X偏移",
type: "slider",
defaultValue: this.shadowOffsetX,
min: -20,
max: 20,
step: 1,
description: "控制阴影的水平偏移",
category: "阴影设置",
order: 120,
},
{
id: "shadowOffsetY",
name: "阴影Y偏移",
type: "slider",
defaultValue: this.shadowOffsetY,
min: -20,
max: 20,
step: 1,
description: "控制阴影的垂直偏移",
category: "阴影设置",
order: 130,
},
{
id: "blendMode",
name: "混合模式",
type: "select",
defaultValue: this.blendMode,
options: [
{ value: "normal", label: "正常" },
{ value: "multiply", label: "正片叠底" },
{ value: "screen", label: "滤色" },
{ value: "overlay", label: "叠加" },
{ value: "darken", label: "变暗" },
{ value: "lighten", label: "变亮" },
{ value: "color-dodge", label: "颜色减淡" },
{ value: "color-burn", label: "颜色加深" },
{ value: "hard-light", label: "强光" },
{ value: "soft-light", label: "柔光" },
{ value: "difference", label: "差值" },
{ value: "exclusion", label: "排除" },
],
description: "设置阴影的混合模式",
category: "阴影设置",
order: 140,
},
];
// 合并并返回所有属性
return [...baseProperties, ...shadedProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理阴影笔刷特有属性
if (propId === "shadowColor") {
this.setShadowColor(value);
return true;
} else if (propId === "shadowBlur") {
this.setShadowBlur(value);
return true;
} else if (propId === "shadowOffsetX") {
this.setShadowOffsetX(value);
return true;
} else if (propId === "shadowOffsetY") {
this.setShadowOffsetY(value);
return true;
} else if (propId === "blendMode") {
this.setBlendMode(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI0MCIgY3k9IjQwIiByPSIyMCIgZmlsbD0iIzY2NiIvPjxjaXJjbGUgY3g9IjQ1IiBjeT0iNDUiIHI9IjIwIiBmaWxsPSIjMDAwIi8+PHBhdGggZD0iTTIwIDgwQzMwIDYwIDUwIDcwIDcwIDUwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTIzIDgzQzMzIDYzIDUzIDczIDczIDUzIiBzdHJva2U9IiM2NjYiIHN0cm9rZS13aWR0aD0iOCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+";
}
}

View File

@@ -0,0 +1,371 @@
import { BaseBrush } from "../BaseBrush";
/**
* 素描笔刷
* 创建手绘素描效果,有不规则的线条和纹理
*/
export class SketchyBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "sketchy",
name: "素描",
description: "创建手绘素描效果,有不规则的线条和纹理",
category: "绘画笔刷",
icon: "sketchy",
...options,
});
// 素描笔刷特有属性
this.roughness = options.roughness || 0.7;
this.bowing = options.bowing || 0.5;
this.stroke = options.stroke !== undefined ? options.stroke : true;
this.hachureAngle = options.hachureAngle || 60;
this.dashOffset = options.dashOffset || 0;
this.dashArray = options.dashArray || [6, 2];
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生素描笔刷
this.brush = new fabric.SketchyBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 素描笔刷特有属性
if (options.roughness !== undefined) {
this.roughness = options.roughness;
// 如果原生笔刷支持此属性,则设置
if (brush.roughness !== undefined) {
brush.roughness = this.roughness;
}
}
if (options.bowing !== undefined) {
this.bowing = options.bowing;
// 如果原生笔刷支持此属性,则设置
if (brush.bowing !== undefined) {
brush.bowing = this.bowing;
}
}
if (options.stroke !== undefined) {
this.stroke = options.stroke;
// 如果原生笔刷支持此属性,则设置
if (brush.stroke !== undefined) {
brush.stroke = this.stroke;
}
}
if (options.hachureAngle !== undefined) {
this.hachureAngle = options.hachureAngle;
// 如果原生笔刷支持此属性,则设置
if (brush.hachureAngle !== undefined) {
brush.hachureAngle = this.hachureAngle;
}
}
if (options.dashOffset !== undefined) {
this.dashOffset = options.dashOffset;
// 如果原生笔刷支持此属性,则设置
if (brush.dashOffset !== undefined) {
brush.dashOffset = this.dashOffset;
}
}
if (options.dashArray !== undefined) {
this.dashArray = options.dashArray;
// 如果原生笔刷支持此属性,则设置
if (brush.dashArray !== undefined) {
brush.dashArray = this.dashArray;
}
}
// 为笔刷设置手绘效果
const originalOnMouseMove = brush.onMouseMove;
brush.onMouseMove = function (pointer, options) {
// 添加微小随机偏移,模拟手绘效果
const jitter = (this.width / 4) * this.roughness;
pointer.x += (Math.random() - 0.5) * jitter;
pointer.y += (Math.random() - 0.5) * jitter;
// 调用原始方法
if (originalOnMouseMove) {
originalOnMouseMove.call(this, pointer, options);
}
};
}
/**
* 设置粗糙度
* @param {Number} value 粗糙度值(0-1)
*/
setRoughness(value) {
this.roughness = Math.max(0, Math.min(1, value));
if (this.brush && this.brush.roughness !== undefined) {
this.brush.roughness = this.roughness;
}
return this.roughness;
}
/**
* 设置弯曲度
* @param {Number} value 弯曲度值(0-1)
*/
setBowing(value) {
this.bowing = Math.max(0, Math.min(1, value));
if (this.brush && this.brush.bowing !== undefined) {
this.brush.bowing = this.bowing;
}
return this.bowing;
}
/**
* 设置是否描边
* @param {Boolean} value 是否描边
*/
setStroke(value) {
this.stroke = value;
if (this.brush && this.brush.stroke !== undefined) {
this.brush.stroke = this.stroke;
}
return this.stroke;
}
/**
* 设置素描线条角度
* @param {Number} value 角度值(0-180)
*/
setHachureAngle(value) {
this.hachureAngle = Math.max(0, Math.min(180, value));
if (this.brush && this.brush.hachureAngle !== undefined) {
this.brush.hachureAngle = this.hachureAngle;
}
return this.hachureAngle;
}
/**
* 设置虚线偏移量
* @param {Number} value 偏移量
*/
setDashOffset(value) {
this.dashOffset = value;
if (this.brush && this.brush.dashOffset !== undefined) {
this.brush.dashOffset = this.dashOffset;
}
return this.dashOffset;
}
/**
* 设置虚线数组
* @param {Array} value 虚线数组[线长, 间隔]
*/
setDashArray(value) {
if (Array.isArray(value) && value.length >= 2) {
this.dashArray = value;
if (this.brush && this.brush.dashArray !== undefined) {
this.brush.dashArray = this.dashArray;
}
}
return this.dashArray;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义素描笔刷特有属性
const sketchyProperties = [
{
id: "roughness",
name: "粗糙度",
type: "slider",
defaultValue: this.roughness,
min: 0,
max: 1,
step: 0.05,
description: "控制素描线条的粗糙程度",
category: "素描设置",
order: 100,
},
{
id: "bowing",
name: "弯曲度",
type: "slider",
defaultValue: this.bowing,
min: 0,
max: 1,
step: 0.05,
description: "控制素描线条的弯曲程度",
category: "素描设置",
order: 110,
},
{
id: "stroke",
name: "描边",
type: "checkbox",
defaultValue: this.stroke,
description: "是否使用描边",
category: "素描设置",
order: 120,
},
{
id: "hachureAngle",
name: "线条角度",
type: "slider",
defaultValue: this.hachureAngle,
min: 0,
max: 180,
step: 5,
description: "控制素描线条的角度",
category: "素描设置",
order: 130,
},
{
id: "dashOffset",
name: "虚线偏移",
type: "slider",
defaultValue: this.dashOffset,
min: 0,
max: 10,
step: 1,
description: "控制虚线的偏移量",
category: "素描设置",
order: 140,
},
{
id: "dashArray",
name: "虚线模式",
type: "select",
defaultValue: JSON.stringify(this.dashArray),
options: [
{ value: JSON.stringify([0]), label: "实线" },
{ value: JSON.stringify([6, 2]), label: "短虚线" },
{ value: JSON.stringify([10, 5]), label: "长虚线" },
{ value: JSON.stringify([2, 2]), label: "点线" },
{ value: JSON.stringify([10, 5, 2, 5]), label: "点划线" },
],
description: "设置虚线的模式",
category: "素描设置",
order: 150,
parseValue: (value) => JSON.parse(value),
},
];
// 合并并返回所有属性
return [...baseProperties, ...sketchyProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理素描笔刷特有属性
if (propId === "roughness") {
this.setRoughness(value);
return true;
} else if (propId === "bowing") {
this.setBowing(value);
return true;
} else if (propId === "stroke") {
this.setStroke(value);
return true;
} else if (propId === "hachureAngle") {
this.setHachureAngle(value);
return true;
} else if (propId === "dashOffset") {
this.setDashOffset(value);
return true;
} else if (propId === "dashArray") {
let parsedValue = value;
if (typeof value === "string") {
try {
parsedValue = JSON.parse(value);
} catch (e) {
console.error("Invalid dashArray value:", e);
return false;
}
}
this.setDashArray(parsedValue);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMjMgMzBDMjUgMjggNTIgMzggNzUgMzciIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjIgNDBDMjIgMzggNTkgNDYgNzYgNDMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjAgNTBDMjIgNDggNTYgNTYgNzYgNTIiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNMjQgNjBDMjMgNTggNDYgNjQgNzUgNjQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyLjgiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yNiA3MkMyNyA2OSA0OSA3NCA3NSA3MiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjIuNCIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+";
}
}

View File

@@ -0,0 +1,316 @@
import { BaseBrush } from "../BaseBrush";
/**
* 喷漆笔刷
* 创建喷漆效果,点状分散的绘制风格
*/
export class SpraypaintBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "spraypaint",
name: "喷漆笔刷",
description: "创建喷漆效果,点状分散的绘制风格",
category: "绘画笔刷",
icon: "spraypaint",
...options,
});
// 喷漆笔刷特有属性
this.density = options.density || 20;
this.sprayRadius = options.sprayRadius || 10;
this.randomOpacity =
options.randomOpacity !== undefined ? options.randomOpacity : true;
this.dotSize = options.dotSize || 1;
this.dotShape = options.dotShape || "circle";
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生喷漆笔刷
this.brush = new fabric.SprayBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 设置基本属性
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 喷漆笔刷特有属性
if (options.density !== undefined) {
this.density = options.density;
// 如果原生笔刷支持此属性,则设置
if (brush.density !== undefined) {
brush.density = this.density;
}
}
if (options.sprayRadius !== undefined) {
this.sprayRadius = options.sprayRadius;
// 如果原生笔刷支持此属性,则设置
if (brush.sprayWidth !== undefined) {
brush.sprayWidth = this.sprayRadius;
} else if (brush.width !== undefined) {
brush.width = this.sprayRadius;
}
}
if (options.randomOpacity !== undefined) {
this.randomOpacity = options.randomOpacity;
// 如果原生笔刷支持此属性,则设置
if (brush.randomOpacity !== undefined) {
brush.randomOpacity = this.randomOpacity;
}
}
if (options.dotSize !== undefined) {
this.dotSize = options.dotSize;
// 如果原生笔刷支持此属性,则设置
if (brush.dotWidth !== undefined) {
brush.dotWidth = this.dotSize;
}
}
if (options.dotShape !== undefined) {
this.dotShape = options.dotShape;
// 如果原生笔刷支持此属性,则设置
if (brush.dotShape !== undefined) {
brush.dotShape = this.dotShape;
}
}
}
/**
* 设置喷漆密度
* @param {Number} value 密度值
*/
setDensity(value) {
this.density = Math.max(1, Math.min(100, value));
if (this.brush && this.brush.density !== undefined) {
this.brush.density = this.density;
}
return this.density;
}
/**
* 设置喷漆半径
* @param {Number} value 半径值
*/
setSprayRadius(value) {
this.sprayRadius = Math.max(1, value);
if (this.brush) {
if (this.brush.sprayWidth !== undefined) {
this.brush.sprayWidth = this.sprayRadius;
} else if (this.brush.width !== undefined) {
this.brush.width = this.sprayRadius;
}
}
return this.sprayRadius;
}
/**
* 设置是否随机透明度
* @param {Boolean} value 是否随机透明度
*/
setRandomOpacity(value) {
this.randomOpacity = value;
if (this.brush && this.brush.randomOpacity !== undefined) {
this.brush.randomOpacity = this.randomOpacity;
}
return this.randomOpacity;
}
/**
* 设置点大小
* @param {Number} value 点大小
*/
setDotSize(value) {
this.dotSize = Math.max(0.1, value);
if (this.brush && this.brush.dotWidth !== undefined) {
this.brush.dotWidth = this.dotSize;
}
return this.dotSize;
}
/**
* 设置点形状
* @param {String} value 点形状,如 'circle', 'square', 'diamond'
*/
setDotShape(value) {
const validShapes = ["circle", "square", "diamond", "random"];
if (validShapes.includes(value)) {
this.dotShape = value;
if (this.brush && this.brush.dotShape !== undefined) {
this.brush.dotShape = this.dotShape;
}
}
return this.dotShape;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义喷漆笔刷特有属性
const spraypaintProperties = [
{
id: "density",
name: "喷漆密度",
type: "slider",
defaultValue: this.density,
min: 1,
max: 100,
step: 1,
description: "控制喷漆点的密度",
category: "喷漆设置",
order: 100,
},
{
id: "sprayRadius",
name: "喷漆半径",
type: "slider",
defaultValue: this.sprayRadius,
min: 1,
max: 50,
step: 1,
description: "控制喷漆的覆盖半径",
category: "喷漆设置",
order: 110,
},
{
id: "randomOpacity",
name: "随机透明度",
type: "checkbox",
defaultValue: this.randomOpacity,
description: "使喷漆点有随机透明度",
category: "喷漆设置",
order: 120,
},
{
id: "dotSize",
name: "点大小",
type: "slider",
defaultValue: this.dotSize,
min: 0.1,
max: 10,
step: 0.1,
description: "控制喷漆点的大小",
category: "喷漆设置",
order: 130,
},
{
id: "dotShape",
name: "点形状",
type: "select",
defaultValue: this.dotShape,
options: [
{ value: "circle", label: "圆形" },
{ value: "square", label: "方形" },
{ value: "diamond", label: "菱形" },
{ value: "random", label: "随机" },
],
description: "设置喷漆点的形状",
category: "喷漆设置",
order: 140,
},
];
// 合并并返回所有属性
return [...baseProperties, ...spraypaintProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理喷漆笔刷特有属性
if (propId === "density") {
this.setDensity(value);
return true;
} else if (propId === "sprayRadius") {
this.setSprayRadius(value);
return true;
} else if (propId === "randomOpacity") {
this.setRandomOpacity(value);
return true;
} else if (propId === "dotSize") {
this.setDotSize(value);
return true;
} else if (propId === "dotShape") {
this.setDotShape(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1NSIgY3k9IjUwIiByPSIyMCIgZmlsbD0icmdiYSgwLDAsMCwwLjEpIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMC41Ii8+PGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iMSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU1IiBjeT0iNTUiIHI9IjAuOCIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjYwIiBjeT0iNDUiIHI9IjEuMiIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjQ1IiBjeT0iNTUiIHI9IjAuNyIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjQ3IiBjeT0iNDgiIHI9IjAuOSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU4IiBjeT0iNTMiIHI9IjEuMSIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjYyIiBjeT0iNTYiIHI9IjAuNiIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjUyIiBjeT0iNTgiIHI9IjAuOCIgZmlsbD0iIzAwMCIvPjxjaXJjbGUgY3g9IjU0IiBjeT0iNDMiIHI9IjEiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0OSIgY3k9IjQzIiByPSIwLjYiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0MyIgY3k9IjQ3IiByPSIwLjciIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2NSIgY3k9IjQ4IiByPSIwLjkiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2MiIgY3k9IjQxIiByPSIwLjUiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0OSIgY3k9IjYxIiByPSIwLjgiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2NiIgY3k9IjUyIiByPSIwLjciIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0MSIgY3k9IjUxIiByPSIwLjYiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI2OCIgY3k9IjU3IiByPSIwLjQiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI0NSIgY3k9IjQwIiByPSIwLjUiIGZpbGw9IiMwMDAiLz48Y2lyY2xlIGN4PSI1NyIgY3k9IjYxIiByPSIwLjciIGZpbGw9IiMwMDAiLz48L3N2Zz4=";
}
}

View File

@@ -0,0 +1,855 @@
import { BaseBrush } from "../BaseBrush";
//import { fabric } from "fabric-with-all";
import texturePresetManager from "../TexturePresetManager";
/**
* 纹理笔刷
* 使用图像纹理进行绘制的笔刷
*/
export class TextureBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "texture",
name: "纹理笔刷",
description: "使用图像纹理进行绘制的笔刷",
category: "特效笔刷",
icon: "texture",
...options,
});
// 纹理笔刷特有属性
this.textureSource = options.textureSource || null;
this.textureRepeat = options.textureRepeat || "repeat";
this.textureScale = options.textureScale || 1;
this.textureAngle = options.textureAngle || 0;
this.textureOpacity =
options.textureOpacity !== undefined ? options.textureOpacity : 1;
// 预设材质相关
this.selectedTextureId = options.selectedTextureId || null;
this.texturePresets = [];
// 加载预设材质
this._loadTexturePresets();
// 当前选中的材质索引
this.currentTextureIndex = options.currentTextureIndex || 0;
// 从预设管理器加载自定义材质
texturePresetManager.loadCustomTexturesFromStorage();
}
/**
* 加载材质预设
* @private
*/
_loadTexturePresets() {
// 从预设管理器获取所有材质
this.texturePresets = texturePresetManager.getAllTextures();
// 如果没有选中的材质ID使用第一个预设材质
if (!this.selectedTextureId && this.texturePresets.length > 0) {
this.selectedTextureId = this.texturePresets[0].id;
this.currentTextureIndex = 0;
}
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生纹理笔刷
this.brush = new fabric.PatternBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
// 如果有选中的材质,则设置纹理
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 设置基本属性
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 纹理笔刷特有属性
if (options.textureRepeat !== undefined) {
this.textureRepeat = options.textureRepeat;
// 需要重新应用纹理以应用重复模式
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
}
if (options.textureScale !== undefined) {
this.textureScale = options.textureScale;
// 需要重新应用纹理以应用缩放
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
}
if (options.textureAngle !== undefined) {
this.textureAngle = options.textureAngle;
// 需要重新应用纹理以应用旋转角度
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
}
if (options.textureOpacity !== undefined) {
this.textureOpacity = options.textureOpacity;
// 需要重新应用纹理以应用透明度
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
}
}
/**
* 根据材质ID设置纹理
* @param {String} textureId 材质ID
* @returns {Promise} 加载完成的Promise
*/
setTextureById(textureId) {
const texture = texturePresetManager.getTextureById(textureId);
if (!texture) {
return Promise.reject(new Error(`材质 ${textureId} 不存在`));
}
this.selectedTextureId = textureId;
// 更新当前材质索引
const allTextures = texturePresetManager.getAllTextures();
this.currentTextureIndex = allTextures.findIndex((t) => t.id === textureId);
return this.setTexture(texture.path);
}
/**
* 设置纹理
* @param {String|Object} source 纹理源URL或Image对象
* @returns {Promise} 加载完成的Promise
*/
setTexture(source) {
this.textureSource = source;
if (!this.brush) {
return Promise.reject(new Error("笔刷实例不存在"));
}
return new Promise((resolve, reject) => {
if (typeof source === "string") {
// 如果是URL加载图像
fabric.util.loadImage(source, (img) => {
if (!img) {
reject(new Error("纹理加载失败"));
return;
}
this._applyTextureToPatternBrush(img);
resolve(img);
});
} else if (
source instanceof Image ||
source instanceof HTMLCanvasElement
) {
// 如果已经是Image或Canvas对象直接使用
this._applyTextureToPatternBrush(source);
resolve(source);
} else {
reject(new Error("无效的纹理源"));
}
});
}
/**
* 将纹理应用到PatternBrush
* @param {Object} img 图像对象
* @private
*/
_applyTextureToPatternBrush(img) {
if (!this.brush || !img) return;
// 创建Canvas来处理纹理
const canvasTexture = document.createElement("canvas");
const ctx = canvasTexture.getContext("2d");
// 根据缩放设置Canvas大小
const width = img.width * this.textureScale;
const height = img.height * this.textureScale;
canvasTexture.width = width;
canvasTexture.height = height;
// 绘制前应用旋转
if (this.textureAngle !== 0) {
ctx.save();
ctx.translate(width / 2, height / 2);
ctx.rotate((this.textureAngle * Math.PI) / 180);
ctx.translate(-width / 2, -height / 2);
ctx.drawImage(img, 0, 0, width, height);
ctx.restore();
} else {
ctx.drawImage(img, 0, 0, width, height);
}
// 应用透明度
if (this.textureOpacity < 1) {
ctx.globalAlpha = this.textureOpacity;
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
}
// 创建Pattern对象
const pattern = new fabric.Pattern({
source: canvasTexture,
repeat: this.textureRepeat,
});
// 设置笔刷源纹理
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;
}
}
/**
* 设置纹理重复模式
* @param {String} mode 重复模式:'repeat', 'repeat-x', 'repeat-y', 'no-repeat'
* @returns {String} 设置后的重复模式
*/
setTextureRepeat(mode) {
const validModes = ["repeat", "repeat-x", "repeat-y", "no-repeat"];
if (validModes.includes(mode)) {
this.textureRepeat = mode;
// 重新应用纹理以更新重复模式
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
}
return this.textureRepeat;
}
/**
* 设置纹理缩放比例
* @param {Number} scale 缩放比例
* @returns {Number} 设置后的缩放比例
*/
setTextureScale(scale) {
this.textureScale = Math.max(0.1, scale);
// 重新应用纹理以更新缩放
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
return this.textureScale;
}
/**
* 设置纹理旋转角度
* @param {Number} angle 旋转角度(度)
* @returns {Number} 设置后的旋转角度
*/
setTextureAngle(angle) {
this.textureAngle = angle % 360;
// 重新应用纹理以更新旋转角度
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
return this.textureAngle;
}
/**
* 设置纹理透明度
* @param {Number} opacity 透明度
* @returns {Number} 设置后的透明度
*/
setTextureOpacity(opacity) {
this.textureOpacity = Math.min(1, Math.max(0, opacity));
// 重新应用纹理以更新透明度
if (this.selectedTextureId) {
this.setTextureById(this.selectedTextureId);
} else if (this.textureSource) {
this.setTexture(this.textureSource);
}
return this.textureOpacity;
}
/**
* 切换到下一个预设材质
* @returns {Promise} 切换完成的Promise
*/
nextTexture() {
const textures = texturePresetManager.getAllTextures();
if (textures.length === 0) return Promise.resolve();
this.currentTextureIndex = (this.currentTextureIndex + 1) % textures.length;
const nextTexture = textures[this.currentTextureIndex];
return this.setTextureById(nextTexture.id);
}
/**
* 切换到上一个预设材质
* @returns {Promise} 切换完成的Promise
*/
previousTexture() {
const textures = texturePresetManager.getAllTextures();
if (textures.length === 0) return Promise.resolve();
this.currentTextureIndex =
this.currentTextureIndex === 0
? textures.length - 1
: this.currentTextureIndex - 1;
const prevTexture = textures[this.currentTextureIndex];
return this.setTextureById(prevTexture.id);
}
/**
* 使用索引切换纹理
* @param {Number} index 纹理索引
*/
switchTexture(index) {
const textures = texturePresetManager.getAllTextures();
if (index >= 0 && index < textures.length) {
this.currentTextureIndex = index;
const texture = textures[index];
return this.setTextureById(texture.id);
}
return Promise.reject(new Error("无效的纹理索引"));
}
/**
* 获取当前选中的材质信息
* @returns {Object|null} 材质信息
*/
getCurrentTexture() {
if (this.selectedTextureId) {
return texturePresetManager.getTextureById(this.selectedTextureId);
}
return null;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 获取所有可用材质
const allTextures = texturePresetManager.getAllTextures();
const textureOptions = allTextures.map((texture, index) => ({
value: texture.id,
label: texture.name,
preview: texturePresetManager.getTexturePreviewUrl(texture),
category: texture.category,
}));
// 定义纹理笔刷特有属性
const textureProperties = [
{
id: "textureSelector",
name: "材质选择",
type: "texture-grid",
defaultValue: this.selectedTextureId,
options: textureOptions,
description: "选择要使用的纹理",
category: "纹理设置",
order: 100,
hidden: allTextures.length === 0,
},
{
id: "textureRepeat",
name: "纹理重复模式",
type: "select",
defaultValue: this.textureRepeat,
options: [
{ value: "repeat", label: "双向重复" },
{ value: "repeat-x", label: "水平重复" },
{ value: "repeat-y", label: "垂直重复" },
{ value: "no-repeat", label: "不重复" },
],
description: "设置纹理的重复模式",
category: "纹理设置",
order: 110,
},
{
id: "textureScale",
name: "纹理缩放",
type: "slider",
defaultValue: this.textureScale,
min: 0.1,
max: 5,
step: 0.1,
description: "调整纹理的缩放比例",
category: "纹理设置",
order: 120,
},
{
id: "textureAngle",
name: "纹理旋转",
type: "slider",
defaultValue: this.textureAngle,
min: 0,
max: 360,
step: 5,
description: "调整纹理的旋转角度",
category: "纹理设置",
order: 130,
},
{
id: "textureOpacity",
name: "纹理透明度",
type: "slider",
defaultValue: this.textureOpacity,
min: 0,
max: 1,
step: 0.05,
description: "调整纹理的透明度",
category: "纹理设置",
order: 140,
},
{
id: "uploadTexture",
name: "上传纹理",
type: "button",
action: "uploadTexture",
description: "上传自定义纹理",
category: "纹理设置",
order: 150,
},
{
id: "texturePreview",
name: "纹理预览",
type: "preview",
description: "当前纹理预览",
category: "纹理设置",
order: 160,
getValue: () => {
const currentTexture = this.getCurrentTexture();
return currentTexture
? texturePresetManager.getTexturePreviewUrl(currentTexture)
: null;
},
},
];
// 合并并返回所有属性
return [...baseProperties, ...textureProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理纹理笔刷特有属性
if (propId === "textureSelector") {
this.setTextureById(value);
return true;
} else if (propId === "textureRepeat") {
this.setTextureRepeat(value);
return true;
} else if (propId === "textureScale") {
this.setTextureScale(value);
return true;
} else if (propId === "textureAngle") {
this.setTextureAngle(value);
return true;
} else if (propId === "textureOpacity") {
this.setTextureOpacity(value);
return true;
} else if (propId === "uploadTexture") {
// 触发上传纹理事件
// 这里通常由外部处理返回true表示属性被处理
return true;
}
return false;
}
/**
* 添加自定义材质
* @param {Object} textureData 材质数据
* @returns {String} 材质ID
*/
addCustomTexture(textureData) {
const textureId = texturePresetManager.addCustomTexture(textureData);
// 重新加载材质预设
this._loadTexturePresets();
// 保存到本地存储
texturePresetManager.saveCustomTexturesToStorage();
return textureId;
}
/**
* 删除自定义材质
* @param {String} textureId 材质ID
* @returns {Boolean} 是否删除成功
*/
removeCustomTexture(textureId) {
const success = texturePresetManager.removeCustomTexture(textureId);
if (success) {
// 如果删除的是当前选中的材质,切换到第一个可用材质
if (this.selectedTextureId === textureId) {
const allTextures = texturePresetManager.getAllTextures();
if (allTextures.length > 0) {
this.setTextureById(allTextures[0].id);
} else {
this.selectedTextureId = null;
this.currentTextureIndex = 0;
}
}
// 重新加载材质预设
this._loadTexturePresets();
// 保存到本地存储
texturePresetManager.saveCustomTexturesToStorage();
}
return success;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
const currentTexture = this.getCurrentTexture();
if (currentTexture) {
return texturePresetManager.getTexturePreviewUrl(currentTexture);
}
// 返回默认纹理预览
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48cGF0dGVybiBpZD0icGF0dGVybiIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIj48cmVjdCB3aWR0aD0iNSIgaGVpZ2h0PSI1IiBmaWxsPSIjZGRkIi8+PHJlY3QgeD0iNSIgeT0iNSIgd2lkdGg9IjUiIGhlaWdodD0iNSIgZmlsbD0iI2RkZCIvPjwvcGF0dGVybj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9InVybCgjcGF0dGVybikiLz48L3N2Zz4=";
}
/**
* 笔刷被选中时调用
* @override
*/
onSelected() {
// 重新加载材质预设(可能有新的自定义材质)
this._loadTexturePresets();
}
/**
* 销毁笔刷实例并清理资源
* @override
*/
destroy() {
super.destroy();
this.textureSource = null;
this.selectedTextureId = null;
this.texturePresets = [];
}
/**
* 设置材质属性
* @param {String} property 属性名称
* @param {any} value 属性值
* @returns {Boolean} 是否设置成功
*/
setTextureProperty(property, value) {
switch (property) {
case "scale":
return this.setTextureScale(value);
case "opacity":
return this.setTextureOpacity(value);
case "repeat":
return this.setTextureRepeat(value);
case "angle":
return this.setTextureAngle(value);
default:
return false;
}
}
/**
* 获取材质属性
* @param {String} property 属性名称
* @returns {any} 属性值
*/
getTextureProperty(property) {
switch (property) {
case "scale":
return this.textureScale;
case "opacity":
return this.textureOpacity;
case "repeat":
return this.textureRepeat;
case "angle":
return this.textureAngle;
case "textureId":
return this.selectedTextureId;
default:
return undefined;
}
}
/**
* 应用材质预设
* @param {String|Object} preset 预设ID或预设对象
* @returns {Boolean} 是否应用成功
*/
applyTexturePreset(preset) {
let presetData = null;
if (typeof preset === "string") {
// 如果是预设ID从预设管理器获取
presetData = texturePresetManager.applyTexturePreset(preset);
} else if (typeof preset === "object") {
// 如果是预设对象,直接使用
presetData = preset;
}
if (!presetData) {
console.warn("无效的材质预设:", preset);
return false;
}
// 应用预设设置
if (presetData.textureId) {
this.setTextureById(presetData.textureId);
}
if (presetData.scale !== undefined) {
this.setTextureScale(presetData.scale);
}
if (presetData.opacity !== undefined) {
this.setTextureOpacity(presetData.opacity);
}
if (presetData.repeat !== undefined) {
this.setTextureRepeat(presetData.repeat);
}
if (presetData.angle !== undefined) {
this.setTextureAngle(presetData.angle);
}
// 如果预设包含笔刷属性,也一并应用
if (presetData.brushSize !== undefined && this.brush) {
this.brush.width = presetData.brushSize;
}
if (presetData.brushOpacity !== undefined && this.brush) {
this.brush.opacity = presetData.brushOpacity;
}
if (presetData.brushColor !== undefined && this.brush) {
this.brush.color = presetData.brushColor;
}
return true;
}
/**
* 获取当前材质状态
* @returns {Object} 当前材质状态
*/
getCurrentTextureState() {
return {
textureId: this.selectedTextureId,
scale: this.textureScale,
opacity: this.textureOpacity,
repeat: this.textureRepeat,
angle: this.textureAngle,
// 包含笔刷状态
brushSize: this.brush ? this.brush.width : this.options.width,
brushOpacity: this.brush ? this.brush.opacity : this.options.opacity,
brushColor: this.brush ? this.brush.color : this.options.color,
};
}
/**
* 恢复材质状态
* @param {Object} state 要恢复的状态
* @returns {Boolean} 是否恢复成功
*/
restoreTextureState(state) {
if (!state) return false;
try {
// 恢复材质属性
if (state.textureId) {
this.setTextureById(state.textureId);
}
if (state.scale !== undefined) {
this.setTextureScale(state.scale);
}
if (state.opacity !== undefined) {
this.setTextureOpacity(state.opacity);
}
if (state.repeat !== undefined) {
this.setTextureRepeat(state.repeat);
}
if (state.angle !== undefined) {
this.setTextureAngle(state.angle);
}
// 恢复笔刷属性
if (this.brush) {
if (state.brushSize !== undefined) {
this.brush.width = state.brushSize;
}
if (state.brushOpacity !== undefined) {
this.brush.opacity = state.brushOpacity;
}
if (state.brushColor !== undefined) {
this.brush.color = state.brushColor;
}
}
return true;
} catch (error) {
console.error("恢复材质状态失败:", error);
return false;
}
}
/**
* 创建材质预设
* @param {String} name 预设名称
* @returns {String} 预设ID
*/
createTexturePreset(name) {
const currentState = this.getCurrentTextureState();
return texturePresetManager.createTexturePreset(name, currentState);
}
/**
* 获取可用的材质分类
* @returns {Array} 分类数组
*/
getTextureCategories() {
return texturePresetManager.getCategories();
}
/**
* 根据分类获取材质
* @param {String} category 分类名称
* @returns {Array} 材质数组
*/
getTexturesByCategory(category) {
return texturePresetManager.getTexturesByCategory(category);
}
/**
* 搜索材质
* @param {String} query 搜索关键词
* @returns {Array} 匹配的材质数组
*/
searchTextures(query) {
return texturePresetManager.searchTextures(query);
}
/**
* 预加载材质图像
* @param {String} textureId 材质ID
* @returns {Promise<HTMLImageElement>} 图像对象
*/
preloadTexture(textureId) {
return texturePresetManager.loadTextureImage(textureId);
}
/**
* 批量预加载材质
* @param {Array} textureIds 材质ID数组
* @returns {Promise<Array>} 加载结果数组
*/
preloadTextures(textureIds) {
const loadPromises = textureIds.map((id) =>
this.preloadTexture(id).catch((error) => ({ id, error }))
);
return Promise.all(loadPromises);
}
/**
* 获取材质统计信息
* @returns {Object} 统计信息
*/
getTextureStats() {
return texturePresetManager.getStats();
}
}

View File

@@ -0,0 +1,266 @@
import { BaseBrush } from "../BaseBrush";
/**
* 书法笔刷
* 模拟中国传统书法效果,具有笔锋和墨色变化
*/
export class WritingBrush extends BaseBrush {
/**
* 构造函数
* @param {Object} canvas fabric画布实例
* @param {Object} options 配置选项
*/
constructor(canvas, options = {}) {
super(canvas, {
id: "writing",
name: "书法笔",
description: "模拟中国传统书法毛笔效果,具有笔锋和墨色变化",
category: "特效笔刷",
icon: "writing",
...options,
});
// 书法笔刷特有属性
this.brushPressure = options.brushPressure || 0.7;
this.inkAmount = options.inkAmount || 20;
this.brushTaperFactor = options.brushTaperFactor || 0.6;
this.enableInkDripping =
options.enableInkDripping !== undefined
? options.enableInkDripping
: true;
}
/**
* 创建笔刷实例
* @returns {Object} fabric笔刷实例
*/
create() {
if (!this.canvas) {
throw new Error("画布实例不存在");
}
// 创建fabric原生书法笔刷
this.brush = new fabric.WritingBrush(this.canvas);
// 配置笔刷
this.configure(this.brush, this.options);
return this.brush;
}
/**
* 配置笔刷
* @param {Object} brush fabric笔刷实例
* @param {Object} options 配置选项
*/
configure(brush, options = {}) {
if (!brush) return;
// 基础属性配置
if (options.width !== undefined) {
brush.width = options.width;
}
if (options.color !== undefined) {
brush.color = options.color;
}
if (options.opacity !== undefined) {
brush.opacity = options.opacity;
}
// 书法笔刷特有属性
if (options.brushPressure !== undefined) {
this.brushPressure = options.brushPressure;
// 如果原生笔刷支持此属性,则设置
if (brush.brushPressure !== undefined) {
brush.brushPressure = this.brushPressure;
}
}
if (options.inkAmount !== undefined) {
this.inkAmount = options.inkAmount;
// 如果原生笔刷支持此属性,则设置
if (brush.inkAmount !== undefined) {
brush.inkAmount = this.inkAmount;
}
}
if (options.brushTaperFactor !== undefined) {
this.brushTaperFactor = options.brushTaperFactor;
// 如果原生笔刷支持此属性,则设置
if (brush.brushTaperFactor !== undefined) {
brush.brushTaperFactor = this.brushTaperFactor;
}
}
if (options.enableInkDripping !== undefined) {
this.enableInkDripping = options.enableInkDripping;
// 如果原生笔刷支持此属性,则设置
if (brush.enableInkDripping !== undefined) {
brush.enableInkDripping = this.enableInkDripping;
}
}
}
/**
* 设置笔压感应
* @param {Number} pressure 笔压值(0-1)
*/
setBrushPressure(pressure) {
this.brushPressure = Math.max(0.1, Math.min(1, pressure));
if (this.brush && this.brush.brushPressure !== undefined) {
this.brush.brushPressure = this.brushPressure;
}
return this.brushPressure;
}
/**
* 设置墨量
* @param {Number} amount 墨量值
*/
setInkAmount(amount) {
this.inkAmount = Math.max(1, Math.min(50, amount));
if (this.brush && this.brush.inkAmount !== undefined) {
this.brush.inkAmount = this.inkAmount;
}
return this.inkAmount;
}
/**
* 设置笔锋系数
* @param {Number} factor 笔锋系数(0-1)
*/
setBrushTaperFactor(factor) {
this.brushTaperFactor = Math.max(0, Math.min(1, factor));
if (this.brush && this.brush.brushTaperFactor !== undefined) {
this.brush.brushTaperFactor = this.brushTaperFactor;
}
return this.brushTaperFactor;
}
/**
* 启用/禁用墨滴效果
* @param {Boolean} enabled 是否启用
*/
setInkDripping(enabled) {
this.enableInkDripping = enabled;
if (this.brush && this.brush.enableInkDripping !== undefined) {
this.brush.enableInkDripping = this.enableInkDripping;
}
return this.enableInkDripping;
}
/**
* 获取笔刷可配置属性
* @returns {Array} 可配置属性描述数组
* @override
*/
getConfigurableProperties() {
// 获取基础属性
const baseProperties = super.getConfigurableProperties();
// 定义书法笔刷特有属性
const writingProperties = [
{
id: "brushPressure",
name: "笔压感应",
type: "slider",
defaultValue: this.brushPressure,
min: 0.1,
max: 1,
step: 0.05,
description: "控制笔触的力度感应",
category: "书法设置",
order: 100,
},
{
id: "inkAmount",
name: "墨量",
type: "slider",
defaultValue: this.inkAmount,
min: 1,
max: 50,
step: 1,
description: "控制笔触中的墨水量",
category: "书法设置",
order: 110,
},
{
id: "brushTaperFactor",
name: "笔锋系数",
type: "slider",
defaultValue: this.brushTaperFactor,
min: 0,
max: 1,
step: 0.05,
description: "控制笔锋的尖锐程度",
category: "书法设置",
order: 120,
},
{
id: "enableInkDripping",
name: "墨滴效果",
type: "checkbox",
defaultValue: this.enableInkDripping,
description: "是否启用墨滴效果",
category: "书法设置",
order: 130,
},
];
// 合并并返回所有属性
return [...baseProperties, ...writingProperties];
}
/**
* 更新笔刷属性
* @param {String} propId 属性ID
* @param {any} value 属性值
* @returns {Boolean} 是否更新成功
* @override
*/
updateProperty(propId, value) {
// 先检查基类能否处理此属性
if (super.updateProperty(propId, value)) {
return true;
}
// 处理书法笔刷特有属性
if (propId === "brushPressure") {
this.setBrushPressure(value);
return true;
} else if (propId === "inkAmount") {
this.setInkAmount(value);
return true;
} else if (propId === "brushTaperFactor") {
this.setBrushTaperFactor(value);
return true;
} else if (propId === "enableInkDripping") {
this.setInkDripping(value);
return true;
}
return false;
}
/**
* 获取预览图
* @returns {String} 预览图URL
*/
getPreview() {
return "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBkPSJNMzAgMzBDNTAgMzAgNjAgNzAgODAgNzAiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTc1IDYwQzc4IDcwIDg1IDY1IDkwIDcwIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+PC9zdmc+";
}
}

View File

@@ -0,0 +1,433 @@
import { CompositeCommand } from "../../commands/Command.js";
import { PerformanceManager } from "./PerformanceManager.js";
/**
* 简化版命令管理器
* 基于经典撤销/重做模式,支持命令队列
* 使用复合命令替代事务处理
*/
export class CommandManager {
constructor(options = {}) {
this.undoStack = [];
this.redoStack = [];
this.maxHistorySize = options.maxHistorySize || 50;
this.executing = false;
// 命令执行队列
this.commandQueue = [];
this.processing = false;
// 可选的性能管理器
this.performanceManager = options.performanceManager || null;
// 状态变化回调
this.onStateChange = null;
}
// 兼容旧的executeCommand方法
async executeCommand(command) {
return this.execute(command);
}
/**
* 执行命令并添加到撤销栈
*/
async execute(command) {
if (!command || typeof command.execute !== "function") {
throw new Error("无效的命令对象");
}
return this._executeDirectly(command);
}
/**
* 直接执行命令(绕过事务检查)
* @private
*/
async _executeDirectly(command) {
// 返回Promise等待命令执行完成
return new Promise((resolve, reject) => {
// 将命令添加到队列
this.commandQueue.push({
type: "execute",
command,
resolve,
reject,
});
// 开始处理队列
this._processQueue();
});
}
/**
* 撤销最后一个命令
*/
async undo() {
return new Promise((resolve, reject) => {
// 检查是否可以撤销
if (this.undoStack.length === 0) {
console.warn("无法撤销:撤销栈为空");
resolve(null);
return;
}
// 将撤销操作添加到队列
this.commandQueue.push({
type: "undo",
resolve,
reject,
});
// 开始处理队列
this._processQueue();
});
}
/**
* 重做最后一个撤销的命令
*/
async redo() {
return new Promise((resolve, reject) => {
// 检查是否可以重做
if (this.redoStack.length === 0) {
console.warn("无法重做:重做栈为空");
resolve(null);
return;
}
// 将重做操作添加到队列
this.commandQueue.push({
type: "redo",
resolve,
reject,
});
// 开始处理队列
this._processQueue();
});
}
/**
* 处理命令队列
* @private
*/
async _processQueue() {
// 如果正在处理或队列为空,直接返回
if (this.processing || this.commandQueue.length === 0) {
return;
}
this.processing = true;
try {
while (this.commandQueue.length > 0) {
const task = this.commandQueue.shift();
try {
let result = null;
switch (task.type) {
case "execute":
result = await this._executeCommandInternal(task.command);
break;
case "undo":
result = await this._undoInternal();
break;
case "redo":
result = await this._redoInternal();
break;
default:
throw new Error(`未知的任务类型: ${task.type}`);
}
task.resolve(result);
} catch (error) {
task.reject(error);
}
}
} finally {
this.processing = false;
}
}
/**
* 内部执行命令方法
* @private
*/
async _executeCommandInternal(command) {
this.executing = true;
const startTime = performance.now();
try {
console.log(`🔄 执行命令: ${command.constructor.name}`);
// 执行命令
const result = await this._executeCommand(command);
// 只有可撤销的命令才加入撤销栈
if (command.undoable !== false) {
this.undoStack.push(command);
this.redoStack = []; // 清空重做栈
// 限制历史记录大小
this._trimHistory();
}
// 记录性能
const duration = performance.now() - startTime;
this._recordPerformance("execute", command.constructor.name, duration);
// 通知状态变化
this._notifyStateChange();
console.log(`✅ 命令执行成功: ${command.constructor.name}`);
return result;
} catch (error) {
console.error(`❌ 命令执行失败: ${command.constructor.name}`, error);
throw error;
} finally {
this.executing = false;
}
}
/**
* 内部撤销方法
* @private
*/
async _undoInternal() {
if (this.undoStack.length === 0) {
console.warn("无法撤销:撤销栈为空");
return null;
}
this.executing = true;
const startTime = performance.now();
try {
const command = this.undoStack.pop();
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
const result = await this._undoCommand(command);
this.redoStack.push(command);
// 记录性能
const duration = performance.now() - startTime;
this._recordPerformance("undo", command.constructor.name, duration);
// 通知状态变化
this._notifyStateChange();
console.log(`✅ 命令撤销成功: ${command.constructor.name}`);
return result;
} catch (error) {
console.error(`❌ 命令撤销失败`, error);
throw error;
} finally {
this.executing = false;
}
}
/**
* 内部重做方法
* @private
*/
async _redoInternal() {
if (this.redoStack.length === 0) {
console.warn("无法重做:重做栈为空");
return null;
}
this.executing = true;
const startTime = performance.now();
try {
const command = this.redoStack.pop();
console.log(`↪️ 重做命令: ${command.constructor.name}`);
const result = await this._executeCommand(command);
this.undoStack.push(command);
// 记录性能
const duration = performance.now() - startTime;
this._recordPerformance("redo", command.constructor.name, duration);
// 通知状态变化
this._notifyStateChange();
console.log(`✅ 命令重做成功: ${command.constructor.name}`);
return result;
} catch (error) {
console.error(`❌ 命令重做失败`, error);
throw error;
} finally {
this.executing = false;
}
}
/**
* 批量执行命令(使用 CompositeCommand
* 推荐使用此方法替代原来的事务机制
*/
async executeBatch(commands, batchName = "批量操作") {
if (!Array.isArray(commands) || commands.length === 0) {
throw new Error("命令数组不能为空");
}
const compositeCommand = new CompositeCommand(commands, {
name: batchName,
});
return this.execute(compositeCommand);
}
/**
* 清空历史记录
*/
clear() {
// 清空队列中的所有任务
while (this.commandQueue.length > 0) {
const task = this.commandQueue.shift();
task.reject(new Error("命令管理器已被清空"));
}
this.undoStack = [];
this.redoStack = [];
this._notifyStateChange();
console.log("📝 命令历史已清空");
}
/**
* 获取管理器状态
*/
getState() {
return {
canUndo: this.undoStack.length > 0,
canRedo: this.redoStack.length > 0,
undoCount: this.undoStack.length,
redoCount: this.redoStack.length,
isExecuting: this.executing,
isProcessing: this.processing,
queueLength: this.commandQueue.length,
lastCommand:
this.undoStack.length > 0
? this.undoStack[this.undoStack.length - 1].constructor.name
: null,
nextRedoCommand:
this.redoStack.length > 0
? this.redoStack[this.redoStack.length - 1].constructor.name
: null,
};
}
/**
* 获取命令历史信息
*/
getHistory() {
return {
undoHistory: this.undoStack.map((cmd) => ({
name: cmd.constructor.name,
info: cmd.getInfo ? cmd.getInfo() : {},
timestamp: cmd.timestamp,
})),
redoHistory: this.redoStack.map((cmd) => ({
name: cmd.constructor.name,
info: cmd.getInfo ? cmd.getInfo() : {},
timestamp: cmd.timestamp,
})),
};
}
setChangeCallback(callback) {
if (typeof callback === "function") {
this.onStateChange = callback;
} else {
throw new Error("回调必须是一个函数");
}
}
/**
* 执行单个命令
* @private
*/
async _executeCommand(command) {
const result = command.execute();
return this._isPromise(result) ? await result : result;
}
/**
* 撤销单个命令
* @private
*/
async _undoCommand(command) {
if (typeof command.undo !== "function") {
throw new Error(`命令 ${command.constructor.name} 不支持撤销`);
}
const result = command.undo();
return this._isPromise(result) ? await result : result;
}
/**
* 检查是否为Promise
* @private
*/
_isPromise(value) {
return (
value &&
typeof value === "object" &&
typeof value.then === "function" &&
typeof value.catch === "function"
);
}
/**
* 限制历史记录大小
* @private
*/
_trimHistory() {
if (this.undoStack.length > this.maxHistorySize) {
this.undoStack.shift();
}
}
/**
* 记录性能数据
* @private
*/
_recordPerformance(type, commandName, duration) {
if (this.performanceManager) {
if (type === "execute") {
this.performanceManager.recordExecution(commandName, duration);
} else if (type === "undo") {
this.performanceManager.recordUndo(commandName, duration);
} else if (type === "redo") {
this.performanceManager.recordRedo(commandName, duration);
}
}
}
/**
* 通知状态变化
* @private
*/
_notifyStateChange() {
if (this.onStateChange) {
try {
this.onStateChange(this.getState());
} catch (error) {
console.error("状态变化回调执行失败:", error);
}
}
}
}
/**
* 创建命令管理器实例的工厂函数
*/
export function createCommandManager(options = {}) {
return new CommandManager(options);
}

View File

@@ -0,0 +1,199 @@
/**
* 简化版性能管理器
* 提供基础的性能统计功能
*/
export class PerformanceManager {
constructor() {
this.stats = {
totalExecutions: 0,
totalUndos: 0,
totalRedos: 0,
totalExecutionTime: 0,
totalUndoTime: 0,
totalRedoTime: 0,
commandStats: new Map(), // 每个命令的统计信息
recentOperations: [], // 最近的操作记录
};
this.maxRecentOperations = 100;
}
/**
* 记录命令执行
*/
recordExecution(commandName, duration) {
this.stats.totalExecutions++;
this.stats.totalExecutionTime += duration;
this._updateCommandStats(commandName, "executions", duration);
this._addRecentOperation("execute", commandName, duration);
}
/**
* 记录撤销操作
*/
recordUndo(commandName, duration) {
this.stats.totalUndos++;
this.stats.totalUndoTime += duration;
this._updateCommandStats(commandName, "undos", duration);
this._addRecentOperation("undo", commandName, duration);
}
/**
* 记录重做操作
*/
recordRedo(commandName, duration) {
this.stats.totalRedos++;
this.stats.totalRedoTime += duration;
this._updateCommandStats(commandName, "redos", duration);
this._addRecentOperation("redo", commandName, duration);
}
/**
* 获取统计信息
*/
getStats() {
const avgExecutionTime =
this.stats.totalExecutions > 0
? this.stats.totalExecutionTime / this.stats.totalExecutions
: 0;
const avgUndoTime =
this.stats.totalUndos > 0
? this.stats.totalUndoTime / this.stats.totalUndos
: 0;
const avgRedoTime =
this.stats.totalRedos > 0
? this.stats.totalRedoTime / this.stats.totalRedos
: 0;
return {
overview: {
totalExecutions: this.stats.totalExecutions,
totalUndos: this.stats.totalUndos,
totalRedos: this.stats.totalRedos,
avgExecutionTime: Number(avgExecutionTime.toFixed(2)),
avgUndoTime: Number(avgUndoTime.toFixed(2)),
avgRedoTime: Number(avgRedoTime.toFixed(2)),
},
commandBreakdown: Array.from(this.stats.commandStats.entries()).map(
([name, stats]) => ({
commandName: name,
executions: stats.executions,
undos: stats.undos,
redos: stats.redos,
avgExecutionTime:
stats.executions > 0
? Number((stats.totalExecutionTime / stats.executions).toFixed(2))
: 0,
avgUndoTime:
stats.undos > 0
? Number((stats.totalUndoTime / stats.undos).toFixed(2))
: 0,
avgRedoTime:
stats.redos > 0
? Number((stats.totalRedoTime / stats.redos).toFixed(2))
: 0,
})
),
recentOperations: this.stats.recentOperations.slice(-20), // 最近20个操作
};
}
/**
* 获取慢命令报告
*/
getSlowCommandsReport(threshold = 100) {
const slowCommands = [];
for (const [name, stats] of this.stats.commandStats.entries()) {
const avgExecTime =
stats.executions > 0 ? stats.totalExecutionTime / stats.executions : 0;
const avgUndoTime =
stats.undos > 0 ? stats.totalUndoTime / stats.undos : 0;
if (avgExecTime > threshold || avgUndoTime > threshold) {
slowCommands.push({
commandName: name,
avgExecutionTime: Number(avgExecTime.toFixed(2)),
avgUndoTime: Number(avgUndoTime.toFixed(2)),
executions: stats.executions,
undos: stats.undos,
});
}
}
return slowCommands.sort(
(a, b) =>
Math.max(b.avgExecutionTime, b.avgUndoTime) -
Math.max(a.avgExecutionTime, a.avgUndoTime)
);
}
/**
* 重置统计信息
*/
reset() {
this.stats = {
totalExecutions: 0,
totalUndos: 0,
totalRedos: 0,
totalExecutionTime: 0,
totalUndoTime: 0,
totalRedoTime: 0,
commandStats: new Map(),
recentOperations: [],
};
}
/**
* 更新命令统计信息
* @private
*/
_updateCommandStats(commandName, type, duration) {
if (!this.stats.commandStats.has(commandName)) {
this.stats.commandStats.set(commandName, {
executions: 0,
undos: 0,
redos: 0,
totalExecutionTime: 0,
totalUndoTime: 0,
totalRedoTime: 0,
});
}
const stats = this.stats.commandStats.get(commandName);
if (type === "executions") {
stats.executions++;
stats.totalExecutionTime += duration;
} else if (type === "undos") {
stats.undos++;
stats.totalUndoTime += duration;
} else if (type === "redos") {
stats.redos++;
stats.totalRedoTime += duration;
}
}
/**
* 添加最近操作记录
* @private
*/
_addRecentOperation(type, commandName, duration) {
this.stats.recentOperations.push({
type,
commandName,
duration: Number(duration.toFixed(2)),
timestamp: Date.now(),
});
// 限制记录数量
if (this.stats.recentOperations.length > this.maxRecentOperations) {
this.stats.recentOperations.shift();
}
}
}

View File

@@ -0,0 +1,909 @@
import { TransformCommand } from "../../commands/StateCommands";
import { generateId } from "../../utils/helper";
import { OperationType, OperationTypes } from "../../utils/layerHelper";
export class CanvasEventManager {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.toolManager = options.toolManager || null;
this.animationManager = options.animationManager;
this.thumbnailManager = options.thumbnailManager;
this.editorMode = options.editorMode || OperationType.SELECT;
this.activeElementId = options.activeElementId || { value: null };
this.layerManager = options.layerManager || null;
this.layers = options.layers || null;
// 事件处理的内部状态 - 优化设备检测
this.deviceInfo = this._detectDeviceType();
this.dragStartTime = 0;
this.lastMousePositions = [];
this.positionHistoryLimit = 5; // 追踪鼠标位置的历史记录,用于计算速度
this.longPressTimer = null;
this.longPressThreshold = 500;
// 初始化所有事件
this.initEvents();
}
initEvents() {
this.setupZoomEvents();
// 优化三端设备的事件处理逻辑
if (this.deviceInfo.isMobile || this.deviceInfo.isTablet) {
// 真正的移动设备和平板设备使用触摸事件
this.setupTouchEvents();
} else {
// PC 和 Mac 设备主要使用鼠标事件
this.setupMouseEvents();
}
// Mac 设备需要额外的触摸手势支持(用于特殊场景)
if (this.deviceInfo.isMac && this.deviceInfo.hasTouchSupport) {
this.setupMacTouchGestures();
}
// 共享事件
this.setupSelectionEvents();
this.setupObjectEvents();
this.setupDoubleClickEvents();
// this.setupHandlePathCreated();
}
setupZoomEvents() {
// 水平/垂直滚动相关状态
this._scrollWheelEvents = [];
this._scrollAccumulatedDelta = { x: 0, y: 0 };
this._scrollAccumulationTimeout = null;
this._scrollAccumulationTime = 100; // 降低滚轮累积时间窗口
this._lastScrollTime = 0; // 跟踪上次滚动时间
this._scrollThrottleDelay = 5; // 滚动节流延迟(毫秒)
// 缩放处理 - 使用动画管理器,针对 Mac 设备优化
this.canvas.on("mouse:wheel", (opt) => {
// Mac 设备双指滚动优化:确保滚动事件正确处理
if (this.deviceInfo.isMac) {
// Mac设备的简化处理逻辑减少不必要的动画中断
// 让动画管理器自行处理冲突,避免过度干预
} else {
// 非 Mac 设备的标准处理
if (
this.animationManager._panAnimation ||
this.animationManager._zoomAnimation
) {
this.animationManager._wasPanning =
!!this.animationManager._panAnimation;
this.animationManager._wasZooming =
!!this.animationManager._zoomAnimation;
this.animationManager.smoothStopAnimations({ duration: 0.1 });
}
}
// 按住 Ctrl 键时实现垂直滚动Mac 下是 Cmd 键)
const isCtrlOrCmd = this.deviceInfo.isMac ? opt.e.metaKey : opt.e.ctrlKey;
if (isCtrlOrCmd) {
this.handleScrollWheel(opt, "vertical");
opt.e.preventDefault();
return;
}
// 按住 Shift 键时实现水平滚动
if (opt.e.shiftKey) {
this.handleScrollWheel(opt, "horizontal");
opt.e.preventDefault();
return;
}
// 标准缩放行为 - 让 AnimationManager 处理平滑过渡
// Mac 设备下的双指滚动将直接进入这里进行缩放
this.animationManager.handleMouseWheel(opt);
});
}
/**
* 处理滚轮滚动事件
* @param {Object} opt 滚轮事件对象
* @param {String} direction 滚动方向: 'vertical' 或 'horizontal'
*/
handleScrollWheel(opt, direction) {
// 获取当前视图变换
const vpt = this.canvas.viewportTransform.slice(0); // 创建副本避免直接修改
const zoom = this.canvas.getZoom();
// 计算滚动量 - 根据方向决定是水平还是垂直滚动
let deltaX = 0;
let deltaY = 0;
// 设置滚动方向和距离
if (direction === "horizontal") {
deltaX = opt.e.deltaY; // 水平滚动
} else {
deltaY = opt.e.deltaY; // 垂直滚动
}
// 计算滚动因子,基于缩放级别和设备类型调整
let scrollFactor = Math.max(0.4, Math.min(1, 1 / zoom));
// Mac 设备优化:触控板滚动通常比鼠标滚轮更敏感
if (this.deviceInfo.isMac) {
const isMacTrackpadScroll =
Math.abs(opt.e.deltaY) < 100 && opt.e.deltaMode === 0;
if (isMacTrackpadScroll) {
// Mac 触控板滚动更细腻,需要调整滚动因子
scrollFactor *= 0.8; // 降低滚动敏感度
}
}
// 直接应用滚动变化,不使用累积和计时器
vpt[4] -= deltaX * scrollFactor;
vpt[5] -= deltaY * scrollFactor;
// 直接设置新的视图变换,不使用动画
this.canvas.setViewportTransform(vpt);
// 请求重新渲染画布
this.canvas.renderAll();
}
/**
* 处理累积的滚轮滚动事件并应用平移
* @private
* @param {String} direction 滚动方向
*/
_processAccumulatedScroll(direction) {
// 这个函数不再需要,但为了兼容性保留空实现
// 所有滚动逻辑已经移到 handleScrollWheel 中直接处理
return;
}
/**
* 停止所有惯性动画
* @param {boolean} smooth 是否平滑过渡,默认为 false立即停止
*/
stopInertiaAnimation(smooth = false) {
if (this.animationManager) {
if (this.animationManager._panAnimation && !smooth) {
this.animationManager._panAnimation.kill();
this.animationManager._panAnimation = null;
}
if (this.animationManager._zoomAnimation && !smooth) {
this.animationManager._zoomAnimation.kill();
this.animationManager._zoomAnimation = null;
}
}
}
/**
* 设置鼠标事件处理
*/
setupMouseEvents() {
// 鼠标按下事件
this.canvas.on("mouse:down", (opt) => {
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
if (
opt.e.altKey ||
opt.e.which === 2 ||
this.editorMode === OperationType.PAN
) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
this.canvas.defaultCursor = "grabbing";
// 记录拖动开始时间和位置,用于计算速度
this.dragStartTime = Date.now();
this.lastMousePositions = []; // 重置位置历史
if (this.canvas.isDragging) {
this.canvas.selection = false;
this.canvas.renderAll();
}
}
});
// 鼠标移动事件
this.canvas.on("mouse:move", (opt) => {
if (!this.canvas.isDragging) return;
const vpt = this.canvas.viewportTransform;
vpt[4] += opt.e.clientX - this.canvas.lastPosX;
vpt[5] += opt.e.clientY - this.canvas.lastPosY;
// 记录鼠标位置和时间,用于计算惯性
const now = Date.now();
this.lastMousePositions.push({
x: opt.e.clientX,
y: opt.e.clientY,
time: now,
});
// 保持历史记录在限定数量内
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.renderAll();
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
});
// 鼠标抬起事件
this.canvas.on("mouse:up", (opt) => {
this.handleDragEnd(opt);
});
}
/**
* 设置触摸事件处理
*/
setupTouchEvents() {
// 触摸开始事件
this.canvas.on("touch:gesture", (opt) => {
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
if (opt.e.touches && opt.e.touches.length === 2) {
this.canvas.isDragging = true;
this.canvas.lastPosX =
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
this.canvas.lastPosY =
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
// 重置触摸位置历史
this.dragStartTime = Date.now();
this.lastMousePositions = [];
if (this.canvas.isDragging) {
this.canvas.selection = false;
this.canvas.renderAll();
}
opt.e.preventDefault();
}
});
// 单指触摸开始 - 处理拖动
this.canvas.on("touch:drag", (opt) => {
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
if (this.editorMode === OperationType.PAN) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.touches[0].clientX;
this.canvas.lastPosY = opt.e.touches[0].clientY;
this.dragStartTime = Date.now();
this.lastMousePositions = [];
if (this.canvas.isDragging) {
this.canvas.selection = false;
this.canvas.renderAll();
}
opt.e.preventDefault();
}
});
// 触摸移动事件
this.canvas.on("touch:gesture:update", (opt) => {
if (!this.canvas.isDragging) return;
if (opt.e.touches && opt.e.touches.length === 2) {
const currentX =
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
const currentY =
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
const vpt = this.canvas.viewportTransform;
vpt[4] += currentX - this.canvas.lastPosX;
vpt[5] += currentY - this.canvas.lastPosY;
// 记录触摸位置和时间
const now = Date.now();
this.lastMousePositions.push({
x: currentX,
y: currentY,
time: now,
});
// 保持历史记录在限定数量内
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.renderAll();
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
opt.e.preventDefault();
}
});
// 单指拖动更新
this.canvas.on("touch:drag:update", (opt) => {
if (!this.canvas.isDragging || this.editorMode !== OperationType.PAN)
return;
const currentX = opt.e.touches[0].clientX;
const currentY = opt.e.touches[0].clientY;
const vpt = this.canvas.viewportTransform;
vpt[4] += currentX - this.canvas.lastPosX;
vpt[5] += currentY - this.canvas.lastPosY;
// 记录触摸位置和时间
const now = Date.now();
this.lastMousePositions.push({
x: currentX,
y: currentY,
time: now,
});
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.renderAll();
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
opt.e.preventDefault();
});
// 触摸结束事件
this.canvas.on("touch:gesture:end", (opt) => {
this.handleDragEnd(opt, true);
});
// 单指拖动结束
this.canvas.on("touch:drag:end", (opt) => {
this.handleDragEnd(opt, true);
});
}
/**
* 处理拖动结束(鼠标抬起或触摸结束)
*/
handleDragEnd(opt, isTouch = false) {
if (this.canvas.isDragging) {
// 使用动画管理器处理惯性效果
if (this.lastMousePositions.length > 1 && opt && opt.e) {
this.animationManager.applyInertiaEffect(
this.lastMousePositions,
isTouch
);
}
}
this.canvas.isDragging = false;
if (this.toolManager) {
this.toolManager.restoreSelectionState(); // 恢复选择状态
}
this.canvas.renderAll();
}
setupSelectionEvents() {
// 监听对象选择事件
this.canvas.on("selection:created", (opt) => this.updateSelectedLayer(opt));
this.canvas.on("selection:updated", (opt) => this.updateSelectedLayer(opt));
// this.canvas.on("selection:cleared", () => this.clearSelectedElements());
}
setupObjectEvents() {
// 监听对象变化事件,用于更新缩略图
this.canvas.on("object:added", (e) => {
if (this.thumbnailManager && e.target && e.target.id) {
// 延迟更新以确保对象完全添加
setTimeout(() => {
// 现在图层就是元素本身,直接更新元素的缩略图
this.thumbnailManager.generateLayerThumbnail(
e.target.layerId,
e.target
);
}, 300);
}
});
// 添加对象开始变换时的状态捕获
this.canvas.on(
"object:moving",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:scaling",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:rotating",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:skewing",
this._captureInitialTransformState.bind(this)
);
this.canvas.on("object:modified", (e) => {
// 移除调试日志
// console.log("object:modified", e);
const activeObj = e.target || this.canvas.getActiveObject();
if (activeObj && this.layerManager?.commandManager) {
// 使用新的轻量级 TransformCommand 替代完整状态保存
// 检查对象是否有初始变换状态记录
if (activeObj._initialTransformState) {
// 创建并执行 TransformCommand只记录变换属性的变化
const transformCmd = new TransformCommand({
canvas: this.canvas,
objectId: activeObj.id,
initialState: activeObj._initialTransformState,
finalState: TransformCommand.captureTransformState(activeObj),
objectType: activeObj.type,
name: `变换 ${activeObj.type || "对象"}`,
});
// 执行并将命令添加到历史栈
this.layerManager.commandManager.execute(transformCmd, {
name: "对象修改",
});
// 清除临时状态记录
delete activeObj._initialTransformState;
}
}
if (this.thumbnailManager && e.target) {
if (e.target.id) {
this.updateLayerThumbnail(e.target.id, e.target);
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
if (e.target.parentId) {
this.updateLayerThumbnail(e.target.parentId);
}
}
}
});
this.canvas.on("object:removed", (e) => {
if (this.thumbnailManager && e.target) {
if (e.target.id) {
this.thumbnailManager.clearElementThumbnail(e.target.id);
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
if (e.target.parentId) {
setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50);
}
}
}
});
// // 鼠标抬起时,检查是否需要保存状态
// this.canvas.on("mouse:up", (e) => {
// // 只在选择模式下处理对象变换的状态保存
// if (this.editorMode !== OperationType.SELECT) {
// // 绘画、擦除等模式通过各自的命令管理状态,不需要在这里保存
// return;
// }
// const activeObj = this.canvas.getActiveObject();
// if (
// activeObj &&
// activeObj._stateRecord &&
// activeObj._stateRecord.isModifying
// ) {
// const original = activeObj._stateRecord.originalState;
// // 检查是否是真正的变换操作(移动、缩放、旋转)
// const hasTransformChanged =
// original.left !== activeObj.left ||
// original.top !== activeObj.top ||
// original.scaleX !== activeObj.scaleX ||
// original.scaleY !== activeObj.scaleY ||
// original.angle !== activeObj.angle;
// // 只有在对象发生变换且不是命令执行过程中时才保存状态
// if (hasTransformChanged && this.layerManager) {
// // 立即保存状态,而不是延迟执行
// this.layerManager.saveCanvasState();
// delete activeObj._stateRecord;
// } else {
// // 清理状态记录,即使没有保存状态
// delete activeObj._stateRecord;
// }
// }
// });
}
setupDoubleClickEvents() {
// 双击处理
this.canvas.on("mouse:dblclick", (opt) => {
if (opt.target) {
// 双击对象的特殊处理
} else {
// 双击空白处重置缩放
if (this.animationManager) {
this.animationManager.resetZoom(true);
}
}
});
}
setupLongPress(callback) {
this.canvas.on("mouse:down", (opt) => {
if (!opt.target) return;
this.longPressTimer = setTimeout(() => {
callback(opt);
}, this.longPressThreshold);
});
this.canvas.on("mouse:up", () => {
clearTimeout(this.longPressTimer);
});
this.canvas.on("mouse:move", () => {
clearTimeout(this.longPressTimer);
});
}
// 设置路径创建事件
setupHandlePathCreated() {
// 在 CanvasEventManager 的构造函数或初始化方法中
// this.canvas.on("path:created", this._handlePathCreated.bind(this));
}
_handlePathCreated(e) {
// // 获取新创建的路径对象
// const path = e.path;
// // 设置路径的ID和其他属性
// path.id = generateId(); // 生成唯一ID
// // 获取当前活动图层
// const activeLayer = this.layerManager.getActiveLayer();
// // 将路径对象绑定到当前活动图层
// if (activeLayer) {
// // 设置路径的图层ID
// path.layerId = activeLayer.id;
// // 更新图层对象列表
// if (!activeLayer.fabricObjects) activeLayer.fabricObjects = [];
// activeLayer.fabricObjects.push(path);
// // 更新图层缩略图
// if (this.thumbnailManager) {
// this.thumbnailManager.generateLayerThumbnail(activeLayer.id);
// }
// }
}
/**
* 合并图层中的对象为图像以提高性能
* @param {Object} options 合并选项
* @param {fabric.Image} options.fabricImage 新的图像对象
* @param {Object} options.activeLayer 当前活动图层
* @private
*/
async mergeLayerObjectsForPerformance({ fabricImage, activeLayer }) {
// 确保有命令管理器
if (!this.layerManager || !this.layerManager.commandManager) {
console.warn("合并对象失败:没有命令管理器");
return;
}
// 确保有活动图层
if (!activeLayer) {
console.warn("合并对象失败:没有活动图层");
return;
}
// 验证是否需要合并
const hasExistingObjects =
Array.isArray(activeLayer.fabricObjects) &&
activeLayer.fabricObjects.length > 0;
const hasNewImage = !!fabricImage;
if (!hasExistingObjects && !hasNewImage) {
console.log("没有对象需要合并");
return;
}
// 如果只有一个新图像且图层为空,直接添加到图层
if (hasNewImage && !hasExistingObjects) {
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
return;
}
// 执行高保真合并操作
try {
console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`);
const command = await this.layerManager.LayerObjectsToGroup(
activeLayer,
fabricImage
);
this.layerManager?.commandManager?.execute?.(command, {
name: `合并图层 ${activeLayer.name} 中的对象为组`,
});
} catch (error) {
console.error("合并图层对象时发生错误:", error);
// 降级处理:如果合并失败,至少保证新图像能添加到图层
if (fabricImage && this.layerManager) {
console.log("执行降级处理:直接添加图像到图层");
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
}
}
}
updateSelectedLayer(opt) {
const selected = opt.selected[0];
if (selected) {
this.layerManager.activeLayerId.value = selected.layerId;
}
}
// clearSelectedElements() {
// this.activeElementId.value = null;
// }
// 更新图层缩略图
updateLayerThumbnail(layerId) {
if (!this.thumbnailManager || !layerId || !this.layers) return;
const layer = this.layers.value.find((l) => l.id === layerId);
if (layer) {
this.thumbnailManager.generateLayerThumbnail(layer);
}
}
// 更新子元素组合缩略图
updateLayerChidrenThumbnail(layerId, fabricObject) {
if (!this.thumbnailManager || !fabricObject || !this.layers) return;
// 查找对应的图层(现在元素就是图层)
const layer = this.layers.value.find(
(l) =>
l.id === elementId ||
(l.fabricObjects && l.fabricObjects?.[0]?.id === layerId)
);
if (layer) {
// 生成图层缩略图
this.thumbnailManager.generateLayerThumbnail(layer);
}
// 同时也维护元素缩略图,以保持向后兼容性
this.thumbnailManager.generateElementThumbnail(
{ id: elementId, type: fabricObject.type },
fabricObject
);
}
/**
* 设置编辑器模式
* @param {string} mode 编辑器模式
*/
setEditorMode(mode) {
if (!OperationTypes.includes(mode)) {
console.warn(`不支持的编辑器模式: ${mode}`);
return;
}
// 切换工具时,立即停止任何惯性动画,但使用平滑过渡
this.stopInertiaAnimation(true);
this.editorMode = mode;
// 如果切换到选择模式,还原鼠标指针
if (mode === OperationType.SELECT) {
this.canvas.defaultCursor = "default";
} else if (mode === OperationType.PAN) {
this.canvas.defaultCursor = "grab";
}
}
dispose() {
// 移除所有事件监听
this.canvas.off();
// 清理 Mac 专用的原生事件监听器
if (this.deviceInfo.isMac && this.canvas.upperCanvasEl) {
const upperCanvas = this.canvas.upperCanvasEl;
// 移除手势事件监听器
upperCanvas.removeEventListener("gesturestart", null);
upperCanvas.removeEventListener("gesturechange", null);
upperCanvas.removeEventListener("gestureend", null);
upperCanvas.removeEventListener("wheel", null);
}
// 清除计时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
// 停止所有动画
this.stopInertiaAnimation();
}
/**
* 捕获对象开始变换时的初始状态
* @private
* @param {Object} e 事件对象
*/
_captureInitialTransformState(e) {
const obj = e.target;
// 只在首次触发变换事件时记录初始状态
if (obj && !obj._initialTransformState && obj.id) {
// 捕获对象的初始变换状态
obj._initialTransformState = TransformCommand.captureTransformState(obj);
// 添加调试日志(可选)
// console.log(`捕获对象 ${obj.id} (${obj.type}) 的初始变换状态`);
}
}
/**
* 精确检测设备类型,区分 PC、Mac、平板和移动设备
* @private
* @returns {Object} 设备信息对象
*/
_detectDeviceType() {
const userAgent = navigator.userAgent.toLowerCase();
const platform = navigator.platform.toLowerCase();
const hasTouchSupport =
"ontouchstart" in window || navigator.maxTouchPoints > 0;
// 检测操作系统
const isMac = /mac|darwin/.test(platform) || /macintosh/.test(userAgent);
const isWindows = /win/.test(platform);
const isLinux = /linux/.test(platform) && !/android/.test(userAgent);
// 检测设备类型
const isMobile = /mobile|phone|android.*mobile|iphone/.test(userAgent);
const isTablet = /tablet|ipad|android(?!.*mobile)/.test(userAgent);
const isDesktop = !isMobile && !isTablet;
// 检测浏览器类型(用于特定优化)
const isSafari = /safari/.test(userAgent) && !/chrome/.test(userAgent);
const isChrome = /chrome/.test(userAgent);
const isFirefox = /firefox/.test(userAgent);
return {
isMac,
isWindows,
isLinux,
isMobile,
isTablet,
isDesktop,
isSafari,
isChrome,
isFirefox,
hasTouchSupport,
// 判断是否应该使用触摸事件作为主要交互方式
preferTouchEvents: (isMobile || isTablet) && !isDesktop,
// 判断是否需要特殊的 Mac 触控板处理
needsMacTrackpadOptimization: isMac && isDesktop && hasTouchSupport,
};
}
/**
* 设置 Mac 专用的触摸手势处理
* 主要用于处理触控板的多指手势,但不干扰双指滚动的缩放功能
*/
setupMacTouchGestures() {
// Mac 触控板专用:三指拖拽进行画布平移
let macGestureState = {
isThreeFingerDrag: false,
startX: 0,
startY: 0,
};
// 监听 Mac 专用的手势事件
this.canvas.upperCanvasEl.addEventListener(
"gesturestart",
(e) => {
// 阻止浏览器默认的手势行为,但保留双指缩放
if (e.scale !== 1) {
e.preventDefault();
}
},
{ passive: false }
);
this.canvas.upperCanvasEl.addEventListener(
"gesturechange",
(e) => {
// 只处理三指以上的手势,保留双指缩放给 mouse:wheel 事件
if (e.touches && e.touches.length >= 3) {
e.preventDefault();
if (!macGestureState.isThreeFingerDrag) {
macGestureState.isThreeFingerDrag = true;
macGestureState.startX = e.pageX;
macGestureState.startY = e.pageY;
this.canvas.isDragging = true;
this.canvas.lastPosX = e.pageX;
this.canvas.lastPosY = e.pageY;
this.stopInertiaAnimation(true);
} else {
// 执行三指拖拽平移
const vpt = this.canvas.viewportTransform;
vpt[4] += e.pageX - this.canvas.lastPosX;
vpt[5] += e.pageY - this.canvas.lastPosY;
this.canvas.renderAll();
this.canvas.lastPosX = e.pageX;
this.canvas.lastPosY = e.pageY;
}
}
},
{ passive: false }
);
this.canvas.upperCanvasEl.addEventListener(
"gestureend",
(e) => {
if (macGestureState.isThreeFingerDrag) {
macGestureState.isThreeFingerDrag = false;
this.canvas.isDragging = false;
if (this.toolManager) {
this.toolManager.restoreSelectionState();
}
this.canvas.renderAll();
}
},
{ passive: false }
);
// 添加 Mac 专用的鼠标滚轮优化,确保双指滚动正常工作
this.setupMacScrollOptimization();
}
/**
* Mac 滚轮优化:确保双指滚动正确触发缩放
*/
setupMacScrollOptimization() {
if (!this.deviceInfo.isMac) return;
// Mac 下的滚轮事件优化
let macScrollState = {
lastWheelTime: 0,
wheelTimeout: null,
};
// 监听原生滚轮事件,确保 Mac 双指滚动正确处理
this.canvas.upperCanvasEl.addEventListener(
"wheel",
(e) => {
const now = Date.now();
// Mac 双指滚动的特征:通常有较高的 deltaY 精度和连续性
const isMacTrackpadScroll =
this.deviceInfo.isMac &&
Math.abs(e.deltaY) < 100 && // 像素模式
e.deltaMode === 0; // 像素模式
if (isMacTrackpadScroll) {
// 清除之前的超时
if (macScrollState.wheelTimeout) {
clearTimeout(macScrollState.wheelTimeout);
}
// 确保这个事件会被 Fabric.js 的 mouse:wheel 正确处理
macScrollState.lastWheelTime = now;
// 设置短暂延迟,防止与触摸事件冲突
macScrollState.wheelTimeout = setTimeout(() => {
// 滚轮事件处理完成
}, 16); // 约一帧的时间
}
},
{ passive: true }
);
}
}

View File

@@ -0,0 +1,720 @@
/**
* 键盘管理器
* 负责处理编辑器中的键盘事件和快捷键
* 支持PC、Mac和iPad三端适配
*/
export class KeyboardManager {
/**
* 创建键盘管理器
* @param {Object} options 配置选项
* @param {Object} options.toolManager 工具管理器实例
* @param {Object} options.commandManager 命令管理器实例
* @param {Object} options.layerManager 图层管理器实例
* @param {HTMLElement} options.container 容器元素,用于添加事件监听
*/
constructor(options = {}) {
this.toolManager = options.toolManager;
this.commandManager = options.commandManager;
this.layerManager = options.layerManager;
this.container = options.container || document;
// 检测平台类型
this.platform = this.detectPlatform();
this.isTouchDevice = this.detectTouchDevice();
// 快捷键的平台特定键名
this.modifierKeys = {
ctrl: this.platform === "mac" ? "meta" : "ctrl",
cmdOrCtrl: this.platform === "mac" ? "meta" : "ctrl",
alt: "alt",
shift: "shift",
option: "alt", // Mac 特有,等同于 alt
cmd: "meta", // Mac 特有,等同于 Command
};
// 快捷键显示的平台特定符号
this.keySymbols = {
ctrl: this.platform === "mac" ? "⌃" : "Ctrl",
meta: this.platform === "mac" ? "⌘" : "Win",
alt: this.platform === "mac" ? "⌥" : "Alt",
shift: this.platform === "mac" ? "⇧" : "Shift",
escape: "Esc",
space: "空格",
};
// 快捷键映射表 - 可通过配置进行扩展
this.shortcuts = this.initShortcuts();
// 触摸相关状态
this.touchState = {
pinchStartDistance: 0,
pinchStartBrushSize: 0,
touchStartX: 0,
touchStartY: 0,
isTwoFingerTouch: false,
};
// 临时工具状态
this.tempToolState = {
active: false,
originalTool: null,
};
// 事件绑定
this._handleKeyDown = this.handleKeyDown.bind(this);
this._handleKeyUp = this.handleKeyUp.bind(this);
this._handleTouchStart = this.handleTouchStart.bind(this);
this._handleTouchMove = this.handleTouchMove.bind(this);
this._handleTouchEnd = this.handleTouchEnd.bind(this);
// 已注册的自定义事件处理程序
this.customHandlers = {};
}
/**
* 检测当前平台
* @returns {'mac'|'windows'|'ios'|'android'|'other'} 平台类型
*/
detectPlatform() {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf("mac") !== -1) return "mac";
if (userAgent.indexOf("win") !== -1) return "windows";
if (/(iphone|ipad|ipod)/.test(userAgent)) return "ios";
if (userAgent.indexOf("android") !== -1) return "android";
return "other";
}
/**
* 检测是否为触摸设备
* @returns {boolean} 是否为触摸设备
*/
detectTouchDevice() {
return (
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0
);
}
/**
* 初始化快捷键配置
* @returns {Object} 快捷键配置
*/
initShortcuts() {
const cmdOrCtrl = this.modifierKeys.cmdOrCtrl;
// 基本快捷键映射,将在构建时根据平台类型自动调整
return {
// 撤销/重做
[`${cmdOrCtrl}+z`]: { action: "undo", description: "撤销" },
[`${cmdOrCtrl}+shift+z`]: { action: "redo", description: "重做" },
[`${cmdOrCtrl}+y`]: { action: "redo", description: "重做" },
// 复制/粘贴
[`${cmdOrCtrl}+c`]: { action: "copy", description: "复制" },
[`${cmdOrCtrl}+v`]: { action: "paste", description: "粘贴" },
[`${cmdOrCtrl}+x`]: { action: "cut", description: "剪切" },
// 删除
delete: { action: "delete", description: "删除" },
backspace: { action: "delete", description: "删除" },
// 选择
[`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" },
escape: { action: "clearSelection", description: "取消选择" },
// 保存
[`${cmdOrCtrl}+s`]: { action: "save", description: "保存" },
// 工具切换 (这些会由工具管理器处理)
v: { action: "selectTool", param: "select", description: "选择工具" },
b: { action: "selectTool", param: "draw", description: "画笔工具" },
e: { action: "selectTool", param: "eraser", description: "橡皮擦" },
i: { action: "selectTool", param: "eyedropper", description: "吸色工具" },
h: { action: "selectTool", param: "pan", description: "移动画布" },
l: { action: "selectTool", param: "lasso", description: "套索工具" },
m: {
action: "selectTool",
param: "area_custom",
description: "自由选区工具",
},
w: { action: "selectTool", param: "wave", description: "波浪工具" },
j: { action: "selectTool", param: "liquify", description: "液化工具" },
// 数值调整
"shift+[": {
action: "decreaseTextureScale",
description: "减小材质图片大小",
},
"shift+]": {
action: "increaseTextureScale",
description: "增大材质图片大小",
},
"[": { action: "decreaseBrushSize", param: 1, description: "减小画笔" },
"]": { action: "increaseBrushSize", param: 1, description: "增大画笔" },
",": {
action: "decreaseBrushOpacity",
param: 0.01,
description: "减小透明度",
},
".": {
action: "increaseBrushOpacity",
param: 0.01,
description: "增大透明度",
},
// 空格 - 临时切换到手型工具
space: {
action: "toggleTempTool",
param: "pan",
description: "临时切换到手形工具",
},
// 图层操作
[`${cmdOrCtrl}+shift+n`]: { action: "newLayer", description: "新建图层" },
[`${cmdOrCtrl}+g`]: { action: "groupLayers", description: "组合图层" },
[`${cmdOrCtrl}+o`]: {
action: "addImageToNewLayer",
description: "上传图片到新图层",
},
[`${cmdOrCtrl}+shift+g`]: {
action: "ungroupLayers",
description: "取消组合",
},
[`${cmdOrCtrl}+j`]: { action: "mergeLayers", description: "合并图层" },
// iPad特有的快捷键(当无法使用键盘时)
...(this.platform === "ios" && {
two_finger_tap: {
action: "contextMenu",
description: "显示上下文菜单",
},
three_finger_swipe_left: { action: "undo", description: "撤销" },
three_finger_swipe_right: { action: "redo", description: "重做" },
}),
};
}
/**
* 初始化并开始监听键盘事件
*/
init() {
// 添加键盘事件监听
this.container.addEventListener("keydown", this._handleKeyDown);
this.container.addEventListener("keyup", this._handleKeyUp);
// 如果是触摸设备,添加触摸事件监听
if (this.isTouchDevice) {
this.container.addEventListener("touchstart", this._handleTouchStart);
this.container.addEventListener("touchmove", this._handleTouchMove);
this.container.addEventListener("touchend", this._handleTouchEnd);
this.container.addEventListener("touchcancel", this._handleTouchEnd);
}
console.log(
`键盘管理器已初始化,平台: ${this.platform}, 触摸设备: ${this.isTouchDevice}`
);
}
/**
* 处理键盘按下事件
* @param {KeyboardEvent} event 键盘事件
*/
handleKeyDown(event) {
// 如果当前焦点在输入框内,不处理大部分快捷键
if (this.isInputActive() && !["Escape", "Tab"].includes(event.key)) {
return;
}
// 构建快捷键标识符
const shortcutKey = this.buildShortcutKey(event);
// 查找并执行快捷键动作
const shortcut = this.shortcuts[shortcutKey];
if (shortcut) {
// 阻止默认行为,例如浏览器的保存对话框等
if (shortcutKey.includes(`${this.modifierKeys.cmdOrCtrl}+`)) {
event.preventDefault();
}
this.executeAction(shortcut.action, shortcut.param, event);
return;
}
// 工具快捷键处理
if (this.toolManager && !event.ctrlKey && !event.metaKey && !event.altKey) {
this.toolManager.handleKeyboardShortcut(event);
}
}
/**
* 处理键盘释放事件
* @param {KeyboardEvent} event 键盘事件
*/
handleKeyUp(event) {
// 当空格键释放时,如果是临时工具,切回原始工具
if (event.key === " " && this.tempToolState.active) {
this.restoreTempTool();
}
// 调用自定义处理程序
const key = event.key.toLowerCase();
if (
this.customHandlers[key] &&
typeof this.customHandlers[key].onKeyUp === "function"
) {
this.customHandlers[key].onKeyUp(event);
}
}
/**
* 处理触摸开始事件
* @param {TouchEvent} event 触摸事件
*/
handleTouchStart(event) {
const touches = event.touches;
// 存储初始状态以便后续计算
if (touches.length === 2) {
// 双指触摸 - 可用于缩放或调整画笔大小
this.touchState.isTwoFingerTouch = true;
this.touchState.pinchStartDistance = this.getDistanceBetweenTouches(
touches[0],
touches[1]
);
// 如果有画笔管理器,记录起始画笔大小
if (this.toolManager && this.toolManager.brushManager) {
this.touchState.pinchStartBrushSize =
this.toolManager.brushManager.brushSize.value;
}
} else if (touches.length === 3) {
// 三指触摸 - 可用于撤销/重做
this.touchState.touchStartX = touches[0].clientX;
}
}
/**
* 处理触摸移动事件
* @param {TouchEvent} event 触摸事件
*/
handleTouchMove(event) {
const touches = event.touches;
// 阻止默认行为(例如滚动)
if (touches.length >= 2) {
event.preventDefault();
}
// 双指缩放处理 - 调整画笔大小
if (touches.length === 2 && this.touchState.isTwoFingerTouch) {
const currentDistance = this.getDistanceBetweenTouches(
touches[0],
touches[1]
);
const scale = currentDistance / this.touchState.pinchStartDistance;
// 调整画笔大小
if (this.toolManager && this.toolManager.brushManager && scale !== 1) {
const newSize = this.touchState.pinchStartBrushSize * scale;
this.toolManager.brushManager.setBrushSize(newSize);
}
}
// 三指滑动处理 - 撤销/重做
else if (touches.length === 3) {
const deltaX = touches[0].clientX - this.touchState.touchStartX;
// 滑动超过50px认为是有效的手势
if (Math.abs(deltaX) > 50) {
if (deltaX < 0) {
// 向左滑动 - 撤销
this.executeAction("undo");
} else {
// 向右滑动 - 重做
this.executeAction("redo");
}
// 更新起始位置,防止连续触发
this.touchState.touchStartX = touches[0].clientX;
}
}
}
/**
* 处理触摸结束事件
* @param {TouchEvent} event 触摸事件
*/
handleTouchEnd(event) {
// 检测双指轻拍 (两个手指几乎同时按下,又几乎同时抬起)
if (this.touchState.isTwoFingerTouch && event.touches.length === 0) {
if (new Date().getTime() - this.touchState.touchStartTime < 300) {
// 双指轻拍 - 可以触发上下文菜单
this.executeAction("contextMenu");
}
}
// 重置触摸状态
this.touchState.isTwoFingerTouch = false;
}
/**
* 计算两个触摸点之间的距离
* @param {Touch} touch1 第一个触摸点
* @param {Touch} touch2 第二个触摸点
* @returns {number} 两点间距离
*/
getDistanceBetweenTouches(touch1, touch2) {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* 执行快捷键对应的动作
* @param {string} action 动作名称
* @param {*} param 动作参数
* @param {Event} event 原始事件
*/
executeAction(action, param, event) {
switch (action) {
case "undo":
if (this.commandManager) {
this.commandManager.undo();
}
break;
case "redo":
if (this.commandManager) {
this.commandManager.redo();
}
break;
case "copy":
// 复制逻辑
console.log("复制当前选中图层");
this.layerManager.copyLayer(this.layerManager.activeLayerId.value);
break;
case "paste":
// 粘贴逻辑
console.log("粘贴");
this.layerManager.pasteLayer();
break;
case "cut":
// 剪切逻辑
console.log("剪切");
this.layerManager.cutLayer(this.layerManager.activeLayerId.value);
break;
case "delete":
// 删除逻辑
console.log("删除");
this.layerManager.removeLayer(this.layerManager.activeLayerId.value);
break;
case "selectAll":
// 全选逻辑
console.log("全选");
// 这里需要实现全选逻辑 TODO: 是否在选择模式下才可以全选?
if (this.layerManager) {
this.layerManager.selectAll();
}
break;
case "clearSelection":
// 清除选择逻辑
console.log("清除选择");
// 这里需要实现清除选择逻辑
if (this.layerManager) {
this.layerManager.clearSelection();
}
break;
case "save":
// 保存逻辑
console.log("保存");
break;
case "selectTool":
// 选择工具
if (this.toolManager && param) {
this.toolManager.setToolWithCommand(param);
}
break;
case "increaseBrushSize":
// 增大画笔尺寸
if (this.toolManager && this.toolManager.brushManager) {
const amount = param || 5;
this.toolManager.brushManager.increaseBrushSize(amount);
}
break;
case "decreaseBrushSize":
// 减小画笔尺寸
if (this.toolManager && this.toolManager.brushManager) {
const amount = param || 5;
this.toolManager.brushManager.decreaseBrushSize(amount);
}
break;
case "increaseBrushOpacity":
// 增大画笔透明度
if (this.toolManager && this.toolManager.brushManager) {
const amount = param || 0.01;
this.toolManager.brushManager.increaseBrushOpacity(amount);
}
break;
case "decreaseTextureScale":
// 减小画笔材质图片大小
if (this.toolManager && this.toolManager.brushManager) {
const amount = param || 5;
this.toolManager.brushManager.decreaseBrushSize(amount);
}
break;
case "increaseTextureScale":
// 增大画笔材质图片大小
if (this.toolManager && this.toolManager.brushManager) {
const amount = param || 0.01;
this.toolManager.brushManager.increaseTextureScale(amount);
}
break;
case "decreaseBrushOpacity":
// 减小画笔透明度
if (this.toolManager && this.toolManager.brushManager) {
const amount = param || 0.01;
this.toolManager.brushManager.decreaseBrushOpacity(amount);
}
break;
case "toggleTempTool":
// 临时切换工具
if (param && this.toolManager) {
this.setTempTool(param);
}
break;
case "newLayer":
// 创建新图层
if (this.layerManager) {
this.layerManager.createNewLayer();
}
break;
case "addImageToNewLayer":
this.toolManager?.openFile?.();
break;
case "groupLayers":
// 组合图层
if (this.layerManager) {
this.layerManager.groupSelectedLayers();
}
break;
case "ungroupLayers":
// 解组图层
if (this.layerManager) {
this.layerManager.ungroupSelectedLayer();
}
break;
case "mergeLayers":
// 合并图层
if (this.layerManager) {
this.layerManager.mergeSelectedLayers();
}
break;
case "contextMenu":
// 上下文菜单(通常由右击或触控设备上的特定手势触发)
console.log("显示上下文菜单");
// 这里需要实现显示上下文菜单的逻辑
break;
default:
// 调用自定义注册的动作处理
if (this.customHandlers[action]) {
this.customHandlers[action].execute(param, event);
}
}
}
/**
* 设置临时工具
* @param {string} toolId 临时工具ID
*/
setTempTool(toolId) {
if (!this.toolManager || this.tempToolState.active) return;
// 保存当前工具
this.tempToolState.originalTool = this.toolManager.getCurrentTool();
this.tempToolState.active = true;
// 切换到临时工具
this.toolManager.setTool(toolId);
}
/**
* 恢复临时工具切换前的工具
*/
restoreTempTool() {
if (!this.toolManager || !this.tempToolState.active) return;
// 恢复到原始工具
if (this.tempToolState.originalTool) {
this.toolManager.setTool(this.tempToolState.originalTool);
}
// 重置状态
this.tempToolState.active = false;
this.tempToolState.originalTool = null;
}
/**
* 构建快捷键标识符
* @param {KeyboardEvent} event 键盘事件
* @returns {string} 快捷键标识符
*/
buildShortcutKey(event) {
let shortcutKey = "";
// 统一处理Mac和PC的修饰键
if (
(this.platform === "mac" && event.metaKey) ||
(this.platform !== "mac" && event.ctrlKey)
) {
shortcutKey += `${this.modifierKeys.cmdOrCtrl}+`;
} else if (event.ctrlKey) {
shortcutKey += "ctrl+";
}
if (event.shiftKey) shortcutKey += "shift+";
if (event.altKey) shortcutKey += "alt+";
const key = event.key.toLowerCase();
// 特殊键处理
switch (key) {
case " ":
shortcutKey += "space";
break;
case "arrowup":
shortcutKey += "up";
break;
case "arrowdown":
shortcutKey += "down";
break;
case "arrowleft":
shortcutKey += "left";
break;
case "arrowright":
shortcutKey += "right";
break;
default:
shortcutKey += key;
}
return shortcutKey;
}
/**
* 检查当前是否有输入框处于活动状态
* @returns {boolean} 是否有输入框处于活动状态
*/
isInputActive() {
const activeElement = document.activeElement;
const tagName = activeElement.tagName.toLowerCase();
return (
tagName === "input" ||
tagName === "textarea" ||
activeElement.getAttribute("contenteditable") === "true"
);
}
/**
* 获取所有可用的快捷键
* @returns {Array} 快捷键列表
*/
getShortcuts() {
return Object.entries(this.shortcuts).map(([key, value]) => ({
key,
displayKey: this.formatShortcutForDisplay(key),
...value,
}));
}
/**
* 格式化快捷键以便显示
* @param {string} shortcut 快捷键标识符
* @returns {string} 格式化后的快捷键显示
*/
formatShortcutForDisplay(shortcut) {
// 将快捷键格式化为适合当前平台显示的形式
return shortcut
.split("+")
.map((key) => {
// 将键名转换为显示符号
return this.keySymbols[key.toLowerCase()] || key.toUpperCase();
})
.join("+");
}
/**
* 注册自定义快捷键处理程序
* @param {string} action 动作名称
* @param {Object} handler 处理程序对象
* @param {Function} handler.execute 执行函数
* @param {Function} handler.onKeyUp 键释放处理函数(可选)
* @param {string} description 描述
*/
registerCustomHandler(action, handler, description = "") {
if (!action || typeof handler.execute !== "function") {
console.error("无效的自定义处理程序");
return;
}
this.customHandlers[action] = handler;
// 如果提供了快捷键,添加到快捷键映射
if (handler.shortcut) {
this.shortcuts[handler.shortcut] = {
action,
description: description || handler.description || action,
};
}
}
/**
* 清理资源
*/
dispose() {
// 移除事件监听
this.container.removeEventListener("keydown", this._handleKeyDown);
this.container.removeEventListener("keyup", this._handleKeyUp);
// 如果有触摸事件,也移除它们
if (this.isTouchDevice) {
this.container.removeEventListener("touchstart", this._handleTouchStart);
this.container.removeEventListener("touchmove", this._handleTouchMove);
this.container.removeEventListener("touchend", this._handleTouchEnd);
this.container.removeEventListener("touchcancel", this._handleTouchEnd);
}
// 清除引用
this.toolManager = null;
this.commandManager = null;
this.layerManager = null;
this.container = null;
this.customHandlers = {};
this.tempToolState = { active: false, originalTool: null };
this.touchState = {};
}
}

View File

@@ -0,0 +1,702 @@
/**
* 增强版液化管理器
* 整合WebGL和CPU实现智能选择最佳渲染方式
*/
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
import { LiquifyCPUManager } from "./LiquifyCPUManager";
export class EnhancedLiquifyManager {
/**
* 创建增强版液化管理器
* @param {Object} options 配置选项
*/
constructor(options = {}) {
this.config = {
// 性能阈值图像超过此尺寸会尝试使用WebGL
webglSizeThreshold: options.webglSizeThreshold || 1000 * 1000, // 默认100万像素
// 是否强制使用CPU模式
forceCPU: options.forceCPU || false,
// 是否强制使用WebGL模式
forceWebGL: options.forceWebGL || false,
// 网格大小
gridSize: options.gridSize || 15,
// 最大变形强度
maxStrength: options.maxStrength || 100,
// 平滑迭代次数
smoothingIterations: options.smoothingIterations || 2,
// 网格弹性因子
relaxFactor: options.relaxFactor || 0.25,
// WebGL网格精度
meshResolution: options.meshResolution || 64,
};
// 性能监控
this.performance = {
lastOperationTime: 0,
renderTimes: [], // 最近的渲染时间记录
isPerformanceIssue: false, // 是否存在性能问题
operationCount: 0, // 操作计数
};
// 初始化标志
this.initialized = false;
// 当前参数
this.params = {
size: 50, // 工具尺寸
pressure: 0.5, // 压力大小 (0-1)
distortion: 0, // 失真程度 (0-1)
power: 0.5, // 动力/强度 (0-1)
};
// 液化工具模式
this.modes = {
PUSH: "push",
CLOCKWISE: "clockwise",
COUNTERCLOCKWISE: "counterclockwise",
PINCH: "pinch",
EXPAND: "expand",
CRYSTAL: "crystal",
EDGE: "edge",
RECONSTRUCT: "reconstruct",
};
// 当前模式
this.currentMode = this.modes.PUSH;
// 图像数据和目标对象
this.originalImageData = null;
this.currentImageData = null;
this.targetObject = null;
this.targetLayerId = null;
// 创建渲染器实例
this.webglRenderer = null;
this.cpuRenderer = null;
// 当前激活的渲染器
this.activeRenderer = null;
this.renderMode = "unknown"; // 'webgl', 'cpu', 'unknown'
// 画布和管理器引用
this.canvas = options.canvas || null;
this.layerManager = options.layerManager || null;
// 渲染器状态
this.isWebGLAvailable = LiquifyWebGLManager.isSupported();
}
/**
* 初始化液化管理器
* @param {Object} options 配置选项
* @returns {Boolean} 是否初始化成功
*/
initialize(options = {}) {
if (options.canvas) this.canvas = options.canvas;
if (options.layerManager) this.layerManager = options.layerManager;
if (!this.canvas || !this.layerManager) {
console.error("液化管理器初始化失败缺少canvas或layerManager");
return false;
}
// 记录初始化时间,用于性能监控
this.performance.lastInitTime = Date.now();
// 创建CPU渲染器 (始终创建作为备选)
this.cpuRenderer = new LiquifyCPUManager({
gridSize: this.config.gridSize,
maxStrength: this.config.maxStrength,
smoothingIterations: this.config.smoothingIterations,
relaxFactor: this.config.relaxFactor,
});
// 检查是否应创建WebGL渲染器
if (this.isWebGLAvailable && !this.config.forceCPU) {
this.webglRenderer = new LiquifyWebGLManager({
gridSize: this.config.gridSize,
maxStrength: this.config.maxStrength,
meshResolution: this.config.meshResolution,
});
}
this.initialized = true;
return true;
}
/**
* 为液化操作准备图像
* @param {Object|String} target 目标对象或图层ID
* @returns {Promise<Object>} 准备结果
*/
async prepareForLiquify(target) {
if (!this.initialized) {
throw new Error("液化管理器未初始化");
}
let targetObject, targetLayerId;
// 处理传入的是图层ID的情况
if (typeof target === "string") {
targetLayerId = target;
const layer = this.layerManager.getLayerById(targetLayerId);
// 检查图层是否存在和是否有对象
let hasObjects = false;
if (layer) {
if (layer.type === "background" && layer.fabricObject) {
hasObjects = true;
targetObject = layer.fabricObject;
} else if (layer.fabricObjects && layer.fabricObjects.length > 0) {
hasObjects = true;
targetObject = layer.fabricObjects[0];
}
}
if (!hasObjects) {
throw new Error("目标图层为空或不存在");
}
} else if (typeof target === "object") {
// 传入的是对象
targetObject = target;
const layer = this.layerManager.findLayerByObject(targetObject);
if (layer) {
targetLayerId = layer.id;
} else {
throw new Error("无法找到目标对象所属图层");
}
} else {
throw new Error("无效的目标参数");
}
// 检查是否为图像对象
if (!targetObject || targetObject.type !== "image") {
throw new Error("目标对象不是图像,无法进行液化操作");
}
// 保存目标对象引用
this.targetObject = targetObject;
this.targetLayerId = targetLayerId;
// 获取图像数据
const imageData = await this._getImageData(targetObject);
if (!imageData) {
throw new Error("无法获取图像数据");
}
// 保存原始图像数据
this.originalImageData = imageData;
this.currentImageData = this._cloneImageData(imageData);
// 检查图像大小,选择适合的渲染器
await this._selectRenderer(imageData);
// 预热选定的渲染器
await this._warmupRenderer(imageData);
return {
targetObject: this.targetObject,
targetLayerId: this.targetLayerId,
imageData: this.currentImageData,
originalImageData: this.originalImageData,
renderMode: this.renderMode,
};
}
/**
* 根据图像大小和设备性能选择渲染器
* @param {ImageData} imageData 图像数据
* @private
*/
async _selectRenderer(imageData) {
// 计算图像大小
const pixelCount = imageData.width * imageData.height;
console.log(
`液化选择渲染器: 图像大小=${pixelCount}像素, WebGL可用=${this.isWebGLAvailable}`
);
// 默认使用CPU渲染器
this.activeRenderer = this.cpuRenderer;
this.renderMode = "cpu";
// 如果配置强制使用WebGL
if (this.config.forceWebGL && this.isWebGLAvailable && this.webglRenderer) {
console.log("液化功能: 强制使用WebGL渲染模式");
this.activeRenderer = this.webglRenderer;
this.renderMode = "webgl";
return;
}
// 如果配置强制使用CPU
if (this.config.forceCPU) {
console.log("液化功能: 强制使用CPU渲染模式");
return;
}
// 根据图像大小和WebGL可用性决定
if (
pixelCount > this.config.webglSizeThreshold / 2 && // 降低阈值让更多尺寸的图像使用WebGL
this.isWebGLAvailable &&
this.webglRenderer
) {
// 切换到WebGL渲染器
console.log("液化功能: 自动选择WebGL渲染模式(基于图像尺寸)");
this.activeRenderer = this.webglRenderer;
this.renderMode = "webgl";
} else {
console.log(
`液化功能: 使用CPU渲染模式${
!this.isWebGLAvailable ? " (WebGL不可用)" : ""
}`
);
}
}
/**
* 预热渲染器
* @param {ImageData} imageData 图像数据
* @private
*/
async _warmupRenderer(imageData) {
// 创建图像元素
const img = document.createElement("img");
// 将ImageData转换为URL
const canvas = document.createElement("canvas");
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext("2d");
ctx.putImageData(imageData, 0, 0);
// 使用Promise等待图像加载
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = canvas.toDataURL();
});
// 初始化当前渲染器
if (this.activeRenderer) {
if (this.renderMode === "webgl") {
this.activeRenderer.initialize(img);
} else {
this.activeRenderer.initialize(imageData);
}
}
}
/**
* 设置液化模式
* @param {String} mode 模式名称
*/
setMode(mode) {
if (Object.values(this.modes).includes(mode)) {
this.currentMode = mode;
// 同步更新当前渲染器
if (this.activeRenderer) {
this.activeRenderer.setMode(mode);
}
return true;
}
return false;
}
/**
* 设置液化参数
* @param {String} param 参数名称
* @param {Number} value 参数值
*/
setParam(param, value) {
if (param in this.params) {
this.params[param] = value;
// 同步更新当前渲染器
if (this.activeRenderer) {
this.activeRenderer.setParam(param, value);
}
return true;
}
return false;
}
/**
* 获取当前参数
* @returns {Object} 当前参数对象
*/
getParams() {
return { ...this.params };
}
/**
* 重置参数为默认值
*/
resetParams() {
this.params = {
size: 50,
pressure: 0.5,
distortion: 0,
power: 0.5,
};
// 同步更新当前渲染器
if (this.activeRenderer) {
this.activeRenderer.resetParams();
}
}
/**
* 应用液化变形
* @param {Object} target 目标对象
* @param {String} mode 液化模式
* @param {Object} params 液化参数
* @param {Number} x 操作中心点X坐标 (图像像素坐标)
* @param {Number} y 操作中心点Y坐标 (图像像素坐标)
* @returns {Promise<ImageData>} 处理后的图像数据
*/
async applyLiquify(target, mode, params, x, y) {
// 性能追踪开始
const startTime = performance.now();
// 如果首次调用,先准备环境
if (!this.targetObject || this.targetObject !== target) {
await this.prepareForLiquify(target);
}
// 更新模式和参数
if (mode) this.setMode(mode);
if (params) {
for (const [key, value] of Object.entries(params)) {
this.setParam(key, value);
}
}
// 验证坐标是否在图像范围内
if (!this.originalImageData) {
console.error("缺少原始图像数据");
return null;
}
const imageWidth = this.originalImageData.width;
const imageHeight = this.originalImageData.height;
// 坐标边界检查
if (x < 0 || x >= imageWidth || y < 0 || y >= imageHeight) {
console.warn(
`液化坐标超出图像范围: (${x}, ${y}), 图像尺寸: ${imageWidth}x${imageHeight}`
);
return null;
}
console.log(
`应用液化变形: 模式=${mode}, 图像坐标=(${x}, ${y}), 图像尺寸=${imageWidth}x${imageHeight}`
);
// 检查并应用变形
if (this.activeRenderer && typeof x === "number" && typeof y === "number") {
// 应用变形
let result;
if (this.renderMode === "webgl") {
// WebGL渲染器传入图像像素坐标
result = this.activeRenderer.applyDeformation(x, y);
} else {
// CPU渲染器传入图像像素坐标
result = this.activeRenderer.applyDeformation(x, y);
}
// 更新当前图像数据
if (result) {
this.currentImageData = result;
}
// 性能追踪结束
const endTime = performance.now();
this._trackPerformance(endTime - startTime);
return result;
}
console.error("无法应用液化变形:渲染器未初始化或坐标无效");
return null;
}
/**
* 追踪性能数据
* @param {Number} time 操作耗时(毫秒)
* @private
*/
_trackPerformance(time) {
this.performance.lastOperationTime = time;
this.performance.operationCount++;
// 维护最近10次操作的耗时记录
this.performance.renderTimes.push(time);
if (this.performance.renderTimes.length > 10) {
this.performance.renderTimes.shift();
}
// 计算平均耗时
const avgTime =
this.performance.renderTimes.reduce((sum, t) => sum + t, 0) /
this.performance.renderTimes.length;
// 检测性能问题
this.performance.isPerformanceIssue = avgTime > 100; // 如果平均耗时超过100毫秒
// 输出性能信息(调试用)
if (this.performance.operationCount % 10 === 0) {
console.log(
`液化性能数据: 模式=${this.renderMode}, 平均耗时=${avgTime.toFixed(
2
)}ms, 图像尺寸=${this.originalImageData?.width}x${
this.originalImageData?.height
}`
);
}
// 如果使用WebGL但性能差可以考虑切换到优化的CPU实现
if (
this.renderMode === "webgl" &&
this.performance.isPerformanceIssue &&
this.performance.operationCount > 5
) {
console.warn("WebGL液化性能不佳考虑切换到CPU模式");
// 注意:这里不自动切换,因为可能会导致中途渲染结果不一致
}
}
/**
* 重置液化操作
* @returns {ImageData} 重置后的图像数据
*/
reset() {
if (!this.activeRenderer) return null;
// 使用当前渲染器重置
const result = this.activeRenderer.reset();
// 更新当前图像数据
if (result) {
this.currentImageData = result;
}
return result;
}
/**
* 检查图层是否可以液化
* @param {String} layerId 图层ID
* @returns {Object} 检查结果
*/
checkLayerForLiquify(layerId) {
if (!this.layerManager) {
return {
valid: false,
message: "图层管理器未初始化",
needsRasterization: false,
isImage: false,
isEmpty: true,
isGroup: false,
};
}
// 获取图层
const layer = this.layerManager.getLayerById(layerId);
if (!layer) {
return {
valid: false,
message: "图层不存在",
needsRasterization: false,
isImage: false,
isEmpty: true,
isGroup: false,
};
}
// 检查图层是否为空
let objectsToCheck = [];
if (layer.isBackground || layer.type === "background") {
// 背景图层使用 fabricObject (单数)
if (layer.fabricObject) {
objectsToCheck = [layer.fabricObject];
}
} else {
// 普通图层使用 fabricObjects (复数)
objectsToCheck = layer.fabricObjects || [];
}
if (objectsToCheck.length === 0) {
return {
valid: false,
message: "图层为空,无法进行液化操作",
needsRasterization: false,
isImage: false,
isEmpty: true,
isGroup: false,
};
}
// 检查是否为单一图像
const singleObject = objectsToCheck.length === 1;
const isImage =
singleObject &&
(objectsToCheck[0].type === "image" ||
objectsToCheck[0].type === "rasterized-layer");
// 检查是否为组
const isGroup = objectsToCheck.some((obj) => obj.type === "group");
// 如果不是单一图像,需要栅格化
const needsRasterization = !isImage || isGroup;
return {
valid: isImage && !isGroup,
message: isImage ? "图层可以进行液化操作" : "需要先将图层栅格化",
needsRasterization: needsRasterization,
isImage: isImage,
isEmpty: false,
isGroup: isGroup,
};
}
/**
* 获取图像数据
* @param {Object} fabricObject Fabric图像对象
* @returns {Promise<ImageData>} 图像数据
* @private
*/
async _getImageData(fabricObject) {
return new Promise((resolve, reject) => {
try {
// 创建临时canvas
const tempCanvas = document.createElement("canvas");
tempCanvas.width = fabricObject.width * fabricObject.scaleX;
tempCanvas.height = fabricObject.height * fabricObject.scaleY;
const tempCtx = tempCanvas.getContext("2d");
// 如果对象有图像元素
if (fabricObject._element) {
tempCtx.drawImage(
fabricObject._element,
0,
0,
tempCanvas.width,
tempCanvas.height
);
} else if (fabricObject.getSrc) {
// 通过URL创建图像
const img = new Image();
img.onload = () => {
tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height);
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
resolve(imageData);
};
img.onerror = reject;
img.src = fabricObject.getSrc();
return;
} else {
reject(new Error("无法获取图像数据"));
return;
}
// 获取图像数据
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
resolve(imageData);
} catch (error) {
reject(error);
}
});
}
/**
* 克隆图像数据
* @param {ImageData} imageData 原始图像数据
* @returns {ImageData} 克隆的图像数据
* @private
*/
_cloneImageData(imageData) {
if (!imageData) return null;
// 使用新的浏览器API直接复制
if (typeof ImageData.prototype.constructor === "function") {
try {
return new ImageData(
new Uint8ClampedArray(imageData.data),
imageData.width,
imageData.height
);
} catch (e) {
console.warn("使用备选方法克隆ImageData");
}
}
// 备选方法
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = imageData.width;
canvas.height = imageData.height;
ctx.putImageData(imageData, 0, 0);
return ctx.getImageData(0, 0, imageData.width, imageData.height);
}
/**
* 释放资源
*/
dispose() {
// 释放渲染器资源
if (this.webglRenderer) {
this.webglRenderer.dispose();
this.webglRenderer = null;
}
if (this.cpuRenderer) {
this.cpuRenderer.dispose();
this.cpuRenderer = null;
}
// 清除引用
this.activeRenderer = null;
this.canvas = null;
this.layerManager = null;
this.targetObject = null;
this.originalImageData = null;
this.currentImageData = null;
this.initialized = false;
this.renderMode = "unknown";
}
/**
* 获取当前状态信息
* @returns {Object} 状态信息
*/
getStatus() {
return {
initialized: this.initialized,
renderMode: this.renderMode,
isWebGLAvailable: this.isWebGLAvailable,
currentMode: this.currentMode,
params: { ...this.params },
performance: { ...this.performance },
imageSize: this.originalImageData
? `${this.originalImageData.width}x${this.originalImageData.height}`
: "N/A",
};
}
}

View File

@@ -0,0 +1,594 @@
/**
* CPU版本的液化管理器
* 修复版本 - 解决三角形网格失真问题
*/
export class LiquifyCPUManager {
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.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.deformHistory = [];
// 性能优化相关
this.lastUpdateTime = 0;
this.updateThrottle = 16; // 限制更新频率约60fps
this.isProcessing = false;
// 鼠标位置跟踪(用于推拉模式)
this.lastMouseX = 0;
this.lastMouseY = 0;
this.mouseMovementX = 0;
this.mouseMovementY = 0;
this.isFirstApply = true; // 标记是否是首次应用
}
initialize(imageSource) {
try {
if (imageSource instanceof ImageData) {
this.originalImageData = new ImageData(
new Uint8ClampedArray(imageSource.data),
imageSource.width,
imageSource.height
);
} else if (imageSource instanceof HTMLImageElement) {
this.canvas.width = imageSource.width;
this.canvas.height = imageSource.height;
this.ctx.drawImage(imageSource, 0, 0);
this.originalImageData = this.ctx.getImageData(
0,
0,
imageSource.width,
imageSource.height
);
} else {
throw new Error("不支持的图像类型");
}
this.currentImageData = new ImageData(
new Uint8ClampedArray(this.originalImageData.data),
this.originalImageData.width,
this.originalImageData.height
);
this._initMesh(
this.originalImageData.width,
this.originalImageData.height
);
this.initialized = true;
return true;
} catch (error) {
console.error("液化管理器初始化失败:", error);
return false;
}
}
_initMesh(width, height) {
const gridSize = this.config.gridSize;
const cols = Math.ceil(width / gridSize);
const rows = Math.ceil(height / gridSize);
this.mesh = {
cols,
rows,
gridSize,
width,
height,
originalPoints: [],
deformedPoints: [],
};
for (let y = 0; y <= rows; y++) {
for (let x = 0; x <= cols; x++) {
const point = { x: x * gridSize, y: y * gridSize };
this.mesh.originalPoints.push({ ...point });
this.mesh.deformedPoints.push({ ...point });
}
}
}
setMode(mode) {
if (Object.values(this.modes).includes(mode)) {
this.currentMode = mode;
return true;
}
return false;
}
setParam(param, value) {
if (param in this.params) {
this.params[param] = value;
return true;
}
return false;
}
getParams() {
return { ...this.params };
}
resetParams() {
this.params = {
size: 80, // 增大默认尺寸
pressure: 0.8, // 增大默认压力
distortion: 0,
power: 0.8, // 增大默认动力
};
}
applyDeformation(x, y) {
// 计算鼠标移动方向
if (!this.isFirstApply) {
this.mouseMovementX = x - this.lastMouseX;
this.mouseMovementY = y - this.lastMouseY;
} else {
// 首次应用时不计算移动,避免初始变形
this.mouseMovementX = 0;
this.mouseMovementY = 0;
this.isFirstApply = false;
}
this.lastMouseX = x;
this.lastMouseY = y;
// 性能优化:限制更新频率
const now = Date.now();
if (now - this.lastUpdateTime < this.updateThrottle || this.isProcessing) {
return this.currentImageData;
}
this.isProcessing = true;
this.lastUpdateTime = now;
if (!this.initialized || !this.mesh) {
this.isProcessing = false;
return this.currentImageData;
}
const { size, pressure, distortion, power } = this.params;
const mode = this.currentMode;
const radius = size * 1.2; // 稍微增大影响半径
const strength = (pressure * power * this.config.maxStrength) / 20; // 调整基础强度
this._applyDeformation(x, y, radius, strength, mode, distortion);
if (this.config.smoothingIterations > 0) {
this._smoothMesh();
}
const result = this._applyMeshToImage();
this.isProcessing = false;
return result;
}
_applyDeformation(x, y, radius, strength, mode, distortion) {
if (!this.mesh) return;
const points = this.mesh.deformedPoints;
const originalPoints = this.mesh.originalPoints;
for (let i = 0; i < points.length; i++) {
const point = points[i];
const originalPoint = originalPoints[i];
const dx = point.x - x;
const dy = point.y - y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius && distance > 0) {
// 使用平方衰减函数
const factor = Math.pow(1 - distance / radius, 2) * strength * 0.1; // 大幅降低基础系数
switch (mode) {
case this.modes.PUSH: {
// 推拉模式 - 真正的拖拽效果
// 计算实际移动距离
const movementLength = Math.sqrt(
this.mouseMovementX * this.mouseMovementX +
this.mouseMovementY * this.mouseMovementY
);
// 只有在有足够移动距离时才应用效果
if (movementLength > 1.0) {
// 提高阈值,确保有明显移动
// 归一化移动方向
const moveX = this.mouseMovementX / movementLength;
const moveY = this.mouseMovementY / movementLength;
// 计算衰减(距离中心越近,效果越强)
const radiusRatio = distance / radius;
const falloff = Math.pow(1 - radiusRatio, 2.0); // 使用更强的衰减
// 基于实际移动距离计算强度
const { pressure, power } = this.params;
const moveStrength = pressure * power * movementLength * 0.3; // 降低移动强度系数
// 计算最终拖拽强度
const dragStrength = moveStrength * falloff * factor;
// 向鼠标移动方向拖拽
const dragX = moveX * dragStrength;
const dragY = moveY * dragStrength;
// 应用变形,但限制最大变形量
const maxDeform = 2.0; // 限制单次最大变形量
point.x += Math.max(-maxDeform, Math.min(maxDeform, dragX));
point.y += Math.max(-maxDeform, Math.min(maxDeform, dragY));
}
break;
}
case this.modes.CLOCKWISE:
case this.modes.COUNTERCLOCKWISE: {
// 旋转模式 - 保持原有效果
const angle = Math.atan2(dy, dx);
const direction = mode === this.modes.CLOCKWISE ? 1 : -1;
const rotationAngle = angle + direction * factor;
const newX = x + Math.cos(rotationAngle) * distance;
const newY = y + Math.sin(rotationAngle) * distance;
point.x += (newX - point.x) * 0.8;
point.y += (newY - point.y) * 0.8;
break;
}
case this.modes.PINCH: {
// 捏合模式 - 保持原有效果
const pinchStrength = factor * 1.2;
point.x -= dx * pinchStrength;
point.y -= dy * pinchStrength;
break;
}
case this.modes.EXPAND: {
// 展开模式 - 参考捏合的反向操作
const expandFactor = factor * 1.5;
point.x += dx * expandFactor;
point.y += dy * expandFactor;
break;
}
case this.modes.CRYSTAL: {
// 水晶模式 - 参考旋转算法创建多重波形
const crystalAngle = Math.atan2(dy, dx);
const crystalRadius = distance / radius;
// 确保有基础效果
const baseDistortion = Math.max(distortion, 0.3);
// 多重波形 - 类似旋转的角度调制
const wave1 = Math.sin(crystalAngle * 8) * 0.6;
const wave2 = Math.cos(crystalAngle * 12) * 0.4;
const waveAngle = crystalAngle + (wave1 + wave2) * baseDistortion;
// 径向调制 - 类似旋转的距离调制
const radialMod = 1 + Math.sin(crystalRadius * Math.PI * 2) * 0.3;
const modDistance = distance * radialMod;
const crystalX = x + Math.cos(waveAngle) * modDistance;
const crystalY = y + Math.sin(waveAngle) * modDistance;
const crystalFactor = factor * baseDistortion;
point.x += (crystalX - point.x) * crystalFactor;
point.y += (crystalY - point.y) * crystalFactor;
break;
}
case this.modes.EDGE: {
// 边缘模式 - 参考旋转算法创建垂直波纹
const edgeAngle = Math.atan2(dy, dx);
const edgeRadius = distance / radius;
// 确保有基础效果
const baseEdgeDistortion = Math.max(distortion, 0.5);
// 边缘波纹 - 垂直于径向的调制
const edgeWave =
Math.sin(edgeRadius * Math.PI * 4) * Math.cos(edgeAngle * 6);
const perpAngle = edgeAngle + Math.PI / 2; // 垂直角度
const edgeFactor = edgeWave * factor * baseEdgeDistortion;
const edgeOffsetX = Math.cos(perpAngle) * edgeFactor;
const edgeOffsetY = Math.sin(perpAngle) * edgeFactor;
point.x += edgeOffsetX;
point.y += edgeOffsetY;
break;
}
case this.modes.RECONSTRUCT: {
// 重建模式 - 向原始位置恢复
const restoreFactor = factor * 0.15;
point.x += (originalPoint.x - point.x) * restoreFactor;
point.y += (originalPoint.y - point.y) * restoreFactor;
break;
}
}
}
}
}
// 优化衰减函数,使过渡更平滑
_smoothFalloff(t) {
if (t >= 1) return 0;
// 使用更平滑的衰减曲线
const smoothT = 1 - t;
return smoothT * smoothT * smoothT * (3 - 2 * smoothT);
}
_smoothMesh() {
const { rows, cols } = this.mesh;
const points = this.mesh.deformedPoints;
const tempPoints = points.map((p) => ({ x: p.x, y: p.y }));
for (
let iteration = 0;
iteration < this.config.smoothingIterations;
iteration++
) {
for (let y = 1; y < rows; y++) {
for (let x = 1; x < cols; x++) {
const idx = y * (cols + 1) + x;
const left = points[y * (cols + 1) + (x - 1)];
const right = points[y * (cols + 1) + (x + 1)];
const top = points[(y - 1) * (cols + 1) + x];
const bottom = points[(y + 1) * (cols + 1) + x];
const centerX = (left.x + right.x + top.x + bottom.x) / 4;
const centerY = (left.y + right.y + top.y + bottom.y) / 4;
const relaxFactor = this.config.relaxFactor;
tempPoints[idx].x += (centerX - points[idx].x) * relaxFactor;
tempPoints[idx].y += (centerY - points[idx].y) * relaxFactor;
}
}
for (let i = 0; i < points.length; i++) {
points[i].x = tempPoints[i].x;
points[i].y = tempPoints[i].y;
}
}
}
_applyMeshToImage() {
if (!this.mesh || !this.originalImageData) {
return this.currentImageData;
}
const width = this.originalImageData.width;
const height = this.originalImageData.height;
const result = new ImageData(width, height);
const srcData = this.originalImageData.data;
const dstData = result.data;
// 性能优化:使用步长采样减少计算量
const step = width > 1000 || height > 1000 ? 2 : 1;
for (let y = 0; y < height; y += step) {
for (let x = 0; x < width; x += step) {
const srcPos = this._mapPointBack(x, y);
if (
srcPos.x >= 0 &&
srcPos.x < width &&
srcPos.y >= 0 &&
srcPos.y < height
) {
const color = this._bilinearInterpolate(
srcData,
width,
height,
srcPos.x,
srcPos.y
);
// 如果使用步长采样,需要填充相邻像素
for (let dy = 0; dy < step && y + dy < height; dy++) {
for (let dx = 0; dx < step && x + dx < width; dx++) {
const dstIdx = ((y + dy) * width + (x + dx)) * 4;
dstData[dstIdx] = color[0];
dstData[dstIdx + 1] = color[1];
dstData[dstIdx + 2] = color[2];
dstData[dstIdx + 3] = color[3];
}
}
}
}
}
this.currentImageData = result;
return result;
}
// 添加异步处理方法用于大图像
async applyDeformationAsync(x, y) {
return new Promise((resolve) => {
setTimeout(() => {
const result = this.applyDeformation(x, y);
resolve(result);
}, 0);
});
}
// 批量处理方法
applyDeformationBatch(positions) {
if (!this.initialized || !this.mesh || positions.length === 0) {
return this.currentImageData;
}
const { size, pressure, distortion, power } = this.params;
const mode = this.currentMode;
const radius = size * 1.0;
const strength = (pressure * power * this.config.maxStrength) / 60;
// 批量应用所有变形
positions.forEach((pos) => {
this._applyDeformation(
pos.x,
pos.y,
radius * 0.5,
strength * 0.3,
mode,
distortion
);
});
if (this.config.smoothingIterations > 0) {
this._smoothMesh();
}
return this._applyMeshToImage();
}
_mapPointBack(x, y) {
const { cols, rows, gridSize } = this.mesh;
const gridX = x / gridSize;
const gridY = y / gridSize;
const x1 = Math.floor(gridX);
const y1 = Math.floor(gridY);
const x2 = Math.min(x1 + 1, cols);
const y2 = Math.min(y1 + 1, rows);
const fx = gridX - x1;
const fy = gridY - y1;
// 获取四个网格点的变形和原始坐标
const deformed = [
this.mesh.deformedPoints[y1 * (cols + 1) + x1],
this.mesh.deformedPoints[y1 * (cols + 1) + x2],
this.mesh.deformedPoints[y2 * (cols + 1) + x1],
this.mesh.deformedPoints[y2 * (cols + 1) + x2],
];
const original = [
this.mesh.originalPoints[y1 * (cols + 1) + x1],
this.mesh.originalPoints[y1 * (cols + 1) + x2],
this.mesh.originalPoints[y2 * (cols + 1) + x1],
this.mesh.originalPoints[y2 * (cols + 1) + x2],
];
// 双线性插值计算变形后的位置
const deformedX =
(1 - fx) * (1 - fy) * deformed[0].x +
fx * (1 - fy) * deformed[1].x +
(1 - fx) * fy * deformed[2].x +
fx * fy * deformed[3].x;
const deformedY =
(1 - fx) * (1 - fy) * deformed[0].y +
fx * (1 - fy) * deformed[1].y +
(1 - fx) * fy * deformed[2].y +
fx * fy * deformed[3].y;
// 计算原始网格位置
const originalX = x1 * gridSize + fx * gridSize;
const originalY = y1 * gridSize + fy * gridSize;
// 计算偏移量并应用反向映射
const offsetX = deformedX - originalX;
const offsetY = deformedY - originalY;
return {
x: x - offsetX,
y: y - offsetY,
};
}
_bilinearInterpolate(data, width, height, x, y) {
const x1 = Math.floor(x);
const y1 = Math.floor(y);
const x2 = Math.min(x1 + 1, width - 1);
const y2 = Math.min(y1 + 1, height - 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]
),
];
}
reset() {
if (!this.mesh || !this.originalImageData) return false;
for (let i = 0; i < this.mesh.deformedPoints.length; i++) {
this.mesh.deformedPoints[i].x = this.mesh.originalPoints[i].x;
this.mesh.deformedPoints[i].y = this.mesh.originalPoints[i].y;
}
this.currentImageData = new ImageData(
new Uint8ClampedArray(this.originalImageData.data),
this.originalImageData.width,
this.originalImageData.height
);
this.deformHistory = [];
return true;
}
getCurrentImageData() {
return this.currentImageData;
}
destroy() {
this.originalImageData = null;
this.currentImageData = null;
this.mesh = null;
this.deformHistory = [];
this.initialized = false;
}
}

View File

@@ -0,0 +1,191 @@
/**
* 液化管理器
* 负责管理液化操作的核心算法和变形处理
*
* 此版本使用增强的液化算法支持GPU加速和优化的CPU处理
*/
import { EnhancedLiquifyManager } from "./EnhancedLiquifyManager";
export class LiquifyManager {
/**
* 创建液化管理器
* @param {Object} options 配置选项
*/
constructor(options = {}) {
// 将核心属性暴露给外部保持API兼容性
this.canvas = options.canvas || null;
this.layerManager = options.layerManager || null;
// 配置参数
this.config = {
gridSize: options.gridSize || 20,
maxStrength: options.maxStrength || 100,
defaultParams: {
size: 50,
pressure: 0.5,
distortion: 0,
power: 0.5,
},
};
// 创建增强版液化管理器实例
this.enhancedManager = new EnhancedLiquifyManager({
// 配置选项
gridSize: options.gridSize || 15,
maxStrength: options.maxStrength || 100,
smoothingIterations: options.smoothingIterations || 2,
relaxFactor: options.relaxFactor || 0.25,
meshResolution: options.meshResolution || 64,
// 根据环境选择合适的渲染模式
forceCPU: true, // 默认不强制使用CPU
forceWebGL: false, // 优先使用WebGL模式
webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL
layerManager: options.layerManager || null,
canvas: options.canvas || null,
});
// 初始化液化管理器
this.initialize();
}
/**
* 初始化液化管理器
* @param {Object} options 配置选项
*/
initialize(options = {}) {
// 更新基础属性
if (options.canvas) this.canvas = options.canvas;
if (options.layerManager) this.layerManager = options.layerManager;
// 初始化增强液化管理器
return this.enhancedManager.initialize({
canvas: this.canvas,
layerManager: this.layerManager,
});
}
/**
* 为液化操作准备图像
* @param {Object|String} target 目标对象或图层ID
* @returns {Promise<Object>} 准备结果
*/
async prepareForLiquify(target) {
return this.enhancedManager.prepareForLiquify(target);
}
/**
* 设置液化模式
* @param {String} mode 液化模式
*/
setMode(mode) {
return this.enhancedManager.setMode(mode);
}
/**
* 设置液化参数
* @param {String} param 参数名称
* @param {Number} value 参数值
*/
setParam(param, value) {
return this.enhancedManager.setParam(param, value);
}
/**
* 获取当前参数
* @returns {Object} 当前参数对象
*/
getParams() {
return this.enhancedManager.getParams();
}
/**
* 重置参数为默认值
*/
resetParams() {
return this.enhancedManager.resetParams();
}
/**
* 应用液化效果
* @param {fabric.Object} targetObject 目标对象
* @param {String} mode 液化模式
* @param {Object} params 参数
* @param {Number} x X坐标
* @param {Number} y Y坐标
* @returns {ImageData} 处理后的图像数据
*/
async applyLiquify(targetObject, mode, params, x, y) {
if (!this.enhancedManager || !targetObject) {
console.error("液化管理器未正确初始化");
return null;
}
// 确保设置正确的模式和参数
if (mode) {
this.enhancedManager.setMode(mode);
}
if (params) {
Object.entries(params).forEach(([key, value]) => {
this.enhancedManager.setParam(key, value);
});
}
// 应用液化变形
console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
try {
// 直接调用EnhancedLiquifyManager的applyLiquify方法
const resultData = await this.enhancedManager.applyLiquify(
targetObject,
mode,
params,
x,
y
);
// 确保返回结果数据
if (!resultData) {
console.warn("液化变形没有返回结果数据");
}
return resultData;
} catch (error) {
console.error("液化变形应用失败:", error);
return null;
}
}
/**
* 重置液化操作
* @returns {ImageData} 重置后的图像数据
*/
reset() {
return this.enhancedManager.reset();
}
/**
* 检查图层是否可以液化
* @param {String} layerId 图层ID
* @returns {Object} 检查结果
*/
checkLayerForLiquify(layerId) {
return this.enhancedManager.checkLayerForLiquify(layerId);
}
/**
* 获取当前状态信息
* @returns {Object} 状态信息
*/
getStatus() {
return this.enhancedManager.getStatus();
}
/**
* 释放资源
*/
dispose() {
if (this.enhancedManager) {
this.enhancedManager.dispose();
}
}
}

View File

@@ -0,0 +1,878 @@
/**
* WebGL加速的液化管理器
* 使用WebGL技术进行加速液化变形处理
*/
export class LiquifyWebGLManager {
/**
* 创建WebGL液化管理器
* @param {Object} options 配置选项
*/
constructor(options = {}) {
this.canvas = null;
this.gl = null;
this.program = null;
this.texture = null;
this.mesh = null;
this.initialized = false;
this.originalImageData = null;
this.currentImageData = null;
// 变形配置
this.config = {
gridSize: options.gridSize || 20,
maxStrength: options.maxStrength || 100,
textureSize: 0,
meshResolution: options.meshResolution || 64,
};
// 当前参数
this.params = {
size: 80, // 增大默认尺寸
pressure: 0.8, // 增大默认压力
distortion: 0,
power: 0.8, // 增大默认动力
};
// 鼠标位置跟踪(用于推拉模式)
this.lastMouseX = 0;
this.lastMouseY = 0;
this.mouseMovementX = 0;
this.mouseMovementY = 0;
this.isFirstApply = true; // 标记是否是首次应用
// 液化工具模式
this.modes = {
PUSH: "push",
CLOCKWISE: "clockwise",
COUNTERCLOCKWISE: "counterclockwise",
PINCH: "pinch",
EXPAND: "expand",
CRYSTAL: "crystal",
EDGE: "edge",
RECONSTRUCT: "reconstruct",
};
this.currentMode = this.modes.PUSH;
// 变形点历史记录
this.deformHistory = [];
// WebGL着色器程序
this.vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
uniform mat3 u_matrix;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
v_texCoord = a_texCoord;
}
`;
this.fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_textureSize;
varying vec2 v_texCoord;
void main() {
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
vec4 color = texture2D(u_image, v_texCoord);
// 简单的边缘检查,保证边缘渲染正确
if(v_texCoord.x < 0.0 || v_texCoord.x > 1.0 ||
v_texCoord.y < 0.0 || v_texCoord.y > 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
} else {
gl_FragColor = color;
}
}
`;
// 变形网格着色器程序
this.deformVertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
attribute vec2 a_deformation;
varying vec2 v_texCoord;
void main() {
vec2 position = a_position + a_deformation;
gl_Position = vec4(position * 2.0 - 1.0, 0, 1);
v_texCoord = a_texCoord;
}
`;
this.deformFragmentShaderSource = `
precision mediump float;
uniform sampler2D u_image;
varying vec2 v_texCoord;
void main() {
vec4 color = texture2D(u_image, v_texCoord);
gl_FragColor = color;
}
`;
}
/**
* 初始化WebGL环境
* @param {HTMLImageElement} image 图像元素
* @returns {Boolean} 是否初始化成功
*/
initialize(image) {
// 创建WebGL Canvas
this.canvas = document.createElement("canvas");
// 设置canvas大小与图像相同
this.canvas.width = image.width;
this.canvas.height = image.height;
// 尝试获取WebGL上下文
try {
this.gl =
this.canvas.getContext("webgl") ||
this.canvas.getContext("experimental-webgl");
} catch (e) {
console.error("WebGL初始化失败:", e);
return false;
}
if (!this.gl) {
console.error("WebGL不可用");
return false;
}
// 设置视口
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
// 编译着色器程序
if (!this._createShaderProgram()) {
console.error("着色器程序创建失败");
return false;
}
// 创建纹理
this.texture = this._createTexture(image);
if (!this.texture) {
console.error("纹理创建失败");
return false;
}
// 记录原始图像数据
const tempCanvas = document.createElement("canvas");
tempCanvas.width = image.width;
tempCanvas.height = image.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.drawImage(image, 0, 0);
this.originalImageData = tempCtx.getImageData(
0,
0,
image.width,
image.height
);
this.currentImageData = new ImageData(
new Uint8ClampedArray(this.originalImageData.data),
this.originalImageData.width,
this.originalImageData.height
);
// 创建变形网格
this._createDeformMesh();
this.config.textureSize = [image.width, image.height];
this.initialized = true;
return true;
}
/**
* 创建着色器程序
* @returns {Boolean} 是否创建成功
* @private
*/
_createShaderProgram() {
// 创建标准渲染程序
const vertexShader = this._compileShader(
this.vertexShaderSource,
this.gl.VERTEX_SHADER
);
const fragmentShader = this._compileShader(
this.fragmentShaderSource,
this.gl.FRAGMENT_SHADER
);
if (!vertexShader || !fragmentShader) return false;
// 创建程序
this.program = this.gl.createProgram();
this.gl.attachShader(this.program, vertexShader);
this.gl.attachShader(this.program, fragmentShader);
this.gl.linkProgram(this.program);
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
console.error(
"着色器程序链接失败:",
this.gl.getProgramInfoLog(this.program)
);
return false;
}
// 创建变形渲染程序
const deformVertexShader = this._compileShader(
this.deformVertexShaderSource,
this.gl.VERTEX_SHADER
);
const deformFragmentShader = this._compileShader(
this.deformFragmentShaderSource,
this.gl.FRAGMENT_SHADER
);
if (!deformVertexShader || !deformFragmentShader) return false;
// 创建变形程序
this.deformProgram = this.gl.createProgram();
this.gl.attachShader(this.deformProgram, deformVertexShader);
this.gl.attachShader(this.deformProgram, deformFragmentShader);
this.gl.linkProgram(this.deformProgram);
if (!this.gl.getProgramParameter(this.deformProgram, this.gl.LINK_STATUS)) {
console.error(
"变形着色器程序链接失败:",
this.gl.getProgramInfoLog(this.deformProgram)
);
return false;
}
return true;
}
/**
* 编译着色器
* @param {String} source 着色器源码
* @param {Number} type 着色器类型
* @returns {WebGLShader} 编译后的着色器
* @private
*/
_compileShader(source, type) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error(
"着色器编译失败:",
this.gl.getShaderInfoLog(shader),
"shader type:",
type === this.gl.VERTEX_SHADER ? "VERTEX_SHADER" : "FRAGMENT_SHADER",
"source:",
source
);
this.gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* 创建WebGL纹理
* @param {HTMLImageElement} image 图像元素
* @returns {WebGLTexture} WebGL纹理
* @private
*/
_createTexture(image) {
const texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
// 设置参数,使我们可以渲染任何尺寸的图像
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_S,
this.gl.CLAMP_TO_EDGE
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_WRAP_T,
this.gl.CLAMP_TO_EDGE
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MIN_FILTER,
this.gl.LINEAR
);
this.gl.texParameteri(
this.gl.TEXTURE_2D,
this.gl.TEXTURE_MAG_FILTER,
this.gl.LINEAR
);
// 上传图像到纹理
try {
this.gl.texImage2D(
this.gl.TEXTURE_2D,
0,
this.gl.RGBA,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
image
);
} catch (e) {
console.error("纹理上传失败:", e);
return null;
}
return texture;
}
/**
* 创建变形网格
* @private
*/
_createDeformMesh() {
const { meshResolution } = this.config;
// 创建网格顶点
const vertices = [];
const texCoords = [];
const indices = [];
const deformations = [];
// 创建顶点和纹理坐标
for (let y = 0; y <= meshResolution; y++) {
for (let x = 0; x <= meshResolution; x++) {
const xPos = x / meshResolution;
const yPos = y / meshResolution;
// 顶点位置
vertices.push(xPos, yPos);
// 纹理坐标
texCoords.push(xPos, yPos);
// 初始无变形
deformations.push(0, 0);
}
}
// 创建索引(三角形)
for (let y = 0; y < meshResolution; y++) {
for (let x = 0; x < meshResolution; x++) {
const i0 = y * (meshResolution + 1) + x;
const i1 = i0 + 1;
const i2 = i0 + meshResolution + 1;
const i3 = i2 + 1;
// 三角形1
indices.push(i0, i2, i1);
// 三角形2
indices.push(i1, i2, i3);
}
}
this.mesh = {
vertices: new Float32Array(vertices),
texCoords: new Float32Array(texCoords),
indices: new Uint16Array(indices),
deformations: new Float32Array(deformations),
resolution: meshResolution,
};
// 创建顶点缓冲区
this.vertexBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
this.mesh.vertices,
this.gl.STATIC_DRAW
);
// 创建纹理坐标缓冲区
this.texCoordBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
this.mesh.texCoords,
this.gl.STATIC_DRAW
);
// 创建变形缓冲区
this.deformBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
this.mesh.deformations,
this.gl.DYNAMIC_DRAW
);
// 创建索引缓冲区
this.indexBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
this.gl.bufferData(
this.gl.ELEMENT_ARRAY_BUFFER,
this.mesh.indices,
this.gl.STATIC_DRAW
);
}
/**
* 应用液化变形
* @param {Number} x 变形中心X坐标 (图像像素坐标)
* @param {Number} y 变形中心Y坐标 (图像像素坐标)
*/
applyDeformation(x, y) {
if (!this.initialized || !this.mesh) return;
// 计算鼠标移动方向
if (!this.isFirstApply) {
this.mouseMovementX = x - this.lastMouseX;
this.mouseMovementY = y - this.lastMouseY;
} else {
// 首次应用时不计算移动,避免初始变形
this.mouseMovementX = 0;
this.mouseMovementY = 0;
this.isFirstApply = false;
}
this.lastMouseX = x;
this.lastMouseY = y;
// 将图像像素坐标转换为纹理坐标 (0-1范围)
// 使用原始图像数据的尺寸进行归一化而不是WebGL canvas的尺寸
const imageWidth = this.originalImageData
? this.originalImageData.width
: this.canvas.width;
const imageHeight = this.originalImageData
? this.originalImageData.height
: this.canvas.height;
const tx = x / imageWidth;
const ty = y / imageHeight;
console.log(
`WebGL变形: 像素坐标(${x}, ${y}) -> 纹理坐标(${tx.toFixed(
3
)}, ${ty.toFixed(3)}), 图像尺寸(${imageWidth}x${imageHeight})`
);
// 获取当前参数
const { size, pressure, distortion, power } = this.params;
const mode = this.currentMode;
// 计算影响半径 (纹理坐标空间)
const radius = (size / 100) * 0.2; // 调整半径计算,使效果更自然
const strength = (pressure * power * this.config.maxStrength) / 800; // 进一步降低基础强度
// 保存当前变形点,用于重建功能
this.deformHistory.push({
x: tx,
y: ty,
radius,
strength,
mode,
distortion,
});
// 对网格顶点应用变形
const { resolution } = this.mesh;
const deformations = this.mesh.deformations;
for (let i = 0; i <= resolution; i++) {
for (let j = 0; j <= resolution; j++) {
const idx = (i * (resolution + 1) + j) * 2;
// 顶点在纹理空间中的位置
const vx = j / resolution;
const vy = i / resolution;
// 计算到变形中心的距离
const dx = vx - tx;
const dy = vy - ty;
const distance = Math.sqrt(dx * dx + dy * dy);
// 只影响半径内的点
if (distance < radius) {
// 计算影响因子
const factor = Math.pow(1 - distance / radius, 2) * strength;
// 根据不同模式应用变形
switch (mode) {
case this.modes.PUSH:
// 推拉模式 - 真正的拖拽效果
// 计算鼠标移动距离(转换为纹理坐标空间)
const movementX = this.mouseMovementX / imageWidth;
const movementY = this.mouseMovementY / imageHeight;
const movementLength = Math.sqrt(
movementX * movementX + movementY * movementY
);
// 只有在有足够移动距离时才应用效果
if (movementLength > 0.002) {
// 提高阈值,确保有明显移动
// 归一化移动方向
const moveX = movementX / movementLength;
const moveY = movementY / movementLength;
// 计算衰减(距离中心越近,效果越强)
const radiusRatio = distance / radius;
const falloff = Math.pow(1 - radiusRatio, 2.0); // 使用更强的衰减
// 基于实际移动距离计算强度
const moveStrength = pressure * power * movementLength * 0.5; // 降低移动强度系数
// 计算最终拖拽强度
const dragStrength = moveStrength * falloff * factor;
// 向鼠标移动方向拖拽
const dragX = moveX * dragStrength;
const dragY = moveY * dragStrength;
// 应用变形,但限制最大变形量
const maxDeform = 0.01; // 限制单次最大变形量(纹理坐标空间)
deformations[idx] += Math.max(
-maxDeform,
Math.min(maxDeform, dragX)
);
deformations[idx + 1] += Math.max(
-maxDeform,
Math.min(maxDeform, dragY)
);
}
break;
case this.modes.CLOCKWISE:
// 顺时针旋转
const angle = Math.atan2(dy, dx) + factor;
const len = distance;
deformations[idx] += Math.cos(angle) * len - dx;
deformations[idx + 1] += Math.sin(angle) * len - dy;
break;
case this.modes.COUNTERCLOCKWISE:
// 逆时针旋转
const angle2 = Math.atan2(dy, dx) - factor;
const len2 = distance;
deformations[idx] += Math.cos(angle2) * len2 - dx;
deformations[idx + 1] += Math.sin(angle2) * len2 - dy;
break;
case this.modes.PINCH:
// 捏合效果 - 向中心收缩
deformations[idx] -= dx * factor;
deformations[idx + 1] -= dy * factor;
break;
case this.modes.EXPAND:
// 展开效果 - 参考捏合算法的反向操作
const expandFactor = factor * 1.5;
deformations[idx] += dx * expandFactor;
deformations[idx + 1] += dy * expandFactor;
break;
case this.modes.CRYSTAL:
// 水晶效果 - 参考旋转算法创建多重角度变形
const crystalAngle = Math.atan2(dy, dx);
const crystalRadius = distance / radius;
// 确保有基础效果
const baseDistortion = Math.max(distortion, 0.3);
// 创建多重波形 - 类似旋转但加入波形调制
const wave1 = Math.sin(crystalAngle * 8) * 0.6;
const wave2 = Math.cos(crystalAngle * 12) * 0.4;
const waveAngle = crystalAngle + (wave1 + wave2) * baseDistortion;
// 径向扭曲 - 类似旋转的距离调制
const radialMod = 1 + Math.sin(crystalRadius * Math.PI * 2) * 0.3;
const modDistance = distance * radialMod;
const crystalX = Math.cos(waveAngle) * modDistance;
const crystalY = Math.sin(waveAngle) * modDistance;
deformations[idx] += (crystalX - (tx + dx)) * factor;
deformations[idx + 1] += (crystalY - (ty + dy)) * factor;
break;
case this.modes.EDGE:
// 边缘效果 - 参考旋转算法创建垂直于径向的波纹
const edgeAngle = Math.atan2(dy, dx);
const edgeRadius = distance / radius;
// 确保有基础效果
const baseEdgeDistortion = Math.max(distortion, 0.5);
// 创建边缘波纹 - 垂直于径向方向的调制
const edgeWave =
Math.sin(edgeRadius * Math.PI * 4) * Math.cos(edgeAngle * 6);
const perpAngle = edgeAngle + Math.PI / 2; // 垂直角度
const edgeFactor = edgeWave * factor * baseEdgeDistortion;
const edgeX = Math.cos(perpAngle) * edgeFactor;
const edgeY = Math.sin(perpAngle) * edgeFactor;
deformations[idx] += edgeX;
deformations[idx + 1] += edgeY;
break;
case this.modes.RECONSTRUCT:
// 重建 - 向原始位置恢复
deformations[idx] *= 0.9;
deformations[idx + 1] *= 0.9;
break;
}
}
}
}
// 更新变形缓冲区
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
deformations,
this.gl.DYNAMIC_DRAW
);
// 重新渲染
this._render();
// 更新当前图像数据
this.currentImageData = this._getImageData();
return this.currentImageData;
}
/**
* 渲染变形后的图像
* @private
*/
_render() {
if (!this.initialized) return;
// 清除画布
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
// 使用变形程序
this.gl.useProgram(this.deformProgram);
// 设置纹理
this.gl.activeTexture(this.gl.TEXTURE0);
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
const u_image = this.gl.getUniformLocation(this.deformProgram, "u_image");
this.gl.uniform1i(u_image, 0);
// 设置顶点位置属性
const a_position = this.gl.getAttribLocation(
this.deformProgram,
"a_position"
);
this.gl.enableVertexAttribArray(a_position);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 0, 0);
// 设置纹理坐标属性
const a_texCoord = this.gl.getAttribLocation(
this.deformProgram,
"a_texCoord"
);
this.gl.enableVertexAttribArray(a_texCoord);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
this.gl.vertexAttribPointer(a_texCoord, 2, this.gl.FLOAT, false, 0, 0);
// 设置变形属性
const a_deformation = this.gl.getAttribLocation(
this.deformProgram,
"a_deformation"
);
this.gl.enableVertexAttribArray(a_deformation);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
this.gl.vertexAttribPointer(a_deformation, 2, this.gl.FLOAT, false, 0, 0);
// 绘制三角形
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
this.gl.drawElements(
this.gl.TRIANGLES,
this.mesh.indices.length,
this.gl.UNSIGNED_SHORT,
0
);
}
/**
* 获取当前图像数据
* @returns {ImageData} 当前图像数据
* @private
*/
_getImageData() {
const width = this.canvas.width;
const height = this.canvas.height;
// 读取WebGL画布像素
const pixels = new Uint8Array(width * height * 4);
this.gl.readPixels(
0,
0,
width,
height,
this.gl.RGBA,
this.gl.UNSIGNED_BYTE,
pixels
);
// 直接创建ImageData不进行翻转
// WebGL和Canvas2D的坐标系不同但这里我们保持WebGL的原始输出
const imageData = new ImageData(
new Uint8ClampedArray(pixels),
width,
height
);
return imageData;
}
/**
* 重置所有变形
* @returns {ImageData} 重置后的图像数据
*/
reset() {
if (!this.initialized) return null;
// 清除变形历史
this.deformHistory = [];
// 重置所有变形
const deformations = new Float32Array(this.mesh.deformations.length);
this.mesh.deformations = deformations;
// 更新变形缓冲区
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
deformations,
this.gl.DYNAMIC_DRAW
);
// 重新渲染
this._render();
// 更新当前图像数据
this.currentImageData = this._getImageData();
return this.currentImageData;
}
/**
* 设置液化模式
* @param {String} mode 液化模式
*/
setMode(mode) {
if (Object.values(this.modes).includes(mode)) {
this.currentMode = mode;
return true;
}
return false;
}
/**
* 设置液化参数
* @param {String} param 参数名
* @param {Number} value 参数值
*/
setParam(param, value) {
if (param in this.params) {
this.params[param] = value;
return true;
}
return false;
}
/**
* 获取当前参数
* @returns {Object} 当前参数
*/
getParams() {
return { ...this.params };
}
/**
* 重置参数为默认值
*/
resetParams() {
this.params = {
size: 50,
pressure: 0.5,
distortion: 0,
power: 0.5,
};
}
/**
* 获取原始图像数据
* @returns {ImageData} 原始图像数据
*/
getOriginalImageData() {
return this.originalImageData;
}
/**
* 获取当前图像数据
* @returns {ImageData} 当前图像数据
*/
getCurrentImageData() {
return this.currentImageData;
}
/**
* 释放资源
*/
dispose() {
if (!this.gl) return;
// 删除缓冲区
if (this.vertexBuffer) this.gl.deleteBuffer(this.vertexBuffer);
if (this.texCoordBuffer) this.gl.deleteBuffer(this.texCoordBuffer);
if (this.deformBuffer) this.gl.deleteBuffer(this.deformBuffer);
if (this.indexBuffer) this.gl.deleteBuffer(this.indexBuffer);
// 删除纹理
if (this.texture) this.gl.deleteTexture(this.texture);
// 删除着色器程序
if (this.program) this.gl.deleteProgram(this.program);
if (this.deformProgram) this.gl.deleteProgram(this.deformProgram);
// 重置属性
this.canvas = null;
this.gl = null;
this.program = null;
this.deformProgram = null;
this.texture = null;
this.mesh = null;
this.initialized = false;
this.deformHistory = [];
}
/**
* 检查是否支持WebGL
* @returns {Boolean} 是否支持WebGL
*/
static isSupported() {
try {
const canvas = document.createElement("canvas");
return !!(
window.WebGLRenderingContext &&
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
);
} catch (e) {
return false;
}
}
}

View File

@@ -0,0 +1,850 @@
//import { fabric } from "fabric-with-all";
/**
* 小地图管理器类
* 实现画布的小地图功能,展示当前视窗位置和内容概览
*/
export class MinimapManager {
/**
* 构造函数
* @param {fabric.Canvas} mainCanvas 主画布实例
* @param {Object} options 配置选项
*/
constructor(mainCanvas, options = {}) {
this.mainCanvas = mainCanvas;
this.minimapCanvas = null;
this.minimapCtx = null;
this.container = null;
this.minimapSize = options.size || { width: 200, height: 120 };
this.visible = options.visible !== undefined ? options.visible : true;
this.isDragging = false;
this.lastRenderTime = 0;
this.renderInterval = options.renderInterval || 100; // 增加渲染间隔到100ms降低频率
this.initialized = false;
this.eventHandlers = {};
// 内容边界,用于确定小地图显示范围
this.contentBounds = {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
};
// 缓存上一次视口大小,用于减少抖动
this.lastViewportSize = { width: 0, height: 0 };
// 添加缓存标志,避免频繁重新计算
this.contentBoundsDirty = true;
// 预先绑定方法,避免上下文丢失
this.render = this.render.bind(this);
this.handleMainCanvasChange = this.handleMainCanvasChange.bind(this);
this.handleMinimapMouseDown = this.handleMinimapMouseDown.bind(this);
this.handleMinimapMouseMove = this.handleMinimapMouseMove.bind(this);
this.handleMinimapMouseUp = this.handleMinimapMouseUp.bind(this);
this.calculateViewportRect = this.calculateViewportRect.bind(this);
this.calculateContentBounds = this.calculateContentBounds.bind(this);
this.moveViewport = this.moveViewport.bind(this);
// 创建canvas元素
this._createCanvas();
}
/**
* 创建小地图的canvas元素
* @private
*/
_createCanvas() {
// 创建canvas元素
this.minimapCanvas = document.createElement("canvas");
this.minimapCanvas.width = this.minimapSize.width;
this.minimapCanvas.height = this.minimapSize.height;
this.minimapCanvas.style.width = "100%";
this.minimapCanvas.style.height = "100%";
this.minimapCanvas.style.display = this.visible ? "block" : "none";
// 获取绘图上下文
this.minimapCtx = this.minimapCanvas.getContext("2d");
}
/**
* 将小地图挂载到指定的DOM容器中
* @param {HTMLElement} containerElement 容器DOM元素
* @returns {MinimapManager} 返回实例自身,支持链式调用
*/
mount(containerElement) {
if (!containerElement) {
console.error("小地图挂载失败:未提供有效的容器元素");
return this;
}
// 保存容器引用
this.container = containerElement;
// 清空容器,防止重复挂载
while (containerElement.firstChild) {
containerElement.removeChild(containerElement.firstChild);
}
// 将canvas添加到容器
containerElement.appendChild(this.minimapCanvas);
// 初始化小地图
if (!this.initialized) {
// 计算初始内容边界
this.calculateContentBounds();
// 添加事件监听器
this.addEventListeners();
// 首次渲染
this.render();
this.initialized = true;
}
return this;
}
/**
* 添加事件监听器
*/
addEventListeners() {
if (!this.mainCanvas || !this.minimapCanvas) return;
// 监听主画布变化事件
this.mainCanvas.on("after:render", this.handleMainCanvasChange);
// 仅在缩放时重新计算内容边界,避免频繁计算
this.mainCanvas.on("zoom:change", () => {
this.contentBoundsDirty = true;
this.handleMainCanvasChange();
});
// 仅当对象添加、删除或修改时重新计算内容边界
this.mainCanvas.on("object:added", () => {
this.contentBoundsDirty = true;
this.handleMainCanvasChange();
});
this.mainCanvas.on("object:removed", () => {
this.contentBoundsDirty = true;
this.handleMainCanvasChange();
});
this.mainCanvas.on("object:modified", () => {
this.contentBoundsDirty = true;
this.handleMainCanvasChange();
});
// 移动、缩放、旋转操作时使用更强的节流,不重新计算内容边界
this.mainCanvas.on("object:moving", this.handleMainCanvasChange);
this.mainCanvas.on("object:scaling", this.handleMainCanvasChange);
this.mainCanvas.on("object:rotating", this.handleMainCanvasChange);
// 小地图交互事件 - 鼠标
this.eventHandlers.mousedown = this.handleMinimapMouseDown;
this.eventHandlers.mousemove = this.handleMinimapMouseMove;
this.eventHandlers.mouseup = this.handleMinimapMouseUp;
// 移除mouseout事件处理允许拖动操作持续到鼠标释放
this.minimapCanvas.addEventListener(
"mousedown",
this.eventHandlers.mousedown
);
document.addEventListener("mousemove", this.eventHandlers.mousemove);
document.addEventListener("mouseup", this.eventHandlers.mouseup);
// 移除mouseout事件监听
// 小地图交互事件 - 触摸
this.eventHandlers.touchstart = (e) => {
e.preventDefault();
const touch = e.touches[0];
this.handleMinimapMouseDown({
clientX: touch.clientX,
clientY: touch.clientY,
preventDefault: () => {},
});
};
this.eventHandlers.touchmove = (e) => {
e.preventDefault();
if (this.isDragging) {
const touch = e.touches[0];
this.handleMinimapMouseMove({
clientX: touch.clientX,
clientY: touch.clientY,
preventDefault: () => {},
});
}
};
this.eventHandlers.touchend = this.handleMinimapMouseUp;
this.minimapCanvas.addEventListener(
"touchstart",
this.eventHandlers.touchstart
);
document.addEventListener("touchmove", this.eventHandlers.touchmove, {
passive: false,
});
document.addEventListener("touchend", this.eventHandlers.touchend);
}
/**
* 移除事件监听器
*/
removeEventListeners() {
if (!this.mainCanvas || !this.minimapCanvas) return;
// 移除画布事件监听
this.mainCanvas.off("after:render", this.handleMainCanvasChange);
this.mainCanvas.off("zoom:change", this.handleMainCanvasChange);
this.mainCanvas.off("object:added", this.handleMainCanvasChange);
this.mainCanvas.off("object:removed", this.handleMainCanvasChange);
this.mainCanvas.off("object:modified", this.handleMainCanvasChange);
this.mainCanvas.off("object:moving", this.handleMainCanvasChange);
this.mainCanvas.off("object:scaling", this.handleMainCanvasChange);
this.mainCanvas.off("object:rotating", this.handleMainCanvasChange);
// 移除鼠标事件监听
this.minimapCanvas.removeEventListener(
"mousedown",
this.eventHandlers.mousedown
);
document.removeEventListener("mousemove", this.eventHandlers.mousemove);
document.removeEventListener("mouseup", this.eventHandlers.mouseup);
// 移除触摸事件监听
this.minimapCanvas.removeEventListener(
"touchstart",
this.eventHandlers.touchstart
);
document.removeEventListener("touchmove", this.eventHandlers.touchmove);
document.removeEventListener("touchend", this.eventHandlers.touchend);
}
/**
* 处理主画布变化事件
* 使用节流限制渲染频率
*/
handleMainCanvasChange() {
const now = Date.now();
if (now - this.lastRenderTime > this.renderInterval) {
this.lastRenderTime = now;
// 只在内容边界标记为脏时才重新计算
if (this.contentBoundsDirty) {
this.calculateContentBounds();
this.contentBoundsDirty = false;
}
this.render();
}
}
/**
* 计算画布内容的边界范围
* 包括所有可见对象和画布本身
*/
calculateContentBounds() {
if (!this.mainCanvas) return;
const objects = this.mainCanvas.getObjects();
// 初始化为画布尺寸
let minX = 0;
let minY = 0;
let maxX = this.mainCanvas.getWidth();
let maxY = this.mainCanvas.getHeight();
// 如果有对象,则计算所有对象的边界
if (objects.length > 0) {
// 重置为极值
minX = Infinity;
minY = Infinity;
maxX = -Infinity;
maxY = -Infinity;
// 考虑所有可见对象的边界
objects.forEach((obj) => {
if (!obj.visible) return;
const rect = obj.getBoundingRect(true, true);
minX = Math.min(minX, rect.left);
minY = Math.min(minY, rect.top);
maxX = Math.max(maxX, rect.left + rect.width);
maxY = Math.max(maxY, rect.top + rect.height);
});
// 确保边界至少包含画布尺寸
minX = Math.min(minX, 0);
minY = Math.min(minY, 0);
maxX = Math.max(maxX, this.mainCanvas.getWidth());
maxY = Math.max(maxY, this.mainCanvas.getHeight());
}
// 添加边距
const padding =
Math.max(this.mainCanvas.getWidth(), this.mainCanvas.getHeight()) * 0.1;
this.contentBounds = {
minX: minX - padding,
minY: minY - padding,
maxX: maxX + padding,
maxY: maxY + padding,
};
}
/**
* 处理小地图鼠标按下事件
*/
handleMinimapMouseDown(e) {
if (!this.visible || !this.minimapCanvas) return;
e.preventDefault();
const rect = this.minimapCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查点击是否在视口矩形内
const vpRect = this.calculateViewportRect();
// 在视口矩形内点击开始拖拽,否则直接跳转到点击位置
if (
x >= vpRect.x &&
x <= vpRect.x + vpRect.width &&
y >= vpRect.y &&
y <= vpRect.y + vpRect.height
) {
this.isDragging = true;
this.dragStart = { x, y };
this.dragStartViewport = { ...vpRect };
// 缓存当前视口大小,确保拖动过程中大小不变
this.lastViewportSize = {
width: vpRect.width,
height: vpRect.height,
};
} else {
// 直接移动视口中心到点击位置
this.moveViewport(x, y, true);
}
}
/**
* 处理小地图鼠标移动事件
*/
handleMinimapMouseMove(e) {
if (!this.isDragging || !this.visible) return;
e.preventDefault();
const rect = this.minimapCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const deltaX = x - this.dragStart.x;
const deltaY = y - this.dragStart.y;
// 更新拖拽起始位置
this.dragStart = { x, y };
// 移动画布视口
this.moveViewport(
this.dragStartViewport.x + deltaX,
this.dragStartViewport.y + deltaY,
false
);
// 更新拖拽起始视口位置
this.dragStartViewport = this.calculateViewportRect();
// 立即渲染小地图,提升拖动流畅度
this.render();
}
/**
* 处理小地图鼠标抬起事件
*/
handleMinimapMouseUp() {
this.isDragging = false;
}
/**
* 移动主画布视口到指定位置
*/
moveViewport(x, y, isCentered) {
if (!this.mainCanvas) return;
// 获取主画布的当前视图信息
const vpt = this.mainCanvas.viewportTransform;
const zoom = this.mainCanvas.getZoom();
// 计算内容边界在小地图上的比例
const contentWidth = this.contentBounds.maxX - this.contentBounds.minX;
const contentHeight = this.contentBounds.maxY - this.contentBounds.minY;
const scaleX = this.minimapSize.width / contentWidth;
const scaleY = this.minimapSize.height / contentHeight;
// 计算视口在小地图上的宽高
let viewportWidth, viewportHeight;
if (this.isDragging && this.lastViewportSize.width > 0) {
viewportWidth = this.lastViewportSize.width;
viewportHeight = this.lastViewportSize.height;
} else {
viewportWidth = Math.round((this.mainCanvas.getWidth() / zoom) * scaleX);
viewportHeight = Math.round(
(this.mainCanvas.getHeight() / zoom) * scaleY
);
}
// 添加边界限制,确保视口不会超出小地图
x = Math.max(0, Math.min(x, this.minimapSize.width - viewportWidth));
y = Math.max(0, Math.min(y, this.minimapSize.height - viewportHeight));
// 将小地图坐标转换为主画布坐标
let targetX = x / scaleX + this.contentBounds.minX;
let targetY = y / scaleY + this.contentBounds.minY;
if (isCentered) {
// 如果是直接点击,则将点击位置设为视口中心
targetX -= this.mainCanvas.getWidth() / zoom / 2;
targetY -= this.mainCanvas.getHeight() / zoom / 2;
}
// 设置主画布的位置
this.mainCanvas.setViewportTransform([
vpt[0],
vpt[1],
vpt[2],
vpt[3],
-targetX * zoom,
-targetY * zoom,
]);
// 触发主画布重新渲染
this.mainCanvas.renderAll();
}
/**
* 计算当前视口在小地图中的位置和大小
*/
calculateViewportRect() {
if (!this.mainCanvas) return { x: 0, y: 0, width: 0, height: 0 };
// 获取主画布的视图变换信息
const vpt = this.mainCanvas.viewportTransform;
const zoom = this.mainCanvas.getZoom();
// 计算内容边界在小地图上的比例
const contentWidth = this.contentBounds.maxX - this.contentBounds.minX;
const contentHeight = this.contentBounds.maxY - this.contentBounds.minY;
const scaleX = this.minimapSize.width / contentWidth;
const scaleY = this.minimapSize.height / contentHeight;
// 计算当前视口区域相对于内容边界的位置
const viewLeft = -vpt[4] / zoom - this.contentBounds.minX;
const viewTop = -vpt[5] / zoom - this.contentBounds.minY;
// 转换为小地图上的坐标,使用取整减少精度误差
const x = Math.round(viewLeft * scaleX);
const y = Math.round(viewTop * scaleY);
// 如果正在拖动,则使用缓存的大小避免抖动
let width, height;
if (this.isDragging && this.lastViewportSize.width > 0) {
width = this.lastViewportSize.width;
height = this.lastViewportSize.height;
} else {
width = Math.round((this.mainCanvas.getWidth() / zoom) * scaleX);
height = Math.round((this.mainCanvas.getHeight() / zoom) * scaleY);
// 更新缓存的视口大小
if (!this.isDragging) {
this.lastViewportSize = { width, height };
}
}
return { x, y, width, height };
}
/**
* 渲染小地图
* 使用高性能的离屏渲染
*/
render() {
if (!this.visible || !this.minimapCanvas || !this.mainCanvas) return;
try {
// 清空小地图
this.minimapCtx.clearRect(
0,
0,
this.minimapSize.width,
this.minimapSize.height
);
// 绘制小地图背景
this.minimapCtx.fillStyle = this.mainCanvas.backgroundColor || "#f0f0f0";
this.minimapCtx.fillRect(
0,
0,
this.minimapSize.width,
this.minimapSize.height
);
// 计算内容边界尺寸
const contentWidth = this.contentBounds.maxX - this.contentBounds.minX;
const contentHeight = this.contentBounds.maxY - this.contentBounds.minY;
// 检查是否有内容需要渲染
const objects = this.mainCanvas.getObjects();
if (objects.length === 0) {
// 如果没有对象,只需绘制视口框
this.drawViewportBox();
return;
}
// 优化离屏渲染尺寸计算
const maxSize = 1000; // 限制离屏canvas最大尺寸提高性能
let offscreenWidth = contentWidth;
let offscreenHeight = contentHeight;
let scale = 1;
if (contentWidth > maxSize || contentHeight > maxSize) {
scale = Math.min(maxSize / contentWidth, maxSize / contentHeight);
offscreenWidth *= scale;
offscreenHeight *= scale;
}
const offscreenCanvas = document.createElement("canvas");
offscreenCanvas.width = offscreenWidth;
offscreenCanvas.height = offscreenHeight;
const offCtx = offscreenCanvas.getContext("2d");
// 创建临时fabric.Canvas用于渲染全内容
const tempFabricCanvas = new fabric.StaticCanvas();
tempFabricCanvas.setWidth(offscreenWidth);
tempFabricCanvas.setHeight(offscreenHeight);
tempFabricCanvas.backgroundColor = this.mainCanvas.backgroundColor;
// 复制主画布对象到临时画布
objects.forEach((obj) => {
if (!obj.visible) return;
try {
// 使用浅克隆,避免深度克隆带来的性能开销
const clonedObj = fabric.util.object.clone(obj);
// 调整对象位置和大小,使其相对于内容边界并适应缩放
clonedObj.set({
left: (obj.left - this.contentBounds.minX) * scale,
top: (obj.top - this.contentBounds.minY) * scale,
scaleX: obj.scaleX * scale,
scaleY: obj.scaleY * scale,
// 禁用对象的交互属性,提高性能
selectable: false,
evented: false,
hasControls: false,
hasBorders: false,
});
tempFabricCanvas.add(clonedObj);
} catch (err) {
console.warn("无法克隆对象到小地图", err);
}
});
// 渲染临时画布
tempFabricCanvas.renderAll();
// 将临时画布内容绘制到离屏canvas
offCtx.drawImage(tempFabricCanvas.getElement(), 0, 0);
// 将离屏canvas缩放绘制到小地图
this.minimapCtx.drawImage(
offscreenCanvas,
0,
0,
offscreenWidth,
offscreenHeight,
0,
0,
this.minimapSize.width,
this.minimapSize.height
);
// 释放临时画布资源
// tempFabricCanvas.dispose();
// 绘制视口框
this.drawViewportBox();
} catch (error) {
console.error("小地图渲染出错:", error);
}
}
/**
* 绘制视口框从render方法中分离出来提高代码清晰度
*/
drawViewportBox() {
// 计算当前视口范围
const vpRect = this.calculateViewportRect();
// 视口矩形边框
this.minimapCtx.strokeStyle = "#ff3333";
this.minimapCtx.lineWidth = 2;
this.minimapCtx.strokeRect(vpRect.x, vpRect.y, vpRect.width, vpRect.height);
// 视口矩形半透明填充
this.minimapCtx.fillStyle = "rgba(255, 0, 0, 0.1)";
this.minimapCtx.fillRect(vpRect.x, vpRect.y, vpRect.width, vpRect.height);
}
/**
* 设置小地图可见性
*/
setVisibility(visible) {
this.visible = visible;
// 更新canvas显示状态
if (this.minimapCanvas) {
this.minimapCanvas.style.display = visible ? "block" : "none";
}
if (visible && this.initialized) {
this.contentBoundsDirty = true; // 标记需要重新计算内容边界
this.calculateContentBounds();
this.render();
}
}
/**
* 刷新小地图
* 重新读取大画布数据并渲染
*/
refresh() {
this.contentBoundsDirty = true;
this.calculateContentBounds();
this.render();
}
/**
* 调整小地图大小
* @param {Object} size 小地图尺寸,{width, height}
*/
resize(size) {
if (!size || !size.width || !size.height) return;
this.minimapSize = {
width: size.width,
height: size.height,
};
if (this.minimapCanvas) {
this.minimapCanvas.width = size.width;
this.minimapCanvas.height = size.height;
this.refresh();
}
}
/**
* 清理资源,释放内存
*/
dispose() {
this.removeEventListeners();
// 从DOM中移除canvas
if (
this.container &&
this.minimapCanvas &&
this.minimapCanvas.parentNode === this.container
) {
this.container.removeChild(this.minimapCanvas);
}
this.mainCanvas = null;
this.minimapCanvas = null;
this.minimapCtx = null;
this.container = null;
this.initialized = false;
}
/**
* 更新小地图
* 使用更高效的渲染策略,减少不必要的重绘
*/
update() {
if (!this.enabled || !this.minimapCanvas) return;
// 使用节流来控制更新频率
if (this._updateTimeout) {
clearTimeout(this._updateTimeout);
}
this._updateTimeout = setTimeout(() => {
this._renderMinimap();
}, 100); // 100ms 的节流,避免频繁渲染
}
/**
* 渲染小地图
* 优化渲染性能,只在必要时重绘
*/
_renderMinimap() {
if (!this.minimapCanvas || !this.canvas) return;
const ctx = this.minimapCanvas.getContext("2d");
const ratio = this.minimapCanvas.width / this.canvas.width;
// 清除小地图
ctx.clearRect(0, 0, this.minimapCanvas.width, this.minimapCanvas.height);
// 使用缓存策略
if (!this._minimapCache || this._shouldUpdateCache()) {
// 创建离屏画布作为缓存
if (!this._offscreenCanvas) {
this._offscreenCanvas = document.createElement("canvas");
this._offscreenCanvas.width = this.minimapCanvas.width;
this._offscreenCanvas.height = this.minimapCanvas.height;
}
const offCtx = this._offscreenCanvas.getContext("2d");
offCtx.clearRect(
0,
0,
this._offscreenCanvas.width,
this._offscreenCanvas.height
);
// 绘制图层内容到离屏画布
this._renderLayersToMinimap(offCtx, ratio);
// 保存渲染时间戳
this._lastCacheUpdate = Date.now();
this._minimapCache = true;
}
// 将缓存的内容渲染到实际小地图画布
if (this._offscreenCanvas) {
ctx.drawImage(this._offscreenCanvas, 0, 0);
}
// 绘制可视区域指示器
this._renderViewportIndicator(ctx, ratio);
}
/**
* 检查是否应该更新小地图缓存
*/
_shouldUpdateCache() {
// 如果没有缓存或缓存时间超过500ms则更新
return !this._lastCacheUpdate || Date.now() - this._lastCacheUpdate > 500;
}
/**
* 渲染图层内容到小地图
*/
_renderLayersToMinimap(ctx, ratio) {
// 获取画布上所有可见的图层
const visibleLayers = [];
// 安全地访问图层数据,避免 "forEach is not a function" 错误
if (this.canvas && this.canvas.layers) {
// 检查 layers 是否是响应式对象 (有 value 属性)
const layersArray =
typeof this.canvas.layers.value !== "undefined"
? this.canvas.layers.value
: Array.isArray(this.canvas.layers)
? this.canvas.layers
: [];
// 过滤出可见图层
layersArray.forEach((layer) => {
if (layer.visible) {
visibleLayers.push(layer);
}
});
}
// 按照图层顺序渲染到小地图
for (const layer of visibleLayers) {
let objectsToRender = [];
// 根据图层类型获取要渲染的对象
if (layer.type === "background" && layer.fabricObject) {
objectsToRender = [layer.fabricObject];
} else if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) {
objectsToRender = layer.fabricObjects;
}
for (const fabricObj of objectsToRender) {
if (!fabricObj.visible) continue;
// 根据对象类型渲染到小地图
if (fabricObj.type === "image" && fabricObj._element) {
ctx.globalAlpha = fabricObj.opacity || 1;
const left = fabricObj.left * ratio;
const top = fabricObj.top * ratio;
const width = fabricObj.width * fabricObj.scaleX * ratio;
const height = fabricObj.height * fabricObj.scaleY * ratio;
ctx.drawImage(fabricObj._element, left, top, width, height);
} else if (
fabricObj.type === "path" ||
fabricObj.type === "rect" ||
fabricObj.type === "circle"
) {
// 简单地用颜色块表示其他类型的对象
ctx.fillStyle = fabricObj.fill || "#888";
ctx.globalAlpha = fabricObj.opacity || 0.5;
const left = fabricObj.left * ratio;
const top = fabricObj.top * ratio;
const width =
(fabricObj.width || 20) * (fabricObj.scaleX || 1) * ratio;
const height =
(fabricObj.height || 20) * (fabricObj.scaleY || 1) * ratio;
ctx.fillRect(left, top, width, height);
}
}
}
ctx.globalAlpha = 1;
}
/**
* 渲染视口指示器
*/
_renderViewportIndicator(ctx, ratio) {
if (!this.canvas) return;
const vpt = this.canvas.viewportTransform;
if (!vpt) return;
// 计算可视区域在小地图上的位置和大小
const zoom = this.canvas.getZoom();
const viewportWidth = this.canvas.width / zoom;
const viewportHeight = this.canvas.height / zoom;
const x = (-vpt[4] / zoom) * ratio;
const y = (-vpt[5] / zoom) * ratio;
const width = viewportWidth * ratio;
const height = viewportHeight * ratio;
// 绘制视口指示器
ctx.strokeStyle = "#ff0000";
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
}
/**
* 强制完全更新小地图
*/
forceUpdate() {
this._minimapCache = false;
this.update();
}
}
export default MinimapManager;

View File

@@ -0,0 +1,951 @@
//import { fabric } from "fabric-with-all";
import { generateId } from "../../utils/helper";
import { OperationType } from "../../utils/layerHelper";
import {
ClearSelectionCommand,
CreateSelectionCommand,
} from "../../commands/SelectionCommands";
/**
* 选区管理器
* 负责管理画布上的选区操作
*/
export class SelectionManager {
/**
* 创建选区管理器
* @param {Object} options 配置选项
* @param {Object} options.canvas fabric.js画布实例
* @param {Object} options.commandManager 命令管理器实例
* @param {Object} options.layerManager 图层管理实例
*/
constructor(options = {}) {
this.canvas = options.canvas;
this.commandManager = options.commandManager;
this.layerManager = options.layerManager;
// 选区状态
this.isActive = false;
this.selectionType = OperationType.LASSO_RECTANGLE; // 使用常量而不是字符串
this.selectionObject = null; // 当前选区对象
this.selectionId = "selection_" + Date.now();
this.featherAmount = 0; // 羽化值
// 选区样式配置
this.selectionStyle = {
stroke: "#0096ff",
strokeWidth: 1,
strokeDashArray: [5, 5],
fill: "rgba(0, 150, 255, 0.1)",
selectable: false,
evented: false,
excludeFromExport: true,
hoverCursor: "default",
moveCursor: "default",
};
// 绘制状态
this.drawingObject = null;
this.startPoint = null;
this.selectionPath = null; // 存储选区路径数据
// 自由选区相关状态
this.drawingPoints = null;
this.currentPathString = null;
// 不再直接绑定事件处理函数
this._mouseDownHandler = null;
this._mouseMoveHandler = null;
this._mouseUpHandler = null;
this._keyDownHandler = null;
// 选区相关的工具类型
this.selectionTools = [
OperationType.LASSO,
OperationType.LASSO_RECTANGLE,
OperationType.LASSO_ELLIPSE,
];
// 当前工具
this.currentTool = OperationType.SELECT;
// 选区状态变化回调
this.onSelectionChanged = null;
// 不再自动初始化事件,改为手动控制
// this.initEvents();
}
/**
* 设置当前工具
* @param {String} toolId 工具ID
*/
setCurrentTool(toolId) {
this.currentTool = toolId;
// 检查是否为选区工具
const wasActive = this.isActive;
this.isActive = this.selectionTools.includes(toolId);
// 如果从非选区工具切换到选区工具,初始化事件
if (!wasActive && this.isActive) {
this.initEvents();
}
// 如果从选区工具切换到非选区工具,清理事件和选区
else if (wasActive && !this.isActive) {
this.cleanupEvents();
this.clearSelection();
}
// 根据工具类型设置选区类型
if (this.isActive) {
this.selectionType = toolId;
}
}
/**
* 初始化选区相关事件
*/
initEvents() {
if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化
// 保存实例引用,用于事件处理函数中
const self = this;
// 鼠标按下事件处理
this._mouseDownHandler = (options) => {
// 如果选区功能未激活,不处理事件
if (!this.isActive) return;
// 如果点击的是已有对象且不是选区对象,则不处理
if (
options.target &&
options.target.id !== this.selectionId &&
options.target.selectable !== false &&
options.target.type !== "selection"
) {
return;
}
// 阻止事件冒泡,避免与 CanvasEventManager 冲突
options.e.stopPropagation();
// 根据选区类型执行不同的起始操作
switch (this.selectionType) {
case OperationType.LASSO:
this.startFreeSelection(options);
break;
case OperationType.LASSO_ELLIPSE:
this.startEllipseSelection(options);
break;
case OperationType.LASSO_RECTANGLE:
this.startRectangleSelection(options);
break;
}
};
// 鼠标移动事件处理
this._mouseMoveHandler = (options) => {
// 如果选区功能未激活或没有正在绘制的对象,不处理事件
if (!this.isActive || !this.drawingObject) return;
// 阻止事件冒泡
options.e.stopPropagation();
// 根据选区类型执行不同的绘制操作
switch (this.selectionType) {
case OperationType.LASSO_RECTANGLE:
this.drawRectangleSelection(options);
break;
case OperationType.LASSO_ELLIPSE:
this.drawEllipseSelection(options);
break;
case OperationType.LASSO:
this.drawFreeSelection(options);
break;
}
};
// 鼠标抬起事件处理
this._mouseUpHandler = (options) => {
// 如果选区功能未激活或没有正在绘制的对象,不处理事件
if (!this.isActive || !this.drawingObject) return;
// 阻止事件冒泡
if (options && options.e) {
options.e.stopPropagation();
}
// 根据选区类型执行不同的完成操作
switch (this.selectionType) {
case OperationType.LASSO_RECTANGLE:
this.endRectangleSelection();
break;
case OperationType.LASSO_ELLIPSE:
this.endEllipseSelection();
break;
case OperationType.LASSO:
this.endFreeSelection();
break;
}
// 如果有命令管理器,使用命令模式记录选区创建
if (this.commandManager && this.selectionObject) {
this.commandManager.execute(
new CreateSelectionCommand({
canvas: this.canvas,
selectionManager: this,
selectionObject: this.selectionObject,
selectionType: this.selectionType,
})
);
}
};
// 键盘事件处理
this._keyDownHandler = (event) => {
// 只在选区功能激活时处理键盘事件
if (!this.isActive) return;
if (event.key === "Escape") {
// ESC键取消当前选区操作
if (this.drawingObject) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
}
// 清除已有选区
else if (this.selectionObject) {
if (this.commandManager) {
this.commandManager.execute(
new ClearSelectionCommand({
selectionManager: this,
})
);
} else {
this.clearSelection();
}
}
}
};
// 添加事件监听
this.canvas.on("mouse:down", this._mouseDownHandler);
this.canvas.on("mouse:move", this._mouseMoveHandler);
this.canvas.on("mouse:up", this._mouseUpHandler);
// 添加键盘事件监听
document.addEventListener("keydown", this._keyDownHandler);
}
/**
* 清理事件监听
*/
cleanupEvents() {
if (!this.canvas) return;
// 移除事件监听
if (this._mouseDownHandler) {
this.canvas.off("mouse:down", this._mouseDownHandler);
this._mouseDownHandler = null;
}
if (this._mouseMoveHandler) {
this.canvas.off("mouse:move", this._mouseMoveHandler);
this._mouseMoveHandler = null;
}
if (this._mouseUpHandler) {
this.canvas.off("mouse:up", this._mouseUpHandler);
this._mouseUpHandler = null;
}
if (this._keyDownHandler) {
document.removeEventListener("keydown", this._keyDownHandler);
this._keyDownHandler = null;
}
}
/**
* 获取选区对象
* @returns {Object} 选区对象
*/
getSelectionObject() {
return this.selectionObject;
}
/**
* 获取选区路径
* @returns {Array|String} 选区路径数据
*/
getSelectionPath() {
return this.selectionPath;
}
/**
* 获取羽化值
* @returns {Number} 羽化值
*/
getFeatherAmount() {
return this.featherAmount;
}
/**
* 设置羽化值
* @param {Number} amount 羽化值
*/
setFeatherAmount(amount) {
this.featherAmount = amount;
return this.updateSelectionAppearance();
}
/**
* 设置选区对象
* @param {Object} object 选区对象
*/
setSelectionObject(object) {
// 如果已存在选区,先移除
if (this.selectionObject) {
this.removeSelectionFromCanvas();
}
// 更新选区对象
this.selectionObject = object;
this.selectionPath = object.path;
this.selectionId = object.id || generateId();
// 更新外观
this.updateSelectionAppearance();
// 添加到画布(确保在顶层)
if (this.canvas && this.selectionObject) {
this.canvas.add(this.selectionObject);
this.canvas.bringToFront(this.selectionObject);
this.canvas.renderAll();
}
// 触发选区变化回调
if (
this.onSelectionChanged &&
typeof this.onSelectionChanged === "function"
) {
this.onSelectionChanged();
}
return true;
}
/**
* 从路径数据设置选区
* @param {Array|String} path 选区路径数据
*/
setSelectionFromPath(path) {
if (!path) return false;
// 创建选区对象
const selectionObj = new fabric.Path(path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
// 设置选区
return this.setSelectionObject(selectionObj);
}
/**
* 更新选区外观
*/
updateSelectionAppearance() {
if (!this.selectionObject) return false;
// 应用基本样式
Object.assign(this.selectionObject, this.selectionStyle);
// 应用羽化效果
if (this.featherAmount > 0) {
this.selectionObject.shadow = new fabric.Shadow({
color: "rgba(0, 150, 255, 0.5)",
blur: this.featherAmount,
offsetX: 0,
offsetY: 0,
});
} else {
this.selectionObject.shadow = null;
}
// 更新画布
this.canvas.renderAll();
return true;
}
/**
* 移除选区
*/
removeSelectionFromCanvas() {
if (this.canvas && this.selectionObject) {
this.canvas.remove(this.selectionObject);
this.canvas.renderAll();
}
}
/**
* 清除选区
*/
clearSelection() {
// 移除选区对象
this.removeSelectionFromCanvas();
// 重置选区状态
this.selectionObject = null;
this.selectionPath = null;
this.selectionId = null;
this.featherAmount = 0;
// 触发选区变化回调
if (
this.onSelectionChanged &&
typeof this.onSelectionChanged === "function"
) {
this.onSelectionChanged();
}
return true;
}
/**
* 反转选区
*/
async invertSelection() {
if (!this.canvas || !this.selectionObject) return false;
// 获取画布范围
const canvasRect = new fabric.Rect({
left: 0,
top: 0,
width: this.canvas.width,
height: this.canvas.height,
selectable: false,
});
// 创建反选路径
let invertedPath;
try {
invertedPath = canvasRect.subtractPathFromRect(this.selectionObject.path);
} catch (error) {
console.error("无法反转选区:", error);
return false;
}
// 设置新的选区
const newSelection = new fabric.Path(invertedPath.path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
return this.setSelectionObject(newSelection);
}
/**
* 添加到选区
* @param {Object} newSelection 要添加的选区对象
*/
async addToSelection(newSelection) {
if (!this.canvas) return false;
// 如果当前没有选区,直接使用新选区
if (!this.selectionObject) {
return this.setSelectionObject(newSelection);
}
// 合并选区
let combinedPath;
try {
combinedPath = this.selectionObject.union(newSelection);
} catch (error) {
console.error("无法添加到选区:", error);
return false;
}
// 设置新的选区
const combinedSelection = new fabric.Path(combinedPath.path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
return this.setSelectionObject(combinedSelection);
}
/**
* 从选区中移除
* @param {Object} removeSelection 要移除的选区对象
*/
async removeFromSelection(removeSelection) {
if (!this.canvas || !this.selectionObject) return false;
// 从当前选区中减去新选区
let resultPath;
try {
resultPath = this.selectionObject.subtract(removeSelection);
} catch (error) {
console.error("无法从选区中移除:", error);
return false;
}
// 设置新的选区
const newSelection = new fabric.Path(resultPath.path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
return this.setSelectionObject(newSelection);
}
/**
* 应用羽化效果
* @param {Number} amount 羽化值
*/
async featherSelection(amount) {
if (!this.selectionObject) return false;
// 更新羽化值
this.featherAmount = amount;
// 更新选区外观
return this.updateSelectionAppearance();
}
/**
* 检查对象是否在选区内
* @param {Object} object 要检查的对象
* @returns {Boolean} 是否在选区内
*/
isObjectInSelection(object) {
if (!this.selectionObject || !object) return false;
// 获取对象的边界框
const bounds = object.getBoundingRect();
const { left, top, width, height } = bounds;
// 检查对象的中心点和四个角是否在选区内
const centerX = left + width / 2;
const centerY = top + height / 2;
// 检查中心点
if (this.isPointInSelection(centerX, centerY)) return true;
// 检查四个角
if (this.isPointInSelection(left, top)) return true;
if (this.isPointInSelection(left + width, top)) return true;
if (this.isPointInSelection(left, top + height)) return true;
if (this.isPointInSelection(left + width, top + height)) return true;
return false;
}
/**
* 检查点是否在选区内
* @param {Number} x X坐标
* @param {Number} y Y坐标
* @returns {Boolean} 是否在选区内
*/
isPointInSelection(x, y) {
if (!this.selectionObject) return false;
// 使用fabric.js的containsPoint方法判断点是否在选区内
return this.selectionObject.containsPoint({ x, y });
}
/**
* 开始自由选区
* @param {Object} options 事件对象
*/
startFreeSelection(options) {
if (!this.canvas || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
this.startPoint = pointer;
// 创建用于绘制轨迹的点数组
this.drawingPoints = [pointer];
// 初始化SVG路径字符串
this.currentPathString = `M ${pointer.x} ${pointer.y}`;
// 创建临时路径对象用于实时显示
this.drawingObject = new fabric.Path(this.currentPathString, {
stroke: this.selectionStyle.stroke,
strokeWidth: this.selectionStyle.strokeWidth,
strokeDashArray: this.selectionStyle.strokeDashArray,
fill: "transparent",
selectable: false,
evented: false,
strokeLineCap: "round",
strokeLineJoin: "round",
});
// 添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
/**
* 绘制自由选区
* @param {Object} options 事件对象
*/
drawFreeSelection(options) {
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
// 添加新的点,但避免添加过于密集的点
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
const distance = Math.sqrt(
Math.pow(pointer.x - lastPoint.x, 2) +
Math.pow(pointer.y - lastPoint.y, 2)
);
// 只有当距离大于2像素时才添加新点避免路径过于复杂
if (distance > 2) {
this.drawingPoints.push(pointer);
// 更新路径字符串
this.currentPathString += ` L ${pointer.x} ${pointer.y}`;
// 移除旧的绘制对象
this.canvas.remove(this.drawingObject);
// 创建新的路径对象
this.drawingObject = new fabric.Path(this.currentPathString, {
stroke: this.selectionStyle.stroke,
strokeWidth: this.selectionStyle.strokeWidth,
strokeDashArray: this.selectionStyle.strokeDashArray,
fill: "transparent",
selectable: false,
evented: false,
strokeLineCap: "round",
strokeLineJoin: "round",
});
// 重新添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
}
/**
* 结束自由选区
*/
endFreeSelection() {
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
// 检查是否有足够的点来形成选区
if (this.drawingPoints.length < 3) {
// 点太少,清除绘制对象
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.drawingPoints = null;
this.startPoint = null;
this.currentPathString = null;
return;
}
// 自动闭合路径 - 连接最后一点到第一点
const firstPoint = this.drawingPoints[0];
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
const closingDistance = Math.sqrt(
Math.pow(firstPoint.x - lastPoint.x, 2) +
Math.pow(firstPoint.y - lastPoint.y, 2)
);
// 如果首尾距离较大,自动添加闭合线段
let finalPathString = this.currentPathString;
if (closingDistance > 10) {
finalPathString += ` L ${firstPoint.x} ${firstPoint.y}`;
}
finalPathString += " Z"; // 闭合路径
// 创建最终选区对象
const selectionObj = new fabric.Path(finalPathString, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
fill: this.selectionStyle.fill, // 恢复填充
});
// 移除绘制中的临时对象
this.canvas.remove(this.drawingObject);
// 重置绘制状态
this.drawingObject = null;
this.drawingPoints = null;
this.startPoint = null;
this.currentPathString = null;
// 设置选区
this.setSelectionObject(selectionObj);
}
/**
* 开始矩形选区
* @param {Object} options 事件对象
*/
startRectangleSelection(options) {
if (!this.canvas || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
this.startPoint = pointer;
// 创建矩形对象
this.drawingObject = new fabric.Rect({
left: pointer.x,
top: pointer.y,
width: 0,
height: 0,
...this.selectionStyle,
fill: "transparent", // 在绘制过程中不显示填充
});
// 添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
/**
* 绘制矩形选区
* @param {Object} options 事件对象
*/
drawRectangleSelection(options) {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
// 计算宽度和高度
const width = Math.abs(pointer.x - this.startPoint.x);
const height = Math.abs(pointer.y - this.startPoint.y);
// 确定左上角坐标
const left = Math.min(this.startPoint.x, pointer.x);
const top = Math.min(this.startPoint.y, pointer.y);
// 更新矩形
this.drawingObject.set({
left: left,
top: top,
width: width,
height: height,
});
this.canvas.renderAll();
}
/**
* 结束矩形选区
*/
endRectangleSelection() {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 将矩形转换为路径
const left = this.drawingObject.left;
const top = this.drawingObject.top;
const width = this.drawingObject.width;
const height = this.drawingObject.height;
// 如果矩形太小,忽略
if (width < 5 || height < 5) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
return;
}
// 创建矩形路径字符串
const pathString = `M ${left} ${top} L ${left + width} ${top} L ${
left + width
} ${top + height} L ${left} ${top + height} Z`;
// 创建最终选区对象
const selectionObj = new fabric.Path(pathString, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
fill: this.selectionStyle.fill, // 恢复填充
});
// 移除绘制中的临时对象
this.canvas.remove(this.drawingObject);
// 重置绘制状态
this.drawingObject = null;
this.startPoint = null;
// 设置选区
this.setSelectionObject(selectionObj);
}
/**
* 开始椭圆选区
* @param {Object} options 事件对象
*/
startEllipseSelection(options) {
if (!this.canvas || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
this.startPoint = pointer;
// 创建椭圆对象
this.drawingObject = new fabric.Ellipse({
left: pointer.x,
top: pointer.y,
rx: 0,
ry: 0,
...this.selectionStyle,
fill: "transparent", // 在绘制过程中不显示填充
// originX: "left",
// originY: "top",
originX: "center",
originY: "center",
});
// 添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
/**
* 绘制椭圆选区
* @param {Object} options 事件对象
*/
drawEllipseSelection(options) {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
// 计算半径
const rx = Math.abs(pointer.x - this.startPoint.x) / 2;
const ry = Math.abs(pointer.y - this.startPoint.y) / 2;
// 确定中心坐标
const left = Math.min(this.startPoint.x, pointer.x);
const top = Math.min(this.startPoint.y, pointer.y);
// 更新椭圆
this.drawingObject.set({
left: left,
top: top,
rx: rx,
ry: ry,
originX: "left",
originY: "top",
});
this.canvas.renderAll();
}
/**
* 结束椭圆选区
*/
endEllipseSelection() {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 获取椭圆参数
const { left, top, rx, ry } = this.drawingObject;
// 如果椭圆太小,忽略
if (rx < 2 || ry < 2) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
return;
}
// 计算中心点
const cx = left + rx;
const cy = top + ry;
// 将椭圆转换为路径字符串
const pathString = this.ellipseToSVGPath(cx, cy, rx, ry);
// 创建最终选区对象
const selectionObj = new fabric.Path(pathString, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
fill: this.selectionStyle.fill, // 恢复填充
});
// 移除绘制中的临时对象
this.canvas.remove(this.drawingObject);
// 重置绘制状态
this.drawingObject = null;
this.startPoint = null;
// 设置选区
this.setSelectionObject(selectionObj);
}
/**
* 将椭圆转换为SVG路径字符串
* @param {Number} cx 中心点X坐标
* @param {Number} cy 中心点Y坐标
* @param {Number} rx X半径
* @param {Number} ry Y半径
* @returns {String} SVG路径字符串
*/
ellipseToSVGPath(cx, cy, rx, ry) {
// 使用椭圆弧命令创建完整椭圆
return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${
cx + rx
} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`;
}
/**
* 设置选区工具
* @param {string} type 选区类型OperationType.LASSO, OperationType.LASSO_RECTANGLE, OperationType.LASSO_ELLIPSE
*/
setSelectionType(type) {
this.selectionType = type;
// 如果正在绘制,清除临时对象
if (this.drawingObject) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
}
}
/**
* 设置选区工具的鼠标事件
*/
setupSelectionEvents() {
// 选区事件现在通过 setCurrentTool 方法管理
// 这个方法现在主要用于刷新或重置事件监听
if (!this.canvas || !this.isActive) return;
// 确保选区处于激活状态
if (this.selectionTools.includes(this.currentTool)) {
this.isActive = true;
// 如果事件还没有初始化,初始化它们
if (!this._mouseDownHandler) {
this.initEvents();
}
}
}
/**
* 清理资源
*/
dispose() {
this.cleanupEvents();
this.clearSelection();
this.canvas = null;
this.commandManager = null;
this.layerManager = null;
}
}

View File

@@ -0,0 +1,582 @@
import { reactive, readonly } from "vue";
import texturePresetManager from "../managers/brushes/TexturePresetManager";
/**
* 笔刷数据存储
* 使用Vue 3的响应式API实现笔刷相关数据的全局状态管理
*/
const state = reactive({
// 笔刷基础属性
size: 5, // 笔刷大小
color: "#000000", // 笔刷颜色
opacity: 1, // 笔刷透明度
type: "pencil", // 当前笔刷类型
// 笔刷材质相关
textureScale: 1, // 材质缩放
textureEnabled: false, // 是否启用材质
texturePath: "", // 材质图片路径
textureOpacity: 1, // 材质透明度
textureRepeat: "repeat", // 材质重复模式
textureAngle: 0, // 材质旋转角度
selectedTextureId: null, // 当前选中的材质ID
// 可用笔刷类型列表 (由BrushManager初始化)
availableBrushes: [],
// 自定义笔刷列表
customBrushes: [],
// 笔刷预设
presets: [
{ name: "细线", size: 2, opacity: 1, color: "#000000", type: "pencil" },
{ name: "中粗", size: 5, opacity: 1, color: "#000000", type: "pencil" },
{ name: "粗线", size: 10, opacity: 1, color: "#000000", type: "pencil" },
{ name: "水彩", size: 15, opacity: 0.7, color: "#3366ff", type: "marker" },
{ name: "喷枪", size: 20, opacity: 0.5, color: "#ff6633", type: "spray" },
],
// 材质预设
texturePresets: [
{
name: "默认纹理",
textureId: "preset_texture_0",
scale: 1,
opacity: 1,
repeat: "repeat",
angle: 0,
},
{
name: "细纹理",
textureId: "preset_texture_1",
scale: 0.5,
opacity: 0.8,
repeat: "repeat",
angle: 0,
},
{
name: "粗纹理",
textureId: "preset_texture_2",
scale: 2,
opacity: 1,
repeat: "repeat",
angle: 45,
},
{
name: "水彩纹理",
textureId: "preset_texture_5",
scale: 1.5,
opacity: 0.6,
repeat: "no-repeat",
angle: 0,
},
],
// 最近使用的颜色
recentColors: ["#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff"],
// 最近使用的材质
recentTextures: [],
// 当前笔刷可配置属性(由当前选中笔刷动态设置)
currentBrushProperties: [],
// 当前笔刷实例的引用
currentBrushInstance: null,
// 笔刷属性值的映射存储可由UI修改的属性值
propertyValues: {},
});
// Actions - 修改状态的函数
const actions = {
setBrushSize(size) {
state.size = Math.max(0.5, Math.min(100, size));
},
setBrushColor(color) {
state.color = color;
// 添加到最近使用的颜色
if (!state.recentColors.includes(color)) {
state.recentColors.unshift(color);
if (state.recentColors.length > 10) {
state.recentColors.pop();
}
}
},
setBrushOpacity(opacity) {
state.opacity = Math.max(0.05, Math.min(1, opacity));
},
setBrushType(type) {
if (state.availableBrushes.some((brush) => brush.id === type)) {
state.type = type;
}
},
setTextureScale(scale) {
state.textureScale = Math.max(0.1, Math.min(10, scale));
},
setTextureEnabled(enabled) {
state.textureEnabled = enabled;
},
setTexturePath(path) {
state.texturePath = path;
},
setAvailableBrushes(brushes) {
state.availableBrushes = brushes;
},
addCustomBrush(brush) {
if (!brush.id) {
brush.id = `custom_${Date.now()}`;
}
state.customBrushes.push(brush);
return brush.id;
},
removeCustomBrush(brushId) {
const index = state.customBrushes.findIndex((b) => b.id === brushId);
if (index !== -1) {
state.customBrushes.splice(index, 1);
return true;
}
return false;
},
// 应用预设
applyPreset(presetIndex) {
const preset = state.presets[presetIndex];
if (preset) {
state.size = preset.size;
state.opacity = preset.opacity;
state.color = preset.color;
state.type = preset.type;
return true;
}
return false;
},
// 将当前设置保存为新预设
saveCurrentAsPreset(name) {
const newPreset = {
name: name || `预设 ${state.presets.length + 1}`,
size: state.size,
opacity: state.opacity,
color: state.color,
type: state.type,
textureEnabled: state.textureEnabled,
textureScale: state.textureScale,
texturePath: state.texturePath,
};
state.presets.push(newPreset);
return state.presets.length - 1; // 返回新预设的索引
},
/**
* 设置当前笔刷实例
* @param {Object} brushInstance BaseBrush实例
*/
setCurrentBrushInstance(brushInstance) {
state.currentBrushInstance = brushInstance;
// 获取并设置当前笔刷的可配置属性
if (brushInstance && brushInstance.getConfigurableProperties) {
const properties = brushInstance.getConfigurableProperties();
state.currentBrushProperties = properties;
// 初始化属性值
properties.forEach((prop) => {
// 如果是基础属性,使用已有值
if (prop.id === "size") {
state.propertyValues[prop.id] = state.size;
} else if (prop.id === "color") {
state.propertyValues[prop.id] = state.color;
} else if (prop.id === "opacity") {
state.propertyValues[prop.id] = state.opacity;
} else {
// 对于特殊属性,使用默认值
state.propertyValues[prop.id] = prop.defaultValue;
}
});
} else {
// 如果没有实例或方法,清空属性列表
state.currentBrushProperties = [];
}
},
/**
* 更新笔刷属性值
* @param {String} propId 属性ID
* @param {any} value 属性值
*/
updatePropertyValue(propId, value) {
// 更新Store中的值
state.propertyValues[propId] = value;
// 同步更新基础属性
if (propId === "size") {
state.size = value;
} else if (propId === "color") {
state.color = value;
} else if (propId === "opacity") {
state.opacity = value;
}
// 如果有当前笔刷实例且有更新方法,则调用
if (
state.currentBrushInstance &&
state.currentBrushInstance.updateProperty
) {
state.currentBrushInstance.updateProperty(propId, value);
}
},
/**
* 获取属性值
* @param {String} propId 属性ID
* @param {any} defaultValue 默认值
* @returns {any} 属性值
*/
getPropertyValue(propId, defaultValue) {
// 检查属性值是否存在
if (state.propertyValues.hasOwnProperty(propId)) {
return state.propertyValues[propId];
}
// 对于基础属性返回store中的值
if (propId === "size") {
return state.size;
} else if (propId === "color") {
return state.color;
} else if (propId === "opacity") {
return state.opacity;
}
// 否则返回默认值
return defaultValue;
},
/**
* 按分类获取当前笔刷可配置属性
* @returns {Object} 按分类分组的属性对象
*/
getPropertiesByCategory() {
const result = {};
state.currentBrushProperties.forEach((prop) => {
const category = prop.category || "默认";
if (!result[category]) {
result[category] = [];
}
result[category].push({
...prop,
value: this.getPropertyValue(prop.id, prop.defaultValue),
});
});
// 按order排序每个分类中的属性
Object.keys(result).forEach((category) => {
result[category].sort((a, b) => (a.order || 0) - (b.order || 0));
});
return result;
},
/**
* 材质相关方法
*/
setTextureOpacity(opacity) {
state.textureOpacity = Math.max(0, Math.min(1, opacity));
},
setTextureRepeat(repeat) {
const validModes = ["repeat", "repeat-x", "repeat-y", "no-repeat"];
if (validModes.includes(repeat)) {
state.textureRepeat = repeat;
}
},
setTextureAngle(angle) {
state.textureAngle = angle % 360;
},
setSelectedTextureId(textureId) {
state.selectedTextureId = textureId;
// 添加到最近使用的材质
if (textureId && !state.recentTextures.includes(textureId)) {
state.recentTextures.unshift(textureId);
if (state.recentTextures.length > 8) {
state.recentTextures.pop();
}
}
},
/**
* 应用材质预设
* @param {Number} presetIndex 预设索引
* @returns {Boolean} 是否应用成功
*/
applyTexturePreset(presetIndex) {
const preset = state.texturePresets[presetIndex];
if (preset) {
state.selectedTextureId = preset.textureId;
state.textureScale = preset.scale;
state.textureOpacity = preset.opacity;
state.textureRepeat = preset.repeat;
state.textureAngle = preset.angle;
// 添加到最近使用
this.setSelectedTextureId(preset.textureId);
return true;
}
return false;
},
/**
* 将当前材质设置保存为新预设
* @param {String} name 预设名称
* @returns {Number} 新预设的索引
*/
saveCurrentTextureAsPreset(name) {
const newPreset = {
name: name || `材质预设 ${state.texturePresets.length + 1}`,
textureId: state.selectedTextureId,
scale: state.textureScale,
opacity: state.textureOpacity,
repeat: state.textureRepeat,
angle: state.textureAngle,
};
state.texturePresets.push(newPreset);
return state.texturePresets.length - 1;
},
/**
* 删除材质预设
* @param {Number} presetIndex 预设索引
* @returns {Boolean} 是否删除成功
*/
removeTexturePreset(presetIndex) {
if (presetIndex >= 0 && presetIndex < state.texturePresets.length) {
state.texturePresets.splice(presetIndex, 1);
return true;
}
return false;
},
/**
* 获取所有可用材质(预设+自定义)
* @returns {Array} 材质列表
*/
getAllTextures() {
return texturePresetManager.getAllTextures();
},
/**
* 根据ID获取材质信息
* @param {String} textureId 材质ID
* @returns {Object|null} 材质对象
*/
getTextureById(textureId) {
return texturePresetManager.getTextureById(textureId);
},
/**
* 按分类获取材质
* @param {String} category 分类名称
* @returns {Array} 材质列表
*/
getTexturesByCategory(category) {
return texturePresetManager.getTexturesByCategory(category);
},
/**
* 获取材质分类列表
* @returns {Array} 分类名称数组
*/
getTextureCategories() {
return texturePresetManager.getCategories();
},
/**
* 搜索材质
* @param {String} keyword 搜索关键词
* @returns {Array} 匹配的材质列表
*/
searchTextures(keyword) {
return texturePresetManager.searchTextures(keyword);
},
/**
* 添加自定义材质
* @param {Object} textureData 材质数据
* @returns {String} 材质ID
*/
addCustomTexture(textureData) {
const textureId = texturePresetManager.addCustomTexture(textureData);
// 保存到本地存储
texturePresetManager.saveCustomTexturesToStorage();
return textureId;
},
/**
* 删除自定义材质
* @param {String} textureId 材质ID
* @returns {Boolean} 是否删除成功
*/
removeCustomTexture(textureId) {
const success = texturePresetManager.removeCustomTexture(textureId);
if (success) {
// 如果删除的是当前选中的材质,清空选择
if (state.selectedTextureId === textureId) {
state.selectedTextureId = null;
}
// 从最近使用中移除
const recentIndex = state.recentTextures.indexOf(textureId);
if (recentIndex !== -1) {
state.recentTextures.splice(recentIndex, 1);
}
// 保存到本地存储
texturePresetManager.saveCustomTexturesToStorage();
}
return success;
},
/**
* 从文件上传自定义材质
* @param {File} file 图片文件
* @param {String} name 材质名称(可选)
* @returns {Promise<String>} 材质ID
*/
uploadCustomTexture(file, name) {
return new Promise((resolve, reject) => {
// 验证文件
if (!texturePresetManager.validateTextureFile(file)) {
reject(new Error("无效的材质文件"));
return;
}
// 读取文件
const reader = new FileReader();
reader.onload = (e) => {
try {
const textureData = {
name: name || file.name.replace(/\.[^/.]+$/, ""),
path: e.target.result,
preview: e.target.result,
description: `用户上传的材质: ${file.name}`,
};
const textureId = this.addCustomTexture(textureData);
resolve(textureId);
} catch (error) {
reject(error);
}
};
reader.onerror = () => {
reject(new Error("文件读取失败"));
};
reader.readAsDataURL(file);
});
},
/**
* 导出材质预设配置
* @returns {String} JSON格式的配置
*/
exportTexturePresets() {
const config = {
texturePresets: state.texturePresets,
customTextures: texturePresetManager.exportCustomTextures(),
};
return JSON.stringify(config, null, 2);
},
/**
* 导入材质预设配置
* @param {String} configJson JSON格式的配置
* @returns {Boolean} 是否导入成功
*/
importTexturePresets(configJson) {
try {
const config = JSON.parse(configJson);
// 导入材质预设
if (config.texturePresets && Array.isArray(config.texturePresets)) {
state.texturePresets = [
...state.texturePresets,
...config.texturePresets,
];
}
// 导入自定义材质
if (config.customTextures) {
texturePresetManager.importCustomTextures(config.customTextures);
}
return true;
} catch (error) {
console.error("导入材质预设失败:", error);
return false;
}
},
/**
* 初始化材质预设管理器
*/
initializeTexturePresets() {
// 从本地存储加载自定义材质
texturePresetManager.loadCustomTexturesFromStorage();
// 确保预设材质引用的是有效的材质ID
state.texturePresets.forEach((preset, index) => {
const texture = texturePresetManager.getTextureById(preset.textureId);
if (!texture) {
console.warn(
`材质预设 "${preset.name}" 引用的材质 ${preset.textureId} 不存在`
);
// 可以选择使用默认材质替换或删除该预设
if (texturePresetManager.getAllTextures().length > 0) {
preset.textureId = texturePresetManager.getAllTextures()[0].id;
}
}
});
},
};
// 暴露给组件使用的Store对象
export const BrushStore = {
// 只读状态,防止直接修改
state: readonly(state),
// 可调用的Actions
...actions,
// 辅助方法
getRGBAColor() {
// 解析十六进制颜色并添加透明度
const hex = state.color.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${state.opacity})`;
},
};

View File

@@ -0,0 +1,31 @@
//import { fabric } from "fabric-with-all";
import { canvasConfig } from "../config/canvasConfig";
/**
* Factory for creating optimized fabric canvas instances
*/
export const createCanvas = (elementId, options = {}) => {
// Create the canvas instance
const canvas = new fabric.Canvas(elementId, {
enableRetinaScaling: canvasConfig.enableRetinaScaling,
renderOnAddRemove: false,
enableRetinaScaling: true,
preserveObjectStacking: true, // 保持对象堆叠顺序
// skipOffscreen: true, // 跳过离屏渲染
...options,
});
return canvas;
};
/**
* Utility to create a static canvas (for improved performance when interaction is not needed)
*/
export const createStaticCanvas = (elementId, options = {}) => {
const canvas = new fabric.StaticCanvas(elementId, {
enableRetinaScaling: canvasConfig.enableRetinaScaling,
...options,
});
return canvas;
};

View File

@@ -0,0 +1,458 @@
export function deepCompare(obj1, obj2) {
const diff = {};
// 处理基础类型
if (obj1 === obj2) {
return null;
}
if (
obj1 === null ||
obj2 === null ||
typeof obj1 !== "object" ||
typeof obj2 !== "object"
) {
return { _value: obj2, _oldValue: obj1 };
}
// 处理数组
if (Array.isArray(obj1) && Array.isArray(obj2)) {
if (obj1.length !== obj2.length) {
return { _value: obj2, _oldValue: obj1 };
}
for (let i = 0; i < obj1.length; i++) {
const itemDiff = deepCompare(obj1[i], obj2[i]);
if (itemDiff !== null) {
diff[i] = itemDiff;
}
}
return Object.keys(diff).length > 0 ? diff : null;
}
// 处理对象
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
for (const key of allKeys) {
const val1 = obj1[key];
const val2 = obj2[key];
if (!(key in obj1)) {
diff[key] = { _value: val2, _type: "added" };
} else if (!(key in obj2)) {
diff[key] = { _value: undefined, _oldValue: val1, _type: "removed" };
} else {
const itemDiff = deepCompare(val1, val2);
if (itemDiff !== null) {
diff[key] = itemDiff;
}
}
}
return Object.keys(diff).length > 0 ? diff : null;
}
/**
* 深度克隆对象
* @param {any} obj 要克隆的对象
* @returns {any} 克隆后的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj);
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}
if (typeof obj === "object") {
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
return obj;
}
/**
* 应用差异到基础对象
* @param {Object} baseObj 基础对象
* @param {Object} diff 差异对象
* @returns {Object} 应用差异后的对象
*/
export function applyDiff(baseObj, diff) {
if (!diff) {
return deepClone(baseObj);
}
// 如果是直接值替换
if (diff._value !== undefined) {
return diff._value;
}
const result = deepClone(baseObj) || {};
for (const key in diff) {
const change = diff[key];
if (change._type === "added" || change._type === "removed") {
if (change._type === "removed") {
delete result[key];
} else {
result[key] = change._value;
}
} else if (change._value !== undefined) {
result[key] = change._value;
} else {
// 递归应用嵌套差异
result[key] = applyDiff(result[key], change);
}
}
return result;
}
/**
* 节流函数
* @param {Function} func 要节流的函数
* @param {number} wait 等待时间
* @returns {Function} 节流后的函数
*/
export function throttle(func, wait) {
let timeout;
let previous = 0;
return function executedFunction(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
return func.apply(this, args);
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now();
timeout = null;
func.apply(this, args);
}, remaining);
}
};
}
/**
* 防抖函数
* @param {Function} func 要防抖的函数
* @param {number} wait 等待时间
* @param {boolean} immediate 是否立即执行
* @returns {Function} 防抖后的函数
*/
export function debounce(func, wait, immediate = false) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
}
/**
* 生成唯一ID
* @param {string} prefix 前缀
* @returns {string} 唯一ID
*/
export function generateId(prefix = "id") {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 格式化文件大小
* @param {number} bytes 字节数
* @returns {string} 格式化后的大小
*/
export function formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
/**
* 格式化时间差
* @param {number} milliseconds 毫秒数
* @returns {string} 格式化后的时间
*/
export function formatDuration(milliseconds) {
if (milliseconds < 1000) {
return `${milliseconds.toFixed(2)}ms`;
} else if (milliseconds < 60000) {
return `${(milliseconds / 1000).toFixed(2)}s`;
} else {
const minutes = Math.floor(milliseconds / 60000);
const seconds = ((milliseconds % 60000) / 1000).toFixed(2);
return `${minutes}m ${seconds}s`;
}
}
/**
* 检查是否是有效的命令对象
* @param {*} command 命令对象
* @returns {boolean} 是否有效
*/
export function isValidCommand(command) {
return (
command &&
typeof command === "object" &&
typeof command.execute === "function"
);
}
/**
* 检查是否是Promise
* @param {*} obj 对象
* @returns {boolean} 是否是Promise
*/
export function isPromise(obj) {
return obj && typeof obj.then === "function";
}
/**
* 安全的JSON解析
* @param {string} jsonString JSON字符串
* @param {*} defaultValue 默认值
* @returns {*} 解析结果
*/
export function safeJSONParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn("JSON解析失败:", error);
return defaultValue;
}
}
/**
* 安全的JSON序列化
* @param {*} obj 要序列化的对象
* @param {*} defaultValue 默认值
* @returns {string} JSON字符串
*/
export function safeJSONStringify(obj, defaultValue = "{}") {
try {
return JSON.stringify(obj);
} catch (error) {
console.warn("JSON序列化失败:", error);
return defaultValue;
}
}
/**
* 计算对象深度
* @param {*} obj 对象
* @returns {number} 深度
*/
export function getObjectDepth(obj) {
if (obj === null || typeof obj !== "object") {
return 0;
}
let maxDepth = 0;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const depth = getObjectDepth(obj[key]);
maxDepth = Math.max(maxDepth, depth);
}
}
return maxDepth + 1;
}
/**
* 计算对象大小(字节)
* @param {*} obj 对象
* @returns {number} 大小(字节)
*/
export function getObjectSize(obj) {
const jsonString = safeJSONStringify(obj, "{}");
return new Blob([jsonString]).size;
}
/**
* 检查浏览器支持
* @returns {Object} 支持信息
*/
export function checkBrowserSupport() {
return {
WeakRef: typeof WeakRef !== "undefined",
FinalizationRegistry: typeof FinalizationRegistry !== "undefined",
PerformanceMemory:
typeof performance !== "undefined" && !!performance.memory,
RequestIdleCallback: typeof requestIdleCallback !== "undefined",
IntersectionObserver: typeof IntersectionObserver !== "undefined",
ResizeObserver: typeof ResizeObserver !== "undefined",
};
}
/**
* 延迟执行
* @param {number} ms 延迟毫秒数
* @returns {Promise} Promise对象
*/
export function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 重试执行函数
* @param {Function} fn 要执行的函数
* @param {Object} options 重试选项
* @returns {Promise} 执行结果
*/
export async function retry(fn, options = {}) {
const {
retries = 3,
delay: delayMs = 1000,
backoff = 1.5,
shouldRetry = () => true,
} = options;
let attempt = 0;
let currentDelay = delayMs;
while (attempt <= retries) {
try {
return await fn();
} catch (error) {
attempt++;
if (attempt > retries || !shouldRetry(error)) {
throw error;
}
await delay(currentDelay);
currentDelay *= backoff;
}
}
}
/**
* 批处理执行
* @param {Array} items 要处理的项目
* @param {Function} processor 处理函数
* @param {Object} options 批处理选项
* @returns {Promise<Array>} 处理结果
*/
export async function batchProcess(items, processor, options = {}) {
const { batchSize = 10, delay: delayMs = 0, onProgress = () => {} } = options;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((item) => processor(item))
);
results.push(...batchResults);
onProgress({
completed: Math.min(i + batchSize, items.length),
total: items.length,
percentage: Math.min(((i + batchSize) / items.length) * 100, 100),
});
if (delayMs > 0 && i + batchSize < items.length) {
await delay(delayMs);
}
}
return results;
}
/**
* 创建可取消的Promise
* @param {Function} executor Promise执行器
* @returns {Object} 包含promise和cancel方法的对象
*/
export function createCancellablePromise(executor) {
let isCancelled = false;
let cancelCallback = null;
const promise = new Promise((resolve, reject) => {
const wrappedResolve = (value) => {
if (!isCancelled) resolve(value);
};
const wrappedReject = (error) => {
if (!isCancelled) reject(error);
};
cancelCallback = () => {
isCancelled = true;
reject(new Error("Promise was cancelled"));
};
executor(wrappedResolve, wrappedReject);
});
return {
promise,
cancel: () => {
if (cancelCallback) {
cancelCallback();
}
},
isCancelled: () => isCancelled,
};
}
// 导出所有工具函数
export default {
deepCompare,
deepClone,
applyDiff,
throttle,
debounce,
generateId,
formatFileSize,
formatDuration,
isValidCommand,
isPromise,
safeJSONParse,
safeJSONStringify,
getObjectDepth,
getObjectSize,
checkBrowserSupport,
delay,
retry,
batchProcess,
createCancellablePromise,
};

View File

@@ -0,0 +1,532 @@
/**
* Should objects be aligned by a bounding box?
* [Bug] Scaled objects sometimes can not be aligned by edges
*
*/
function initAligningGuidelines(canvas) {
var ctx = canvas.getSelectionContext(),
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineWidth = 1,
aligningLineColor = "rgb(0,255,0)",
viewportTransform,
zoom = 1;
function drawVerticalLine(coords) {
drawLine(
coords.x + 0.5,
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
coords.x + 0.5,
coords.y2 > coords.y1 ? coords.y2 : coords.y1
);
}
function drawHorizontalLine(coords) {
drawLine(
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
coords.y + 0.5,
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
coords.y + 0.5
);
}
function drawLine(x1, y1, x2, y2) {
ctx.save();
ctx.lineWidth = aligningLineWidth;
ctx.strokeStyle = aligningLineColor;
ctx.beginPath();
ctx.moveTo(
x1 * zoom + viewportTransform[4],
y1 * zoom + viewportTransform[5]
);
ctx.lineTo(
x2 * zoom + viewportTransform[4],
y2 * zoom + viewportTransform[5]
);
ctx.stroke();
ctx.restore();
}
function isInRange(value1, value2) {
value1 = Math.round(value1);
value2 = Math.round(value2);
for (
var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin;
i <= len;
i++
) {
if (i === value2) {
return true;
}
}
return false;
}
var verticalLines = [],
horizontalLines = [];
canvas.on("mouse:down", function () {
viewportTransform = canvas.viewportTransform;
zoom = canvas.getZoom();
});
canvas.on("object:moving", function (e) {
var activeObject = e.target,
canvasObjects = canvas.getObjects(),
activeObjectCenter = activeObject.getCenterPoint(),
activeObjectLeft = activeObjectCenter.x,
activeObjectTop = activeObjectCenter.y,
activeObjectBoundingRect = activeObject.getBoundingRect(),
activeObjectHeight =
activeObjectBoundingRect.height / viewportTransform[3],
activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0],
horizontalInTheRange = false,
verticalInTheRange = false,
transform = canvas._currentTransform;
if (!transform) return;
// It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions,
// but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move
for (var i = canvasObjects.length; i--; ) {
if (canvasObjects[i] === activeObject) continue;
var objectCenter = canvasObjects[i].getCenterPoint(),
objectLeft = objectCenter.x,
objectTop = objectCenter.y,
objectBoundingRect = canvasObjects[i].getBoundingRect(),
objectHeight = objectBoundingRect.height / viewportTransform[3],
objectWidth = objectBoundingRect.width / viewportTransform[0];
// snaps if the right side of the active object touches the left side of the object
if (
isInRange(
activeObjectLeft + activeObjectWidth / 2,
objectLeft - objectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snaps if the left side of the active object touches the right side of the object
if (
isInRange(
activeObjectLeft - activeObjectWidth / 2,
objectLeft + objectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snaps if the bottom of the object touches the top of the active object
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 + activeObjectHeight / 2
),
"center",
"center"
);
}
// snaps if the top of the object touches the bottom of the active object
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 - activeObjectHeight / 2
),
"center",
"center"
);
}
// snap by the horizontal center line
if (isInRange(objectLeft, activeObjectLeft)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(objectLeft, activeObjectTop),
"center",
"center"
);
}
// snap by the left edge
if (
isInRange(
objectLeft - objectWidth / 2,
activeObjectLeft - activeObjectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snap by the right edge
if (
isInRange(
objectLeft + objectWidth / 2,
activeObjectLeft + activeObjectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snap by the vertical center line
if (isInRange(objectTop, activeObjectTop)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(activeObjectLeft, objectTop),
"center",
"center"
);
}
// snap by the top edge
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 + activeObjectHeight / 2
),
"center",
"center"
);
}
// snap by the bottom edge
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 - activeObjectHeight / 2
),
"center",
"center"
);
}
}
if (!horizontalInTheRange) {
horizontalLines.length = 0;
}
if (!verticalInTheRange) {
verticalLines.length = 0;
}
});
canvas.on("before:render", function () {
if (canvas.contextTop) {
canvas.clearContext(canvas.contextTop);
}
});
canvas.on("after:render", function () {
for (var i = verticalLines.length; i--; ) {
drawVerticalLine(verticalLines[i]);
}
for (var i = horizontalLines.length; i--; ) {
drawHorizontalLine(horizontalLines[i]);
}
verticalLines.length = horizontalLines.length = 0;
});
canvas.on("mouse:up", function () {
verticalLines.length = horizontalLines.length = 0;
canvas.renderAll();
});
}
export default initAligningGuidelines;
/**
* Augments canvas by assigning to `onObjectMove` and `onAfterRender`.
* This kind of sucks because other code using those methods will stop functioning.
* Need to fix it by replacing callbacks with pub/sub kind of subscription model.
* (or maybe use existing fabric.util.fire/observe (if it won't be too slow))
*/
export function initCenteringGuidelines(canvas) {
var canvasWidth = canvas.getWidth(),
canvasHeight = canvas.getHeight(),
canvasWidthCenter = canvasWidth / 2,
canvasHeightCenter = canvasHeight / 2,
canvasWidthCenterMap = {},
canvasHeightCenterMap = {},
centerLineMargin = 4,
centerLineColor = "rgba(255,0,241,0.5)",
centerLineWidth = 1,
ctx = canvas.getSelectionContext(),
viewportTransform;
for (
var i = canvasWidthCenter - centerLineMargin,
len = canvasWidthCenter + centerLineMargin;
i <= len;
i++
) {
canvasWidthCenterMap[Math.round(i)] = true;
}
for (
var i = canvasHeightCenter - centerLineMargin,
len = canvasHeightCenter + centerLineMargin;
i <= len;
i++
) {
canvasHeightCenterMap[Math.round(i)] = true;
}
function showVerticalCenterLine() {
showCenterLine(
canvasWidthCenter + 0.5,
0,
canvasWidthCenter + 0.5,
canvasHeight
);
}
function showHorizontalCenterLine() {
showCenterLine(
0,
canvasHeightCenter + 0.5,
canvasWidth,
canvasHeightCenter + 0.5
);
}
function showCenterLine(x1, y1, x2, y2) {
ctx.save();
ctx.strokeStyle = centerLineColor;
ctx.lineWidth = centerLineWidth;
ctx.beginPath();
ctx.moveTo(x1 * viewportTransform[0], y1 * viewportTransform[3]);
ctx.lineTo(x2 * viewportTransform[0], y2 * viewportTransform[3]);
ctx.stroke();
ctx.restore();
}
var afterRenderActions = [],
isInVerticalCenter,
isInHorizontalCenter;
canvas.on("mouse:down", function () {
viewportTransform = canvas.viewportTransform;
});
canvas.on("object:moving", function (e) {
var object = e.target,
objectCenter = object.getCenterPoint(),
transform = canvas._currentTransform;
if (!transform) return;
(isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap),
(isInHorizontalCenter =
Math.round(objectCenter.y) in canvasHeightCenterMap);
if (isInHorizontalCenter || isInVerticalCenter) {
object.setPositionByOrigin(
new fabric.Point(
isInVerticalCenter ? canvasWidthCenter : objectCenter.x,
isInHorizontalCenter ? canvasHeightCenter : objectCenter.y
),
"center",
"center"
);
}
});
canvas.on("before:render", function () {
if (canvas.contextTop) {
canvas.clearContext(canvas.contextTop);
}
});
canvas.on("after:render", function () {
if (isInVerticalCenter) {
showVerticalCenterLine();
}
if (isInHorizontalCenter) {
showHorizontalCenterLine();
}
});
canvas.on("mouse:up", function () {
// clear these values, to stop drawing guidelines once mouse is up
isInVerticalCenter = isInHorizontalCenter = null;
canvas.renderAll();
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,463 @@
/**
* 图层类型枚举
*/
export const LayerType = {
EMPTY: "empty", // 空图层
BITMAP: "bitmap", // 位图图层
VECTOR: "vector", // 矢量图层
TEXT: "text", // 文字图层
GROUP: "group", // 组图层
ADJUSTMENT: "adjustment", // 调整图层
SMART_OBJECT: "smartObject", // 智能对象
SHAPE: "shape", // 形状图层
VIDEO: "video", // 视频图层 (预留)
AUDIO: "audio", // 音频图层 (预留)
FIXED: "fixed", // 固定图层 - 位于背景图层之上,普通图层之下
};
/**
* 画布操作模式枚举draw(绘画)、select(选择)、pan(拖拽)....
*/
export const OperationType = {
// 编辑器模式
DRAW: "draw", // 绘画模式
ERASER: "eraser", // 橡皮擦模式
SELECT: "select", // 选择模式
PAN: "pan", // 拖拽模式
EYEDROPPER: "eyedropper", // 吸色器模式
// 套索工具
LASSO: "lasso", // 套索工具模式 - 自由套索模式
LASSO_RECTANGLE: "lasso_rectangle", // 套索工具模式 - 矩形模式
LASSO_ELLIPSE: "lasso_ellipse", // 套索工具模式 - 椭圆
// 创建临时选区工具模式 - 类似于临时图层 在这个区域的操作不会影响其他图层
AREA_RECTANGLE: "area_rectangle", // 矩形选区模式
// 材质笔刷工具模式
TEXTURE: "texture", // 选择材质笔刷工具模式 - // 选择材质笔刷后会切换到绘画模式 笔刷固定到材质笔刷
// 液化工具
LIQUIFY: "liquify", // 液化工具模式
// 矢量工具
// VECTOR: "vector", // 矢量工具模式
// 矢量工具模式 - 自由绘制
// VECTOR_FREE: "vector_free",
TEXT: "text", // 文字工具模式
// 红绿图模式
RED_GREEN: "red_green", // 红绿图模式 - 只有红色和绿色笔刷还有橡皮擦 不支持添加其他图片 特殊模式
RED_BRUSH: "red_brush", // 红色笔刷
GREEN_BRUSH: "green_brush", // 绿色笔刷
// SHAPE: "shape", // 形状模式
// 可以根据需要添加更多工具
};
// 所有操作模式类型列表
export const OperationTypes = Object.values(OperationType);
/**
* 混合模式枚举
* 与 fabricjs 和 CSS3 的 globalCompositeOperation 对应
*/
export const BlendMode = {
NORMAL: "source-over", // 正常模式
MULTIPLY: "multiply", // 正片叠底
SCREEN: "screen", // 滤色
OVERLAY: "overlay", // 叠加
DARKEN: "darken", // 变暗
LIGHTEN: "lighten", // 变亮
COLOR_DODGE: "color-dodge", // 颜色减淡
COLOR_BURN: "color-burn", // 颜色加深
HARD_LIGHT: "hard-light", // 强光
SOFT_LIGHT: "soft-light", // 柔光
DIFFERENCE: "difference", // 差值
EXCLUSION: "exclusion", // 排除
HUE: "hue", // 色相
SATURATION: "saturation", // 饱和度
COLOR: "color", // 颜色
LUMINOSITY: "luminosity", // 明度
DESTINATION_IN: "destination-in", // 目标内
DESTINATION_OUT: "destination-out", // 目标外
};
/**
* 判断图层是否为组图层
* @param {Object} layer 要检查的图层
* @returns {boolean} 是否为组图层
*/
export function isGroupLayer(layer) {
if (!layer) return false;
return (
layer.type === LayerType.GROUP ||
(Array.isArray(layer.children) && layer.children.length > 0)
);
}
/**
* 从fabric对象创建图层
* @param {Object} fabricObject fabric对象
* @param {String} layerType 图层类型
* @param {Object} options 其他选项
* @returns {Object} 创建的图层对象
*/
export function createLayerFromFabricObject(
fabricObject,
layerType = "bitmap",
options = {}
) {
if (!fabricObject) return null;
// 确定图层类型
let type = layerType;
if (fabricObject.type === "textbox" || fabricObject.type === "text") {
type = LayerType.TEXT;
} else if (
fabricObject.type === "rect" ||
fabricObject.type === "circle" ||
fabricObject.type === "polygon" ||
fabricObject.type === "polyline"
) {
type = LayerType.SHAPE;
} else if (fabricObject.type === "path" || fabricObject.type === "line") {
type = LayerType.VECTOR;
} else if (fabricObject.type === "image") {
type = LayerType.BITMAP;
}
// 创建基础图层
let layer = createLayer({
...options,
type: type,
name:
options.name ||
`${
fabricObject.type.charAt(0).toUpperCase() + fabricObject.type.slice(1)
} 图层`,
parentId: options.parentId || null,
});
// 添加对象到图层
if (Array.isArray(layer.fabricObjects)) {
layer.fabricObjects.push(fabricObject);
} else {
layer.fabricObjects = [fabricObject];
}
// 如果对象有自己的ID将其与图层关联
if (fabricObject.id) {
fabricObject.layerId = layer.id;
fabricObject.layerName = layer.name;
}
return layer;
}
/**
* 创建标准图层对象
* @param {Object} options 图层选项
* @returns {Object} 图层对象
*/
export function createLayer(options = {}) {
const id =
options.id || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
name: options.name || `图层 ${id.substring(id.lastIndexOf("_") + 1)}`,
type: options.type || LayerType.EMPTY,
visible: options.visible !== undefined ? options.visible : true,
locked: options.locked !== undefined ? options.locked : false,
opacity: options.opacity !== undefined ? options.opacity : 1.0,
blendMode: options.blendMode || BlendMode.NORMAL,
// 确保不是背景图层
isBackground: false,
// Fabric.js 对象列表
fabricObjects: options.fabricObjects || [],
// 嵌套结构 - 适用于组图层
children: options.children || [],
// 剪切蒙版
clippingMask: options.clippingMask || null,
// 位置和大小信息(可选)
bounds: options.bounds || null,
// 图层特定属性
layerProperties: options.layerProperties || {},
// 元数据 - 可用于存储任意数据
metadata: options.metadata || {},
};
}
/**
* 创建背景图层
* @param {Object} options 背景图层选项
* @returns {Object} 背景图层对象
*/
export function createBackgroundLayer(options = {}) {
const id =
options.id || `bg_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
name: options.name || "背景",
type: LayerType.BITMAP,
visible: true,
locked: true, // 背景图层默认锁定
opacity: 1.0, // 背景图层始终不透明
blendMode: BlendMode.NORMAL, // 背景图层始终使用正常混合模式
// 标记为背景图层
isBackground: true,
// 画布尺寸
canvasWidth: options.canvasWidth || 800,
canvasHeight: options.canvasHeight || 600,
backgroundColor: options.backgroundColor || "#ffffff",
// Fabric.js 背景对象 (单个矩形对象)
fabricObject: null, // 创建后设置
// Fabric.js 对象列表
fabricObjects: [], // 创建后设置
// 无子图层
children: [],
// 元数据 - 可用于存储任意数据
metadata: options.metadata || {},
};
}
/**
* 创建位图图层
* @param {Object} options 图层选项
* @returns {Object} 位图图层对象
*/
export function createBitmapLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.BITMAP,
});
// 添加位图特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
filters: options.filters || [], // 滤镜数组
imageUrl: options.imageUrl || null, // 图片URL
imageElement: options.imageElement || null, // 图片元素
};
return baseLayer;
}
/**
* 创建文本图层
* @param {Object} options 图层选项
* @returns {Object} 文本图层对象
*/
export function createTextLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.TEXT,
});
// 添加文字特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
text: options.text || "新文本",
fontFamily: options.fontFamily || "Arial",
fontSize: options.fontSize || 24,
fontWeight: options.fontWeight || "normal",
fontStyle: options.fontStyle || "normal",
textAlign: options.textAlign || "left",
underline: options.underline || false,
overline: options.overline || false,
linethrough: options.linethrough || false,
textBackgroundColor: options.textBackgroundColor || "transparent",
lineHeight: options.lineHeight || 1.16,
charSpacing: options.charSpacing || 0,
};
return baseLayer;
}
/**
* 创建矢量图层
* @param {Object} options 图层选项
* @returns {Object} 矢量图层对象
*/
export function createVectorLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.VECTOR,
});
// 添加矢量特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
vectorType: options.vectorType || "path", // path, polygon, polyline等
strokeWidth: options.strokeWidth !== undefined ? options.strokeWidth : 1,
strokeColor: options.strokeColor || "#000000",
fillColor: options.fillColor || "transparent",
fillRule: options.fillRule || "nonzero",
strokeLineCap: options.strokeLineCap || "butt",
strokeLineJoin: options.strokeLineJoin || "miter",
strokeDashArray: options.strokeDashArray || null,
strokeDashOffset: options.strokeDashOffset || 0,
};
return baseLayer;
}
/**
* 创建形状图层
* @param {Object} options 图层选项
* @returns {Object} 形状图层对象
*/
export function createShapeLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.SHAPE,
});
// 添加形状特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
shapeType: options.shapeType || "rect", // rect, circle, ellipse等
strokeWidth: options.strokeWidth !== undefined ? options.strokeWidth : 1,
strokeColor: options.strokeColor || "#000000",
fillColor: options.fillColor || "#ffffff",
rx: options.rx || 0, // 矩形圆角
ry: options.ry || 0, // 矩形圆角
};
return baseLayer;
}
/**
* 创建调整图层
* @param {Object} options 图层选项
* @returns {Object} 调整图层对象
*/
export function createAdjustmentLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.ADJUSTMENT,
});
// 添加调整图层特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
adjustmentType: options.adjustmentType || "brightness", // brightness, contrast, hue, saturation等
value: options.value !== undefined ? options.value : 0,
affectedLayerIds: options.affectedLayerIds || [], // 受影响的图层ID列表
};
return baseLayer;
}
/**
* 创建智能对象图层
* @param {Object} options 图层选项
* @returns {Object} 智能对象图层
*/
export function createSmartObjectLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.SMART_OBJECT,
});
// 添加智能对象特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
sourceType: options.sourceType || "image", // image, vector, embedded等
sourceUrl: options.sourceUrl || null,
sourceData: options.sourceData || null,
originalWidth: options.originalWidth || 0,
originalHeight: options.originalHeight || 0,
embedded: options.embedded !== undefined ? options.embedded : true,
};
return baseLayer;
}
/**
* 创建固定图层 - 位于背景图层之上,普通图层之下
* @param {Object} options 固定图层选项
* @returns {Object} 固定图层对象
*/
export function createFixedLayer(options = {}) {
const id =
options.id ||
`fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
name: options.name || "固定图层",
type: LayerType.FIXED,
visible: true, // 固定图层始终可见
locked: true, // 固定图层默认锁定
opacity: options.opacity !== undefined ? options.opacity : 1.0,
blendMode: options.blendMode || BlendMode.NORMAL,
// 标记为固定图层
isFixed: true,
isBackground: false,
// Fabric.js 对象列表
fabricObjects: options.fabricObjects || [],
// 无子图层
children: [],
// 元数据 - 可用于存储任意数据
metadata: options.metadata || {},
};
}
/**
* 深拷贝图层对象
* @param {Object} layer 要拷贝的图层
* @returns {Object} 拷贝后的图层
*/
export function cloneLayer(layer) {
if (!layer) return null;
// 基本属性深拷贝
const clonedLayer = {
...JSON.parse(JSON.stringify(layer)), // 深拷贝基本属性
fabricObjects: [], // 重置,后面处理
};
// 复制 fabric 对象 (如果存在)
if (Array.isArray(layer.fabricObjects)) {
clonedLayer.fabricObjects = layer.fabricObjects.map((obj) => {
return obj && typeof obj.clone === "function"
? obj.clone()
: JSON.parse(JSON.stringify(obj));
});
}
// 复制背景对象 (如果存在)
if (layer.isBackground && layer.fabricObject) {
clonedLayer.fabricObject =
typeof layer.fabricObject.clone === "function"
? layer.fabricObject.clone()
: JSON.parse(JSON.stringify(layer.fabricObject));
}
// 递归复制子图层
if (Array.isArray(layer.children) && layer.children.length > 0) {
clonedLayer.children = layer.children.map((child) => cloneLayer(child));
}
return clonedLayer;
}

View File

@@ -0,0 +1,49 @@
<template>
<div class="c-svg">
<svg
:class="svgClass"
v-bind="$attrs"
:style="{ color: color, fontSize: size + 'px' }"
>
<use :href="iconName"></use>
</svg>
</div>
</template>
<script setup>
import { computed } from "vue";
const props = defineProps({
name: {
type: String,
required: true,
},
color: {
type: String,
default: "",
},
size: {
type: [Number, String],
default: 16,
},
});
const iconName = computed(() => `#icon-${props.name}`);
const svgClass = computed(() => {
if (props.name) return `svg-icon icon-${props.name}`;
return "svg-icon";
});
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
}
.c-svg {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -1,360 +1,83 @@
<template>
<div class="generalCanvas">
<div class="argument-box">
<argument ref="argument" v-if="canvasObj.canvas" :elementList="elementList"></argument>
</div>
<div class="canvasBox">
<tool ref="tool" v-if="canvasObj.canvas" @toolLiquefaction="toolLiquefaction"></tool>
<div class="canvas">
<canvasContent ref="canvasContent"></canvasContent>
</div>
<div class="detail-box">
<detail ref="detail" v-if="canvasObj.canvas"></detail>
</div>
</div>
<div class="mark_loading" v-show="isShowMark">
<a-spin size="large" />
</div>
<div style="display: flex; justify-content: center; margin-top: 2rem;">
<div class="gallery_btn" style="margin: 0 2rem;" @click="setShare">{{ $t('exportModel.Share') }}</div>
<div class="gallery_btn" style="margin: 0 2rem;" @click="setExport">{{ $t('exportModel.Export') }}</div>
<div class="gallery_btn" style="margin: 0 2rem;" @click="setExport">{{ $t('exportModel.Export') }}</div>
</div>
<liquefaction ref="liquefaction" @submitLiquefaction="submitLiquefaction"></liquefaction>
<publish ref="publish" @clearPublish="()=>{}"></publish>
</div>
<div class="red-green-mode-example">
<!-- 画布编辑器 - 永久启用红绿图模式 -->
<div class="canvas-wrapper">
<div class="canvas-wrapper-btns">
<div @click="getJSON">获取JSON</div>
<div @click="loadJSON">读取JSON</div>
</div>
<CanvasEditor
ref="canvasEditor"
:enabledRedGreenMode="false"
:clothingImageUrl="imageUrls.baseImage"
:redGreenImageUrl="imageUrls.maskImage"
/>
</div>
</div>
</template>
<script>
import {defineComponent, computed, provide, h, ref, watch, onBeforeUnmount, reactive, onMounted, toRefs,
} from "vue";
import {message} from 'ant-design-vue'
import { Https } from "@/tool/https";
import { useStore } from "vuex";
import { useI18n } from "vue-i18n";
import canvasGeneral from "@/tool/canvasGeneralCopy";
import tool from "./tool.vue"
import argument from "./argument.vue"
import detail from "./detail.vue"
import canvasContent from "./canvasContent.vue"
import liquefaction from "@/component/modules/liquefaction.vue";
import JSZip, { forEach } from "jszip";
import publish from "@/component/WorksPage/publish.vue";
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import CanvasEditor from "./CanvasEditor/index.vue";
export default defineComponent({
components: {
tool,
argument,
detail,
canvasContent,
liquefaction,publish
},
props: {
isState: {
type: Boolean,
default: false,
},
},
setup(props,{emit}) {
watch(()=>props.isState,(newVal)=>{
if(!newVal && canvasObj)canvasObj.clearEvent()
})
const { t } = useI18n();
const store = useStore();
const isShowMark = ref(false)
let liquefaction = ref(null)
let liquefactionData = ref()
let groupDashed = ref(null)//用来判断是否需要对group添加img
let canvasType = 'export'
let canvasObj = reactive(new canvasGeneral('export'))
const dataDom = reactive({
argument:null,
canvasContent:null,
tool:null,
detail:null,
publish:null,
})
let data = reactive({
elementList:null,
showCanvas:false,
// 画布编辑器引用
const canvasEditor = ref(null);
})
provide('canvasType',canvasType)
provide('canvasObj',canvasObj)
provide('isShowMark',isShowMark)
const close = ()=>{
dataDom.forEach((item)=>{
if(item.closeModal)item.closeModal()
})
}
let expoet = ()=>{
console.log( canvasObj.selectExport());
console.log( canvasObj.allExport());
// 图像URL配置
const imageUrls = {
baseImage: "@/assets/redGreenPic/clothing_base_image.png",
maskImage: "@/assets/redGreenPic/clothing_mask_image.png",
};
const getJSON = () => {
if (canvasEditor.value) {
const json = canvasEditor.value.getJSON();
console.log("获取的JSON数据", json);
localStorage.setItem("redGreenModeJSON", json);
}
};
}
const setLiquefaction = async ()=>{//进入液化页面
canvasObj.getLiquefactionImgObj().then((data)=>{
if(data?.img){
liquefactionData.value = data
liquefaction.value.init(data.img)
}else {
message.info(t('exportModel.jsContent6'))
return null;
}
})
}
const toolLiquefaction = ()=>{//工具点击按钮
setLiquefaction()
}
const submitLiquefaction = (rv)=>{//液化回参
canvasObj.setLiquefactionImgObj(liquefactionData.value,rv)
// liquefactionData.value.setSrc(rv, (value)=>{
// // liquefactionData.value.scaleToWidth(originalWidth);
// // liquefactionData.value.scaleToHeight(originalHeight);
// delete liquefactionData.value.minioUrl
// if(groupDashed.value && groupDashed.value._objects.length == 1){
// value.set({
// left:-groupDashed.value.width/2,
// top:-groupDashed.value.height/2,
// })
// groupDashed.value.insertAt(value)
// // canvasObj.addDashedImg(value)
// }
// canvasObj.canvas.renderAll();
// canvasObj.updateCanvasState()
// });
}
const getCanvasData = ()=>{
if(!canvasObj.canvas)return
var json = canvasObj.canvas.toJSON(['src','minioUrl','custom','perPixelTargetFind','hasBorders','selectable','hasControls','erasable']);
json.objects.forEach(item=>{
if(item.type == 'image'){
delete item.src
}
})
let canvasExport = {
canvas:json,
groupList: canvasObj.layer.list
}
return canvasExport
}
const openSetData = async ()=>{
//获取所有所选元素
let arr = store.state.Workspace.projectList
let obj = {}
for (let index = 0; index < arr.length; index++) {
const item = arr[index];
await new Promise((resolve, reject) => {
store.dispatch('getProjectCanvasData',item.value).then((value)=>{
const keys = Object.keys(value)[0];
if(!value[keys] || value[keys].length == 0){
resolve('')
return
}
if(keys == 'design'){
value[keys].forEach((designItem)=>{
let minioUrl = designItem.url
designItem.url = designItem.designOutfitUrl
designItem.minioUrl = designItem.minioUrl
})
}
let rv = {
list:value[keys],
name:item.name,
}
obj[keys] = rv
resolve('')
})
})
}
data.elementList = obj
//获取所有所选元素 END
const loadJSON = () => {
if (canvasEditor.value) {
const currentJSON = localStorage.getItem("redGreenModeJSON");
canvasEditor.value.loadJSON(currentJSON);
console.log("加载的JSON数据", currentJSON);
}
};
if(data.showCanvas)return
data.showCanvas = true
dataDom.canvasContent.openSetData()
}
let setShare = async ()=>{
dataDom.publish.publishMask = true
canvasObj.detailSubmit().then((img)=>{
let data = {
"imgUrl":img,
userlikeGroupId:store.state.Workspace.probjects.id,
}
dataDom.publish.init(data)
})
}
//设置导出
let setExport = async () => {
var imageDataURL = await canvasObj.detailSubmit()
let a = document.createElement("a");
let img = [];
let index = 0;
img.push({
imgUrl: imageDataURL,
name: "collection.png",
});
let num = 0;
for (let key in data.elementList) {
for (let index = 0; index < data.elementList[key].list.length; index++) {
const item = data.elementList[key].list[index];
let url = item.imgUrl || item.url || item.designOutfitUrl || item.minioUrl
let name = url?.split(".").pop().split("?").shift();
img.push({imgUrl:url,name:`element${index}.${name}`})
}
}
downImg(img);
};
let getImgArrayBuffer = (url) => {
return new Promise((resolve, reject) => {
//通过请求获取文件blob格式
let xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", url, true);
xmlhttp.responseType = "blob";
xmlhttp.withCredentials = false;
xmlhttp.onload = function () {
if (this.status == 200) {
resolve(this.response);
} else {
reject(this.status);
}
};
xmlhttp.send();
});
};
let downImg = (imagesParams) => {
let zip = new JSZip();
let cache = {};
let promises = [];
for (let item of imagesParams) {
const promise = getImgArrayBuffer(item.imgUrl).then((data) => {
// 下载文件, 并存成ArrayBuffer对象(blob)
zip.file(item.name, data, { binary: true }); // 逐个添加文件
cache[item.title] = data;
})
promises.push(promise);
}
Promise.all(promises)
.then(() => {
function downloadBlob(blob, filename) {
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
document.body.removeChild(link);
}
zip.generateAsync({ type: "blob" }).then((content) => {
// 生成二进制流
downloadBlob(content,'DesignFiles')
// FileSaver.saveAs(content, "DesignFiles"); // 利用file-saver保存文件 自定义文件名
isShowMark.value = false;
// 组件挂载时绑定键盘事件
onMounted(() => {});
});
setSubmit()//导出的时候保存
initAligningGuidelines(canvas,true)
})
.catch((res) => {
// message.warning(t('HomeView.jsContent3'));
isShowMark.value = false;
initAligningGuidelines(canvas,true)
});
};
onMounted(() => {
});
onBeforeUnmount(()=>{
canvasObj.canvasClear()
})
return {
isShowMark,
liquefaction,
...toRefs(dataDom),
...toRefs(data),
canvasObj,
close,
expoet,
toolLiquefaction,
submitLiquefaction,
getCanvasData,
openSetData,
setShare,
setExport,
};
},
data(prop) {
return {
};
},
computed: {
},
watch: {
},
mounted() {},
methods: {
},
});
// 组件卸载时移除键盘事件
onUnmounted(() => {});
</script>
<style lang="less" scoped>
.generalCanvas{
width: 100%;
<style scoped lang="less">
.red-green-mode-example {
width: 100%;
// height: 100vh;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
.argument-box,
.canvasBox,
.detail-box{
:deep(i){
font-size: 2.5rem;
cursor: pointer;
width: 5rem;
height: 5rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all .3s;
margin-bottom: .5rem;
padding: 1rem;
&.active{
border: 1px solid;
border-radius: .4rem;
}
&.icon-xiala{
transform: rotate(-90deg);
}
&.icon-xialaActive{
transform: rotate(90deg);
}
}
}
.argument-box{
margin-left: 4rem;
height: 4rem;
margin-bottom: 1rem;
}
.canvasBox{
flex: 1;
overflow: hidden;
display: flex;
.canvas{
flex: 1;
overflow: hidden;
}
}
.detail-box{
width: 20%;
margin-left: 1rem;
}
display: flex;
flex-direction: column;
}
</style>
.canvas-wrapper {
flex: 1;
position: relative;
}
.canvas-wrapper-btns {
position: fixed;
top: 0;
right: 150px;
z-index: 1000;
display: flex;
gap: 20px;
& > div {
cursor: pointer;
padding: 10px 20px;
background-color: #f0f0f0;
border-radius: 5px;
transition: background-color 0.3s;
}
}
</style>

View File

@@ -0,0 +1,360 @@
<template>
<div class="generalCanvas">
<div class="argument-box">
<argument ref="argument" v-if="canvasObj.canvas" :elementList="elementList"></argument>
</div>
<div class="canvasBox">
<tool ref="tool" v-if="canvasObj.canvas" @toolLiquefaction="toolLiquefaction"></tool>
<div class="canvas">
<canvasContent ref="canvasContent"></canvasContent>
</div>
<div class="detail-box">
<detail ref="detail" v-if="canvasObj.canvas"></detail>
</div>
</div>
<div class="mark_loading" v-show="isShowMark">
<a-spin size="large" />
</div>
<div style="display: flex; justify-content: center; margin-top: 2rem;">
<div class="gallery_btn" style="margin: 0 2rem;" @click="setShare">{{ $t('exportModel.Share') }}</div>
<div class="gallery_btn" style="margin: 0 2rem;" @click="setExport">{{ $t('exportModel.Export') }}</div>
<div class="gallery_btn" style="margin: 0 2rem;" @click="setExport">{{ $t('exportModel.Export') }}</div>
</div>
<liquefaction ref="liquefaction" @submitLiquefaction="submitLiquefaction"></liquefaction>
<publish ref="publish" @clearPublish="()=>{}"></publish>
</div>
</template>
<script>
import {defineComponent, computed, provide, h, ref, watch, onBeforeUnmount, reactive, onMounted, toRefs,
} from "vue";
import {message} from 'ant-design-vue'
import { Https } from "@/tool/https";
import { useStore } from "vuex";
import { useI18n } from "vue-i18n";
import canvasGeneral from "@/tool/canvasGeneralCopy";
import tool from "./tool.vue"
import argument from "./argument.vue"
import detail from "./detail.vue"
import canvasContent from "./canvasContent.vue"
import liquefaction from "@/component/modules/liquefaction.vue";
import JSZip, { forEach } from "jszip";
import publish from "@/component/WorksPage/publish.vue";
export default defineComponent({
components: {
tool,
argument,
detail,
canvasContent,
liquefaction,publish
},
props: {
isState: {
type: Boolean,
default: false,
},
},
setup(props,{emit}) {
watch(()=>props.isState,(newVal)=>{
if(!newVal && canvasObj)canvasObj.clearEvent()
})
const { t } = useI18n();
const store = useStore();
const isShowMark = ref(false)
let liquefaction = ref(null)
let liquefactionData = ref()
let groupDashed = ref(null)//用来判断是否需要对group添加img
let canvasType = 'export'
let canvasObj = reactive(new canvasGeneral('export'))
const dataDom = reactive({
argument:null,
canvasContent:null,
tool:null,
detail:null,
publish:null,
})
let data = reactive({
elementList:null,
showCanvas:false,
})
provide('canvasType',canvasType)
provide('canvasObj',canvasObj)
provide('isShowMark',isShowMark)
const close = ()=>{
dataDom.forEach((item)=>{
if(item.closeModal)item.closeModal()
})
}
let expoet = ()=>{
console.log( canvasObj.selectExport());
console.log( canvasObj.allExport());
}
const setLiquefaction = async ()=>{//进入液化页面
canvasObj.getLiquefactionImgObj().then((data)=>{
if(data?.img){
liquefactionData.value = data
liquefaction.value.init(data.img)
}else {
message.info(t('exportModel.jsContent6'))
return null;
}
})
}
const toolLiquefaction = ()=>{//工具点击按钮
setLiquefaction()
}
const submitLiquefaction = (rv)=>{//液化回参
canvasObj.setLiquefactionImgObj(liquefactionData.value,rv)
// liquefactionData.value.setSrc(rv, (value)=>{
// // liquefactionData.value.scaleToWidth(originalWidth);
// // liquefactionData.value.scaleToHeight(originalHeight);
// delete liquefactionData.value.minioUrl
// if(groupDashed.value && groupDashed.value._objects.length == 1){
// value.set({
// left:-groupDashed.value.width/2,
// top:-groupDashed.value.height/2,
// })
// groupDashed.value.insertAt(value)
// // canvasObj.addDashedImg(value)
// }
// canvasObj.canvas.renderAll();
// canvasObj.updateCanvasState()
// });
}
const getCanvasData = ()=>{
if(!canvasObj.canvas)return
var json = canvasObj.canvas.toJSON(['src','minioUrl','custom','perPixelTargetFind','hasBorders','selectable','hasControls','erasable']);
json.objects.forEach(item=>{
if(item.type == 'image'){
delete item.src
}
})
let canvasExport = {
canvas:json,
groupList: canvasObj.layer.list
}
return canvasExport
}
const openSetData = async ()=>{
//获取所有所选元素
let arr = store.state.Workspace.projectList
let obj = {}
for (let index = 0; index < arr.length; index++) {
const item = arr[index];
await new Promise((resolve, reject) => {
store.dispatch('getProjectCanvasData',item.value).then((value)=>{
const keys = Object.keys(value)[0];
if(!value[keys] || value[keys].length == 0){
resolve('')
return
}
if(keys == 'design'){
value[keys].forEach((designItem)=>{
let minioUrl = designItem.url
designItem.url = designItem.designOutfitUrl
designItem.minioUrl = designItem.minioUrl
})
}
let rv = {
list:value[keys],
name:item.name,
}
obj[keys] = rv
resolve('')
})
})
}
data.elementList = obj
//获取所有所选元素 END
if(data.showCanvas)return
data.showCanvas = true
dataDom.canvasContent.openSetData()
}
let setShare = async ()=>{
dataDom.publish.publishMask = true
canvasObj.detailSubmit().then((img)=>{
let data = {
"imgUrl":img,
userlikeGroupId:store.state.Workspace.probjects.id,
}
dataDom.publish.init(data)
})
}
//设置导出
let setExport = async () => {
var imageDataURL = await canvasObj.detailSubmit()
let a = document.createElement("a");
let img = [];
let index = 0;
img.push({
imgUrl: imageDataURL,
name: "collection.png",
});
let num = 0;
for (let key in data.elementList) {
for (let index = 0; index < data.elementList[key].list.length; index++) {
const item = data.elementList[key].list[index];
let url = item.imgUrl || item.url || item.designOutfitUrl || item.minioUrl
let name = url?.split(".").pop().split("?").shift();
img.push({imgUrl:url,name:`element${index}.${name}`})
}
}
downImg(img);
};
let getImgArrayBuffer = (url) => {
return new Promise((resolve, reject) => {
//通过请求获取文件blob格式
let xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", url, true);
xmlhttp.responseType = "blob";
xmlhttp.withCredentials = false;
xmlhttp.onload = function () {
if (this.status == 200) {
resolve(this.response);
} else {
reject(this.status);
}
};
xmlhttp.send();
});
};
let downImg = (imagesParams) => {
let zip = new JSZip();
let cache = {};
let promises = [];
for (let item of imagesParams) {
const promise = getImgArrayBuffer(item.imgUrl).then((data) => {
// 下载文件, 并存成ArrayBuffer对象(blob)
zip.file(item.name, data, { binary: true }); // 逐个添加文件
cache[item.title] = data;
})
promises.push(promise);
}
Promise.all(promises)
.then(() => {
function downloadBlob(blob, filename) {
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(url);
document.body.removeChild(link);
}
zip.generateAsync({ type: "blob" }).then((content) => {
// 生成二进制流
downloadBlob(content,'DesignFiles')
// FileSaver.saveAs(content, "DesignFiles"); // 利用file-saver保存文件 自定义文件名
isShowMark.value = false;
});
setSubmit()//导出的时候保存
initAligningGuidelines(canvas,true)
})
.catch((res) => {
// message.warning(t('HomeView.jsContent3'));
isShowMark.value = false;
initAligningGuidelines(canvas,true)
});
};
onMounted(() => {
});
onBeforeUnmount(()=>{
canvasObj.canvasClear()
})
return {
isShowMark,
liquefaction,
...toRefs(dataDom),
...toRefs(data),
canvasObj,
close,
expoet,
toolLiquefaction,
submitLiquefaction,
getCanvasData,
openSetData,
setShare,
setExport,
};
},
data(prop) {
return {
};
},
computed: {
},
watch: {
},
mounted() {},
methods: {
},
});
</script>
<style lang="less" scoped>
.generalCanvas{
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
.argument-box,
.canvasBox,
.detail-box{
:deep(i){
font-size: 2.5rem;
cursor: pointer;
width: 5rem;
height: 5rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all .3s;
margin-bottom: .5rem;
padding: 1rem;
&.active{
border: 1px solid;
border-radius: .4rem;
}
&.icon-xiala{
transform: rotate(-90deg);
}
&.icon-xialaActive{
transform: rotate(90deg);
}
}
}
.argument-box{
margin-left: 4rem;
height: 4rem;
margin-bottom: 1rem;
}
.canvasBox{
flex: 1;
overflow: hidden;
display: flex;
.canvas{
flex: 1;
overflow: hidden;
}
}
.detail-box{
width: 20%;
margin-left: 1rem;
}
}
</style>