491 lines
14 KiB
JavaScript
491 lines
14 KiB
JavaScript
// 栅格化帮助
|
||
import { fabric } from "fabric-with-all";
|
||
import { createStaticCanvas } from "./canvasFactory";
|
||
/**
|
||
* 创建栅格化图像
|
||
* 使用组对象方式,避免边界计算误差
|
||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||
* @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<fabric.Image>} 栅格化后的图像对象
|
||
*/
|
||
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<fabric.Image>} 栅格化后的图像对象
|
||
*/
|
||
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<fabric.Object>} 克隆的对象
|
||
*/
|
||
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.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) => {
|
||
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;
|
||
};
|