合并画布
This commit is contained in:
@@ -1238,3 +1238,905 @@ export const ImageUtils = {
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 栅格化画布对象为图像
|
||||
* 参考fabric.brushes.js中的convertToImg方法,考虑画布变换参数
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.objects - 要栅格化的对象数组
|
||||
* @param {Object} options.bounds - 边界框 {left, top, width, height} (可选)
|
||||
* @param {boolean} options.trimWhitespace - 是否裁剪空白区域,默认true
|
||||
* @param {number} options.trimPadding - 裁剪时保留的空白边距,默认10像素
|
||||
* @param {number} options.quality - 图像质量 0-1,默认1
|
||||
* @param {string} options.format - 图像格式 'png'|'jpeg',默认'png'
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeCanvasObjects(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
canvas,
|
||||
objects = [],
|
||||
bounds = null,
|
||||
trimWhitespace = true,
|
||||
trimPadding = 10,
|
||||
quality = 1,
|
||||
format = "png",
|
||||
} = options;
|
||||
|
||||
if (!canvas || !Array.isArray(objects)) {
|
||||
reject(new Error("无效的参数:需要画布实例和对象数组"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (objects.length === 0) {
|
||||
reject(new Error("没有对象可栅格化"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用改进的栅格化方法
|
||||
_rasterizeUsingCanvasCopy(canvas, objects, {
|
||||
trimWhitespace,
|
||||
trimPadding,
|
||||
quality,
|
||||
format,
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} catch (error) {
|
||||
console.error("栅格化对象失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用画布复制方式进行栅格化(参考convertToImg实现)
|
||||
* @param {fabric.Canvas} canvas - fabric画布实例
|
||||
* @param {Array} objects - 要栅格化的对象数组
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
* @private
|
||||
*/
|
||||
function _rasterizeUsingCanvasCopy(canvas, objects, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
trimWhitespace = true,
|
||||
trimPadding = 10,
|
||||
quality = 1,
|
||||
format = "png",
|
||||
} = options;
|
||||
|
||||
// 保存原始状态
|
||||
const originalObjects = canvas.getObjects();
|
||||
const originalViewportTransform = [...canvas.viewportTransform];
|
||||
const originalZoom = canvas.getZoom();
|
||||
|
||||
// 临时隐藏其他对象,只显示要栅格化的对象
|
||||
const objectsToHide = originalObjects.filter(
|
||||
(obj) => !objects.includes(obj)
|
||||
);
|
||||
|
||||
// 隐藏不需要的对象
|
||||
objectsToHide.forEach((obj) => {
|
||||
obj._originalVisible = obj.visible;
|
||||
obj.set("visible", false);
|
||||
});
|
||||
|
||||
// 确保要栅格化的对象可见
|
||||
objects.forEach((obj) => {
|
||||
obj._originalVisible = obj.visible;
|
||||
obj.set("visible", true);
|
||||
});
|
||||
|
||||
// 重新渲染画布以应用可见性变化
|
||||
canvas.renderAll();
|
||||
|
||||
// 等待一帧确保渲染完成
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
// 获取画布的像素比例
|
||||
const pixelRatio = canvas.getRetinaScaling();
|
||||
|
||||
// 复制画布元素(这会保持所有变换状态)
|
||||
const copiedCanvas = fabric.util.copyCanvasElement(
|
||||
canvas.lowerCanvasEl
|
||||
);
|
||||
|
||||
let finalCanvas = copiedCanvas;
|
||||
let trimOffset = { x: 0, y: 0 };
|
||||
|
||||
// 裁剪空白区域(如果需要,支持padding)
|
||||
if (trimWhitespace) {
|
||||
const trimResult = _trimCanvas(copiedCanvas, trimPadding);
|
||||
if (trimResult) {
|
||||
finalCanvas = trimResult.canvas;
|
||||
trimOffset = { x: trimResult.offset.x, y: trimResult.offset.y };
|
||||
}
|
||||
}
|
||||
|
||||
// 创建fabric图像对象
|
||||
const fabricImage = new fabric.Image(finalCanvas);
|
||||
|
||||
if (!fabricImage) {
|
||||
throw new Error("创建fabric图像失败");
|
||||
}
|
||||
|
||||
// 获取画布变换参数
|
||||
const pointerX = canvas.viewportTransform[4];
|
||||
const pointerY = canvas.viewportTransform[5];
|
||||
const zoom = canvas.getZoom();
|
||||
|
||||
// 计算最终位置(参考convertToImg的实现)
|
||||
const finalLeft = (trimOffset.x / pixelRatio - pointerX) / zoom;
|
||||
const finalTop = (trimOffset.y / pixelRatio - pointerY) / zoom;
|
||||
const finalScaleX = 1 / pixelRatio / zoom;
|
||||
const finalScaleY = 1 / pixelRatio / zoom;
|
||||
|
||||
// 设置图像属性
|
||||
fabricImage.set({
|
||||
id: generateId("rasterized_image_"),
|
||||
left: finalLeft,
|
||||
top: finalTop,
|
||||
scaleX: finalScaleX,
|
||||
scaleY: finalScaleY,
|
||||
selectable: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
custom: {
|
||||
type: "rasterized",
|
||||
originalObjects: objects.map((obj) => obj.id).filter(Boolean),
|
||||
rasterizedAt: new Date().toISOString(),
|
||||
trimPadding: trimPadding,
|
||||
},
|
||||
});
|
||||
|
||||
fabricImage.setCoords();
|
||||
|
||||
// 恢复对象的原始可见性
|
||||
_restoreObjectVisibility(originalObjects);
|
||||
|
||||
// 重新渲染画布
|
||||
canvas.renderAll();
|
||||
|
||||
resolve(fabricImage);
|
||||
} catch (error) {
|
||||
// 确保恢复对象状态
|
||||
_restoreObjectVisibility(originalObjects);
|
||||
canvas.renderAll();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复对象的原始可见性状态
|
||||
* @param {Array} objects - 对象数组
|
||||
* @private
|
||||
*/
|
||||
function _restoreObjectVisibility(objects) {
|
||||
objects.forEach((obj) => {
|
||||
if (obj.hasOwnProperty("_originalVisible")) {
|
||||
obj.set("visible", obj._originalVisible);
|
||||
delete obj._originalVisible;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 备用栅格化方法:使用toDataURL方式
|
||||
* 当画布复制方法不可用时的备选方案
|
||||
* @param {fabric.Canvas} canvas - fabric画布实例
|
||||
* @param {Array} objects - 要栅格化的对象数组
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
* @private
|
||||
*/
|
||||
function _rasterizeUsingDataURL(canvas, objects, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const { quality = 1, format = "png" } = options;
|
||||
|
||||
// 保存原始状态
|
||||
const originalObjects = canvas.getObjects();
|
||||
|
||||
// 临时移除其他对象
|
||||
const objectsToRemove = originalObjects.filter(
|
||||
(obj) => !objects.includes(obj)
|
||||
);
|
||||
objectsToRemove.forEach((obj) => {
|
||||
canvas.remove(obj);
|
||||
});
|
||||
|
||||
// 重新渲染画布
|
||||
canvas.renderAll();
|
||||
|
||||
// 获取画布数据URL
|
||||
const dataUrl = canvas.toDataURL({
|
||||
format: format,
|
||||
quality: quality,
|
||||
multiplier: canvas.getRetinaScaling(),
|
||||
});
|
||||
|
||||
// 恢复原始对象
|
||||
objectsToRemove.forEach((obj) => {
|
||||
canvas.add(obj);
|
||||
});
|
||||
|
||||
// 恢复原始渲染顺序
|
||||
canvas._objects = [...originalObjects];
|
||||
canvas.renderAll();
|
||||
|
||||
// 创建fabric图像
|
||||
fabric.Image.fromURL(
|
||||
dataUrl,
|
||||
(fabricImage) => {
|
||||
if (!fabricImage) {
|
||||
reject(new Error("创建fabric图像失败"));
|
||||
return;
|
||||
}
|
||||
|
||||
fabricImage.set({
|
||||
id: generateId("rasterized_image_"),
|
||||
left: 0,
|
||||
top: 0,
|
||||
selectable: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
custom: {
|
||||
type: "rasterized",
|
||||
originalObjects: objects.map((obj) => obj.id).filter(Boolean),
|
||||
rasterizedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
fabricImage.setCoords();
|
||||
resolve(fabricImage);
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格化画布对象为图像(兼容版本)
|
||||
* 自动选择最适合的栅格化方法
|
||||
* @param {Object} options - 配置选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeCanvasObjectsCompat(options = {}) {
|
||||
const { canvas, objects = [] } = options;
|
||||
|
||||
// 检测是否支持copyCanvasElement
|
||||
if (fabric.util.copyCanvasElement && canvas.lowerCanvasEl) {
|
||||
// 使用画布复制方法(推荐)
|
||||
return rasterizeCanvasObjects(options);
|
||||
} else {
|
||||
// 使用备用方法
|
||||
console.warn("使用备用栅格化方法:toDataURL");
|
||||
return _rasterizeUsingDataURL(canvas, objects, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高级栅格化方法:支持更多选项和优化
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.objects - 要栅格化的对象数组
|
||||
* @param {Object} options.bounds - 边界框 {left, top, width, height} (可选)
|
||||
* @param {boolean} options.trimWhitespace - 是否裁剪空白区域,默认true
|
||||
* @param {number} options.quality - 图像质量 0-1,默认1
|
||||
* @param {string} options.format - 图像格式 'png'|'jpeg',默认'png'
|
||||
* @param {boolean} options.preserveObjectState - 是否保持对象状态,默认true
|
||||
* @param {number} options.multiplier - 输出倍数,默认使用画布的retina缩放
|
||||
* @param {boolean} options.useBackgroundColor - 是否使用画布背景色,默认false
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeCanvasObjectsAdvanced(options = {}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const {
|
||||
canvas,
|
||||
objects = [],
|
||||
bounds = null,
|
||||
trimWhitespace = true,
|
||||
quality = 1,
|
||||
format = "png",
|
||||
preserveObjectState = true,
|
||||
multiplier = null,
|
||||
useBackgroundColor = false,
|
||||
} = options;
|
||||
|
||||
if (!canvas || !Array.isArray(objects)) {
|
||||
reject(new Error("无效的参数:需要画布实例和对象数组"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (objects.length === 0) {
|
||||
reject(new Error("没有对象可栅格化"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测画布状态
|
||||
const hasTransform =
|
||||
canvas.getZoom() !== 1 ||
|
||||
canvas.viewportTransform[4] !== 0 ||
|
||||
canvas.viewportTransform[5] !== 0;
|
||||
|
||||
let result;
|
||||
|
||||
if (hasTransform && fabric.util.copyCanvasElement) {
|
||||
// 有变换时使用画布复制方法
|
||||
console.log("🎯 检测到画布变换,使用画布复制方法");
|
||||
result = await _rasterizeUsingCanvasCopy(canvas, objects, {
|
||||
trimWhitespace,
|
||||
quality,
|
||||
format,
|
||||
});
|
||||
} else {
|
||||
// 无变换时可以使用更灵活的方法
|
||||
console.log("📐 画布无变换,使用标准栅格化方法");
|
||||
result = await _rasterizeUsingDataURL(canvas, objects, {
|
||||
quality,
|
||||
format,
|
||||
});
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
console.error("高级栅格化失败:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算多个对象的边界框
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {Object} 边界框 {left, top, width, height}
|
||||
* @private
|
||||
*/
|
||||
function _calculateObjectsBounds(objects) {
|
||||
if (!objects || objects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
if (!obj || typeof obj.getBoundingRect !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = obj.getBoundingRect();
|
||||
minX = Math.min(minX, bounds.left);
|
||||
minY = Math.min(minY, bounds.top);
|
||||
maxX = Math.max(maxX, bounds.left + bounds.width);
|
||||
maxY = Math.max(maxY, bounds.top + bounds.height);
|
||||
});
|
||||
|
||||
if (minX === Infinity || minY === Infinity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
left: minX,
|
||||
top: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪画布空白区域(支持保留边距)
|
||||
* 参考fabric.util.trimCanvas方法,添加padding支持
|
||||
* @param {HTMLCanvasElement} canvas - 要裁剪的画布
|
||||
* @param {number} padding - 保留的边距像素,默认0
|
||||
* @returns {Object|null} 裁剪结果 {canvas: 新画布, offset: {x, y}}
|
||||
* @private
|
||||
*/
|
||||
function _trimCanvas(canvas, padding = 0) {
|
||||
try {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const imageData = ctx.getImageData(0, 0, w, h);
|
||||
const pixels = imageData.data;
|
||||
|
||||
let minX = w;
|
||||
let minY = h;
|
||||
let maxX = 0;
|
||||
let maxY = 0;
|
||||
let hasContent = false;
|
||||
|
||||
// 扫描像素找到有内容的区域
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const alpha = pixels[(y * w + x) * 4 + 3];
|
||||
if (alpha > 0) {
|
||||
hasContent = true;
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 应用padding,确保不超出原始画布边界
|
||||
const paddedMinX = Math.max(0, minX - padding);
|
||||
const paddedMinY = Math.max(0, minY - padding);
|
||||
const paddedMaxX = Math.min(w - 1, maxX + padding);
|
||||
const paddedMaxY = Math.min(h - 1, maxY + padding);
|
||||
|
||||
const trimWidth = paddedMaxX - paddedMinX + 1;
|
||||
const trimHeight = paddedMaxY - paddedMinY + 1;
|
||||
|
||||
// 创建裁剪后的画布
|
||||
const trimmedCanvas = document.createElement("canvas");
|
||||
const trimmedCtx = trimmedCanvas.getContext("2d");
|
||||
|
||||
trimmedCanvas.width = trimWidth;
|
||||
trimmedCanvas.height = trimHeight;
|
||||
|
||||
// 复制裁剪区域(包含padding)
|
||||
const trimmedImageData = ctx.getImageData(
|
||||
paddedMinX,
|
||||
paddedMinY,
|
||||
trimWidth,
|
||||
trimHeight
|
||||
);
|
||||
trimmedCtx.putImageData(trimmedImageData, 0, 0);
|
||||
|
||||
return {
|
||||
canvas: trimmedCanvas,
|
||||
offset: { x: paddedMinX, y: paddedMinY },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("裁剪画布失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格化图层对象(简化版接口)
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Object} options.layer - 图层对象
|
||||
* @param {boolean} options.includeChildren - 是否包含子图层,默认true
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function rasterizeLayer(options = {}) {
|
||||
const { canvas, layer, includeChildren = true } = options;
|
||||
|
||||
if (!canvas || !layer) {
|
||||
return Promise.reject(new Error("缺少必要参数:画布或图层"));
|
||||
}
|
||||
|
||||
// 收集图层的所有对象
|
||||
const objects = [];
|
||||
|
||||
if (layer.fabricObjects && Array.isArray(layer.fabricObjects)) {
|
||||
objects.push(...layer.fabricObjects.filter(Boolean));
|
||||
}
|
||||
|
||||
// 如果包含子图层
|
||||
if (includeChildren && layer.children && Array.isArray(layer.children)) {
|
||||
const collectChildObjects = (childLayer) => {
|
||||
if (childLayer.fabricObjects && Array.isArray(childLayer.fabricObjects)) {
|
||||
objects.push(...childLayer.fabricObjects.filter(Boolean));
|
||||
}
|
||||
if (childLayer.children && Array.isArray(childLayer.children)) {
|
||||
childLayer.children.forEach(collectChildObjects);
|
||||
}
|
||||
};
|
||||
|
||||
layer.children.forEach(collectChildObjects);
|
||||
}
|
||||
|
||||
if (objects.length === 0) {
|
||||
return Promise.reject(new Error("图层没有可栅格化的对象"));
|
||||
}
|
||||
|
||||
// 调用通用栅格化方法
|
||||
return rasterizeCanvasObjects({
|
||||
canvas,
|
||||
objects,
|
||||
trimWhitespace: true,
|
||||
quality: 1,
|
||||
format: "png",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量栅格化多个图层
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.layers - 图层数组
|
||||
* @param {function} options.onProgress - 进度回调
|
||||
* @returns {Promise<Array>} 栅格化结果数组
|
||||
*/
|
||||
export async function batchRasterizeLayers(options = {}) {
|
||||
const { canvas, layers = [], onProgress = null } = options;
|
||||
|
||||
if (!canvas || !Array.isArray(layers)) {
|
||||
throw new Error("缺少必要参数:画布或图层数组");
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const total = layers.length;
|
||||
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
const layer = layers[i];
|
||||
|
||||
try {
|
||||
onProgress?.({ current: i + 1, total, layer, status: "processing" });
|
||||
|
||||
const rasterizedImage = await rasterizeLayer({
|
||||
canvas,
|
||||
layer,
|
||||
includeChildren: true,
|
||||
});
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
layer,
|
||||
image: rasterizedImage,
|
||||
layerId: layer.id,
|
||||
});
|
||||
|
||||
onProgress?.({ current: i + 1, total, layer, status: "success" });
|
||||
} catch (error) {
|
||||
console.error(`栅格化图层失败: ${layer.name || layer.id}`, error);
|
||||
|
||||
results.push({
|
||||
success: false,
|
||||
layer,
|
||||
error: error.message,
|
||||
layerId: layer.id,
|
||||
});
|
||||
|
||||
onProgress?.({
|
||||
current: i + 1,
|
||||
total,
|
||||
layer,
|
||||
status: "error",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能栅格化:根据对象类型和画布状态自动选择最佳方法
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {fabric.Canvas} options.canvas - fabric画布实例
|
||||
* @param {Array} options.objects - 要栅格化的对象数组
|
||||
* @param {Object} options.strategy - 策略配置
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
export function smartRasterize(options = {}) {
|
||||
const { canvas, objects = [], strategy = {} } = options;
|
||||
|
||||
// 分析对象和画布状态
|
||||
const analysis = _analyzeRasterizationContext(canvas, objects);
|
||||
|
||||
// 选择最佳策略
|
||||
const selectedStrategy = _selectOptimalStrategy(analysis, strategy);
|
||||
|
||||
console.log(`🧠 智能栅格化策略: ${selectedStrategy.method}`, {
|
||||
reason: selectedStrategy.reason,
|
||||
analysis: analysis,
|
||||
});
|
||||
|
||||
// 执行对应的栅格化方法
|
||||
switch (selectedStrategy.method) {
|
||||
case "canvasCopy":
|
||||
return rasterizeCanvasObjects({
|
||||
canvas,
|
||||
objects,
|
||||
...selectedStrategy.options,
|
||||
});
|
||||
|
||||
case "dataURL":
|
||||
return _rasterizeUsingDataURL(canvas, objects, selectedStrategy.options);
|
||||
|
||||
case "advanced":
|
||||
return rasterizeCanvasObjectsAdvanced({
|
||||
canvas,
|
||||
objects,
|
||||
...selectedStrategy.options,
|
||||
});
|
||||
|
||||
default:
|
||||
return rasterizeCanvasObjects({ canvas, objects, ...options });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析栅格化上下文
|
||||
* @param {fabric.Canvas} canvas - 画布实例
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {Object} 分析结果
|
||||
* @private
|
||||
*/
|
||||
function _analyzeRasterizationContext(canvas, objects) {
|
||||
const zoom = canvas.getZoom();
|
||||
const viewportTransform = canvas.viewportTransform;
|
||||
const hasTransform =
|
||||
zoom !== 1 || viewportTransform[4] !== 0 || viewportTransform[5] !== 0;
|
||||
|
||||
// 分析对象类型分布
|
||||
const objectTypes = objects.reduce((acc, obj) => {
|
||||
const type = obj.type || "unknown";
|
||||
acc[type] = (acc[type] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 估算复杂度
|
||||
const complexity = _estimateRenderingComplexity(objects);
|
||||
|
||||
// 计算画布利用率
|
||||
const canvasArea = canvas.width * canvas.height;
|
||||
const objectsBounds = _calculateObjectsBounds(objects);
|
||||
const objectsArea = objectsBounds
|
||||
? objectsBounds.width * objectsBounds.height
|
||||
: 0;
|
||||
const utilization = objectsArea / canvasArea;
|
||||
|
||||
return {
|
||||
hasTransform,
|
||||
zoom,
|
||||
objectCount: objects.length,
|
||||
objectTypes,
|
||||
complexity,
|
||||
utilization,
|
||||
canvasSize: { width: canvas.width, height: canvas.height },
|
||||
objectsBounds,
|
||||
supportsCanvasCopy: !!(
|
||||
fabric.util.copyCanvasElement && canvas.lowerCanvasEl
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择最优策略
|
||||
* @param {Object} analysis - 分析结果
|
||||
* @param {Object} userStrategy - 用户指定策略
|
||||
* @returns {Object} 选择的策略
|
||||
* @private
|
||||
*/
|
||||
function _selectOptimalStrategy(analysis, userStrategy = {}) {
|
||||
// 用户指定策略优先
|
||||
if (userStrategy.force) {
|
||||
return {
|
||||
method: userStrategy.force,
|
||||
reason: "用户强制指定",
|
||||
options: userStrategy.options || {},
|
||||
};
|
||||
}
|
||||
|
||||
// 画布变换场景
|
||||
if (analysis.hasTransform && analysis.supportsCanvasCopy) {
|
||||
return {
|
||||
method: "canvasCopy",
|
||||
reason: "画布有变换,使用画布复制方法保持变换状态",
|
||||
options: { trimWhitespace: true },
|
||||
};
|
||||
}
|
||||
|
||||
// 高复杂度场景
|
||||
if (analysis.complexity > 0.8) {
|
||||
return {
|
||||
method: "advanced",
|
||||
reason: "高复杂度渲染,使用高级栅格化方法",
|
||||
options: {
|
||||
preserveObjectState: true,
|
||||
useBackgroundColor: analysis.utilization > 0.5,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 大量对象场景
|
||||
if (analysis.objectCount > 50) {
|
||||
return {
|
||||
method: "dataURL",
|
||||
reason: "大量对象,使用dataURL方法优化性能",
|
||||
options: { quality: 0.9 },
|
||||
};
|
||||
}
|
||||
|
||||
// 低利用率场景(空白较多)
|
||||
if (analysis.utilization < 0.1) {
|
||||
return {
|
||||
method: "canvasCopy",
|
||||
reason: "空白区域较多,使用画布复制+裁剪优化",
|
||||
options: { trimWhitespace: true },
|
||||
};
|
||||
}
|
||||
|
||||
// 默认策略
|
||||
return {
|
||||
method: "canvasCopy",
|
||||
reason: "标准场景,使用画布复制方法",
|
||||
options: { trimWhitespace: true },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算渲染复杂度
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {number} 复杂度分数 0-1
|
||||
* @private
|
||||
*/
|
||||
function _estimateRenderingComplexity(objects) {
|
||||
let complexity = 0;
|
||||
let totalWeight = 0;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
let objectComplexity = 0;
|
||||
let weight = 1;
|
||||
|
||||
// 基于对象类型的复杂度
|
||||
switch (obj.type) {
|
||||
case "path":
|
||||
objectComplexity = 0.8;
|
||||
weight = 2;
|
||||
break;
|
||||
case "group":
|
||||
objectComplexity = 0.7;
|
||||
weight = obj.getObjects?.()?.length || 3;
|
||||
break;
|
||||
case "text":
|
||||
case "i-text":
|
||||
case "textbox":
|
||||
objectComplexity = 0.6;
|
||||
weight = (obj.text?.length || 10) / 50;
|
||||
break;
|
||||
case "image":
|
||||
objectComplexity = 0.4;
|
||||
break;
|
||||
case "rect":
|
||||
case "circle":
|
||||
case "ellipse":
|
||||
objectComplexity = 0.2;
|
||||
break;
|
||||
default:
|
||||
objectComplexity = 0.3;
|
||||
}
|
||||
|
||||
// 考虑变换复杂度
|
||||
if (obj.angle && obj.angle !== 0) objectComplexity += 0.1;
|
||||
if (obj.scaleX !== 1 || obj.scaleY !== 1) objectComplexity += 0.1;
|
||||
if (obj.skewX || obj.skewY) objectComplexity += 0.2;
|
||||
|
||||
// 考虑样式复杂度
|
||||
if (obj.shadow) objectComplexity += 0.2;
|
||||
if (obj.stroke) objectComplexity += 0.1;
|
||||
if (obj.strokeDashArray?.length) objectComplexity += 0.1;
|
||||
|
||||
complexity += objectComplexity * weight;
|
||||
totalWeight += weight;
|
||||
});
|
||||
|
||||
return totalWeight > 0 ? Math.min(complexity / totalWeight, 1) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 栅格化工具集合
|
||||
* 提供不同场景下的栅格化方法选择
|
||||
*/
|
||||
export const RasterizeUtils = {
|
||||
// 基础栅格化
|
||||
rasterizeCanvasObjects,
|
||||
rasterizeLayer,
|
||||
batchRasterizeLayers,
|
||||
|
||||
// 智能栅格化
|
||||
smartRasterize,
|
||||
|
||||
// 兼容性栅格化
|
||||
rasterizeCanvasObjectsCompat,
|
||||
rasterizeCanvasObjectsAdvanced,
|
||||
|
||||
// 策略栅格化
|
||||
fastRasterize: (canvas, objects) => {
|
||||
return _rasterizeUsingDataURL(canvas, objects, { quality: 0.8 });
|
||||
},
|
||||
|
||||
highQualityRasterize: (canvas, objects) => {
|
||||
return rasterizeCanvasObjectsAdvanced({
|
||||
canvas,
|
||||
objects,
|
||||
quality: 1,
|
||||
trimWhitespace: true,
|
||||
preserveObjectState: true,
|
||||
});
|
||||
},
|
||||
|
||||
compactRasterize: (canvas, objects) => {
|
||||
return rasterizeCanvasObjects({
|
||||
canvas,
|
||||
objects,
|
||||
trimWhitespace: true,
|
||||
format: "jpeg",
|
||||
quality: 0.9,
|
||||
});
|
||||
},
|
||||
|
||||
// 分析工具
|
||||
analyzeRasterizationContext: _analyzeRasterizationContext,
|
||||
estimateComplexity: _estimateRenderingComplexity,
|
||||
calculateObjectsBounds: _calculateObjectsBounds,
|
||||
|
||||
/**
|
||||
* 获取推荐的栅格化方法
|
||||
* @param {fabric.Canvas} canvas - 画布实例
|
||||
* @param {Array} objects - 对象数组
|
||||
* @returns {Object} 推荐结果
|
||||
*/
|
||||
getRecommendation: (canvas, objects) => {
|
||||
const analysis = _analyzeRasterizationContext(canvas, objects);
|
||||
const strategy = _selectOptimalStrategy(analysis);
|
||||
|
||||
return {
|
||||
recommendedMethod: strategy.method,
|
||||
reason: strategy.reason,
|
||||
analysis: analysis,
|
||||
alternatives: _getAlternativeMethods(analysis),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取备选方法
|
||||
* @param {Object} analysis - 分析结果
|
||||
* @returns {Array} 备选方法列表
|
||||
* @private
|
||||
*/
|
||||
function _getAlternativeMethods(analysis) {
|
||||
const alternatives = [];
|
||||
|
||||
if (analysis.supportsCanvasCopy) {
|
||||
alternatives.push({
|
||||
method: "canvasCopy",
|
||||
pros: ["保持变换状态", "高质量输出", "自动裁剪"],
|
||||
cons: ["可能较慢"],
|
||||
suitable: "有画布变换或需要高质量输出",
|
||||
});
|
||||
}
|
||||
|
||||
alternatives.push({
|
||||
method: "dataURL",
|
||||
pros: ["性能较好", "兼容性强", "处理大量对象"],
|
||||
cons: ["不保持变换", "可能有质量损失"],
|
||||
suitable: "大量对象或性能优先",
|
||||
});
|
||||
|
||||
alternatives.push({
|
||||
method: "advanced",
|
||||
pros: ["智能优化", "完整功能", "自适应策略"],
|
||||
cons: ["复杂度较高"],
|
||||
suitable: "复杂场景或需要最佳效果",
|
||||
});
|
||||
|
||||
return alternatives;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,9 @@ export function createLayerFromFabricObject(
|
||||
*/
|
||||
export function createLayer(options = {}) {
|
||||
const id =
|
||||
options.id || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
options.id ||
|
||||
generateId("layer_") ||
|
||||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
return {
|
||||
id: id,
|
||||
// 图层基本属性
|
||||
@@ -471,30 +473,36 @@ export function cloneLayer(layer) {
|
||||
* @returns {Object|null} 包含layer和parent的对象,如果未找到返回null
|
||||
*/
|
||||
export function findLayerRecursively(layers, layerId, parent = null) {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
try {
|
||||
if (!layers || !Array.isArray(layers) || !layerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 在当前图层列表中查找
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.id === layerId) {
|
||||
return { layer, parent };
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找子图层
|
||||
if (
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const result = findInChildLayers(layer.children, layerId, layer);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`查找图层 ${layerId} 时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 在当前图层列表中查找
|
||||
for (const layer of layers) {
|
||||
if (layer && layer.id === layerId) {
|
||||
return { layer, parent };
|
||||
}
|
||||
|
||||
// 如果是组图层,递归查找子图层
|
||||
if (
|
||||
layer &&
|
||||
(layer.type === "group" ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
(layer.children && Array.isArray(layer.children)))
|
||||
) {
|
||||
const result = findInChildLayers(layer.children, layerId, layer);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`图层 ${layerId} 未找到`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@ export function buildLayerAssociations(layer, canvasObjects) {
|
||||
|
||||
if (layer.clippingMask) {
|
||||
// clippingMask 可能是一个fabricObject或组
|
||||
layer.clippingMask =
|
||||
canvasObjects.find((obj) => obj.id === layer.clippingMask.id) || null;
|
||||
const clippingMaskObj = canvasObjects.find(
|
||||
(obj) => obj.id === layer.clippingMask.id
|
||||
);
|
||||
layer.clippingMask = clippingMaskObj?.toObject?.(["id"]) || null;
|
||||
}
|
||||
|
||||
// 处理多个fabricObjects关联
|
||||
@@ -49,6 +51,7 @@ export function buildLayerAssociations(layer, canvasObjects) {
|
||||
*/
|
||||
export function restoreObjectLayerAssociations(layers, canvasObjects) {
|
||||
if (!layers || !canvasObjects || !isArray(canvasObjects)) return;
|
||||
|
||||
layers.forEach((layer) => {
|
||||
buildLayerAssociations(layer, canvasObjects);
|
||||
// 处理子图层
|
||||
@@ -150,55 +153,181 @@ export function validateLayerAssociations(layers, canvasObjects) {
|
||||
|
||||
/**
|
||||
* 简化layers对象属性,只保留必要的属性
|
||||
* @param {Array} layers 图层数组 simplifyLayers(JSON.parse(JSON.stringify(this.layers.value)))
|
||||
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {Array} 简化后的图层数组
|
||||
*/
|
||||
|
||||
export function simplifyLayers(layers) {
|
||||
if (!layers || !isArray(layers)) {
|
||||
console.warn("simplifyLayers 请传入有效的图层数组:", layers);
|
||||
return [];
|
||||
}
|
||||
|
||||
layers.forEach((layer) => {
|
||||
// 处理图层遮罩
|
||||
// 如果clippingMask是一个fabricObject或组,确保它的id正确 // 因为是fabric对象,所以没办法直接获取id,只能通过序列化获取
|
||||
if (layer.clippingMask) {
|
||||
layer.clippingMask = layer.clippingMask?.id || null;
|
||||
}
|
||||
|
||||
// 处理单个fabricObject
|
||||
if (layer.fabricObject) {
|
||||
layer.fabricObject = layer.fabricObject?.id;
|
||||
}
|
||||
// 处理多个fabricObjects
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
layer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricObject) => {
|
||||
return fabricObject?.id || null; // 确保每个fabricObject都能转换为对象
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
}
|
||||
// 处理子图层
|
||||
if (layer.children && isArray(layer.children)) {
|
||||
layer.children = simplifyLayers(layer.children);
|
||||
}
|
||||
|
||||
// 只保留必要的属性
|
||||
layer = {
|
||||
return layers.map((layer) => {
|
||||
const simplifiedLayer = {
|
||||
id: layer.id,
|
||||
name: layer.name,
|
||||
visible: layer.visible,
|
||||
locked: layer.locked,
|
||||
opacity: layer.opacity,
|
||||
clippingMask: layer.clippingMask || null,
|
||||
fabricObject: layer.fabricObject || null,
|
||||
fabricObjects: layer.fabricObjects || [],
|
||||
children: layer.children || [],
|
||||
isBackground: layer.isBackground || false,
|
||||
ifFixed: layer.ifFixed || false,
|
||||
isFixed: layer.isFixed || false,
|
||||
clippingMask: layer.clippingMask
|
||||
? {
|
||||
id: layer.clippingMask.id,
|
||||
type: layer.clippingMask.type,
|
||||
}
|
||||
: null,
|
||||
fabricObject: layer.fabricObject
|
||||
? {
|
||||
id: layer.fabricObject.id,
|
||||
type: layer.fabricObject.type,
|
||||
}
|
||||
: null,
|
||||
fabricObjects:
|
||||
layer.fabricObjects && isArray(layer.fabricObjects)
|
||||
? layer.fabricObjects
|
||||
.map((fabricObject) =>
|
||||
fabricObject?.id
|
||||
? {
|
||||
id: fabricObject.id,
|
||||
type: fabricObject.type,
|
||||
}
|
||||
: null
|
||||
)
|
||||
.filter((obj) => obj !== null)
|
||||
: [],
|
||||
children:
|
||||
layer.children && isArray(layer.children)
|
||||
? simplifyLayers(layer.children)
|
||||
: [],
|
||||
};
|
||||
});
|
||||
|
||||
return layers;
|
||||
return simplifiedLayer;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复图层的完整关联关系
|
||||
* @param {Array} simplifiedLayers 简化的图层数组
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
* @returns {Array} 恢复关联后的图层数组
|
||||
*/
|
||||
export function restoreLayers(simplifiedLayers, canvasObjects) {
|
||||
if (!simplifiedLayers || !isArray(simplifiedLayers)) {
|
||||
console.warn("restoreLayers 请传入有效的简化图层数组:", simplifiedLayers);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!canvasObjects || !isArray(canvasObjects)) {
|
||||
console.warn("restoreLayers 请传入有效的画布对象数组:", canvasObjects);
|
||||
return simplifiedLayers;
|
||||
}
|
||||
|
||||
return simplifiedLayers.map((layer) => {
|
||||
const restoredLayer = { ...layer };
|
||||
|
||||
// 恢复clippingMask关联
|
||||
if (layer.clippingMask?.id) {
|
||||
const clippingMaskObj = canvasObjects.find(
|
||||
(obj) => obj.id === layer.clippingMask.id
|
||||
);
|
||||
restoredLayer.clippingMask = clippingMaskObj || null;
|
||||
}
|
||||
|
||||
// 恢复单个fabricObject关联
|
||||
if (layer.fabricObject?.id) {
|
||||
const fabricObj = canvasObjects.find(
|
||||
(obj) => obj.id === layer.fabricObject.id
|
||||
);
|
||||
if (fabricObj) {
|
||||
fabricObj.layerId = layer.id;
|
||||
fabricObj.layerName = layer.name;
|
||||
restoredLayer.fabricObject = fabricObj;
|
||||
} else {
|
||||
restoredLayer.fabricObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复多个fabricObjects关联
|
||||
if (layer.fabricObjects && isArray(layer.fabricObjects)) {
|
||||
restoredLayer.fabricObjects = layer.fabricObjects
|
||||
.map((fabricRef) => {
|
||||
const fabricObj = canvasObjects.find(
|
||||
(obj) => obj.id === fabricRef.id
|
||||
);
|
||||
if (fabricObj) {
|
||||
fabricObj.layerId = layer.id;
|
||||
fabricObj.layerName = layer.name;
|
||||
return fabricObj;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((obj) => obj !== null);
|
||||
}
|
||||
|
||||
// 递归处理子图层
|
||||
if (layer.children && isArray(layer.children)) {
|
||||
restoredLayer.children = restoreLayers(layer.children, canvasObjects);
|
||||
}
|
||||
|
||||
return restoredLayer;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化图层数据用于保存
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {string} 序列化后的JSON字符串
|
||||
*/
|
||||
export function serializeLayers(layers) {
|
||||
try {
|
||||
const simplified = simplifyLayers(layers);
|
||||
return JSON.stringify(simplified, null, 2);
|
||||
} catch (error) {
|
||||
console.error("序列化图层数据失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化图层数据并恢复关联
|
||||
* @param {string} serializedLayers 序列化的图层JSON字符串
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
* @returns {Array} 恢复关联后的图层数组
|
||||
*/
|
||||
export function deserializeLayers(serializedLayers, canvasObjects) {
|
||||
try {
|
||||
const simplified = JSON.parse(serializedLayers);
|
||||
return restoreLayers(simplified, canvasObjects);
|
||||
} catch (error) {
|
||||
console.error("反序列化图层数据失败:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图层的存储快照(用于撤销/重做)
|
||||
* @param {Array} layers 图层数组
|
||||
* @returns {Object} 图层快照对象
|
||||
*/
|
||||
export function createLayerSnapshot(layers) {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
data: simplifyLayers(layers),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从快照恢复图层状态
|
||||
* @param {Object} snapshot 图层快照对象
|
||||
* @param {Array} canvasObjects 画布对象数组
|
||||
* @returns {Array} 恢复的图层数组
|
||||
*/
|
||||
export function restoreFromSnapshot(snapshot, canvasObjects) {
|
||||
if (!snapshot?.data) {
|
||||
console.warn("无效的图层快照:", snapshot);
|
||||
return [];
|
||||
}
|
||||
|
||||
return restoreLayers(snapshot.data, canvasObjects);
|
||||
}
|
||||
|
||||
339
src/component/Canvas/CanvasEditor/utils/rasterizedImage.js
Normal file
339
src/component/Canvas/CanvasEditor/utils/rasterizedImage.js
Normal file
@@ -0,0 +1,339 @@
|
||||
// 栅格化帮助
|
||||
import { fabric } from "fabric-with-all";
|
||||
/**
|
||||
* 创建栅格化图像
|
||||
* 使用增强版栅格化方法,不受原始画布变换影响
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
* @private
|
||||
*/
|
||||
export const createRasterizedImage = async ({
|
||||
canvas, // 画布对象 必填
|
||||
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
|
||||
maskObject = null, // 用于裁剪的对象 - 可选
|
||||
trimWhitespace = true, // 是否裁剪空白区域
|
||||
trimPadding = 1, // 裁剪边距
|
||||
quality = 1.0, // 图像质量
|
||||
format = "png", // 图像格式
|
||||
scaleFactor = 2, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
||||
} = {}) => {
|
||||
try {
|
||||
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
||||
|
||||
// 确保有对象需要栅格化
|
||||
if (fabricObjects.length === 0) {
|
||||
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 高清倍数
|
||||
const currentZoom = canvas.getZoom?.() || 1;
|
||||
scaleFactor = Math.max(
|
||||
scaleFactor || canvas?.getRetinaScaling?.(),
|
||||
currentZoom
|
||||
);
|
||||
|
||||
scaleFactor = Math.min(scaleFactor, 3); // 最大不能大于3
|
||||
|
||||
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
|
||||
|
||||
// 计算绝对边界框(原始尺寸)和相对边界框(当前缩放后的尺寸)
|
||||
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
|
||||
|
||||
console.log("📏 绝对边界框:", absoluteBounds);
|
||||
console.log("📏 相对边界框:", relativeBounds);
|
||||
|
||||
// 使用绝对边界框创建高质量的离屏渲染
|
||||
const rasterizedImage = await createOffscreenRasterization({
|
||||
canvas,
|
||||
objects: fabricObjects,
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
scaleFactor,
|
||||
maskObject,
|
||||
trimWhitespace,
|
||||
trimPadding,
|
||||
quality,
|
||||
format,
|
||||
currentZoom,
|
||||
isReturenDataURL,
|
||||
});
|
||||
|
||||
if (!rasterizedImage) {
|
||||
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isReturenDataURL) {
|
||||
console.log("✅ 栅格化图像创建成功,返回DataURL");
|
||||
return rasterizedImage; // 返回DataURL
|
||||
}
|
||||
|
||||
// 设置栅格化图像的属性
|
||||
if (rasterizedImage) {
|
||||
rasterizedImage.set({
|
||||
selectable: true,
|
||||
evented: true,
|
||||
hasControls: true,
|
||||
hasBorders: true,
|
||||
custom: {
|
||||
type: "rasterized",
|
||||
rasterizedAt: new Date().toISOString(),
|
||||
objectCount: fabricObjects.length,
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
originalZoom: currentZoom,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ 栅格化图像创建完成`);
|
||||
}
|
||||
|
||||
return rasterizedImage;
|
||||
} catch (error) {
|
||||
console.error("创建栅格化图像失败:", error);
|
||||
throw new Error(`栅格化失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算对象的绝对边界框和相对边界框
|
||||
* @param {Array} fabricObjects fabric对象数组
|
||||
* @returns {Object} 包含绝对边界框和相对边界框的对象
|
||||
*/
|
||||
const calculateBounds = (fabricObjects) => {
|
||||
if (fabricObjects.length === 0) {
|
||||
console.warn("⚠️ 没有对象,无法计算边界框");
|
||||
return { absoluteBounds: null, relativeBounds: null };
|
||||
}
|
||||
|
||||
let absoluteBounds = null;
|
||||
let relativeBounds = null;
|
||||
|
||||
fabricObjects.forEach((obj, index) => {
|
||||
// 获取相对边界框(考虑画布缩放和平移)
|
||||
const relativeBound = obj.getBoundingRect();
|
||||
// 获取绝对边界框(原始大小和位置)
|
||||
const absoluteBound = obj.getBoundingRect(true, true);
|
||||
|
||||
console.log(`对象 ${obj.id || index} 边界框比较:`, {
|
||||
relative: relativeBound,
|
||||
absolute: absoluteBound,
|
||||
scaleX: obj.scaleX,
|
||||
scaleY: obj.scaleY,
|
||||
});
|
||||
|
||||
// 计算绝对边界框的累积范围
|
||||
if (!absoluteBounds) {
|
||||
absoluteBounds = { ...absoluteBound };
|
||||
} else {
|
||||
const right = Math.max(
|
||||
absoluteBounds.left + absoluteBounds.width,
|
||||
absoluteBound.left + absoluteBound.width
|
||||
);
|
||||
const bottom = Math.max(
|
||||
absoluteBounds.top + absoluteBounds.height,
|
||||
absoluteBound.top + absoluteBound.height
|
||||
);
|
||||
|
||||
absoluteBounds.left = Math.min(absoluteBounds.left, absoluteBound.left);
|
||||
absoluteBounds.top = Math.min(absoluteBounds.top, absoluteBound.top);
|
||||
absoluteBounds.width = right - absoluteBounds.left;
|
||||
absoluteBounds.height = bottom - absoluteBounds.top;
|
||||
}
|
||||
|
||||
// 计算相对边界框的累积范围
|
||||
if (!relativeBounds) {
|
||||
relativeBounds = { ...relativeBound };
|
||||
} else {
|
||||
const right = Math.max(
|
||||
relativeBounds.left + relativeBounds.width,
|
||||
relativeBound.left + relativeBound.width
|
||||
);
|
||||
const bottom = Math.max(
|
||||
relativeBounds.top + relativeBounds.height,
|
||||
relativeBound.top + relativeBound.height
|
||||
);
|
||||
|
||||
relativeBounds.left = Math.min(relativeBounds.left, relativeBound.left);
|
||||
relativeBounds.top = Math.min(relativeBounds.top, relativeBound.top);
|
||||
relativeBounds.width = right - relativeBounds.left;
|
||||
relativeBounds.height = bottom - relativeBounds.top;
|
||||
}
|
||||
});
|
||||
|
||||
return { absoluteBounds, relativeBounds };
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建离屏栅格化渲染
|
||||
* @param {Object} options 渲染选项
|
||||
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||
*/
|
||||
const createOffscreenRasterization = async ({
|
||||
canvas,
|
||||
objects,
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
scaleFactor,
|
||||
maskObject,
|
||||
trimWhitespace,
|
||||
trimPadding,
|
||||
quality,
|
||||
format,
|
||||
currentZoom,
|
||||
isReturenDataURL,
|
||||
}) => {
|
||||
try {
|
||||
// 创建离屏画布,使用绝对尺寸以保证高质量
|
||||
const offscreenCanvas = new fabric.Canvas();
|
||||
|
||||
// 设置离屏画布尺寸为绝对边界框大小,并应用高清倍数
|
||||
const canvasWidth = Math.ceil(absoluteBounds.width * scaleFactor);
|
||||
const canvasHeight = Math.ceil(absoluteBounds.height * scaleFactor);
|
||||
|
||||
offscreenCanvas.setDimensions({
|
||||
width: canvasWidth,
|
||||
height: canvasHeight,
|
||||
});
|
||||
|
||||
// 设置离屏画布的缩放,确保对象以原始尺寸渲染
|
||||
// offscreenCanvas.setZoom(scaleFactor);
|
||||
|
||||
console.log(
|
||||
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
|
||||
);
|
||||
|
||||
// 克隆对象到离屏画布
|
||||
const clonedObjects = [];
|
||||
for (const obj of objects) {
|
||||
const clonedObj = await cloneObjectAsync(obj);
|
||||
|
||||
// 调整对象位置,相对于绝对边界框的左上角
|
||||
// const absoluteObjBounds = obj.getBoundingRect(true, true);
|
||||
clonedObj.set({
|
||||
left: clonedObj.left - absoluteBounds.left,
|
||||
top: clonedObj.top - absoluteBounds.top,
|
||||
});
|
||||
|
||||
clonedObjects.push(clonedObj);
|
||||
offscreenCanvas.add(clonedObj);
|
||||
}
|
||||
|
||||
// 渲染离屏画布
|
||||
offscreenCanvas.renderAll();
|
||||
|
||||
// 如果有遮罩对象,应用遮罩
|
||||
if (maskObject) {
|
||||
await applyMaskToCanvas(offscreenCanvas, maskObject, absoluteBounds);
|
||||
}
|
||||
|
||||
// 生成图像数据
|
||||
const dataURL = offscreenCanvas.toDataURL({
|
||||
format,
|
||||
quality,
|
||||
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
|
||||
});
|
||||
|
||||
if (isReturenDataURL) {
|
||||
return dataURL; // 如果需要返回DataURL
|
||||
}
|
||||
// 清理离屏画布
|
||||
offscreenCanvas.dispose();
|
||||
|
||||
// 创建fabric.Image对象
|
||||
const fabricImage = await createFabricImageFromDataURL(dataURL);
|
||||
|
||||
// // 应用变换到fabric图像
|
||||
fabricImage.set({
|
||||
...absoluteBounds,
|
||||
});
|
||||
|
||||
return fabricImage;
|
||||
} catch (error) {
|
||||
console.error("离屏栅格化失败:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 异步克隆fabric对象
|
||||
* @param {fabric.Object} obj 要克隆的对象
|
||||
* @returns {Promise<fabric.Object>} 克隆的对象
|
||||
*/
|
||||
const cloneObjectAsync = (obj) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
obj.clone((cloned) => {
|
||||
if (cloned) {
|
||||
resolve(cloned);
|
||||
} else {
|
||||
reject(new Error("对象克隆失败"));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从DataURL创建fabric.Image对象
|
||||
* @param {string} dataURL 图像数据URL
|
||||
* @returns {Promise<fabric.Image>} fabric图像对象
|
||||
*/
|
||||
const createFabricImageFromDataURL = (dataURL) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fabric.Image.fromURL(dataURL, (img) => {
|
||||
if (img) {
|
||||
resolve(img);
|
||||
} else {
|
||||
reject(new Error("无法从DataURL创建图像"));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算栅格化图像在当前画布上的正确变换
|
||||
* @param {Object} params 计算参数
|
||||
* @returns {Object} 变换属性对象
|
||||
*/
|
||||
const calculateImageTransform = ({
|
||||
absoluteBounds,
|
||||
relativeBounds,
|
||||
currentZoom,
|
||||
scaleFactor,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
}) => {
|
||||
// 计算缩放比例:相对尺寸 / 绝对尺寸
|
||||
const scaleX = relativeBounds.width / absoluteBounds.width;
|
||||
const scaleY = relativeBounds.height / absoluteBounds.height;
|
||||
|
||||
// 由于我们生成的图像是基于绝对尺寸的高清版本,需要考虑scaleFactor
|
||||
const finalScaleX = scaleX / scaleFactor;
|
||||
const finalScaleY = scaleY / scaleFactor;
|
||||
|
||||
return {
|
||||
left: relativeBounds.left + relativeBounds.width / 2, // 设置为中心点
|
||||
top: relativeBounds.top + relativeBounds.height / 2, // 设置为中心点
|
||||
// scaleX: finalScaleX,
|
||||
// scaleY: finalScaleY,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用遮罩到画布(如果需要)
|
||||
* @param {fabric.Canvas} canvas 目标画布
|
||||
* @param {fabric.Object} maskObject 遮罩对象
|
||||
* @param {Object} bounds 边界框
|
||||
*/
|
||||
const applyMaskToCanvas = async (canvas, maskObject, bounds) => {
|
||||
// 这里可以实现遮罩逻辑
|
||||
// 例如使用canvas的clipPath或其他遮罩技术
|
||||
console.log("应用遮罩功能待实现");
|
||||
};
|
||||
|
||||
export const getObjectsBounds = (fabricObjects) => {
|
||||
const { absoluteBounds } = calculateBounds(fabricObjects);
|
||||
return absoluteBounds;
|
||||
};
|
||||
Reference in New Issue
Block a user