428 lines
13 KiB
JavaScript
428 lines
13 KiB
JavaScript
/**
|
||
* 液化实时更新器
|
||
* 负责高效地更新液化效果到画布上,避免频繁创建fabric对象导致的性能问题
|
||
*/
|
||
export class LiquifyRealTimeUpdater {
|
||
constructor(canvas, options = {}) {
|
||
this.canvas = canvas;
|
||
this.targetObject = null;
|
||
this.isUpdating = false;
|
||
this.updateQueue = [];
|
||
this.lastUpdateTime = 0;
|
||
this.currImage = options.currImage || { value: null };
|
||
|
||
// 配置选项
|
||
this.config = {
|
||
throttleTime: options.throttleTime || 16, // 60fps
|
||
maxQueueSize: options.maxQueueSize || 5,
|
||
useDirectUpdate: options.useDirectUpdate !== false, // 默认启用直接更新
|
||
imageQuality: options.imageQuality || 1.0, // 图像质量 (0.1-1.0)
|
||
skipRenderDuringDrag: options.skipRenderDuringDrag || false, // 拖拽时跳过渲染
|
||
};
|
||
|
||
// 临时canvas用于快速渲染
|
||
this.tempCanvas = document.createElement("canvas");
|
||
this.tempCtx = this.tempCanvas.getContext("2d");
|
||
|
||
// 高质量canvas用于最终输出
|
||
this.highQualityCanvas = document.createElement("canvas");
|
||
this.highQualityCtx = this.highQualityCanvas.getContext("2d");
|
||
|
||
// 当前缓存的图像数据
|
||
this.cachedDataURL = null;
|
||
this.pendingImageData = null;
|
||
this.renderingScheduled = false;
|
||
|
||
// 优化Canvas画布渲染设置
|
||
this.canvas.renderOnAddRemove = false; // 禁用自动渲染
|
||
this.canvas.skipOffscreen = true; // 跳过离屏元素渲染
|
||
}
|
||
|
||
/**
|
||
* 设置目标对象
|
||
* @param {Object} fabricObject fabric图像对象
|
||
*/
|
||
setTargetObject(fabricObject) {
|
||
this.targetObject = fabricObject;
|
||
if (fabricObject && fabricObject._element) {
|
||
// 设置临时canvas尺寸
|
||
this.tempCanvas.width = fabricObject.width;
|
||
this.tempCanvas.height = fabricObject.height;
|
||
|
||
// 设置高质量canvas尺寸
|
||
this.highQualityCanvas.width = fabricObject.width;
|
||
this.highQualityCanvas.height = fabricObject.height;
|
||
|
||
// 配置高质量渲染上下文
|
||
this.highQualityCtx.imageSmoothingEnabled = true;
|
||
this.highQualityCtx.imageSmoothingQuality = "high";
|
||
|
||
// 配置临时canvas上下文(快速渲染)
|
||
this.tempCtx.imageSmoothingEnabled = false; // 快速模式关闭平滑
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 实时更新图像数据到画布
|
||
* @param {ImageData} imageData 新的图像数据
|
||
* @param {Boolean} isDrawing 是否正在绘制(拖拽过程中)
|
||
* @returns {Promise} 更新完成的Promise
|
||
*/
|
||
async updateImage(imageData, isDrawing = false) {
|
||
if (!this.targetObject || !imageData) {
|
||
return;
|
||
}
|
||
|
||
// 节流控制
|
||
const now = Date.now();
|
||
if (now - this.lastUpdateTime < this.config.throttleTime && isDrawing) {
|
||
// 在绘制过程中进行节流,缓存最新的图像数据
|
||
this.pendingImageData = imageData;
|
||
return;
|
||
}
|
||
|
||
this.lastUpdateTime = now;
|
||
|
||
if (isDrawing && this.config.useDirectUpdate) {
|
||
// 拖拽过程中使用快速更新(降低质量以提高性能)
|
||
this._fastUpdate(imageData);
|
||
} else {
|
||
// 拖拽结束后使用完整更新(最高质量)
|
||
await this._fullUpdate(imageData);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 智能图像质量更新
|
||
* 根据图像尺寸和设备性能动态调整质量
|
||
* @param {ImageData} imageData 图像数据
|
||
* @param {Boolean} isDrawing 是否正在绘制
|
||
* @private
|
||
*/
|
||
_getOptimalQuality(imageData, isDrawing) {
|
||
const pixelCount = imageData.width * imageData.height;
|
||
|
||
if (isDrawing) {
|
||
// 拖拽时根据图像大小调整质量
|
||
if (pixelCount > 1000000) {
|
||
// 大于1M像素
|
||
return 0.7;
|
||
} else if (pixelCount > 500000) {
|
||
// 大于500K像素
|
||
return 0.8;
|
||
} else {
|
||
return 0.9;
|
||
}
|
||
} else {
|
||
// 拖拽结束时始终使用最高质量
|
||
return 1.0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 快速更新 - 直接修改现有对象的图像源
|
||
* @param {ImageData} imageData 图像数据
|
||
* @private
|
||
*/
|
||
_fastUpdate(imageData) {
|
||
if (!this.targetObject || !this.targetObject._element) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 将ImageData渲染到临时canvas(快速模式)
|
||
this.tempCtx.putImageData(imageData, 0, 0);
|
||
|
||
// 获取智能质量设置
|
||
const quality = this._getOptimalQuality(imageData, true);
|
||
|
||
// 直接更新fabric对象的图像源(使用PNG格式保持质量)
|
||
const targetElement = this.targetObject._element;
|
||
|
||
// 方案1: 直接设置src属性(最高性能)
|
||
const dataURL = this.tempCanvas.toDataURL("image/png", quality);
|
||
|
||
if (targetElement.src !== dataURL) {
|
||
targetElement.src = dataURL;
|
||
|
||
// 关键优化:直接设置fabric对象为脏状态,但不立即渲染
|
||
// this.targetObject.dirty = false; // 标记为不需要立即渲染
|
||
// this.canvas.renderOnAddRemove = true; // 恢复自动渲染
|
||
// this.renderingScheduled = false; // 重置渲染调度状态
|
||
this?.scheduleRender?.(); // 调度一次渲染
|
||
// 使用requestAnimationFrame进行批量渲染优化
|
||
// if (!this.renderingScheduled && !this.config.skipRenderDuringDrag) {
|
||
// this.renderingScheduled = true;
|
||
// requestIdleCallback(() => {
|
||
// this.canvas.renderAll();
|
||
// this.renderingScheduled = false;
|
||
// });
|
||
// }
|
||
} else {
|
||
console.warn("=================快速更新液化效果时,图像数据未变化,跳过更新");
|
||
}
|
||
} catch (error) {
|
||
console.error("快速更新液化效果失败:", error);
|
||
}
|
||
}
|
||
|
||
getImageData(imageData) {
|
||
// 使用高质量canvas进行最终渲染
|
||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||
|
||
// 生成高质量DataURL(PNG格式,最大质量)
|
||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||
return dataURL;
|
||
}
|
||
|
||
/**
|
||
* 完整更新 - 创建新的fabric对象
|
||
* @param {ImageData} imageData 图像数据
|
||
* @private
|
||
*/
|
||
async _fullUpdate(imageData) {
|
||
return new Promise((resolve, reject) => {
|
||
// 临时禁用画布自动渲染
|
||
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
|
||
try {
|
||
// 使用高质量canvas进行最终渲染
|
||
this.highQualityCtx.putImageData(imageData, 0, 0);
|
||
|
||
// 生成高质量DataURL(PNG格式,最大质量)
|
||
const dataURL = this.highQualityCanvas.toDataURL("image/png", 1.0);
|
||
|
||
// 如果DataURL没有变化,跳过更新
|
||
if (this.cachedDataURL === dataURL) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
this.cachedDataURL = dataURL;
|
||
|
||
// 创建新的fabric图像对象,保持最高质量
|
||
fabric.Image.fromURL(
|
||
dataURL,
|
||
(newImg) => {
|
||
try {
|
||
if (!this.targetObject) {
|
||
console.warn("目标对象为空,跳过更新");
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// 保存原对象信息用于智能查找
|
||
const originalObjId = this.targetObject.id;
|
||
const originalObjLayerId = this.targetObject.layerId;
|
||
|
||
// 保留原对象的所有变换属性
|
||
const originalObj = this.targetObject;
|
||
newImg.set({
|
||
left: originalObj.left,
|
||
top: originalObj.top,
|
||
scaleX: originalObj.scaleX,
|
||
scaleY: originalObj.scaleY,
|
||
angle: originalObj.angle,
|
||
flipX: originalObj.flipX,
|
||
flipY: originalObj.flipY,
|
||
opacity: originalObj.opacity,
|
||
originX: originalObj.originX,
|
||
originY: originalObj.originY,
|
||
id: originalObj.id,
|
||
name: originalObj.name,
|
||
layerId: originalObj.layerId,
|
||
selected: false,
|
||
evented: originalObj.evented,
|
||
});
|
||
this.canvas.renderOnAddRemove = false;
|
||
|
||
// 智能查找和替换canvas上的对象
|
||
const allObjects = this.canvas.getObjects();
|
||
let targetIndex = allObjects.indexOf(originalObj);
|
||
|
||
// 如果直接查找失败,尝试通过ID查找
|
||
if (targetIndex === -1 && originalObjId) {
|
||
targetIndex = allObjects.findIndex((obj) => obj.id === originalObjId);
|
||
if (targetIndex !== -1) {
|
||
console.log(`通过ID找到目标对象: ${originalObjId}`);
|
||
// 更新目标对象引用
|
||
this.targetObject = allObjects[targetIndex];
|
||
}
|
||
}
|
||
|
||
// 如果通过ID查找仍然失败,尝试通过图层ID查找
|
||
if (targetIndex === -1 && originalObjLayerId) {
|
||
targetIndex = allObjects.findIndex((obj) => obj.layerId === originalObjLayerId);
|
||
if (targetIndex !== -1) {
|
||
console.log(`通过图层ID找到目标对象: ${originalObjLayerId}`);
|
||
// 更新目标对象引用
|
||
this.targetObject = allObjects[targetIndex];
|
||
}
|
||
}
|
||
|
||
if (targetIndex !== -1) {
|
||
// 找到目标对象,执行替换
|
||
this.canvas.remove(this.targetObject);
|
||
this.canvas.insertAt(newImg, targetIndex);
|
||
|
||
// 恢复自动渲染设置
|
||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||
|
||
// 更新目标对象引用
|
||
this.targetObject = newImg;
|
||
|
||
// 一次性重新渲染画布
|
||
this.canvas.renderAll();
|
||
|
||
console.log(`✅ 液化对象更新成功,位置: ${targetIndex}`);
|
||
resolve(newImg);
|
||
} else {
|
||
// 如果在画布中找不到对象,可能对象已被移除或引用已更新
|
||
console.warn("在画布中找不到目标对象,可能已被其他操作移除或替换");
|
||
|
||
// 恢复自动渲染设置
|
||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||
|
||
// 尝试添加新对象到画布末尾
|
||
this.canvas.add(newImg);
|
||
this.targetObject = newImg;
|
||
this.canvas.renderAll();
|
||
|
||
console.log("🔄 已将新对象添加到画布末尾");
|
||
resolve(newImg);
|
||
}
|
||
} catch (error) {
|
||
// 恢复自动渲染设置
|
||
this.canvas.renderOnAddRemove = oldRenderOnAddRemove;
|
||
console.error("更新fabric对象时出错:", error);
|
||
reject(error);
|
||
}
|
||
},
|
||
{ crossOrigin: "anonymous" }
|
||
); // 确保跨域支持
|
||
} catch (error) {
|
||
console.error("完整更新过程出错:", error);
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理待处理的图像数据
|
||
* 在拖拽结束后调用,处理可能积压的更新
|
||
*/
|
||
async processPendingUpdates() {
|
||
if (this.pendingImageData && !this.isUpdating) {
|
||
this.isUpdating = true;
|
||
try {
|
||
await this._fullUpdate(this.pendingImageData);
|
||
this.pendingImageData = null;
|
||
} catch (error) {
|
||
console.error("处理待处理更新失败:", error);
|
||
} finally {
|
||
this.isUpdating = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前目标对象
|
||
* @returns {Object} 当前的fabric对象
|
||
*/
|
||
getTargetObject() {
|
||
return this.targetObject;
|
||
}
|
||
|
||
/**
|
||
* 强制进行完整更新
|
||
* @param {ImageData} imageData 图像数据
|
||
*/
|
||
async forceFullUpdate(imageData) {
|
||
return this._fullUpdate(imageData);
|
||
}
|
||
|
||
/**
|
||
* 启用拖拽模式 - 暂停渲染以提高性能
|
||
*/
|
||
enableDragMode() {
|
||
this.config.skipRenderDuringDrag = true;
|
||
this.canvas.renderOnAddRemove = false;
|
||
console.log("🚀 启用拖拽优化模式");
|
||
}
|
||
|
||
/**
|
||
* 禁用拖拽模式 - 恢复正常渲染
|
||
*/
|
||
disableDragMode() {
|
||
this.config.skipRenderDuringDrag = false;
|
||
this.canvas.renderOnAddRemove = true;
|
||
|
||
// 执行一次完整渲染
|
||
this.canvas.renderAll();
|
||
console.log("✅ 恢复正常渲染模式");
|
||
}
|
||
|
||
/**
|
||
* 设置图像质量
|
||
* @param {Number} quality 质量值 (0.1-1.0)
|
||
*/
|
||
setImageQuality(quality) {
|
||
this.config.imageQuality = Math.max(0.1, Math.min(1.0, quality));
|
||
}
|
||
|
||
/**
|
||
* 优化的批量渲染方法
|
||
*/
|
||
scheduleRender() {
|
||
if (!this.renderingScheduled) {
|
||
this.renderingScheduled = true;
|
||
requestAnimationFrame(() => {
|
||
this.canvas.renderAll();
|
||
this.renderingScheduled = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
// /**
|
||
// * 清理资源
|
||
// */
|
||
// dispose() {
|
||
// this.targetObject = null;
|
||
// this.cachedDataURL = null;
|
||
// this.pendingImageData = null;
|
||
// this.updateQueue.length = 0;
|
||
|
||
// // 清理临时canvas
|
||
// if (this.tempCanvas) {
|
||
// this.tempCanvas.width = 0;
|
||
// this.tempCanvas.height = 0;
|
||
// this.tempCanvas = null;
|
||
// this.tempCtx = null;
|
||
// }
|
||
// }
|
||
|
||
/**
|
||
* 清理资源
|
||
*/
|
||
dispose() {
|
||
// 恢复canvas设置
|
||
this.canvas.renderOnAddRemove = true;
|
||
|
||
// 清理缓存
|
||
this.cachedDataURL = null;
|
||
this.pendingImageData = null;
|
||
|
||
// 清理canvas
|
||
if (this.tempCanvas) {
|
||
this.tempCanvas.width = 0;
|
||
this.tempCanvas.height = 0;
|
||
}
|
||
|
||
if (this.highQualityCanvas) {
|
||
this.highQualityCanvas.width = 0;
|
||
this.highQualityCanvas.height = 0;
|
||
}
|
||
|
||
console.log("🧹 液化实时更新器资源已清理");
|
||
}
|
||
}
|