画布增加的新功能

This commit is contained in:
李志鹏
2026-01-02 11:24:11 +08:00
parent 1ae365b1f3
commit f8e4ab8cdb
59 changed files with 4401 additions and 1213 deletions

View File

@@ -9,7 +9,12 @@ import {
isGroupLayer,
OperationType,
OperationTypes,
findLayer,
createLayer,
LayerType,
SpecialLayerId,
} from "../utils/layerHelper";
import { ObjectMoveCommand } from "../commands/ObjectCommands";
import { AnimationManager } from "./animation/AnimationManager";
import { createCanvas } from "../utils/canvasFactory";
import { CanvasEventManager } from "./events/CanvasEventManager";
@@ -21,6 +26,10 @@ import {
findObjectById,
generateId,
optimizeCanvasRendering,
palletToFill,
fillToCssStyle,
calculateRotatedTopLeftDeg,
createPatternTransform,
} from "../utils/helper";
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
import { isFunction } from "lodash-es";
@@ -30,6 +39,11 @@ import {
validateLayerAssociations,
} from "../utils/layerUtils";
import { imageModeHandler } from "../utils/imageHelper";
import { getObjectAlphaToCanvas } from "../utils/objectHelper";
import { AddLayerCommand } from "../commands/LayerCommands";
import { fa, id } from "element-plus/es/locales.mjs";
import i18n from "@/lang/index.ts";
const {t} = i18n.global;
export class CanvasManager {
constructor(canvasElement, options) {
@@ -50,6 +64,7 @@ export class CanvasManager {
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
this.eraserStateManager = null; // 橡皮擦状态管理器引用
this.handleCanvasInit = null; // 画布初始化回调函数
this.props = options.props || {};
// 初始化画布
this.initializeCanvas();
}
@@ -83,10 +98,10 @@ export class CanvasManager {
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
// // 设置画布辅助线
// initAligningGuidelines(this.canvas);
// 设置画布辅助线
initAligningGuidelines(this.canvas);
// // 设置画布中心线
// 设置画布中心线
// initCenteringGuidelines(this.canvas);
// 初始化画布事件监听器
@@ -431,7 +446,7 @@ export class CanvasManager {
* 以背景层为参照,计算背景层的偏移量并应用到所有对象上
* 这样可以保持对象间的相对位置关系不变
*/
centerAllObjects() {
async centerAllObjects() {
if (!this.canvas) return;
// 获取所有可见对象(不是背景元素的对象)
@@ -448,8 +463,8 @@ export class CanvasManager {
// 获取背景对象
const backgroundObject = visibleObjects.find((obj) => obj.isBackground);
!this.canvas?.clipPath &&
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
// !this.canvas?.clipPath &&
// this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
this.canvas?.clipPath?.set?.({
left: this.width / 2,
@@ -496,7 +511,6 @@ export class CanvasManager {
// 计算背景层的偏移量
const deltaX = backgroundObject.left - backgroundOldLeft;
const deltaY = backgroundObject.top - backgroundOldTop;
// 将相同的偏移量应用到所有其他对象上
const otherObjects = visibleObjects.filter(
(obj) => obj !== backgroundObject
@@ -549,8 +563,21 @@ export class CanvasManager {
this.updateMaskPosition(backgroundObject);
}
// 更新颜色层信息
const fixedLayerObj = this.getFixedLayerObject();
const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
if(colorObject && fixedLayerObj){
await this.setColorObjectInfo(colorObject, fixedLayerObj);
}
const groupLayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
if(groupLayer && fixedLayerObj){
const groupRect = new fabric.Rect({});
await this.setColorObjectInfo(groupRect, fixedLayerObj);
groupLayer.clippingMask = groupRect.toObject();
}
// 重新渲染画布
// this.canvas.renderAll();
this.canvas.renderAll();
}
/**
@@ -600,7 +627,7 @@ export class CanvasManager {
* @param {Number} canvasWidth 画布宽度
* @param {Number} canvasHeight 画布高度
*/
centerBackgroundLayer(canvasWidth, canvasHeight) {
async centerBackgroundLayer(canvasWidth, canvasHeight) {
const backgroundLayerObject = this.getBackgroundLayer();
if (!backgroundLayerObject) return false;
@@ -646,6 +673,11 @@ export class CanvasManager {
if (this.maskLayer) {
this.canvas.remove(this.maskLayer);
}
this.canvas.getObjects().forEach((obj) => {
if (obj.id === "canvasMaskLayer") {
this.canvas.remove(obj);
}
})
// 创建蒙层 - 使用透明矩形作为裁剪区域
this.maskLayer = new fabric.Rect({
@@ -706,6 +738,75 @@ export class CanvasManager {
return backgroundLayerByBgLayer;
}
getFixedLayerObject() {
if (!this.canvas) return null;
const fixedLayer = this.canvas.getObjects().find((obj) => {
return obj.isFixed;
});
if (fixedLayer) return fixedLayer;
// 如果没有找到固定层则根据图层ID查找
const fixedLayerId = this.layers.value.find((layer) => {
return layer.isFixed;
})?.id;
const fixedLayerByFixedLayer = this.canvas.getObjects().find((obj) => {
return obj.isFixed || obj.id === fixedLayerId;
});
if (!fixedLayerByFixedLayer) {
console.warn(
"CanvasManager.js = >getFixedLayerObject 方法没有找到固定层"
);
}
return fixedLayerByFixedLayer;
}
getBackgroundLayerObject() {
if (!this.canvas) return null;
const backgroundLayer = this.canvas.getObjects().find((obj) => {
return obj.isBackground;
});
if (backgroundLayer) return backgroundLayer;
// 如果没有找到背景层则根据图层ID查找
const backgroundLayerId = this.layers.value.find((layer) => {
return layer.isBackground;
})?.id;
const backgroundLayerByBgLayer = this.canvas.getObjects().find((obj) => {
return obj.isBackground || obj.id === backgroundLayerId;
});
if (!backgroundLayerByBgLayer) {
console.warn(
"CanvasManager.js = >getBackgroundLayerObject 方法没有找到背景层"
);
}
return backgroundLayerByBgLayer;
}
getLayerObjectById(layerId) {
if (!this.canvas) return null;
const layerObject = this.canvas.getObjects().find((obj) => {
return obj.id === layerId;
});
if (layerObject) return layerObject;
// 如果没有找到图层对象则根据图层ID查找
const layerObjectByLayerId = this.canvas.getObjects().find((obj) => {
return obj.id === layerId;
});
if (!layerObjectByLayerId) {
console.warn(
"CanvasManager.js = >getLayerObjectById 方法没有找到图层对象"
);
}
return layerObjectByLayerId;
}
/**
* 更新蒙层位置
* @param {Object} backgroundLayerObject 背景层对象
@@ -798,7 +899,7 @@ export class CanvasManager {
// 如果找到了图层,则生成缩略图
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
this.layerManager?.sortLayers?.();
return result;
}
@@ -812,6 +913,7 @@ export class CanvasManager {
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @param {Boolean} options.isEnhanceImg 是否是增强图片
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
* @returns {String} 导出的图片数据URL
*/
async exportImage(options = {}) {
@@ -857,11 +959,55 @@ export class CanvasManager {
}
return await this.exportManager.exportImage(enhancedOptions);
} catch (error) {
console.error("CanvasManager导出图片失败:", error);
console.warn("CanvasManager导出图片失败:", error);
throw error;
}
}
/**
* 导出颜色图层
* @returns {Object} 导出的颜色图层数据URL
*/
async exportColorLayer() {
if (!this.exportManager) {
console.warn("导出管理器未初始化,请确保已设置图层管理器");
return Promise.reject("颜色图层不存在");
}
const object = this.getLayerObjectById(SpecialLayerId.COLOR);
if(!object){
console.warn("颜色图层不存在,请确保已添加颜色图层");
return Promise.reject("颜色图层不存在");
}
const color = fillToCssStyle(object.fill)
const canvas = new fabric.StaticCanvas();
canvas.setDimensions({
width: object.width,
height: object.height,
backgroundColor: null,
// enableRetinaScaling: true,
imageSmoothingEnabled: true,
});
const cloneObject = await new Promise((resolve, reject) => {
object.clone(resolve);
});
cloneObject.set({
left: canvas.width / 2,
top: canvas.height / 2,
scaleX: 1,
scaleY: 1,
visible: true,
clipPath: null,
});
canvas.add(cloneObject);
canvas.renderAll();
const base64 = canvas.toDataURL({
format: "png",
quality: 1,
});
canvas.clear();
return {color, base64};
}
dispose() {
// 释放导出管理器资源
if (this.exportManager) {
@@ -956,14 +1102,13 @@ export class CanvasManager {
// };
try {
// 清除画布中选中状态
this.canvas.discardActiveObject();
// this.canvas.discardActiveObject();
this.canvas.renderAll();
const simplifyLayersData = simplifyLayers(
JSON.parse(JSON.stringify(this.layers.value))
);
console.log("获取画布JSON数据...", simplifyLayersData);
return JSON.stringify({
const data = JSON.stringify({
canvas: this.canvas.toJSON([
"id",
"type",
@@ -978,6 +1123,7 @@ export class CanvasManager {
"eraserable",
"erasable",
"customType",
"fill_",
]),
layers: simplifyLayersData, // 简化图层数据
// layers: JSON.stringify(JSON.parse(JSON.stringify(this.layers.value))), // 全数据
@@ -988,6 +1134,8 @@ export class CanvasManager {
canvasColor: this.canvasColor.value,
activeLayerId: this.layerManager?.activeLayerId?.value,
});
console.log("获取画布JSON数据...", data);
return data;
} catch (error) {
console.error("获取画布JSON失败:", error);
throw new Error("获取画布JSON失败");
@@ -1070,8 +1218,10 @@ export class CanvasManager {
// }
try {
// 重置画布数据
this.setCanvasSize(this.canvas.width, this.canvas.height);
this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
await this.setCanvasSize(this.canvas.width, this.canvas.height);
await this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
await this.createOtherLayers(this.props.otherData);
// 重新构建对象关系
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
// 验证图层关联关系 - 稳定后可以注释
@@ -1099,9 +1249,7 @@ export class CanvasManager {
await calllBack?.();
// 确保所有对象的交互性正确设置
await this.layerManager?.updateLayersObjectsInteractivity?.(
false
);
await this.layerManager?.updateLayersObjectsInteractivity?.();
console.log(this.layerManager.layers.value);
// 更新所有缩略图
@@ -1126,6 +1274,253 @@ export class CanvasManager {
}
}
/**
* 创建其他图层:印花、颜色、元素...
* @param {Object} otherData - 其他图层数据
*/
async createOtherLayers(otherData) {
if (!otherData) return console.warn("otherData 为空不需要添加");
const otherData_ = JSON.parse(JSON.stringify(otherData));
console.log("==========创建其他图层", otherData_);
// 创建颜色图层
await this.createColorLayer(otherData_.color);
if(findLayer(this.layers.value, SpecialLayerId.SPECIAL_GROUP)){
console.warn("画布中已存在印花和元素组图层");
}else{
const printTrimsLayers = [];// 印花和元素图层
const singleLayers = [];// 平铺图层
otherData_?.printObject?.prints?.forEach((print, index) => {
print.name = t("Canvas.Print") + (index + 1);
if(print.ifSingle){
printTrimsLayers.unshift({...print});
}else{
singleLayers.unshift({...print});
}
})
otherData_?.trims?.prints?.forEach((print, index) => {
print.name = t("Canvas.Elements") + (index + 1);
printTrimsLayers.unshift({...print});
})
await this.createPrintTrimsLayers(printTrimsLayers, singleLayers);
}
}
async setColorObjectInfo(colorRect, fixedLayerObj){
colorRect.set({
top: fixedLayerObj.top,
left: fixedLayerObj.left,
width: fixedLayerObj.width,
height: fixedLayerObj.height,
originX: fixedLayerObj.originX,
originY: fixedLayerObj.originY,
scaleX: fixedLayerObj.scaleX,
scaleY: fixedLayerObj.scaleY,
});
var object = fixedLayerObj;
const imageUrl = this.props.clothingImageUrl2;
if(imageUrl){
object = await new Promise((resolve, reject) => {
fabric.Image.fromURL(imageUrl, (imgObject) => {
colorRect.set({
width: imgObject.width,
height: imgObject.height,
});
resolve(imgObject);
}, { crossOrigin: "anonymous" });
});
}
const canvas = getObjectAlphaToCanvas(object);
const transparentMask = new fabric.Image(canvas, {
top: 0,
left: 0,
originX: fixedLayerObj.originX,
originY: fixedLayerObj.originY,
});
colorRect.set('clipPath', transparentMask);
}
async createColorLayer(color){
if(!color) return console.warn("颜色为空不需要添加");
if(findLayer(this.layers.value, SpecialLayerId.COLOR)) return console.warn("画布中已存在颜色图层");
console.log("==========添加颜色图层", color, this.layers.value.length)
const fixedLayerObj = this.getFixedLayerObject();
// 创建颜色图层对象
const colorRect = new fabric.Rect({
id: SpecialLayerId.COLOR,
layerId: SpecialLayerId.COLOR,
layerName: t("Canvas.color"),
isVisible: true,
isLocked: true,
});
await this.setColorObjectInfo(colorRect, fixedLayerObj);
const gradientObj = palletToFill(color);
const gradient = new fabric.Gradient({
type: 'linear',
gradientUnits: 'percentage',
...gradientObj,
})
colorRect.set('fill', gradient);
this.canvas.add(colorRect);
// 创建颜色图层
const colorLayer = createLayer({
id: colorRect.layerId,
name: colorRect.layerName,
type: LayerType.SHAPE,
visible: colorRect.isVisible,
locked: colorRect.isLocked,
opacity: 1.0,
isFixedOther: true,
fabricObjects: [colorRect.toObject(["id", "layerId", "layerName"])],
})
const groupIndex = this.layers.value.findIndex(layer => layer.isFixed || layer.isBackground);
this.layers.value.splice(groupIndex, 0, colorLayer);
}
// 创建印花和元素图层
async createPrintTrimsLayers(printTrimsLayers, singleLayers){
console.log("==========添加印花和元素图层组", printTrimsLayers, singleLayers)
const fixedLayerObj = this.getFixedLayerObject();
const flWidth = fixedLayerObj.width
const flHeight = fixedLayerObj.height
const flTop = fixedLayerObj.top
const flLeft = fixedLayerObj.left
const flScaleX = fixedLayerObj.scaleX
const flScaleY = fixedLayerObj.scaleY
const children = [];
// 添加印花和元素图层
for(let index = 0; index < printTrimsLayers.length; index++){
let print = printTrimsLayers[index];
let id = generateId("layer_image_");
let name = print.name;
let image = await new Promise(resolve => {
fabric.Image.fromURL(print.path, (fabricImage)=>{
const left = flLeft - flWidth * flScaleX / 2 + (print.location?.[0] || 0) * flScaleX
const top = flTop - flHeight * flScaleY / 2 + (print.location?.[1] || 0) * flScaleY
const scaleX = flWidth * (print.scale?.[0] || 1) / fabricImage.width * flScaleX
const scaleY = flHeight * (print.scale?.[1] || 1) / fabricImage.height * flScaleY
const {x, y} = calculateRotatedTopLeftDeg(
fabricImage.width * scaleX,
fabricImage.height * scaleY,
left,
top,
0,
print.angle || 0
)
const angle = print.angle || 0
fabricImage.set({
left: x,
top: y,
scaleX: scaleX,
scaleY: scaleY,
angle: angle,
id: id,
layerId: id,
layerName: name,
selectable: true,
hasControls: true,
hasBorders: true,
});
resolve(fabricImage);
}, { crossOrigin: "anonymous" });
})
this.canvas.add(image);
let layer = createLayer({
id: id,
name: name,
type: LayerType.BITMAP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [image.toObject(["id", "layerId", "layerName"])],
})
children.push(layer);
};
// 添加平铺图层
for(let index = 0; index < singleLayers.length; index++){
let print = singleLayers[index];
let id = generateId("layer_image_");
let name = print.name;
let image = await new Promise(resolve => {
fabric.Image.fromURL(print.path, (fabricImage)=>{
const imgElement = fabricImage.getElement();
const tcanvas = document.createElement('canvas');
tcanvas.width = imgElement.width;
tcanvas.height = imgElement.height;
const ctx = tcanvas.getContext('2d');
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(imgElement, 0, 0);
resolve(tcanvas);
}, { crossOrigin: "anonymous" });
})
console.log("==========添加平铺图层", fixedLayerObj.width,image.width)
let scaleX = fixedLayerObj.width / image.width * (print.scale?.[0] || 1) / 5;
let scaleY = fixedLayerObj.height / image.height * (print.scale?.[1] || 1) / 5;
let scale = fixedLayerObj.width > fixedLayerObj.height ? scaleX : scaleY;
let left = (print.location?.[0] || 0) - image.width * scale / 2
let top = (print.location?.[1] || 0) - image.height * scale / 2
let rect = new fabric.Rect({
id: id,
layerId: id,
layerName: name,
width: fixedLayerObj.width,
height: fixedLayerObj.height,
top: fixedLayerObj.top,
left: fixedLayerObj.left,
scaleX: fixedLayerObj.scaleX,
scaleY: fixedLayerObj.scaleY,
originX: fixedLayerObj.originX,
originY: fixedLayerObj.originY,
fill: new fabric.Pattern({
source: image,
repeat: "repeat",
patternTransform: createPatternTransform(scale, print.angle || 0),
offsetX: left, // 水平偏移
offsetY: top, // 垂直偏移
}),
});
this.canvas.add(rect);
let layer = createLayer({
id: id,
name: name,
type: LayerType.BITMAP,
visible: true,
locked: true,
opacity: 1,
fabricObjects: [rect.toObject(["id", "layerId", "layerName"])],
})
children.push(layer);
};
if(children.length === 0){
let layer = createLayer({
id: generateId("layer_image_"),
name: t("Canvas.EmptyLayer"),
type: LayerType.BITMAP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [],
})
children.push(layer);
}
const groupRect = new fabric.Rect({});
await this.setColorObjectInfo(groupRect, fixedLayerObj);
// 插入组图层
const groupIndex = this.layers.value.findIndex(layer => layer.isFixedOther || layer.isFixed || layer.isBackground);
const groupLayer = createLayer({
id: SpecialLayerId.SPECIAL_GROUP,
name: t("Canvas.PrintAndElementsGroup"),
type: LayerType.GROUP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [],
children: children,
clippingMask: groupRect.toObject(),
isFixedClipMask: true,
});
this.layers.value.splice(groupIndex, 0, groupLayer);
}
/**
* 缩放红绿图模式内容以适应当前画布大小
* 确保衣服底图和红绿图永远在画布内可见
@@ -1249,6 +1644,7 @@ export class CanvasManager {
return fixedLayer.fabricObject || null;
}
/**
* 获取所有普通图层对象(包括红绿图)
* @returns {Array} 普通图层对象数组
@@ -1315,4 +1711,46 @@ export class CanvasManager {
return sizeMatch && positionMatch;
}
/**
* 键盘移动激活对象
* @param {String} direction 移动方向up, down, left, right
* @param {<Number>} step 移动步长
* @private
*/
moveActiveObject(direction, step = 1) {
const objects = [];
const activeObject = this.canvas.getActiveObject();
if(!activeObject) return;
const initPos = {
id: activeObject.id,
left: activeObject.left,
top: activeObject.top,
};
switch(direction) {
case "up":
activeObject.top -= step;
break;
case "down":
activeObject.top += step;
break;
case "left":
activeObject.left -= step;
break;
case "right":
activeObject.left += step;
break;
}
if(!activeObject.id) return this.canvas.renderAll();
const cmd = new ObjectMoveCommand({
canvas: this.canvas,
initPos,
finalPos: {
id: activeObject.id,
left: activeObject.left,
top: activeObject.top,
},
});
this.commandManager.executeCommand(cmd);
}
}

View File

@@ -1,6 +1,7 @@
import { fabric } from "fabric-with-all";
import { findObjectById } from "../utils/helper";
import { createRasterizedImage } from "../utils/selectionToImage";
import { OperationType, SpecialLayerId } from "../utils/layerHelper";
/**
* 图片导出管理器
@@ -18,7 +19,7 @@ export class ExportManager {
* @param {Object} options 导出选项
* @param {Boolean} options.isContainBg 是否包含背景图层
* @param {Boolean} options.isContainFixed 是否包含固定图层
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
* @param {Boolean} options.isCropByBg 是否使用背景大小裁剪
* @param {String} options.layerId 导出具体图层ID
* @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
@@ -26,7 +27,7 @@ export class ExportManager {
* @param {Boolean} options.isEnhanceImg 是否是增强图片
* @returns {String} 导出的图片数据URL
*/
exportImage(options = {}) {
async exportImage(options = {}) {
const {
isContainBg = false,
isContainFixed = false,
@@ -35,9 +36,16 @@ export class ExportManager {
layerIdArray = [],
expPicType = "png",
restoreOpacityInRedGreen = true,
isEnhanceImg, // 是否是增强图片
isEnhanceImg, // 是否是增强图片
} = options;
try {
// 查找颜色图层并隐藏
const colorLayer = this.layerManager.getLayerById(SpecialLayerId.COLOR);
if (colorLayer && colorLayer.visible) {
colorLayer.visible = false;
await this.layerManager?.updateLayersObjectsInteractivity();
}
// 检查是否为红绿图模式
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
// 如果指定了具体图层ID导出指定图层
@@ -48,7 +56,7 @@ export class ExportManager {
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg,
isEnhanceImg, // 是否是增强图片
isEnhanceImg, // 是否是增强图片
);
}
@@ -62,7 +70,7 @@ export class ExportManager {
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg,
isEnhanceImg, // 是否是增强图片
isEnhanceImg, // 是否是增强图片
);
}
@@ -74,7 +82,7 @@ export class ExportManager {
isRedGreenMode,
restoreOpacityInRedGreen,
isCropByBg,
isEnhanceImg, // 是否是增强图片
isEnhanceImg, // 是否是增强图片
);
} catch (error) {
console.error("导出图片失败:", error);
@@ -128,8 +136,6 @@ export class ExportManager {
objectsToExport,
expPicType,
restoreOpacityInRedGreen,
isCropByBg, // 是否使用背景大小裁剪
isEnhanceImg, // 是否是增强图片
);
}
@@ -555,37 +561,22 @@ export class ExportManager {
);
}
// 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息)
const fixedBounds = fixedLayerObject?.getBoundingRect?.();
// 使用固定图层的实际显示尺寸作为导出画布尺寸
const canvasWidth = Math.round(fixedBounds.width);
const canvasHeight = Math.round(fixedBounds.height);
const canvasWidth = (fixedLayerObject.width);
const canvasHeight = (fixedLayerObject.height);
console.log(`红绿图模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
console.log("固定图层边界:", fixedBounds);
// 创建固定尺寸的临时画布
const scaleFactor = 2; // 高清导出
const tempCanvas = document.createElement("canvas");
tempCanvas.width = canvasWidth * scaleFactor;
tempCanvas.height = canvasHeight * scaleFactor;
tempCanvas.style.width = canvasWidth + "px";
tempCanvas.style.height = canvasHeight + "px";
const tempFabricCanvas = new fabric.StaticCanvas(tempCanvas, {
const tempFabricCanvas = new fabric.StaticCanvas()
tempFabricCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
backgroundColor: null,
enableRetinaScaling: true,
// enableRetinaScaling: true,
imageSmoothingEnabled: true,
});
tempFabricCanvas.setZoom(1);
// tempFabricCanvas.setZoom(1);
console.log("==========", fixedLayerObject)
try {
// 获取裁剪路径对象(如果存在)
const clipPathObject = await this._getClipPathObject(fixedBounds);
// 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层
for (let i = 0; i < objectsToExport.length; i++) {
const obj = objectsToExport[i];
@@ -596,18 +587,16 @@ export class ExportManager {
if (cloned) {
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
cloned.set({
left: cloned.left - fixedBounds.left,
top: cloned.top - fixedBounds.top,
left: canvasWidth / 2,
top: canvasHeight / 2,
scaleX: cloned.scaleX / fixedLayerObject.scaleX,
scaleY: cloned.scaleY / fixedLayerObject.scaleY,
originX: "center",
originY: "center",
});
console.log("==========", {...cloned})
// 更新对象坐标
cloned.setCoords();
// 设置裁剪路径到对象
if (clipPathObject) {
cloned.clipPath = clipPathObject;
}
tempFabricCanvas.add(cloned);
}
}
@@ -616,7 +605,7 @@ export class ExportManager {
tempFabricCanvas.renderAll();
// 生成图片
return this._generateHighQualityDataURL(tempCanvas, expPicType);
return this._generateHighQualityDataURL(tempFabricCanvas, expPicType);
} finally {
this._cleanupTempCanvas(tempFabricCanvas);
}
@@ -736,7 +725,7 @@ export class ExportManager {
*/
_cloneObjectAsync(
obj,
propertiesToInclude = ["id", "layerId", "layerName", "name"]
propertiesToInclude = ["id", "layerId", "layerName", "name", "scaleX", "scaleY"]
) {
return new Promise((resolve, reject) => {
if (!obj) {

View File

@@ -31,6 +31,7 @@ import {
} from "../commands/ObjectLayerCommands";
import {
LayerType,
SpecialLayerId,
BlendMode,
createLayer,
createBackgroundLayer,
@@ -343,35 +344,36 @@ export class LayerManager {
});
// 批量更新对象
objects.forEach(async (obj) => {
const layer = layerMap[obj.layerId];
for(let obj of objects){
let layer = layerMap[obj.layerId];
if (!obj.layerId) {
// 没有关联图层的对象使用默认设置
obj.selectable = false;
obj.evented = false;
obj.erasable = false; // 未关联图层的对象不可擦除
return;
break;
}
if (!layer) return;
if (!layer) break;
// 设置一级图层对象的交互性
await this._setObjectInteractivity(obj, layer, editorMode);
// 设置子图层对象的交互性
layer?.children?.forEach(async (childLayer) => {
const childObj = this.canvas
for(let childLayer of layer.children){
let childObj = this.canvas
.getObjects()
.find((o) => o.layerId === childLayer.id);
if (childObj) {
await this._setObjectInteractivity(childObj, childLayer, editorMode);
}
});
});
};
};
// 设置裁剪对象
layers.forEach(async (layer) => {
for(let layer of layers){
if(layer.id === SpecialLayerId.COLOR) break;
let clippingMaskFabricObject = null;
if (layer.clippingMask) {
// 反序列化 clippingMask
@@ -379,7 +381,7 @@ export class LayerManager {
layer.clippingMask,
this.canvas
);
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
// 设置绝对定位
@@ -403,7 +405,7 @@ export class LayerManager {
.find((o) => o.layerId === childLayer.id);
if (childObj) {
childObj.clipPath = clippingMaskFabricObject;
childObj.dirty = true; // 标记为脏对象
// childObj.dirty = true; // 标记为脏对象
childObj.setCoords();
}
@@ -499,7 +501,7 @@ export class LayerManager {
isOldSelectObject
);
}
});
};
}
/**
@@ -952,18 +954,28 @@ export class LayerManager {
// 查找要删除的图层
const { layer, parent } = findLayerRecursively(this.layers.value, layerId);
// 如果是背景层或固定层,不允许删除
if (layer && (layer.isBackground || layer.isFixed)) {
if (layer && (layer.isBackground || layer.isFixed || layer.isFixedOther)) {
console.warn(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
message.warning(layer.isBackground ? "背景层不可删除" : "固定层不可删除");
message.warning(layer.isBackground ? this.t("Canvas.backLayerCannotDelete") : this.t("Canvas.fixedLayerCannotDelete"));
return false;
}
// 检查是否是唯一的普通图层
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
var isChild = false;
var parentLength = 0;
const normalLayers = this.layers.value.filter((layer) => {
if(layer.children.some(v => v.id == layerId)){
isChild = true;
parentLength = layer.children.length;
}
return !layer.isFixed && !layer.isFixedOther && !layer.isBackground
})
// const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
console.log("普通图层:", normalLayers)
if (normalLayers.length === 1) {
if (isChild ? parentLength <= 1 : normalLayers.length <= 1) {
console.warn("不能删除唯一的普通图层");
message.warning("不能删除唯一的普通图层");
message.warning(this.t("Canvas.cannotDeleteOnlyLayer"));
return false;
}
// // 如果图层有子图层,提示确认
@@ -1132,7 +1144,7 @@ export class LayerManager {
return acc;
}, []);
console.log("==========", allObjects)
// if (layer.fill) {
// // 如果图层有填充颜色,设置所有对象的填充颜色
// const { object } = findObjectById(this.canvas, layer.fill.id);
@@ -1578,6 +1590,12 @@ export class LayerManager {
// 如果b是固定图层而a不是固定图层b应该排在后面固定图层在普通图层下方
if (b.isFixed && !a.isFixed) return -1;
// 如果a是固定图层而b不是固定图层a应该排在后面固定图层在普通图层下方
if (a.isFixedOther && !b.isFixedOther) return 1;
// 如果b是固定图层而a不是固定图层b应该排在后面固定图层在普通图层下方
if (b.isFixedOther && !a.isFixedOther) return -1;
// 其他情况保持原有顺序
return 0;
});
@@ -1848,9 +1866,9 @@ export class LayerManager {
}
// 检查是否是唯一的普通图层
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed);
const normalLayers = this.layers.value.filter((l) => !l.isBackground && !l.isFixed && !l.isFixedOther);
console.log("普通图层:", normalLayers)
if (normalLayers.length === 1) {
if (normalLayers.length <= 1) {
console.warn("不能剪切唯一的普通图层");
return null;
}
@@ -3250,7 +3268,7 @@ export class LayerManager {
* @private
*/
_setupGroupMaskMovementSync(activeSelection, layer) {
if (!activeSelection || !layer || !layer.clippingMask) {
if (!activeSelection || !layer || !layer.clippingMask || layer.isFixedClipMask) {
return;
}
@@ -3314,7 +3332,6 @@ export class LayerManager {
// 计算移动距离
const deltaX = target.left - initialLeft;
const deltaY = target.top - initialTop;
// 创建更新遮罩位置的命令
const command = new UpdateGroupMaskPositionCommand({
canvas: this.canvas,

View File

@@ -91,12 +91,12 @@ export class ThumbnailManager {
// 重新创建遮罩对象
clippingMaskFabricObject = await restoreFabricObject(layer?.clippingMask, this.canvas);
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
clippingMaskFabricObject.dirty = true;
// clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
}
@@ -128,8 +128,13 @@ export class ThumbnailManager {
}
const { layer } = findLayerRecursively(this.layers.value, layerId);
let layersToRasterize = [];
if (!layer) {
console.warn("⚠️ 无效的图层,无法收集对象");
return [];
}
let layersToRasterize = [];
if (layer.children && layer.children.length > 0) {
// 组图层:收集自身和所有子图层
layersToRasterize = this._collectLayersToRasterize(layer);

View File

@@ -69,7 +69,7 @@ export class AnimationManager {
// 如果变化太小,直接应用缩放
if (Math.abs(targetZoom - currentZoom) < 0.01) {
// this._applyZoom(point, targetZoom);
this._applyZoom(point, targetZoom);
return;
}
@@ -121,7 +121,7 @@ export class AnimationManager {
this._zoomAnimation = null;
// 确保最终状态准确
// this._applyZoom(point, targetZoom, true);
this._applyZoom(point, targetZoom, true);
},
};
@@ -173,7 +173,7 @@ export class AnimationManager {
this._zoomAnimation = null;
// 确保最终状态准确
// this._applyZoom(point, targetZoom, true);
this._applyZoom(point, targetZoom, true);
},
};
@@ -817,7 +817,7 @@ export class AnimationManager {
this._wasZooming = false;
// 确保最终状态准确
// this._applyZoom(point, targetZoom, true);
this._applyZoom(point, targetZoom, true);
},
});
}

View File

@@ -8,6 +8,7 @@ import { PerformanceManager } from "./PerformanceManager.js";
*/
export class CommandManager {
constructor(options = {}) {
this.canvas = options.canvas;
this.undoStack = [];
this.redoStack = [];
this.maxHistorySize = options.maxHistorySize || 50;
@@ -205,6 +206,7 @@ export class CommandManager {
const startTime = performance.now();
try {
this.canvas?.discardActiveObject();
const command = this.undoStack.pop();
console.log(`↩️ 撤销命令: ${command.constructor.name}`);
@@ -243,6 +245,7 @@ export class CommandManager {
const startTime = performance.now();
try {
this.canvas?.discardActiveObject();
const command = this.redoStack.pop();
console.log(`↪️ 重做命令: ${command.constructor.name}`);

View File

@@ -688,7 +688,6 @@ export class CanvasEventManager {
this.layerManager.commandManager.execute(transformCmd, {
name: "对象修改",
});
// 清除临时状态记录
delete activeObj._initialTransformState;
}

View File

@@ -10,6 +10,7 @@ export class KeyboardManager {
* @param {Object} options.toolManager 工具管理器实例
* @param {Object} options.commandManager 命令管理器实例
* @param {Object} options.layerManager 图层管理器实例
* @param {Object} options.canvasManager 画布管理器实例
* @param {Function} options.pasteText 粘贴文本回调函数
* @param {Function} options.pasteImage 粘贴图片回调函数
* @param {Ref<Boolean>} options.isRedGreenMode 是否为红绿模式
@@ -19,6 +20,7 @@ export class KeyboardManager {
this.toolManager = options.toolManager;
this.commandManager = options.commandManager;
this.layerManager = options.layerManager;
this.canvasManager = options.canvasManager;
this.container = options.container || document;
this.pasteText = options.pasteText || (() => {});
this.pasteImage = options.pasteImage || (() => {});
@@ -125,6 +127,10 @@ export class KeyboardManager {
// 删除
delete: { action: "delete", description: "删除" },
backspace: { action: "delete", description: "删除" },
up: { action: "up", description: "上" },
down: { action: "down", description: "下" },
left: { action: "left", description: "左" },
right: { action: "right", description: "右" },
// 选择
[`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" },
@@ -488,6 +494,14 @@ export class KeyboardManager {
}
break;
case "up":
case "down":
case "left":
case "right":
// 方向键逻辑
this.canvasManager.moveActiveObject(action);
break;
case "increaseBrushSize":
// 增大画笔尺寸
if (this.toolManager && this.toolManager.brushManager) {
@@ -639,7 +653,6 @@ export class KeyboardManager {
if (event.altKey) shortcutKey += "alt+";
const key = event.key.toLowerCase();
// 特殊键处理
switch (key) {
case " ":

View File

@@ -12,7 +12,7 @@ export class LiquifyCPUManager {
sharpenAmount: 0.3, // 添加锐化强度参数
...options,
};
console.log("CPU版本的液化管理器config",this.config);
console.log("CPU版本的液化管理器config", this.config);
this.params = {
size: 60, // 增大默认尺寸
@@ -63,7 +63,8 @@ export class LiquifyCPUManager {
// 新增:持续按压相关状态
this.pressStartTime = 0; // 按压开始时间
this.pressDuration = 0; // 按压持续时间
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)--废除使用固定角度
this.fixedRotationAngle = 0.32; // 固定旋转角度
this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开)
this.lastApplyTime = 0; // 上次应用时间
this.continuousApplyInterval = 50; // 持续应用间隔(毫秒)
@@ -189,7 +190,7 @@ export class LiquifyCPUManager {
this.isHolding = true;
// 启动持续效果定时器(对于所有模式都支持持续按压)
this.startContinuousEffect();
// this.startContinuousEffect();
console.log(`开始液化操作,初始点: (${x}, ${y})`);
}
@@ -220,7 +221,6 @@ export class LiquifyCPUManager {
// 新增:启动持续效果
startContinuousEffect() {
this.stopContinuousEffect(); // 先停止已有的定时器
this.continuousTimer = setInterval(() => {
if (this.isHolding && this.initialized) {
// 更新持续时间
@@ -273,7 +273,6 @@ export class LiquifyCPUManager {
*/
_applyEnhancedRotationDeformation(centerX, centerY, radius, strength, isClockwise) {
if (!this.currentImageData) return;
const data = this.currentImageData.data;
const width = this.currentImageData.width;
const height = this.currentImageData.height;
@@ -286,6 +285,7 @@ export class LiquifyCPUManager {
const rotationAngle =
(isClockwise ? 1 : -1) * baseRotationSpeed * pressure * power * (1.0 + timeFactor * 0.5);
console.log("持续应用旋转效果");
// 累积旋转角度 - 关键:这确保了持续旋转效果
this.accumulatedRotation += rotationAngle;
@@ -309,13 +309,14 @@ export class LiquifyCPUManager {
// 计算旋转后的源位置 - 关键算法
const angle = Math.atan2(dy, dx);
const newAngle = angle + this.accumulatedRotation * falloff;
// const newAngle = angle + this.accumulatedRotation * falloff;
const newAngle = angle + (isClockwise ? this.fixedRotationAngle : -this.fixedRotationAngle) * falloff;
const sourceX = centerX + Math.cos(newAngle) * distance;
const sourceY = centerY + Math.sin(newAngle) * distance;
// 双线性插值采样 - 确保像素连续性
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -376,7 +377,7 @@ export class LiquifyCPUManager {
const sourceY = centerY + dy * scale;
// 双线性插值采样
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -401,16 +402,17 @@ export class LiquifyCPUManager {
*/
_applyEnhancedPushDeformation(centerX, centerY, radius, strength) {
if (!this.currentImageData) return;
const data = this.currentImageData.data;
const width = this.currentImageData.width;
const height = this.currentImageData.height;
const tempData = new Uint8ClampedArray(data);
// 计算推拉方向
const deltaX = this.currentMouseX - this.initialMouseX;
const deltaY = this.currentMouseY - this.initialMouseY;
const deltaX = this.currentMouseX - this.lastMouseX;
const deltaY = this.currentMouseY - this.lastMouseY;
const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
this.lastMouseX = this.currentMouseX;
this.lastMouseY = this.currentMouseY;
const processRadius = Math.min(radius, Math.min(width, height) / 2);
const minX = Math.max(0, Math.floor(centerX - processRadius));
@@ -426,6 +428,7 @@ export class LiquifyCPUManager {
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
// 此处循环4万次
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
@@ -442,7 +445,7 @@ export class LiquifyCPUManager {
const sourceX = x - pushX;
const sourceY = y - pushY;
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -461,9 +464,9 @@ export class LiquifyCPUManager {
// 有拖拽时的推拉效果
const dirX = deltaX / dragLength;
const dirY = deltaY / dragLength;
for (let y = minY; y < maxY; y++) {
for (let x = minX; x < maxX; x++) {
// 此处循环4万次
const dx = x - centerX;
const dy = y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
@@ -473,13 +476,13 @@ export class LiquifyCPUManager {
const falloff = 1 - normalizedDistance * normalizedDistance;
const factor = falloff * strength;
const offsetX = dirX * factor * Math.min(dragLength * 0.3, 15);
const offsetY = dirY * factor * Math.min(dragLength * 0.3, 15);
const offsetX = dirX * factor * Math.min(dragLength * 2, 30);
const offsetY = dirY * factor * Math.min(dragLength * 2, 30);
const sourceX = x - offsetX;
const sourceY = y - offsetY;
const color = this._bilinearSample(tempData, width, height, sourceX, sourceY);
const color = this._bicubicInterpolate(tempData, width, height, sourceX, sourceY);
if (color) {
const targetIdx = (y * width + x) * 4;
@@ -527,7 +530,7 @@ export class LiquifyCPUManager {
break;
case this.modes.PUSH:
this._applyEnhancedPushDeformation(x, y, radius, strength);
// this._applyEnhancedPushDeformation(x, y, radius, strength);
break;
default: {
@@ -553,101 +556,7 @@ export class LiquifyCPUManager {
}
/**
* 应用液化变形 - 主要入口,集成增强算法
*/
// applyDeformation(x, y) {
// if (!this.initialized || !this.originalImageData) {
// console.warn("液化管理器未初始化或缺少必要数据");
// return this.currentImageData;
// }
// // 更新鼠标位置
// this.currentMouseX = x;
// this.currentMouseY = y;
// // 计算拖拽参数
// const deltaX = this.currentMouseX - this.initialMouseX;
// const deltaY = this.currentMouseY - this.initialMouseY;
// this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// this.dragAngle = Math.atan2(deltaY, deltaX);
// // 获取当前参数
// const { size, pressure, power } = this.params;
// const mode = this.currentMode;
// const radius = size;
// const strength = pressure * power;
// // 根据模式选择算法
// const pixelModes = [
// this.modes.CLOCKWISE,
// this.modes.COUNTERCLOCKWISE,
// this.modes.PINCH,
// this.modes.EXPAND,
// this.modes.PUSH,
// ];
// if (pixelModes.includes(mode)) {
// // 使用增强的像素算法
// switch (mode) {
// case this.modes.CLOCKWISE:
// this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
// break;
// case this.modes.COUNTERCLOCKWISE:
// this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
// break;
// case this.modes.PINCH:
// this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
// break;
// case this.modes.EXPAND:
// this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
// break;
// case this.modes.PUSH:
// this._applyEnhancedPushDeformation(x, y, radius, strength);
// break;
// }
// // 更新最后应用时间
// this.lastApplyTime = Date.now();
// this.isFirstApply = false;
// return this.currentImageData;
// } else {
// // 使用原有的网格算法处理其他模式
// if (!this.mesh) {
// console.warn("网格未初始化");
// return this.currentImageData;
// }
// const finalStrength = (strength * this.config.maxStrength) / 100;
// // 应用变形
// this._applyDeformation(
// x,
// y,
// radius,
// finalStrength,
// mode,
// this.params.distortion,
// );
// // 平滑处理
// if (this.config.smoothingIterations > 0) {
// this._smoothMesh();
// }
// // 更新图像数据
// const result = this._applyMeshToImage();
// // 更新最后应用时间
// this.lastApplyTime = Date.now();
// this.isFirstApply = false;
// return result;
// }
// }
/**
* 双线性插值采样 - 用于像素级算法
* 双线性插值函数
* @param {Uint8ClampedArray} data 图像数据
* @param {number} width 图像宽度
* @param {number} height 图像高度
@@ -655,19 +564,55 @@ export class LiquifyCPUManager {
* @param {number} y Y坐标
* @returns {Array|null} RGBA颜色值数组或null
*/
_bilinearSample(data, width, height, x, y) {
return this._bicubicInterpolate(data, width, height, x, y);
_bilinearInterpolate(data, width, height, x, y) {
const x1 = Math.floor(x);
const y1 = Math.floor(y);
const x2 = Math.min(width - 1, x1 + 1);
const y2 = Math.min(height - 1, y1 + 1);
const dx = x - x1;
const dy = y - y1;
const dx1 = 1 - dx;
const dy1 = 1 - dy;
const index1 = (y1 * width + x1) * 4;
const index2 = (y1 * width + x2) * 4;
const index3 = (y2 * width + x1) * 4;
const index4 = (y2 * width + x2) * 4;
const r =
data[index1] * dx1 * dy1 +
data[index2] * dx * dy1 +
data[index3] * dx1 * dy +
data[index4] * dx * dy;
const g =
data[index1 + 1] * dx1 * dy1 +
data[index2 + 1] * dx * dy1 +
data[index3 + 1] * dx1 * dy +
data[index4 + 1] * dx * dy;
const b =
data[index1 + 2] * dx1 * dy1 +
data[index2 + 2] * dx * dy1 +
data[index3 + 2] * dx1 * dy +
data[index4 + 2] * dx * dy;
const a =
data[index1 + 3] * dx1 * dy1 +
data[index2 + 3] * dx * dy1 +
data[index3 + 3] * dx1 * dy +
data[index4 + 3] * dx * dy;
return [Math.round(r), Math.round(g), Math.round(b), Math.round(a)];
}
/**
* 三次插值实现 - 确保正确处理Alpha通道
* @param {Uint8ClampedArray} data 图像数据
* @param {number} width 图像宽度
* @param {number} height 图像高度
* @param {number} x X坐标
* @param {number} y Y坐标
* @returns {Array|null} RGBA颜色值数组或null
*/
* 三次插值实现 - 确保正确处理Alpha通道
* @param {Uint8ClampedArray} data 图像数据
* @param {number} width 图像宽度
* @param {number} height 图像高度
* @param {number} x X坐标
* @param {number} y Y坐标
* @returns {Array|null} RGBA颜色值数组或null
*/
_bicubicInterpolate(data, width, height, x, y) {
// return this._bilinearInterpolate(data, width, height, x, y);
// 获取周围16个像素点
const x1 = Math.floor(x) - 1;
const y1 = Math.floor(y) - 1;

View File

@@ -310,7 +310,7 @@ export class LiquifyWebGLManager {
this.isHolding = true;
// 启动持续效果定时器
this.startContinuousEffect();
// this.startContinuousEffect();
console.log(`WebGL液化开始初始点: (${x}, ${y})`);
}