2025-06-23 00:40:45 +08:00
|
|
|
|
// 栅格化帮助
|
|
|
|
|
|
import { fabric } from "fabric-with-all";
|
2026-01-06 14:17:04 +08:00
|
|
|
|
import { SpecialLayerId } from "./layerHelper";
|
2025-06-23 00:40:45 +08:00
|
|
|
|
/**
|
2025-06-26 00:37:07 +08:00
|
|
|
|
* 创建栅格化图像 - 重构版本
|
|
|
|
|
|
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
|
|
|
|
|
|
* @returns {Promise<fabric.Image|fabric.Group|string>} 栅格化后的图像对象或DataURL
|
2025-06-23 00:40:45 +08:00
|
|
|
|
* @private
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const createRasterizedImage = async ({
|
|
|
|
|
|
canvas, // 画布对象 必填
|
|
|
|
|
|
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
|
|
|
|
|
|
maskObject = null, // 用于裁剪的对象 - 可选
|
|
|
|
|
|
clipPath = null, // 裁剪路径对象 - 可选,优先级高于maskObject
|
|
|
|
|
|
trimWhitespace = true, // 是否裁剪空白区域
|
2025-06-26 00:37:07 +08:00
|
|
|
|
trimPadding = 0, // 裁剪边距
|
2025-06-23 00:40:45 +08:00
|
|
|
|
quality = 1.0, // 图像质量
|
|
|
|
|
|
format = "png", // 图像格式
|
|
|
|
|
|
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
|
|
|
|
|
|
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
2025-06-26 00:37:07 +08:00
|
|
|
|
preserveOriginalQuality = true, // 是否保持原始质量(新增)
|
2025-06-29 23:29:47 +08:00
|
|
|
|
selectionManager = null, // 选区管理器,用于获取羽化值等设置
|
2025-07-22 21:40:39 +08:00
|
|
|
|
restoreOpacityInRedGreen, // 是否在红绿图模式下恢复透明度
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-23 00:40:45 +08:00
|
|
|
|
} = {}) => {
|
|
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 确保有对象需要栅格化
|
|
|
|
|
|
if (fabricObjects.length === 0) {
|
|
|
|
|
|
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理裁剪对象,优先使用clipPath
|
|
|
|
|
|
const clippingObject = clipPath || maskObject;
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 如果保持原始质量且有裁剪对象,使用新的裁剪方法
|
|
|
|
|
|
if (preserveOriginalQuality && clippingObject) {
|
|
|
|
|
|
return await createClippedObjects({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
isReturenDataURL,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
selectionManager, // 传递选区管理器
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-26 00:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 如果只是简单复制而不需要裁剪,直接克隆对象
|
|
|
|
|
|
if (!clippingObject) {
|
|
|
|
|
|
return await createSimpleClone({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
isReturenDataURL,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
format,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 兼容原有的离屏渲染方法(作为备选方案)
|
|
|
|
|
|
return await createLegacyRasterization({
|
2025-06-23 00:40:45 +08:00
|
|
|
|
canvas,
|
2025-06-26 00:37:07 +08:00
|
|
|
|
fabricObjects,
|
2025-06-23 00:40:45 +08:00
|
|
|
|
clippingObject,
|
2025-06-26 00:37:07 +08:00
|
|
|
|
scaleFactor,
|
2025-06-23 00:40:45 +08:00
|
|
|
|
quality,
|
|
|
|
|
|
format,
|
|
|
|
|
|
isReturenDataURL,
|
|
|
|
|
|
});
|
2025-06-26 00:37:07 +08:00
|
|
|
|
} catch (error) {
|
2026-01-02 11:24:11 +08:00
|
|
|
|
console.warn("创建栅格化图像失败:", error);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
throw new Error(`栅格化失败: ${error.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 创建带裁剪的对象 - 新方法
|
|
|
|
|
|
* 直接复制原对象并应用裁剪路径,保持原始质量
|
|
|
|
|
|
*/
|
|
|
|
|
|
const createClippedObjects = async ({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
isReturenDataURL,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
selectionManager = null, // 新增选区管理器参数
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-26 00:37:07 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用优化后的边界计算,确保包含描边区域
|
2025-07-22 21:40:39 +08:00
|
|
|
|
const optimizedBounds = calculateOptimizedBounds(
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
fabricObjects
|
|
|
|
|
|
);
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("📐 优化后的选区边界框:", optimizedBounds);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取羽化值
|
|
|
|
|
|
let featherAmount = 0;
|
2025-07-22 21:40:39 +08:00
|
|
|
|
if (
|
|
|
|
|
|
selectionManager &&
|
|
|
|
|
|
typeof selectionManager.getFeatherAmount === "function"
|
|
|
|
|
|
) {
|
2025-06-29 23:29:47 +08:00
|
|
|
|
featherAmount = selectionManager.getFeatherAmount();
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(`🌟 应用羽化效果: ${featherAmount}px`);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
}
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 方法1:如果只需要返回DataURL,使用画布裁剪方法
|
2025-06-23 00:40:45 +08:00
|
|
|
|
if (isReturenDataURL) {
|
2025-06-26 00:37:07 +08:00
|
|
|
|
return await createClippedDataURLByCanvas({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
clippingObject,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
|
|
|
|
|
featherAmount,
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-26 00:37:07 +08:00
|
|
|
|
});
|
2025-06-23 00:40:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 方法2:如果需要返回fabric对象,先生成DataURL再转换为fabric对象
|
|
|
|
|
|
const clippedDataURL = await createClippedDataURLByCanvas({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
clippingObject,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
|
|
|
|
|
featherAmount,
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-26 00:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 将DataURL转换为fabric.Image对象
|
|
|
|
|
|
const fabricImage = await createFabricImageFromDataURL(clippedDataURL);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用fabric原生方法恢复到选区的原始大小和位置
|
2025-06-29 23:29:47 +08:00
|
|
|
|
fabricImage.scaleToWidth(optimizedBounds.width);
|
|
|
|
|
|
fabricImage.scaleToHeight(optimizedBounds.height);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置到选区的原始位置(中心点)
|
|
|
|
|
|
fabricImage.set({
|
2025-06-29 23:29:47 +08:00
|
|
|
|
left: optimizedBounds.left + optimizedBounds.width / 2,
|
|
|
|
|
|
top: optimizedBounds.top + optimizedBounds.height / 2,
|
2025-06-26 00:37:07 +08:00
|
|
|
|
originX: "center",
|
|
|
|
|
|
originY: "center",
|
|
|
|
|
|
selectable: true,
|
|
|
|
|
|
evented: true,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
// hasControls: true,
|
|
|
|
|
|
// hasBorders: true,
|
2025-06-26 00:37:07 +08:00
|
|
|
|
custom: {
|
|
|
|
|
|
type: "clipped",
|
|
|
|
|
|
clippedAt: new Date().toISOString(),
|
|
|
|
|
|
hasClipping: true,
|
|
|
|
|
|
preservedQuality: true,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
originalBounds: optimizedBounds, // 保存优化后的边界框
|
2025-06-26 00:37:07 +08:00
|
|
|
|
restoredToOriginalSize: true,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
usedImageMask: true, // 标记使用了图像遮罩
|
|
|
|
|
|
featherAmount: featherAmount,
|
|
|
|
|
|
boundaryOptimized: true, // 标记使用了边界优化
|
2025-06-26 00:37:07 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新坐标
|
|
|
|
|
|
fabricImage.setCoords();
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 返回裁剪后的fabric对象,已恢复到优化后的原始大小和位置");
|
2025-06-26 00:37:07 +08:00
|
|
|
|
return fabricImage;
|
|
|
|
|
|
} catch (error) {
|
2026-01-02 11:24:11 +08:00
|
|
|
|
console.warn("创建裁剪对象失败:", error);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通过画布裁剪生成DataURL
|
|
|
|
|
|
* 裁剪掉选区以外的内容,保持和选区大小一致
|
|
|
|
|
|
*/
|
|
|
|
|
|
const createClippedDataURLByCanvas = async ({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
featherAmount = 0,
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isEnhanceImg = false, // 是否是增强图片
|
2025-06-26 00:37:07 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
2025-06-29 23:29:47 +08:00
|
|
|
|
// 使用优化后的边界计算,确保包含描边区域
|
2026-01-05 11:47:36 +08:00
|
|
|
|
// const optimizedBounds = calculateOptimizedBounds(
|
|
|
|
|
|
// clippingObject,
|
|
|
|
|
|
// fabricObjects
|
|
|
|
|
|
// );
|
|
|
|
|
|
const optimizedBounds = {
|
|
|
|
|
|
left: clippingObject.left - clippingObject.width / 2,
|
|
|
|
|
|
top: clippingObject.top - clippingObject.height / 2,
|
|
|
|
|
|
width: clippingObject.width,
|
|
|
|
|
|
height: clippingObject.height,
|
|
|
|
|
|
}
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用高分辨率以保证质量
|
|
|
|
|
|
const pixelRatio = window.devicePixelRatio || 1;
|
2025-09-25 13:52:05 +08:00
|
|
|
|
const qualityMultiplier = !!isEnhanceImg ? Math.max(2, pixelRatio) : 1;
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("使用高分辨率以保证质量:" + isEnhanceImg, optimizedBounds);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
2025-06-29 23:29:47 +08:00
|
|
|
|
const canvasWidth = Math.ceil(optimizedBounds.width * qualityMultiplier);
|
|
|
|
|
|
const canvasHeight = Math.ceil(optimizedBounds.height * qualityMultiplier);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(
|
|
|
|
|
|
// `📏 优化后画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
|
|
|
|
|
|
// );
|
|
|
|
|
|
// console.log("🎯 边界框对比:", {
|
|
|
|
|
|
// original: selectionBounds,
|
|
|
|
|
|
// optimized: optimizedBounds,
|
|
|
|
|
|
// });
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
2025-06-29 23:29:47 +08:00
|
|
|
|
// 步骤1: 先将路径转换为遮罩图像(支持羽化)
|
|
|
|
|
|
const maskImageDataURL =
|
|
|
|
|
|
featherAmount > 0
|
|
|
|
|
|
? await createAdvancedMaskImage({
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
|
|
|
|
|
qualityMultiplier,
|
|
|
|
|
|
featherAmount,
|
|
|
|
|
|
})
|
|
|
|
|
|
: await createMaskImageFromPath({
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
|
|
|
|
|
qualityMultiplier,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 步骤2: 渲染原始内容
|
|
|
|
|
|
const contentImageDataURL = await renderContentToImage({
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
selectionBounds: optimizedBounds, // 使用优化后的边界框
|
|
|
|
|
|
qualityMultiplier,
|
2025-06-26 00:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-06-29 23:29:47 +08:00
|
|
|
|
// 步骤3: 使用遮罩合成最终结果
|
|
|
|
|
|
const clippedDataURL = await applyImageMask({
|
|
|
|
|
|
contentImageDataURL,
|
|
|
|
|
|
maskImageDataURL,
|
|
|
|
|
|
canvasWidth,
|
|
|
|
|
|
canvasHeight,
|
|
|
|
|
|
});
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 图像遮罩裁剪完成,生成DataURL");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
return clippedDataURL;
|
2025-06-26 00:37:07 +08:00
|
|
|
|
} catch (error) {
|
2025-06-29 23:29:47 +08:00
|
|
|
|
console.error("图像遮罩裁剪失败:", error);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建简单克隆对象
|
|
|
|
|
|
* 当不需要裁剪时,直接克隆原对象
|
|
|
|
|
|
*/
|
2025-07-22 21:40:39 +08:00
|
|
|
|
const createSimpleClone = async ({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
isReturenDataURL,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
format,
|
|
|
|
|
|
}) => {
|
2025-06-26 00:37:07 +08:00
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("📋 创建简单克隆对象");
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
const clonedObjects = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆所有对象
|
|
|
|
|
|
for (const obj of fabricObjects) {
|
|
|
|
|
|
const clonedObj = await cloneObjectAsync(obj);
|
|
|
|
|
|
|
|
|
|
|
|
clonedObj.set({
|
2025-06-23 00:40:45 +08:00
|
|
|
|
selectable: true,
|
|
|
|
|
|
evented: true,
|
|
|
|
|
|
hasControls: true,
|
|
|
|
|
|
hasBorders: true,
|
|
|
|
|
|
custom: {
|
2025-06-26 00:37:07 +08:00
|
|
|
|
...clonedObj.custom,
|
|
|
|
|
|
type: "cloned",
|
|
|
|
|
|
clonedAt: new Date().toISOString(),
|
|
|
|
|
|
preservedQuality: true,
|
2025-06-23 00:40:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
clonedObjects.push(clonedObj);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果需要返回DataURL,需要渲染
|
|
|
|
|
|
if (isReturenDataURL) {
|
|
|
|
|
|
return await renderObjectsToDataURL(clonedObjects, quality, format);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果只有一个对象,直接返回
|
|
|
|
|
|
if (clonedObjects.length === 1) {
|
|
|
|
|
|
return clonedObjects[0];
|
2025-06-23 00:40:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
// 创建组合
|
|
|
|
|
|
const group = new fabric.Group(clonedObjects, {
|
|
|
|
|
|
selectable: true,
|
|
|
|
|
|
evented: true,
|
|
|
|
|
|
hasControls: true,
|
|
|
|
|
|
hasBorders: true,
|
|
|
|
|
|
custom: {
|
|
|
|
|
|
type: "clonedGroup",
|
|
|
|
|
|
clonedAt: new Date().toISOString(),
|
|
|
|
|
|
objectCount: clonedObjects.length,
|
|
|
|
|
|
preservedQuality: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return group;
|
2025-06-23 00:40:45 +08:00
|
|
|
|
} catch (error) {
|
2025-06-26 00:37:07 +08:00
|
|
|
|
console.error("创建简单克隆失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将对象渲染为DataURL
|
|
|
|
|
|
*/
|
|
|
|
|
|
const renderObjectsToDataURL = async (objects, quality, format) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 计算对象边界框
|
|
|
|
|
|
const bounds = calculateBounds(objects);
|
|
|
|
|
|
if (!bounds.absoluteBounds) {
|
|
|
|
|
|
throw new Error("无法计算对象边界框");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建临时画布用于渲染
|
|
|
|
|
|
const tempCanvas = new fabric.StaticCanvas();
|
|
|
|
|
|
const { absoluteBounds } = bounds;
|
|
|
|
|
|
|
|
|
|
|
|
tempCanvas.setDimensions({
|
|
|
|
|
|
width: Math.ceil(absoluteBounds.width),
|
|
|
|
|
|
height: Math.ceil(absoluteBounds.height),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 调整对象位置并添加到临时画布
|
|
|
|
|
|
for (const obj of objects) {
|
|
|
|
|
|
const tempObj = await cloneObjectAsync(obj);
|
|
|
|
|
|
tempObj.set({
|
|
|
|
|
|
left: tempObj.left - absoluteBounds.left,
|
|
|
|
|
|
top: tempObj.top - absoluteBounds.top,
|
|
|
|
|
|
});
|
|
|
|
|
|
tempCanvas.add(tempObj);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染并获取DataURL
|
|
|
|
|
|
tempCanvas.renderAll();
|
|
|
|
|
|
const dataURL = tempCanvas.toDataURL({
|
|
|
|
|
|
format,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理临时画布
|
|
|
|
|
|
tempCanvas.dispose();
|
|
|
|
|
|
|
|
|
|
|
|
return dataURL;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("渲染对象为DataURL失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 渲染裁剪后的对象为DataURL
|
|
|
|
|
|
* 专门处理带有裁剪路径的对象渲染(备用方法)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const renderClippedObjectsToDataURL = async (clippedObjects) => {
|
|
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🖼️ 渲染裁剪对象为DataURL");
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算所有裁剪对象的总边界框
|
|
|
|
|
|
let totalBounds = null;
|
|
|
|
|
|
|
|
|
|
|
|
for (const obj of clippedObjects) {
|
|
|
|
|
|
const objBounds = obj.getBoundingRect(true, true);
|
|
|
|
|
|
|
|
|
|
|
|
if (!totalBounds) {
|
|
|
|
|
|
totalBounds = { ...objBounds };
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const right = Math.max(
|
|
|
|
|
|
totalBounds.left + totalBounds.width,
|
|
|
|
|
|
objBounds.left + objBounds.width
|
|
|
|
|
|
);
|
|
|
|
|
|
const bottom = Math.max(
|
|
|
|
|
|
totalBounds.top + totalBounds.height,
|
|
|
|
|
|
objBounds.top + objBounds.height
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
totalBounds.left = Math.min(totalBounds.left, objBounds.left);
|
|
|
|
|
|
totalBounds.top = Math.min(totalBounds.top, objBounds.top);
|
|
|
|
|
|
totalBounds.width = right - totalBounds.left;
|
|
|
|
|
|
totalBounds.height = bottom - totalBounds.top;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!totalBounds) {
|
|
|
|
|
|
throw new Error("无法计算对象边界框");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建临时画布,使用高分辨率
|
|
|
|
|
|
const tempCanvas = new fabric.StaticCanvas();
|
|
|
|
|
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
|
|
|
|
const scaleFactor = Math.max(2, pixelRatio);
|
|
|
|
|
|
|
|
|
|
|
|
const canvasWidth = Math.ceil(totalBounds.width * scaleFactor);
|
|
|
|
|
|
const canvasHeight = Math.ceil(totalBounds.height * scaleFactor);
|
|
|
|
|
|
|
|
|
|
|
|
tempCanvas.setDimensions({
|
|
|
|
|
|
width: canvasWidth,
|
|
|
|
|
|
height: canvasHeight,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 调整对象位置并添加到临时画布
|
|
|
|
|
|
for (const obj of clippedObjects) {
|
|
|
|
|
|
const tempObj = await cloneObjectAsync(obj);
|
|
|
|
|
|
|
|
|
|
|
|
// 调整位置到画布坐标系
|
|
|
|
|
|
tempObj.set({
|
|
|
|
|
|
left: (tempObj.left - totalBounds.left) * scaleFactor,
|
|
|
|
|
|
top: (tempObj.top - totalBounds.top) * scaleFactor,
|
|
|
|
|
|
scaleX: (tempObj.scaleX || 1) * scaleFactor,
|
|
|
|
|
|
scaleY: (tempObj.scaleY || 1) * scaleFactor,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有裁剪路径,也需要调整裁剪路径
|
|
|
|
|
|
if (tempObj.clipPath) {
|
|
|
|
|
|
tempObj.clipPath.set({
|
|
|
|
|
|
scaleX: (tempObj.clipPath.scaleX || 1) * scaleFactor,
|
|
|
|
|
|
scaleY: (tempObj.clipPath.scaleY || 1) * scaleFactor,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tempCanvas.add(tempObj);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染画布
|
|
|
|
|
|
tempCanvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取DataURL
|
|
|
|
|
|
const dataURL = tempCanvas.toDataURL({
|
|
|
|
|
|
format: "png",
|
|
|
|
|
|
quality: 1.0,
|
|
|
|
|
|
multiplier: 1, // 已经通过尺寸处理了缩放
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理临时画布
|
|
|
|
|
|
tempCanvas.dispose();
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 裁剪对象渲染完成");
|
2025-06-26 00:37:07 +08:00
|
|
|
|
return dataURL;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("渲染裁剪对象失败:", error);
|
|
|
|
|
|
throw error;
|
2025-06-23 00:40:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 兼容的离屏渲染方法(原有逻辑,作为备选)
|
|
|
|
|
|
*/
|
|
|
|
|
|
const createLegacyRasterization = async ({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
scaleFactor,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
format,
|
|
|
|
|
|
isReturenDataURL,
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isCropByBg, // 是否根据背景裁剪
|
|
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-26 00:37:07 +08:00
|
|
|
|
}) => {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("⚠️ 使用兼容的离屏渲染方法");
|
2025-06-26 00:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 这里保留原有的离屏渲染逻辑作为备选方案
|
|
|
|
|
|
const currentZoom = canvas.getZoom?.() || 1;
|
2025-07-22 21:40:39 +08:00
|
|
|
|
scaleFactor = Math.max(
|
|
|
|
|
|
scaleFactor || canvas?.getRetinaScaling?.(),
|
|
|
|
|
|
currentZoom
|
|
|
|
|
|
);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
scaleFactor = Math.min(scaleFactor, 3);
|
|
|
|
|
|
|
|
|
|
|
|
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
|
|
|
|
|
|
|
|
|
|
|
|
return await createOffscreenRasterization({
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
objects: fabricObjects,
|
|
|
|
|
|
absoluteBounds,
|
|
|
|
|
|
relativeBounds,
|
|
|
|
|
|
scaleFactor,
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
trimWhitespace: true,
|
|
|
|
|
|
trimPadding: 1,
|
|
|
|
|
|
quality,
|
|
|
|
|
|
format,
|
|
|
|
|
|
currentZoom,
|
|
|
|
|
|
isReturenDataURL,
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isCropByBg, // 是否根据背景裁剪
|
|
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-26 00:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-23 00:40:45 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 计算对象的绝对边界框和相对边界框
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(`对象 ${obj.id || index} 边界框比较:`, {
|
|
|
|
|
|
// relative: relativeBound,
|
|
|
|
|
|
// absolute: absoluteBound,
|
|
|
|
|
|
// scaleX: obj.scaleX,
|
|
|
|
|
|
// scaleY: obj.scaleY,
|
|
|
|
|
|
// });
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算绝对边界框的累积范围
|
|
|
|
|
|
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,
|
2025-09-25 13:52:05 +08:00
|
|
|
|
isCropByBg, // 是否根据背景裁剪
|
|
|
|
|
|
isEnhanceImg, // 是否是增强图片
|
2025-06-23 00:40:45 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 创建离屏画布,使用绝对尺寸以保证高质量
|
|
|
|
|
|
const offscreenCanvas = new fabric.StaticCanvas();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有裁剪对象,使用裁剪对象的边界框
|
|
|
|
|
|
let renderBounds = absoluteBounds;
|
|
|
|
|
|
if (clippingObject) {
|
|
|
|
|
|
const clippingBounds = clippingObject.getBoundingRect(true, true);
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🎯 使用裁剪对象边界框:", clippingBounds);
|
2025-06-23 00:40:45 +08:00
|
|
|
|
renderBounds = clippingBounds;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置离屏画布尺寸,并应用高清倍数
|
2025-06-26 00:37:07 +08:00
|
|
|
|
const canvasWidth = Math.ceil(renderBounds.width);
|
|
|
|
|
|
const canvasHeight = Math.ceil(renderBounds.height);
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
|
|
|
|
|
offscreenCanvas.setDimensions({
|
|
|
|
|
|
width: canvasWidth,
|
|
|
|
|
|
height: canvasHeight,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(
|
|
|
|
|
|
// `🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
|
|
|
|
|
|
// );
|
2025-06-23 00:40:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 克隆对象到离屏画布
|
|
|
|
|
|
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) {
|
2026-01-06 14:17:04 +08:00
|
|
|
|
cloned.set({
|
|
|
|
|
|
scaleX: obj.scaleX,
|
|
|
|
|
|
scaleY: obj.scaleY,
|
|
|
|
|
|
top: obj.top,
|
|
|
|
|
|
left: obj.left,
|
|
|
|
|
|
width: obj.width,
|
|
|
|
|
|
height: obj.height,
|
|
|
|
|
|
zoomX: obj.zoomX,
|
|
|
|
|
|
zoomY: obj.zoomY,
|
|
|
|
|
|
})
|
2025-06-23 00:40:45 +08:00
|
|
|
|
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;
|
|
|
|
|
|
};
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将路径对象转换为遮罩图像
|
|
|
|
|
|
* @param {Object} clippingObject 裁剪路径对象
|
|
|
|
|
|
* @param {Object} selectionBounds 选区边界框
|
|
|
|
|
|
* @param {Number} qualityMultiplier 质量倍数
|
|
|
|
|
|
* @returns {Promise<String>} 遮罩图像的DataURL
|
|
|
|
|
|
*/
|
2025-07-22 21:40:39 +08:00
|
|
|
|
const createMaskImageFromPath = async ({
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds,
|
|
|
|
|
|
qualityMultiplier,
|
|
|
|
|
|
}) => {
|
2025-06-29 23:29:47 +08:00
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🎭 创建路径遮罩图像");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建专门用于渲染遮罩的画布
|
|
|
|
|
|
const maskCanvas = new fabric.StaticCanvas();
|
|
|
|
|
|
|
|
|
|
|
|
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
|
|
|
|
|
|
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
|
|
|
|
|
|
|
|
|
|
|
|
maskCanvas.setDimensions({
|
|
|
|
|
|
width: canvasWidth,
|
|
|
|
|
|
height: canvasHeight,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆路径对象并处理描边转填充
|
2025-07-22 21:40:39 +08:00
|
|
|
|
const maskPath = await createSolidMaskPath(
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds,
|
|
|
|
|
|
qualityMultiplier
|
|
|
|
|
|
);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加路径到遮罩画布
|
|
|
|
|
|
maskCanvas.add(maskPath);
|
|
|
|
|
|
maskCanvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
// 生成遮罩图像
|
|
|
|
|
|
const maskDataURL = maskCanvas.toDataURL({
|
|
|
|
|
|
format: "png",
|
|
|
|
|
|
quality: 1.0,
|
|
|
|
|
|
multiplier: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理遮罩画布
|
|
|
|
|
|
maskCanvas.dispose();
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 遮罩图像创建完成");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
return maskDataURL;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("创建遮罩图像失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 渲染内容对象为图像
|
|
|
|
|
|
* @param {Array} fabricObjects 要渲染的对象数组
|
|
|
|
|
|
* @param {Object} selectionBounds 选区边界框
|
|
|
|
|
|
* @param {Number} qualityMultiplier 质量倍数
|
|
|
|
|
|
* @returns {Promise<String>} 内容图像的DataURL
|
|
|
|
|
|
*/
|
2025-07-22 21:40:39 +08:00
|
|
|
|
const renderContentToImage = async ({
|
|
|
|
|
|
fabricObjects,
|
|
|
|
|
|
selectionBounds,
|
|
|
|
|
|
qualityMultiplier,
|
|
|
|
|
|
}) => {
|
2025-06-29 23:29:47 +08:00
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🖼️ 渲染内容图像");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建内容渲染画布
|
|
|
|
|
|
const contentCanvas = new fabric.StaticCanvas();
|
|
|
|
|
|
|
|
|
|
|
|
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
|
|
|
|
|
|
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
|
|
|
|
|
|
|
|
|
|
|
|
contentCanvas.setDimensions({
|
|
|
|
|
|
width: canvasWidth,
|
|
|
|
|
|
height: canvasHeight,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆并添加所有需要渲染的对象
|
2026-01-06 14:17:04 +08:00
|
|
|
|
for (let obj of fabricObjects) {
|
|
|
|
|
|
let clonedObj = await cloneObjectAsync(obj);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
// 调整对象位置:将选区左上角作为新的原点(0,0)
|
|
|
|
|
|
clonedObj.set({
|
|
|
|
|
|
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
|
|
|
|
|
|
top: (clonedObj.top - selectionBounds.top) * qualityMultiplier,
|
|
|
|
|
|
scaleX: (clonedObj.scaleX || 1) * qualityMultiplier,
|
|
|
|
|
|
scaleY: (clonedObj.scaleY || 1) * qualityMultiplier,
|
|
|
|
|
|
selectable: false,
|
|
|
|
|
|
evented: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-18 01:28:31 +08:00
|
|
|
|
// 如果有裁剪路径,也需要调整裁剪路径
|
2026-01-06 14:17:04 +08:00
|
|
|
|
if (clonedObj.clipPath && obj.id !== SpecialLayerId.COLOR) {
|
2025-07-18 01:28:31 +08:00
|
|
|
|
clonedObj.clipPath.set({
|
2026-01-06 14:17:04 +08:00
|
|
|
|
left: (clonedObj.clipPath.left - selectionBounds.left) * qualityMultiplier,
|
|
|
|
|
|
top: (clonedObj.clipPath.top - selectionBounds.top) * qualityMultiplier,
|
2025-07-18 01:28:31 +08:00
|
|
|
|
scaleX: (clonedObj.clipPath.scaleX || 1) * qualityMultiplier,
|
|
|
|
|
|
scaleY: (clonedObj.clipPath.scaleY || 1) * qualityMultiplier,
|
|
|
|
|
|
});
|
|
|
|
|
|
clonedObj.clipPath.setCoords(); // 更新裁剪路径坐标
|
|
|
|
|
|
}
|
2026-01-06 14:17:04 +08:00
|
|
|
|
// if(obj.globalCompositeOperation === "multiply"){
|
|
|
|
|
|
// clonedObj.clipPath = null;
|
|
|
|
|
|
// }
|
2025-06-29 23:29:47 +08:00
|
|
|
|
contentCanvas.add(clonedObj);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染内容画布
|
|
|
|
|
|
contentCanvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
// 生成内容图像
|
|
|
|
|
|
const contentDataURL = contentCanvas.toDataURL({
|
|
|
|
|
|
format: "png",
|
|
|
|
|
|
quality: 1.0,
|
|
|
|
|
|
multiplier: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理内容画布
|
|
|
|
|
|
contentCanvas.dispose();
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 内容图像渲染完成");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
return contentDataURL;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("渲染内容图像失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 使用遮罩图像对内容图像进行裁剪
|
|
|
|
|
|
* @param {String} contentImageDataURL 内容图像DataURL
|
|
|
|
|
|
* @param {String} maskImageDataURL 遮罩图像DataURL
|
|
|
|
|
|
* @param {Number} canvasWidth 画布宽度
|
|
|
|
|
|
* @param {Number} canvasHeight 画布高度
|
|
|
|
|
|
* @returns {Promise<String>} 裁剪后的图像DataURL
|
|
|
|
|
|
*/
|
|
|
|
|
|
const applyImageMask = async ({
|
|
|
|
|
|
contentImageDataURL,
|
|
|
|
|
|
maskImageDataURL,
|
|
|
|
|
|
canvasWidth,
|
|
|
|
|
|
canvasHeight,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🎯 应用图像遮罩");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
// 创建用于合成的Canvas元素
|
|
|
|
|
|
const compositeCanvas = document.createElement("canvas");
|
|
|
|
|
|
const ctx = compositeCanvas.getContext("2d");
|
|
|
|
|
|
|
|
|
|
|
|
compositeCanvas.width = canvasWidth;
|
|
|
|
|
|
compositeCanvas.height = canvasHeight;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载内容图像
|
|
|
|
|
|
const contentImg = new Image();
|
|
|
|
|
|
contentImg.onload = () => {
|
|
|
|
|
|
// 加载遮罩图像
|
|
|
|
|
|
const maskImg = new Image();
|
|
|
|
|
|
maskImg.onload = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先绘制内容图像
|
|
|
|
|
|
ctx.drawImage(contentImg, 0, 0, canvasWidth, canvasHeight);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置合成模式为遮罩模式
|
|
|
|
|
|
ctx.globalCompositeOperation = "destination-in";
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制遮罩图像
|
|
|
|
|
|
ctx.drawImage(maskImg, 0, 0, canvasWidth, canvasHeight);
|
|
|
|
|
|
|
|
|
|
|
|
// 重置合成模式
|
|
|
|
|
|
ctx.globalCompositeOperation = "source-over";
|
|
|
|
|
|
|
|
|
|
|
|
// 获取最终结果
|
|
|
|
|
|
const resultDataURL = compositeCanvas.toDataURL("image/png", 1.0);
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 图像遮罩应用完成");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
resolve(resultDataURL);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("合成图像失败:", error);
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
maskImg.onerror = () => {
|
|
|
|
|
|
reject(new Error("加载遮罩图像失败"));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
maskImg.src = maskImageDataURL;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
contentImg.onerror = () => {
|
|
|
|
|
|
reject(new Error("加载内容图像失败"));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
contentImg.src = contentImageDataURL;
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("应用图像遮罩失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建带羽化效果的遮罩图像(高级版本)
|
|
|
|
|
|
* @param {Object} clippingObject 裁剪路径对象
|
|
|
|
|
|
* @param {Object} selectionBounds 选区边界框
|
|
|
|
|
|
* @param {Number} qualityMultiplier 质量倍数
|
|
|
|
|
|
* @param {Number} featherAmount 羽化值
|
|
|
|
|
|
* @returns {Promise<String>} 遮罩图像的DataURL
|
|
|
|
|
|
*/
|
|
|
|
|
|
const createAdvancedMaskImage = async ({
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds,
|
|
|
|
|
|
qualityMultiplier,
|
|
|
|
|
|
featherAmount = 0,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(`🎭 创建高级遮罩图像 (羽化: ${featherAmount})`);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建专门用于渲染遮罩的画布
|
|
|
|
|
|
const maskCanvas = new fabric.StaticCanvas();
|
|
|
|
|
|
|
|
|
|
|
|
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
|
|
|
|
|
|
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
|
|
|
|
|
|
|
|
|
|
|
|
maskCanvas.setDimensions({
|
|
|
|
|
|
width: canvasWidth,
|
|
|
|
|
|
height: canvasHeight,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆路径对象并处理描边转填充
|
2025-07-22 21:40:39 +08:00
|
|
|
|
const maskPath = await createSolidMaskPath(
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds,
|
|
|
|
|
|
qualityMultiplier
|
|
|
|
|
|
);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果有羽化值,添加模糊效果
|
|
|
|
|
|
if (featherAmount > 0) {
|
|
|
|
|
|
const adjustedFeather = featherAmount * qualityMultiplier;
|
|
|
|
|
|
maskPath.shadow = new fabric.Shadow({
|
|
|
|
|
|
color: "#ffffff",
|
|
|
|
|
|
blur: adjustedFeather,
|
|
|
|
|
|
offsetX: 0,
|
|
|
|
|
|
offsetY: 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加路径到遮罩画布
|
|
|
|
|
|
maskCanvas.add(maskPath);
|
|
|
|
|
|
maskCanvas.renderAll();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有羽化,需要进行后处理
|
|
|
|
|
|
if (featherAmount > 0) {
|
2025-07-22 21:40:39 +08:00
|
|
|
|
return await applyCanvasBlur(
|
|
|
|
|
|
maskCanvas,
|
|
|
|
|
|
featherAmount * qualityMultiplier
|
|
|
|
|
|
);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成遮罩图像
|
|
|
|
|
|
const maskDataURL = maskCanvas.toDataURL({
|
|
|
|
|
|
format: "png",
|
|
|
|
|
|
quality: 1.0,
|
|
|
|
|
|
multiplier: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理遮罩画布
|
|
|
|
|
|
maskCanvas.dispose();
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 高级遮罩图像创建完成");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
return maskDataURL;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("创建高级遮罩图像失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 对画布应用模糊效果
|
|
|
|
|
|
* @param {fabric.StaticCanvas} canvas 要处理的画布
|
|
|
|
|
|
* @param {Number} blurAmount 模糊值
|
|
|
|
|
|
* @returns {Promise<String>} 处理后的DataURL
|
|
|
|
|
|
*/
|
|
|
|
|
|
const applyCanvasBlur = async (canvas, blurAmount) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取原始图像数据
|
|
|
|
|
|
const originalDataURL = canvas.toDataURL({
|
|
|
|
|
|
format: "png",
|
|
|
|
|
|
quality: 1.0,
|
|
|
|
|
|
multiplier: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const img = new Image();
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
|
// 创建一个新的Canvas进行模糊处理
|
|
|
|
|
|
const blurCanvas = document.createElement("canvas");
|
|
|
|
|
|
const ctx = blurCanvas.getContext("2d");
|
|
|
|
|
|
|
|
|
|
|
|
blurCanvas.width = canvas.width;
|
|
|
|
|
|
blurCanvas.height = canvas.height;
|
|
|
|
|
|
|
|
|
|
|
|
// 应用CSS滤镜模糊
|
|
|
|
|
|
ctx.filter = `blur(${Math.max(1, blurAmount / 2)}px)`;
|
|
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 重置滤镜
|
|
|
|
|
|
ctx.filter = "none";
|
|
|
|
|
|
|
|
|
|
|
|
const blurredDataURL = blurCanvas.toDataURL("image/png", 1.0);
|
|
|
|
|
|
resolve(blurredDataURL);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
img.onerror = () => {
|
|
|
|
|
|
reject(new Error("处理模糊效果失败"));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
img.src = originalDataURL;
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("应用画布模糊失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建实体遮罩路径(将描边转换为填充)
|
|
|
|
|
|
* @param {Object} clippingObject 原始裁剪对象
|
|
|
|
|
|
* @param {Object} selectionBounds 选区边界框
|
|
|
|
|
|
* @param {Number} qualityMultiplier 质量倍数
|
|
|
|
|
|
* @returns {Promise<fabric.Object>} 处理后的遮罩路径对象
|
|
|
|
|
|
*/
|
2025-07-22 21:40:39 +08:00
|
|
|
|
const createSolidMaskPath = async (
|
|
|
|
|
|
clippingObject,
|
|
|
|
|
|
selectionBounds,
|
|
|
|
|
|
qualityMultiplier
|
|
|
|
|
|
) => {
|
2025-06-29 23:29:47 +08:00
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("🔧 创建实体遮罩路径,处理描边转填充");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 克隆原始对象
|
|
|
|
|
|
const maskPath = await cloneObjectAsync(clippingObject);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有描边需要处理
|
|
|
|
|
|
const hasStroke = maskPath.stroke && maskPath.strokeWidth > 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (hasStroke) {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(
|
|
|
|
|
|
// `📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`
|
|
|
|
|
|
// );
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 对于有描边的路径,我们需要更精确的处理
|
|
|
|
|
|
const strokeWidth = maskPath.strokeWidth;
|
|
|
|
|
|
|
|
|
|
|
|
// 方法1: 如果是简单的几何形状(矩形、圆形等),可以通过调整尺寸来补偿描边
|
2025-07-22 21:40:39 +08:00
|
|
|
|
if (
|
|
|
|
|
|
maskPath.type === "rect" ||
|
|
|
|
|
|
maskPath.type === "circle" ||
|
|
|
|
|
|
maskPath.type === "ellipse"
|
|
|
|
|
|
) {
|
2025-06-29 23:29:47 +08:00
|
|
|
|
// 对于矩形和椭圆,增加宽高来包含描边
|
|
|
|
|
|
const strokeOffset = strokeWidth;
|
|
|
|
|
|
|
|
|
|
|
|
maskPath.set({
|
2025-07-22 21:40:39 +08:00
|
|
|
|
left:
|
|
|
|
|
|
(maskPath.left - selectionBounds.left - strokeOffset / 2) *
|
|
|
|
|
|
qualityMultiplier,
|
|
|
|
|
|
top:
|
|
|
|
|
|
(maskPath.top - selectionBounds.top - strokeOffset / 2) *
|
|
|
|
|
|
qualityMultiplier,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
|
|
|
|
|
|
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
|
|
|
|
|
|
width: (maskPath.width || 0) + strokeOffset,
|
|
|
|
|
|
height: (maskPath.height || 0) + strokeOffset,
|
|
|
|
|
|
fill: "#ffffff",
|
|
|
|
|
|
stroke: "",
|
|
|
|
|
|
strokeWidth: 0,
|
|
|
|
|
|
selectable: false,
|
|
|
|
|
|
evented: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 对于复杂路径,使用缩放方式来近似包含描边区域
|
|
|
|
|
|
const pathBounds = maskPath.getBoundingRect(true, true);
|
|
|
|
|
|
const minDimension = Math.min(pathBounds.width, pathBounds.height);
|
|
|
|
|
|
const expandRatio = 1 + (strokeWidth * 2) / minDimension;
|
|
|
|
|
|
const strokeOffset = strokeWidth / 2;
|
|
|
|
|
|
|
|
|
|
|
|
maskPath.set({
|
2025-07-22 21:40:39 +08:00
|
|
|
|
left:
|
|
|
|
|
|
(maskPath.left - selectionBounds.left - strokeOffset) *
|
|
|
|
|
|
qualityMultiplier,
|
|
|
|
|
|
top:
|
|
|
|
|
|
(maskPath.top - selectionBounds.top - strokeOffset) *
|
|
|
|
|
|
qualityMultiplier,
|
2025-06-29 23:29:47 +08:00
|
|
|
|
scaleX: (maskPath.scaleX || 1) * qualityMultiplier * expandRatio,
|
|
|
|
|
|
scaleY: (maskPath.scaleY || 1) * qualityMultiplier * expandRatio,
|
|
|
|
|
|
fill: "#ffffff",
|
|
|
|
|
|
stroke: "",
|
|
|
|
|
|
strokeWidth: 0,
|
|
|
|
|
|
selectable: false,
|
|
|
|
|
|
evented: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(`✅ 描边已转换为填充,类型: ${maskPath.type}`);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 没有描边,直接处理位置和缩放
|
|
|
|
|
|
maskPath.set({
|
|
|
|
|
|
left: (maskPath.left - selectionBounds.left) * qualityMultiplier,
|
|
|
|
|
|
top: (maskPath.top - selectionBounds.top) * qualityMultiplier,
|
|
|
|
|
|
scaleX: (maskPath.scaleX || 1) * qualityMultiplier,
|
|
|
|
|
|
scaleY: (maskPath.scaleY || 1) * qualityMultiplier,
|
|
|
|
|
|
fill: "#ffffff", // 白色表示可见区域
|
|
|
|
|
|
stroke: "", // 确保没有描边
|
|
|
|
|
|
strokeWidth: 0,
|
|
|
|
|
|
selectable: false,
|
|
|
|
|
|
evented: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保对象在画布中心正确对齐
|
|
|
|
|
|
maskPath.setCoords();
|
|
|
|
|
|
|
|
|
|
|
|
return maskPath;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("创建实体遮罩路径失败:", error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 优化边界计算,确保遮罩和内容对齐
|
|
|
|
|
|
* @param {Object} clippingObject 裁剪对象
|
|
|
|
|
|
* @param {Array} fabricObjects 内容对象数组
|
|
|
|
|
|
* @returns {Object} 优化后的边界框信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
const calculateOptimizedBounds = (clippingObject, fabricObjects) => {
|
|
|
|
|
|
try {
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("📐 计算优化后的边界框");
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取裁剪对象的边界框(包含描边)
|
|
|
|
|
|
const clippingBounds = clippingObject.getBoundingRect(true, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有描边,需要调整边界框
|
|
|
|
|
|
if (clippingObject.stroke && clippingObject.strokeWidth > 0) {
|
|
|
|
|
|
const strokeWidth = clippingObject.strokeWidth;
|
|
|
|
|
|
const halfStroke = strokeWidth / 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 扩展边界框以包含完整的描边区域
|
|
|
|
|
|
clippingBounds.left -= halfStroke;
|
|
|
|
|
|
clippingBounds.top -= halfStroke;
|
|
|
|
|
|
clippingBounds.width += strokeWidth;
|
|
|
|
|
|
clippingBounds.height += strokeWidth;
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log(`🖊️ 调整描边边界框,描边宽度: ${strokeWidth}`);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算内容对象的边界框
|
|
|
|
|
|
const contentBounds = calculateBounds(fabricObjects);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用裁剪边界框作为最终的选区边界框
|
|
|
|
|
|
const optimizedBounds = {
|
|
|
|
|
|
...clippingBounds,
|
|
|
|
|
|
// 确保边界框不为负数或零
|
|
|
|
|
|
width: Math.max(1, clippingBounds.width),
|
|
|
|
|
|
height: Math.max(1, clippingBounds.height),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-19 16:57:11 +08:00
|
|
|
|
// console.log("✅ 边界框优化完成", {
|
|
|
|
|
|
// original: clippingObject.getBoundingRect(true, true),
|
|
|
|
|
|
// optimized: optimizedBounds,
|
|
|
|
|
|
// hasStroke: !!(clippingObject.stroke && clippingObject.strokeWidth > 0),
|
|
|
|
|
|
// });
|
2025-06-29 23:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
return optimizedBounds;
|
|
|
|
|
|
} catch (error) {
|
2026-01-02 11:24:11 +08:00
|
|
|
|
console.warn("计算优化边界框失败:", error);
|
2025-06-29 23:29:47 +08:00
|
|
|
|
// 返回原始计算方式作为备选
|
|
|
|
|
|
return clippingObject.getBoundingRect(true, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|