合并画布代码
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
|
||||
import { LiquifyCPUManager } from "./LiquifyCPUManager";
|
||||
import { LayerType } from "../../utils/layerHelper";
|
||||
|
||||
export class EnhancedLiquifyManager {
|
||||
/**
|
||||
@@ -313,16 +314,40 @@ export class EnhancedLiquifyManager {
|
||||
if (param in this.params) {
|
||||
this.params[param] = value;
|
||||
|
||||
// 同步更新当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
// 同步更新当前渲染器 - 关键修复:确保参数正确传递
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.setParam === "function"
|
||||
) {
|
||||
console.log(`EnhancedLiquifyManager 设置参数: ${param}=${value}`);
|
||||
this.activeRenderer.setParam(param, value);
|
||||
} else {
|
||||
console.warn(
|
||||
`EnhancedLiquifyManager: 无法设置参数 ${param},渲染器未就绪`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(`EnhancedLiquifyManager: 无效参数 ${param}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("EnhancedLiquifyManager 批量设置参数:", params);
|
||||
|
||||
if (params && typeof params === "object") {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.setParam(key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -348,6 +373,36 @@ export class EnhancedLiquifyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.startDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.startDeformation(x, y);
|
||||
}
|
||||
console.log(
|
||||
`开始液化操作,渲染模式=${this.renderMode}, 初始点: (${x}, ${y})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
if (
|
||||
this.activeRenderer &&
|
||||
typeof this.activeRenderer.endDeformation === "function"
|
||||
) {
|
||||
this.activeRenderer.endDeformation();
|
||||
}
|
||||
console.log(`结束液化操作,渲染模式=${this.renderMode}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用液化变形
|
||||
* @param {Object} target 目标对象
|
||||
@@ -468,7 +523,9 @@ export class EnhancedLiquifyManager {
|
||||
// 注意:这里不自动切换,因为可能会导致中途渲染结果不一致
|
||||
}
|
||||
}
|
||||
|
||||
setRealtimeUpdater(realtimeUpdater) {
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
}
|
||||
/**
|
||||
* 重置液化操作
|
||||
* @returns {ImageData} 重置后的图像数据
|
||||
@@ -519,7 +576,7 @@ export class EnhancedLiquifyManager {
|
||||
|
||||
// 检查图层是否为空
|
||||
let objectsToCheck = [];
|
||||
if (layer.isBackground || layer.type === "background") {
|
||||
if (layer.isBackground || layer.type === "background" || layer.isFixed) {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (layer.fabricObject) {
|
||||
objectsToCheck = [layer.fabricObject];
|
||||
@@ -548,7 +605,10 @@ export class EnhancedLiquifyManager {
|
||||
objectsToCheck[0].type === "rasterized-layer");
|
||||
|
||||
// 检查是否为组
|
||||
const isGroup = objectsToCheck.some((obj) => obj.type === "group");
|
||||
const isGroup =
|
||||
objectsToCheck.some((obj) => obj.type === "group") ||
|
||||
layer.type === LayerType.GROUP ||
|
||||
layer.children?.length > 0;
|
||||
|
||||
// 如果不是单一图像,需要栅格化
|
||||
const needsRasterization = !isImage || isGroup;
|
||||
@@ -572,26 +632,34 @@ export class EnhancedLiquifyManager {
|
||||
async _getImageData(fabricObject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
// 创建临时canvas - 关键修复:使用原始图像尺寸,不考虑fabric对象的缩放
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = fabricObject.width * fabricObject.scaleX;
|
||||
tempCanvas.height = fabricObject.height * fabricObject.scaleY;
|
||||
// 使用图像的原始尺寸,而不是缩放后的尺寸
|
||||
tempCanvas.width = fabricObject.width;
|
||||
tempCanvas.height = fabricObject.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 如果对象有图像元素
|
||||
if (fabricObject._element) {
|
||||
// 绘制原始尺寸的图像
|
||||
tempCtx.drawImage(
|
||||
fabricObject._element,
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
} else if (fabricObject.getSrc) {
|
||||
// 通过URL创建图像
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
tempCtx.drawImage(
|
||||
img,
|
||||
0,
|
||||
0,
|
||||
fabricObject.width,
|
||||
fabricObject.height
|
||||
);
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
@@ -615,6 +683,13 @@ export class EnhancedLiquifyManager {
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
|
||||
console.log(
|
||||
`获取图像数据: 对象尺寸=${fabricObject.width}x${fabricObject.height}, ` +
|
||||
`对象缩放=(${fabricObject.scaleX}, ${fabricObject.scaleY}), ` +
|
||||
`图像数据尺寸=${imageData.width}x${imageData.height}`
|
||||
);
|
||||
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* 混合液化管理器 - 根据模式智能选择算法
|
||||
*/
|
||||
export class HybridLiquifyManager {
|
||||
constructor(options = {}) {
|
||||
this.config = {
|
||||
gridSize: options.gridSize || 16,
|
||||
maxStrength: options.maxStrength || 200,
|
||||
smoothingIterations: options.smoothingIterations || 1,
|
||||
relaxFactor: options.relaxFactor || 0.1,
|
||||
};
|
||||
|
||||
this.params = {
|
||||
size: 80,
|
||||
pressure: 0.8,
|
||||
distortion: 0,
|
||||
power: 0.8,
|
||||
};
|
||||
|
||||
this.modes = {
|
||||
PUSH: "push",
|
||||
CLOCKWISE: "clockwise",
|
||||
COUNTERCLOCKWISE: "counterclockwise",
|
||||
PINCH: "pinch",
|
||||
EXPAND: "expand",
|
||||
CRYSTAL: "crystal",
|
||||
EDGE: "edge",
|
||||
RECONSTRUCT: "reconstruct",
|
||||
};
|
||||
|
||||
// 定义哪些模式使用像素算法
|
||||
this.pixelModes = new Set([
|
||||
this.modes.CLOCKWISE,
|
||||
this.modes.COUNTERCLOCKWISE,
|
||||
this.modes.CRYSTAL,
|
||||
this.modes.EDGE,
|
||||
]);
|
||||
|
||||
// 定义哪些模式使用网格算法
|
||||
this.meshModes = new Set([
|
||||
this.modes.PUSH,
|
||||
this.modes.PINCH,
|
||||
this.modes.EXPAND,
|
||||
this.modes.RECONSTRUCT,
|
||||
]);
|
||||
|
||||
this.currentMode = this.modes.PUSH;
|
||||
this.originalImageData = null;
|
||||
this.currentImageData = null;
|
||||
this.mesh = null;
|
||||
this.initialized = false;
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
|
||||
// 持续按压相关状态
|
||||
this.pressStartTime = 0;
|
||||
this.pressDuration = 0;
|
||||
this.accumulatedRotation = 0;
|
||||
this.accumulatedScale = 0;
|
||||
this.isHolding = false;
|
||||
this.continuousTimer = null;
|
||||
|
||||
// 鼠标状态
|
||||
this.initialMouseX = 0;
|
||||
this.initialMouseY = 0;
|
||||
this.currentMouseX = 0;
|
||||
this.currentMouseY = 0;
|
||||
this.isDragging = false;
|
||||
this.dragDistance = 0;
|
||||
}
|
||||
|
||||
// ...existing initialization methods...
|
||||
|
||||
/**
|
||||
* 应用液化变形 - 智能选择算法
|
||||
*/
|
||||
applyDeformation(x, y) {
|
||||
if (!this.initialized || !this.originalImageData) {
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
// 更新鼠标位置
|
||||
this.currentMouseX = x;
|
||||
this.currentMouseY = y;
|
||||
|
||||
const { size, pressure, power } = this.params;
|
||||
const radius = size;
|
||||
const strength = pressure * power;
|
||||
|
||||
// 根据模式选择算法
|
||||
if (this.pixelModes.has(this.currentMode)) {
|
||||
return this._applyPixelDeformation(x, y, radius, strength);
|
||||
} else if (this.meshModes.has(this.currentMode)) {
|
||||
return this._applyMeshDeformation(x, y, radius, strength);
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级液化算法 - 适用于旋转、水晶、边缘模式
|
||||
*/
|
||||
_applyPixelDeformation(centerX, centerY, radius, strength) {
|
||||
const data = this.currentImageData.data;
|
||||
const width = this.currentImageData.width;
|
||||
const height = this.currentImageData.height;
|
||||
const tempData = new Uint8ClampedArray(data);
|
||||
|
||||
const processRadius = Math.min(radius, Math.min(width, height) / 2);
|
||||
|
||||
// 计算影响区域边界
|
||||
const minX = Math.max(0, Math.floor(centerX - processRadius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + processRadius));
|
||||
const minY = Math.max(0, Math.floor(centerY - processRadius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + processRadius));
|
||||
|
||||
switch (this.currentMode) {
|
||||
case this.modes.CLOCKWISE:
|
||||
case this.modes.COUNTERCLOCKWISE:
|
||||
this._applyPixelRotation(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength,
|
||||
this.currentMode === this.modes.CLOCKWISE
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.CRYSTAL:
|
||||
this._applyPixelCrystal(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
|
||||
case this.modes.EDGE:
|
||||
this._applyPixelEdge(
|
||||
tempData,
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
processRadius,
|
||||
strength
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级旋转算法
|
||||
*/
|
||||
_applyPixelRotation(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength,
|
||||
clockwise
|
||||
) {
|
||||
// 计算旋转角度
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
|
||||
const baseRotationSpeed = 0.015;
|
||||
const rotationAngle =
|
||||
(clockwise ? 1 : -1) *
|
||||
baseRotationSpeed *
|
||||
strength *
|
||||
(1.0 + timeFactor * 0.3);
|
||||
|
||||
this.accumulatedRotation += rotationAngle;
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
// 距离衰减:内圈快,外圈慢
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = Math.pow(1 - normalizedDistance, 2); // 二次衰减
|
||||
|
||||
// 计算旋转后的源位置
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const newAngle = angle + this.accumulatedRotation * falloff;
|
||||
|
||||
const sourceX = centerX + Math.cos(newAngle) * distance;
|
||||
const sourceY = centerY + Math.sin(newAngle) * distance;
|
||||
|
||||
// 双线性插值采样
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级水晶效果
|
||||
*/
|
||||
_applyPixelCrystal(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
|
||||
const distortionStrength = strength * (1.0 + timeFactor * 0.5);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const crystalRadius = normalizedDistance;
|
||||
|
||||
// 多层波浪扭曲
|
||||
const wave1 = Math.sin(angle * 8 + this.pressDuration * 0.005) * 0.6;
|
||||
const wave2 = Math.cos(angle * 12 + this.pressDuration * 0.003) * 0.4;
|
||||
const waveAngle =
|
||||
angle + (wave1 + wave2) * distortionStrength * falloff;
|
||||
|
||||
const radialMod =
|
||||
1 +
|
||||
Math.sin(crystalRadius * Math.PI * 2 + this.pressDuration * 0.002) *
|
||||
0.3;
|
||||
const modDistance = distance * radialMod;
|
||||
|
||||
const sourceX = centerX + Math.cos(waveAngle) * modDistance;
|
||||
const sourceY = centerY + Math.sin(waveAngle) * modDistance;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
const factor = falloff * distortionStrength * 0.7;
|
||||
|
||||
// 混合原始颜色和扭曲颜色
|
||||
const originalIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = Math.round(
|
||||
srcData[originalIdx] * (1 - factor) + color[0] * factor
|
||||
);
|
||||
dstData[targetIdx + 1] = Math.round(
|
||||
srcData[originalIdx + 1] * (1 - factor) + color[1] * factor
|
||||
);
|
||||
dstData[targetIdx + 2] = Math.round(
|
||||
srcData[originalIdx + 2] * (1 - factor) + color[2] * factor
|
||||
);
|
||||
dstData[targetIdx + 3] = srcData[originalIdx + 3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 像素级边缘效果
|
||||
*/
|
||||
_applyPixelEdge(
|
||||
srcData,
|
||||
dstData,
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
strength
|
||||
) {
|
||||
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
|
||||
const edgeStrength = strength * (1.0 + timeFactor * 0.4);
|
||||
|
||||
const minX = Math.max(0, Math.floor(centerX - radius));
|
||||
const maxX = Math.min(width, Math.ceil(centerX + radius));
|
||||
const minY = Math.max(0, Math.floor(centerY - radius));
|
||||
const maxY = Math.min(height, Math.ceil(centerY + radius));
|
||||
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < radius && distance > 0.1) {
|
||||
const normalizedDistance = distance / radius;
|
||||
const falloff = 1 - normalizedDistance * normalizedDistance;
|
||||
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const edgeRadius = normalizedDistance;
|
||||
|
||||
const edgeWave =
|
||||
Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) *
|
||||
Math.cos(angle * 6 + this.pressDuration * 0.002);
|
||||
const perpAngle = angle + Math.PI / 2;
|
||||
|
||||
const edgeFactor = edgeWave * falloff * edgeStrength * 0.5;
|
||||
const offsetX = Math.cos(perpAngle) * edgeFactor;
|
||||
const offsetY = Math.sin(perpAngle) * edgeFactor;
|
||||
|
||||
const sourceX = x + offsetX;
|
||||
const sourceY = y + offsetY;
|
||||
|
||||
const color = this._bilinearSample(
|
||||
srcData,
|
||||
width,
|
||||
height,
|
||||
sourceX,
|
||||
sourceY
|
||||
);
|
||||
|
||||
if (color) {
|
||||
const targetIdx = (y * width + x) * 4;
|
||||
dstData[targetIdx] = color[0];
|
||||
dstData[targetIdx + 1] = color[1];
|
||||
dstData[targetIdx + 2] = color[2];
|
||||
dstData[targetIdx + 3] = color[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 双线性插值采样
|
||||
*/
|
||||
_bilinearSample(data, width, height, x, y) {
|
||||
if (x < 0 || x >= width - 1 || y < 0 || y >= height - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const x1 = Math.floor(x);
|
||||
const y1 = Math.floor(y);
|
||||
const x2 = x1 + 1;
|
||||
const y2 = y1 + 1;
|
||||
|
||||
const fx = x - x1;
|
||||
const fy = y - y1;
|
||||
|
||||
const getPixel = (px, py) => {
|
||||
const idx = (py * width + px) * 4;
|
||||
return [data[idx], data[idx + 1], data[idx + 2], data[idx + 3]];
|
||||
};
|
||||
|
||||
const p1 = getPixel(x1, y1);
|
||||
const p2 = getPixel(x2, y1);
|
||||
const p3 = getPixel(x1, y2);
|
||||
const p4 = getPixel(x2, y2);
|
||||
|
||||
return [
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[0] +
|
||||
fx * (1 - fy) * p2[0] +
|
||||
(1 - fx) * fy * p3[0] +
|
||||
fx * fy * p4[0]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[1] +
|
||||
fx * (1 - fy) * p2[1] +
|
||||
(1 - fx) * fy * p3[1] +
|
||||
fx * fy * p4[1]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[2] +
|
||||
fx * (1 - fy) * p2[2] +
|
||||
(1 - fx) * fy * p3[2] +
|
||||
fx * fy * p4[2]
|
||||
),
|
||||
Math.round(
|
||||
(1 - fx) * (1 - fy) * p1[3] +
|
||||
fx * (1 - fy) * p2[3] +
|
||||
(1 - fx) * fy * p3[3] +
|
||||
fx * fy * p4[3]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格液化算法 - 适用于推拉、捏合、展开模式
|
||||
*/
|
||||
_applyMeshDeformation(x, y, radius, strength) {
|
||||
if (!this.mesh) return this.currentImageData;
|
||||
|
||||
// 使用现有的网格算法处理推拉、捏合、展开
|
||||
const mode = this.currentMode;
|
||||
const { distortion } = this.params;
|
||||
|
||||
this._applyDeformation(x, y, radius, strength, mode, distortion);
|
||||
|
||||
if (this.config.smoothingIterations > 0) {
|
||||
this._smoothMesh();
|
||||
}
|
||||
|
||||
return this._applyMeshToImage();
|
||||
}
|
||||
|
||||
// ...existing mesh methods...
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ export class LiquifyManager {
|
||||
meshResolution: options.meshResolution || 64,
|
||||
// 根据环境选择合适的渲染模式
|
||||
forceCPU: true, // 默认不强制使用CPU
|
||||
forceWebGL: false, // 优先使用WebGL模式
|
||||
forceWebGL: true, // 优先使用WebGL模式
|
||||
webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL
|
||||
layerManager: options.layerManager || null,
|
||||
canvas: options.canvas || null,
|
||||
@@ -87,9 +87,19 @@ export class LiquifyManager {
|
||||
* @param {Number} value 参数值
|
||||
*/
|
||||
setParam(param, value) {
|
||||
console.log(`LiquifyManager 设置参数: ${param}=${value}`);
|
||||
return this.enhancedManager.setParam(param, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置参数
|
||||
* @param {Object} params 参数对象
|
||||
*/
|
||||
setParams(params) {
|
||||
console.log("LiquifyManager 批量设置参数:", params);
|
||||
return this.enhancedManager.setParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
@@ -120,21 +130,14 @@ export class LiquifyManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保设置正确的模式和参数
|
||||
if (mode) {
|
||||
this.enhancedManager.setMode(mode);
|
||||
}
|
||||
console.log(
|
||||
`LiquifyManager.applyLiquify: 模式=${mode}, 坐标=(${x}, ${y}), 参数=`,
|
||||
params
|
||||
);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.enhancedManager.setParam(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 应用液化变形
|
||||
console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
|
||||
try {
|
||||
// 直接调用EnhancedLiquifyManager的applyLiquify方法
|
||||
// 避免重复设置参数,让EnhancedLiquifyManager处理参数设置
|
||||
const resultData = await this.enhancedManager.applyLiquify(
|
||||
targetObject,
|
||||
mode,
|
||||
@@ -146,6 +149,13 @@ export class LiquifyManager {
|
||||
// 确保返回结果数据
|
||||
if (!resultData) {
|
||||
console.warn("液化变形没有返回结果数据");
|
||||
} else {
|
||||
console.log(
|
||||
"✅ 液化变形成功,返回图像数据尺寸:",
|
||||
resultData.width,
|
||||
"x",
|
||||
resultData.height
|
||||
);
|
||||
}
|
||||
|
||||
return resultData;
|
||||
@@ -180,6 +190,22 @@ export class LiquifyManager {
|
||||
return this.enhancedManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作(记录初始点)
|
||||
* @param {Number} x 初始X坐标
|
||||
* @param {Number} y 初始Y坐标
|
||||
*/
|
||||
startLiquifyOperation(x, y) {
|
||||
return this.enhancedManager.startLiquifyOperation(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endLiquifyOperation() {
|
||||
return this.enhancedManager.endLiquifyOperation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* 液化实时更新器
|
||||
* 负责高效地更新液化效果到画布上,避免频繁创建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) => {
|
||||
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,
|
||||
});
|
||||
|
||||
// 临时禁用画布自动渲染
|
||||
const oldRenderOnAddRemove = this.canvas.renderOnAddRemove;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
* @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("✅ 恢复正常渲染模式");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前目标对象
|
||||
*/
|
||||
getTargetObject() {
|
||||
return this.targetObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图像质量
|
||||
* @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() {
|
||||
// 恢复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("🧹 液化实时更新器资源已清理");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
/**
|
||||
* 液化面板状态管理器
|
||||
* 负责管理液化操作的状态、性能优化和用户反馈
|
||||
*/
|
||||
export class LiquifyStateManager {
|
||||
constructor(canvas, realtimeUpdater) {
|
||||
this.canvas = canvas;
|
||||
this.realtimeUpdater = realtimeUpdater;
|
||||
|
||||
// 状态管理
|
||||
this.isOperating = false;
|
||||
this.isDragging = false;
|
||||
this.operationCount = 0;
|
||||
this.startTime = null;
|
||||
|
||||
// 性能监控
|
||||
this.performanceMetrics = {
|
||||
totalOperations: 0,
|
||||
totalTime: 0,
|
||||
averageTime: 0,
|
||||
maxTime: 0,
|
||||
minTime: Infinity,
|
||||
lastOperationTime: 0,
|
||||
};
|
||||
|
||||
// 用户反馈
|
||||
this.feedbackEnabled = true;
|
||||
this.cursorCache = new Map();
|
||||
|
||||
// 设备性能检测
|
||||
this.devicePerformance = this._detectDevicePerformance();
|
||||
|
||||
console.log(
|
||||
"🎯 液化状态管理器已初始化,设备性能等级:",
|
||||
this.devicePerformance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始液化操作
|
||||
*/
|
||||
startOperation() {
|
||||
if (this.isOperating) return;
|
||||
|
||||
this.isOperating = true;
|
||||
this.startTime = performance.now();
|
||||
|
||||
// 根据设备性能调整设置
|
||||
this._adjustPerformanceSettings();
|
||||
|
||||
// 显示操作反馈
|
||||
this._showOperationFeedback();
|
||||
|
||||
console.log("🚀 开始液化操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始拖拽
|
||||
*/
|
||||
startDrag() {
|
||||
if (this.isDragging) return;
|
||||
|
||||
this.isDragging = true;
|
||||
|
||||
// 优化拖拽性能
|
||||
this.realtimeUpdater?.enableDragMode();
|
||||
|
||||
// 更新鼠标样式
|
||||
this._updateCursor("liquifying");
|
||||
|
||||
// 禁用不必要的画布功能
|
||||
this._disableCanvasFeatures();
|
||||
|
||||
console.log("🖱️ 开始拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束拖拽
|
||||
*/
|
||||
async endDrag() {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
// 恢复鼠标样式
|
||||
this._updateCursor("default");
|
||||
|
||||
// 恢复画布功能
|
||||
this._enableCanvasFeatures();
|
||||
|
||||
// 处理待处理的更新
|
||||
if (this.realtimeUpdater) {
|
||||
try {
|
||||
await this.realtimeUpdater.processPendingUpdates();
|
||||
} catch (error) {
|
||||
console.error("处理待处理更新失败:", error);
|
||||
} finally {
|
||||
this.isDragging = false;
|
||||
// 恢复正常模式
|
||||
this.realtimeUpdater?.disableDragMode();
|
||||
|
||||
// 结束液化操作 添加结果到命令中 更新当前激活图层对象
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ 结束拖拽操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束液化操作
|
||||
*/
|
||||
endOperation() {
|
||||
if (!this.isOperating) return;
|
||||
|
||||
const operationTime = performance.now() - this.startTime;
|
||||
this._updatePerformanceMetrics(operationTime);
|
||||
|
||||
this.isOperating = false;
|
||||
this.operationCount++;
|
||||
|
||||
// 隐藏操作反馈
|
||||
this._hideOperationFeedback();
|
||||
|
||||
console.log(`⏱️ 液化操作完成,耗时: ${operationTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录单次变形操作
|
||||
*/
|
||||
recordDeformation(operationTime) {
|
||||
this.performanceMetrics.totalOperations++;
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime /
|
||||
this.performanceMetrics.totalOperations;
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.lastOperationTime = operationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作性能指标
|
||||
* @param {Object} metrics 性能指标对象
|
||||
*/
|
||||
recordOperationMetrics(metrics) {
|
||||
const {
|
||||
operationTime,
|
||||
operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
} = metrics;
|
||||
|
||||
// 记录基础性能数据
|
||||
this.recordDeformation(operationTime);
|
||||
|
||||
// 记录详细操作信息
|
||||
this.performanceMetrics.lastOperation = {
|
||||
type: operationType,
|
||||
mode,
|
||||
coordinates,
|
||||
imageSize,
|
||||
renderMode,
|
||||
isRealTime,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 根据性能数据动态调整设置
|
||||
this._adaptivePerformanceOptimization(operationTime);
|
||||
|
||||
console.log(
|
||||
`📊 记录性能指标: ${operationType}/${mode}, 耗时: ${operationTime.toFixed(
|
||||
2
|
||||
)}ms, 渲染模式: ${renderMode}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应性能优化
|
||||
* @param {Number} operationTime 操作耗时
|
||||
* @private
|
||||
*/
|
||||
_adaptivePerformanceOptimization(operationTime) {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
// 如果操作耗时过长,动态降低质量或增加节流时间
|
||||
if (operationTime > 50 && this.devicePerformance !== "high") {
|
||||
// 降低图像质量
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality > 0.7) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.max(0.7, currentQuality - 0.1)
|
||||
);
|
||||
console.log("⚡ 自动降低图像质量以提升性能");
|
||||
}
|
||||
|
||||
// 增加节流时间
|
||||
if (this.realtimeUpdater.config.throttleTime < 33) {
|
||||
this.realtimeUpdater.config.throttleTime = Math.min(
|
||||
33,
|
||||
this.realtimeUpdater.config.throttleTime + 8
|
||||
);
|
||||
console.log("⏱️ 自动增加节流时间以提升性能");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果性能很好,可以适当提高质量
|
||||
if (operationTime < 20 && this.devicePerformance === "high") {
|
||||
const currentQuality = this.realtimeUpdater.config.imageQuality || 1.0;
|
||||
if (currentQuality < 1.0) {
|
||||
this.realtimeUpdater.setImageQuality(
|
||||
Math.min(1.0, currentQuality + 0.05)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能报告
|
||||
*/
|
||||
getPerformanceReport() {
|
||||
return {
|
||||
...this.performanceMetrics,
|
||||
devicePerformance: this.devicePerformance,
|
||||
fps: this._calculateFPS(),
|
||||
recommendations: this._generateRecommendations(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户反馈
|
||||
*/
|
||||
setFeedbackEnabled(enabled) {
|
||||
this.feedbackEnabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
this._enableCanvasFeatures();
|
||||
this._updateCursor("default");
|
||||
this.cursorCache.clear();
|
||||
|
||||
console.log("🧹 液化状态管理器已清理");
|
||||
}
|
||||
|
||||
// === 私有方法 ===
|
||||
|
||||
/**
|
||||
* 检测设备性能
|
||||
*/
|
||||
_detectDevicePerformance() {
|
||||
// 检测硬件并发数
|
||||
const cores = navigator.hardwareConcurrency || 4;
|
||||
|
||||
// 检测内存
|
||||
const memory = navigator.deviceMemory || 4;
|
||||
|
||||
// 检测连接类型
|
||||
const connection = navigator.connection;
|
||||
const effectiveType = connection?.effectiveType || "4g";
|
||||
|
||||
// 简单的性能评分算法
|
||||
let score = 0;
|
||||
score += cores * 10;
|
||||
score += memory * 5;
|
||||
|
||||
if (effectiveType === "4g") score += 10;
|
||||
else if (effectiveType === "3g") score += 5;
|
||||
|
||||
// 性能等级
|
||||
if (score >= 70) return "high";
|
||||
if (score >= 40) return "medium";
|
||||
return "low";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设备性能调整设置
|
||||
*/
|
||||
_adjustPerformanceSettings() {
|
||||
if (!this.realtimeUpdater) return;
|
||||
|
||||
switch (this.devicePerformance) {
|
||||
case "high":
|
||||
this.realtimeUpdater.setImageQuality(1.0);
|
||||
this.realtimeUpdater.config.throttleTime = 8; // 120fps
|
||||
break;
|
||||
case "medium":
|
||||
this.realtimeUpdater.setImageQuality(0.9);
|
||||
this.realtimeUpdater.config.throttleTime = 16; // 60fps
|
||||
break;
|
||||
case "low":
|
||||
this.realtimeUpdater.setImageQuality(0.8);
|
||||
this.realtimeUpdater.config.throttleTime = 33; // 30fps
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示操作反馈
|
||||
*/
|
||||
_showOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
// 添加视觉反馈(可以是加载动画、进度条等)
|
||||
document.body.style.cursor = "wait";
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏操作反馈
|
||||
*/
|
||||
_hideOperationFeedback() {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
document.body.style.cursor = "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新鼠标样式
|
||||
*/
|
||||
_updateCursor(type) {
|
||||
if (!this.feedbackEnabled) return;
|
||||
|
||||
const cursors = {
|
||||
default: "default",
|
||||
liquifying: "crosshair",
|
||||
wait: "wait",
|
||||
"not-allowed": "not-allowed",
|
||||
};
|
||||
|
||||
const cursor = cursors[type] || "default";
|
||||
|
||||
if (this.canvas && this.canvas.upperCanvasEl) {
|
||||
this.canvas.upperCanvasEl.style.cursor = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用画布功能以提高性能
|
||||
*/
|
||||
_disableCanvasFeatures() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 保存原始设置
|
||||
this._originalSettings = {
|
||||
renderOnAddRemove: this.canvas.renderOnAddRemove,
|
||||
skipOffscreen: this.canvas.skipOffscreen,
|
||||
enableRetinaScaling: this.canvas.enableRetinaScaling,
|
||||
};
|
||||
|
||||
// 应用性能优化设置
|
||||
this.canvas.renderOnAddRemove = false;
|
||||
this.canvas.skipOffscreen = true;
|
||||
|
||||
// 低性能设备关闭高分辨率支持
|
||||
if (this.devicePerformance === "low") {
|
||||
this.canvas.enableRetinaScaling = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复画布功能
|
||||
*/
|
||||
_enableCanvasFeatures() {
|
||||
if (!this.canvas || !this._originalSettings) return;
|
||||
|
||||
// 恢复原始设置
|
||||
this.canvas.renderOnAddRemove = this._originalSettings.renderOnAddRemove;
|
||||
this.canvas.skipOffscreen = this._originalSettings.skipOffscreen;
|
||||
this.canvas.enableRetinaScaling =
|
||||
this._originalSettings.enableRetinaScaling;
|
||||
|
||||
this._originalSettings = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新性能指标
|
||||
*/
|
||||
_updatePerformanceMetrics(operationTime) {
|
||||
this.performanceMetrics.totalTime += operationTime;
|
||||
this.performanceMetrics.averageTime =
|
||||
this.performanceMetrics.totalTime / (this.operationCount + 1);
|
||||
|
||||
this.performanceMetrics.maxTime = Math.max(
|
||||
this.performanceMetrics.maxTime,
|
||||
operationTime
|
||||
);
|
||||
this.performanceMetrics.minTime = Math.min(
|
||||
this.performanceMetrics.minTime,
|
||||
operationTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算FPS
|
||||
*/
|
||||
_calculateFPS() {
|
||||
if (this.performanceMetrics.averageTime === 0) return 0;
|
||||
return Math.round(1000 / this.performanceMetrics.averageTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成性能建议
|
||||
*/
|
||||
_generateRecommendations() {
|
||||
const recommendations = [];
|
||||
|
||||
if (this.performanceMetrics.averageTime > 50) {
|
||||
recommendations.push("操作响应较慢,建议降低图像尺寸或关闭高质量模式");
|
||||
}
|
||||
|
||||
if (this.devicePerformance === "low") {
|
||||
recommendations.push("检测到低性能设备,已自动启用性能优化模式");
|
||||
}
|
||||
|
||||
const fps = this._calculateFPS();
|
||||
if (fps < 30) {
|
||||
recommendations.push("帧率较低,建议减少同时进行的操作或降低液化强度");
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user