合并画布代码
This commit is contained in:
@@ -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...
|
||||
}
|
||||
Reference in New Issue
Block a user