2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* CPU版本的液化管理器
|
2025-06-18 11:05:23 +08:00
|
|
|
|
* 修复版本 - 解决三角形网格失真问题,优化持续按压效果
|
2025-06-09 10:25:54 +08:00
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// 鼠标位置跟踪(用于推拉模式)
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.initialMouseX = 0; // 初始点击位置X
|
|
|
|
|
|
this.initialMouseY = 0; // 初始点击位置Y
|
|
|
|
|
|
this.currentMouseX = 0; // 当前鼠标位置X
|
|
|
|
|
|
this.currentMouseY = 0; // 当前鼠标位置Y
|
2025-06-09 10:25:54 +08:00
|
|
|
|
this.lastMouseX = 0;
|
|
|
|
|
|
this.lastMouseY = 0;
|
|
|
|
|
|
this.mouseMovementX = 0;
|
|
|
|
|
|
this.mouseMovementY = 0;
|
|
|
|
|
|
this.isFirstApply = true; // 标记是否是首次应用
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.isDragging = false; // 标记是否正在拖拽
|
|
|
|
|
|
this.dragDistance = 0; // 拖拽距离
|
|
|
|
|
|
this.dragAngle = 0; // 拖拽角度
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:持续按压相关状态
|
|
|
|
|
|
this.pressStartTime = 0; // 按压开始时间
|
|
|
|
|
|
this.pressDuration = 0; // 按压持续时间
|
|
|
|
|
|
this.accumulatedRotation = 0; // 累积旋转角度(用于顺时针/逆时针)
|
|
|
|
|
|
this.accumulatedScale = 0; // 累积缩放量(用于捏合/展开)
|
|
|
|
|
|
this.lastApplyTime = 0; // 上次应用时间
|
|
|
|
|
|
this.continuousApplyInterval = 50; // 持续应用间隔(毫秒)
|
|
|
|
|
|
this.isHolding = false; // 是否正在持续按压
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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, // 增大默认动力
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 开始液化操作(记录初始点)
|
|
|
|
|
|
* @param {Number} x 初始X坐标
|
|
|
|
|
|
* @param {Number} y 初始Y坐标
|
|
|
|
|
|
*/
|
|
|
|
|
|
startDeformation(x, y) {
|
|
|
|
|
|
this.initialMouseX = x;
|
|
|
|
|
|
this.initialMouseY = y;
|
|
|
|
|
|
this.currentMouseX = x;
|
|
|
|
|
|
this.currentMouseY = y;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
this.lastMouseX = x;
|
|
|
|
|
|
this.lastMouseY = y;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this.isDragging = true;
|
|
|
|
|
|
this.isFirstApply = true;
|
|
|
|
|
|
this.dragDistance = 0;
|
|
|
|
|
|
this.dragAngle = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:初始化持续按压状态
|
|
|
|
|
|
this.pressStartTime = Date.now();
|
|
|
|
|
|
this.pressDuration = 0;
|
|
|
|
|
|
this.accumulatedRotation = 0;
|
|
|
|
|
|
this.accumulatedScale = 0;
|
|
|
|
|
|
this.lastApplyTime = this.pressStartTime;
|
|
|
|
|
|
this.isHolding = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 启动持续效果定时器(对于所有模式都支持持续按压)
|
|
|
|
|
|
this.startContinuousEffect();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`开始液化操作,初始点: (${x}, ${y})`);
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 结束液化操作
|
|
|
|
|
|
*/
|
|
|
|
|
|
endDeformation() {
|
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
|
this.isFirstApply = true;
|
|
|
|
|
|
this.dragDistance = 0;
|
|
|
|
|
|
this.dragAngle = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:重置持续按压状态
|
|
|
|
|
|
this.isHolding = false;
|
|
|
|
|
|
this.pressStartTime = 0;
|
|
|
|
|
|
this.pressDuration = 0;
|
|
|
|
|
|
this.accumulatedRotation = 0;
|
|
|
|
|
|
this.accumulatedScale = 0;
|
|
|
|
|
|
this.lastApplyTime = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 停止持续效果定时器
|
|
|
|
|
|
this.stopContinuousEffect();
|
|
|
|
|
|
|
|
|
|
|
|
console.log("结束液化操作");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:启动持续效果
|
|
|
|
|
|
startContinuousEffect() {
|
|
|
|
|
|
this.stopContinuousEffect(); // 先停止已有的定时器
|
|
|
|
|
|
|
|
|
|
|
|
this.continuousTimer = setInterval(() => {
|
|
|
|
|
|
if (this.isHolding && this.initialized) {
|
|
|
|
|
|
// 更新持续时间
|
|
|
|
|
|
this.pressDuration = Date.now() - this.pressStartTime;
|
|
|
|
|
|
|
|
|
|
|
|
// 所有模式都支持持续效果
|
|
|
|
|
|
this.applyContinuousDeformation();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, this.continuousApplyInterval);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:停止持续效果
|
|
|
|
|
|
stopContinuousEffect() {
|
|
|
|
|
|
if (this.continuousTimer) {
|
|
|
|
|
|
clearInterval(this.continuousTimer);
|
|
|
|
|
|
this.continuousTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 稳定的旋转衰减函数 - 确保内圈快外圈慢,保持纹理连续性
|
|
|
|
|
|
* @param {number} t 归一化距离 (0-1)
|
|
|
|
|
|
* @returns {number} 衰减因子 (0-1)
|
|
|
|
|
|
*/
|
|
|
|
|
|
_stableRotationFalloff(t) {
|
|
|
|
|
|
if (t >= 1.0) return 0;
|
|
|
|
|
|
if (t <= 0) return 1;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用反向二次函数:内圈(t=0)时值为1,外圈(t=1)时值为0
|
|
|
|
|
|
// 这确保了内圈旋转最快,外圈旋转最慢
|
|
|
|
|
|
const inverseFalloff = 1 - t;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用平滑的二次衰减,确保内圈效果强,外圈效果弱
|
|
|
|
|
|
const quadraticFalloff = inverseFalloff * inverseFalloff;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加轻微的线性分量,确保过渡平滑
|
|
|
|
|
|
const linearFalloff = inverseFalloff;
|
|
|
|
|
|
|
|
|
|
|
|
// 混合二次和线性衰减,70%二次衰减 + 30%线性衰减
|
|
|
|
|
|
return quadraticFalloff * 0.7 + linearFalloff * 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 基于test-liquify-enhanced.html的旋转算法 - 像素级实现
|
|
|
|
|
|
* @param {number} centerX 旋转中心X坐标
|
|
|
|
|
|
* @param {number} centerY 旋转中心Y坐标
|
|
|
|
|
|
* @param {number} radius 影响半径
|
|
|
|
|
|
* @param {number} strength 强度
|
|
|
|
|
|
* @param {boolean} isClockwise 是否顺时针旋转
|
|
|
|
|
|
*/
|
|
|
|
|
|
_applyEnhancedRotationDeformation(
|
|
|
|
|
|
centerX,
|
|
|
|
|
|
centerY,
|
|
|
|
|
|
radius,
|
|
|
|
|
|
strength,
|
|
|
|
|
|
isClockwise
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (!this.currentImageData) return;
|
|
|
|
|
|
|
|
|
|
|
|
const data = this.currentImageData.data;
|
|
|
|
|
|
const width = this.currentImageData.width;
|
|
|
|
|
|
const height = this.currentImageData.height;
|
|
|
|
|
|
const tempData = new Uint8ClampedArray(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算旋转角度 - 基于test-liquify-enhanced.html的算法
|
|
|
|
|
|
const { pressure, power } = this.params;
|
|
|
|
|
|
const timeFactor = Math.min(this.pressDuration / 1000, 5.0);
|
|
|
|
|
|
const baseRotationSpeed = 0.02; // 使用与测试文件相同的速度
|
|
|
|
|
|
const rotationAngle =
|
|
|
|
|
|
(isClockwise ? 1 : -1) *
|
|
|
|
|
|
baseRotationSpeed *
|
|
|
|
|
|
pressure *
|
|
|
|
|
|
power *
|
|
|
|
|
|
(1.0 + timeFactor * 0.5);
|
|
|
|
|
|
|
|
|
|
|
|
// 累积旋转角度 - 关键:这确保了持续旋转效果
|
|
|
|
|
|
this.accumulatedRotation += rotationAngle;
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历影响区域内的每个像素
|
|
|
|
|
|
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 < processRadius && distance > 0.1) {
|
|
|
|
|
|
// 距离衰减:内圈快,外圈慢 - 与测试文件算法一致
|
|
|
|
|
|
const normalizedDistance = distance / processRadius;
|
|
|
|
|
|
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(
|
|
|
|
|
|
tempData,
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
sourceX,
|
|
|
|
|
|
sourceY
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (color) {
|
|
|
|
|
|
const targetIdx = (y * width + x) * 4;
|
|
|
|
|
|
data[targetIdx] = color[0];
|
|
|
|
|
|
data[targetIdx + 1] = color[1];
|
|
|
|
|
|
data[targetIdx + 2] = color[2];
|
|
|
|
|
|
data[targetIdx + 3] = color[3];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 基于test-liquify-enhanced.html的捏合/展开算法
|
|
|
|
|
|
* @param {number} centerX 中心X坐标
|
|
|
|
|
|
* @param {number} centerY 中心Y坐标
|
|
|
|
|
|
* @param {number} radius 影响半径
|
|
|
|
|
|
* @param {number} strength 强度
|
|
|
|
|
|
* @param {boolean} isPinch 是否为捏合模式
|
|
|
|
|
|
*/
|
|
|
|
|
|
_applyEnhancedPinchDeformation(centerX, centerY, radius, strength, isPinch) {
|
|
|
|
|
|
if (!this.currentImageData) return;
|
|
|
|
|
|
|
|
|
|
|
|
const data = this.currentImageData.data;
|
|
|
|
|
|
const width = this.currentImageData.width;
|
|
|
|
|
|
const height = this.currentImageData.height;
|
|
|
|
|
|
const tempData = new Uint8ClampedArray(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算时间相关的缩放因子 - 基于test-liquify-enhanced.html
|
|
|
|
|
|
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
|
|
|
|
|
|
const baseScaleFactor = isPinch ? -0.01 : 0.01;
|
|
|
|
|
|
const scaleFactor = baseScaleFactor * (1.0 + timeFactor * 0.5);
|
|
|
|
|
|
|
|
|
|
|
|
this.accumulatedScale += scaleFactor;
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
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 < processRadius && distance > 0.1) {
|
|
|
|
|
|
const normalizedDistance = distance / processRadius;
|
|
|
|
|
|
const falloff = 1 - normalizedDistance * normalizedDistance;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算缩放后的位置
|
|
|
|
|
|
const scale = 1 + this.accumulatedScale * falloff;
|
|
|
|
|
|
const sourceX = centerX + dx * scale;
|
|
|
|
|
|
const sourceY = centerY + dy * scale;
|
|
|
|
|
|
|
|
|
|
|
|
// 双线性插值采样
|
|
|
|
|
|
const color = this._bilinearSample(
|
|
|
|
|
|
tempData,
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
sourceX,
|
|
|
|
|
|
sourceY
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (color) {
|
|
|
|
|
|
const targetIdx = (y * width + x) * 4;
|
|
|
|
|
|
data[targetIdx] = color[0];
|
|
|
|
|
|
data[targetIdx + 1] = color[1];
|
|
|
|
|
|
data[targetIdx + 2] = color[2];
|
|
|
|
|
|
data[targetIdx + 3] = color[3];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 基于test-liquify-enhanced.html的推拉算法
|
|
|
|
|
|
* @param {number} centerX 中心X坐标
|
|
|
|
|
|
* @param {number} centerY 中心Y坐标
|
|
|
|
|
|
* @param {number} radius 影响半径
|
|
|
|
|
|
* @param {number} strength 强度
|
|
|
|
|
|
*/
|
|
|
|
|
|
_applyEnhancedPushDeformation(centerX, centerY, radius, strength) {
|
|
|
|
|
|
if (!this.currentImageData) return;
|
|
|
|
|
|
|
|
|
|
|
|
const data = this.currentImageData.data;
|
|
|
|
|
|
const width = this.currentImageData.width;
|
|
|
|
|
|
const height = this.currentImageData.height;
|
|
|
|
|
|
const tempData = new Uint8ClampedArray(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算推拉方向
|
|
|
|
|
|
const deltaX = this.currentMouseX - this.initialMouseX;
|
|
|
|
|
|
const deltaY = this.currentMouseY - this.initialMouseY;
|
|
|
|
|
|
const dragLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
if (dragLength === 0) {
|
|
|
|
|
|
// 如果没有拖拽,在持续按压时执行基础的外推效果
|
|
|
|
|
|
if (this.isHolding) {
|
|
|
|
|
|
const timeFactor = Math.min(this.pressDuration / 1000, 2.0);
|
|
|
|
|
|
const pushStrength = strength * timeFactor * 0.3;
|
|
|
|
|
|
|
|
|
|
|
|
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 < processRadius && distance > 0.1) {
|
|
|
|
|
|
const normalizedDistance = distance / processRadius;
|
|
|
|
|
|
const falloff = 1 - normalizedDistance * normalizedDistance;
|
|
|
|
|
|
const factor = falloff * pushStrength;
|
|
|
|
|
|
|
|
|
|
|
|
// 径向外推效果
|
|
|
|
|
|
const pushX = (dx / distance) * factor;
|
|
|
|
|
|
const pushY = (dy / distance) * factor;
|
|
|
|
|
|
|
|
|
|
|
|
const sourceX = x - pushX;
|
|
|
|
|
|
const sourceY = y - pushY;
|
|
|
|
|
|
|
|
|
|
|
|
const color = this._bilinearSample(
|
|
|
|
|
|
tempData,
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
sourceX,
|
|
|
|
|
|
sourceY
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (color) {
|
|
|
|
|
|
const targetIdx = (y * width + x) * 4;
|
|
|
|
|
|
data[targetIdx] = color[0];
|
|
|
|
|
|
data[targetIdx + 1] = color[1];
|
|
|
|
|
|
data[targetIdx + 2] = color[2];
|
|
|
|
|
|
data[targetIdx + 3] = color[3];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 有拖拽时的推拉效果
|
|
|
|
|
|
const dirX = deltaX / dragLength;
|
|
|
|
|
|
const dirY = deltaY / dragLength;
|
|
|
|
|
|
|
|
|
|
|
|
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 < processRadius && distance > 0.1) {
|
|
|
|
|
|
const normalizedDistance = distance / processRadius;
|
|
|
|
|
|
const falloff = 1 - normalizedDistance * normalizedDistance;
|
|
|
|
|
|
const factor = falloff * strength;
|
|
|
|
|
|
|
|
|
|
|
|
const offsetX = dirX * factor * Math.min(dragLength * 0.3, 15);
|
|
|
|
|
|
const offsetY = dirY * factor * Math.min(dragLength * 0.3, 15);
|
|
|
|
|
|
|
|
|
|
|
|
const sourceX = x - offsetX;
|
|
|
|
|
|
const sourceY = y - offsetY;
|
|
|
|
|
|
|
|
|
|
|
|
const color = this._bilinearSample(
|
|
|
|
|
|
tempData,
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
sourceX,
|
|
|
|
|
|
sourceY
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (color) {
|
|
|
|
|
|
const targetIdx = (y * width + x) * 4;
|
|
|
|
|
|
data[targetIdx] = color[0];
|
|
|
|
|
|
data[targetIdx + 1] = color[1];
|
|
|
|
|
|
data[targetIdx + 2] = color[2];
|
|
|
|
|
|
data[targetIdx + 3] = color[3];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 优化的持续变形效果处理 - 使用增强算法
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyContinuousDeformation() {
|
|
|
|
|
|
if (!this.isHolding || !this.initialized || !this.currentImageData) return;
|
|
|
|
|
|
|
|
|
|
|
|
const { size, pressure, power } = this.params;
|
|
|
|
|
|
const mode = this.currentMode;
|
|
|
|
|
|
const radius = size;
|
|
|
|
|
|
const x = this.initialMouseX;
|
|
|
|
|
|
const y = this.initialMouseY;
|
|
|
|
|
|
const strength = pressure * power;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据模式使用相应的增强算法
|
|
|
|
|
|
switch (mode) {
|
|
|
|
|
|
case this.modes.CLOCKWISE:
|
|
|
|
|
|
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case this.modes.COUNTERCLOCKWISE:
|
|
|
|
|
|
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case this.modes.PINCH:
|
|
|
|
|
|
this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case this.modes.EXPAND:
|
|
|
|
|
|
this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case this.modes.PUSH:
|
|
|
|
|
|
this._applyEnhancedPushDeformation(x, y, radius, strength);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 对于其他模式,使用原有的网格算法
|
|
|
|
|
|
if (!this.mesh) return;
|
|
|
|
|
|
|
|
|
|
|
|
const baseStrength = (pressure * power * this.config.maxStrength) / 100;
|
|
|
|
|
|
const timeFactor = Math.min(this.pressDuration / 1000, 4.0);
|
|
|
|
|
|
const finalStrength = baseStrength * (1.0 + timeFactor * 0.5);
|
|
|
|
|
|
|
|
|
|
|
|
this._applyDeformation(
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
radius,
|
|
|
|
|
|
finalStrength,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
this.params.distortion
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (this.config.smoothingIterations > 0) {
|
|
|
|
|
|
this._lightSmoothing();
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
return this._applyMeshToImage();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对于像素算法,直接返回当前图像数据
|
|
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用液化变形 - 主要入口,集成增强算法
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyDeformation(x, y) {
|
|
|
|
|
|
if (!this.initialized || !this.originalImageData) {
|
|
|
|
|
|
console.warn("液化管理器未初始化或缺少必要数据");
|
2025-06-09 10:25:54 +08:00
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 更新鼠标位置
|
|
|
|
|
|
this.currentMouseX = x;
|
|
|
|
|
|
this.currentMouseY = y;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算拖拽参数
|
|
|
|
|
|
const deltaX = this.currentMouseX - this.initialMouseX;
|
|
|
|
|
|
const deltaY = this.currentMouseY - this.initialMouseY;
|
|
|
|
|
|
this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
|
|
|
|
this.dragAngle = Math.atan2(deltaY, deltaX);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前参数
|
|
|
|
|
|
const { size, pressure, power } = this.params;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const mode = this.currentMode;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const radius = size;
|
|
|
|
|
|
const strength = pressure * power;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据模式选择算法
|
|
|
|
|
|
const pixelModes = [
|
|
|
|
|
|
this.modes.CLOCKWISE,
|
|
|
|
|
|
this.modes.COUNTERCLOCKWISE,
|
|
|
|
|
|
this.modes.PINCH,
|
|
|
|
|
|
this.modes.EXPAND,
|
|
|
|
|
|
this.modes.PUSH,
|
|
|
|
|
|
];
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
if (pixelModes.includes(mode)) {
|
|
|
|
|
|
// 使用增强的像素算法
|
|
|
|
|
|
switch (mode) {
|
|
|
|
|
|
case this.modes.CLOCKWISE:
|
|
|
|
|
|
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.COUNTERCLOCKWISE:
|
|
|
|
|
|
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.PINCH:
|
|
|
|
|
|
this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.EXPAND:
|
|
|
|
|
|
this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.PUSH:
|
|
|
|
|
|
this._applyEnhancedPushDeformation(x, y, radius, strength);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 更新最后应用时间
|
|
|
|
|
|
this.lastApplyTime = Date.now();
|
|
|
|
|
|
this.isFirstApply = false;
|
|
|
|
|
|
|
|
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 使用原有的网格算法处理其他模式
|
|
|
|
|
|
if (!this.mesh) {
|
|
|
|
|
|
console.warn("网格未初始化");
|
|
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const finalStrength = (strength * this.config.maxStrength) / 100;
|
|
|
|
|
|
|
|
|
|
|
|
// 应用变形
|
|
|
|
|
|
this._applyDeformation(
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
radius,
|
|
|
|
|
|
finalStrength,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
this.params.distortion
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 平滑处理
|
|
|
|
|
|
if (this.config.smoothingIterations > 0) {
|
|
|
|
|
|
this._smoothMesh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新图像数据
|
|
|
|
|
|
const result = this._applyMeshToImage();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新最后应用时间
|
|
|
|
|
|
this.lastApplyTime = Date.now();
|
|
|
|
|
|
this.isFirstApply = false;
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 双线性插值采样 - 用于像素级算法
|
|
|
|
|
|
* @param {Uint8ClampedArray} data 图像数据
|
|
|
|
|
|
* @param {number} width 图像宽度
|
|
|
|
|
|
* @param {number} height 图像高度
|
|
|
|
|
|
* @param {number} x X坐标
|
|
|
|
|
|
* @param {number} y Y坐标
|
|
|
|
|
|
* @returns {Array|null} RGBA颜色值数组或null
|
|
|
|
|
|
*/
|
|
|
|
|
|
_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]
|
|
|
|
|
|
),
|
|
|
|
|
|
];
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 应用变形到网格 - 原有的网格算法(用于其他模式)
|
|
|
|
|
|
*/
|
2025-06-09 10:25:54 +08:00
|
|
|
|
_applyDeformation(x, y, radius, strength, mode, distortion) {
|
|
|
|
|
|
if (!this.mesh) return;
|
|
|
|
|
|
|
|
|
|
|
|
const points = this.mesh.deformedPoints;
|
|
|
|
|
|
const originalPoints = this.mesh.originalPoints;
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 性能优化:只计算影响范围内的网格点
|
|
|
|
|
|
const affectedPoints = this._getAffectedPoints(x, y, radius);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
for (const pointInfo of affectedPoints) {
|
|
|
|
|
|
const { index: i, point, originalPoint, distance } = pointInfo;
|
|
|
|
|
|
|
|
|
|
|
|
if (distance > 0) {
|
|
|
|
|
|
// 使用优化的衰减函数
|
|
|
|
|
|
const normalizedDistance = distance / radius;
|
|
|
|
|
|
const factor = this._optimizedFalloff(normalizedDistance) * strength;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
switch (mode) {
|
|
|
|
|
|
case this.modes.CRYSTAL: {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 水晶模式
|
|
|
|
|
|
const dx = point.x - x;
|
|
|
|
|
|
const dy = point.y - y;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const crystalAngle = Math.atan2(dy, dx);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const crystalRadius = normalizedDistance;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
const baseDistortion = Math.max(distortion, 0.3);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const timeFactor = Math.min(this.pressDuration / 1000, 2.0);
|
|
|
|
|
|
const timeEnhancedDistortion =
|
|
|
|
|
|
baseDistortion * (1.0 + timeFactor * 0.3);
|
|
|
|
|
|
|
|
|
|
|
|
const wave1 =
|
|
|
|
|
|
Math.sin(crystalAngle * 8 + this.pressDuration * 0.005) * 0.6;
|
|
|
|
|
|
const wave2 =
|
|
|
|
|
|
Math.cos(crystalAngle * 12 + this.pressDuration * 0.003) * 0.4;
|
|
|
|
|
|
const waveAngle =
|
|
|
|
|
|
crystalAngle + (wave1 + wave2) * timeEnhancedDistortion;
|
|
|
|
|
|
|
|
|
|
|
|
const radialMod =
|
|
|
|
|
|
1 +
|
|
|
|
|
|
Math.sin(
|
|
|
|
|
|
crystalRadius * Math.PI * 2 + this.pressDuration * 0.002
|
|
|
|
|
|
) *
|
|
|
|
|
|
0.3;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const modDistance = distance * radialMod;
|
|
|
|
|
|
|
|
|
|
|
|
const crystalX = x + Math.cos(waveAngle) * modDistance;
|
|
|
|
|
|
const crystalY = y + Math.sin(waveAngle) * modDistance;
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const crystalFactor = factor * timeEnhancedDistortion * 0.7;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
point.x += (crystalX - point.x) * crystalFactor;
|
|
|
|
|
|
point.y += (crystalY - point.y) * crystalFactor;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
case this.modes.EDGE: {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 边缘模式
|
|
|
|
|
|
const dx = point.x - x;
|
|
|
|
|
|
const dy = point.y - y;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const edgeAngle = Math.atan2(dy, dx);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const edgeRadius = normalizedDistance;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
const baseEdgeDistortion = Math.max(distortion, 0.5);
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const timeFactor = Math.min(this.pressDuration / 1000, 2.5);
|
|
|
|
|
|
const timeEnhancedDistortion =
|
|
|
|
|
|
baseEdgeDistortion * (1.0 + timeFactor * 0.4);
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
const edgeWave =
|
2025-06-18 11:05:23 +08:00
|
|
|
|
Math.sin(edgeRadius * Math.PI * 4 + this.pressDuration * 0.004) *
|
|
|
|
|
|
Math.cos(edgeAngle * 6 + this.pressDuration * 0.002);
|
|
|
|
|
|
const perpAngle = edgeAngle + Math.PI / 2;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
const edgeFactor = edgeWave * factor * timeEnhancedDistortion * 0.5;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
const edgeOffsetX = Math.cos(perpAngle) * edgeFactor;
|
|
|
|
|
|
const edgeOffsetY = Math.sin(perpAngle) * edgeFactor;
|
|
|
|
|
|
|
|
|
|
|
|
point.x += edgeOffsetX;
|
|
|
|
|
|
point.y += edgeOffsetY;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
case this.modes.RECONSTRUCT: {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 重建模式
|
|
|
|
|
|
const restoreFactor = factor * 0.2;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
point.x += (originalPoint.x - point.x) * restoreFactor;
|
|
|
|
|
|
point.y += (originalPoint.y - point.y) * restoreFactor;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取受影响的网格点(范围优化)
|
|
|
|
|
|
*/
|
|
|
|
|
|
_getAffectedPoints(centerX, centerY, radius) {
|
|
|
|
|
|
const { cols, rows, gridSize } = this.mesh;
|
|
|
|
|
|
const points = this.mesh.deformedPoints;
|
|
|
|
|
|
const originalPoints = this.mesh.originalPoints;
|
|
|
|
|
|
const affectedPoints = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 计算影响范围的网格边界
|
|
|
|
|
|
const minGridX = Math.max(0, Math.floor((centerX - radius) / gridSize));
|
|
|
|
|
|
const maxGridX = Math.min(cols, Math.ceil((centerX + radius) / gridSize));
|
|
|
|
|
|
const minGridY = Math.max(0, Math.floor((centerY - radius) / gridSize));
|
|
|
|
|
|
const maxGridY = Math.min(rows, Math.ceil((centerY + radius) / gridSize));
|
|
|
|
|
|
|
|
|
|
|
|
// 只遍历影响范围内的网格点
|
|
|
|
|
|
for (let gridY = minGridY; gridY <= maxGridY; gridY++) {
|
|
|
|
|
|
for (let gridX = minGridX; gridX <= maxGridX; gridX++) {
|
|
|
|
|
|
const index = gridY * (cols + 1) + gridX;
|
|
|
|
|
|
if (index < points.length) {
|
|
|
|
|
|
const point = points[index];
|
|
|
|
|
|
const originalPoint = originalPoints[index];
|
|
|
|
|
|
const dx = point.x - centerX;
|
|
|
|
|
|
const dy = point.y - centerY;
|
|
|
|
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
|
|
|
|
|
|
|
|
// 只包含在影响半径内的点
|
|
|
|
|
|
if (distance <= radius) {
|
|
|
|
|
|
affectedPoints.push({
|
|
|
|
|
|
index,
|
|
|
|
|
|
point,
|
|
|
|
|
|
originalPoint,
|
|
|
|
|
|
distance,
|
|
|
|
|
|
dx,
|
|
|
|
|
|
dy,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return affectedPoints;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 专门为旋转模式优化的网格平滑
|
|
|
|
|
|
*/
|
|
|
|
|
|
_lightSmoothing() {
|
|
|
|
|
|
const { rows, cols } = this.mesh;
|
|
|
|
|
|
const points = this.mesh.deformedPoints;
|
|
|
|
|
|
const tempPoints = points.map((p) => ({ x: p.x, y: p.y }));
|
|
|
|
|
|
|
|
|
|
|
|
// 只进行一次轻微平滑
|
|
|
|
|
|
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 lightRelaxFactor = this.config.relaxFactor * 0.3;
|
|
|
|
|
|
tempPoints[idx].x += (centerX - points[idx].x) * lightRelaxFactor;
|
|
|
|
|
|
tempPoints[idx].y += (centerY - points[idx].y) * lightRelaxFactor;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < points.length; i++) {
|
|
|
|
|
|
points[i].x = tempPoints[i].x;
|
|
|
|
|
|
points[i].y = tempPoints[i].y;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 使用更优化的衰减函数
|
|
|
|
|
|
* @param {number} t 归一化距离 (0-1)
|
|
|
|
|
|
* @returns {number} 衰减因子 (0-1)
|
|
|
|
|
|
*/
|
|
|
|
|
|
_optimizedFalloff(t) {
|
|
|
|
|
|
if (t >= 1.0) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 对于旋转模式,使用专门的衰减函数
|
|
|
|
|
|
if (
|
|
|
|
|
|
this.currentMode === this.modes.CLOCKWISE ||
|
|
|
|
|
|
this.currentMode === this.modes.COUNTERCLOCKWISE
|
|
|
|
|
|
) {
|
|
|
|
|
|
return this._stableRotationFalloff(t); // 修复函数名
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 其他模式使用原来的衰减函数
|
|
|
|
|
|
const smoothT = 1 - t;
|
|
|
|
|
|
|
|
|
|
|
|
// 多项式衰减 + 指数衰减的组合
|
|
|
|
|
|
const polynomial = smoothT * smoothT * (3 - 2 * smoothT); // 平滑阶梯函数
|
|
|
|
|
|
const exponential = Math.exp(-t * 2); // 指数衰减
|
|
|
|
|
|
|
|
|
|
|
|
// 组合两种衰减方式,在不同区域有不同特性
|
|
|
|
|
|
const weight = Math.cos(t * Math.PI * 0.5); // 权重函数
|
|
|
|
|
|
|
|
|
|
|
|
return polynomial * weight + exponential * (1 - weight);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 对于批量处理,模拟连续的拖拽操作
|
|
|
|
|
|
if (positions.length > 0) {
|
|
|
|
|
|
// 使用第一个位置作为初始点
|
|
|
|
|
|
this.startDeformation(positions[0].x, positions[0].y);
|
|
|
|
|
|
|
|
|
|
|
|
// 逐个应用每个位置的变形
|
|
|
|
|
|
positions.forEach((pos, index) => {
|
|
|
|
|
|
if (index === 0) return; // 跳过第一个,因为已经作为初始点
|
|
|
|
|
|
|
|
|
|
|
|
// 更新当前位置并应用变形
|
|
|
|
|
|
this.currentMouseX = pos.x;
|
|
|
|
|
|
this.currentMouseY = pos.y;
|
|
|
|
|
|
|
|
|
|
|
|
// 重新计算拖拽参数
|
|
|
|
|
|
const deltaX = this.currentMouseX - this.initialMouseX;
|
|
|
|
|
|
const deltaY = this.currentMouseY - this.initialMouseY;
|
|
|
|
|
|
this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
|
|
|
|
this.dragAngle = Math.atan2(deltaY, deltaX);
|
|
|
|
|
|
|
|
|
|
|
|
const { size, pressure, distortion, power } = this.params;
|
|
|
|
|
|
const mode = this.currentMode;
|
|
|
|
|
|
const radius = size * 0.8;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据推拉模式和拖拽距离动态调整强度
|
|
|
|
|
|
let strength;
|
|
|
|
|
|
if (mode === this.modes.PUSH) {
|
|
|
|
|
|
const baseStrength =
|
|
|
|
|
|
(pressure * power * this.config.maxStrength) / 100;
|
|
|
|
|
|
const distanceFactor = Math.min(this.dragDistance / radius, 2.0);
|
|
|
|
|
|
strength = baseStrength * distanceFactor * 0.3; // 批量处理时降低强度
|
|
|
|
|
|
} else {
|
|
|
|
|
|
strength = (pressure * power * this.config.maxStrength) / 100;
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
this._applyDeformation(
|
|
|
|
|
|
pos.x,
|
|
|
|
|
|
pos.y,
|
|
|
|
|
|
radius,
|
|
|
|
|
|
strength,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
distortion
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 结束拖拽操作
|
|
|
|
|
|
this.endDeformation();
|
|
|
|
|
|
}
|
2025-06-09 10:25:54 +08:00
|
|
|
|
|
|
|
|
|
|
if (this.config.smoothingIterations > 0) {
|
|
|
|
|
|
this._smoothMesh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this._applyMeshToImage();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 改进的网格映射算法 - 防止空白区域
|
|
|
|
|
|
*/
|
2025-06-09 10:25:54 +08:00
|
|
|
|
_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;
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 限制偏移量,防止过度扭曲
|
|
|
|
|
|
const maxOffset = gridSize * 0.5;
|
|
|
|
|
|
const limitedOffsetX = Math.max(-maxOffset, Math.min(maxOffset, offsetX));
|
|
|
|
|
|
const limitedOffsetY = Math.max(-maxOffset, Math.min(maxOffset, offsetY));
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
return {
|
2025-06-18 11:05:23 +08:00
|
|
|
|
x: Math.max(0, Math.min(this.mesh.width - 1, x - limitedOffsetX)),
|
|
|
|
|
|
y: Math.max(0, Math.min(this.mesh.height - 1, y - limitedOffsetY)),
|
2025-06-09 10:25:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_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
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 重置拖拽状态
|
|
|
|
|
|
this.initialMouseX = 0;
|
|
|
|
|
|
this.initialMouseY = 0;
|
|
|
|
|
|
this.currentMouseX = 0;
|
|
|
|
|
|
this.currentMouseY = 0;
|
|
|
|
|
|
this.lastMouseX = 0;
|
|
|
|
|
|
this.lastMouseY = 0;
|
|
|
|
|
|
this.mouseMovementX = 0;
|
|
|
|
|
|
this.mouseMovementY = 0;
|
|
|
|
|
|
this.isFirstApply = true;
|
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
|
this.dragDistance = 0;
|
|
|
|
|
|
this.dragAngle = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:重置持续按压状态
|
|
|
|
|
|
this.isHolding = false;
|
|
|
|
|
|
this.pressStartTime = 0;
|
|
|
|
|
|
this.pressDuration = 0;
|
|
|
|
|
|
this.accumulatedRotation = 0;
|
|
|
|
|
|
this.accumulatedScale = 0;
|
|
|
|
|
|
this.lastApplyTime = 0;
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
this.deformHistory = [];
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
|
// 新增:获取持续按压状态信息
|
|
|
|
|
|
getHoldingInfo() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
isHolding: this.isHolding,
|
|
|
|
|
|
pressDuration: this.pressDuration,
|
|
|
|
|
|
accumulatedRotation: this.accumulatedRotation,
|
|
|
|
|
|
accumulatedScale: this.accumulatedScale,
|
|
|
|
|
|
pressStartTime: this.pressStartTime,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 应用液化变形 - 主要的公共接口方法
|
|
|
|
|
|
* @param {Number} x X坐标
|
|
|
|
|
|
* @param {Number} y Y坐标
|
|
|
|
|
|
* @returns {ImageData} 变形后的图像数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
applyDeformation(x, y) {
|
|
|
|
|
|
if (!this.initialized || !this.mesh || !this.originalImageData) {
|
|
|
|
|
|
console.warn("液化管理器未初始化或缺少必要数据");
|
|
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新鼠标位置
|
|
|
|
|
|
this.currentMouseX = x;
|
|
|
|
|
|
this.currentMouseY = y;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算拖拽参数
|
|
|
|
|
|
const deltaX = this.currentMouseX - this.initialMouseX;
|
|
|
|
|
|
const deltaY = this.currentMouseY - this.initialMouseY;
|
|
|
|
|
|
this.dragDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
|
|
|
|
this.dragAngle = Math.atan2(deltaY, deltaX);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前参数
|
|
|
|
|
|
const { size, pressure, power } = this.params;
|
|
|
|
|
|
const mode = this.currentMode;
|
|
|
|
|
|
const radius = size;
|
|
|
|
|
|
const strength = pressure * power;
|
|
|
|
|
|
|
|
|
|
|
|
// 根据模式选择算法
|
|
|
|
|
|
const pixelModes = [
|
|
|
|
|
|
this.modes.CLOCKWISE,
|
|
|
|
|
|
this.modes.COUNTERCLOCKWISE,
|
|
|
|
|
|
this.modes.PINCH,
|
|
|
|
|
|
this.modes.EXPAND,
|
|
|
|
|
|
this.modes.PUSH,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (pixelModes.includes(mode)) {
|
|
|
|
|
|
// 使用增强的像素算法
|
|
|
|
|
|
switch (mode) {
|
|
|
|
|
|
case this.modes.CLOCKWISE:
|
|
|
|
|
|
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.COUNTERCLOCKWISE:
|
|
|
|
|
|
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.PINCH:
|
|
|
|
|
|
this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.EXPAND:
|
|
|
|
|
|
this._applyEnhancedPinchDeformation(x, y, radius, strength, false);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case this.modes.PUSH:
|
|
|
|
|
|
this._applyEnhancedPushDeformation(x, y, radius, strength);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新最后应用时间
|
|
|
|
|
|
this.lastApplyTime = Date.now();
|
|
|
|
|
|
this.isFirstApply = false;
|
|
|
|
|
|
|
|
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 使用原有的网格算法处理其他模式
|
|
|
|
|
|
if (!this.mesh) {
|
|
|
|
|
|
console.warn("网格未初始化");
|
|
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const finalStrength = (strength * this.config.maxStrength) / 100;
|
|
|
|
|
|
|
|
|
|
|
|
// 应用变形
|
|
|
|
|
|
this._applyDeformation(
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
radius,
|
|
|
|
|
|
finalStrength,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
this.params.distortion
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 平滑处理
|
|
|
|
|
|
if (this.config.smoothingIterations > 0) {
|
|
|
|
|
|
this._smoothMesh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新图像数据
|
|
|
|
|
|
const result = this._applyMeshToImage();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新最后应用时间
|
|
|
|
|
|
this.lastApplyTime = Date.now();
|
|
|
|
|
|
this.isFirstApply = false;
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
getCurrentImageData() {
|
|
|
|
|
|
return this.currentImageData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
|
|
this.originalImageData = null;
|
|
|
|
|
|
this.currentImageData = null;
|
|
|
|
|
|
this.mesh = null;
|
|
|
|
|
|
this.deformHistory = [];
|
|
|
|
|
|
this.initialized = false;
|
2025-06-18 11:05:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 清理拖拽状态
|
|
|
|
|
|
this.initialMouseX = 0;
|
|
|
|
|
|
this.initialMouseY = 0;
|
|
|
|
|
|
this.currentMouseX = 0;
|
|
|
|
|
|
this.currentMouseY = 0;
|
|
|
|
|
|
this.lastMouseX = 0;
|
|
|
|
|
|
this.lastMouseY = 0;
|
|
|
|
|
|
this.mouseMovementX = 0;
|
|
|
|
|
|
this.mouseMovementY = 0;
|
|
|
|
|
|
this.isFirstApply = true;
|
|
|
|
|
|
this.isDragging = false;
|
|
|
|
|
|
this.dragDistance = 0;
|
|
|
|
|
|
this.dragAngle = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:清理持续按压状态
|
|
|
|
|
|
this.isHolding = false;
|
|
|
|
|
|
this.pressStartTime = 0;
|
|
|
|
|
|
this.pressDuration = 0;
|
|
|
|
|
|
this.accumulatedRotation = 0;
|
|
|
|
|
|
this.accumulatedScale = 0;
|
|
|
|
|
|
this.lastApplyTime = 0;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|