Files
aida_front/src/component/Canvas/CanvasEditor/utils/rasterizedImage.js

279 lines
7.3 KiB
JavaScript
Raw Normal View History

2025-06-22 13:52:28 +08:00
// 栅格化帮助
import { fabric } from "fabric-with-all";
import { createStaticCanvas } from "./canvasFactory";
2025-06-22 13:52:28 +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, // 是否裁剪空白区域
trimPadding = 0, // 裁剪边距
2025-06-22 13:52:28 +08:00
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
2025-06-29 23:29:47 +08:00
isThumbnail = false, // 是否为缩略图
2025-06-22 13:52:28 +08:00
} = {}) => {
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
);
2025-06-29 23:29:47 +08:00
if (isThumbnail) scaleFactor = 0.2; // 缩略图使用较小的高清倍数
2025-06-22 13:52:28 +08:00
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
// 使用组对象方式创建栅格化图像
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-22 13:52:28 +08:00
* @param {Object} options 渲染选项
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
*/
const createRasterizedImageWithGroup = async ({
2025-06-22 13:52:28 +08:00
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,
});
clonedObjects.push(clonedObj);
}
2025-06-22 13:52:28 +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,
hasControls: false,
2025-06-22 13:52:28 +08:00
});
console.log(
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
);
// 调整组的位置,让它位于画布的左上角
group.set({
left: 0,
top: 0,
});
2025-06-22 13:52:28 +08:00
// 取消对象激活
group.set({
selectable: false, // 禁用组的选择
evented: false, // 禁用组的事件
});
2025-06-22 13:52:28 +08:00
// 将组添加到离屏画布
offscreenCanvas.add(group);
2025-06-22 13:52:28 +08:00
// 设置离屏画布的缩放
offscreenCanvas.setZoom(scaleFactor);
2025-06-22 13:52:28 +08:00
// 如果有遮罩对象,应用遮罩
if (maskObject) {
await applyMaskToCanvas(offscreenCanvas, maskObject, groupBounds);
2025-06-22 13:52:28 +08:00
}
// 渲染离屏画布
offscreenCanvas.renderAll();
2025-06-22 13:52:28 +08:00
// 生成图像数据
const dataURL = offscreenCanvas.toDataURL({
format,
quality,
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
});
if (isReturenDataURL) {
// 清理离屏画布
offscreenCanvas.dispose();
2025-06-22 13:52:28 +08:00
return dataURL; // 如果需要返回DataURL
}
2025-06-22 13:52:28 +08:00
// 清理离屏画布
offscreenCanvas.dispose();
// 创建fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(dataURL);
// 设置图像的位置和缩放,使其与原始组的位置和大小匹配
2025-06-22 13:52:28 +08:00
fabricImage.set({
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) {
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("应用遮罩功能待实现");
};
/**
* 获取对象组的边界框
* @param {Array} fabricObjects fabric对象数组
* @returns {Object} 边界框信息
*/
2025-06-22 13:52:28 +08:00
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;
2025-06-22 13:52:28 +08:00
};