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

1261 lines
36 KiB
JavaScript
Raw Normal View History

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
/**
* 创建栅格化图像 - 重构版本
* 采用复制原对象+裁剪路径的方式保持原始质量和准确位置
* @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, // 是否裁剪空白区域
trimPadding = 0, // 裁剪边距
2025-06-23 00:40:45 +08:00
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
preserveOriginalQuality = true, // 是否保持原始质量(新增)
2025-06-29 23:29:47 +08:00
selectionManager = null, // 选区管理器,用于获取羽化值等设置
restoreOpacityInRedGreen, // 是否在红绿图模式下恢复透明度
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;
// 如果保持原始质量且有裁剪对象,使用新的裁剪方法
if (preserveOriginalQuality && clippingObject) {
return await createClippedObjects({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
2025-06-29 23:29:47 +08:00
selectionManager, // 传递选区管理器
isEnhanceImg, // 是否是增强图片
});
}
2025-06-23 00:40:45 +08:00
// 如果只是简单复制而不需要裁剪,直接克隆对象
if (!clippingObject) {
return await createSimpleClone({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
});
}
2025-06-23 00:40:45 +08:00
// 兼容原有的离屏渲染方法(作为备选方案)
return await createLegacyRasterization({
2025-06-23 00:40:45 +08:00
canvas,
fabricObjects,
2025-06-23 00:40:45 +08:00
clippingObject,
scaleFactor,
2025-06-23 00:40:45 +08:00
quality,
format,
isReturenDataURL,
});
} catch (error) {
2026-01-02 11:24:11 +08:00
console.warn("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
2025-06-23 00:40:45 +08:00
/**
* 创建带裁剪的对象 - 新方法
* 直接复制原对象并应用裁剪路径保持原始质量
*/
const createClippedObjects = async ({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
2025-06-29 23:29:47 +08:00
selectionManager = null, // 新增选区管理器参数
isEnhanceImg, // 是否是增强图片
}) => {
try {
2026-01-19 16:57:11 +08:00
// console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
2025-06-29 23:29:47 +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;
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
// 方法1如果只需要返回DataURL使用画布裁剪方法
2025-06-23 00:40:45 +08:00
if (isReturenDataURL) {
return await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
2025-06-29 23:29:47 +08:00
selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount,
isEnhanceImg, // 是否是增强图片
});
2025-06-23 00:40:45 +08:00
}
// 方法2如果需要返回fabric对象先生成DataURL再转换为fabric对象
const clippedDataURL = await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
2025-06-29 23:29:47 +08:00
selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount,
isEnhanceImg, // 是否是增强图片
});
// 将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);
// 设置到选区的原始位置(中心点)
fabricImage.set({
2025-06-29 23:29:47 +08:00
left: optimizedBounds.left + optimizedBounds.width / 2,
top: optimizedBounds.top + optimizedBounds.height / 2,
originX: "center",
originY: "center",
selectable: true,
evented: true,
2025-06-29 23:29:47 +08:00
// hasControls: true,
// hasBorders: true,
custom: {
type: "clipped",
clippedAt: new Date().toISOString(),
hasClipping: true,
preservedQuality: true,
2025-06-29 23:29:47 +08:00
originalBounds: optimizedBounds, // 保存优化后的边界框
restoredToOriginalSize: true,
2025-06-29 23:29:47 +08:00
usedImageMask: true, // 标记使用了图像遮罩
featherAmount: featherAmount,
boundaryOptimized: true, // 标记使用了边界优化
},
});
// 更新坐标
fabricImage.setCoords();
2026-01-19 16:57:11 +08:00
// console.log("✅ 返回裁剪后的fabric对象已恢复到优化后的原始大小和位置");
return fabricImage;
} catch (error) {
2026-01-02 11:24:11 +08:00
console.warn("创建裁剪对象失败:", error);
throw error;
}
};
/**
* 通过画布裁剪生成DataURL
* 裁剪掉选区以外的内容保持和选区大小一致
*/
const createClippedDataURLByCanvas = async ({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
2025-06-29 23:29:47 +08:00
featherAmount = 0,
isEnhanceImg = false, // 是否是增强图片
}) => {
try {
2026-01-19 16:57:11 +08:00
// console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
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,
}
// 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1;
const qualityMultiplier = !!isEnhanceImg ? Math.max(2, pixelRatio) : 1;
2026-01-19 16:57:11 +08:00
// console.log("使用高分辨率以保证质量:" + isEnhanceImg, optimizedBounds);
2025-06-29 23:29:47 +08:00
const canvasWidth = Math.ceil(optimizedBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(optimizedBounds.height * qualityMultiplier);
2026-01-19 16:57:11 +08:00
// console.log(
// `📏 优化后画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
// );
// console.log("🎯 边界框对比:", {
// original: selectionBounds,
// optimized: optimizedBounds,
// });
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-29 23:29:47 +08:00
// 步骤3: 使用遮罩合成最终结果
const clippedDataURL = await applyImageMask({
contentImageDataURL,
maskImageDataURL,
canvasWidth,
canvasHeight,
});
2026-01-19 16:57:11 +08:00
// console.log("✅ 图像遮罩裁剪完成生成DataURL");
2025-06-29 23:29:47 +08:00
return clippedDataURL;
} catch (error) {
2025-06-29 23:29:47 +08:00
console.error("图像遮罩裁剪失败:", error);
throw error;
}
};
/**
* 创建简单克隆对象
* 当不需要裁剪时直接克隆原对象
*/
const createSimpleClone = async ({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
}) => {
try {
2026-01-19 16:57:11 +08:00
// console.log("📋 创建简单克隆对象");
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: {
...clonedObj.custom,
type: "cloned",
clonedAt: new Date().toISOString(),
preservedQuality: true,
2025-06-23 00:40:45 +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
}
// 创建组合
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) {
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");
// 计算所有裁剪对象的总边界框
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("✅ 裁剪对象渲染完成");
return dataURL;
} catch (error) {
console.error("渲染裁剪对象失败:", error);
throw error;
2025-06-23 00:40:45 +08:00
}
};
/**
* 兼容的离屏渲染方法原有逻辑作为备选
*/
const createLegacyRasterization = async ({
canvas,
fabricObjects,
clippingObject,
scaleFactor,
quality,
format,
isReturenDataURL,
isCropByBg, // 是否根据背景裁剪
isEnhanceImg, // 是否是增强图片
}) => {
2026-01-19 16:57:11 +08:00
// console.log("⚠️ 使用兼容的离屏渲染方法");
// 这里保留原有的离屏渲染逻辑作为备选方案
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
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,
isCropByBg, // 是否根据背景裁剪
isEnhanceImg, // 是否是增强图片
});
};
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,
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;
}
// 设置离屏画布尺寸,并应用高清倍数
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
*/
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,
});
// 克隆路径对象并处理描边转填充
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
*/
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,
});
// 如果有裁剪路径,也需要调整裁剪路径
2026-01-06 14:17:04 +08:00
if (clonedObj.clipPath && obj.id !== SpecialLayerId.COLOR) {
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,
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,
});
// 克隆路径对象并处理描边转填充
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) {
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>} 处理后的遮罩路径对象
*/
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: 如果是简单的几何形状(矩形、圆形等),可以通过调整尺寸来补偿描边
if (
maskPath.type === "rect" ||
maskPath.type === "circle" ||
maskPath.type === "ellipse"
) {
2025-06-29 23:29:47 +08:00
// 对于矩形和椭圆,增加宽高来包含描边
const strokeOffset = strokeWidth;
maskPath.set({
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({
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);
}
};