2025-06-22 13:52:28 +08:00
|
|
|
|
// 栅格化帮助
|
|
|
|
|
|
import { fabric } from "fabric-with-all";
|
2025-06-25 01:03:39 +08:00
|
|
|
|
import { createStaticCanvas } from "./canvasFactory";
|
2025-06-22 13:52:28 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 创建栅格化图像
|
2025-06-25 01:03:39 +08:00
|
|
|
|
* 使用组对象方式,避免边界计算误差
|
2025-06-22 13:52:28 +08:00
|
|
|
|
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
|
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const createRasterizedImage = async ({
|
|
|
|
|
|
canvas, // 画布对象 必填
|
|
|
|
|
|
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
|
|
|
|
|
|
maskObject = null, // 用于裁剪的对象 - 可选
|
|
|
|
|
|
trimWhitespace = true, // 是否裁剪空白区域
|
2025-06-25 01:03:39 +08:00
|
|
|
|
trimPadding = 0, // 裁剪边距
|
2025-06-22 13:52:28 +08:00
|
|
|
|
quality = 1.0, // 图像质量
|
|
|
|
|
|
format = "png", // 图像格式
|
|
|
|
|
|
scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数
|
|
|
|
|
|
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
|
|
|
|
|
} = {}) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
|
|
|
|
|
|
|
|
|
|
|
// 确保有对象需要栅格化
|
|
|
|
|
|
if (fabricObjects.length === 0) {
|
|
|
|
|
|
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 高清倍数
|
|
|
|
|
|
const currentZoom = canvas.getZoom?.() || 1;
|
|
|
|
|
|
scaleFactor = Math.max(
|
|
|
|
|
|
scaleFactor || canvas?.getRetinaScaling?.(),
|
|
|
|
|
|
currentZoom
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
|
|
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 使用组对象方式创建栅格化图像
|
|
|
|
|
|
const rasterizedImage = await createRasterizedImageWithGroup({
|
2025-06-22 13:52:28 +08:00
|
|
|
|
canvas,
|
|
|
|
|
|
objects: fabricObjects,
|
|
|
|
|
|
scaleFactor,
|
|
|
|
|
|
maskObject,
|
|
|
|
|
|
trimWhitespace,
|
|
|
|
|
|
trimPadding,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
format,
|
|
|
|
|
|
currentZoom,
|
|
|
|
|
|
isReturenDataURL,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!rasterizedImage) {
|
|
|
|
|
|
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isReturenDataURL) {
|
|
|
|
|
|
console.log("✅ 栅格化图像创建成功,返回DataURL");
|
|
|
|
|
|
return rasterizedImage; // 返回DataURL
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置栅格化图像的属性
|
|
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-06-25 01:03:39 +08:00
|
|
|
|
* 使用组对象方式创建栅格化图像
|
2025-06-22 13:52:28 +08:00
|
|
|
|
* @param {Object} options 渲染选项
|
|
|
|
|
|
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
|
|
|
|
|
*/
|
2025-06-25 01:03:39 +08:00
|
|
|
|
const createRasterizedImageWithGroup = async ({
|
2025-06-22 13:52:28 +08:00
|
|
|
|
canvas,
|
|
|
|
|
|
objects,
|
|
|
|
|
|
scaleFactor,
|
|
|
|
|
|
maskObject,
|
|
|
|
|
|
trimWhitespace,
|
|
|
|
|
|
trimPadding,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
format,
|
|
|
|
|
|
currentZoom,
|
|
|
|
|
|
isReturenDataURL,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
try {
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 创建离屏画布
|
|
|
|
|
|
const offscreenCanvas = createStaticCanvas();
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆所有对象
|
|
|
|
|
|
const clonedObjects = [];
|
|
|
|
|
|
for (const obj of objects) {
|
|
|
|
|
|
const clonedObj = await cloneObjectAsync(obj);
|
|
|
|
|
|
clonedObj.set({
|
|
|
|
|
|
select: false,
|
|
|
|
|
|
evented: false,
|
|
|
|
|
|
hasControls: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
clonedObjects.push(clonedObj);
|
|
|
|
|
|
}
|
2025-06-22 13:52:28 +08:00
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 创建组对象
|
|
|
|
|
|
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);
|
2025-06-22 13:52:28 +08:00
|
|
|
|
|
|
|
|
|
|
offscreenCanvas.setDimensions({
|
|
|
|
|
|
width: canvasWidth,
|
|
|
|
|
|
height: canvasHeight,
|
2025-06-25 01:03:39 +08:00
|
|
|
|
hasControls: false,
|
2025-06-22 13:52:28 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 调整组的位置,让它位于画布的左上角
|
|
|
|
|
|
group.set({
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
top: 0,
|
|
|
|
|
|
});
|
2025-06-22 13:52:28 +08:00
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 取消对象激活
|
|
|
|
|
|
group.set({
|
|
|
|
|
|
selectable: false, // 禁用组的选择
|
|
|
|
|
|
evented: false, // 禁用组的事件
|
|
|
|
|
|
});
|
2025-06-22 13:52:28 +08:00
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 将组添加到离屏画布
|
|
|
|
|
|
offscreenCanvas.add(group);
|
2025-06-22 13:52:28 +08:00
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 设置离屏画布的缩放
|
|
|
|
|
|
offscreenCanvas.setZoom(scaleFactor);
|
2025-06-22 13:52:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果有遮罩对象,应用遮罩
|
|
|
|
|
|
if (maskObject) {
|
2025-06-25 01:03:39 +08:00
|
|
|
|
await applyMaskToCanvas(offscreenCanvas, maskObject, groupBounds);
|
2025-06-22 13:52:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 渲染离屏画布
|
|
|
|
|
|
offscreenCanvas.renderAll();
|
|
|
|
|
|
|
2025-06-22 13:52:28 +08:00
|
|
|
|
// 生成图像数据
|
|
|
|
|
|
const dataURL = offscreenCanvas.toDataURL({
|
|
|
|
|
|
format,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (isReturenDataURL) {
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 清理离屏画布
|
|
|
|
|
|
offscreenCanvas.dispose();
|
2025-06-22 13:52:28 +08:00
|
|
|
|
return dataURL; // 如果需要返回DataURL
|
|
|
|
|
|
}
|
2025-06-25 01:03:39 +08:00
|
|
|
|
|
2025-06-22 13:52:28 +08:00
|
|
|
|
// 清理离屏画布
|
|
|
|
|
|
offscreenCanvas.dispose();
|
|
|
|
|
|
|
|
|
|
|
|
// 创建fabric.Image对象
|
|
|
|
|
|
const fabricImage = await createFabricImageFromDataURL(dataURL);
|
|
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
// 设置图像的位置和缩放,使其与原始组的位置和大小匹配
|
2025-06-22 13:52:28 +08:00
|
|
|
|
fabricImage.set({
|
2025-06-25 01:03:39 +08:00
|
|
|
|
scaleX: 1 / scaleFactor, // 由于我们生成的图像是高清版本,需要缩放回原始大小
|
|
|
|
|
|
scaleY: 1 / scaleFactor,
|
|
|
|
|
|
left: groupBounds.left + groupBounds.width / 2, // 设置为组中心点
|
|
|
|
|
|
top: groupBounds.top + groupBounds.height / 2, // 设置为组中心点
|
|
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
2025-06-22 13:52:28 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return fabricImage;
|
|
|
|
|
|
} catch (error) {
|
2025-06-25 01:03:39 +08:00
|
|
|
|
console.error("组对象栅格化失败:", error);
|
2025-06-22 13:52:28 +08:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 异步克隆fabric对象
|
|
|
|
|
|
* @param {fabric.Object} obj 要克隆的对象
|
|
|
|
|
|
* @returns {Promise<fabric.Object>} 克隆的对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
const cloneObjectAsync = (obj) => {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
obj.clone((cloned) => {
|
|
|
|
|
|
if (cloned) {
|
|
|
|
|
|
resolve(cloned);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
reject(new Error("对象克隆失败"));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从DataURL创建fabric.Image对象
|
|
|
|
|
|
* @param {string} dataURL 图像数据URL
|
|
|
|
|
|
* @returns {Promise<fabric.Image>} 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) => {
|
|
|
|
|
|
// 这里可以实现遮罩逻辑
|
|
|
|
|
|
// 例如使用canvas的clipPath或其他遮罩技术
|
|
|
|
|
|
console.log("应用遮罩功能待实现");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-25 01:03:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取对象组的边界框
|
|
|
|
|
|
* @param {Array} fabricObjects fabric对象数组
|
|
|
|
|
|
* @returns {Object} 边界框信息
|
|
|
|
|
|
*/
|
2025-06-22 13:52:28 +08:00
|
|
|
|
export const getObjectsBounds = (fabricObjects) => {
|
2025-06-25 01:03:39 +08:00
|
|
|
|
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;
|
2025-06-22 13:52:28 +08:00
|
|
|
|
};
|