Files
aida_front/src/component/Canvas/CanvasEditor/managers/liquify/LiquifyCPUManager.js

595 lines
18 KiB
JavaScript
Raw Normal View History

2025-06-09 10:25:54 +08:00
/**
* CPU版本的液化管理器
* 修复版本 - 解决三角形网格失真问题
*/
export class LiquifyCPUManager {
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.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.deformHistory = [];
// 性能优化相关
this.lastUpdateTime = 0;
this.updateThrottle = 16; // 限制更新频率约60fps
this.isProcessing = false;
// 鼠标位置跟踪(用于推拉模式)
this.lastMouseX = 0;
this.lastMouseY = 0;
this.mouseMovementX = 0;
this.mouseMovementY = 0;
this.isFirstApply = true; // 标记是否是首次应用
}
initialize(imageSource) {
try {
if (imageSource instanceof ImageData) {
this.originalImageData = new ImageData(
new Uint8ClampedArray(imageSource.data),
imageSource.width,
imageSource.height
);
} else if (imageSource instanceof HTMLImageElement) {
this.canvas.width = imageSource.width;
this.canvas.height = imageSource.height;
this.ctx.drawImage(imageSource, 0, 0);
this.originalImageData = this.ctx.getImageData(
0,
0,
imageSource.width,
imageSource.height
);
} else {
throw new Error("不支持的图像类型");
}
this.currentImageData = new ImageData(
new Uint8ClampedArray(this.originalImageData.data),
this.originalImageData.width,
this.originalImageData.height
);
this._initMesh(
this.originalImageData.width,
this.originalImageData.height
);
this.initialized = true;
return true;
} catch (error) {
console.error("液化管理器初始化失败:", error);
return false;
}
}
_initMesh(width, height) {
const gridSize = this.config.gridSize;
const cols = Math.ceil(width / gridSize);
const rows = Math.ceil(height / gridSize);
this.mesh = {
cols,
rows,
gridSize,
width,
height,
originalPoints: [],
deformedPoints: [],
};
for (let y = 0; y <= rows; y++) {
for (let x = 0; x <= cols; x++) {
const point = { x: x * gridSize, y: y * gridSize };
this.mesh.originalPoints.push({ ...point });
this.mesh.deformedPoints.push({ ...point });
}
}
}
setMode(mode) {
if (Object.values(this.modes).includes(mode)) {
this.currentMode = mode;
return true;
}
return false;
}
setParam(param, value) {
if (param in this.params) {
this.params[param] = value;
return true;
}
return false;
}
getParams() {
return { ...this.params };
}
resetParams() {
this.params = {
size: 80, // 增大默认尺寸
pressure: 0.8, // 增大默认压力
distortion: 0,
power: 0.8, // 增大默认动力
};
}
applyDeformation(x, y) {
// 计算鼠标移动方向
if (!this.isFirstApply) {
this.mouseMovementX = x - this.lastMouseX;
this.mouseMovementY = y - this.lastMouseY;
} else {
// 首次应用时不计算移动,避免初始变形
this.mouseMovementX = 0;
this.mouseMovementY = 0;
this.isFirstApply = false;
}
this.lastMouseX = x;
this.lastMouseY = y;
// 性能优化:限制更新频率
const now = Date.now();
if (now - this.lastUpdateTime < this.updateThrottle || this.isProcessing) {
return this.currentImageData;
}
this.isProcessing = true;
this.lastUpdateTime = now;
if (!this.initialized || !this.mesh) {
this.isProcessing = false;
return this.currentImageData;
}
const { size, pressure, distortion, power } = this.params;
const mode = this.currentMode;
const radius = size * 1.2; // 稍微增大影响半径
const strength = (pressure * power * this.config.maxStrength) / 20; // 调整基础强度
this._applyDeformation(x, y, radius, strength, mode, distortion);
if (this.config.smoothingIterations > 0) {
this._smoothMesh();
}
const result = this._applyMeshToImage();
this.isProcessing = false;
return result;
}
_applyDeformation(x, y, radius, strength, mode, distortion) {
if (!this.mesh) return;
const points = this.mesh.deformedPoints;
const originalPoints = this.mesh.originalPoints;
for (let i = 0; i < points.length; i++) {
const point = points[i];
const originalPoint = originalPoints[i];
const dx = point.x - x;
const dy = point.y - y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius && distance > 0) {
// 使用平方衰减函数
const factor = Math.pow(1 - distance / radius, 2) * strength * 0.1; // 大幅降低基础系数
switch (mode) {
case this.modes.PUSH: {
// 推拉模式 - 真正的拖拽效果
// 计算实际移动距离
const movementLength = Math.sqrt(
this.mouseMovementX * this.mouseMovementX +
this.mouseMovementY * this.mouseMovementY
);
// 只有在有足够移动距离时才应用效果
if (movementLength > 1.0) {
// 提高阈值,确保有明显移动
// 归一化移动方向
const moveX = this.mouseMovementX / movementLength;
const moveY = this.mouseMovementY / movementLength;
// 计算衰减(距离中心越近,效果越强)
const radiusRatio = distance / radius;
const falloff = Math.pow(1 - radiusRatio, 2.0); // 使用更强的衰减
// 基于实际移动距离计算强度
const { pressure, power } = this.params;
const moveStrength = pressure * power * movementLength * 0.3; // 降低移动强度系数
// 计算最终拖拽强度
const dragStrength = moveStrength * falloff * factor;
// 向鼠标移动方向拖拽
const dragX = moveX * dragStrength;
const dragY = moveY * dragStrength;
// 应用变形,但限制最大变形量
const maxDeform = 2.0; // 限制单次最大变形量
point.x += Math.max(-maxDeform, Math.min(maxDeform, dragX));
point.y += Math.max(-maxDeform, Math.min(maxDeform, dragY));
}
break;
}
case this.modes.CLOCKWISE:
case this.modes.COUNTERCLOCKWISE: {
// 旋转模式 - 保持原有效果
const angle = Math.atan2(dy, dx);
const direction = mode === this.modes.CLOCKWISE ? 1 : -1;
const rotationAngle = angle + direction * factor;
const newX = x + Math.cos(rotationAngle) * distance;
const newY = y + Math.sin(rotationAngle) * distance;
point.x += (newX - point.x) * 0.8;
point.y += (newY - point.y) * 0.8;
break;
}
case this.modes.PINCH: {
// 捏合模式 - 保持原有效果
const pinchStrength = factor * 1.2;
point.x -= dx * pinchStrength;
point.y -= dy * pinchStrength;
break;
}
case this.modes.EXPAND: {
// 展开模式 - 参考捏合的反向操作
const expandFactor = factor * 1.5;
point.x += dx * expandFactor;
point.y += dy * expandFactor;
break;
}
case this.modes.CRYSTAL: {
// 水晶模式 - 参考旋转算法创建多重波形
const crystalAngle = Math.atan2(dy, dx);
const crystalRadius = distance / radius;
// 确保有基础效果
const baseDistortion = Math.max(distortion, 0.3);
// 多重波形 - 类似旋转的角度调制
const wave1 = Math.sin(crystalAngle * 8) * 0.6;
const wave2 = Math.cos(crystalAngle * 12) * 0.4;
const waveAngle = crystalAngle + (wave1 + wave2) * baseDistortion;
// 径向调制 - 类似旋转的距离调制
const radialMod = 1 + Math.sin(crystalRadius * Math.PI * 2) * 0.3;
const modDistance = distance * radialMod;
const crystalX = x + Math.cos(waveAngle) * modDistance;
const crystalY = y + Math.sin(waveAngle) * modDistance;
const crystalFactor = factor * baseDistortion;
point.x += (crystalX - point.x) * crystalFactor;
point.y += (crystalY - point.y) * crystalFactor;
break;
}
case this.modes.EDGE: {
// 边缘模式 - 参考旋转算法创建垂直波纹
const edgeAngle = Math.atan2(dy, dx);
const edgeRadius = distance / radius;
// 确保有基础效果
const baseEdgeDistortion = Math.max(distortion, 0.5);
// 边缘波纹 - 垂直于径向的调制
const edgeWave =
Math.sin(edgeRadius * Math.PI * 4) * Math.cos(edgeAngle * 6);
const perpAngle = edgeAngle + Math.PI / 2; // 垂直角度
const edgeFactor = edgeWave * factor * baseEdgeDistortion;
const edgeOffsetX = Math.cos(perpAngle) * edgeFactor;
const edgeOffsetY = Math.sin(perpAngle) * edgeFactor;
point.x += edgeOffsetX;
point.y += edgeOffsetY;
break;
}
case this.modes.RECONSTRUCT: {
// 重建模式 - 向原始位置恢复
const restoreFactor = factor * 0.15;
point.x += (originalPoint.x - point.x) * restoreFactor;
point.y += (originalPoint.y - point.y) * restoreFactor;
break;
}
}
}
}
}
// 优化衰减函数,使过渡更平滑
_smoothFalloff(t) {
if (t >= 1) return 0;
// 使用更平滑的衰减曲线
const smoothT = 1 - t;
return smoothT * smoothT * smoothT * (3 - 2 * smoothT);
}
_smoothMesh() {
const { rows, cols } = this.mesh;
const points = this.mesh.deformedPoints;
const tempPoints = points.map((p) => ({ x: p.x, y: p.y }));
for (
let iteration = 0;
iteration < this.config.smoothingIterations;
iteration++
) {
for (let y = 1; y < rows; y++) {
for (let x = 1; x < cols; x++) {
const idx = y * (cols + 1) + x;
const left = points[y * (cols + 1) + (x - 1)];
const right = points[y * (cols + 1) + (x + 1)];
const top = points[(y - 1) * (cols + 1) + x];
const bottom = points[(y + 1) * (cols + 1) + x];
const centerX = (left.x + right.x + top.x + bottom.x) / 4;
const centerY = (left.y + right.y + top.y + bottom.y) / 4;
const relaxFactor = this.config.relaxFactor;
tempPoints[idx].x += (centerX - points[idx].x) * relaxFactor;
tempPoints[idx].y += (centerY - points[idx].y) * relaxFactor;
}
}
for (let i = 0; i < points.length; i++) {
points[i].x = tempPoints[i].x;
points[i].y = tempPoints[i].y;
}
}
}
_applyMeshToImage() {
if (!this.mesh || !this.originalImageData) {
return this.currentImageData;
}
const width = this.originalImageData.width;
const height = this.originalImageData.height;
const result = new ImageData(width, height);
const srcData = this.originalImageData.data;
const dstData = result.data;
// 性能优化:使用步长采样减少计算量
const step = width > 1000 || height > 1000 ? 2 : 1;
for (let y = 0; y < height; y += step) {
for (let x = 0; x < width; x += step) {
const srcPos = this._mapPointBack(x, y);
if (
srcPos.x >= 0 &&
srcPos.x < width &&
srcPos.y >= 0 &&
srcPos.y < height
) {
const color = this._bilinearInterpolate(
srcData,
width,
height,
srcPos.x,
srcPos.y
);
// 如果使用步长采样,需要填充相邻像素
for (let dy = 0; dy < step && y + dy < height; dy++) {
for (let dx = 0; dx < step && x + dx < width; dx++) {
const dstIdx = ((y + dy) * width + (x + dx)) * 4;
dstData[dstIdx] = color[0];
dstData[dstIdx + 1] = color[1];
dstData[dstIdx + 2] = color[2];
dstData[dstIdx + 3] = color[3];
}
}
}
}
}
this.currentImageData = result;
return result;
}
// 添加异步处理方法用于大图像
async applyDeformationAsync(x, y) {
return new Promise((resolve) => {
setTimeout(() => {
const result = this.applyDeformation(x, y);
resolve(result);
}, 0);
});
}
// 批量处理方法
applyDeformationBatch(positions) {
if (!this.initialized || !this.mesh || positions.length === 0) {
return this.currentImageData;
}
const { size, pressure, distortion, power } = this.params;
const mode = this.currentMode;
const radius = size * 1.0;
const strength = (pressure * power * this.config.maxStrength) / 60;
// 批量应用所有变形
positions.forEach((pos) => {
this._applyDeformation(
pos.x,
pos.y,
radius * 0.5,
strength * 0.3,
mode,
distortion
);
});
if (this.config.smoothingIterations > 0) {
this._smoothMesh();
}
return this._applyMeshToImage();
}
_mapPointBack(x, y) {
const { cols, rows, gridSize } = this.mesh;
const gridX = x / gridSize;
const gridY = y / gridSize;
const x1 = Math.floor(gridX);
const y1 = Math.floor(gridY);
const x2 = Math.min(x1 + 1, cols);
const y2 = Math.min(y1 + 1, rows);
const fx = gridX - x1;
const fy = gridY - y1;
// 获取四个网格点的变形和原始坐标
const deformed = [
this.mesh.deformedPoints[y1 * (cols + 1) + x1],
this.mesh.deformedPoints[y1 * (cols + 1) + x2],
this.mesh.deformedPoints[y2 * (cols + 1) + x1],
this.mesh.deformedPoints[y2 * (cols + 1) + x2],
];
const original = [
this.mesh.originalPoints[y1 * (cols + 1) + x1],
this.mesh.originalPoints[y1 * (cols + 1) + x2],
this.mesh.originalPoints[y2 * (cols + 1) + x1],
this.mesh.originalPoints[y2 * (cols + 1) + x2],
];
// 双线性插值计算变形后的位置
const deformedX =
(1 - fx) * (1 - fy) * deformed[0].x +
fx * (1 - fy) * deformed[1].x +
(1 - fx) * fy * deformed[2].x +
fx * fy * deformed[3].x;
const deformedY =
(1 - fx) * (1 - fy) * deformed[0].y +
fx * (1 - fy) * deformed[1].y +
(1 - fx) * fy * deformed[2].y +
fx * fy * deformed[3].y;
// 计算原始网格位置
const originalX = x1 * gridSize + fx * gridSize;
const originalY = y1 * gridSize + fy * gridSize;
// 计算偏移量并应用反向映射
const offsetX = deformedX - originalX;
const offsetY = deformedY - originalY;
return {
x: x - offsetX,
y: y - offsetY,
};
}
_bilinearInterpolate(data, width, height, x, y) {
const x1 = Math.floor(x);
const y1 = Math.floor(y);
const x2 = Math.min(x1 + 1, width - 1);
const y2 = Math.min(y1 + 1, height - 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]
),
];
}
reset() {
if (!this.mesh || !this.originalImageData) return false;
for (let i = 0; i < this.mesh.deformedPoints.length; i++) {
this.mesh.deformedPoints[i].x = this.mesh.originalPoints[i].x;
this.mesh.deformedPoints[i].y = this.mesh.originalPoints[i].y;
}
this.currentImageData = new ImageData(
new Uint8ClampedArray(this.originalImageData.data),
this.originalImageData.width,
this.originalImageData.height
);
this.deformHistory = [];
return true;
}
getCurrentImageData() {
return this.currentImageData;
}
destroy() {
this.originalImageData = null;
this.currentImageData = null;
this.mesh = null;
this.deformHistory = [];
this.initialized = false;
}
}