画布增加的新功能

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

@@ -69,7 +69,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
layer.clippingMask,
this.canvas
);
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({ absolutePositioned: true });
this.newFill = new fabric.Rect({
width: clippingMaskFabricObject.width,
@@ -117,7 +117,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
this.parent.clippingMask,
this.canvas
);
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({ absolutePositioned: true });
this.newFill = new fabric.Rect({
width: clippingMaskFabricObject.width,
@@ -222,7 +222,7 @@ export class FillGroupLayerBackgroundCommand extends Command {
this.parent?.clippingMask,
this.canvas
);
clipPath.clipPath = null;
// clipPath.clipPath = null;
clipPath.set({ absolutePositioned: true });
this.group.clipPath = clipPath;
}

View File

@@ -56,7 +56,7 @@ export class FillLayerBackgroundCommand extends Command {
layer.clippingMask,
this.canvas
);
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
// 设置绝对定位

View File

@@ -0,0 +1,301 @@
import { Command } from "./Command";
import { findLayerRecursively } from "../utils/layerHelper";
import { fabric } from "fabric-with-all";
import {
findObjectById,
generateId,
insertObjectAtZIndex,
removeCanvasObjectByObject,
createPatternTransform,
} from "../utils/helper";
import { restoreFabricObject } from "../utils/objectHelper";
const scale = 0.3;// 默认缩放比例
export const FillSourceToBase64 = (source) => {
if (source?.toDataURL) {
return source.toDataURL?.();
} else if (source?.src) {
return source.src;
}
return source;
}
/**
* 填充图案平铺命令
* 填充重复属性repeat | repeat-x | repeat-y | no-repeat
* 默认缩放比例0.3
* 默认偏移量50%
*/
export class FillRepeatCommand extends Command {
constructor(options) {
super({ name: "填充图案平铺", saveState: true });
this.canvas = options.canvas;
this.layers = options.layers;
this.canvasManager = options.canvasManager;
this.layerManager = options.layerManager;
this.layerId = options.layerId;
this.fillRepeat = options.fillRepeat;
this.oldObjects = null;
this.oldLocked = null;
this.oldIsDisableUnlock = null;
}
async execute() {
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
console.warn("图层不存在或没有 fabric 对象");
return false;
}
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
if (!object || (object.type !== "rect" && object.type !== "image")) {
console.warn("当前对象不能平铺", object.type);
return false;
}
this.oldObjects = object;
const img = await new Promise((resolve, reject) => {
if (object.type === "rect") {
let source = object.fill.source;
resolve(source);
} else if (object.type === "image") {
// resolve(object.getElement());
// fabric.Image.fromURL(
// object.src,
// v => resolve(v),
// { crossOrigin: "anonymous" }
// );
const imgElement = object.getElement();
// 创建透明 Canvas
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);
}
});
const fill_ = {
source: FillSourceToBase64(img),
gapX: 0,
gapY: 0,
};
const bgObject = this.canvasManager.getBackgroundLayerObject();
const pattern = new fabric.Pattern({
source: img,
repeat: this.fillRepeat,
patternTransform: object.fill?.hasOwnProperty("patternTransform") ? object.fill.patternTransform : createPatternTransform(scale, 0),
offsetX: object.fill?.hasOwnProperty("offsetX") ? object.fill.offsetX : bgObject.width / 2, // 水平偏移
offsetY: object.fill?.hasOwnProperty("offsetY") ? object.fill.offsetY : bgObject.height / 2, // 垂直偏移
});
const rect = new fabric.Rect({
id: object.id,
layerId: object.layerId,
layerName: object.layerName,
fill_,
});
layer.fabricObjects = [rect.toObject(["id", "layerId", "layerName"])];
this.oldLocked = layer.locked;
// this.oldIsDisableUnlock = layer.isDisableUnlock;
// layer.isDisableUnlock = true;
if (this.oldObjects.type === "rect") {
rect.set({
width: object.width,
height: object.height,
top: object.top,
left: object.left,
originX: object.originX,
originY: object.originY,
angle: object.angle,
scaleX: object.scaleX,
scaleY: object.scaleY,
flipX: object.flipX,
flipY: object.flipY,
});
} else {
rect.set({
width: bgObject.width,
height: bgObject.height,
top: bgObject.top,
left: bgObject.left,
originX: bgObject.originX,
originY: bgObject.originY,
});
layer.locked = true;
}
rect.set("fill", pattern);
this.canvas.add(rect);
this.canvas.remove(object);
await this.layerManager?.updateLayersObjectsInteractivity();
await this.layerManager?.sortLayersWithTool?.();
await this.canvasManager.thumbnailManager?.generateLayerThumbnail(
this.layerId
);
await this.layerManager.selectLayerObjects(this.layerId);
return true;
}
async undo() {
if (!this.oldObjects) {
console.warn("没有旧对象可恢复");
return false;
}
const { layer } = findLayerRecursively(this.layers.value, this.oldObjects.layerId);
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
console.warn("图层不存在或没有 fabric 对象");
return false;
}
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
this.canvas.remove(object);
this.canvas.add(this.oldObjects);
layer.fabricObjects = [this.oldObjects.toObject(["id", "layerId", "layerName"])];
layer.locked = this.oldLocked;
// layer.isDisableUnlock = this.oldIsDisableUnlock;
await this.layerManager?.updateLayersObjectsInteractivity();
await this.layerManager?.sortLayersWithTool?.();
this.canvas.renderAll();
this.canvasManager.thumbnailManager?.generateLayerThumbnail(this.layerId);
return true;
}
}
/**
* 填充图案更改参数
*/
export class FillRepeatChangeCommand extends Command {
constructor(options) {
super({ name: "填充图案更改参数", saveState: true });
this.canvas = options.canvas;
this.layers = options.layers;
this.canvasManager = options.canvasManager;
this.layerManager = options.layerManager;
this.layerId = options.layerId;
this.newPattern = options.newPattern;
this.oldPattern = null;
}
async execute() {
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
console.warn("图层不存在或没有 fabric 对象");
return false;
}
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
if (!object || object.type !== "rect") {
console.warn("当前对象不是矩形", object);
return false;
}
this.oldPattern = object.oldPattern || object.get("fill");
delete object.oldPattern;
const oldPattern = { ...this.oldPattern };
delete oldPattern.id;
const pattern = new fabric.Pattern({
...oldPattern,
...this.newPattern,
});
object.set("fill", pattern);
this.canvas.renderAll();
return true;
}
async undo() {
if (!this.oldPattern) {
console.warn("没有旧图案可恢复");
return false;
}
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
console.warn("图层不存在或没有 fabric 对象");
return false;
}
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
if (!object || object.type !== "rect") {
console.warn("当前对象不是矩形", object);
return false;
}
const pattern = new fabric.Pattern({
...this.oldPattern
});
object.set("fill", pattern);
this.canvas.renderAll();
return true;
}
}
/**
* 填充图案更改间隙
*/
export class FillRepeatGapChangeCommand extends Command {
constructor(options) {
super({ name: "填充图案更改间隙", saveState: true });
this.canvas = options.canvas;
this.layers = options.layers;
this.canvasManager = options.canvasManager;
this.layerManager = options.layerManager;
this.layerId = options.layerId;
this.newGapX = options.newGapX;
this.newGapY = options.newGapY;
this.record = !!options.record;
this.oldGapX = null;
this.oldGapY = null;
}
async execute(isUndo = false) {
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
if (!layer || !layer.fabricObjects || layer.fabricObjects.length === 0) {
console.warn("图层不存在或没有 fabric 对象");
return false;
}
const { object } = findObjectById(this.canvas, layer?.fabricObjects?.[0]?.id);
if (!object || object.type !== "rect") {
console.warn("当前对象不是矩形", object);
return false;
}
if (!object.fill_) {
object.fill_ = {
source: FillSourceToBase64(object.fill.source),
gapX: 0,
gapY: 0,
}
}
if (isUndo) {
object.fill_.gapX = this.oldGapX;
object.fill_.gapY = this.oldGapY;
} else {
if (!object.oldFill_ && this.record) {
object.oldFill_ = { ...object.fill_ };
}
this.oldGapX = object.fill_.gapX;
this.oldGapY = object.fill_.gapY;
object.fill_.gapX = this.newGapX;
object.fill_.gapY = this.newGapY;
}
const image = new Image();
image.src = object.fill_.source;
await image.decode();
// 创建透明 Canvas
const tcanvas = document.createElement('canvas');
tcanvas.width = image.width + object.fill_.gapX;
tcanvas.height = image.height + object.fill_.gapY;
const ctx = tcanvas.getContext('2d');
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(image, 0, 0);
const fill = object.get("fill");
fill.source = tcanvas;
object.set("fill", new fabric.Pattern(fill));
this.canvas.renderAll();
return true;
}
async undo() {
if (this.oldGapX === null || this.oldGapY === null) {
console.warn("没有旧间隙可恢复");
return false;
}
await this.execute(true);
return true;
}
}

