画布增加的新功能
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -688,7 +688,6 @@ export class CanvasEventManager {
|
||||
this.layerManager.commandManager.execute(transformCmd, {
|
||||
name: "对象修改",
|
||||
});
|
||||
|
||||
// 清除临时状态记录
|
||||
delete activeObj._initialTransformState;
|
||||
}
|
||||
|
||||
@@ -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 " ":
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -310,7 +310,7 @@ export class LiquifyWebGLManager {
|
||||
this.isHolding = true;
|
||||
|
||||
// 启动持续效果定时器
|
||||
this.startContinuousEffect();
|
||||
// this.startContinuousEffect();
|
||||
|
||||
console.log(`WebGL液化开始,初始点: (${x}, ${y})`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user