接入画布
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user