feat(CanvasManager): enhance image layer management and event handling

This commit is contained in:
bighuixiang
2025-06-26 00:37:07 +08:00
parent afa3b69f71
commit 2fcba962d1
16 changed files with 901 additions and 448 deletions

View File

@@ -1,9 +1,10 @@
// 栅格化帮助
import { fabric } from "fabric-with-all";
/**
* 创建栅格化图像
* 使用增强版栅格化方法,不受原始画布变换影响
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
* 创建栅格化图像 - 重构版本
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
* @returns {Promise<fabric.Image|fabric.Group|string>} 栅格化后的图像对象或DataURL
* @private
*/
export const createRasterizedImage = async ({
@@ -12,11 +13,12 @@ export const createRasterizedImage = async ({
maskObject = null, // 用于裁剪的对象 - 可选
clipPath = null, // 裁剪路径对象 - 可选优先级高于maskObject
trimWhitespace = true, // 是否裁剪空白区域
trimPadding = 1, // 裁剪边距
trimPadding = 0, // 裁剪边距
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
preserveOriginalQuality = true, // 是否保持原始质量(新增)
} = {}) => {
try {
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
@@ -29,79 +31,448 @@ export const createRasterizedImage = async ({
// 处理裁剪对象优先使用clipPath
const clippingObject = clipPath || maskObject;
// 如果保持原始质量且有裁剪对象,使用新的裁剪方法
if (preserveOriginalQuality && clippingObject) {
return await createClippedObjects({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
});
}
// 高清倍数
const currentZoom = canvas.getZoom?.() || 1;
// 如果只是简单复制而不需要裁剪,直接克隆对象
if (!clippingObject) {
return await createSimpleClone({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
});
}
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.min(scaleFactor, 3); // 最大不能大于3
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
// 计算绝对边界框(原始尺寸)和相对边界框(当前缩放后的尺寸)
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
console.log("📏 绝对边界框:", absoluteBounds);
console.log("📏 相对边界框:", relativeBounds);
// 使用绝对边界框创建高质量的离屏渲染
const rasterizedImage = await createOffscreenRasterization({
// 兼容原有的离屏渲染方法(作为备选方案)
return await createLegacyRasterization({
canvas,
objects: fabricObjects,
absoluteBounds,
relativeBounds,
scaleFactor,
fabricObjects,
clippingObject,
trimWhitespace,
trimPadding,
scaleFactor,
quality,
format,
currentZoom,
isReturenDataURL,
});
} catch (error) {
console.error("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
if (!rasterizedImage) {
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
return null;
}
/**
* 创建带裁剪的对象 - 新方法
* 直接复制原对象并应用裁剪路径,保持原始质量
*/
const createClippedObjects = async ({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
}) => {
try {
console.log("🎯 使用新的裁剪方法创建对象");
// 获取选区边界框
const selectionBounds = clippingObject.getBoundingRect(true);
console.log("📐 选区边界框:", selectionBounds);
// 方法1如果只需要返回DataURL使用画布裁剪方法
if (isReturenDataURL) {
console.log("✅ 栅格化图像创建成功返回DataURL");
return rasterizedImage; // 返回DataURL
return await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
});
}
// 设置栅格化图像的属性
if (rasterizedImage) {
rasterizedImage.set({
// 方法2如果需要返回fabric对象先生成DataURL再转换为fabric对象
const clippedDataURL = await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
});
// 将DataURL转换为fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(clippedDataURL);
// 使用fabric原生方法恢复到选区的原始大小和位置
fabricImage.scaleToWidth(selectionBounds.width);
fabricImage.scaleToHeight(selectionBounds.height);
// 设置到选区的原始位置(中心点)
fabricImage.set({
left: selectionBounds.left + selectionBounds.width / 2,
top: selectionBounds.top + selectionBounds.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: selectionBounds,
restoredToOriginalSize: true,
},
});
// 更新坐标
fabricImage.setCoords();
console.log("✅ 返回裁剪后的fabric对象已恢复到原始大小和位置");
return fabricImage;
} catch (error) {
console.error("创建裁剪对象失败:", error);
throw error;
}
};
/**
* 通过画布裁剪生成DataURL
* 裁剪掉选区以外的内容,保持和选区大小一致
*/
const createClippedDataURLByCanvas = async ({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
}) => {
try {
console.log("🖼️ 使用画布裁剪方法生成DataURL");
// 创建临时画布,尺寸与选区完全一致
const tempCanvas = new fabric.StaticCanvas();
// 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1;
const qualityMultiplier = Math.max(2, pixelRatio);
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
tempCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
console.log(
`📏 临时画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
);
// 克隆并添加所有需要裁剪的对象
for (const obj of fabricObjects) {
const 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,
});
tempCanvas.add(clonedObj);
}
// 克隆裁剪路径并调整位置
const clipPath = await cloneObjectAsync(clippingObject);
clipPath.set({
left: (clipPath.left - selectionBounds.left) * qualityMultiplier,
top: (clipPath.top - selectionBounds.top) * qualityMultiplier,
scaleX: (clipPath.scaleX || 1) * qualityMultiplier,
scaleY: (clipPath.scaleY || 1) * qualityMultiplier,
fill: "transparent",
stroke: "",
strokeWidth: 0,
absolutePositioned: true,
});
// 为整个画布设置裁剪路径
tempCanvas.clipPath = clipPath;
// 渲染画布
tempCanvas.renderAll();
// 生成高质量DataURL
const dataURL = tempCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1, // 已经通过尺寸处理了缩放
});
// 清理临时画布
tempCanvas.dispose();
console.log("✅ 画布裁剪完成生成DataURL");
return dataURL;
} 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: {
type: "rasterized",
rasterizedAt: new Date().toISOString(),
objectCount: fabricObjects.length,
absoluteBounds,
relativeBounds,
originalZoom: currentZoom,
hasClipping: !!clippingObject,
...clonedObj.custom,
type: "cloned",
clonedAt: new Date().toISOString(),
preservedQuality: true,
},
});
console.log(`✅ 栅格化图像创建完成`);
clonedObjects.push(clonedObj);
}
return rasterizedImage;
// 如果需要返回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 new Error(`栅格化失败: ${error.message}`);
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,
}) => {
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,
});
};
/**
* 计算对象的绝对边界框和相对边界框
* @param {Array} fabricObjects fabric对象数组
@@ -203,8 +574,8 @@ const createOffscreenRasterization = async ({
}
// 设置离屏画布尺寸,并应用高清倍数
const canvasWidth = Math.ceil(renderBounds.width * scaleFactor);
const canvasHeight = Math.ceil(renderBounds.height * scaleFactor);
const canvasWidth = Math.ceil(renderBounds.width);
const canvasHeight = Math.ceil(renderBounds.height);
offscreenCanvas.setDimensions({
width: canvasWidth,