452 lines
12 KiB
JavaScript
452 lines
12 KiB
JavaScript
|
|
/**
|
||
|
|
* 混合液化管理器 - 根据模式智能选择算法
|
||
|
|
*/
|
||
|
|
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...
|
||
|
|
}
|