View File

@@ -10,6 +10,7 @@ import { AddObjectToLayerCommand } from "./ObjectLayerCommands";
import { ToolCommand } from "./ToolCommands";
import {
findObjectById,
findObjectByLayerId,
generateId,
getObjectZIndex,
insertObjectAtZIndex,
@@ -19,7 +20,7 @@ import {
} from "../utils/helper";
import { fabric } from "fabric-with-all";
import { restoreFabricObject } from "../utils/objectHelper";
import EventManager from "../utils/event.js";
/**
* 添加图层命令
*/
@@ -36,7 +37,7 @@ export class AddLayerCommand extends Command {
this.insertIndex = options.insertIndex;
this.oldActiveLayerId = null;
this.beforeLayers = [...this.layers.value]; // 备份原图层列表
this.beforeLayers = JSON.stringify(this.layers.value); // 备份原图层列表
this.options = options.options || {};
}
@@ -70,7 +71,7 @@ export class AddLayerCommand extends Command {
undo() {
// 从图层列表删除该图层
this.layers.value = [...this.beforeLayers];
this.layers.value = JSON.parse(this.beforeLayers);
// 恢复原活动图层
this.activeLayerId.value = this.oldActiveLayerId;
@@ -251,12 +252,12 @@ export class PasteLayerCommand extends Command {
(await restoreFabricObject(groupLayer?.clippingMask, this.canvas)) ||
null;
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
clippingMaskFabricObject.dirty = true;
// clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
// 添加所有对象到画布
allObjects.forEach((obj) => {
@@ -802,15 +803,23 @@ export class ToggleLayerVisibilityCommand extends Command {
// 切换可见性
this.layer.visible = !this.layer.visible;
const ids = [this.layerId];
const childLayers = this.layer?.children || [];
childLayers.forEach((childLayer) => {
childLayer.visible = this.layer.visible;
ids.push(childLayer.id);
});
// 更新画布上图层对象的可见性
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.visible = this.layer.visible;
});
this.canvas.getObjects().forEach((obj) => {
if (ids.includes(obj.layerId)) {
obj.getObjects?.()?.forEach((item) => {
item.visible = this.layer.visible;
});
obj.visible = this.layer.visible;
}
});
}
// 更新画布上对象的可选择状态
await this.layerManager?.updateLayersObjectsInteractivity();
@@ -868,13 +877,14 @@ export class ToggleChildLayerVisibilityCommand extends Command {
// 更新画布上图层对象的可见性
if (this.canvas) {
const layerObjects = this.canvas
.getObjects()
.filter((obj) => obj.layerId === this.layerId);
layerObjects.forEach((obj) => {
obj.visible = this.childLayer.visible;
});
this.canvas.getObjects().forEach((obj) => {
if (obj.layerId === this.layerId) {
obj.getObjects?.()?.forEach((item) => {
item.visible = this.childLayer.visible;
});
obj.visible = this.childLayer.visible;
}
});
}
// 更新画布上对象的可选择状态
@@ -1007,9 +1017,8 @@ export class LayerLockCommand extends Command {
// 如果是组图层,递归更新所有子图层
if (
layer.type === "group" &&
layer.children &&
Array.isArray(layer.children)
Array.isArray(layer.children) && layer.children.length > 0
) {
layer.children.forEach((child) => {
this._updateLayerLockState(child, locked);
@@ -1108,7 +1117,7 @@ export class SetLayerOpacityCommand extends Command {
this.canvas.renderAll();
}
EventManager.emit("object:opacity:execute", this.layerId, this.opacity);
return true;
}
@@ -1130,6 +1139,7 @@ export class SetLayerOpacityCommand extends Command {
this.canvas.renderAll();
}
}
EventManager.emit("object:opacity:undo", this.layerId, this.opacity);
}
getInfo() {
@@ -1371,7 +1381,7 @@ export class GroupLayersCommand extends Command {
// 备份原图层
this.originalLayers = [...this.layers.value];
// 新组ID
this.groupId =
this.groupId = options.id ||
generateId("group_layer_") ||
`group_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
@@ -4434,3 +4444,87 @@ export class ChildLayerLockCommand extends Command {
};
}
}
/**
* 设置图层混合模式
*/
export class SetLayerCompositeCommand extends Command {
constructor(options) {
super({
name: "设置图层混合模式",
saveState: false,
});
this.canvas = options.canvas;
this.layers = options.layers;
this.layerManager = options.layerManager;
this.layerId = options.layerId;
this.newValue = options.newValue;
this.oldValue = options.oldValue;
}
execute(isUndo = false) {
const { layer } = findLayerRecursively(this.layers.value, this.layerId);
const { object } = findObjectByLayerId(this.canvas, this.layerId);
if (!layer || !object) {
console.error(`图层${this.layerId}不存在`);
return false;
}
// console.log("==========", this.newValue, this.oldValue);
const value = isUndo ? this.oldValue : this.newValue;
layer.blendMode = value;
object.set("globalCompositeOperation", value);
this.canvas.renderAll();
const event = isUndo ? "object:composite:undo" : "object:composite:execute";
EventManager.emit(event, object);
return true;
}
undo() {
return this.execute(true);
}
}
/**
* 设置颜色图层颜色
*/
export class SetColorLayerFillCommand extends Command {
constructor(options) {
super({
name: "设置颜色图层颜色",
saveState: false,
});
this.canvas = options.canvas;
this.layerManager = options.layerManager;
this.object = options.object;
this.layer = this.layerManager?.getLayerById(this.object.layerId);
this.newFill = options.newFill;
this.oldFill = JSON.parse(JSON.stringify(this.object.fill));
}
async execute(isUndo = false) {
if (!this.object) {
console.error(`颜色图层不存在`);
return false;
}
const isVisible = this.layer?.visible;
if(!isVisible && this.layer) this.layer.visible = true;
const gradient = new fabric.Gradient({
type: "linear",
gradientUnits: "percentage",
...(isUndo ? this.oldFill : this.newFill),
});
this.object.setFill(gradient);
this.canvas.renderAll();
await this.canvas?.thumbnailManager?.generateLayerThumbnail?.(
this.object.id
);
if(!isVisible && this.layer) this.layer.visible = false;
this.layerManager?.updateLayersObjectsInteractivity();
return true;
}
undo() {
this.execute(true);
return true;
}
}

View File

@@ -0,0 +1,50 @@
import { Command } from "./Command.js";
/**
* 对象移动命令
* 轻量级命令,只记录对象的移动属性变化(位置)
*/
export class ObjectMoveCommand extends Command {
constructor(options) {
super({
name: options.name || "对象移动",
description: options.description || "移动对象",
saveState: false, // 自己管理状态,避免递归
});
this.canvas = options.canvas;
this.initPos = options.initPos;
this.finalPos = options.finalPos;
}
/**
* 执行命令
*/
async execute() {
this.setObjectsPos(this.finalPos);
return true;
}
/**
* 撤销命令
* 应用初始状态
*/
async undo() {
this.setObjectsPos(this.initPos);
return true;
}
async setObjectsPos(pos) {
const objects = this.canvas.getObjects();
const arr = typeof pos === "object" ? [pos] : pos;
arr.forEach((item) => {
const obj = objects.find((o) => o.id === item.id);
if(obj) {
obj.set({
left: item.left,
top: item.top,
});
}
});
this.canvas.renderAll();
}
}

View File

@@ -234,7 +234,7 @@ export class AddObjectToLayerCommand extends Command {
parent.clippingMask,
this.canvas
);
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({ absolutePositioned: true });
this.fabricObject.clipPath = clippingMaskFabricObject;
// 标记为脏对象

View File

@@ -46,13 +46,13 @@ export class RasterizeLayerCommand extends Command {
this.layerId
);
this.layer = layer;
this.parentLayer = parent;
// this.parentLayer = parent;
// 新增:如果有父图层,则栅格化父图层及其所有子图层
if (this.parentLayer) {
this.layer = this.parentLayer;
this.layerId = this.parentLayer.id;
}
// // 新增:如果有父图层,则栅格化父图层及其所有子图层
// if (this.parentLayer) {
// this.layer = this.parentLayer;
// this.layerId = this.parentLayer.id;
// }
this.isGroupLayer = this.layer?.children && this.layer.children.length > 0;
@@ -153,7 +153,7 @@ export class RasterizeLayerCommand extends Command {
});
// 恢复原始图层结构
this.layers.value = [...this.originalLayerStructure];
this.layers.value = JSON.parse(this.originalLayerStructure);
// 恢复原活动图层
this.activeLayerId.value = this.layerId;
@@ -191,7 +191,7 @@ export class RasterizeLayerCommand extends Command {
*/
_saveOriginalLayerStructure() {
// 只保存相关的图层结构,而不是整个图层数组
this.originalLayerStructure = JSON.parse(JSON.stringify(this.layers.value));
this.originalLayerStructure = JSON.stringify(this.layers.value);
}
/**
@@ -517,12 +517,12 @@ export class ExportLayerToImageCommand extends Command {
this.canvas
);
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});
clippingMaskFabricObject.dirty = true;
// clippingMaskFabricObject.dirty = true;
clippingMaskFabricObject.setCoords();
}

View File

@@ -2,6 +2,7 @@ import { findObjectById } from "../utils/helper";
import { findLayerRecursively } from "../utils/layerHelper";
import { restoreFabricObject } from "../utils/objectHelper";
import { Command } from "./Command";
import EventManager from "../utils/event.js";
/**
* 对象变换命令
@@ -75,7 +76,7 @@ export class TransformCommand extends Command {
// 触发画布更新
this.canvas.renderAll();
EventManager.emit("object:modified:execute", targetObject);
return true;
}
@@ -113,7 +114,7 @@ export class TransformCommand extends Command {
}, 300);
// 触发画布更新
this.canvas.renderAll();
EventManager.emit("object:modified:undo", targetObject);
return true;
}
@@ -167,7 +168,7 @@ export class TransformCommand extends Command {
);
if (clippingMaskFabricObject) {
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});

View File

@@ -233,7 +233,7 @@ export class UpdateGroupMaskPositionCommand extends Command {
return;
}
clippingMaskFabricObject.clipPath = null;
// clippingMaskFabricObject.clipPath = null;
clippingMaskFabricObject.set({
absolutePositioned: true,
});