363 lines
10 KiB
JavaScript
363 lines
10 KiB
JavaScript
|
|
// 栅格化帮助
|
|||
|
|
import { fabric } from "fabric-with-all";
|
|||
|
|
/**
|
|||
|
|
* 创建栅格化图像
|
|||
|
|
* 使用增强版栅格化方法,不受原始画布变换影响
|
|||
|
|
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
|||
|
|
* @private
|
|||
|
|
*/
|
|||
|
|
export const createRasterizedImage = async ({
|
|||
|
|
canvas, // 画布对象 必填
|
|||
|
|
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
|
|||
|
|
maskObject = null, // 用于裁剪的对象 - 可选
|
|||
|
|
clipPath = null, // 裁剪路径对象 - 可选,优先级高于maskObject
|
|||
|
|
trimWhitespace = true, // 是否裁剪空白区域
|
|||
|
|
trimPadding = 1, // 裁剪边距
|
|||
|
|
quality = 1.0, // 图像质量
|
|||
|
|
format = "png", // 图像格式
|
|||
|
|
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
|
|||
|
|
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
|||
|
|
} = {}) => {
|
|||
|
|
try {
|
|||
|
|
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
|||
|
|
|
|||
|
|
// 确保有对象需要栅格化
|
|||
|
|
if (fabricObjects.length === 0) {
|
|||
|
|
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理裁剪对象,优先使用clipPath
|
|||
|
|
const clippingObject = clipPath || maskObject;
|
|||
|
|
|
|||
|
|
// 高清倍数
|
|||
|
|
const currentZoom = canvas.getZoom?.() || 1;
|
|||
|
|
|
|||
|
|
scaleFactor = Math.max(
|
|||
|
|
scaleFactor || canvas?.getRetinaScaling?.(),
|
|||
|
|
currentZoom
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
scaleFactor = Math.min(scaleFactor, 3); // 最大不能大于3
|
|||
|
|
|
|||
|
|
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
|
|||
|
|
|
|||
|
|
// 计算绝对边界框(原始尺寸)和相对边界框(当前缩放后的尺寸)
|
|||
|
|
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
|
|||
|
|
|
|||
|
|
console.log("📏 绝对边界框:", absoluteBounds);
|
|||
|
|
console.log("📏 相对边界框:", relativeBounds);
|
|||
|
|
|
|||
|
|
// 使用绝对边界框创建高质量的离屏渲染
|
|||
|
|
const rasterizedImage = await createOffscreenRasterization({
|
|||
|
|
canvas,
|
|||
|
|
objects: fabricObjects,
|
|||
|
|
absoluteBounds,
|
|||
|
|
relativeBounds,
|
|||
|
|
scaleFactor,
|
|||
|
|
clippingObject,
|
|||
|
|
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,
|
|||
|
|
absoluteBounds,
|
|||
|
|
relativeBounds,
|
|||
|
|
originalZoom: currentZoom,
|
|||
|
|
hasClipping: !!clippingObject,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log(`✅ 栅格化图像创建完成`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return rasterizedImage;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error("创建栅格化图像失败:", error);
|
|||
|
|
throw new Error(`栅格化失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算对象的绝对边界框和相对边界框
|
|||
|
|
* @param {Array} fabricObjects fabric对象数组
|
|||
|
|
* @returns {Object} 包含绝对边界框和相对边界框的对象
|
|||
|
|
*/
|
|||
|
|
const calculateBounds = (fabricObjects) => {
|
|||
|
|
if (fabricObjects.length === 0) {
|
|||
|
|
console.warn("⚠️ 没有对象,无法计算边界框");
|
|||
|
|
return { absoluteBounds: null, relativeBounds: null };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let absoluteBounds = null;
|
|||
|
|
let relativeBounds = null;
|
|||
|
|
|
|||
|
|
fabricObjects.forEach((obj, index) => {
|
|||
|
|
// 获取相对边界框(考虑画布缩放和平移)
|
|||
|
|
const relativeBound = obj.getBoundingRect();
|
|||
|
|
// 获取绝对边界框(原始大小和位置)
|
|||
|
|
const absoluteBound = obj.getBoundingRect(true, true);
|
|||
|
|
|
|||
|
|
console.log(`对象 ${obj.id || index} 边界框比较:`, {
|
|||
|
|
relative: relativeBound,
|
|||
|
|
absolute: absoluteBound,
|
|||
|
|
scaleX: obj.scaleX,
|
|||
|
|
scaleY: obj.scaleY,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 计算绝对边界框的累积范围
|
|||
|
|
if (!absoluteBounds) {
|
|||
|
|
absoluteBounds = { ...absoluteBound };
|
|||
|
|
} else {
|
|||
|
|
const right = Math.max(
|
|||
|
|
absoluteBounds.left + absoluteBounds.width,
|
|||
|
|
absoluteBound.left + absoluteBound.width
|
|||
|
|
);
|
|||
|
|
const bottom = Math.max(
|
|||
|
|
absoluteBounds.top + absoluteBounds.height,
|
|||
|
|
absoluteBound.top + absoluteBound.height
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
absoluteBounds.left = Math.min(absoluteBounds.left, absoluteBound.left);
|
|||
|
|
absoluteBounds.top = Math.min(absoluteBounds.top, absoluteBound.top);
|
|||
|
|
absoluteBounds.width = right - absoluteBounds.left;
|
|||
|
|
absoluteBounds.height = bottom - absoluteBounds.top;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算相对边界框的累积范围
|
|||
|
|
if (!relativeBounds) {
|
|||
|
|
relativeBounds = { ...relativeBound };
|
|||
|
|
} else {
|
|||
|
|
const right = Math.max(
|
|||
|
|
relativeBounds.left + relativeBounds.width,
|
|||
|
|
relativeBound.left + relativeBound.width
|
|||
|
|
);
|
|||
|
|
const bottom = Math.max(
|
|||
|
|
relativeBounds.top + relativeBounds.height,
|
|||
|
|
relativeBound.top + relativeBound.height
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
relativeBounds.left = Math.min(relativeBounds.left, relativeBound.left);
|
|||
|
|
relativeBounds.top = Math.min(relativeBounds.top, relativeBound.top);
|
|||
|
|
relativeBounds.width = right - relativeBounds.left;
|
|||
|
|
relativeBounds.height = bottom - relativeBounds.top;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return { absoluteBounds, relativeBounds };
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建离屏栅格化渲染
|
|||
|
|
* @param {Object} options 渲染选项
|
|||
|
|
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
|||
|
|
*/
|
|||
|
|
const createOffscreenRasterization = async ({
|
|||
|
|
canvas,
|
|||
|
|
objects,
|
|||
|
|
absoluteBounds,
|
|||
|
|
relativeBounds,
|
|||
|
|
scaleFactor,
|
|||
|
|
clippingObject,
|
|||
|
|
trimWhitespace,
|
|||
|
|
trimPadding,
|
|||
|
|
quality,
|
|||
|
|
format,
|
|||
|
|
currentZoom,
|
|||
|
|
isReturenDataURL,
|
|||
|
|
}) => {
|
|||
|
|
try {
|
|||
|
|
// 创建离屏画布,使用绝对尺寸以保证高质量
|
|||
|
|
const offscreenCanvas = new fabric.StaticCanvas();
|
|||
|
|
|
|||
|
|
// 如果有裁剪对象,使用裁剪对象的边界框
|
|||
|
|
let renderBounds = absoluteBounds;
|
|||
|
|
if (clippingObject) {
|
|||
|
|
const clippingBounds = clippingObject.getBoundingRect(true, true);
|
|||
|
|
console.log("🎯 使用裁剪对象边界框:", clippingBounds);
|
|||
|
|
renderBounds = clippingBounds;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置离屏画布尺寸,并应用高清倍数
|
|||
|
|
const canvasWidth = Math.ceil(renderBounds.width * scaleFactor);
|
|||
|
|
const canvasHeight = Math.ceil(renderBounds.height * scaleFactor);
|
|||
|
|
|
|||
|
|
offscreenCanvas.setDimensions({
|
|||
|
|
width: canvasWidth,
|
|||
|
|
height: canvasHeight,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log(
|
|||
|
|
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 克隆对象到离屏画布
|
|||
|
|
const clonedObjects = [];
|
|||
|
|
for (const obj of objects) {
|
|||
|
|
const clonedObj = await cloneObjectAsync(obj);
|
|||
|
|
|
|||
|
|
// 调整对象位置,相对于渲染边界框的左上角
|
|||
|
|
clonedObj.set({
|
|||
|
|
left: clonedObj.left - renderBounds.left,
|
|||
|
|
top: clonedObj.top - renderBounds.top,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 如果有裁剪对象,为每个对象设置裁剪路径
|
|||
|
|
if (clippingObject) {
|
|||
|
|
const clippingPath = await cloneObjectAsync(clippingObject);
|
|||
|
|
clippingPath.set({
|
|||
|
|
left: clippingPath.left - renderBounds.left,
|
|||
|
|
top: clippingPath.top - renderBounds.top,
|
|||
|
|
fill: "",
|
|||
|
|
stroke: "",
|
|||
|
|
absolutePositioned: true,
|
|||
|
|
});
|
|||
|
|
clonedObj.set({
|
|||
|
|
clipPath: clippingPath,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clonedObjects.push(clonedObj);
|
|||
|
|
offscreenCanvas.add(clonedObj);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染离屏画布
|
|||
|
|
offscreenCanvas.renderAll();
|
|||
|
|
|
|||
|
|
// 生成图像数据
|
|||
|
|
const dataURL = offscreenCanvas.toDataURL({
|
|||
|
|
format,
|
|||
|
|
quality,
|
|||
|
|
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (isReturenDataURL) {
|
|||
|
|
return dataURL; // 如果需要返回DataURL
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理离屏画布
|
|||
|
|
offscreenCanvas.dispose();
|
|||
|
|
|
|||
|
|
// 创建fabric.Image对象
|
|||
|
|
const fabricImage = await createFabricImageFromDataURL(dataURL);
|
|||
|
|
|
|||
|
|
// 设置图像位置为裁剪区域的位置
|
|||
|
|
fabricImage.set({
|
|||
|
|
left: renderBounds.left + renderBounds.width / 2,
|
|||
|
|
top: renderBounds.top + renderBounds.height / 2,
|
|||
|
|
originX: "center",
|
|||
|
|
originY: "center",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
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) => {
|
|||
|
|
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 {Object} params 计算参数
|
|||
|
|
* @returns {Object} 变换属性对象
|
|||
|
|
*/
|
|||
|
|
const calculateImageTransform = ({
|
|||
|
|
absoluteBounds,
|
|||
|
|
relativeBounds,
|
|||
|
|
currentZoom,
|
|||
|
|
scaleFactor,
|
|||
|
|
imageWidth,
|
|||
|
|
imageHeight,
|
|||
|
|
}) => {
|
|||
|
|
// 计算缩放比例:相对尺寸 / 绝对尺寸
|
|||
|
|
const scaleX = relativeBounds.width / absoluteBounds.width;
|
|||
|
|
const scaleY = relativeBounds.height / absoluteBounds.height;
|
|||
|
|
|
|||
|
|
// 由于我们生成的图像是基于绝对尺寸的高清版本,需要考虑scaleFactor
|
|||
|
|
const finalScaleX = scaleX / scaleFactor;
|
|||
|
|
const finalScaleY = scaleY / scaleFactor;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
left: relativeBounds.left + relativeBounds.width / 2, // 设置为中心点
|
|||
|
|
top: relativeBounds.top + relativeBounds.height / 2, // 设置为中心点
|
|||
|
|
// scaleX: finalScaleX,
|
|||
|
|
// scaleY: finalScaleY,
|
|||
|
|
originX: "center",
|
|||
|
|
originY: "center",
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 应用遮罩到画布(已弃用,使用clipPath方式)
|
|||
|
|
* @param {fabric.Canvas} canvas 目标画布
|
|||
|
|
* @param {fabric.Object} maskObject 遮罩对象
|
|||
|
|
* @param {Object} bounds 边界框
|
|||
|
|
*/
|
|||
|
|
const applyMaskToCanvas = async (canvas, maskObject, bounds) => {
|
|||
|
|
// 此方法已被clipPath方式替代
|
|||
|
|
console.log("⚠️ applyMaskToCanvas已被clipPath方式替代");
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const getObjectsBounds = (fabricObjects) => {
|
|||
|
|
const { absoluteBounds } = calculateBounds(fabricObjects);
|
|||
|
|
return absoluteBounds;
|
|||
|
|
};
|