合并画布
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import { findObjectById } from "../utils/helper";
|
||||
import { findLayerRecursively } from "../utils/layerHelper";
|
||||
import { createRasterizedImage } from "../utils/rasterizedImage";
|
||||
|
||||
/**
|
||||
* 缩略图管理器 - 负责生成和缓存图层和元素的预览缩略图
|
||||
*/
|
||||
@@ -6,12 +10,8 @@ export class ThumbnailManager {
|
||||
this.canvas = canvas;
|
||||
this.layers = options.layers || []; // 图层管理器
|
||||
this.layerThumbSize = options.layerThumbSize || { width: 48, height: 48 };
|
||||
this.elementThumbSize = options.elementThumbSize || {
|
||||
width: 36,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
this.layerThumbnails = new Map(); // 图层缩略图缓存
|
||||
this.elementThumbnails = new Map(); // 元素缩略图缓存
|
||||
this.defaultThumbnail =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; // 1x1 透明图
|
||||
}
|
||||
@@ -20,279 +20,31 @@ export class ThumbnailManager {
|
||||
* 生成图层缩略图
|
||||
* @param {Object} layer 图层对象ID
|
||||
*/
|
||||
generateLayerThumbnail(layerId) {
|
||||
// const layer = this?.layers.value?.find((layer) => layer.id === layerId);
|
||||
// if (!layer) return;
|
||||
// // 延迟执行,避免阻塞UI
|
||||
// requestAnimationFrame(() => {
|
||||
// this._generateLayerThumbnailNow(layer);
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即生成图层缩略图
|
||||
* @param {Object} layer 图层对象
|
||||
* @private
|
||||
*/
|
||||
_generateLayerThumbnailNow(layer) {
|
||||
if (
|
||||
!layer ||
|
||||
!this.canvas ||
|
||||
!layer.fabricObjects ||
|
||||
!layer.fabricObjects?.length
|
||||
)
|
||||
return;
|
||||
|
||||
try {
|
||||
let thumbnail = null;
|
||||
|
||||
if (!layer.children?.length) {
|
||||
// 如果是元素图层,直接生成元素缩略图
|
||||
thumbnail = this._generateThumbnailFromObjects(
|
||||
layer.fabricObjects,
|
||||
this.layerThumbSize.width,
|
||||
this.layerThumbSize.height
|
||||
);
|
||||
} else if (layer.type === "group" || layer.children?.length) {
|
||||
const fabricObjects = layer.children.reduce((pre, next) => {
|
||||
if (next.fabricObjects.length) {
|
||||
pre.push(...next.fabricObjects);
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
// 如果是分组图层,合并所有子对象的缩略图
|
||||
thumbnail = this._generateThumbnailFromObjects(
|
||||
fabricObjects,
|
||||
this.layerThumbSize.width,
|
||||
this.layerThumbSize.height
|
||||
);
|
||||
}
|
||||
|
||||
// 保存到缩略图缓存
|
||||
if (thumbnail) {
|
||||
this.layerThumbnails.set(layer.id, thumbnail);
|
||||
} else {
|
||||
// 如果无法生成缩略图,使用默认缩略图
|
||||
this.layerThumbnails.set(layer.id, this.defaultThumbnail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成图层缩略图出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成元素缩略图
|
||||
* @param {Object} element 元素对象
|
||||
* @param {Object} fabricObject fabric对象
|
||||
*/
|
||||
generateElementThumbnail(element, fabricObject) {
|
||||
if (!element || !element.id || !fabricObject) return;
|
||||
async generateLayerThumbnail(layerId) {
|
||||
const fabricObjects = this._collectLayersAndObjects(layerId);
|
||||
|
||||
// 延迟执行,避免阻塞UI
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
const thumbnail = this._generateThumbnailFromObject(
|
||||
fabricObject,
|
||||
this.elementThumbSize.width,
|
||||
this.elementThumbSize.height
|
||||
);
|
||||
fabricObjects.length > 0 &&
|
||||
requestAnimationFrame(async () => {
|
||||
const base64 = await this._generateLayerThumbnailNow(fabricObjects);
|
||||
this.layerThumbnails.set(layerId, base64);
|
||||
try {
|
||||
const { layer, parent } = findLayerRecursively(
|
||||
this.layers.value,
|
||||
layerId
|
||||
);
|
||||
if (layer) {
|
||||
layer.thumbnailUrl = base64; // 更新图层对象的缩略图
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
this.elementThumbnails.set(element.id, thumbnail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成元素缩略图出错:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从fabric对象生成缩略图
|
||||
* @param {Object} obj fabric对象
|
||||
* @param {Number} width 缩略图宽度
|
||||
* @param {Number} height 缩略图高度
|
||||
* @returns {String} 缩略图数据URL
|
||||
* @private
|
||||
*/
|
||||
_generateThumbnailFromObject(obj, width, height) {
|
||||
if (!obj || !this.canvas) return null;
|
||||
|
||||
// 保存对象状态
|
||||
const originalState = {
|
||||
active: obj.active,
|
||||
visible: obj.visible,
|
||||
left: obj.left,
|
||||
top: obj.top,
|
||||
scaleX: obj.scaleX,
|
||||
scaleY: obj.scaleY,
|
||||
opacity: obj.opacity,
|
||||
};
|
||||
|
||||
// 临时修改对象状态
|
||||
obj.set({
|
||||
active: false,
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
});
|
||||
|
||||
// 创建临时画布
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = width;
|
||||
tempCanvas.height = height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 获取对象边界
|
||||
const bounds = obj.getBoundingRect();
|
||||
|
||||
// 绘制缩略图
|
||||
try {
|
||||
// 清空画布
|
||||
tempCtx.clearRect(0, 0, width, height);
|
||||
|
||||
// 计算缩放比例
|
||||
const scaleFactorX = width / bounds.width;
|
||||
const scaleFactorY = height / bounds.height;
|
||||
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
|
||||
|
||||
// 居中绘制
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
tempCtx.save();
|
||||
tempCtx.translate(centerX, centerY);
|
||||
tempCtx.scale(scaleFactor, scaleFactor);
|
||||
tempCtx.translate(
|
||||
-bounds.left - bounds.width / 2,
|
||||
-bounds.top - bounds.height / 2
|
||||
);
|
||||
|
||||
// 绘制对象
|
||||
obj.render(tempCtx);
|
||||
|
||||
tempCtx.restore();
|
||||
|
||||
// 转换为数据URL
|
||||
const dataUrl = tempCanvas.toDataURL("image/png");
|
||||
|
||||
// 恢复对象状态
|
||||
obj.set(originalState);
|
||||
|
||||
return dataUrl;
|
||||
} catch (error) {
|
||||
console.error("绘制对象缩略图出错:", error);
|
||||
|
||||
// 恢复对象状态
|
||||
obj.set(originalState);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多个fabric对象生成组合缩略图
|
||||
* @param {Array} objects fabric对象数组
|
||||
* @param {Number} width 缩略图宽度
|
||||
* @param {Number} height 缩略图高度
|
||||
* @returns {String} 缩略图数据URL
|
||||
* @private
|
||||
*/
|
||||
_generateThumbnailFromObjects(objects, width, height) {
|
||||
if (!objects || !objects.length || !this.canvas) return null;
|
||||
|
||||
// 创建临时画布
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = width;
|
||||
tempCanvas.height = height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 计算所有对象的总边界
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
|
||||
objects.forEach((obj) => {
|
||||
if (!obj.visible) 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);
|
||||
});
|
||||
|
||||
const groupWidth = maxX - minX;
|
||||
const groupHeight = maxY - minY;
|
||||
|
||||
// 如果没有有效对象,返回null
|
||||
if (groupWidth <= 0 || groupHeight <= 0) return null;
|
||||
|
||||
// 保存对象状态
|
||||
const originalStates = objects.map((obj) => ({
|
||||
obj,
|
||||
state: {
|
||||
active: obj.active,
|
||||
visible: obj.visible,
|
||||
opacity: obj.opacity,
|
||||
},
|
||||
}));
|
||||
|
||||
// 临时修改对象状态
|
||||
originalStates.forEach((item) => {
|
||||
item.obj.set({
|
||||
active: false,
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
});
|
||||
});
|
||||
|
||||
// 绘制缩略图
|
||||
try {
|
||||
// 清空画布
|
||||
tempCtx.clearRect(0, 0, width, height);
|
||||
|
||||
// 计算缩放比例
|
||||
const scaleFactorX = width / groupWidth;
|
||||
const scaleFactorY = height / groupHeight;
|
||||
const scaleFactor = Math.min(scaleFactorX, scaleFactorY) * 0.8; // 保留一些边距
|
||||
|
||||
// 居中绘制
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
tempCtx.save();
|
||||
tempCtx.translate(centerX, centerY);
|
||||
tempCtx.scale(scaleFactor, scaleFactor);
|
||||
tempCtx.translate(-(minX + groupWidth / 2), -(minY + groupHeight / 2));
|
||||
|
||||
// 按顺序绘制所有对象
|
||||
objects.forEach((obj) => {
|
||||
if (obj.visible) {
|
||||
obj.render(tempCtx);
|
||||
if (parent) {
|
||||
// 如果是组图层,则同步更新父图层的缩略图
|
||||
this.generateLayerThumbnail(parent.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("生成图层缩略图时出错:", error);
|
||||
}
|
||||
});
|
||||
|
||||
tempCtx.restore();
|
||||
|
||||
// 转换为数据URL
|
||||
const dataUrl = tempCanvas.toDataURL("image/png");
|
||||
|
||||
// 恢复对象状态
|
||||
originalStates.forEach((item) => {
|
||||
item.obj.set(item.state);
|
||||
});
|
||||
|
||||
return dataUrl;
|
||||
} catch (error) {
|
||||
console.error("绘制组合缩略图出错:", error);
|
||||
|
||||
// 恢复对象状态
|
||||
originalStates.forEach((item) => {
|
||||
item.obj.set(item.state);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,13 +55,133 @@ export class ThumbnailManager {
|
||||
if (!layers || !Array.isArray(layers)) return;
|
||||
|
||||
// 使用requestAnimationFrame批量生成,避免阻塞主线程
|
||||
requestAnimationFrame(() => {
|
||||
layers.forEach((layer) => {
|
||||
if (layer && layer.id) {
|
||||
this._generateLayerThumbnailNow(layer);
|
||||
layers.forEach((layer) => {
|
||||
if (layer && layer.id) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
if (layer.children && layer.children.length) {
|
||||
this.generateLayerThumbnail(layer.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
async _generateLayerThumbnailNow(fabricObjects) {
|
||||
if (!fabricObjects || fabricObjects.length === 0) {
|
||||
console.warn("⚠️ 没有对象需要生成缩略图,返回默认缩略图");
|
||||
return this.defaultThumbnail;
|
||||
}
|
||||
return await createRasterizedImage({
|
||||
canvas: this.canvas, // 画布对象 必填
|
||||
fabricObjects, // 要栅格化的对象列表 - 按顺序 必填
|
||||
// maskObject = null, // 用于裁剪的对象 - 可选 // TODO: 后期看是否需要裁剪
|
||||
trimWhitespace: true, // 是否裁剪空白区域
|
||||
trimPadding: 2, // 裁剪边距
|
||||
quality: 0.8, // 图像质量
|
||||
format: "png", // 图像格式
|
||||
scaleFactor: 1, // 高清倍数 - 默认是画布的高清倍数
|
||||
isReturenDataURL: true, // 是否返回DataURL而不是fabric.Image对象
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集要栅格化的图层和对象
|
||||
* @private
|
||||
*/
|
||||
_collectLayersAndObjects(layerId) {
|
||||
if (!layerId) {
|
||||
console.warn("⚠️ 无效的图层ID,无法收集对象");
|
||||
return [];
|
||||
}
|
||||
|
||||
const { layer } = findLayerRecursively(this.layers.value, layerId);
|
||||
let layersToRasterize = [];
|
||||
|
||||
if (layer.children && layer.children.length > 0) {
|
||||
// 组图层:收集自身和所有子图层
|
||||
layersToRasterize = this._collectLayersToRasterize(layer);
|
||||
} else {
|
||||
// 普通图层:只收集自身
|
||||
layersToRasterize = [layer];
|
||||
}
|
||||
|
||||
// 收集所有图层的fabricObjects并按画布z-index顺序排序
|
||||
const allCanvasObjects = this.canvas.getObjects();
|
||||
const objectsWithZIndex = [];
|
||||
|
||||
layersToRasterize.forEach((layer) => {
|
||||
if (layer.fabricObject) {
|
||||
// 如果图层本身有fabricObject,直接添加
|
||||
const { object } = findObjectById(this.canvas, layer.fabricObject.id);
|
||||
|
||||
if (object) {
|
||||
const zIndex = allCanvasObjects.indexOf(object);
|
||||
objectsWithZIndex.push({
|
||||
object: object,
|
||||
zIndex: zIndex,
|
||||
layerObj: layer.fabricObject,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||
layer.fabricObjects.forEach((layerObj) => {
|
||||
if (layerObj && layerObj.id) {
|
||||
const { object } = findObjectById(this.canvas, layerObj.id);
|
||||
if (object) {
|
||||
// 获取对象在画布中的z-index(数组索引)
|
||||
const zIndex = allCanvasObjects.indexOf(object);
|
||||
objectsWithZIndex.push({
|
||||
object: object,
|
||||
zIndex: zIndex,
|
||||
layerObj: layerObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 按z-index排序,确保保持原有的渲染顺序
|
||||
objectsWithZIndex.sort((a, b) => a.zIndex - b.zIndex);
|
||||
|
||||
// 提取排序后的对象
|
||||
const objectsToRasterize = objectsWithZIndex.map((item) => item.object);
|
||||
|
||||
console.log(
|
||||
`📊 收集到 ${layersToRasterize.length} 个图层,${objectsToRasterize.length} 个对象进行栅格化`
|
||||
);
|
||||
console.log(
|
||||
"🔢 对象z-index顺序:",
|
||||
objectsWithZIndex.map((item) => ({
|
||||
id: item.object.id,
|
||||
type: item.object.type,
|
||||
zIndex: item.zIndex,
|
||||
}))
|
||||
);
|
||||
|
||||
return objectsToRasterize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集要栅格化的图层(递归收集子图层)
|
||||
* @param {Object} sourceLayer 源图层
|
||||
* @returns {Array} 图层数组
|
||||
* @private
|
||||
*/
|
||||
_collectLayersToRasterize(sourceLayer) {
|
||||
const result = [sourceLayer];
|
||||
|
||||
// 如果是组图层,收集所有子图层
|
||||
if (sourceLayer.children && sourceLayer.children.length > 0) {
|
||||
sourceLayer.children.forEach((childLayer) => {
|
||||
if (childLayer) {
|
||||
result.push(...this._collectLayersToRasterize(childLayer));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,16 +194,6 @@ export class ThumbnailManager {
|
||||
return this.layerThumbnails.get(layerId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素缩略图
|
||||
* @param {String} elementId 元素ID
|
||||
* @returns {String|null} 缩略图URL或null
|
||||
*/
|
||||
getElementThumbnail(elementId) {
|
||||
if (!elementId) return null;
|
||||
return this.elementThumbnails.get(elementId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除图层缩略图
|
||||
* @param {String} layerId 图层ID
|
||||
@@ -342,22 +204,11 @@ export class ThumbnailManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除元素缩略图
|
||||
* @param {String} elementId 元素ID
|
||||
*/
|
||||
clearElementThumbnail(elementId) {
|
||||
if (elementId && this.elementThumbnails.has(elementId)) {
|
||||
this.elementThumbnails.delete(elementId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缩略图
|
||||
*/
|
||||
clearAllThumbnails() {
|
||||
this.layerThumbnails.clear();
|
||||
this.elementThumbnails.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user