合并画布

This commit is contained in:
X1627315083
2025-06-22 13:52:28 +08:00
parent fd6d61a44a
commit 584f6a7db0
47 changed files with 4540 additions and 1952 deletions

View File

@@ -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;
}