接入画布
This commit is contained in:
1186
src/component/Canvas/CanvasEditor/managers/CanvasManager.js
Normal file
1186
src/component/Canvas/CanvasEditor/managers/CanvasManager.js
Normal file
File diff suppressed because it is too large
Load Diff
355
src/component/Canvas/CanvasEditor/managers/ExportManager.js
Normal file
355
src/component/Canvas/CanvasEditor/managers/ExportManager.js
Normal 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;
|
||||
}
|
||||
}
|
||||
2198
src/component/Canvas/CanvasEditor/managers/LayerManager.js
Normal file
2198
src/component/Canvas/CanvasEditor/managers/LayerManager.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 已销毁");
|
||||
}
|
||||
}
|
||||
370
src/component/Canvas/CanvasEditor/managers/ThumbnailManager.js
Normal file
370
src/component/Canvas/CanvasEditor/managers/ThumbnailManager.js
Normal 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;
|
||||
}
|
||||
}
|
||||
1192
src/component/Canvas/CanvasEditor/managers/ToolManager.js
Normal file
1192
src/component/Canvas/CanvasEditor/managers/ToolManager.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
234
src/component/Canvas/CanvasEditor/managers/brushes/BaseBrush.js
Normal file
234
src/component/Canvas/CanvasEditor/managers/brushes/BaseBrush.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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=";
|
||||
}
|
||||
}
|
||||
@@ -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+";
|
||||
}
|
||||
}
|
||||
@@ -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=";
|
||||
}
|
||||
}
|
||||
@@ -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+";
|
||||
}
|
||||
}
|
||||
@@ -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+";
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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==";
|
||||
}
|
||||
}
|
||||
@@ -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+";
|
||||
}
|
||||
}
|
||||
@@ -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+";
|
||||
}
|
||||
}
|
||||
@@ -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=";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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+";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 = {};
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user