接入画布

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

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;
}
}