diff --git a/src/assets/images/canvas/xiangao.png b/src/assets/images/canvas/xiangao.png new file mode 100644 index 00000000..89774ded Binary files /dev/null and b/src/assets/images/canvas/xiangao.png differ diff --git a/src/assets/images/canvas/xiangaofenge.png b/src/assets/images/canvas/xiangaofenge.png new file mode 100644 index 00000000..a7c53528 Binary files /dev/null and b/src/assets/images/canvas/xiangaofenge.png differ diff --git a/src/assets/images/canvas/yinhua1.jpg b/src/assets/images/canvas/yinhua1.jpg new file mode 100644 index 00000000..9cb7fb93 Binary files /dev/null and b/src/assets/images/canvas/yinhua1.jpg differ diff --git a/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js b/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js index 617c4ea4..923cddf9 100644 --- a/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js +++ b/src/component/Canvas/CanvasEditor/commands/FillRepeatCommand.js @@ -52,6 +52,7 @@ export class FillRepeatCommand extends Command { console.warn("当前对象不能平铺", object.type); return false; } + console.log("===========", object.toObject(["id", "layerId", "layerName"])) this.oldObjects = object; const img = await new Promise((resolve, reject) => { if (object.type === "rect") { diff --git a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js index 524b2e37..5f6335c3 100644 --- a/src/component/Canvas/CanvasEditor/commands/LayerCommands.js +++ b/src/component/Canvas/CanvasEditor/commands/LayerCommands.js @@ -4499,6 +4499,8 @@ export class SetColorLayerFillCommand extends Command { this.layer = this.layerManager?.getLayerById(this.object.layerId); this.newFill = options.newFill; this.oldFill = JSON.parse(JSON.stringify(this.object.fill)); + this.layer.blendMode = "multiply"; + this.object.set("globalCompositeOperation", "multiply"); } async execute(isUndo = false) { diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue index 7e1bd2f3..641c7e73 100644 --- a/src/component/Canvas/CanvasEditor/index.vue +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -884,6 +884,7 @@ const changeCanvas = async (command) => { ...command, // 传递完整的命令数据 }; emit("changeCanvas", commandData); + canvasManager.changeCanvas(commandData); if ((command.canUndo || command.canRedo) && props.enabledRedGreenMode) { setTimeout(async () => { try { diff --git a/src/component/Canvas/CanvasEditor/managers/CanvasManager.js b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js index eef846ec..5bec6c3f 100644 --- a/src/component/Canvas/CanvasEditor/managers/CanvasManager.js +++ b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js @@ -13,6 +13,7 @@ import { createLayer, LayerType, SpecialLayerId, + BlendMode, } from "../utils/layerHelper"; import { ObjectMoveCommand } from "../commands/ObjectCommands"; import { AnimationManager } from "./animation/AnimationManager"; @@ -30,6 +31,7 @@ import { fillToCssStyle, calculateRotatedTopLeftDeg, createPatternTransform, + base64ToCanvas, } from "../utils/helper"; import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands"; import { isFunction } from "lodash-es"; @@ -564,15 +566,14 @@ export class CanvasManager { } // 更新颜色层信息 - const fixedLayerObj = this.getFixedLayerObject(); - const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR); - if(colorObject && fixedLayerObj){ - await this.setColorObjectInfo(colorObject, fixedLayerObj); - } + // const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR); + // if(colorObject){ + // await this.setObjecCliptInfo(colorObject); + // } const groupLayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP); - if(groupLayer && fixedLayerObj){ + if(groupLayer){ const groupRect = new fabric.Rect({}); - await this.setColorObjectInfo(groupRect, fixedLayerObj); + await this.setObjecCliptInfo(groupRect); groupLayer.clippingMask = groupRect.toObject(); } @@ -908,6 +909,7 @@ export class CanvasManager { * @param {Object} options 导出选项 * @param {Boolean} options.isContainBg 是否包含背景图层 * @param {Boolean} options.isContainFixed 是否包含固定图层 + * @param {Boolean} options.isContainFixedOther 是否包含其他固定图层 * @param {String} options.layerId 导出具体图层ID * @param {Array} options.layerIdArray 导出多个图层ID数组 * @param {String} options.expPicType 导出图片类型 (png/jpg/svg) @@ -948,7 +950,7 @@ export class CanvasManager { const normalLayerIds = this.layers?.value ?.filter( - (layer) => !layer.isBackground && !layer.isFixed && layer.visible + (layer) => !layer.isBackground && !layer.isFixed && !layer.isFixedOther && layer.visible ) ?.map((layer) => layer.id) || []; @@ -1233,7 +1235,7 @@ export class CanvasManager { // console.log("图层关联验证结果:", isValidate); // 排序 // 使用LayerSort工具重新排列画布对象(如果可用) - await this?.layerManager?.layerSort?.rearrangeObjects(); + // await this?.layerManager?.layerSort?.rearrangeObjects(); this.layerManager.activeLayerId.value = this.layers.value[0] .children?.length @@ -1304,10 +1306,15 @@ export class CanvasManager { }) await this.createPrintTrimsLayers(printTrimsLayers, singleLayers); } + + await this.changeCanvas(); } - async setColorObjectInfo(colorRect, fixedLayerObj){ - colorRect.set({ + // 设置画布对象的裁剪信息 + async setObjecCliptInfo(tagObject, data){ + const fixedLayerObj = this.getFixedLayerObject(); + if(!fixedLayerObj) return console.warn("固定图层为空"); + tagObject.set({ top: fixedLayerObj.top, left: fixedLayerObj.left, width: fixedLayerObj.width, @@ -1322,7 +1329,7 @@ export class CanvasManager { if(imageUrl){ object = await new Promise((resolve, reject) => { fabric.Image.fromURL(imageUrl, (imgObject) => { - colorRect.set({ + tagObject.set({ width: imgObject.width, height: imgObject.height, }); @@ -1330,14 +1337,14 @@ export class CanvasManager { }, { crossOrigin: "anonymous" }); }); } - const canvas = getObjectAlphaToCanvas(object); + const canvas = getObjectAlphaToCanvas(object, data); const transparentMask = new fabric.Image(canvas, { top: 0, left: 0, originX: fixedLayerObj.originX, originY: fixedLayerObj.originY, }); - colorRect.set('clipPath', transparentMask); + tagObject.set('clipPath', transparentMask); } async createColorLayer(color){ if(!color) return console.warn("颜色为空不需要添加"); @@ -1351,8 +1358,9 @@ export class CanvasManager { layerName: t("Canvas.color"), isVisible: true, isLocked: true, + globalCompositeOperation: BlendMode.MULTIPLY, }); - await this.setColorObjectInfo(colorRect, fixedLayerObj); + // await this.setObjecCliptInfo(colorRect); const gradientObj = palletToFill(color); const gradient = new fabric.Gradient({ type: 'linear', @@ -1370,6 +1378,7 @@ export class CanvasManager { locked: colorRect.isLocked, opacity: 1.0, isFixedOther: true, + blendMode: BlendMode.MULTIPLY, fabricObjects: [colorRect.toObject(["id", "layerId", "layerName"])], }) const groupIndex = this.layers.value.findIndex(layer => layer.isFixed || layer.isBackground); @@ -1503,7 +1512,7 @@ export class CanvasManager { children.push(layer); } const groupRect = new fabric.Rect({}); - await this.setColorObjectInfo(groupRect, fixedLayerObj); + await this.setObjecCliptInfo(groupRect); // 插入组图层 const groupIndex = this.layers.value.findIndex(layer => layer.isFixedOther || layer.isFixed || layer.isBackground); const groupLayer = createLayer({ @@ -1521,6 +1530,34 @@ export class CanvasManager { this.layers.value.splice(groupIndex, 0, groupLayer); } + /** + * + */ + async changeCanvas(){ + const fixedLayerObj = this.getFixedLayerObject(); + if(!fixedLayerObj) return console.warn("固定图层对象不存在", fixedLayerObj) + const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR); + if(colorObject){ + const ids = this.layerManager.getBlendModeLayerIds(SpecialLayerId.SPECIAL_GROUP); + if(ids.length === 0){ + ids.unshift(SpecialLayerId.SPECIAL_GROUP); + await this.setObjecCliptInfo(colorObject); + return; + } + const base64 = await this.exportManager.exportImage({layerIdArray2: ids, isEnhanceImg: true}); + if(!base64) return console.warn("导出图片失败", base64) + const canvas = await base64ToCanvas(base64, fixedLayerObj.scaleX * 2, true); + const ctx = canvas.getContext('2d'); + const width = fixedLayerObj.width; + const height = fixedLayerObj.height; + const x = (canvas.width - width) / 2; + const y = (canvas.height - height) / 2; + const data = ctx.getImageData(x, y, width, height); + await this.setObjecCliptInfo(colorObject, data); + this.canvas.renderAll(); + } + } + /** * 缩放红绿图模式内容以适应当前画布大小 * 确保衣服底图和红绿图永远在画布内可见 diff --git a/src/component/Canvas/CanvasEditor/managers/ExportManager.js b/src/component/Canvas/CanvasEditor/managers/ExportManager.js index 58514887..0db49fb1 100644 --- a/src/component/Canvas/CanvasEditor/managers/ExportManager.js +++ b/src/component/Canvas/CanvasEditor/managers/ExportManager.js @@ -19,9 +19,11 @@ export class ExportManager { * @param {Object} options 导出选项 * @param {Boolean} options.isContainBg 是否包含背景图层 * @param {Boolean} options.isContainFixed 是否包含固定图层 + * @param {Boolean} options.isContainFixedOther 是否包含其他固定图层 * @param {Boolean} options.isCropByBg 是否使用背景大小裁剪 * @param {String} options.layerId 导出具体图层ID * @param {Array} options.layerIdArray 导出多个图层ID数组 + * @param {Array} options.layerIdArray2 导出多个图层ID数组2 * @param {String} options.expPicType 导出图片类型 (png/jpg/svg) * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} options.isEnhanceImg 是否是增强图片 @@ -31,20 +33,22 @@ export class ExportManager { const { isContainBg = false, isContainFixed = false, + isContainFixedOther = false, // 是否包含其他固定图层 isCropByBg = false, // 是否使用背景大小裁剪 layerId = "", layerIdArray = [], + layerIdArray2 = null, expPicType = "png", restoreOpacityInRedGreen = true, isEnhanceImg, // 是否是增强图片 } = options; try { // 查找颜色图层并隐藏 - const colorLayer = this.layerManager.getLayerById(SpecialLayerId.COLOR); - if (colorLayer && colorLayer.visible) { - colorLayer.visible = false; - await this.layerManager?.updateLayersObjectsInteractivity(); - } + // const colorLayer = this.layerManager.getLayerById(SpecialLayerId.COLOR); + // if (colorLayer && colorLayer.visible) { + // colorLayer.visible = false; + // await this.layerManager?.updateLayersObjectsInteractivity(); + // } // 检查是否为红绿图模式 const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false; @@ -67,6 +71,7 @@ export class ExportManager { expPicType, isContainBg, isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 isRedGreenMode, restoreOpacityInRedGreen, isCropByBg, @@ -79,10 +84,12 @@ export class ExportManager { expPicType, isContainBg, isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 isRedGreenMode, restoreOpacityInRedGreen, isCropByBg, isEnhanceImg, // 是否是增强图片 + layerIdArray2, ); } catch (error) { console.error("导出图片失败:", error); @@ -155,6 +162,7 @@ export class ExportManager { * @param {String} expPicType 导出类型 * @param {Boolean} isContainBg 是否包含背景图层 * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 * @param {Boolean} isRedGreenMode 是否为红绿图模式 * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} isCropByBg 是否使用背景大小裁剪 @@ -167,6 +175,7 @@ export class ExportManager { expPicType, isContainBg, isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 isRedGreenMode, restoreOpacityInRedGreen, isCropByBg, // 是否使用背景大小裁剪 @@ -180,7 +189,8 @@ export class ExportManager { const objectsToExport = this._collectObjectsByLayerOrder( layerIdArray, isContainBg, - isContainFixed + isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 ); if (objectsToExport.length === 0) { @@ -212,10 +222,12 @@ export class ExportManager { * @param {String} expPicType 导出类型 * @param {Boolean} isContainBg 是否包含背景图层 * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 * @param {Boolean} isRedGreenMode 是否为红绿图模式 * @param {Boolean} restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} isCropByBg 是否使用背景大小裁剪 * @param {Boolean} isEnhanceImg 是否是增强图片 + * @param {Array} layerIdArray 导出多个图层ID数组2 * @returns {String} 图片数据URL * @private */ @@ -223,16 +235,19 @@ export class ExportManager { expPicType, isContainBg, isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 isRedGreenMode, restoreOpacityInRedGreen, isCropByBg, // 是否使用背景大小裁剪 - isEnhanceImg, // 是否是增强图片 + isEnhanceImg, // 是否是增强图片 + layerIdArray, ) { // 按图层顺序收集对象(从底到顶) const objectsToExport = this._collectObjectsByLayerOrder( - null, // 导出所有图层 + layerIdArray, // 导出所有图层 isContainBg, isContainFixed, + isContainFixedOther, // 是否包含其他固定图层 ); if (objectsToExport.length === 0) { @@ -389,10 +404,11 @@ export class ExportManager { * @param {Array|null} layerIdArray 图层ID数组,null表示所有图层 * @param {Boolean} isContainBg 是否包含背景图层 * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 * @returns {Array} 按正确顺序排列的真实对象数组 * @private */ - _collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed) { + _collectObjectsByLayerOrder(layerIdArray, isContainBg, isContainFixed, isContainFixedOther) { const objectsToExport = []; const allLayers = this._getAllLayersFlattened(); // 获取扁平化的图层列表 @@ -404,7 +420,7 @@ export class ExportManager { if (layerIdArray && !layerIdArray.includes(layer.id)) continue; // 检查图层类型过滤条件 - if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) + if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther)) continue; if (layer.visible) { @@ -1019,10 +1035,11 @@ export class ExportManager { * @param {Object} layer 图层对象 * @param {Boolean} isContainBg 是否包含背景图层 * @param {Boolean} isContainFixed 是否包含固定图层 + * @param {Boolean} isContainFixedOther 是否包含其他固定图层 * @returns {Boolean} 是否应该包含 * @private */ - _shouldIncludeLayer(layer, isContainBg, isContainFixed) { + _shouldIncludeLayer(layer, isContainBg, isContainFixed, isContainFixedOther) { if (!layer) return false; // 检查背景图层 @@ -1035,6 +1052,11 @@ export class ExportManager { return isContainFixed; } + // 检查其他固定图层 + if (layer.isFixedOther) { + return isContainFixedOther; + } + // 普通图层总是包含 return true; } diff --git a/src/component/Canvas/CanvasEditor/managers/LayerManager.js b/src/component/Canvas/CanvasEditor/managers/LayerManager.js index 272c22fc..8f353d31 100644 --- a/src/component/Canvas/CanvasEditor/managers/LayerManager.js +++ b/src/component/Canvas/CanvasEditor/managers/LayerManager.js @@ -3436,4 +3436,22 @@ export class LayerManager { console.log("🎨 已设置组遮罩移动同步 - 使用 object:modified 事件"); } + + /** + * 获取印花和颜色图层设置了blendMode的图层ID + * @returns {string[]} - 包含blendMode的图层ID数组 + */ + getBlendModeLayerIds() { + const blendModeLayerIds = []; + this.layers.value.forEach(layer => { + if(layer.id === SpecialLayerId.SPECIAL_GROUP){ + layer.children.forEach(child => { + if(child.blendMode && child.blendMode !== BlendMode.NORMAL){ + blendModeLayerIds.push(child.id); + } + }); + } + }); + return blendModeLayerIds; + } } diff --git a/src/component/Canvas/CanvasEditor/utils/helper.js b/src/component/Canvas/CanvasEditor/utils/helper.js index 729298ad..20a8a614 100644 --- a/src/component/Canvas/CanvasEditor/utils/helper.js +++ b/src/component/Canvas/CanvasEditor/utils/helper.js @@ -959,3 +959,32 @@ export function getTransformScaleAngle(Transform) { const angle = Math.round(Math.atan2(b, a) * 180 / Math.PI); return { scale, angle }; } + +/** + * 图片转换为canvas + * @param {String} base64 - 图片base64编码 + * @param {Number} scale - 缩放比例 + * @param {Boolean} sr - 缩放反转,默认false + * @returns {Promise} canvas元素 +*/ +export async function base64ToCanvas(base64, scale = 1, sr = false) { + return new Promise((resolve, reject) => { + const image = new Image(); + image.src = base64; + image.crossOrigin = 'anonymous'; + image.onload = () => { + image.width = image.width; + image.height = image.height; + const canvas = document.createElement('canvas'); + const width = (sr ? image.width / scale : image.width * scale); + const height = sr ? image.height / scale : image.height * scale; + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, width, height); + ctx.drawImage(image, 0, 0, width, height); + resolve(canvas); + }; + image.onerror = reject; + }); +} diff --git a/src/component/Canvas/CanvasEditor/utils/objectHelper.js b/src/component/Canvas/CanvasEditor/utils/objectHelper.js index 96fd3986..8c8fb42e 100644 --- a/src/component/Canvas/CanvasEditor/utils/objectHelper.js +++ b/src/component/Canvas/CanvasEditor/utils/objectHelper.js @@ -61,11 +61,14 @@ export async function restoreFabricObject(serializedObject, canvas) { /** * 获取对象黑白通道画布 + * @param {fabric.Object} object - 要处理的 fabric 对象 + * @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明 + * @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败 */ -export function getObjectAlphaToCanvas(object) { +export function getObjectAlphaToCanvas(object, revData) { const image = object.getElement(); const { width, height } = image; - if(!width || !height){ + if (!width || !height) { console.warn("对象没有元素"); return null; } @@ -80,12 +83,23 @@ export function getObjectAlphaToCanvas(object) { const g = data.data[i + 1]; const b = data.data[i + 2]; const a = data.data[i + 3]; + const revR = revData?.data[i + 0] || 0; + const revG = revData?.data[i + 1] || 0; + const revB = revData?.data[i + 2] || 0; + const revA = revData?.data[i + 3] || 0; if (r || g || b || a) { - data.data[i + 0] = 255; - data.data[i + 1] = 255; - data.data[i + 2] = 255; - data.data[i + 3] = 255; - }else{ + if (revR || revG || revB || revA) { + data.data[i + 0] = 0; + data.data[i + 1] = 0; + data.data[i + 2] = 0; + data.data[i + 3] = 0; + } else { + data.data[i + 0] = 255; + data.data[i + 1] = 255; + data.data[i + 2] = 255; + data.data[i + 3] = 255; + } + } else { data.data[i + 0] = 0; data.data[i + 1] = 0; data.data[i + 2] = 0; diff --git a/src/component/Canvas/CanvasEditor/utils/selectionToImage.js b/src/component/Canvas/CanvasEditor/utils/selectionToImage.js index 34827db7..b1f019ef 100644 --- a/src/component/Canvas/CanvasEditor/utils/selectionToImage.js +++ b/src/component/Canvas/CanvasEditor/utils/selectionToImage.js @@ -184,10 +184,16 @@ const createClippedDataURLByCanvas = async ({ console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL"); // 使用优化后的边界计算,确保包含描边区域 - const optimizedBounds = calculateOptimizedBounds( - clippingObject, - fabricObjects - ); + // const optimizedBounds = calculateOptimizedBounds( + // clippingObject, + // fabricObjects + // ); + const optimizedBounds = { + left: clippingObject.left - clippingObject.width / 2, + top: clippingObject.top - clippingObject.height / 2, + width: clippingObject.width, + height: clippingObject.height, + } // 使用高分辨率以保证质量 const pixelRatio = window.devicePixelRatio || 1; diff --git a/src/component/Canvas/canvasExample.vue b/src/component/Canvas/canvasExample.vue index aebfa8ea..de88a03a 100644 --- a/src/component/Canvas/canvasExample.vue +++ b/src/component/Canvas/canvasExample.vue @@ -9,8 +9,8 @@ import ToolButton from "@/component/Canvas/ExistsImageList/ToolButton.vue"; const canvasEditor = ref(); const currentView = ref("canvasEditor"); // 默认显示红绿图示例 canvasEditor redGreenExample -const clothingImageUrl = "https://www.minio-api.aida.com.hk/aida-collection-element/24299/Printboard/4eba03bd-4367-4c69-b1a3-3f3177a1be1f.jpg?response-content-type=image%2Fjpeg&response-content-disposition=inline&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20251217%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251217T081126Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=b8223ba37625370c9716024536a08ce1ee5c5a7aaefc47d9baf8bd1e0e4d2d91"; -const clothingImageUrlInit = "/src/assets/work/5.PNG"; +const clothingImageUrl = "/src/assets/images/canvas/xiangao.png"; +const clothingImageUrlInit = "/src/assets/images/canvas/xiangaofenge.png"; const imageData = [ { @@ -71,7 +71,7 @@ const editorConfig = { const exportImage = async () => { if (canvasEditor.value) { const base64 = await canvasEditor.value.exportImage({ - isContainFixed: true, // 是否导出底图 + isContainFixed: false, // 是否导出底图 isContainBg: false, // 是否导出背景 }); @@ -272,6 +272,31 @@ const customToolsList = ref([ class: "export-btn", }, ]); +const otherData = { + color: {rgba: {r:255,g:0,b:0,a:1}}, + printObject: { + prints: [ + { + ifSingle: true, + level2Type: "Pattern", + designType: "Library", + path: "/src/assets/images/canvas/yinhua1.jpg", + location: [250, 780], + scale: [0.5 * 0.7, 0.272541 * 0.7], + angle: 0, + }, + { + ifSingle: true, + level2Type: "Pattern", + designType: "Library", + path: "/src/assets/images/canvas/yinhua1.jpg", + location: [300, 500], + scale: [0.5 * 0.4, 0.272541 * 0.4], + angle: 0, + } + ] + }, +}