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

452 lines
12 KiB
JavaScript
Raw Normal View History

2025-06-18 11:05:23 +08:00
/**
* 混合液化管理器 - 根据模式智能选择算法
*/
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...
}