// 栅格化帮助 import { fabric } from "fabric-with-all"; import { createStaticCanvas } from "./canvasFactory"; /** * 创建栅格化图像 * 使用组对象方式,避免边界计算误差 * @returns {Promise} 栅格化后的图像对象 * @private */ export const createRasterizedImage = async ({ canvas, // 画布对象 必填 fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填 maskObject = null, // 用于裁剪的对象 - 可选 trimWhitespace = true, // 是否裁剪空白区域 trimPadding = 0, // 裁剪边距 quality = 1.0, // 图像质量 format = "png", // 图像格式 scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数 isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象 isThumbnail = false, // 是否为缩略图 preserveOriginalQuality = true, // 是否保持原始质量 isGroupWithMask = false, // 是否为带遮罩的组图层 } = {}) => { try { // console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象${maskObject ? "(带遮罩)" : ""}`); // 确保有对象需要栅格化 if (fabricObjects.length === 0) { console.warn("⚠️ 没有对象需要栅格化,返回空图像"); return null; } // 高清倍数 const currentZoom = canvas.getZoom?.() || 1; scaleFactor = Math.max(scaleFactor || canvas?.getRetinaScaling?.(), currentZoom); if (isThumbnail) scaleFactor = 0.2; // 缩略图使用较小的高清倍数 // console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`); // 如果有遮罩且保持原始质量,使用高质量的遮罩处理方法 if (maskObject && preserveOriginalQuality) { const rasterizedImage = await createRasterizedImageWithMask({ canvas, objects: fabricObjects, maskObject, scaleFactor, quality, format, currentZoom, isReturenDataURL, isGroupWithMask, }); if (!rasterizedImage) { console.warn("⚠️ 带遮罩的栅格化图像创建失败,返回空图像"); return null; } if (isReturenDataURL) { // console.log("✅ 带遮罩的栅格化图像创建成功,返回DataURL"); return rasterizedImage; } // 设置栅格化图像的属性 rasterizedImage.set({ selectable: true, evented: true, hasControls: true, hasBorders: true, custom: { type: "rasterized", rasterizedAt: new Date().toISOString(), objectCount: fabricObjects.length, originalZoom: currentZoom, hasMask: true, }, }); // console.log(`✅ 带遮罩的栅格化图像创建完成`); return rasterizedImage; } // 使用原有的组对象方式创建栅格化图像 const rasterizedImage = await createRasterizedImageWithGroup({ canvas, objects: fabricObjects, scaleFactor, maskObject, trimWhitespace, trimPadding, quality, format, currentZoom, isReturenDataURL, }); if (!rasterizedImage) { console.warn("⚠️ 栅格化图像创建失败,返回空图像"); return null; } if (isReturenDataURL) { // console.log("✅ 栅格化图像创建成功,返回DataURL"); return rasterizedImage; } // 设置栅格化图像的属性 if (rasterizedImage) { rasterizedImage.set({ selectable: true, evented: true, hasControls: true, hasBorders: true, custom: { type: "rasterized", rasterizedAt: new Date().toISOString(), objectCount: fabricObjects.length, originalZoom: currentZoom, }, }); // console.log(`✅ 栅格化图像创建完成`); } return rasterizedImage; } catch (error) { console.error("创建栅格化图像失败:", error); throw new Error(`栅格化失败: ${error.message}`); } }; /** * 使用组对象方式创建栅格化图像 * @param {Object} options 渲染选项 * @returns {Promise} 栅格化后的图像对象 */ const createRasterizedImageWithGroup = async ({ canvas, objects, scaleFactor, maskObject, trimWhitespace, trimPadding, quality, format, currentZoom, isReturenDataURL, }) => { try { // 创建离屏画布 const offscreenCanvas = createStaticCanvas(); // 克隆所有对象 const clonedObjects = []; for (const obj of objects) { const clonedObj = await cloneObjectAsync(obj); clonedObj.set({ select: false, evented: false, hasControls: false, clipPath: null, // 确保克隆对象没有clipPath }); clonedObjects.push(clonedObj); } // 创建组对象 const group = new fabric.Group(clonedObjects, { originX: "left", originY: "top", }); // 获取组的绝对边界框 const groupBounds = group.getBoundingRect(true, true); // console.log("📏 组边界框:", groupBounds); // 设置离屏画布尺寸,使用组的边界大小 const canvasWidth = Math.ceil(groupBounds.width * scaleFactor); const canvasHeight = Math.ceil(groupBounds.height * scaleFactor); offscreenCanvas.setDimensions({ width: canvasWidth, height: canvasHeight, hasControls: false, }); // console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`); // 调整组的位置,让它位于画布的左上角 group.set({ left: 0, top: 0, }); // 取消对象激活 group.set({ selectable: false, // 禁用组的选择 evented: false, // 禁用组的事件 }); // 将组添加到离屏画布 offscreenCanvas.add(group); // 设置离屏画布的缩放 offscreenCanvas.setZoom(scaleFactor); // 如果有遮罩对象,应用遮罩 if (maskObject) { await applyMaskToCanvas(offscreenCanvas, maskObject, groupBounds); } // 渲染离屏画布 offscreenCanvas.renderAll(); // 生成图像数据 const dataURL = offscreenCanvas.toDataURL({ format, quality, multiplier: 1, // 已经通过画布尺寸处理了高清倍数 }); if (isReturenDataURL) { // 清理离屏画布 offscreenCanvas.dispose(); return dataURL; // 如果需要返回DataURL } // 清理离屏画布 offscreenCanvas.dispose(); // 创建fabric.Image对象 const fabricImage = await createFabricImageFromDataURL(dataURL); // 设置图像的位置和缩放,使其与原始组的位置和大小匹配 fabricImage.set({ scaleX: 1 / scaleFactor, // 由于我们生成的图像是高清版本,需要缩放回原始大小 scaleY: 1 / scaleFactor, left: groupBounds.left + groupBounds.width / 2, // 设置为组中心点 top: groupBounds.top + groupBounds.height / 2, // 设置为组中心点 originX: "center", originY: "center", }); return fabricImage; } catch (error) { console.error("组对象栅格化失败:", error); throw error; } }; /** * 使用遮罩创建栅格化图像 - 专门处理带遮罩的组图层 * 基于遮罩的位置和大小来裁剪内容,确保正确的定位 * @param {Object} options 渲染选项 * @returns {Promise} 栅格化后的图像对象 */ const createRasterizedImageWithMask = async ({ canvas, objects, maskObject, scaleFactor, quality, format, currentZoom, isReturenDataURL, isGroupWithMask, }) => { try { // console.log("🎭 使用遮罩创建栅格化图像"); // 获取遮罩的边界框,这将作为最终图像的边界 const maskBounds = maskObject.getBoundingRect(true, true); // console.log("📏 遮罩边界框:", maskBounds); // 克隆所有对象,并清除它们的遮罩,避免重复应用 const clonedObjects = []; for (const obj of objects) { const clonedObj = await cloneObjectAsync(obj); clonedObj.set({ select: false, evented: false, hasControls: false, clipPath: null, // 清除clipPath,避免重复应用遮罩 }); clonedObjects.push(clonedObj); } // 克隆遮罩对象用于裁剪 const clonedMask = await cloneObjectAsync(maskObject); clonedMask.set({ select: false, evented: false, hasControls: false, absolutePositioned: false, // 设置为绝对定位 // fill: "#ffffff", // 遮罩使用白色 // stroke: "", // 确保没有描边 // strokeWidth: 0, }); clonedMask.clipPath = null; // 确保克隆的遮罩没有clipPath // 创建离屏画布,使用遮罩的边界大小 const offscreenCanvas = createStaticCanvas(); const canvasWidth = Math.ceil(maskBounds.width * scaleFactor); const canvasHeight = Math.ceil(maskBounds.height * scaleFactor); offscreenCanvas.setDimensions({ width: canvasWidth, height: canvasHeight, }); // console.log(`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`); // 调整对象位置,相对于遮罩边界重新定位 clonedObjects.forEach((obj) => { obj.set({ left: (obj.left - maskBounds.left) * scaleFactor, top: (obj.top - maskBounds.top) * scaleFactor, scaleX: (obj.scaleX || 1) * scaleFactor, scaleY: (obj.scaleY || 1) * scaleFactor, }); obj.setCoords(); }); // 调整遮罩位置 clonedMask.set({ left: 0, // 遮罩从画布左上角开始 top: 0, scaleX: (clonedMask.scaleX || 1) * scaleFactor, scaleY: (clonedMask.scaleY || 1) * scaleFactor, originX: "left", originY: "top", absolutePositioned: true, // 设置为绝对定位 }); clonedMask.setCoords(); // 添加所有对象到离屏画布 clonedObjects.forEach((obj) => { offscreenCanvas.add(obj); }); // 使用遮罩作为画布的clipPath来裁剪内容 offscreenCanvas.clipPath = clonedMask; // 渲染离屏画布 offscreenCanvas.renderAll(); // 生成图像数据 const dataURL = offscreenCanvas.toDataURL({ format, quality, multiplier: 1, // 已经通过画布尺寸和对象缩放处理了高清倍数 }); if (isReturenDataURL) { // 清理离屏画布 offscreenCanvas.dispose(); return dataURL; } // 清理离屏画布 offscreenCanvas.dispose(); // 创建fabric.Image对象 const fabricImage = await createFabricImageFromDataURL(dataURL); // 设置图像的位置,使其与原始遮罩的位置匹配 fabricImage.set({ scaleX: 1 / scaleFactor, // 由于我们生成的图像是高清版本,需要缩放回原始大小 scaleY: 1 / scaleFactor, left: maskBounds.left + maskBounds.width / 2, // 设置为遮罩中心点 top: maskBounds.top + maskBounds.height / 2, // 设置为遮罩中心点 originX: "center", originY: "center", }); // 确保图像位置正确 fabricImage.setCoords(); // console.log("✅ 带遮罩的栅格化图像创建完成"); return fabricImage; } catch (error) { console.error("带遮罩的栅格化失败:", error); throw error; } }; /** * 异步克隆fabric对象 * @param {fabric.Object} obj 要克隆的对象 * @returns {Promise} 克隆的对象 */ const cloneObjectAsync = (obj) => { return new Promise((resolve, reject) => { try { obj?.clone?.((cloned) => { if (cloned) { resolve(cloned); } else { reject(new Error("对象克隆失败")); } }, {}); } catch (error) { reject(new Error(`克隆对象时发生错误: ${error.message}`)); } }); }; /** * 从DataURL创建fabric.Image对象 * @param {string} dataURL 图像数据URL * @returns {Promise} fabric图像对象 */ const createFabricImageFromDataURL = (dataURL) => { return new Promise((resolve, reject) => { fabric.Image.fromURL(dataURL, (img) => { if (img) { resolve(img); } else { reject(new Error("无法从DataURL创建图像")); } }); }); }; /** * 应用遮罩到画布(如果需要) * @param {fabric.Canvas} canvas 目标画布 * @param {fabric.Object} maskObject 遮罩对象 * @param {Object} bounds 边界框 */ const applyMaskToCanvas = async (canvas, maskObject, bounds) => { if (!maskObject) { // console.log("没有遮罩对象,跳过遮罩应用"); return; } try { // console.log("🎭 应用遮罩到画布"); // 克隆遮罩对象,避免影响原对象 const clonedMask = await cloneObjectAsync(maskObject); // 设置遮罩属性 clonedMask.set({ fill: "#ffffff", // 遮罩使用白色 stroke: "", // 确保没有描边 strokeWidth: 0, selectable: false, evented: false, absolutePositioned: true, // 设置为绝对定位 }); // 调整遮罩位置,相对于画布边界重新定位 clonedMask.set({ left: clonedMask.left - bounds.left, top: clonedMask.top - bounds.top, }); // 将遮罩设置为画布的clipPath canvas.clipPath = clonedMask; // console.log("✅ 遮罩应用完成"); } catch (error) { console.error("应用遮罩失败:", error); } }; /** * 获取对象组的边界框 * @param {Array} fabricObjects fabric对象数组 * @returns {Object} 边界框信息 */ export const getObjectsBounds = (fabricObjects) => { if (fabricObjects.length === 0) { return null; } // 创建临时组来获取准确的边界框 const tempGroup = new fabric.Group([...fabricObjects], { originX: "left", originY: "top", }); const bounds = tempGroup.getBoundingRect(true, true); // 清理临时组 tempGroup.destroy(); return bounds; };