Merge branch 'dev_vite' of https://e.coding.net/aidlabfashion/aida/aida_front into dev_vite
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { findObjectById } from "../utils/helper";
|
||||
import { Command } from "./Command";
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
@@ -129,14 +130,13 @@ export class UpdateBackgroundCommand extends Command {
|
||||
});
|
||||
this.canvas = options.canvas;
|
||||
this.layers = options.layers;
|
||||
this.backgroundColor = options.backgroundColor;
|
||||
this.backgroundColorValue = options.backgroundColorValue; // 使用.value获取实际值
|
||||
this.backgroundColor = options.backgroundColor; //
|
||||
this.historyManager = options.historyManager;
|
||||
|
||||
// 查找背景图层
|
||||
this.bgLayer = this.layers.value.find((layer) => layer.isBackground);
|
||||
this.oldBackgroundColor = this.bgLayer
|
||||
? this.bgLayer.backgroundColor
|
||||
: "#ffffff";
|
||||
this.oldBackgroundColor = this.bgLayer.backgroundColor;
|
||||
}
|
||||
|
||||
execute() {
|
||||
@@ -150,10 +150,14 @@ export class UpdateBackgroundCommand extends Command {
|
||||
|
||||
// 更新背景对象属性
|
||||
if (this.bgLayer.fabricObject) {
|
||||
this.bgLayer.fabricObject.set("fill", this.backgroundColor);
|
||||
const { object } = findObjectById(
|
||||
this.canvas,
|
||||
this.bgLayer.fabricObject.id
|
||||
);
|
||||
object.set("fill", this.backgroundColor);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
this.backgroundColorValue.value = this.backgroundColor; // 设置背景颜色
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,10 +171,14 @@ export class UpdateBackgroundCommand extends Command {
|
||||
|
||||
// 恢复背景对象属性
|
||||
if (this.bgLayer.fabricObject) {
|
||||
this.bgLayer.fabricObject.set("fill", this.oldBackgroundColor);
|
||||
const { object } = findObjectById(
|
||||
this.canvas,
|
||||
this.bgLayer.fabricObject.id
|
||||
);
|
||||
object.set("fill", this.oldBackgroundColor);
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
this.backgroundColorValue.value = this.oldBackgroundColor; // 恢复背景颜色
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
createLayer,
|
||||
findInChildLayers,
|
||||
LayerType,
|
||||
} from "../utils/layerHelper.js";
|
||||
import { createRasterizedImage } from "../utils/selectionToImage.js";
|
||||
import { CompositeCommand } from "./Command.js";
|
||||
import { CreateImageLayerCommand } from "./LayerCommands.js";
|
||||
@@ -27,6 +32,15 @@ export class LassoCutoutCommand extends CompositeCommand {
|
||||
// 高清截图选项
|
||||
this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用
|
||||
this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数
|
||||
|
||||
this.groupId = options.groupId || `cutout-group-${Date.now()}`;
|
||||
this.groupName = options.groupName || `选区组`;
|
||||
this.groupLayer = null; // 新增:保存组图层的引用
|
||||
this.originalLayersLength = 0; // 新增:保存原始图层数量
|
||||
|
||||
// 序列化保存选区对象,用于重做时恢复
|
||||
this.serializedSelectionObject = null;
|
||||
this._serializeSelectionObject();
|
||||
}
|
||||
|
||||
async execute() {
|
||||
@@ -38,8 +52,11 @@ export class LassoCutoutCommand extends CompositeCommand {
|
||||
try {
|
||||
this.executedCommands = [];
|
||||
|
||||
// 保存原始图层数量,用于撤销时的验证
|
||||
this.originalLayersLength = this.layerManager.layers.value.length;
|
||||
|
||||
// 获取选区
|
||||
const selectionObject = this.selectionManager.getSelectionObject();
|
||||
const selectionObject = await this._getSelectionObject();
|
||||
if (!selectionObject) {
|
||||
console.error("无法执行套索抠图:当前没有选区");
|
||||
return false;
|
||||
@@ -112,10 +129,47 @@ export class LassoCutoutCommand extends CompositeCommand {
|
||||
await clearSelectionCmd.execute();
|
||||
this.executedCommands.push(clearSelectionCmd);
|
||||
|
||||
const topLayerIndex = this.layerManager.layers.value.findIndex(
|
||||
(layer) => layer.id === this.newLayerId
|
||||
);
|
||||
|
||||
const selectLayer = this.layerManager.layers.value[topLayerIndex];
|
||||
|
||||
// 创建新的组图层
|
||||
this.groupLayer = createLayer({
|
||||
id: this.groupId,
|
||||
name: this.groupName || `选区组`,
|
||||
type: LayerType.GROUP,
|
||||
visible: true,
|
||||
locked: false,
|
||||
opacity: 1.0,
|
||||
fabricObjects: [],
|
||||
children: [],
|
||||
});
|
||||
|
||||
this.fabricImage.set({
|
||||
selectable: true,
|
||||
evented: true,
|
||||
});
|
||||
|
||||
selectLayer.parentId = this.groupId; // 设置新图层的parentId为组图层ID
|
||||
selectLayer.fabricObjects = [
|
||||
this.fabricImage.toObject("id", "layerId", "layerName", "parentId"),
|
||||
];
|
||||
this.groupLayer.children.push(selectLayer);
|
||||
// 插入新组图层
|
||||
this.layerManager.layers.value.splice(topLayerIndex, 1, this.groupLayer);
|
||||
|
||||
this.layerManager.updateLayersObjectsInteractivity();
|
||||
this.canvas.discardActiveObject();
|
||||
this.canvas.setActiveObject(this.fabricImage);
|
||||
this.canvas.renderAll();
|
||||
console.log(`套索抠图完成,新图层ID: ${this.newLayerId}`);
|
||||
return {
|
||||
newLayerId: this.newLayerId,
|
||||
cutoutImageUrl: this.cutoutImageUrl,
|
||||
guroupId: this.groupId,
|
||||
groupName: this.groupName,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("套索抠图过程中出错:", error);
|
||||
@@ -129,32 +183,127 @@ export class LassoCutoutCommand extends CompositeCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// 清理组图层(如果已创建)
|
||||
if (this.groupLayer && this.groupId) {
|
||||
try {
|
||||
const groupIndex = this.layerManager.layers.value.findIndex(
|
||||
(layer) => layer.id === this.groupId
|
||||
);
|
||||
if (groupIndex !== -1) {
|
||||
this.layerManager.layers.value.splice(groupIndex, 1);
|
||||
console.log(`清理了异常创建的组图层: ${this.groupId}`);
|
||||
}
|
||||
this.groupLayer = null;
|
||||
} catch (cleanupError) {
|
||||
console.warn("清理组图层失败:", cleanupError);
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试回滚已执行的命令
|
||||
if (this.executedCommands.length > 0) {
|
||||
try {
|
||||
for (let i = this.executedCommands.length - 1; i >= 0; i--) {
|
||||
const command = this.executedCommands[i];
|
||||
if (command && typeof command.undo === "function") {
|
||||
await command.undo();
|
||||
}
|
||||
}
|
||||
this.executedCommands = [];
|
||||
} catch (rollbackError) {
|
||||
console.warn("回滚已执行命令失败:", rollbackError);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async undo() {
|
||||
try {
|
||||
// 逆序撤销所有已执行的命令
|
||||
for (let i = this.executedCommands.length - 1; i >= 0; i--) {
|
||||
const command = this.executedCommands[i];
|
||||
if (command && typeof command.undo === "function") {
|
||||
await command.undo();
|
||||
console.log(`↩️ 开始撤销套索抠图操作`);
|
||||
|
||||
// 1. 首先移除组图层(如果存在)
|
||||
if (this.groupId) {
|
||||
const groupIndex = this.layerManager.layers.value.findIndex(
|
||||
(layer) => layer.id === this.groupId
|
||||
);
|
||||
|
||||
if (groupIndex !== -1) {
|
||||
console.log(`↩️ 移除组图层: ${this.groupId}`);
|
||||
// 从图层列表中移除组图层
|
||||
this.layerManager.layers.value.splice(groupIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.fabricImage) {
|
||||
console.log(`↩️ 移除抠图图像: ${this.fabricImage.id}`);
|
||||
// 从画布中移除抠图图像
|
||||
this.canvas.remove(this.fabricImage);
|
||||
}
|
||||
|
||||
// 2. 逆序撤销所有已执行的子命令
|
||||
for (let i = this.executedCommands.length - 1; i >= 0; i--) {
|
||||
const command = this.executedCommands[i];
|
||||
if (command && typeof command.undo === "function") {
|
||||
try {
|
||||
console.log(`↩️ 撤销子命令: ${command.constructor.name}`);
|
||||
await command.undo();
|
||||
console.log(`✅ 子命令撤销成功: ${command.constructor.name}`);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`❌ 子命令撤销失败: ${command.constructor.name}`,
|
||||
error
|
||||
);
|
||||
// 子命令撤销失败不中断整个撤销过程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 清理状态
|
||||
this.executedCommands = [];
|
||||
this.newLayerId = null;
|
||||
this.cutoutImageUrl = null;
|
||||
this.fabricImage = null;
|
||||
this.groupLayer = null; // 清理组图层引用
|
||||
// 注意:不重置groupId,因为重做时可能需要使用相同的ID
|
||||
|
||||
// 4. 更新画布和图层交互性
|
||||
await this.layerManager.updateLayersObjectsInteractivity();
|
||||
|
||||
console.log(`✅ 套索抠图撤销完成`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("撤销套索抠图失败:", error);
|
||||
console.error("❌ 撤销套索抠图失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命令信息
|
||||
* @returns {Object} 命令详细信息
|
||||
*/
|
||||
getInfo() {
|
||||
return {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
newLayerId: this.newLayerId,
|
||||
newLayerName: this.newLayerName,
|
||||
groupId: this.groupId,
|
||||
groupName: this.groupName,
|
||||
executedCommandsCount: this.executedCommands.length,
|
||||
hasGroupLayer: !!this.groupLayer,
|
||||
sourceLayerId: this.sourceLayerId,
|
||||
highResolutionEnabled: this.highResolutionEnabled,
|
||||
baseResolutionScale: this.baseResolutionScale,
|
||||
hasSerializedSelection: !!this.serializedSelectionObject,
|
||||
selectionType: this.serializedSelectionObject?.type || null,
|
||||
subCommands: this.executedCommands.map((cmd) => ({
|
||||
name: cmd.constructor.name,
|
||||
info: cmd.getInfo ? cmd.getInfo() : {},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的所有对象(包括子图层,从画布中查找真实对象)
|
||||
* @param {Object} layer 图层对象
|
||||
@@ -249,6 +398,7 @@ export class LassoCutoutCommand extends CompositeCommand {
|
||||
scaleFactor: scaleFactor,
|
||||
// isReturenDataURL: true, // 返回DataURL
|
||||
preserveOriginalQuality: true, // 启用高质量模式
|
||||
selectionManager: this.selectionManager, // 传递选区管理器,用于获取羽化值
|
||||
});
|
||||
|
||||
if (!rasterizedDataURL) {
|
||||
@@ -500,4 +650,129 @@ export class LassoCutoutCommand extends CompositeCommand {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化选区对象
|
||||
* @private
|
||||
*/
|
||||
_serializeSelectionObject() {
|
||||
try {
|
||||
if (!this.selectionManager) {
|
||||
console.warn("选区管理器不存在,无法序列化选区对象");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionObject = this.selectionManager.getSelectionObject();
|
||||
if (!selectionObject) {
|
||||
console.warn("当前没有选区对象,无法序列化");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将选区对象转换为可序列化的对象
|
||||
this.serializedSelectionObject = selectionObject.toObject([
|
||||
"id",
|
||||
"layerId",
|
||||
"layerName",
|
||||
"parentId",
|
||||
]);
|
||||
|
||||
console.log("选区对象已序列化保存");
|
||||
} catch (error) {
|
||||
console.error("序列化选区对象失败:", error);
|
||||
this.serializedSelectionObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化选区对象
|
||||
* @returns {Promise<Object>} 选区对象
|
||||
* @private
|
||||
*/
|
||||
async _getSelectionObject() {
|
||||
try {
|
||||
// 首先尝试从选区管理器获取当前选区
|
||||
const currentSelection = this.selectionManager.getSelectionObject();
|
||||
if (currentSelection) {
|
||||
console.log("从选区管理器获取到当前选区");
|
||||
return currentSelection;
|
||||
}
|
||||
|
||||
// 如果当前没有选区,则从序列化数据恢复
|
||||
if (!this.serializedSelectionObject) {
|
||||
console.error("没有序列化的选区对象数据");
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("从序列化数据恢复选区对象");
|
||||
|
||||
// 根据选区对象类型进行反序列化
|
||||
return new Promise((resolve, reject) => {
|
||||
const objectType = this.serializedSelectionObject.type;
|
||||
|
||||
if (objectType === "path") {
|
||||
// 如果是路径类型(套索选区)
|
||||
fabric.Path.fromObject(this.serializedSelectionObject, (path) => {
|
||||
if (path) {
|
||||
console.log("路径选区对象反序列化成功");
|
||||
resolve(path);
|
||||
} else {
|
||||
reject(new Error("路径选区对象反序列化失败"));
|
||||
}
|
||||
});
|
||||
} else if (objectType === "polygon") {
|
||||
// 如果是多边形类型
|
||||
fabric.Polygon.fromObject(
|
||||
this.serializedSelectionObject,
|
||||
(polygon) => {
|
||||
if (polygon) {
|
||||
console.log("多边形选区对象反序列化成功");
|
||||
resolve(polygon);
|
||||
} else {
|
||||
reject(new Error("多边形选区对象反序列化失败"));
|
||||
}
|
||||
}
|
||||
);
|
||||
} else if (objectType === "rect") {
|
||||
// 如果是矩形选区
|
||||
fabric.Rect.fromObject(this.serializedSelectionObject, (rect) => {
|
||||
if (rect) {
|
||||
console.log("矩形选区对象反序列化成功");
|
||||
resolve(rect);
|
||||
} else {
|
||||
reject(new Error("矩形选区对象反序列化失败"));
|
||||
}
|
||||
});
|
||||
} else if (objectType === "ellipse" || objectType === "circle") {
|
||||
// 如果是椭圆/圆形选区
|
||||
fabric.Ellipse.fromObject(
|
||||
this.serializedSelectionObject,
|
||||
(ellipse) => {
|
||||
if (ellipse) {
|
||||
console.log("椭圆选区对象反序列化成功");
|
||||
resolve(ellipse);
|
||||
} else {
|
||||
reject(new Error("椭圆选区对象反序列化失败"));
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 通用对象反序列化
|
||||
fabric.util.enlivenObjects(
|
||||
[this.serializedSelectionObject],
|
||||
(objects) => {
|
||||
if (objects && objects.length > 0) {
|
||||
console.log("通用选区对象反序列化成功");
|
||||
resolve(objects[0]);
|
||||
} else {
|
||||
reject(new Error("通用选区对象反序列化失败"));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("获取选区对象失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +490,7 @@ export class LiquifyStateCommand extends Command {
|
||||
* @param {String} options.targetLayerId 目标图层ID
|
||||
* @param {ImageData} options.initialImageData 初始图像数据
|
||||
* @param {ImageData} options.finalImageData 最终图像数据
|
||||
* @param {Object} options.liquifyManager 液化管理器实例
|
||||
*/
|
||||
constructor(options) {
|
||||
super({
|
||||
@@ -502,6 +503,7 @@ export class LiquifyStateCommand extends Command {
|
||||
this.targetObject = options.targetObject;
|
||||
this.targetLayerId = options.targetLayerId;
|
||||
this.targetObjectId = options.targetObjectId;
|
||||
this.liquifyManager = options.liquifyManager; // 添加液化管理器引用
|
||||
|
||||
// 获取引用管理器实例
|
||||
this.refManager = getLiquifyReferenceManager();
|
||||
@@ -520,6 +522,10 @@ export class LiquifyStateCommand extends Command {
|
||||
this.initialImageData = options.initialImageData;
|
||||
this.finalImageData = options.finalImageData;
|
||||
|
||||
// 保存液化管理器的操作记录状态
|
||||
this.initialLiquifyState = null;
|
||||
this.finalLiquifyState = null;
|
||||
|
||||
this.currentState = "initial";
|
||||
|
||||
// 创建初始快照
|
||||
@@ -547,6 +553,19 @@ export class LiquifyStateCommand extends Command {
|
||||
this.finalImageData
|
||||
);
|
||||
|
||||
// 恢复液化管理器到最终状态
|
||||
if (this.liquifyManager && this.finalLiquifyState) {
|
||||
this._restoreLiquifyManagerState(this.finalLiquifyState);
|
||||
} else if (this.liquifyManager) {
|
||||
// 如果没有保存的最终状态,重新准备液化环境
|
||||
const currentTarget = this.refManager.getObjectRef(this.objectRefId);
|
||||
if (currentTarget) {
|
||||
await this.liquifyManager.prepareForLiquify(currentTarget);
|
||||
// 保存当前的液化管理器状态作为最终状态
|
||||
this.finalLiquifyState = this._captureLiquifyManagerState();
|
||||
}
|
||||
}
|
||||
|
||||
this.currentState = "final";
|
||||
this.canvas.renderAll();
|
||||
|
||||
@@ -568,6 +587,21 @@ export class LiquifyStateCommand extends Command {
|
||||
this.initialSnapshotId
|
||||
);
|
||||
|
||||
// 恢复液化管理器到初始状态
|
||||
if (this.liquifyManager) {
|
||||
if (this.initialLiquifyState) {
|
||||
this._restoreLiquifyManagerState(this.initialLiquifyState);
|
||||
} else {
|
||||
// 如果没有初始状态,重置液化管理器
|
||||
this.liquifyManager.reset();
|
||||
// 重新准备液化环境
|
||||
const currentTarget = this.refManager.getObjectRef(this.objectRefId);
|
||||
if (currentTarget) {
|
||||
await this.liquifyManager.prepareForLiquify(currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.currentState = "initial";
|
||||
this.canvas.renderAll();
|
||||
|
||||
@@ -589,6 +623,11 @@ export class LiquifyStateCommand extends Command {
|
||||
setFinalImageData(finalImageData) {
|
||||
this.finalImageData = finalImageData;
|
||||
this.finalSnapshotId = null; // 重置快照ID,下次执行时重新创建
|
||||
|
||||
// 捕获当前液化管理器状态作为最终状态
|
||||
if (this.liquifyManager) {
|
||||
this.finalLiquifyState = this._captureLiquifyManagerState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -672,6 +711,12 @@ export class LiquifyStateCommand extends Command {
|
||||
};
|
||||
|
||||
this.refManager.stateSnapshots.set(this.initialSnapshotId, snapshot);
|
||||
|
||||
// 捕获初始液化管理器状态
|
||||
if (this.liquifyManager) {
|
||||
this.initialLiquifyState = this._captureLiquifyManagerState();
|
||||
}
|
||||
|
||||
console.log(`📸 初始状态快照已创建: ${this.initialSnapshotId}`);
|
||||
}
|
||||
}
|
||||
@@ -697,27 +742,180 @@ export class LiquifyStateCommand extends Command {
|
||||
};
|
||||
|
||||
this.refManager.stateSnapshots.set(this.finalSnapshotId, snapshot);
|
||||
|
||||
// 捕获最终液化管理器状态
|
||||
if (this.liquifyManager) {
|
||||
this.finalLiquifyState = this._captureLiquifyManagerState();
|
||||
}
|
||||
|
||||
console.log(`📸 最终状态快照已创建: ${this.finalSnapshotId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算命令本身的内存使用量
|
||||
* @returns {Number} 内存使用量(字节)
|
||||
* 捕获液化管理器的当前状态
|
||||
* @returns {Object} 液化管理器状态
|
||||
* @private
|
||||
*/
|
||||
_calculateCommandMemory() {
|
||||
let bytes = 0;
|
||||
_captureLiquifyManagerState() {
|
||||
if (!this.liquifyManager) return null;
|
||||
|
||||
// 计算ImageData内存使用
|
||||
if (this.initialImageData) {
|
||||
bytes += this.initialImageData.width * this.initialImageData.height * 4;
|
||||
}
|
||||
if (this.finalImageData) {
|
||||
bytes += this.finalImageData.width * this.finalImageData.height * 4;
|
||||
}
|
||||
try {
|
||||
const state = {
|
||||
// 捕获增强管理器状态
|
||||
enhancedManagerState: null,
|
||||
// 捕获当前渲染器状态
|
||||
activeRendererState: null,
|
||||
// 捕获目标对象引用
|
||||
targetObjectRef: this.liquifyManager.targetObject,
|
||||
// 捕获初始化状态
|
||||
initialized: this.liquifyManager.initialized || false,
|
||||
};
|
||||
|
||||
return bytes;
|
||||
// 如果有增强管理器,捕获其状态
|
||||
if (this.liquifyManager.enhancedManager) {
|
||||
const enhancedManager = this.liquifyManager.enhancedManager;
|
||||
state.enhancedManagerState = {
|
||||
initialized: enhancedManager.initialized,
|
||||
renderMode: enhancedManager.renderMode,
|
||||
targetObject: enhancedManager.targetObject,
|
||||
originalImageData: enhancedManager.originalImageData,
|
||||
currentImageData: enhancedManager.currentImageData,
|
||||
params: { ...enhancedManager.params },
|
||||
currentMode: enhancedManager.currentMode,
|
||||
};
|
||||
|
||||
// 如果有激活的渲染器,捕获其状态
|
||||
if (enhancedManager.activeRenderer) {
|
||||
const renderer = enhancedManager.activeRenderer;
|
||||
state.activeRendererState = {
|
||||
initialized: renderer.initialized,
|
||||
originalImageData: renderer.originalImageData,
|
||||
currentImageData: renderer.currentImageData,
|
||||
params: { ...renderer.params },
|
||||
currentMode: renderer.currentMode,
|
||||
// 对于CPU渲染器,还需要保存网格状态
|
||||
meshState: renderer.mesh
|
||||
? this._captureMeshState(renderer.mesh)
|
||||
: null,
|
||||
// 保存变形历史
|
||||
deformHistory: renderer.deformHistory
|
||||
? [...renderer.deformHistory]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`💾 液化管理器状态已捕获:`, state);
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.error("捕获液化管理器状态失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复液化管理器状态
|
||||
* @param {Object} state 要恢复的状态
|
||||
* @private
|
||||
*/
|
||||
_restoreLiquifyManagerState(state) {
|
||||
if (!this.liquifyManager || !state) return;
|
||||
|
||||
try {
|
||||
// 恢复基本状态
|
||||
this.liquifyManager.initialized = state.initialized;
|
||||
if (state.targetObjectRef) {
|
||||
this.liquifyManager.targetObject = state.targetObjectRef;
|
||||
}
|
||||
|
||||
// 恢复增强管理器状态
|
||||
if (state.enhancedManagerState && this.liquifyManager.enhancedManager) {
|
||||
const enhancedManager = this.liquifyManager.enhancedManager;
|
||||
const enhancedState = state.enhancedManagerState;
|
||||
|
||||
enhancedManager.initialized = enhancedState.initialized;
|
||||
enhancedManager.renderMode = enhancedState.renderMode;
|
||||
enhancedManager.targetObject = enhancedState.targetObject;
|
||||
enhancedManager.originalImageData = enhancedState.originalImageData;
|
||||
enhancedManager.currentImageData = enhancedState.currentImageData;
|
||||
enhancedManager.params = { ...enhancedState.params };
|
||||
enhancedManager.currentMode = enhancedState.currentMode;
|
||||
|
||||
// 恢复激活渲染器状态
|
||||
if (state.activeRendererState && enhancedManager.activeRenderer) {
|
||||
const renderer = enhancedManager.activeRenderer;
|
||||
const rendererState = state.activeRendererState;
|
||||
|
||||
renderer.initialized = rendererState.initialized;
|
||||
renderer.originalImageData = rendererState.originalImageData;
|
||||
renderer.currentImageData = rendererState.currentImageData;
|
||||
renderer.params = { ...rendererState.params };
|
||||
renderer.currentMode = rendererState.currentMode;
|
||||
|
||||
// 恢复网格状态(如果是CPU渲染器)
|
||||
if (rendererState.meshState && renderer.mesh) {
|
||||
this._restoreMeshState(renderer.mesh, rendererState.meshState);
|
||||
}
|
||||
|
||||
// 恢复变形历史
|
||||
if (rendererState.deformHistory) {
|
||||
renderer.deformHistory = [...rendererState.deformHistory];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔄 液化管理器状态已恢复`);
|
||||
} catch (error) {
|
||||
console.error("恢复液化管理器状态失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获网格状态
|
||||
* @param {Array} mesh 网格数组
|
||||
* @returns {Array} 网格状态副本
|
||||
* @private
|
||||
*/
|
||||
_captureMeshState(mesh) {
|
||||
if (!mesh || !Array.isArray(mesh)) return null;
|
||||
|
||||
return mesh.map((row) =>
|
||||
row.map((point) => ({
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
originalX: point.originalX,
|
||||
originalY: point.originalY,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复网格状态
|
||||
* @param {Array} mesh 目标网格
|
||||
* @param {Array} meshState 要恢复的网格状态
|
||||
* @private
|
||||
*/
|
||||
_restoreMeshState(mesh, meshState) {
|
||||
if (
|
||||
!mesh ||
|
||||
!meshState ||
|
||||
!Array.isArray(mesh) ||
|
||||
!Array.isArray(meshState)
|
||||
)
|
||||
return;
|
||||
|
||||
for (let i = 0; i < Math.min(mesh.length, meshState.length); i++) {
|
||||
for (let j = 0; j < Math.min(mesh[i].length, meshState[i].length); j++) {
|
||||
const point = mesh[i][j];
|
||||
const statePoint = meshState[i][j];
|
||||
|
||||
point.x = statePoint.x;
|
||||
point.y = statePoint.y;
|
||||
point.originalX = statePoint.originalX;
|
||||
point.originalY = statePoint.originalY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -257,6 +257,12 @@ function setBrushOpacity(opacity) {
|
||||
// 如果工具管理器存在,立即应用此更改
|
||||
if (toolManager) {
|
||||
toolManager.updateBrushOpacity(opacity);
|
||||
|
||||
// 同时更新颜色以确保透明度生效
|
||||
const currentColor = BrushStore.state.color;
|
||||
if (currentColor) {
|
||||
toolManager.updateBrushColor(currentColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -345,7 +345,224 @@
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- 阴影设置 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
<span>{{ $t("阴影设置") }}</span>
|
||||
</div>
|
||||
|
||||
<div class="property-list">
|
||||
<!-- 阴影开关 -->
|
||||
<div class="property-item">
|
||||
<div class="checkbox-property">
|
||||
<span>{{ $t("启用阴影") }}</span>
|
||||
<div class="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="brushStore.state.shadowEnabled"
|
||||
@change="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowEnabled',
|
||||
e.target.checked
|
||||
)
|
||||
"
|
||||
id="shadow-enabled"
|
||||
/>
|
||||
<label for="shadow-enabled"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影控制组 - 仅在启用阴影时显示 -->
|
||||
<template v-if="brushStore.state.shadowEnabled">
|
||||
<!-- 阴影颜色 -->
|
||||
<div class="property-item">
|
||||
<div class="color-property">
|
||||
<div class="color-header">
|
||||
<span>{{ $t("阴影颜色") }}</span>
|
||||
<div
|
||||
class="color-preview"
|
||||
:style="{ backgroundColor: brushStore.state.shadowColor }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="color-row">
|
||||
<input
|
||||
type="color"
|
||||
:value="brushStore.state.shadowColor"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowColor',
|
||||
e.target.value
|
||||
)
|
||||
"
|
||||
class="color-picker"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影宽度 -->
|
||||
<div class="property-item">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ $t("阴影宽度") }}</span>
|
||||
<span class="slider-value"
|
||||
>{{ brushStore.state.shadowWidth }}px</span
|
||||
>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="brushStore.state.shadowWidth"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowWidth',
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:step="1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<div class="property-presets">
|
||||
<button
|
||||
v-for="preset in [0, 5, 10, 15, 20]"
|
||||
:key="preset"
|
||||
@click="handleShadowPropertyChange('shadowWidth', preset)"
|
||||
:class="{
|
||||
active:
|
||||
Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
|
||||
}"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影X偏移 -->
|
||||
<div class="property-item">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ $t("阴影X偏移") }}</span>
|
||||
<span class="slider-value"
|
||||
>{{ brushStore.state.shadowOffsetX }}px</span
|
||||
>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="brushStore.state.shadowOffsetX"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowOffsetX',
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="-50"
|
||||
:max="50"
|
||||
:step="1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<div class="property-presets">
|
||||
<button
|
||||
v-for="preset in [-10, -5, 0, 5, 10]"
|
||||
:key="preset"
|
||||
@click="
|
||||
handleShadowPropertyChange('shadowOffsetX', preset)
|
||||
"
|
||||
:class="{
|
||||
active:
|
||||
Math.abs(brushStore.state.shadowOffsetX - preset) <
|
||||
0.1,
|
||||
}"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影Y偏移 -->
|
||||
<div class="property-item">
|
||||
<div class="slider-property">
|
||||
<div class="slider-header">
|
||||
<span>{{ $t("阴影Y偏移") }}</span>
|
||||
<span class="slider-value"
|
||||
>{{ brushStore.state.shadowOffsetY }}px</span
|
||||
>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="brushStore.state.shadowOffsetY"
|
||||
@input="
|
||||
(e) =>
|
||||
handleShadowPropertyChange(
|
||||
'shadowOffsetY',
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
"
|
||||
:min="-50"
|
||||
:max="50"
|
||||
:step="1"
|
||||
class="property-slider"
|
||||
/>
|
||||
</div>
|
||||
<div class="property-presets">
|
||||
<button
|
||||
v-for="preset in [-10, -5, 0, 5, 10]"
|
||||
:key="preset"
|
||||
@click="
|
||||
handleShadowPropertyChange('shadowOffsetY', preset)
|
||||
"
|
||||
:class="{
|
||||
active:
|
||||
Math.abs(brushStore.state.shadowOffsetY - preset) <
|
||||
0.1,
|
||||
}"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阴影预览 -->
|
||||
<div class="property-item">
|
||||
<div class="shadow-preview-container">
|
||||
<div class="shadow-preview-title">{{ $t("阴影预览") }}</div>
|
||||
<div class="shadow-preview-box">
|
||||
<div
|
||||
class="shadow-preview-element"
|
||||
:style="{
|
||||
backgroundColor: brushStore.state.color,
|
||||
width: `${Math.max(
|
||||
20,
|
||||
Math.min(60, brushStore.state.size)
|
||||
)}px`,
|
||||
height: `${Math.max(
|
||||
20,
|
||||
Math.min(60, brushStore.state.size)
|
||||
)}px`,
|
||||
boxShadow: brushStore.state.shadowEnabled
|
||||
? `${brushStore.state.shadowOffsetX}px ${brushStore.state.shadowOffsetY}px ${brushStore.state.shadowWidth}px ${brushStore.state.shadowColor}`
|
||||
: 'none',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 笔刷预设 -->
|
||||
<div class="brush-section">
|
||||
<div class="section-header">
|
||||
@@ -625,6 +842,14 @@ const debouncedPropertyCommand = debounce((propId, value) => {
|
||||
commandManager.execute(command, { nonUndoable: true });
|
||||
}, 200);
|
||||
|
||||
// 处理阴影属性变化的防抖函数
|
||||
const debouncedShadowCommand = debounce((propId, value) => {
|
||||
// 通知工具管理器更新阴影
|
||||
if (toolManager?.brushManager) {
|
||||
toolManager.brushManager.updateShadow();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 处理属性变化
|
||||
function handlePropertyChange(propId, value) {
|
||||
// 直接更新UI,通过防抖函数延迟创建命令
|
||||
@@ -632,6 +857,25 @@ function handlePropertyChange(propId, value) {
|
||||
debouncedPropertyCommand(propId, value);
|
||||
}
|
||||
|
||||
// 处理阴影属性变化
|
||||
function handleShadowPropertyChange(propId, value) {
|
||||
// 更新BrushStore中的阴影属性
|
||||
if (propId === "shadowEnabled") {
|
||||
BrushStore.setShadowEnabled(value);
|
||||
} else if (propId === "shadowColor") {
|
||||
BrushStore.setShadowColor(value);
|
||||
} else if (propId === "shadowWidth") {
|
||||
BrushStore.setShadowWidth(value);
|
||||
} else if (propId === "shadowOffsetX") {
|
||||
BrushStore.setShadowOffsetX(value);
|
||||
} else if (propId === "shadowOffsetY") {
|
||||
BrushStore.setShadowOffsetY(value);
|
||||
}
|
||||
|
||||
// 通知笔刷管理器更新阴影设置
|
||||
debouncedShadowCommand(propId, value);
|
||||
}
|
||||
|
||||
// 使用命令设置笔刷类型
|
||||
function setBrushTypeWithCommand(type) {
|
||||
const command = new BrushTypeCommand({
|
||||
@@ -1761,4 +2005,65 @@ const brushStore = BrushStore;
|
||||
background-color: rgba(244, 67, 54, 1);
|
||||
transform: scale(1.2) !important;
|
||||
}
|
||||
|
||||
/* 阴影设置样式 */
|
||||
.shadow-preview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shadow-preview-title {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shadow-preview-box {
|
||||
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
|
||||
10px 10px;
|
||||
border-radius: 8px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shadow-preview-element {
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.brush-panel {
|
||||
width: 95%;
|
||||
right: 2.5%;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.shadow-preview-box {
|
||||
min-height: 100px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.property-presets {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.brush-type-grid,
|
||||
.presets-container,
|
||||
.texture-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -332,6 +332,7 @@ async function prepareForLiquify(targetObj) {
|
||||
targetObject: targetObject.value,
|
||||
targetLayerId: targetLayerId.value,
|
||||
originalData: originalImageData.value,
|
||||
liquifyManager: props.liquifyManager,
|
||||
layerManager: props.layerManager,
|
||||
});
|
||||
|
||||
@@ -483,6 +484,7 @@ function showPanel(event) {
|
||||
targetLayerId: targetLayerId.value,
|
||||
originalData: originalImageData.value,
|
||||
layerManager: props.layerManager,
|
||||
liquifyManager: props.liquifyManager,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -739,10 +741,59 @@ function removeCanvasListeners() {
|
||||
_handleMouseUp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前图像的实际状态数据
|
||||
* @param {Object} targetObject Fabric图像对象
|
||||
* @returns {Promise<ImageData|null>} 当前图像数据或null
|
||||
*/
|
||||
async function getCurrentImageData(targetObject) {
|
||||
if (!targetObject || !targetObject._element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建临时canvas来获取当前图像状态
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const element = targetObject._element;
|
||||
|
||||
// 设置canvas尺寸为原始图像尺寸
|
||||
if (originalImageData.value) {
|
||||
tempCanvas.width = originalImageData.value.width;
|
||||
tempCanvas.height = originalImageData.value.height;
|
||||
} else {
|
||||
tempCanvas.width = element.naturalWidth || element.width;
|
||||
tempCanvas.height = element.naturalHeight || element.height;
|
||||
}
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 绘制当前图像到临时canvas
|
||||
tempCtx.drawImage(element, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
|
||||
// 获取ImageData
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
"✅ 成功获取当前图像状态,尺寸:",
|
||||
imageData.width,
|
||||
"x",
|
||||
imageData.height
|
||||
);
|
||||
return imageData;
|
||||
} catch (error) {
|
||||
console.warn("获取当前图像数据失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标按下事件处理
|
||||
*/
|
||||
function handleMouseDown(event) {
|
||||
async function handleMouseDown(event) {
|
||||
if (!isEditing.value || !visible.value || !props.liquifyManager) return;
|
||||
|
||||
isDrawing.value = true;
|
||||
@@ -760,29 +811,43 @@ function handleMouseDown(event) {
|
||||
lastX.value = pointer.x;
|
||||
lastY.value = pointer.y;
|
||||
|
||||
// === 修复:记录初始图像数据 ===
|
||||
// === 修复:记录当前图像状态(而不是原始状态)===
|
||||
try {
|
||||
const currentTarget = getCurrentTargetObject();
|
||||
if (currentTarget && originalImageData.value) {
|
||||
console.log("🎯 记录液化操作初始状态,对象ID:", targetObjectId.value);
|
||||
if (currentTarget) {
|
||||
console.log("🎯 记录液化操作当前状态,对象ID:", targetObjectId.value);
|
||||
|
||||
// 记录初始图像数据(深拷贝)
|
||||
const originalData = originalImageData.value;
|
||||
initialImageData.value = new ImageData(
|
||||
new Uint8ClampedArray(originalData.data),
|
||||
originalData.width,
|
||||
originalData.height
|
||||
);
|
||||
// 获取当前图像的实际状态(而不是原始状态)
|
||||
const currentImageData = await getCurrentImageData(currentTarget);
|
||||
if (currentImageData) {
|
||||
// 记录当前图像数据(深拷贝)
|
||||
initialImageData.value = new ImageData(
|
||||
new Uint8ClampedArray(currentImageData.data),
|
||||
currentImageData.width,
|
||||
currentImageData.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
"✅ 当前图像状态已记录,尺寸:",
|
||||
initialImageData.value.width,
|
||||
"x",
|
||||
initialImageData.value.height
|
||||
);
|
||||
} else {
|
||||
// 如果无法获取当前状态,使用原始状态作为备用
|
||||
if (originalImageData.value) {
|
||||
const originalData = originalImageData.value;
|
||||
initialImageData.value = new ImageData(
|
||||
new Uint8ClampedArray(originalData.data),
|
||||
originalData.width,
|
||||
originalData.height
|
||||
);
|
||||
console.log("⚠️ 使用原始状态作为备用初始状态");
|
||||
}
|
||||
}
|
||||
|
||||
// 备用:也保存序列化状态
|
||||
initialObjectState.value = serializeFabricObject(currentTarget);
|
||||
|
||||
console.log(
|
||||
"✅ 初始图像数据已记录,尺寸:",
|
||||
initialImageData.value.width,
|
||||
"x",
|
||||
initialImageData.value.height
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 记录初始状态失败:", error);
|
||||
@@ -965,6 +1030,7 @@ async function handleMouseUp() {
|
||||
currentLiquifyCommand.value = createLiquifyStateCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
liquifyManager: props.liquifyManager,
|
||||
targetObject: currentTarget,
|
||||
targetLayerId: targetLayerId.value,
|
||||
targetObjectId: targetObjectId.value,
|
||||
@@ -1646,7 +1712,7 @@ function stopPressTimer() {
|
||||
/* 平板适配:最多6列 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.liquify-modes {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
@@ -1663,7 +1729,7 @@ function stopPressTimer() {
|
||||
/* 手机适配:最多4列 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.liquify-modes {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- 顶部选区类型工具栏 -->
|
||||
<div class="toolbar-section">
|
||||
<div class="toolbar-header">
|
||||
<div class="header-title">选区工具</div>
|
||||
<div class="header-title">{{ t("选区工具") }}</div>
|
||||
<!-- 移除关闭按钮,完全通过工具切换控制显示隐藏 -->
|
||||
</div>
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
CreateSelectionCommand,
|
||||
InvertSelectionCommand,
|
||||
@@ -232,8 +233,8 @@ const hasSelection = ref(false);
|
||||
const showFeatherDialog = ref(false);
|
||||
const showColorPicker = ref(false);
|
||||
|
||||
// 国际化函数 (简单实现,可根据需要替换为实际的国际化方案)
|
||||
const $t = (key) => key;
|
||||
// 国际化
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
// 为选区管理器添加监听,以便在选区变化时更新状态
|
||||
@@ -647,7 +648,7 @@ function confirmColorPicker() {
|
||||
|
||||
.tool-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { LiquifyManager } from "./managers/liquify/LiquifyManager";
|
||||
import { SelectionManager } from "./managers/selection/SelectionManager";
|
||||
import { RedGreenModeManager } from "./managers/RedGreenModeManager";
|
||||
import texturePresetManager from "./managers/brushes/TexturePresetManager";
|
||||
import { BrushStore } from "./store/BrushStore";
|
||||
|
||||
// import { MinimapManager } from "./managers/minimap/MinimapManager";
|
||||
|
||||
@@ -185,6 +186,11 @@ function toggleMinimap(enabled) {
|
||||
|
||||
// 初始化画布
|
||||
onMounted(async () => {
|
||||
// 设置BrushStore的全局引用,供BaseBrush使用
|
||||
if (typeof window !== "undefined") {
|
||||
window.BrushStore = BrushStore;
|
||||
}
|
||||
|
||||
// 如果启用了红绿图模式,设置画布大小为默认值
|
||||
if (props.enabledRedGreenMode) {
|
||||
canvasHeight.value = canvasContainerRef.value.clientWidth;
|
||||
@@ -218,7 +224,7 @@ onMounted(async () => {
|
||||
canvas: canvasManager.canvas,
|
||||
canvasWidth: canvasWidth.value,
|
||||
canvasHeight: canvasHeight.value,
|
||||
backgroundColor: canvasColor.value,
|
||||
backgroundColor: canvasColor,
|
||||
isRedGreenMode: props.enabledRedGreenMode,
|
||||
layers,
|
||||
activeLayerId,
|
||||
@@ -744,17 +750,17 @@ defineExpose({
|
||||
//图片url或者base64
|
||||
addImageToLayer: async (
|
||||
url,
|
||||
{ layerId, undoable } = { layerId: null, undoable: true } // 可选参数 layerId 指定图层 将内容添加到指定图层 undoable 是否可撤销 false不可撤销 默认可撤销
|
||||
{ layerId, undoable, ...optios } = { layerId: null, undoable: true } // 可选参数 layerId 指定图层 将内容添加到指定图层 undoable 是否可撤销 false不可撤销 默认可撤销
|
||||
) => {
|
||||
if (!url) return Promise.reject(new Error("图片URL不能为空"));
|
||||
|
||||
if (layerId) {
|
||||
const fabricImage = await loadImage(url);
|
||||
// 如果指定了图层ID,确保图层存在
|
||||
return await canvasManager?.addImageToLayer?.(url, {
|
||||
return await canvasManager?.addImageToLayer?.({
|
||||
targetLayerId: layerId,
|
||||
fabricImage,
|
||||
undoable, // 是否可撤销操作
|
||||
...optios,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -766,7 +772,7 @@ defineExpose({
|
||||
canvas: canvasManager.canvas,
|
||||
toolManager,
|
||||
},
|
||||
{ undoable }
|
||||
{ undoable, ...optios }
|
||||
);
|
||||
},
|
||||
// 导出图片
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
simplifyLayers,
|
||||
validateLayerAssociations,
|
||||
} from "../utils/layerUtils";
|
||||
import { imageModeHandler } from "../utils/imageHelper";
|
||||
|
||||
export class CanvasManager {
|
||||
constructor(canvasElement, options) {
|
||||
@@ -107,6 +108,23 @@ export class CanvasManager {
|
||||
id: fabricImage.id || generateId("brush_img_"),
|
||||
});
|
||||
|
||||
if (options.imageMode) {
|
||||
imageModeHandler({
|
||||
imageMode: options.imageMode,
|
||||
newImage: fabricImage,
|
||||
canvasWidth: this.canvasWidth.value,
|
||||
canvasHeight: this.canvasHeight.value,
|
||||
});
|
||||
|
||||
// 默认居中
|
||||
fabricImage.set({
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
left: this.canvas.width / 2,
|
||||
top: this.canvas.height / 2,
|
||||
});
|
||||
}
|
||||
|
||||
// 执行高保真合并操作
|
||||
await this.eventManager?.mergeLayerObjectsForPerformance?.({
|
||||
fabricImage,
|
||||
|
||||
@@ -103,7 +103,7 @@ export class LayerManager {
|
||||
this.canvasHeight = options.canvasHeight || 600;
|
||||
|
||||
// 默认背景颜色
|
||||
this.backgroundColor = options.backgroundColor || "#ffffff";
|
||||
this.backgroundColor = options.backgroundColor || { value: "#ffffff" };
|
||||
|
||||
// 复制粘贴相关
|
||||
this.clipboardData = null;
|
||||
@@ -428,7 +428,7 @@ export class LayerManager {
|
||||
name: name,
|
||||
canvasWidth: this.canvasWidth,
|
||||
canvasHeight: this.canvasHeight,
|
||||
backgroundColor: this.backgroundColor,
|
||||
backgroundColor: this.backgroundColor.value,
|
||||
});
|
||||
|
||||
// 直接创建和执行命令
|
||||
@@ -1388,7 +1388,7 @@ export class LayerManager {
|
||||
activeLayerId: this.activeLayerId.value,
|
||||
canvasWidth: this.canvasWidth,
|
||||
canvasHeight: this.canvasHeight,
|
||||
backgroundColor: this.backgroundColor,
|
||||
backgroundColor: this.backgroundColor.value,
|
||||
editorMode: this.editorMode,
|
||||
};
|
||||
}
|
||||
@@ -1724,7 +1724,6 @@ export class LayerManager {
|
||||
* @param {string} backgroundColor 背景颜色
|
||||
*/
|
||||
updateBackgroundColor(backgroundColor) {
|
||||
// 查找背景图层
|
||||
const backgroundLayer = this.layers.value.find(
|
||||
(layer) => layer.isBackground
|
||||
);
|
||||
@@ -1739,7 +1738,8 @@ export class LayerManager {
|
||||
canvas: this.canvas,
|
||||
layers: this.layers,
|
||||
canvasManager: this.canvasManager,
|
||||
backgroundColor: backgroundColor,
|
||||
backgroundColor,
|
||||
backgroundColorValue: this.backgroundColor,
|
||||
});
|
||||
|
||||
// 执行命令
|
||||
@@ -1748,9 +1748,6 @@ export class LayerManager {
|
||||
} else {
|
||||
command.execute();
|
||||
}
|
||||
|
||||
// 更新存储的背景颜色
|
||||
this.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,25 +25,27 @@ export class ThumbnailManager {
|
||||
|
||||
// 延迟执行,避免阻塞UI
|
||||
fabricObjects.length > 0 &&
|
||||
requestAnimationFrame(async () => {
|
||||
const base64 = await this._generateLayerThumbnailNow(fabricObjects);
|
||||
this.layerThumbnails.set(layerId, base64);
|
||||
try {
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId
|
||||
);
|
||||
if (layer) {
|
||||
layer.thumbnailUrl = base64; // 更新图层对象的缩略图
|
||||
}
|
||||
requestIdleCallback(() => {
|
||||
setTimeout(async () => {
|
||||
const base64 = await this._generateLayerThumbnailNow(fabricObjects);
|
||||
this.layerThumbnails.set(layerId, base64);
|
||||
try {
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId
|
||||
);
|
||||
if (layer) {
|
||||
layer.thumbnailUrl = base64; // 更新图层对象的缩略图
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
// 如果是组图层,则同步更新父图层的缩略图
|
||||
this.generateLayerThumbnail(parent.id);
|
||||
if (parent) {
|
||||
// 如果是组图层,则同步更新父图层的缩略图
|
||||
this.generateLayerThumbnail(parent.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成图层缩略图时出错:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成图层缩略图时出错:", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,14 +56,17 @@ export class ThumbnailManager {
|
||||
generateAllLayerThumbnails(layers) {
|
||||
if (!layers || !Array.isArray(layers)) return;
|
||||
|
||||
// 使用requestAnimationFrame批量生成,避免阻塞主线程
|
||||
layers.forEach((layer) => {
|
||||
if (layer && layer.id) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
if (layer.children && layer.children.length) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
}
|
||||
}
|
||||
requestIdleCallback(() => {
|
||||
setTimeout(() => {
|
||||
layers.forEach((layer) => {
|
||||
if (layer && layer.id) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
if (layer.children && layer.children.length) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,17 +76,22 @@ export class ThumbnailManager {
|
||||
console.warn("⚠️ 没有对象需要生成缩略图,返回默认缩略图");
|
||||
return this.defaultThumbnail;
|
||||
}
|
||||
return await createRasterizedImage({
|
||||
canvas: this.canvas, // 画布对象 必填
|
||||
fabricObjects, // 要栅格化的对象列表 - 按顺序 必填
|
||||
// maskObject = null, // 用于裁剪的对象 - 可选 // TODO: 后期看是否需要裁剪
|
||||
trimWhitespace: true, // 是否裁剪空白区域
|
||||
trimPadding: 2, // 裁剪边距
|
||||
quality: 0.8, // 图像质量
|
||||
format: "png", // 图像格式
|
||||
scaleFactor: 1, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL: true, // 是否返回DataURL而不是fabric.Image对象
|
||||
});
|
||||
try {
|
||||
return await createRasterizedImage({
|
||||
canvas: this.canvas, // 画布对象 必填
|
||||
fabricObjects, // 要栅格化的对象列表 - 按顺序 必填
|
||||
// maskObject = null, // 用于裁剪的对象 - 可选 // TODO: 后期看是否需要裁剪
|
||||
trimWhitespace: true, // 是否裁剪空白区域
|
||||
trimPadding: 2, // 裁剪边距
|
||||
quality: 0.2, // 图像质量
|
||||
format: "png", // 图像格式
|
||||
scaleFactor: 0.2, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL: true, // 是否返回DataURL而不是fabric.Image对象
|
||||
isThumbnail: true, // 为缩略图
|
||||
});
|
||||
} catch (error) {
|
||||
return this.defaultThumbnail; // 出错时返回默认缩略图
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,8 +36,137 @@ export class BaseBrush {
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configure(brush, options) {
|
||||
throw new Error("必须由子类实现configure方法");
|
||||
configure(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 基础属性配置
|
||||
if (options.width !== undefined) {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined && options.opacity !== undefined) {
|
||||
// 使用RGBA颜色而不是设置globalAlpha
|
||||
brush.color = this._createRGBAColor(options.color, options.opacity);
|
||||
brush.opacity = 1; // 保持fabric层面的opacity为1
|
||||
} else if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
} else if (options.opacity !== undefined) {
|
||||
// 如果只设置了透明度,基于当前颜色创建RGBA
|
||||
const currentColor = brush.color || this.options.color || "#000000";
|
||||
brush.color = this._createRGBAColor(currentColor, options.opacity);
|
||||
brush.opacity = 1;
|
||||
}
|
||||
|
||||
// 配置阴影
|
||||
this.configureShadow(brush, options);
|
||||
|
||||
// 确保不使用globalAlpha,避免圆圈绘制问题
|
||||
if (brush.canvas && brush.canvas.contextTop) {
|
||||
brush.canvas.contextTop.globalAlpha = 1;
|
||||
brush.canvas.contextTop.lineCap = "round";
|
||||
brush.canvas.contextTop.lineJoin = "round";
|
||||
brush.canvas.contextTop.globalCompositeOperation = "source-over";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置笔刷阴影
|
||||
* @param {Object} brush fabric笔刷实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
configureShadow(brush, options = {}) {
|
||||
if (!brush) return;
|
||||
|
||||
// 简化的阴影配置获取方法
|
||||
let shadowConfig = null;
|
||||
|
||||
// 尝试从全局获取BrushStore(在Vue组件中已经导入)
|
||||
if (typeof window !== "undefined" && window.BrushStore) {
|
||||
shadowConfig = window.BrushStore.getShadowConfig();
|
||||
} else {
|
||||
// 如果没有全局BrushStore,尝试从选项中获取
|
||||
if (options.shadowEnabled) {
|
||||
shadowConfig = {
|
||||
color: options.shadowColor || "#000000",
|
||||
blur: options.shadowWidth || 0,
|
||||
offsetX: options.shadowOffsetX || 0,
|
||||
offsetY: options.shadowOffsetY || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (shadowConfig) {
|
||||
// 创建fabric.Shadow实例
|
||||
if (typeof fabric !== "undefined" && fabric.Shadow) {
|
||||
brush.shadow = new fabric.Shadow(shadowConfig);
|
||||
}
|
||||
} else {
|
||||
// 清除阴影
|
||||
brush.shadow = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷阴影设置
|
||||
*/
|
||||
updateShadow() {
|
||||
if (this.brush) {
|
||||
this.configureShadow(this.brush);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建RGBA颜色字符串
|
||||
* @private
|
||||
* @param {String} color 十六进制颜色或已有颜色
|
||||
* @param {Number} opacity 透明度 (0-1)
|
||||
* @returns {String} RGBA颜色字符串
|
||||
*/
|
||||
_createRGBAColor(color, opacity) {
|
||||
// 如果已经是rgba颜色,先提取RGB部分
|
||||
if (color.startsWith("rgba")) {
|
||||
const rgbaMatch = color.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/
|
||||
);
|
||||
if (rgbaMatch) {
|
||||
const [, r, g, b] = rgbaMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是rgb颜色,提取RGB部分
|
||||
if (color.startsWith("rgb")) {
|
||||
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理十六进制颜色
|
||||
if (color.startsWith("#")) {
|
||||
const hex = color.replace("#", "");
|
||||
let r, g, b;
|
||||
|
||||
if (hex.length === 3) {
|
||||
r = parseInt(hex[0] + hex[0], 16);
|
||||
g = parseInt(hex[1] + hex[1], 16);
|
||||
b = parseInt(hex[2] + hex[2], 16);
|
||||
} else if (hex.length === 6) {
|
||||
r = parseInt(hex.substring(0, 2), 16);
|
||||
g = parseInt(hex.substring(2, 4), 16);
|
||||
b = parseInt(hex.substring(4, 6), 16);
|
||||
} else {
|
||||
// 无效的十六进制颜色,使用默认
|
||||
r = g = b = 0;
|
||||
}
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
|
||||
// 如果是其他格式的颜色,尝试转换(例如颜色名称)
|
||||
// 这里简化处理,实际项目中可以使用更复杂的颜色解析
|
||||
return color; // fallback到原颜色
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,23 +274,26 @@ export class BaseBrush {
|
||||
* @returns {Boolean} 是否更新成功
|
||||
*/
|
||||
updateProperty(propId, value) {
|
||||
// 基础实现,可以被子类覆盖以处理特殊属性
|
||||
if (propId === "size") {
|
||||
if (this.brush) {
|
||||
this.brush.width = value;
|
||||
return true;
|
||||
this.configure(this.brush, { width: value });
|
||||
}
|
||||
return true;
|
||||
} else if (propId === "color") {
|
||||
if (this.brush) {
|
||||
this.brush.color = value;
|
||||
return true;
|
||||
this.configure(this.brush, { color: value });
|
||||
}
|
||||
return true;
|
||||
} else if (propId === "opacity") {
|
||||
if (this.brush) {
|
||||
this.brush.opacity = value;
|
||||
return true;
|
||||
this.configure(this.brush, { opacity: value });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ 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";
|
||||
// import { SpraypaintBrush } from "./types/SpraypaintBrush";
|
||||
|
||||
/**
|
||||
* 笔刷管理器
|
||||
@@ -116,53 +116,53 @@ export class BrushManager {
|
||||
category: "基础笔刷",
|
||||
});
|
||||
// brushRegistry.register("sketchy", SketchyBrush);
|
||||
brushRegistry.register("spraypaint", SpraypaintBrush, {
|
||||
name: "Spraypaint",
|
||||
description: "喷漆笔刷,模拟喷漆效果",
|
||||
category: "基础笔刷",
|
||||
});
|
||||
// brushRegistry.register("spraypaint", SpraypaintBrush, {
|
||||
// name: "Spraypaint",
|
||||
// description: "喷漆笔刷,模拟喷漆效果",
|
||||
// category: "基础笔刷",
|
||||
// });
|
||||
|
||||
// // 注册喷枪笔刷
|
||||
// brushRegistry.register(
|
||||
// "spray",
|
||||
// class SprayBrush extends PencilBrush {
|
||||
// constructor(canvas, options = {}) {
|
||||
// super(canvas, {
|
||||
// id: "spray",
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// category: "基础笔刷",
|
||||
// ...options,
|
||||
// });
|
||||
// }
|
||||
brushRegistry.register(
|
||||
"spray",
|
||||
class SprayBrush extends PencilBrush {
|
||||
constructor(canvas, options = {}) {
|
||||
super(canvas, {
|
||||
id: "spray",
|
||||
name: "Spray",
|
||||
description: "模拟喷枪效果,创建散点效果",
|
||||
category: "基础笔刷",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// create() {
|
||||
// this.brush = new fabric.SprayBrush(this.canvas);
|
||||
// this.configure(this.brush, this.options);
|
||||
// return this.brush;
|
||||
// }
|
||||
create() {
|
||||
this.brush = new fabric.SprayBrush(this.canvas);
|
||||
this.configure(this.brush, this.options);
|
||||
return this.brush;
|
||||
}
|
||||
|
||||
// configure(brush, options = {}) {
|
||||
// super.configure(brush, options);
|
||||
configure(brush, options = {}) {
|
||||
super.configure(brush, options);
|
||||
|
||||
// if (options.density !== undefined) {
|
||||
// brush.density = options.density;
|
||||
// }
|
||||
if (options.density !== undefined) {
|
||||
brush.density = options.density;
|
||||
}
|
||||
|
||||
// if (options.randomOpacity !== undefined) {
|
||||
// brush.randomOpacity = options.randomOpacity;
|
||||
// }
|
||||
if (options.randomOpacity !== undefined) {
|
||||
brush.randomOpacity = options.randomOpacity;
|
||||
}
|
||||
|
||||
// if (options.dotWidth !== undefined) {
|
||||
// brush.dotWidth = options.dotWidth;
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// name: "喷枪",
|
||||
// description: "模拟喷枪效果,创建散点效果",
|
||||
// }
|
||||
// );
|
||||
if (options.dotWidth !== undefined) {
|
||||
brush.dotWidth = options.dotWidth;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "喷枪",
|
||||
description: "模拟喷枪效果,创建散点效果",
|
||||
}
|
||||
);
|
||||
// 注册橡皮擦笔刷
|
||||
brushRegistry.register(
|
||||
"eraser",
|
||||
@@ -387,6 +387,13 @@ export class BrushManager {
|
||||
width: this.brushStore.state.size,
|
||||
opacity: this.brushStore.state.opacity,
|
||||
|
||||
// 阴影相关配置
|
||||
shadowEnabled: this.brushStore.state.shadowEnabled,
|
||||
shadowColor: this.brushStore.state.shadowColor,
|
||||
shadowWidth: this.brushStore.state.shadowWidth,
|
||||
shadowOffsetX: this.brushStore.state.shadowOffsetX,
|
||||
shadowOffsetY: this.brushStore.state.shadowOffsetY,
|
||||
|
||||
// 材质笔刷特有配置
|
||||
textureEnabled: this.brushStore.state.textureEnabled,
|
||||
texturePath: this.brushStore.state.texturePath,
|
||||
@@ -522,16 +529,23 @@ export class BrushManager {
|
||||
// 限制透明度范围
|
||||
const brushOpacity = Math.max(0.05, Math.min(1, opacity));
|
||||
|
||||
// 更新笔刷透明度
|
||||
this.canvas.freeDrawingBrush.opacity = brushOpacity;
|
||||
// 不再设置fabric笔刷的opacity属性,而是通过颜色的RGBA值来实现透明度
|
||||
// this.canvas.freeDrawingBrush.opacity = brushOpacity;
|
||||
|
||||
// 更新活动笔刷
|
||||
// 更新活动笔刷配置,使用当前颜色和新的透明度
|
||||
if (this.activeBrush) {
|
||||
const currentColor = this.brushStore.state.color;
|
||||
this.activeBrush.configure(this.canvas.freeDrawingBrush, {
|
||||
color: currentColor,
|
||||
opacity: brushOpacity,
|
||||
});
|
||||
}
|
||||
|
||||
// 确保上下文透明度始终为1,避免全局透明度问题
|
||||
if (this.canvas.contextTop) {
|
||||
this.canvas.contextTop.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// 更新Store
|
||||
this.brushStore.setBrushOpacity(brushOpacity);
|
||||
|
||||
@@ -842,6 +856,16 @@ export class BrushManager {
|
||||
|
||||
return this.canvas.freeDrawingBrush;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔刷阴影设置
|
||||
*/
|
||||
updateShadow() {
|
||||
if (this.activeBrush && this.activeBrush.updateShadow) {
|
||||
this.activeBrush.updateShadow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建橡皮擦
|
||||
* @returns {Object} 橡皮擦笔刷
|
||||
|
||||
@@ -427,6 +427,12 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
},
|
||||
|
||||
onMouseDown: function (pointer) {
|
||||
// 添加坐标转换处理画布缩放和偏移
|
||||
pointer.x =
|
||||
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
|
||||
pointer.y =
|
||||
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
|
||||
|
||||
this._points = [pointer];
|
||||
this._count = 0;
|
||||
|
||||
@@ -449,6 +455,12 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
},
|
||||
|
||||
onMouseMove: function (pointer) {
|
||||
// 添加坐标转换处理画布缩放和偏移
|
||||
pointer.x =
|
||||
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
|
||||
pointer.y =
|
||||
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
|
||||
|
||||
this._points.push(pointer);
|
||||
|
||||
var i,
|
||||
@@ -985,9 +997,9 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
ctx.globalAlpha = 0.8 * this.opacity;
|
||||
ctx.moveTo(this._lastPoint.x, this._lastPoint.y);
|
||||
let x =
|
||||
content > this._lastPoint.x
|
||||
? content - this._lastPoint.x + content
|
||||
: content * 2 - this._lastPoint.x;
|
||||
this.content > this._lastPoint.x
|
||||
? this.content - this._lastPoint.x + this.content
|
||||
: this.content * 2 - this._lastPoint.x;
|
||||
ctx.lineTo(x, this._lastPoint.y);
|
||||
// ctx.lineTo(pointer.y + lineWidthDiff,pointer.x + lineWidthDiff);
|
||||
ctx.stroke();
|
||||
@@ -997,10 +1009,12 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
},
|
||||
|
||||
onMouseDown: function (pointer) {
|
||||
// 添加坐标转换处理画布缩放和偏移
|
||||
pointer.x =
|
||||
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
|
||||
pointer.y =
|
||||
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
|
||||
|
||||
this._lastPoint = pointer;
|
||||
this.canvas.contextTop.strokeStyle = this.color;
|
||||
this.canvas.contextTop.lineWidth = this._lineWidth;
|
||||
@@ -1009,10 +1023,12 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
|
||||
onMouseMove: function (pointer) {
|
||||
if (this.canvas._isCurrentlyDrawing) {
|
||||
// 添加坐标转换处理画布缩放和偏移
|
||||
pointer.x =
|
||||
pointer.x * this.canvas.getZoom() + this.canvas.viewportTransform[4];
|
||||
pointer.y =
|
||||
pointer.y * this.canvas.getZoom() + this.canvas.viewportTransform[5];
|
||||
|
||||
this._render(pointer);
|
||||
}
|
||||
},
|
||||
@@ -1022,7 +1038,7 @@ import { sprayBrushDataUrl } from "./data/sprayBrushData.js";
|
||||
this.canvas.contextTop.globalAlpha = 1;
|
||||
this.convertToImg();
|
||||
},
|
||||
}); // End MarkerBrush
|
||||
}); // End MarkerBrush1
|
||||
/**
|
||||
* PenBrush
|
||||
* Based on code by Tennison Chan.
|
||||
|
||||
@@ -87,8 +87,15 @@ export class PencilBrush extends BaseBrush {
|
||||
|
||||
// 调用 convertToImg 方法将绘制内容转换为图片
|
||||
if (typeof this.convertToImg === "function") {
|
||||
// 确保在转换前设置正确的透明度
|
||||
const currentAlpha = ctx.globalAlpha;
|
||||
ctx.globalAlpha = this.opacity || 1;
|
||||
|
||||
this.convertToImg();
|
||||
console.log("PencilBrush: convertToImg called successfully");
|
||||
|
||||
// 恢复透明度
|
||||
ctx.globalAlpha = currentAlpha;
|
||||
} else {
|
||||
console.warn(
|
||||
"convertToImg method not found, falling back to original behavior"
|
||||
@@ -157,31 +164,96 @@ export class PencilBrush extends BaseBrush {
|
||||
brush.width = options.width;
|
||||
}
|
||||
|
||||
if (options.color !== undefined) {
|
||||
if (options.color !== undefined && options.opacity !== undefined) {
|
||||
// 使用RGBA颜色而不是设置globalAlpha
|
||||
brush.color = this._createRGBAColor(options.color, options.opacity);
|
||||
} else if (options.color !== undefined) {
|
||||
brush.color = options.color;
|
||||
} else if (options.opacity !== undefined) {
|
||||
// 如果只设置了透明度,基于当前颜色创建RGBA
|
||||
const currentColor = brush.color || "#000000";
|
||||
brush.color = this._createRGBAColor(currentColor, options.opacity);
|
||||
}
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
// 确保不使用globalAlpha,避免圆圈绘制问题
|
||||
if (brush.canvas && brush.canvas.contextTop) {
|
||||
brush.canvas.contextTop.globalAlpha = 1;
|
||||
brush.canvas.contextTop.lineCap = "round";
|
||||
brush.canvas.contextTop.lineJoin = "round";
|
||||
}
|
||||
|
||||
// 特殊属性配置
|
||||
options.decimate = 0;
|
||||
if (options.decimate !== undefined) {
|
||||
brush.decimate = options.decimate;
|
||||
this.decimate = options.decimate;
|
||||
}
|
||||
|
||||
options.strokeLineCap = "round";
|
||||
if (options.strokeLineCap !== undefined) {
|
||||
brush.strokeLineCap = options.strokeLineCap;
|
||||
this.strokeLineCap = options.strokeLineCap;
|
||||
}
|
||||
|
||||
options.strokeLineJoin = "round";
|
||||
if (options.strokeLineJoin !== undefined) {
|
||||
brush.strokeLineJoin = options.strokeLineJoin;
|
||||
this.strokeLineJoin = options.strokeLineJoin;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建RGBA颜色字符串
|
||||
* @private
|
||||
* @param {String} color 十六进制颜色或已有颜色
|
||||
* @param {Number} opacity 透明度 (0-1)
|
||||
* @returns {String} RGBA颜色字符串
|
||||
*/
|
||||
_createRGBAColor(color, opacity) {
|
||||
// 如果已经是rgba颜色,先提取RGB部分
|
||||
if (color.startsWith("rgba")) {
|
||||
const rgbaMatch = color.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/
|
||||
);
|
||||
if (rgbaMatch) {
|
||||
const [, r, g, b] = rgbaMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是rgb颜色,提取RGB部分
|
||||
if (color.startsWith("rgb")) {
|
||||
const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch;
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理十六进制颜色
|
||||
if (color.startsWith("#")) {
|
||||
const hex = color.replace("#", "");
|
||||
let r, g, b;
|
||||
|
||||
if (hex.length === 3) {
|
||||
r = parseInt(hex[0] + hex[0], 16);
|
||||
g = parseInt(hex[1] + hex[1], 16);
|
||||
b = parseInt(hex[2] + hex[2], 16);
|
||||
} else if (hex.length === 6) {
|
||||
r = parseInt(hex.substring(0, 2), 16);
|
||||
g = parseInt(hex.substring(2, 4), 16);
|
||||
b = parseInt(hex.substring(4, 6), 16);
|
||||
} else {
|
||||
// 无效的十六进制颜色,使用默认
|
||||
r = g = b = 0;
|
||||
}
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
|
||||
// 如果是其他格式的颜色,尝试转换(例如颜色名称)
|
||||
// 这里简化处理,实际项目中可以使用更复杂的颜色解析
|
||||
return color; // fallback到原颜色
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔刷可配置属性
|
||||
* @returns {Array} 可配置属性描述数组
|
||||
@@ -193,69 +265,69 @@ export class PencilBrush extends BaseBrush {
|
||||
|
||||
// 定义铅笔笔刷特有属性
|
||||
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 },
|
||||
},
|
||||
// {
|
||||
// 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 },
|
||||
// },
|
||||
];
|
||||
|
||||
// 合并并返回所有属性
|
||||
|
||||
@@ -73,6 +73,15 @@ export class RibbonBrush extends BaseBrush {
|
||||
|
||||
if (options.opacity !== undefined) {
|
||||
brush.opacity = options.opacity;
|
||||
// 如果启用渐变,更新渐变的第一个颜色
|
||||
if (this.gradient && this.gradientColors.length > 0) {
|
||||
this.gradientColors[0] = this._createRGBAColor(
|
||||
brush.color,
|
||||
options.opacity
|
||||
);
|
||||
this.updateGradient();
|
||||
}
|
||||
brush.canvas.freeDrawingBrush.opacity = options.opacity;
|
||||
}
|
||||
|
||||
// 丝带笔刷特有属性
|
||||
|
||||
@@ -27,8 +27,7 @@ export class TextureBrush extends BaseBrush {
|
||||
this.textureRepeat = options.textureRepeat || "repeat";
|
||||
this.textureScale = options.textureScale || 1;
|
||||
this.textureAngle = options.textureAngle || 0;
|
||||
this.textureOpacity =
|
||||
options.textureOpacity !== undefined ? options.textureOpacity : 1;
|
||||
this.opacity = options.opacity !== undefined ? options.opacity : 1;
|
||||
|
||||
// 预设材质相关
|
||||
this.selectedTextureId = options.selectedTextureId || null;
|
||||
@@ -201,8 +200,8 @@ export class TextureBrush extends BaseBrush {
|
||||
}
|
||||
}
|
||||
|
||||
if (options.textureOpacity !== undefined) {
|
||||
this.textureOpacity = options.textureOpacity;
|
||||
if (options.opacity !== undefined) {
|
||||
this.opacity = options.opacity;
|
||||
// 需要重新应用纹理以应用透明度
|
||||
if (this.selectedTextureId) {
|
||||
this.setTextureById(this.selectedTextureId);
|
||||
@@ -286,9 +285,9 @@ export class TextureBrush extends BaseBrush {
|
||||
canvasTexture.width = width;
|
||||
canvasTexture.height = height;
|
||||
|
||||
// 应用透明度设置
|
||||
if (this.textureOpacity < 1) {
|
||||
ctx.globalAlpha = this.textureOpacity;
|
||||
// 应用纹理透明度设置 - 仅控制纹理的透明度
|
||||
if (this.opacity < 1) {
|
||||
ctx.globalAlpha = this.opacity;
|
||||
}
|
||||
|
||||
// 绘制前应用旋转
|
||||
@@ -310,6 +309,18 @@ export class TextureBrush extends BaseBrush {
|
||||
// 直接设置source属性,这是PatternBrush的标准用法
|
||||
this.brush.source = canvasTexture;
|
||||
|
||||
this.brush.canvas.opacity = this.opacity; // 确保画布不透明
|
||||
|
||||
// 设置笔刷颜色为完全透明的RGBA,避免重复绘制时的叠加效果
|
||||
// 笔刷的透明度通过RGBA颜色控制,而不是通过globalAlpha
|
||||
const brushOpacity = this.brush.opacity || 1;
|
||||
this.brush.color = `rgba(0, 0, 0, ${brushOpacity})`;
|
||||
|
||||
// 确保画布上下文使用完整的透明度,避免圆圈重叠问题
|
||||
// if (this.canvas && this.canvas.contextTop) {
|
||||
// this.canvas.contextTop.globalAlpha = 1;
|
||||
// }
|
||||
|
||||
// 通知画布重绘
|
||||
if (this.canvas && this.canvas.requestRenderAll) {
|
||||
this.canvas.requestRenderAll();
|
||||
@@ -378,8 +389,8 @@ export class TextureBrush extends BaseBrush {
|
||||
* @param {Number} opacity 透明度
|
||||
* @returns {Number} 设置后的透明度
|
||||
*/
|
||||
setTextureOpacity(opacity) {
|
||||
this.textureOpacity = Math.min(1, Math.max(0, opacity));
|
||||
setopacity(opacity) {
|
||||
this.opacity = Math.min(1, Math.max(0, opacity));
|
||||
|
||||
// 重新应用纹理以更新透明度
|
||||
if (this.selectedTextureId) {
|
||||
@@ -388,7 +399,7 @@ export class TextureBrush extends BaseBrush {
|
||||
this.setTexture(this.textureSource);
|
||||
}
|
||||
|
||||
return this.textureOpacity;
|
||||
return this.opacity;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -518,10 +529,10 @@ export class TextureBrush extends BaseBrush {
|
||||
// order: 130,
|
||||
// },
|
||||
// {
|
||||
// id: "textureOpacity",
|
||||
// id: "opacity",
|
||||
// name: "纹理透明度",
|
||||
// type: "slider",
|
||||
// defaultValue: this.textureOpacity,
|
||||
// defaultValue: this.opacity,
|
||||
// min: 0,
|
||||
// max: 1,
|
||||
// step: 0.05,
|
||||
@@ -584,8 +595,8 @@ export class TextureBrush extends BaseBrush {
|
||||
} else if (propId === "textureAngle") {
|
||||
this.setTextureAngle(value);
|
||||
return true;
|
||||
} else if (propId === "textureOpacity") {
|
||||
this.setTextureOpacity(value);
|
||||
} else if (propId === "opacity") {
|
||||
this.setopacity(value);
|
||||
return true;
|
||||
} else if (propId === "uploadTexture") {
|
||||
// 触发上传纹理事件
|
||||
@@ -688,7 +699,7 @@ export class TextureBrush extends BaseBrush {
|
||||
case "scale":
|
||||
return this.setTextureScale(value);
|
||||
case "opacity":
|
||||
return this.setTextureOpacity(value);
|
||||
return this.setopacity(value);
|
||||
case "repeat":
|
||||
return this.setTextureRepeat(value);
|
||||
case "angle":
|
||||
@@ -708,7 +719,7 @@ export class TextureBrush extends BaseBrush {
|
||||
case "scale":
|
||||
return this.textureScale;
|
||||
case "opacity":
|
||||
return this.textureOpacity;
|
||||
return this.opacity;
|
||||
case "repeat":
|
||||
return this.textureRepeat;
|
||||
case "angle":
|
||||
@@ -751,7 +762,7 @@ export class TextureBrush extends BaseBrush {
|
||||
}
|
||||
|
||||
if (presetData.opacity !== undefined) {
|
||||
this.setTextureOpacity(presetData.opacity);
|
||||
this.setopacity(presetData.opacity);
|
||||
}
|
||||
|
||||
if (presetData.repeat !== undefined) {
|
||||
@@ -786,7 +797,7 @@ export class TextureBrush extends BaseBrush {
|
||||
return {
|
||||
textureId: this.selectedTextureId,
|
||||
scale: this.textureScale,
|
||||
opacity: this.textureOpacity,
|
||||
opacity: this.opacity,
|
||||
repeat: this.textureRepeat,
|
||||
angle: this.textureAngle,
|
||||
// 包含笔刷状态
|
||||
@@ -815,7 +826,7 @@ export class TextureBrush extends BaseBrush {
|
||||
}
|
||||
|
||||
if (state.opacity !== undefined) {
|
||||
this.setTextureOpacity(state.opacity);
|
||||
this.setopacity(state.opacity);
|
||||
}
|
||||
|
||||
if (state.repeat !== undefined) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
|
||||
import { LiquifyCPUManager } from "./LiquifyCPUManager";
|
||||
import { LayerType } from "../../utils/layerHelper";
|
||||
import { findInChildLayers, LayerType } from "../../utils/layerHelper";
|
||||
|
||||
export class EnhancedLiquifyManager {
|
||||
/**
|
||||
@@ -160,7 +160,10 @@ export class EnhancedLiquifyManager {
|
||||
} else if (typeof target === "object") {
|
||||
// 传入的是对象
|
||||
targetObject = target;
|
||||
const layer = this.layerManager.findLayerByObject(targetObject);
|
||||
const { layer } = findInChildLayers(
|
||||
this.layerManager.layers.value,
|
||||
targetObject.layerId
|
||||
);
|
||||
if (layer) {
|
||||
targetLayerId = layer.id;
|
||||
} else {
|
||||
@@ -632,16 +635,69 @@ export class EnhancedLiquifyManager {
|
||||
async _getImageData(fabricObject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas - 关键修复:使用原始图像尺寸,不考虑fabric对象的缩放
|
||||
console.log("开始获取图像数据,对象类型:", fabricObject.type);
|
||||
console.log("对象属性:", {
|
||||
width: fabricObject.width,
|
||||
height: fabricObject.height,
|
||||
hasElement: !!fabricObject._element,
|
||||
hasSrc: !!fabricObject.getSrc,
|
||||
});
|
||||
|
||||
// 检查基本属性
|
||||
if (!fabricObject.width || !fabricObject.height) {
|
||||
reject(new Error("图像对象缺少有效的宽度或高度"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建临时canvas - 使用原始图像尺寸,不考虑fabric对象的缩放
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
// 使用图像的原始尺寸,而不是缩放后的尺寸
|
||||
tempCanvas.width = fabricObject.width;
|
||||
tempCanvas.height = fabricObject.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 如果对象有图像元素
|
||||
console.log(
|
||||
`创建临时Canvas,尺寸: ${tempCanvas.width}x${tempCanvas.height}`
|
||||
);
|
||||
|
||||
// 处理不同的图像源
|
||||
if (fabricObject._element) {
|
||||
// 绘制原始尺寸的图像
|
||||
console.log("使用 _element 绘制图像");
|
||||
|
||||
// 检查_element是否有效
|
||||
if (
|
||||
!fabricObject._element.complete &&
|
||||
fabricObject._element.tagName === "IMG"
|
||||
) {
|
||||
console.log("图像未加载完成,等待加载...");
|
||||
fabricObject._element.onload = () => {
|
||||
try {
|
||||
tempCtx.drawImage(
|
||||
fabricObject._element,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
console.log("✅ 图像加载完成后获取数据成功");
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
console.error("图像加载后绘制失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
fabricObject._element.onerror = () => {
|
||||
reject(new Error("图像加载失败"));
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接绘制已加载的图像
|
||||
tempCtx.drawImage(
|
||||
fabricObject._element,
|
||||
0,
|
||||
@@ -649,49 +705,124 @@ export class EnhancedLiquifyManager {
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
} else if (fabricObject.getSrc) {
|
||||
} else if (
|
||||
fabricObject.getSrc &&
|
||||
typeof fabricObject.getSrc === "function"
|
||||
) {
|
||||
console.log("使用 getSrc() 方法获取图像源");
|
||||
|
||||
// 通过URL创建图像
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous"; // 避免跨域问题
|
||||
|
||||
img.onload = () => {
|
||||
tempCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
resolve(imageData);
|
||||
try {
|
||||
console.log(
|
||||
`图像加载成功,原始尺寸: ${img.naturalWidth}x${img.naturalHeight}`
|
||||
);
|
||||
|
||||
tempCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log("✅ 通过URL获取图像数据成功");
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
console.error("绘制图像失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = fabricObject.getSrc();
|
||||
|
||||
img.onerror = (error) => {
|
||||
console.error("图像加载失败:", error);
|
||||
reject(new Error("无法加载图像URL: " + fabricObject.getSrc()));
|
||||
};
|
||||
|
||||
const srcUrl = fabricObject.getSrc();
|
||||
console.log("加载图像URL:", srcUrl);
|
||||
img.src = srcUrl;
|
||||
return;
|
||||
} else if (fabricObject.src) {
|
||||
console.log("使用 src 属性获取图像源");
|
||||
|
||||
// 通过src属性创建图像
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
tempCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log("✅ 通过src属性获取图像数据成功");
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
console.error("通过src绘制图像失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = (error) => {
|
||||
console.error("通过src加载图像失败:", error);
|
||||
reject(new Error("无法加载图像src: " + fabricObject.src));
|
||||
};
|
||||
|
||||
console.log("加载图像src:", fabricObject.src);
|
||||
img.src = fabricObject.src;
|
||||
return;
|
||||
} else {
|
||||
reject(new Error("无法获取图像数据"));
|
||||
console.error("无法找到有效的图像源");
|
||||
reject(
|
||||
new Error("图像对象缺少有效的图像源(_element, getSrc, 或 src)")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取图像数据
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
// 如果走到这里,说明使用了_element直接绘制
|
||||
try {
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
`获取图像数据: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +
|
||||
`对象缩放=(${fabricObject.scaleX}, ${fabricObject.scaleY}), ` +
|
||||
`图像数据尺寸=${imageData.width}x${imageData.height}`
|
||||
);
|
||||
console.log(
|
||||
`✅ 获取图像数据成功: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +
|
||||
`对象缩放=(${fabricObject.scaleX}, ${fabricObject.scaleY}), ` +
|
||||
`图像数据尺寸=${imageData.width}x${imageData.height}`
|
||||
);
|
||||
|
||||
resolve(imageData);
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
console.error("获取ImageData失败:", error);
|
||||
reject(new Error("无法从Canvas获取图像数据: " + error.message));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("_getImageData 执行失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1361,6 +1361,9 @@ export class LiquifyCPUManager {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 停止持续效果定时器
|
||||
this.stopContinuousEffect();
|
||||
|
||||
this.originalImageData = null;
|
||||
this.currentImageData = null;
|
||||
this.mesh = null;
|
||||
@@ -1389,4 +1392,11 @@ export class LiquifyCPUManager {
|
||||
this.accumulatedScale = 0;
|
||||
this.lastApplyTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源 - 别名方法,与其他管理器保持一致
|
||||
*/
|
||||
dispose() {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@ const state = reactive({
|
||||
opacity: 1, // 笔刷透明度
|
||||
type: "pencil", // 当前笔刷类型
|
||||
|
||||
// 阴影相关属性
|
||||
shadowEnabled: false, // 是否启用阴影
|
||||
shadowColor: "#000000", // 阴影颜色(默认为笔刷颜色)
|
||||
shadowWidth: 0, // 阴影宽度
|
||||
shadowOffsetX: 0, // 阴影X偏移
|
||||
shadowOffsetY: 0, // 阴影Y偏移
|
||||
|
||||
// 笔刷材质相关
|
||||
textureScale: 1, // 材质缩放
|
||||
textureEnabled: false, // 是否启用材质
|
||||
@@ -130,6 +137,27 @@ const actions = {
|
||||
state.texturePath = path;
|
||||
},
|
||||
|
||||
// 阴影相关方法
|
||||
setShadowEnabled(enabled) {
|
||||
state.shadowEnabled = enabled;
|
||||
},
|
||||
|
||||
setShadowColor(color) {
|
||||
state.shadowColor = color;
|
||||
},
|
||||
|
||||
setShadowWidth(width) {
|
||||
state.shadowWidth = Math.max(0, Math.min(50, width));
|
||||
},
|
||||
|
||||
setShadowOffsetX(offsetX) {
|
||||
state.shadowOffsetX = Math.max(-50, Math.min(50, offsetX));
|
||||
},
|
||||
|
||||
setShadowOffsetY(offsetY) {
|
||||
state.shadowOffsetY = Math.max(-50, Math.min(50, offsetY));
|
||||
},
|
||||
|
||||
setAvailableBrushes(brushes) {
|
||||
state.availableBrushes = brushes;
|
||||
},
|
||||
@@ -611,4 +639,78 @@ export const BrushStore = {
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${state.opacity})`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前笔刷的实际绘制颜色(包含透明度)
|
||||
* @returns {String} RGBA格式的颜色字符串
|
||||
*/
|
||||
getCurrentBrushColor() {
|
||||
return this.getRGBAColor();
|
||||
},
|
||||
|
||||
/**
|
||||
* 从RGBA颜色字符串中提取RGB值
|
||||
* @param {String} rgbaColor RGBA颜色字符串
|
||||
* @returns {Object} {r, g, b, a} 颜色值对象
|
||||
*/
|
||||
parseRGBAColor(rgbaColor) {
|
||||
const rgbaMatch = rgbaColor.match(
|
||||
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
|
||||
);
|
||||
if (rgbaMatch) {
|
||||
return {
|
||||
r: parseInt(rgbaMatch[1]),
|
||||
g: parseInt(rgbaMatch[2]),
|
||||
b: parseInt(rgbaMatch[3]),
|
||||
a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 将RGB值转换为十六进制颜色
|
||||
* @param {Number} r 红色值 (0-255)
|
||||
* @param {Number} g 绿色值 (0-255)
|
||||
* @param {Number} b 蓝色值 (0-255)
|
||||
* @returns {String} 十六进制颜色字符串
|
||||
*/
|
||||
rgbToHex(r, g, b) {
|
||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前阴影配置对象
|
||||
* @returns {Object|null} fabric.Shadow配置对象或null
|
||||
*/
|
||||
getShadowConfig() {
|
||||
if (!state.shadowEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
color: state.shadowColor,
|
||||
blur: state.shadowWidth,
|
||||
offsetX: state.shadowOffsetX,
|
||||
offsetY: state.shadowOffsetY,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建fabric.Shadow实例
|
||||
* @returns {fabric.Shadow|null} fabric.Shadow实例或null
|
||||
*/
|
||||
createFabricShadow() {
|
||||
const config = this.getShadowConfig();
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保fabric已加载
|
||||
if (typeof fabric !== "undefined" && fabric.Shadow) {
|
||||
return new fabric.Shadow(config);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -248,6 +248,23 @@ export function loadImageUrlToLayer(
|
||||
...options,
|
||||
});
|
||||
|
||||
if (options.imageMode) {
|
||||
imageModeHandler({
|
||||
imageMode: options.imageMode,
|
||||
newImage: fabricImage,
|
||||
canvasWidth: maxWidth,
|
||||
canvasHeight: maxHeight,
|
||||
});
|
||||
|
||||
// 默认居中
|
||||
fabricImage.set({
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
left: canvas.width / 2,
|
||||
top: canvas.height / 2,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建图片图层
|
||||
const layerId = await createImageLayer({
|
||||
layerManager,
|
||||
|
||||
@@ -17,6 +17,7 @@ export const createRasterizedImage = async ({
|
||||
format = "png", // 图像格式
|
||||
scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
||||
isThumbnail = false, // 是否为缩略图
|
||||
} = {}) => {
|
||||
try {
|
||||
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
||||
@@ -34,6 +35,8 @@ export const createRasterizedImage = async ({
|
||||
currentZoom
|
||||
);
|
||||
|
||||
if (isThumbnail) scaleFactor = 0.2; // 缩略图使用较小的高清倍数
|
||||
|
||||
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
|
||||
|
||||
// 使用组对象方式创建栅格化图像
|
||||
|
||||
@@ -19,6 +19,7 @@ export const createRasterizedImage = async ({
|
||||
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
||||
preserveOriginalQuality = true, // 是否保持原始质量(新增)
|
||||
selectionManager = null, // 选区管理器,用于获取羽化值等设置
|
||||
} = {}) => {
|
||||
try {
|
||||
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
||||
@@ -38,6 +39,7 @@ export const createRasterizedImage = async ({
|
||||
fabricObjects,
|
||||
clippingObject,
|
||||
isReturenDataURL,
|
||||
selectionManager, // 传递选区管理器
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,20 +79,36 @@ const createClippedObjects = async ({
|
||||
fabricObjects,
|
||||
clippingObject,
|
||||
isReturenDataURL,
|
||||
selectionManager = null, // 新增选区管理器参数
|
||||
}) => {
|
||||
try {
|
||||
console.log("🎯 使用新的裁剪方法创建对象");
|
||||
console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
|
||||
|
||||
// 使用优化后的边界计算,确保包含描边区域
|
||||
const optimizedBounds = calculateOptimizedBounds(
|
||||
clippingObject,
|
||||
fabricObjects
|
||||
);
|
||||
console.log("📐 优化后的选区边界框:", optimizedBounds);
|
||||
|
||||
// 获取羽化值
|
||||
let featherAmount = 0;
|
||||
if (
|
||||
selectionManager &&
|
||||
typeof selectionManager.getFeatherAmount === "function"
|
||||
) {
|
||||
featherAmount = selectionManager.getFeatherAmount();
|
||||
console.log(`🌟 应用羽化效果: ${featherAmount}px`);
|
||||
}
|
||||
|
||||
// 获取选区边界框
|
||||
const selectionBounds = clippingObject.getBoundingRect(true);
|
||||
console.log("📐 选区边界框:", selectionBounds);
|
||||
// 方法1:如果只需要返回DataURL,使用画布裁剪方法
|
||||
if (isReturenDataURL) {
|
||||
return await createClippedDataURLByCanvas({
|
||||
canvas,
|
||||
fabricObjects,
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
||||
featherAmount,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,40 +117,44 @@ const createClippedObjects = async ({
|
||||
canvas,
|
||||
fabricObjects,
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
||||
featherAmount,
|
||||
});
|
||||
|
||||
// 将DataURL转换为fabric.Image对象
|
||||
const fabricImage = await createFabricImageFromDataURL(clippedDataURL);
|
||||
|
||||
// 使用fabric原生方法恢复到选区的原始大小和位置
|
||||
fabricImage.scaleToWidth(selectionBounds.width);
|
||||
fabricImage.scaleToHeight(selectionBounds.height);
|
||||
fabricImage.scaleToWidth(optimizedBounds.width);
|
||||
fabricImage.scaleToHeight(optimizedBounds.height);
|
||||
|
||||
// 设置到选区的原始位置(中心点)
|
||||
fabricImage.set({
|
||||
left: selectionBounds.left + selectionBounds.width / 2,
|
||||
top: selectionBounds.top + selectionBounds.height / 2,
|
||||
left: optimizedBounds.left + optimizedBounds.width / 2,
|
||||
top: optimizedBounds.top + optimizedBounds.height / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
selectable: true,
|
||||
evented: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
// hasControls: true,
|
||||
// hasBorders: true,
|
||||
custom: {
|
||||
type: "clipped",
|
||||
clippedAt: new Date().toISOString(),
|
||||
hasClipping: true,
|
||||
preservedQuality: true,
|
||||
originalBounds: selectionBounds,
|
||||
originalBounds: optimizedBounds, // 保存优化后的边界框
|
||||
restoredToOriginalSize: true,
|
||||
usedImageMask: true, // 标记使用了图像遮罩
|
||||
featherAmount: featherAmount,
|
||||
boundaryOptimized: true, // 标记使用了边界优化
|
||||
},
|
||||
});
|
||||
|
||||
// 更新坐标
|
||||
fabricImage.setCoords();
|
||||
|
||||
console.log("✅ 返回裁剪后的fabric对象,已恢复到原始大小和位置");
|
||||
console.log("✅ 返回裁剪后的fabric对象,已恢复到优化后的原始大小和位置");
|
||||
return fabricImage;
|
||||
} catch (error) {
|
||||
console.error("创建裁剪对象失败:", error);
|
||||
@@ -149,78 +171,66 @@ const createClippedDataURLByCanvas = async ({
|
||||
fabricObjects,
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
featherAmount = 0,
|
||||
}) => {
|
||||
try {
|
||||
console.log("🖼️ 使用画布裁剪方法生成DataURL");
|
||||
console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
|
||||
|
||||
// 创建临时画布,尺寸与选区完全一致
|
||||
const tempCanvas = new fabric.StaticCanvas();
|
||||
// 使用优化后的边界计算,确保包含描边区域
|
||||
const optimizedBounds = calculateOptimizedBounds(
|
||||
clippingObject,
|
||||
fabricObjects
|
||||
);
|
||||
|
||||
// 使用高分辨率以保证质量
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const qualityMultiplier = Math.max(2, pixelRatio);
|
||||
|
||||
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
|
||||
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
|
||||
|
||||
tempCanvas.setDimensions({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
});
|
||||
const canvasWidth = Math.ceil(optimizedBounds.width * qualityMultiplier);
|
||||
const canvasHeight = Math.ceil(optimizedBounds.height * qualityMultiplier);
|
||||
|
||||
console.log(
|
||||
`📏 临时画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
|
||||
`📏 优化后画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
|
||||
);
|
||||
|
||||
// 克隆并添加所有需要裁剪的对象
|
||||
for (const obj of fabricObjects) {
|
||||
const clonedObj = await cloneObjectAsync(obj);
|
||||
|
||||
// 调整对象位置:将选区左上角作为新的原点(0,0)
|
||||
// 同时应用质量倍数缩放
|
||||
clonedObj.set({
|
||||
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
|
||||
top: (clonedObj.top - selectionBounds.top) * qualityMultiplier,
|
||||
scaleX: (clonedObj.scaleX || 1) * qualityMultiplier,
|
||||
scaleY: (clonedObj.scaleY || 1) * qualityMultiplier,
|
||||
});
|
||||
|
||||
tempCanvas.add(clonedObj);
|
||||
}
|
||||
|
||||
// 克隆裁剪路径并调整位置
|
||||
const clipPath = await cloneObjectAsync(clippingObject);
|
||||
clipPath.set({
|
||||
left: (clipPath.left - selectionBounds.left) * qualityMultiplier,
|
||||
top: (clipPath.top - selectionBounds.top) * qualityMultiplier,
|
||||
scaleX: (clipPath.scaleX || 1) * qualityMultiplier,
|
||||
scaleY: (clipPath.scaleY || 1) * qualityMultiplier,
|
||||
fill: "transparent",
|
||||
stroke: "",
|
||||
strokeWidth: 0,
|
||||
absolutePositioned: true,
|
||||
console.log("🎯 边界框对比:", {
|
||||
original: selectionBounds,
|
||||
optimized: optimizedBounds,
|
||||
});
|
||||
|
||||
// 为整个画布设置裁剪路径
|
||||
tempCanvas.clipPath = clipPath;
|
||||
// 步骤1: 先将路径转换为遮罩图像(支持羽化)
|
||||
const maskImageDataURL =
|
||||
featherAmount > 0
|
||||
? await createAdvancedMaskImage({
|
||||
clippingObject,
|
||||
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
||||
qualityMultiplier,
|
||||
featherAmount,
|
||||
})
|
||||
: await createMaskImageFromPath({
|
||||
clippingObject,
|
||||
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
||||
qualityMultiplier,
|
||||
});
|
||||
|
||||
// 渲染画布
|
||||
tempCanvas.renderAll();
|
||||
|
||||
// 生成高质量DataURL
|
||||
const dataURL = tempCanvas.toDataURL({
|
||||
format: "png",
|
||||
quality: 1.0,
|
||||
multiplier: 1, // 已经通过尺寸处理了缩放
|
||||
// 步骤2: 渲染原始内容
|
||||
const contentImageDataURL = await renderContentToImage({
|
||||
fabricObjects,
|
||||
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
||||
qualityMultiplier,
|
||||
});
|
||||
|
||||
// 清理临时画布
|
||||
tempCanvas.dispose();
|
||||
// 步骤3: 使用遮罩合成最终结果
|
||||
const clippedDataURL = await applyImageMask({
|
||||
contentImageDataURL,
|
||||
maskImageDataURL,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
});
|
||||
|
||||
console.log("✅ 画布裁剪完成,生成DataURL");
|
||||
return dataURL;
|
||||
console.log("✅ 图像遮罩裁剪完成,生成DataURL");
|
||||
return clippedDataURL;
|
||||
} catch (error) {
|
||||
console.error("画布裁剪失败:", error);
|
||||
console.error("图像遮罩裁剪失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -731,3 +741,477 @@ export const getObjectsBounds = (fabricObjects) => {
|
||||
const { absoluteBounds } = calculateBounds(fabricObjects);
|
||||
return absoluteBounds;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将路径对象转换为遮罩图像
|
||||
* @param {Object} clippingObject 裁剪路径对象
|
||||
* @param {Object} selectionBounds 选区边界框
|
||||
* @param {Number} qualityMultiplier 质量倍数
|
||||
* @returns {Promise<String>} 遮罩图像的DataURL
|
||||
*/
|
||||
const createMaskImageFromPath = async ({
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
qualityMultiplier,
|
||||
}) => {
|
||||
try {
|
||||
console.log("🎭 创建路径遮罩图像");
|
||||
|
||||
// 创建专门用于渲染遮罩的画布
|
||||
const maskCanvas = new fabric.StaticCanvas();
|
||||
|
||||
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
|
||||
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
|
||||
|
||||
maskCanvas.setDimensions({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
});
|
||||
|
||||
// 克隆路径对象并处理描边转填充
|
||||
const maskPath = await createSolidMaskPath(
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
qualityMultiplier
|
||||
);
|
||||
|
||||
// 添加路径到遮罩画布
|
||||
maskCanvas.add(maskPath);
|
||||
maskCanvas.renderAll();
|
||||
|
||||
// 生成遮罩图像
|
||||
const maskDataURL = maskCanvas.toDataURL({
|
||||
format: "png",
|
||||
quality: 1.0,
|
||||
multiplier: 1,
|
||||
});
|
||||
|
||||
// 清理遮罩画布
|
||||
maskCanvas.dispose();
|
||||
|
||||
console.log("✅ 遮罩图像创建完成");
|
||||
return maskDataURL;
|
||||
} catch (error) {
|
||||
console.error("创建遮罩图像失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染内容对象为图像
|
||||
* @param {Array} fabricObjects 要渲染的对象数组
|
||||
* @param {Object} selectionBounds 选区边界框
|
||||
* @param {Number} qualityMultiplier 质量倍数
|
||||
* @returns {Promise<String>} 内容图像的DataURL
|
||||
*/
|
||||
const renderContentToImage = async ({
|
||||
fabricObjects,
|
||||
selectionBounds,
|
||||
qualityMultiplier,
|
||||
}) => {
|
||||
try {
|
||||
console.log("🖼️ 渲染内容图像");
|
||||
|
||||
// 创建内容渲染画布
|
||||
const contentCanvas = new fabric.StaticCanvas();
|
||||
|
||||
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
|
||||
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
|
||||
|
||||
contentCanvas.setDimensions({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
});
|
||||
|
||||
// 克隆并添加所有需要渲染的对象
|
||||
for (const obj of fabricObjects) {
|
||||
const clonedObj = await cloneObjectAsync(obj);
|
||||
|
||||
// 调整对象位置:将选区左上角作为新的原点(0,0)
|
||||
clonedObj.set({
|
||||
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
|
||||
top: (clonedObj.top - selectionBounds.top) * qualityMultiplier,
|
||||
scaleX: (clonedObj.scaleX || 1) * qualityMultiplier,
|
||||
scaleY: (clonedObj.scaleY || 1) * qualityMultiplier,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
});
|
||||
|
||||
contentCanvas.add(clonedObj);
|
||||
}
|
||||
|
||||
// 渲染内容画布
|
||||
contentCanvas.renderAll();
|
||||
|
||||
// 生成内容图像
|
||||
const contentDataURL = contentCanvas.toDataURL({
|
||||
format: "png",
|
||||
quality: 1.0,
|
||||
multiplier: 1,
|
||||
});
|
||||
|
||||
// 清理内容画布
|
||||
contentCanvas.dispose();
|
||||
|
||||
console.log("✅ 内容图像渲染完成");
|
||||
return contentDataURL;
|
||||
} catch (error) {
|
||||
console.error("渲染内容图像失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用遮罩图像对内容图像进行裁剪
|
||||
* @param {String} contentImageDataURL 内容图像DataURL
|
||||
* @param {String} maskImageDataURL 遮罩图像DataURL
|
||||
* @param {Number} canvasWidth 画布宽度
|
||||
* @param {Number} canvasHeight 画布高度
|
||||
* @returns {Promise<String>} 裁剪后的图像DataURL
|
||||
*/
|
||||
const applyImageMask = async ({
|
||||
contentImageDataURL,
|
||||
maskImageDataURL,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
}) => {
|
||||
try {
|
||||
console.log("🎯 应用图像遮罩");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 创建用于合成的Canvas元素
|
||||
const compositeCanvas = document.createElement("canvas");
|
||||
const ctx = compositeCanvas.getContext("2d");
|
||||
|
||||
compositeCanvas.width = canvasWidth;
|
||||
compositeCanvas.height = canvasHeight;
|
||||
|
||||
// 加载内容图像
|
||||
const contentImg = new Image();
|
||||
contentImg.onload = () => {
|
||||
// 加载遮罩图像
|
||||
const maskImg = new Image();
|
||||
maskImg.onload = () => {
|
||||
try {
|
||||
// 先绘制内容图像
|
||||
ctx.drawImage(contentImg, 0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// 设置合成模式为遮罩模式
|
||||
ctx.globalCompositeOperation = "destination-in";
|
||||
|
||||
// 绘制遮罩图像
|
||||
ctx.drawImage(maskImg, 0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// 重置合成模式
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
|
||||
// 获取最终结果
|
||||
const resultDataURL = compositeCanvas.toDataURL("image/png", 1.0);
|
||||
|
||||
console.log("✅ 图像遮罩应用完成");
|
||||
resolve(resultDataURL);
|
||||
} catch (error) {
|
||||
console.error("合成图像失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
maskImg.onerror = () => {
|
||||
reject(new Error("加载遮罩图像失败"));
|
||||
};
|
||||
|
||||
maskImg.src = maskImageDataURL;
|
||||
};
|
||||
|
||||
contentImg.onerror = () => {
|
||||
reject(new Error("加载内容图像失败"));
|
||||
};
|
||||
|
||||
contentImg.src = contentImageDataURL;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("应用图像遮罩失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建带羽化效果的遮罩图像(高级版本)
|
||||
* @param {Object} clippingObject 裁剪路径对象
|
||||
* @param {Object} selectionBounds 选区边界框
|
||||
* @param {Number} qualityMultiplier 质量倍数
|
||||
* @param {Number} featherAmount 羽化值
|
||||
* @returns {Promise<String>} 遮罩图像的DataURL
|
||||
*/
|
||||
const createAdvancedMaskImage = async ({
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
qualityMultiplier,
|
||||
featherAmount = 0,
|
||||
}) => {
|
||||
try {
|
||||
console.log(`🎭 创建高级遮罩图像 (羽化: ${featherAmount})`);
|
||||
|
||||
// 创建专门用于渲染遮罩的画布
|
||||
const maskCanvas = new fabric.StaticCanvas();
|
||||
|
||||
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
|
||||
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
|
||||
|
||||
maskCanvas.setDimensions({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
});
|
||||
|
||||
// 克隆路径对象并处理描边转填充
|
||||
const maskPath = await createSolidMaskPath(
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
qualityMultiplier
|
||||
);
|
||||
|
||||
// 如果有羽化值,添加模糊效果
|
||||
if (featherAmount > 0) {
|
||||
const adjustedFeather = featherAmount * qualityMultiplier;
|
||||
maskPath.shadow = new fabric.Shadow({
|
||||
color: "#ffffff",
|
||||
blur: adjustedFeather,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加路径到遮罩画布
|
||||
maskCanvas.add(maskPath);
|
||||
maskCanvas.renderAll();
|
||||
|
||||
// 如果有羽化,需要进行后处理
|
||||
if (featherAmount > 0) {
|
||||
return await applyCanvasBlur(
|
||||
maskCanvas,
|
||||
featherAmount * qualityMultiplier
|
||||
);
|
||||
}
|
||||
|
||||
// 生成遮罩图像
|
||||
const maskDataURL = maskCanvas.toDataURL({
|
||||
format: "png",
|
||||
quality: 1.0,
|
||||
multiplier: 1,
|
||||
});
|
||||
|
||||
// 清理遮罩画布
|
||||
maskCanvas.dispose();
|
||||
|
||||
console.log("✅ 高级遮罩图像创建完成");
|
||||
return maskDataURL;
|
||||
} catch (error) {
|
||||
console.error("创建高级遮罩图像失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 对画布应用模糊效果
|
||||
* @param {fabric.StaticCanvas} canvas 要处理的画布
|
||||
* @param {Number} blurAmount 模糊值
|
||||
* @returns {Promise<String>} 处理后的DataURL
|
||||
*/
|
||||
const applyCanvasBlur = async (canvas, blurAmount) => {
|
||||
try {
|
||||
// 获取原始图像数据
|
||||
const originalDataURL = canvas.toDataURL({
|
||||
format: "png",
|
||||
quality: 1.0,
|
||||
multiplier: 1,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// 创建一个新的Canvas进行模糊处理
|
||||
const blurCanvas = document.createElement("canvas");
|
||||
const ctx = blurCanvas.getContext("2d");
|
||||
|
||||
blurCanvas.width = canvas.width;
|
||||
blurCanvas.height = canvas.height;
|
||||
|
||||
// 应用CSS滤镜模糊
|
||||
ctx.filter = `blur(${Math.max(1, blurAmount / 2)}px)`;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 重置滤镜
|
||||
ctx.filter = "none";
|
||||
|
||||
const blurredDataURL = blurCanvas.toDataURL("image/png", 1.0);
|
||||
resolve(blurredDataURL);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error("处理模糊效果失败"));
|
||||
};
|
||||
|
||||
img.src = originalDataURL;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("应用画布模糊失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建实体遮罩路径(将描边转换为填充)
|
||||
* @param {Object} clippingObject 原始裁剪对象
|
||||
* @param {Object} selectionBounds 选区边界框
|
||||
* @param {Number} qualityMultiplier 质量倍数
|
||||
* @returns {Promise<fabric.Object>} 处理后的遮罩路径对象
|
||||
*/
|
||||
const createSolidMaskPath = async (
|
||||
clippingObject,
|
||||
selectionBounds,
|
||||
qualityMultiplier
|
||||
) => {
|
||||
try {
|
||||
console.log("🔧 创建实体遮罩路径,处理描边转填充");
|
||||
|
||||
// 克隆原始对象
|
||||
const maskPath = await cloneObjectAsync(clippingObject);
|
||||
|
||||
// 检查是否有描边需要处理
|
||||
const hasStroke = maskPath.stroke && maskPath.strokeWidth > 0;
|
||||
|
||||
if (hasStroke) {
|
||||
console.log(
|
||||
`📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`
|
||||
);
|
||||
|
||||
// 对于有描边的路径,我们需要更精确的处理
|
||||
const strokeWidth = maskPath.strokeWidth;
|
||||
|
||||
// 方法1: 如果是简单的几何形状(矩形、圆形等),可以通过调整尺寸来补偿描边
|
||||
if (
|
||||
maskPath.type === "rect" ||
|
||||
maskPath.type === "circle" ||
|
||||
maskPath.type === "ellipse"
|
||||
) {
|
||||
// 对于矩形和椭圆,增加宽高来包含描边
|
||||
const strokeOffset = strokeWidth;
|
||||
|
||||
maskPath.set({
|
||||
left:
|
||||
(maskPath.left - selectionBounds.left - strokeOffset / 2) *
|
||||
qualityMultiplier,
|
||||
top:
|
||||
(maskPath.top - selectionBounds.top - strokeOffset / 2) *
|
||||
qualityMultiplier,
|
||||
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
|
||||
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
|
||||
width: (maskPath.width || 0) + strokeOffset,
|
||||
height: (maskPath.height || 0) + strokeOffset,
|
||||
fill: "#ffffff",
|
||||
stroke: "",
|
||||
strokeWidth: 0,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
});
|
||||
} else {
|
||||
// 对于复杂路径,使用缩放方式来近似包含描边区域
|
||||
const pathBounds = maskPath.getBoundingRect(true, true);
|
||||
const minDimension = Math.min(pathBounds.width, pathBounds.height);
|
||||
const expandRatio = 1 + (strokeWidth * 2) / minDimension;
|
||||
const strokeOffset = strokeWidth / 2;
|
||||
|
||||
maskPath.set({
|
||||
left:
|
||||
(maskPath.left - selectionBounds.left - strokeOffset) *
|
||||
qualityMultiplier,
|
||||
top:
|
||||
(maskPath.top - selectionBounds.top - strokeOffset) *
|
||||
qualityMultiplier,
|
||||
scaleX: (maskPath.scaleX || 1) * qualityMultiplier * expandRatio,
|
||||
scaleY: (maskPath.scaleY || 1) * qualityMultiplier * expandRatio,
|
||||
fill: "#ffffff",
|
||||
stroke: "",
|
||||
strokeWidth: 0,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 描边已转换为填充,类型: ${maskPath.type}`);
|
||||
} else {
|
||||
// 没有描边,直接处理位置和缩放
|
||||
maskPath.set({
|
||||
left: (maskPath.left - selectionBounds.left) * qualityMultiplier,
|
||||
top: (maskPath.top - selectionBounds.top) * qualityMultiplier,
|
||||
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
|
||||
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
|
||||
fill: "#ffffff", // 白色表示可见区域
|
||||
stroke: "", // 确保没有描边
|
||||
strokeWidth: 0,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 确保对象在画布中心正确对齐
|
||||
maskPath.setCoords();
|
||||
|
||||
return maskPath;
|
||||
} catch (error) {
|
||||
console.error("创建实体遮罩路径失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 优化边界计算,确保遮罩和内容对齐
|
||||
* @param {Object} clippingObject 裁剪对象
|
||||
* @param {Array} fabricObjects 内容对象数组
|
||||
* @returns {Object} 优化后的边界框信息
|
||||
*/
|
||||
const calculateOptimizedBounds = (clippingObject, fabricObjects) => {
|
||||
try {
|
||||
console.log("📐 计算优化后的边界框");
|
||||
|
||||
// 获取裁剪对象的边界框(包含描边)
|
||||
const clippingBounds = clippingObject.getBoundingRect(true, true);
|
||||
|
||||
// 如果有描边,需要调整边界框
|
||||
if (clippingObject.stroke && clippingObject.strokeWidth > 0) {
|
||||
const strokeWidth = clippingObject.strokeWidth;
|
||||
const halfStroke = strokeWidth / 2;
|
||||
|
||||
// 扩展边界框以包含完整的描边区域
|
||||
clippingBounds.left -= halfStroke;
|
||||
clippingBounds.top -= halfStroke;
|
||||
clippingBounds.width += strokeWidth;
|
||||
clippingBounds.height += strokeWidth;
|
||||
|
||||
console.log(`🖊️ 调整描边边界框,描边宽度: ${strokeWidth}`);
|
||||
}
|
||||
|
||||
// 计算内容对象的边界框
|
||||
const contentBounds = calculateBounds(fabricObjects);
|
||||
|
||||
// 使用裁剪边界框作为最终的选区边界框
|
||||
const optimizedBounds = {
|
||||
...clippingBounds,
|
||||
// 确保边界框不为负数或零
|
||||
width: Math.max(1, clippingBounds.width),
|
||||
height: Math.max(1, clippingBounds.height),
|
||||
};
|
||||
|
||||
console.log("✅ 边界框优化完成", {
|
||||
original: clippingObject.getBoundingRect(true, true),
|
||||
optimized: optimizedBounds,
|
||||
hasStroke: !!(clippingObject.stroke && clippingObject.strokeWidth > 0),
|
||||
});
|
||||
|
||||
return optimizedBounds;
|
||||
} catch (error) {
|
||||
console.error("计算优化边界框失败:", error);
|
||||
// 返回原始计算方式作为备选
|
||||
return clippingObject.getBoundingRect(true, true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ const canvasEditor = ref();
|
||||
const currentView = ref("canvasEditor"); // 默认显示红绿图示例 canvasEditor redGreenExample
|
||||
|
||||
const clothingImageUrl = "/src/assets/work/3.PNG";
|
||||
const clothingImageUrlInit = "/src/assets/work/5.PNG";
|
||||
|
||||
const imageData = [
|
||||
{
|
||||
@@ -76,12 +77,17 @@ const changeCanvas = (command) => {
|
||||
console.log(command);
|
||||
};
|
||||
|
||||
const changeImageUrl = "/src/assets/work/1.PNG";
|
||||
const loadImageUrlToLayer = async () => {
|
||||
try {
|
||||
const imageUrl =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABrQAAAZNCAYAAACENbGaAAAAAXNSR0IArs4c6QAAIABJREFUeF7s3Yt208YaBtCflkuhhXLv+z/e4X6HQiFnfbYmTIwDsRM7srS91ixJY8WW9kjjVh8jXS";
|
||||
const layerId = await canvasEditor?.value?.addImageToLayer?.(imageUrl,{layerId:canvasEditor?.value.layers[0].id});
|
||||
// const layerId = await canvasEditor?.value?.addImageToLayer?.(imageUrl);
|
||||
const layerToLayer1 = canvasEditor?.value?.layers?.[0]?.id;
|
||||
const layerId = await canvasEditor?.value?.addImageToLayer?.(
|
||||
changeImageUrl,
|
||||
{
|
||||
layerId: layerToLayer1, // 指定添加到的图层ID
|
||||
imageMode: "contains", // 设置图片包含在画布内
|
||||
}
|
||||
);
|
||||
console.log("新图层ID:", layerId);
|
||||
} catch (error) {
|
||||
console.error("加载图片到图层失败:", error);
|
||||
@@ -145,13 +151,19 @@ const handleCustomToolClick = (tool) => {
|
||||
tool.action();
|
||||
};
|
||||
|
||||
const changeImageUrl = "/src/assets/work/1.PNG";
|
||||
|
||||
const changeFixedImage = () => {
|
||||
canvasEditor.value.changeFixedImage(changeImageUrl, {
|
||||
imageMode: "contains", // 设置底图包含在画布内
|
||||
});
|
||||
};
|
||||
|
||||
const canvasInit = () => {
|
||||
console.log("画布初始化完成");
|
||||
// 可以在这里执行一些初始化逻辑
|
||||
canvasEditor.value.changeFixedImage(clothingImageUrlInit, {
|
||||
imageMode: "contains", // 设置底图包含在画布内
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -176,6 +188,7 @@ const changeFixedImage = () => {
|
||||
imageMode: 'contains', // 设置底图包含在画布内
|
||||
}"
|
||||
@change-canvas="changeCanvas"
|
||||
@canvas-init="canvasInit"
|
||||
isFixedErasable
|
||||
showFixedLayer
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user