Files
aida_front/src/component/Canvas/CanvasEditor/utils/selectionToImage.js
2026-01-19 16:57:11 +08:00

1261 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 栅格化帮助
import { fabric } from "fabric-with-all";
import { SpecialLayerId } from "./layerHelper";
/**
* 创建栅格化图像 - 重构版本
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
* @returns {Promise<fabric.Image|fabric.Group|string>} 栅格化后的图像对象或DataURL
* @private
*/
export const createRasterizedImage = async ({
canvas, // 画布对象 必填
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
maskObject = null, // 用于裁剪的对象 - 可选
clipPath = null, // 裁剪路径对象 - 可选优先级高于maskObject
trimWhitespace = true, // 是否裁剪空白区域
trimPadding = 0, // 裁剪边距
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
preserveOriginalQuality = true, // 是否保持原始质量(新增)
selectionManager = null, // 选区管理器,用于获取羽化值等设置
restoreOpacityInRedGreen, // 是否在红绿图模式下恢复透明度
isEnhanceImg, // 是否是增强图片
} = {}) => {
try {
// console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
// 确保有对象需要栅格化
if (fabricObjects.length === 0) {
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
return null;
}
// 处理裁剪对象优先使用clipPath
const clippingObject = clipPath || maskObject;
// 如果保持原始质量且有裁剪对象,使用新的裁剪方法
if (preserveOriginalQuality && clippingObject) {
return await createClippedObjects({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
selectionManager, // 传递选区管理器
isEnhanceImg, // 是否是增强图片
});
}
// 如果只是简单复制而不需要裁剪,直接克隆对象
if (!clippingObject) {
return await createSimpleClone({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
});
}
// 兼容原有的离屏渲染方法(作为备选方案)
return await createLegacyRasterization({
canvas,
fabricObjects,
clippingObject,
scaleFactor,
quality,
format,
isReturenDataURL,
});
} catch (error) {
console.warn("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
/**
* 创建带裁剪的对象 - 新方法
* 直接复制原对象并应用裁剪路径,保持原始质量
*/
const createClippedObjects = async ({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
selectionManager = null, // 新增选区管理器参数
isEnhanceImg, // 是否是增强图片
}) => {
try {
// console.log("🎯 使用新的图像遮罩裁剪方法创建对象");
// 使用优化后的边界计算,确保包含描边区域
const optimizedBounds = calculateOptimizedBounds(
clippingObject,
fabricObjects
);
// console.log("📐 优化后的选区边界框:", optimizedBounds);
// 获取羽化值
let featherAmount = 0;
if (
selectionManager &&
typeof selectionManager.getFeatherAmount === "function"
) {
featherAmount = selectionManager.getFeatherAmount();
// console.log(`🌟 应用羽化效果: ${featherAmount}px`);
}
// 方法1如果只需要返回DataURL使用画布裁剪方法
if (isReturenDataURL) {
return await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount,
isEnhanceImg, // 是否是增强图片
});
}
// 方法2如果需要返回fabric对象先生成DataURL再转换为fabric对象
const clippedDataURL = await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds: optimizedBounds, // 使用优化后的边界框
featherAmount,
isEnhanceImg, // 是否是增强图片
});
// 将DataURL转换为fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(clippedDataURL);
// 使用fabric原生方法恢复到选区的原始大小和位置
fabricImage.scaleToWidth(optimizedBounds.width);
fabricImage.scaleToHeight(optimizedBounds.height);
// 设置到选区的原始位置(中心点)
fabricImage.set({
left: optimizedBounds.left + optimizedBounds.width / 2,
top: optimizedBounds.top + optimizedBounds.height / 2,
originX: "center",
originY: "center",
selectable: true,
evented: true,
// hasControls: true,
// hasBorders: true,
custom: {
type: "clipped",
clippedAt: new Date().toISOString(),
hasClipping: true,
preservedQuality: true,
originalBounds: optimizedBounds, // 保存优化后的边界框
restoredToOriginalSize: true,
usedImageMask: true, // 标记使用了图像遮罩
featherAmount: featherAmount,
boundaryOptimized: true, // 标记使用了边界优化
},
});
// 更新坐标
fabricImage.setCoords();
// console.log("✅ 返回裁剪后的fabric对象已恢复到优化后的原始大小和位置");
return fabricImage;
} catch (error) {
console.warn("创建裁剪对象失败:", error);
throw error;
}
};
/**
* 通过画布裁剪生成DataURL
* 裁剪掉选区以外的内容,保持和选区大小一致
*/
const createClippedDataURLByCanvas = async ({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
featherAmount = 0,
isEnhanceImg = false, // 是否是增强图片
}) => {
try {
// console.log("🖼️ 使用图像遮罩裁剪方法生成DataURL");
// 使用优化后的边界计算,确保包含描边区域
// 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;
// console.log("使用高分辨率以保证质量:" + isEnhanceImg, optimizedBounds);
const canvasWidth = Math.ceil(optimizedBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(optimizedBounds.height * qualityMultiplier);
// console.log(
// `📏 优化后画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
// );
// console.log("🎯 边界框对比:", {
// original: selectionBounds,
// optimized: optimizedBounds,
// });
// 步骤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,
});
// 步骤3: 使用遮罩合成最终结果
const clippedDataURL = await applyImageMask({
contentImageDataURL,
maskImageDataURL,
canvasWidth,
canvasHeight,
});
// console.log("✅ 图像遮罩裁剪完成生成DataURL");
return clippedDataURL;
} catch (error) {
console.error("图像遮罩裁剪失败:", error);
throw error;
}
};
/**
* 创建简单克隆对象
* 当不需要裁剪时,直接克隆原对象
*/
const createSimpleClone = async ({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
}) => {
try {
// console.log("📋 创建简单克隆对象");
const clonedObjects = [];
// 克隆所有对象
for (const obj of fabricObjects) {
const clonedObj = await cloneObjectAsync(obj);
clonedObj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
...clonedObj.custom,
type: "cloned",
clonedAt: new Date().toISOString(),
preservedQuality: true,
},
});
clonedObjects.push(clonedObj);
}
// 如果需要返回DataURL需要渲染
if (isReturenDataURL) {
return await renderObjectsToDataURL(clonedObjects, quality, format);
}
// 如果只有一个对象,直接返回
if (clonedObjects.length === 1) {
return clonedObjects[0];
}
// 创建组合
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;
} 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 {
// 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();
// console.log("✅ 裁剪对象渲染完成");
return dataURL;
} catch (error) {
console.error("渲染裁剪对象失败:", error);
throw error;
}
};
/**
* 兼容的离屏渲染方法(原有逻辑,作为备选)
*/
const createLegacyRasterization = async ({
canvas,
fabricObjects,
clippingObject,
scaleFactor,
quality,
format,
isReturenDataURL,
isCropByBg, // 是否根据背景裁剪
isEnhanceImg, // 是否是增强图片
}) => {
// 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, // 是否是增强图片
});
};
/**
* 计算对象的绝对边界框和相对边界框
* @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,
isCropByBg, // 是否根据背景裁剪
isEnhanceImg, // 是否是增强图片
}) => {
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);
const canvasHeight = Math.ceil(renderBounds.height);
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) {
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,
})
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;
};
/**
* 将路径对象转换为遮罩图像
* @param {Object} clippingObject 裁剪路径对象
* @param {Object} selectionBounds 选区边界框
* @param {Number} qualityMultiplier 质量倍数
* @returns {Promise<String>} 遮罩图像的DataURL
*/
const createMaskImageFromPath = async ({
clippingObject,
selectionBounds,
qualityMultiplier,
}) => {
try {
// console.log("🎭 创建路径遮罩图像");
// 创建专门用于渲染遮罩的画布
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
);
// 添加路径到遮罩画布
maskCanvas.add(maskPath);
maskCanvas.renderAll();
// 生成遮罩图像
const maskDataURL = maskCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1,
});
// 清理遮罩画布
maskCanvas.dispose();
// console.log("✅ 遮罩图像创建完成");
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,
}) => {
try {
// console.log("🖼️ 渲染内容图像");
// 创建内容渲染画布
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,
});
// 克隆并添加所有需要渲染的对象
for (let obj of fabricObjects) {
let clonedObj = await cloneObjectAsync(obj);
// 调整对象位置:将选区左上角作为新的原点(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,
});
// 如果有裁剪路径,也需要调整裁剪路径
if (clonedObj.clipPath && obj.id !== SpecialLayerId.COLOR) {
clonedObj.clipPath.set({
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(); // 更新裁剪路径坐标
}
// if(obj.globalCompositeOperation === "multiply"){
// clonedObj.clipPath = null;
// }
contentCanvas.add(clonedObj);
}
// 渲染内容画布
contentCanvas.renderAll();
// 生成内容图像
const contentDataURL = contentCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1,
});
// 清理内容画布
contentCanvas.dispose();
// console.log("✅ 内容图像渲染完成");
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 {
// console.log("🎯 应用图像遮罩");
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);
// console.log("✅ 图像遮罩应用完成");
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 {
// console.log(`🎭 创建高级遮罩图像 (羽化: ${featherAmount})`);
// 创建专门用于渲染遮罩的画布
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
);
// 如果有羽化值,添加模糊效果
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
);
}
// 生成遮罩图像
const maskDataURL = maskCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1,
});
// 清理遮罩画布
maskCanvas.dispose();
// console.log("✅ 高级遮罩图像创建完成");
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
) => {
try {
// console.log("🔧 创建实体遮罩路径,处理描边转填充");
// 克隆原始对象
const maskPath = await cloneObjectAsync(clippingObject);
// 检查是否有描边需要处理
const hasStroke = maskPath.stroke && maskPath.strokeWidth > 0;
if (hasStroke) {
// console.log(
// `📏 检测到描边: ${maskPath.stroke}, 宽度: ${maskPath.strokeWidth}`
// );
// 对于有描边的路径,我们需要更精确的处理
const strokeWidth = maskPath.strokeWidth;
// 方法1: 如果是简单的几何形状(矩形、圆形等),可以通过调整尺寸来补偿描边
if (
maskPath.type === "rect" ||
maskPath.type === "circle" ||
maskPath.type === "ellipse"
) {
// 对于矩形和椭圆,增加宽高来包含描边
const strokeOffset = strokeWidth;
maskPath.set({
left:
(maskPath.left - selectionBounds.left - strokeOffset / 2) *
qualityMultiplier,
top:
(maskPath.top - selectionBounds.top - strokeOffset / 2) *
qualityMultiplier,
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,
scaleX: (maskPath.scaleX || 1) * qualityMultiplier * expandRatio,
scaleY: (maskPath.scaleY || 1) * qualityMultiplier * expandRatio,
fill: "#ffffff",
stroke: "",
strokeWidth: 0,
selectable: false,
evented: false,
});
}
// console.log(`✅ 描边已转换为填充,类型: ${maskPath.type}`);
} 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 {
// console.log("📐 计算优化后的边界框");
// 获取裁剪对象的边界框(包含描边)
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;
// console.log(`🖊️ 调整描边边界框,描边宽度: ${strokeWidth}`);
}
// 计算内容对象的边界框
const contentBounds = calculateBounds(fabricObjects);
// 使用裁剪边界框作为最终的选区边界框
const optimizedBounds = {
...clippingBounds,
// 确保边界框不为负数或零
width: Math.max(1, clippingBounds.width),
height: Math.max(1, clippingBounds.height),
};
// console.log("✅ 边界框优化完成", {
// original: clippingObject.getBoundingRect(true, true),
// optimized: optimizedBounds,
// hasStroke: !!(clippingObject.stroke && clippingObject.strokeWidth > 0),
// });
return optimizedBounds;
} catch (error) {
console.warn("计算优化边界框失败:", error);
// 返回原始计算方式作为备选
return clippingObject.getBoundingRect(true, true);
}
};