接入画布
This commit is contained in:
@@ -0,0 +1,702 @@
|
||||
/**
|
||||
* 增强版液化管理器
|
||||
* 整合WebGL和CPU实现,智能选择最佳渲染方式
|
||||
*/
|
||||
import { LiquifyWebGLManager } from "./LiquifyWebGLManager";
|
||||
import { LiquifyCPUManager } from "./LiquifyCPUManager";
|
||||
|
||||
export class EnhancedLiquifyManager {
|
||||
/**
|
||||
* 创建增强版液化管理器
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.config = {
|
||||
// 性能阈值:图像超过此尺寸会尝试使用WebGL
|
||||
webglSizeThreshold: options.webglSizeThreshold || 1000 * 1000, // 默认100万像素
|
||||
// 是否强制使用CPU模式
|
||||
forceCPU: options.forceCPU || false,
|
||||
// 是否强制使用WebGL模式
|
||||
forceWebGL: options.forceWebGL || false,
|
||||
// 网格大小
|
||||
gridSize: options.gridSize || 15,
|
||||
// 最大变形强度
|
||||
maxStrength: options.maxStrength || 100,
|
||||
// 平滑迭代次数
|
||||
smoothingIterations: options.smoothingIterations || 2,
|
||||
// 网格弹性因子
|
||||
relaxFactor: options.relaxFactor || 0.25,
|
||||
// WebGL网格精度
|
||||
meshResolution: options.meshResolution || 64,
|
||||
};
|
||||
|
||||
// 性能监控
|
||||
this.performance = {
|
||||
lastOperationTime: 0,
|
||||
renderTimes: [], // 最近的渲染时间记录
|
||||
isPerformanceIssue: false, // 是否存在性能问题
|
||||
operationCount: 0, // 操作计数
|
||||
};
|
||||
|
||||
// 初始化标志
|
||||
this.initialized = false;
|
||||
|
||||
// 当前参数
|
||||
this.params = {
|
||||
size: 50, // 工具尺寸
|
||||
pressure: 0.5, // 压力大小 (0-1)
|
||||
distortion: 0, // 失真程度 (0-1)
|
||||
power: 0.5, // 动力/强度 (0-1)
|
||||
};
|
||||
|
||||
// 液化工具模式
|
||||
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.targetObject = null;
|
||||
this.targetLayerId = null;
|
||||
|
||||
// 创建渲染器实例
|
||||
this.webglRenderer = null;
|
||||
this.cpuRenderer = null;
|
||||
|
||||
// 当前激活的渲染器
|
||||
this.activeRenderer = null;
|
||||
this.renderMode = "unknown"; // 'webgl', 'cpu', 'unknown'
|
||||
|
||||
// 画布和管理器引用
|
||||
this.canvas = options.canvas || null;
|
||||
this.layerManager = options.layerManager || null;
|
||||
|
||||
// 渲染器状态
|
||||
this.isWebGLAvailable = LiquifyWebGLManager.isSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化液化管理器
|
||||
* @param {Object} options 配置选项
|
||||
* @returns {Boolean} 是否初始化成功
|
||||
*/
|
||||
initialize(options = {}) {
|
||||
if (options.canvas) this.canvas = options.canvas;
|
||||
if (options.layerManager) this.layerManager = options.layerManager;
|
||||
|
||||
if (!this.canvas || !this.layerManager) {
|
||||
console.error("液化管理器初始化失败:缺少canvas或layerManager");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 记录初始化时间,用于性能监控
|
||||
this.performance.lastInitTime = Date.now();
|
||||
|
||||
// 创建CPU渲染器 (始终创建作为备选)
|
||||
this.cpuRenderer = new LiquifyCPUManager({
|
||||
gridSize: this.config.gridSize,
|
||||
maxStrength: this.config.maxStrength,
|
||||
smoothingIterations: this.config.smoothingIterations,
|
||||
relaxFactor: this.config.relaxFactor,
|
||||
});
|
||||
|
||||
// 检查是否应创建WebGL渲染器
|
||||
if (this.isWebGLAvailable && !this.config.forceCPU) {
|
||||
this.webglRenderer = new LiquifyWebGLManager({
|
||||
gridSize: this.config.gridSize,
|
||||
maxStrength: this.config.maxStrength,
|
||||
meshResolution: this.config.meshResolution,
|
||||
});
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为液化操作准备图像
|
||||
* @param {Object|String} target 目标对象或图层ID
|
||||
* @returns {Promise<Object>} 准备结果
|
||||
*/
|
||||
async prepareForLiquify(target) {
|
||||
if (!this.initialized) {
|
||||
throw new Error("液化管理器未初始化");
|
||||
}
|
||||
|
||||
let targetObject, targetLayerId;
|
||||
|
||||
// 处理传入的是图层ID的情况
|
||||
if (typeof target === "string") {
|
||||
targetLayerId = target;
|
||||
const layer = this.layerManager.getLayerById(targetLayerId);
|
||||
|
||||
// 检查图层是否存在和是否有对象
|
||||
let hasObjects = false;
|
||||
if (layer) {
|
||||
if (layer.type === "background" && layer.fabricObject) {
|
||||
hasObjects = true;
|
||||
targetObject = layer.fabricObject;
|
||||
} else if (layer.fabricObjects && layer.fabricObjects.length > 0) {
|
||||
hasObjects = true;
|
||||
targetObject = layer.fabricObjects[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasObjects) {
|
||||
throw new Error("目标图层为空或不存在");
|
||||
}
|
||||
} else if (typeof target === "object") {
|
||||
// 传入的是对象
|
||||
targetObject = target;
|
||||
const layer = this.layerManager.findLayerByObject(targetObject);
|
||||
if (layer) {
|
||||
targetLayerId = layer.id;
|
||||
} else {
|
||||
throw new Error("无法找到目标对象所属图层");
|
||||
}
|
||||
} else {
|
||||
throw new Error("无效的目标参数");
|
||||
}
|
||||
|
||||
// 检查是否为图像对象
|
||||
if (!targetObject || targetObject.type !== "image") {
|
||||
throw new Error("目标对象不是图像,无法进行液化操作");
|
||||
}
|
||||
|
||||
// 保存目标对象引用
|
||||
this.targetObject = targetObject;
|
||||
this.targetLayerId = targetLayerId;
|
||||
|
||||
// 获取图像数据
|
||||
const imageData = await this._getImageData(targetObject);
|
||||
if (!imageData) {
|
||||
throw new Error("无法获取图像数据");
|
||||
}
|
||||
|
||||
// 保存原始图像数据
|
||||
this.originalImageData = imageData;
|
||||
this.currentImageData = this._cloneImageData(imageData);
|
||||
|
||||
// 检查图像大小,选择适合的渲染器
|
||||
await this._selectRenderer(imageData);
|
||||
|
||||
// 预热选定的渲染器
|
||||
await this._warmupRenderer(imageData);
|
||||
|
||||
return {
|
||||
targetObject: this.targetObject,
|
||||
targetLayerId: this.targetLayerId,
|
||||
imageData: this.currentImageData,
|
||||
originalImageData: this.originalImageData,
|
||||
renderMode: this.renderMode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图像大小和设备性能选择渲染器
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
async _selectRenderer(imageData) {
|
||||
// 计算图像大小
|
||||
const pixelCount = imageData.width * imageData.height;
|
||||
|
||||
console.log(
|
||||
`液化选择渲染器: 图像大小=${pixelCount}像素, WebGL可用=${this.isWebGLAvailable}`
|
||||
);
|
||||
|
||||
// 默认使用CPU渲染器
|
||||
this.activeRenderer = this.cpuRenderer;
|
||||
this.renderMode = "cpu";
|
||||
|
||||
// 如果配置强制使用WebGL
|
||||
if (this.config.forceWebGL && this.isWebGLAvailable && this.webglRenderer) {
|
||||
console.log("液化功能: 强制使用WebGL渲染模式");
|
||||
this.activeRenderer = this.webglRenderer;
|
||||
this.renderMode = "webgl";
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果配置强制使用CPU
|
||||
if (this.config.forceCPU) {
|
||||
console.log("液化功能: 强制使用CPU渲染模式");
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据图像大小和WebGL可用性决定
|
||||
if (
|
||||
pixelCount > this.config.webglSizeThreshold / 2 && // 降低阈值,让更多尺寸的图像使用WebGL
|
||||
this.isWebGLAvailable &&
|
||||
this.webglRenderer
|
||||
) {
|
||||
// 切换到WebGL渲染器
|
||||
console.log("液化功能: 自动选择WebGL渲染模式(基于图像尺寸)");
|
||||
this.activeRenderer = this.webglRenderer;
|
||||
this.renderMode = "webgl";
|
||||
} else {
|
||||
console.log(
|
||||
`液化功能: 使用CPU渲染模式${
|
||||
!this.isWebGLAvailable ? " (WebGL不可用)" : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热渲染器
|
||||
* @param {ImageData} imageData 图像数据
|
||||
* @private
|
||||
*/
|
||||
async _warmupRenderer(imageData) {
|
||||
// 创建图像元素
|
||||
const img = document.createElement("img");
|
||||
|
||||
// 将ImageData转换为URL
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
// 使用Promise等待图像加载
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
img.src = canvas.toDataURL();
|
||||
});
|
||||
|
||||
// 初始化当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
if (this.renderMode === "webgl") {
|
||||
this.activeRenderer.initialize(img);
|
||||
} else {
|
||||
this.activeRenderer.initialize(imageData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化模式
|
||||
* @param {String} mode 模式名称
|
||||
*/
|
||||
setMode(mode) {
|
||||
if (Object.values(this.modes).includes(mode)) {
|
||||
this.currentMode = mode;
|
||||
|
||||
// 同步更新当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
this.activeRenderer.setMode(mode);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化参数
|
||||
* @param {String} param 参数名称
|
||||
* @param {Number} value 参数值
|
||||
*/
|
||||
setParam(param, value) {
|
||||
if (param in this.params) {
|
||||
this.params[param] = value;
|
||||
|
||||
// 同步更新当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
this.activeRenderer.setParam(param, value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
*/
|
||||
getParams() {
|
||||
return { ...this.params };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置参数为默认值
|
||||
*/
|
||||
resetParams() {
|
||||
this.params = {
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0,
|
||||
power: 0.5,
|
||||
};
|
||||
|
||||
// 同步更新当前渲染器
|
||||
if (this.activeRenderer) {
|
||||
this.activeRenderer.resetParams();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用液化变形
|
||||
* @param {Object} target 目标对象
|
||||
* @param {String} mode 液化模式
|
||||
* @param {Object} params 液化参数
|
||||
* @param {Number} x 操作中心点X坐标 (图像像素坐标)
|
||||
* @param {Number} y 操作中心点Y坐标 (图像像素坐标)
|
||||
* @returns {Promise<ImageData>} 处理后的图像数据
|
||||
*/
|
||||
async applyLiquify(target, mode, params, x, y) {
|
||||
// 性能追踪开始
|
||||
const startTime = performance.now();
|
||||
|
||||
// 如果首次调用,先准备环境
|
||||
if (!this.targetObject || this.targetObject !== target) {
|
||||
await this.prepareForLiquify(target);
|
||||
}
|
||||
|
||||
// 更新模式和参数
|
||||
if (mode) this.setMode(mode);
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
this.setParam(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证坐标是否在图像范围内
|
||||
if (!this.originalImageData) {
|
||||
console.error("缺少原始图像数据");
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageWidth = this.originalImageData.width;
|
||||
const imageHeight = this.originalImageData.height;
|
||||
|
||||
// 坐标边界检查
|
||||
if (x < 0 || x >= imageWidth || y < 0 || y >= imageHeight) {
|
||||
console.warn(
|
||||
`液化坐标超出图像范围: (${x}, ${y}), 图像尺寸: ${imageWidth}x${imageHeight}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`应用液化变形: 模式=${mode}, 图像坐标=(${x}, ${y}), 图像尺寸=${imageWidth}x${imageHeight}`
|
||||
);
|
||||
|
||||
// 检查并应用变形
|
||||
if (this.activeRenderer && typeof x === "number" && typeof y === "number") {
|
||||
// 应用变形
|
||||
let result;
|
||||
|
||||
if (this.renderMode === "webgl") {
|
||||
// WebGL渲染器:传入图像像素坐标
|
||||
result = this.activeRenderer.applyDeformation(x, y);
|
||||
} else {
|
||||
// CPU渲染器:传入图像像素坐标
|
||||
result = this.activeRenderer.applyDeformation(x, y);
|
||||
}
|
||||
|
||||
// 更新当前图像数据
|
||||
if (result) {
|
||||
this.currentImageData = result;
|
||||
}
|
||||
|
||||
// 性能追踪结束
|
||||
const endTime = performance.now();
|
||||
this._trackPerformance(endTime - startTime);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
console.error("无法应用液化变形:渲染器未初始化或坐标无效");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追踪性能数据
|
||||
* @param {Number} time 操作耗时(毫秒)
|
||||
* @private
|
||||
*/
|
||||
_trackPerformance(time) {
|
||||
this.performance.lastOperationTime = time;
|
||||
this.performance.operationCount++;
|
||||
|
||||
// 维护最近10次操作的耗时记录
|
||||
this.performance.renderTimes.push(time);
|
||||
if (this.performance.renderTimes.length > 10) {
|
||||
this.performance.renderTimes.shift();
|
||||
}
|
||||
|
||||
// 计算平均耗时
|
||||
const avgTime =
|
||||
this.performance.renderTimes.reduce((sum, t) => sum + t, 0) /
|
||||
this.performance.renderTimes.length;
|
||||
|
||||
// 检测性能问题
|
||||
this.performance.isPerformanceIssue = avgTime > 100; // 如果平均耗时超过100毫秒
|
||||
|
||||
// 输出性能信息(调试用)
|
||||
if (this.performance.operationCount % 10 === 0) {
|
||||
console.log(
|
||||
`液化性能数据: 模式=${this.renderMode}, 平均耗时=${avgTime.toFixed(
|
||||
2
|
||||
)}ms, 图像尺寸=${this.originalImageData?.width}x${
|
||||
this.originalImageData?.height
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// 如果使用WebGL但性能差,可以考虑切换到优化的CPU实现
|
||||
if (
|
||||
this.renderMode === "webgl" &&
|
||||
this.performance.isPerformanceIssue &&
|
||||
this.performance.operationCount > 5
|
||||
) {
|
||||
console.warn("WebGL液化性能不佳,考虑切换到CPU模式");
|
||||
// 注意:这里不自动切换,因为可能会导致中途渲染结果不一致
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置液化操作
|
||||
* @returns {ImageData} 重置后的图像数据
|
||||
*/
|
||||
reset() {
|
||||
if (!this.activeRenderer) return null;
|
||||
|
||||
// 使用当前渲染器重置
|
||||
const result = this.activeRenderer.reset();
|
||||
|
||||
// 更新当前图像数据
|
||||
if (result) {
|
||||
this.currentImageData = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查图层是否可以液化
|
||||
* @param {String} layerId 图层ID
|
||||
* @returns {Object} 检查结果
|
||||
*/
|
||||
checkLayerForLiquify(layerId) {
|
||||
if (!this.layerManager) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "图层管理器未初始化",
|
||||
needsRasterization: false,
|
||||
isImage: false,
|
||||
isEmpty: true,
|
||||
isGroup: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取图层
|
||||
const layer = this.layerManager.getLayerById(layerId);
|
||||
if (!layer) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "图层不存在",
|
||||
needsRasterization: false,
|
||||
isImage: false,
|
||||
isEmpty: true,
|
||||
isGroup: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 检查图层是否为空
|
||||
let objectsToCheck = [];
|
||||
if (layer.isBackground || layer.type === "background") {
|
||||
// 背景图层使用 fabricObject (单数)
|
||||
if (layer.fabricObject) {
|
||||
objectsToCheck = [layer.fabricObject];
|
||||
}
|
||||
} else {
|
||||
// 普通图层使用 fabricObjects (复数)
|
||||
objectsToCheck = layer.fabricObjects || [];
|
||||
}
|
||||
|
||||
if (objectsToCheck.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: "图层为空,无法进行液化操作",
|
||||
needsRasterization: false,
|
||||
isImage: false,
|
||||
isEmpty: true,
|
||||
isGroup: false,
|
||||
};
|
||||
}
|
||||
|
||||
// 检查是否为单一图像
|
||||
const singleObject = objectsToCheck.length === 1;
|
||||
const isImage =
|
||||
singleObject &&
|
||||
(objectsToCheck[0].type === "image" ||
|
||||
objectsToCheck[0].type === "rasterized-layer");
|
||||
|
||||
// 检查是否为组
|
||||
const isGroup = objectsToCheck.some((obj) => obj.type === "group");
|
||||
|
||||
// 如果不是单一图像,需要栅格化
|
||||
const needsRasterization = !isImage || isGroup;
|
||||
|
||||
return {
|
||||
valid: isImage && !isGroup,
|
||||
message: isImage ? "图层可以进行液化操作" : "需要先将图层栅格化",
|
||||
needsRasterization: needsRasterization,
|
||||
isImage: isImage,
|
||||
isEmpty: false,
|
||||
isGroup: isGroup,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图像数据
|
||||
* @param {Object} fabricObject Fabric图像对象
|
||||
* @returns {Promise<ImageData>} 图像数据
|
||||
* @private
|
||||
*/
|
||||
async _getImageData(fabricObject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 创建临时canvas
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = fabricObject.width * fabricObject.scaleX;
|
||||
tempCanvas.height = fabricObject.height * fabricObject.scaleY;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
// 如果对象有图像元素
|
||||
if (fabricObject._element) {
|
||||
tempCtx.drawImage(
|
||||
fabricObject._element,
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
} else if (fabricObject.getSrc) {
|
||||
// 通过URL创建图像
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
tempCtx.drawImage(img, 0, 0, tempCanvas.width, tempCanvas.height);
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
resolve(imageData);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = fabricObject.getSrc();
|
||||
return;
|
||||
} else {
|
||||
reject(new Error("无法获取图像数据"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取图像数据
|
||||
const imageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
);
|
||||
resolve(imageData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆图像数据
|
||||
* @param {ImageData} imageData 原始图像数据
|
||||
* @returns {ImageData} 克隆的图像数据
|
||||
* @private
|
||||
*/
|
||||
_cloneImageData(imageData) {
|
||||
if (!imageData) return null;
|
||||
|
||||
// 使用新的浏览器API直接复制
|
||||
if (typeof ImageData.prototype.constructor === "function") {
|
||||
try {
|
||||
return new ImageData(
|
||||
new Uint8ClampedArray(imageData.data),
|
||||
imageData.width,
|
||||
imageData.height
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("使用备选方法克隆ImageData");
|
||||
}
|
||||
}
|
||||
|
||||
// 备选方法
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
return ctx.getImageData(0, 0, imageData.width, imageData.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
dispose() {
|
||||
// 释放渲染器资源
|
||||
if (this.webglRenderer) {
|
||||
this.webglRenderer.dispose();
|
||||
this.webglRenderer = null;
|
||||
}
|
||||
|
||||
if (this.cpuRenderer) {
|
||||
this.cpuRenderer.dispose();
|
||||
this.cpuRenderer = null;
|
||||
}
|
||||
|
||||
// 清除引用
|
||||
this.activeRenderer = null;
|
||||
this.canvas = null;
|
||||
this.layerManager = null;
|
||||
this.targetObject = null;
|
||||
this.originalImageData = null;
|
||||
this.currentImageData = null;
|
||||
this.initialized = false;
|
||||
this.renderMode = "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态信息
|
||||
* @returns {Object} 状态信息
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
initialized: this.initialized,
|
||||
renderMode: this.renderMode,
|
||||
isWebGLAvailable: this.isWebGLAvailable,
|
||||
currentMode: this.currentMode,
|
||||
params: { ...this.params },
|
||||
performance: { ...this.performance },
|
||||
imageSize: this.originalImageData
|
||||
? `${this.originalImageData.width}x${this.originalImageData.height}`
|
||||
: "N/A",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 液化管理器
|
||||
* 负责管理液化操作的核心算法和变形处理
|
||||
*
|
||||
* 此版本使用增强的液化算法,支持GPU加速和优化的CPU处理
|
||||
*/
|
||||
import { EnhancedLiquifyManager } from "./EnhancedLiquifyManager";
|
||||
|
||||
export class LiquifyManager {
|
||||
/**
|
||||
* 创建液化管理器
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
// 将核心属性暴露给外部,保持API兼容性
|
||||
this.canvas = options.canvas || null;
|
||||
this.layerManager = options.layerManager || null;
|
||||
|
||||
// 配置参数
|
||||
this.config = {
|
||||
gridSize: options.gridSize || 20,
|
||||
maxStrength: options.maxStrength || 100,
|
||||
defaultParams: {
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0,
|
||||
power: 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
// 创建增强版液化管理器实例
|
||||
this.enhancedManager = new EnhancedLiquifyManager({
|
||||
// 配置选项
|
||||
gridSize: options.gridSize || 15,
|
||||
maxStrength: options.maxStrength || 100,
|
||||
smoothingIterations: options.smoothingIterations || 2,
|
||||
relaxFactor: options.relaxFactor || 0.25,
|
||||
meshResolution: options.meshResolution || 64,
|
||||
// 根据环境选择合适的渲染模式
|
||||
forceCPU: true, // 默认不强制使用CPU
|
||||
forceWebGL: false, // 优先使用WebGL模式
|
||||
webglSizeThreshold: options.webglSizeThreshold || 500 * 500, // 降低阈值以更倾向使用WebGL
|
||||
layerManager: options.layerManager || null,
|
||||
canvas: options.canvas || null,
|
||||
});
|
||||
|
||||
// 初始化液化管理器
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化液化管理器
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
initialize(options = {}) {
|
||||
// 更新基础属性
|
||||
if (options.canvas) this.canvas = options.canvas;
|
||||
if (options.layerManager) this.layerManager = options.layerManager;
|
||||
|
||||
// 初始化增强液化管理器
|
||||
return this.enhancedManager.initialize({
|
||||
canvas: this.canvas,
|
||||
layerManager: this.layerManager,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为液化操作准备图像
|
||||
* @param {Object|String} target 目标对象或图层ID
|
||||
* @returns {Promise<Object>} 准备结果
|
||||
*/
|
||||
async prepareForLiquify(target) {
|
||||
return this.enhancedManager.prepareForLiquify(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化模式
|
||||
* @param {String} mode 液化模式
|
||||
*/
|
||||
setMode(mode) {
|
||||
return this.enhancedManager.setMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化参数
|
||||
* @param {String} param 参数名称
|
||||
* @param {Number} value 参数值
|
||||
*/
|
||||
setParam(param, value) {
|
||||
return this.enhancedManager.setParam(param, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数对象
|
||||
*/
|
||||
getParams() {
|
||||
return this.enhancedManager.getParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置参数为默认值
|
||||
*/
|
||||
resetParams() {
|
||||
return this.enhancedManager.resetParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用液化效果
|
||||
* @param {fabric.Object} targetObject 目标对象
|
||||
* @param {String} mode 液化模式
|
||||
* @param {Object} params 参数
|
||||
* @param {Number} x X坐标
|
||||
* @param {Number} y Y坐标
|
||||
* @returns {ImageData} 处理后的图像数据
|
||||
*/
|
||||
async applyLiquify(targetObject, mode, params, x, y) {
|
||||
if (!this.enhancedManager || !targetObject) {
|
||||
console.error("液化管理器未正确初始化");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保设置正确的模式和参数
|
||||
if (mode) {
|
||||
this.enhancedManager.setMode(mode);
|
||||
}
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
this.enhancedManager.setParam(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 应用液化变形
|
||||
console.log(`应用液化变形, 模式=${mode}, 坐标=(${x}, ${y}), 参数=`, params);
|
||||
try {
|
||||
// 直接调用EnhancedLiquifyManager的applyLiquify方法
|
||||
const resultData = await this.enhancedManager.applyLiquify(
|
||||
targetObject,
|
||||
mode,
|
||||
params,
|
||||
x,
|
||||
y
|
||||
);
|
||||
|
||||
// 确保返回结果数据
|
||||
if (!resultData) {
|
||||
console.warn("液化变形没有返回结果数据");
|
||||
}
|
||||
|
||||
return resultData;
|
||||
} catch (error) {
|
||||
console.error("液化变形应用失败:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置液化操作
|
||||
* @returns {ImageData} 重置后的图像数据
|
||||
*/
|
||||
reset() {
|
||||
return this.enhancedManager.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查图层是否可以液化
|
||||
* @param {String} layerId 图层ID
|
||||
* @returns {Object} 检查结果
|
||||
*/
|
||||
checkLayerForLiquify(layerId) {
|
||||
return this.enhancedManager.checkLayerForLiquify(layerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态信息
|
||||
* @returns {Object} 状态信息
|
||||
*/
|
||||
getStatus() {
|
||||
return this.enhancedManager.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
dispose() {
|
||||
if (this.enhancedManager) {
|
||||
this.enhancedManager.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,878 @@
|
||||
/**
|
||||
* WebGL加速的液化管理器
|
||||
* 使用WebGL技术进行加速液化变形处理
|
||||
*/
|
||||
export class LiquifyWebGLManager {
|
||||
/**
|
||||
* 创建WebGL液化管理器
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.canvas = null;
|
||||
this.gl = null;
|
||||
this.program = null;
|
||||
this.texture = null;
|
||||
this.mesh = null;
|
||||
this.initialized = false;
|
||||
this.originalImageData = null;
|
||||
this.currentImageData = null;
|
||||
|
||||
// 变形配置
|
||||
this.config = {
|
||||
gridSize: options.gridSize || 20,
|
||||
maxStrength: options.maxStrength || 100,
|
||||
textureSize: 0,
|
||||
meshResolution: options.meshResolution || 64,
|
||||
};
|
||||
|
||||
// 当前参数
|
||||
this.params = {
|
||||
size: 80, // 增大默认尺寸
|
||||
pressure: 0.8, // 增大默认压力
|
||||
distortion: 0,
|
||||
power: 0.8, // 增大默认动力
|
||||
};
|
||||
|
||||
// 鼠标位置跟踪(用于推拉模式)
|
||||
this.lastMouseX = 0;
|
||||
this.lastMouseY = 0;
|
||||
this.mouseMovementX = 0;
|
||||
this.mouseMovementY = 0;
|
||||
this.isFirstApply = true; // 标记是否是首次应用
|
||||
|
||||
// 液化工具模式
|
||||
this.modes = {
|
||||
PUSH: "push",
|
||||
CLOCKWISE: "clockwise",
|
||||
COUNTERCLOCKWISE: "counterclockwise",
|
||||
PINCH: "pinch",
|
||||
EXPAND: "expand",
|
||||
CRYSTAL: "crystal",
|
||||
EDGE: "edge",
|
||||
RECONSTRUCT: "reconstruct",
|
||||
};
|
||||
this.currentMode = this.modes.PUSH;
|
||||
|
||||
// 变形点历史记录
|
||||
this.deformHistory = [];
|
||||
|
||||
// WebGL着色器程序
|
||||
this.vertexShaderSource = `
|
||||
attribute vec2 a_position;
|
||||
attribute vec2 a_texCoord;
|
||||
|
||||
uniform mat3 u_matrix;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
|
||||
v_texCoord = a_texCoord;
|
||||
}
|
||||
`;
|
||||
|
||||
this.fragmentShaderSource = `
|
||||
precision mediump float;
|
||||
|
||||
uniform sampler2D u_image;
|
||||
uniform vec2 u_textureSize;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
|
||||
void main() {
|
||||
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
|
||||
vec4 color = texture2D(u_image, v_texCoord);
|
||||
|
||||
// 简单的边缘检查,保证边缘渲染正确
|
||||
if(v_texCoord.x < 0.0 || v_texCoord.x > 1.0 ||
|
||||
v_texCoord.y < 0.0 || v_texCoord.y > 1.0) {
|
||||
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
} else {
|
||||
gl_FragColor = color;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 变形网格着色器程序
|
||||
this.deformVertexShaderSource = `
|
||||
attribute vec2 a_position;
|
||||
attribute vec2 a_texCoord;
|
||||
attribute vec2 a_deformation;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
|
||||
void main() {
|
||||
vec2 position = a_position + a_deformation;
|
||||
gl_Position = vec4(position * 2.0 - 1.0, 0, 1);
|
||||
v_texCoord = a_texCoord;
|
||||
}
|
||||
`;
|
||||
|
||||
this.deformFragmentShaderSource = `
|
||||
precision mediump float;
|
||||
|
||||
uniform sampler2D u_image;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
|
||||
void main() {
|
||||
vec4 color = texture2D(u_image, v_texCoord);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化WebGL环境
|
||||
* @param {HTMLImageElement} image 图像元素
|
||||
* @returns {Boolean} 是否初始化成功
|
||||
*/
|
||||
initialize(image) {
|
||||
// 创建WebGL Canvas
|
||||
this.canvas = document.createElement("canvas");
|
||||
|
||||
// 设置canvas大小与图像相同
|
||||
this.canvas.width = image.width;
|
||||
this.canvas.height = image.height;
|
||||
|
||||
// 尝试获取WebGL上下文
|
||||
try {
|
||||
this.gl =
|
||||
this.canvas.getContext("webgl") ||
|
||||
this.canvas.getContext("experimental-webgl");
|
||||
} catch (e) {
|
||||
console.error("WebGL初始化失败:", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.gl) {
|
||||
console.error("WebGL不可用");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置视口
|
||||
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 编译着色器程序
|
||||
if (!this._createShaderProgram()) {
|
||||
console.error("着色器程序创建失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建纹理
|
||||
this.texture = this._createTexture(image);
|
||||
if (!this.texture) {
|
||||
console.error("纹理创建失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 记录原始图像数据
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = image.width;
|
||||
tempCanvas.height = image.height;
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
tempCtx.drawImage(image, 0, 0);
|
||||
this.originalImageData = tempCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
image.width,
|
||||
image.height
|
||||
);
|
||||
this.currentImageData = new ImageData(
|
||||
new Uint8ClampedArray(this.originalImageData.data),
|
||||
this.originalImageData.width,
|
||||
this.originalImageData.height
|
||||
);
|
||||
|
||||
// 创建变形网格
|
||||
this._createDeformMesh();
|
||||
|
||||
this.config.textureSize = [image.width, image.height];
|
||||
this.initialized = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建着色器程序
|
||||
* @returns {Boolean} 是否创建成功
|
||||
* @private
|
||||
*/
|
||||
_createShaderProgram() {
|
||||
// 创建标准渲染程序
|
||||
const vertexShader = this._compileShader(
|
||||
this.vertexShaderSource,
|
||||
this.gl.VERTEX_SHADER
|
||||
);
|
||||
const fragmentShader = this._compileShader(
|
||||
this.fragmentShaderSource,
|
||||
this.gl.FRAGMENT_SHADER
|
||||
);
|
||||
|
||||
if (!vertexShader || !fragmentShader) return false;
|
||||
|
||||
// 创建程序
|
||||
this.program = this.gl.createProgram();
|
||||
this.gl.attachShader(this.program, vertexShader);
|
||||
this.gl.attachShader(this.program, fragmentShader);
|
||||
this.gl.linkProgram(this.program);
|
||||
|
||||
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
|
||||
console.error(
|
||||
"着色器程序链接失败:",
|
||||
this.gl.getProgramInfoLog(this.program)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建变形渲染程序
|
||||
const deformVertexShader = this._compileShader(
|
||||
this.deformVertexShaderSource,
|
||||
this.gl.VERTEX_SHADER
|
||||
);
|
||||
const deformFragmentShader = this._compileShader(
|
||||
this.deformFragmentShaderSource,
|
||||
this.gl.FRAGMENT_SHADER
|
||||
);
|
||||
|
||||
if (!deformVertexShader || !deformFragmentShader) return false;
|
||||
|
||||
// 创建变形程序
|
||||
this.deformProgram = this.gl.createProgram();
|
||||
this.gl.attachShader(this.deformProgram, deformVertexShader);
|
||||
this.gl.attachShader(this.deformProgram, deformFragmentShader);
|
||||
this.gl.linkProgram(this.deformProgram);
|
||||
|
||||
if (!this.gl.getProgramParameter(this.deformProgram, this.gl.LINK_STATUS)) {
|
||||
console.error(
|
||||
"变形着色器程序链接失败:",
|
||||
this.gl.getProgramInfoLog(this.deformProgram)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编译着色器
|
||||
* @param {String} source 着色器源码
|
||||
* @param {Number} type 着色器类型
|
||||
* @returns {WebGLShader} 编译后的着色器
|
||||
* @private
|
||||
*/
|
||||
_compileShader(source, type) {
|
||||
const shader = this.gl.createShader(type);
|
||||
this.gl.shaderSource(shader, source);
|
||||
this.gl.compileShader(shader);
|
||||
|
||||
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
||||
console.error(
|
||||
"着色器编译失败:",
|
||||
this.gl.getShaderInfoLog(shader),
|
||||
"shader type:",
|
||||
type === this.gl.VERTEX_SHADER ? "VERTEX_SHADER" : "FRAGMENT_SHADER",
|
||||
"source:",
|
||||
source
|
||||
);
|
||||
this.gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建WebGL纹理
|
||||
* @param {HTMLImageElement} image 图像元素
|
||||
* @returns {WebGLTexture} WebGL纹理
|
||||
* @private
|
||||
*/
|
||||
_createTexture(image) {
|
||||
const texture = this.gl.createTexture();
|
||||
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
||||
|
||||
// 设置参数,使我们可以渲染任何尺寸的图像
|
||||
this.gl.texParameteri(
|
||||
this.gl.TEXTURE_2D,
|
||||
this.gl.TEXTURE_WRAP_S,
|
||||
this.gl.CLAMP_TO_EDGE
|
||||
);
|
||||
this.gl.texParameteri(
|
||||
this.gl.TEXTURE_2D,
|
||||
this.gl.TEXTURE_WRAP_T,
|
||||
this.gl.CLAMP_TO_EDGE
|
||||
);
|
||||
this.gl.texParameteri(
|
||||
this.gl.TEXTURE_2D,
|
||||
this.gl.TEXTURE_MIN_FILTER,
|
||||
this.gl.LINEAR
|
||||
);
|
||||
this.gl.texParameteri(
|
||||
this.gl.TEXTURE_2D,
|
||||
this.gl.TEXTURE_MAG_FILTER,
|
||||
this.gl.LINEAR
|
||||
);
|
||||
|
||||
// 上传图像到纹理
|
||||
try {
|
||||
this.gl.texImage2D(
|
||||
this.gl.TEXTURE_2D,
|
||||
0,
|
||||
this.gl.RGBA,
|
||||
this.gl.RGBA,
|
||||
this.gl.UNSIGNED_BYTE,
|
||||
image
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("纹理上传失败:", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建变形网格
|
||||
* @private
|
||||
*/
|
||||
_createDeformMesh() {
|
||||
const { meshResolution } = this.config;
|
||||
|
||||
// 创建网格顶点
|
||||
const vertices = [];
|
||||
const texCoords = [];
|
||||
const indices = [];
|
||||
const deformations = [];
|
||||
|
||||
// 创建顶点和纹理坐标
|
||||
for (let y = 0; y <= meshResolution; y++) {
|
||||
for (let x = 0; x <= meshResolution; x++) {
|
||||
const xPos = x / meshResolution;
|
||||
const yPos = y / meshResolution;
|
||||
|
||||
// 顶点位置
|
||||
vertices.push(xPos, yPos);
|
||||
|
||||
// 纹理坐标
|
||||
texCoords.push(xPos, yPos);
|
||||
|
||||
// 初始无变形
|
||||
deformations.push(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建索引(三角形)
|
||||
for (let y = 0; y < meshResolution; y++) {
|
||||
for (let x = 0; x < meshResolution; x++) {
|
||||
const i0 = y * (meshResolution + 1) + x;
|
||||
const i1 = i0 + 1;
|
||||
const i2 = i0 + meshResolution + 1;
|
||||
const i3 = i2 + 1;
|
||||
|
||||
// 三角形1
|
||||
indices.push(i0, i2, i1);
|
||||
|
||||
// 三角形2
|
||||
indices.push(i1, i2, i3);
|
||||
}
|
||||
}
|
||||
|
||||
this.mesh = {
|
||||
vertices: new Float32Array(vertices),
|
||||
texCoords: new Float32Array(texCoords),
|
||||
indices: new Uint16Array(indices),
|
||||
deformations: new Float32Array(deformations),
|
||||
resolution: meshResolution,
|
||||
};
|
||||
|
||||
// 创建顶点缓冲区
|
||||
this.vertexBuffer = this.gl.createBuffer();
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
||||
this.gl.bufferData(
|
||||
this.gl.ARRAY_BUFFER,
|
||||
this.mesh.vertices,
|
||||
this.gl.STATIC_DRAW
|
||||
);
|
||||
|
||||
// 创建纹理坐标缓冲区
|
||||
this.texCoordBuffer = this.gl.createBuffer();
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
|
||||
this.gl.bufferData(
|
||||
this.gl.ARRAY_BUFFER,
|
||||
this.mesh.texCoords,
|
||||
this.gl.STATIC_DRAW
|
||||
);
|
||||
|
||||
// 创建变形缓冲区
|
||||
this.deformBuffer = this.gl.createBuffer();
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
|
||||
this.gl.bufferData(
|
||||
this.gl.ARRAY_BUFFER,
|
||||
this.mesh.deformations,
|
||||
this.gl.DYNAMIC_DRAW
|
||||
);
|
||||
|
||||
// 创建索引缓冲区
|
||||
this.indexBuffer = this.gl.createBuffer();
|
||||
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
||||
this.gl.bufferData(
|
||||
this.gl.ELEMENT_ARRAY_BUFFER,
|
||||
this.mesh.indices,
|
||||
this.gl.STATIC_DRAW
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用液化变形
|
||||
* @param {Number} x 变形中心X坐标 (图像像素坐标)
|
||||
* @param {Number} y 变形中心Y坐标 (图像像素坐标)
|
||||
*/
|
||||
applyDeformation(x, y) {
|
||||
if (!this.initialized || !this.mesh) return;
|
||||
|
||||
// 计算鼠标移动方向
|
||||
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;
|
||||
|
||||
// 将图像像素坐标转换为纹理坐标 (0-1范围)
|
||||
// 使用原始图像数据的尺寸进行归一化,而不是WebGL canvas的尺寸
|
||||
const imageWidth = this.originalImageData
|
||||
? this.originalImageData.width
|
||||
: this.canvas.width;
|
||||
const imageHeight = this.originalImageData
|
||||
? this.originalImageData.height
|
||||
: this.canvas.height;
|
||||
|
||||
const tx = x / imageWidth;
|
||||
const ty = y / imageHeight;
|
||||
|
||||
console.log(
|
||||
`WebGL变形: 像素坐标(${x}, ${y}) -> 纹理坐标(${tx.toFixed(
|
||||
3
|
||||
)}, ${ty.toFixed(3)}), 图像尺寸(${imageWidth}x${imageHeight})`
|
||||
);
|
||||
|
||||
// 获取当前参数
|
||||
const { size, pressure, distortion, power } = this.params;
|
||||
const mode = this.currentMode;
|
||||
|
||||
// 计算影响半径 (纹理坐标空间)
|
||||
const radius = (size / 100) * 0.2; // 调整半径计算,使效果更自然
|
||||
const strength = (pressure * power * this.config.maxStrength) / 800; // 进一步降低基础强度
|
||||
|
||||
// 保存当前变形点,用于重建功能
|
||||
this.deformHistory.push({
|
||||
x: tx,
|
||||
y: ty,
|
||||
radius,
|
||||
strength,
|
||||
mode,
|
||||
distortion,
|
||||
});
|
||||
|
||||
// 对网格顶点应用变形
|
||||
const { resolution } = this.mesh;
|
||||
const deformations = this.mesh.deformations;
|
||||
|
||||
for (let i = 0; i <= resolution; i++) {
|
||||
for (let j = 0; j <= resolution; j++) {
|
||||
const idx = (i * (resolution + 1) + j) * 2;
|
||||
|
||||
// 顶点在纹理空间中的位置
|
||||
const vx = j / resolution;
|
||||
const vy = i / resolution;
|
||||
|
||||
// 计算到变形中心的距离
|
||||
const dx = vx - tx;
|
||||
const dy = vy - ty;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 只影响半径内的点
|
||||
if (distance < radius) {
|
||||
// 计算影响因子
|
||||
const factor = Math.pow(1 - distance / radius, 2) * strength;
|
||||
|
||||
// 根据不同模式应用变形
|
||||
switch (mode) {
|
||||
case this.modes.PUSH:
|
||||
// 推拉模式 - 真正的拖拽效果
|
||||
// 计算鼠标移动距离(转换为纹理坐标空间)
|
||||
const movementX = this.mouseMovementX / imageWidth;
|
||||
const movementY = this.mouseMovementY / imageHeight;
|
||||
const movementLength = Math.sqrt(
|
||||
movementX * movementX + movementY * movementY
|
||||
);
|
||||
|
||||
// 只有在有足够移动距离时才应用效果
|
||||
if (movementLength > 0.002) {
|
||||
// 提高阈值,确保有明显移动
|
||||
// 归一化移动方向
|
||||
const moveX = movementX / movementLength;
|
||||
const moveY = movementY / movementLength;
|
||||
|
||||
// 计算衰减(距离中心越近,效果越强)
|
||||
const radiusRatio = distance / radius;
|
||||
const falloff = Math.pow(1 - radiusRatio, 2.0); // 使用更强的衰减
|
||||
|
||||
// 基于实际移动距离计算强度
|
||||
const moveStrength = pressure * power * movementLength * 0.5; // 降低移动强度系数
|
||||
|
||||
// 计算最终拖拽强度
|
||||
const dragStrength = moveStrength * falloff * factor;
|
||||
|
||||
// 向鼠标移动方向拖拽
|
||||
const dragX = moveX * dragStrength;
|
||||
const dragY = moveY * dragStrength;
|
||||
|
||||
// 应用变形,但限制最大变形量
|
||||
const maxDeform = 0.01; // 限制单次最大变形量(纹理坐标空间)
|
||||
deformations[idx] += Math.max(
|
||||
-maxDeform,
|
||||
Math.min(maxDeform, dragX)
|
||||
);
|
||||
deformations[idx + 1] += Math.max(
|
||||
-maxDeform,
|
||||
Math.min(maxDeform, dragY)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case this.modes.CLOCKWISE:
|
||||
// 顺时针旋转
|
||||
const angle = Math.atan2(dy, dx) + factor;
|
||||
const len = distance;
|
||||
deformations[idx] += Math.cos(angle) * len - dx;
|
||||
deformations[idx + 1] += Math.sin(angle) * len - dy;
|
||||
break;
|
||||
|
||||
case this.modes.COUNTERCLOCKWISE:
|
||||
// 逆时针旋转
|
||||
const angle2 = Math.atan2(dy, dx) - factor;
|
||||
const len2 = distance;
|
||||
deformations[idx] += Math.cos(angle2) * len2 - dx;
|
||||
deformations[idx + 1] += Math.sin(angle2) * len2 - dy;
|
||||
break;
|
||||
|
||||
case this.modes.PINCH:
|
||||
// 捏合效果 - 向中心收缩
|
||||
deformations[idx] -= dx * factor;
|
||||
deformations[idx + 1] -= dy * factor;
|
||||
break;
|
||||
|
||||
case this.modes.EXPAND:
|
||||
// 展开效果 - 参考捏合算法的反向操作
|
||||
const expandFactor = factor * 1.5;
|
||||
deformations[idx] += dx * expandFactor;
|
||||
deformations[idx + 1] += 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 = Math.cos(waveAngle) * modDistance;
|
||||
const crystalY = Math.sin(waveAngle) * modDistance;
|
||||
|
||||
deformations[idx] += (crystalX - (tx + dx)) * factor;
|
||||
deformations[idx + 1] += (crystalY - (ty + dy)) * factor;
|
||||
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 edgeX = Math.cos(perpAngle) * edgeFactor;
|
||||
const edgeY = Math.sin(perpAngle) * edgeFactor;
|
||||
|
||||
deformations[idx] += edgeX;
|
||||
deformations[idx + 1] += edgeY;
|
||||
break;
|
||||
|
||||
case this.modes.RECONSTRUCT:
|
||||
// 重建 - 向原始位置恢复
|
||||
deformations[idx] *= 0.9;
|
||||
deformations[idx + 1] *= 0.9;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新变形缓冲区
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
|
||||
this.gl.bufferData(
|
||||
this.gl.ARRAY_BUFFER,
|
||||
deformations,
|
||||
this.gl.DYNAMIC_DRAW
|
||||
);
|
||||
|
||||
// 重新渲染
|
||||
this._render();
|
||||
|
||||
// 更新当前图像数据
|
||||
this.currentImageData = this._getImageData();
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染变形后的图像
|
||||
* @private
|
||||
*/
|
||||
_render() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
// 清除画布
|
||||
this.gl.clearColor(0, 0, 0, 0);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// 使用变形程序
|
||||
this.gl.useProgram(this.deformProgram);
|
||||
|
||||
// 设置纹理
|
||||
this.gl.activeTexture(this.gl.TEXTURE0);
|
||||
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
|
||||
const u_image = this.gl.getUniformLocation(this.deformProgram, "u_image");
|
||||
this.gl.uniform1i(u_image, 0);
|
||||
|
||||
// 设置顶点位置属性
|
||||
const a_position = this.gl.getAttribLocation(
|
||||
this.deformProgram,
|
||||
"a_position"
|
||||
);
|
||||
this.gl.enableVertexAttribArray(a_position);
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);
|
||||
this.gl.vertexAttribPointer(a_position, 2, this.gl.FLOAT, false, 0, 0);
|
||||
|
||||
// 设置纹理坐标属性
|
||||
const a_texCoord = this.gl.getAttribLocation(
|
||||
this.deformProgram,
|
||||
"a_texCoord"
|
||||
);
|
||||
this.gl.enableVertexAttribArray(a_texCoord);
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
|
||||
this.gl.vertexAttribPointer(a_texCoord, 2, this.gl.FLOAT, false, 0, 0);
|
||||
|
||||
// 设置变形属性
|
||||
const a_deformation = this.gl.getAttribLocation(
|
||||
this.deformProgram,
|
||||
"a_deformation"
|
||||
);
|
||||
this.gl.enableVertexAttribArray(a_deformation);
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
|
||||
this.gl.vertexAttribPointer(a_deformation, 2, this.gl.FLOAT, false, 0, 0);
|
||||
|
||||
// 绘制三角形
|
||||
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
||||
this.gl.drawElements(
|
||||
this.gl.TRIANGLES,
|
||||
this.mesh.indices.length,
|
||||
this.gl.UNSIGNED_SHORT,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前图像数据
|
||||
* @returns {ImageData} 当前图像数据
|
||||
* @private
|
||||
*/
|
||||
_getImageData() {
|
||||
const width = this.canvas.width;
|
||||
const height = this.canvas.height;
|
||||
|
||||
// 读取WebGL画布像素
|
||||
const pixels = new Uint8Array(width * height * 4);
|
||||
this.gl.readPixels(
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
this.gl.RGBA,
|
||||
this.gl.UNSIGNED_BYTE,
|
||||
pixels
|
||||
);
|
||||
|
||||
// 直接创建ImageData,不进行翻转
|
||||
// WebGL和Canvas2D的坐标系不同,但这里我们保持WebGL的原始输出
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray(pixels),
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
return imageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有变形
|
||||
* @returns {ImageData} 重置后的图像数据
|
||||
*/
|
||||
reset() {
|
||||
if (!this.initialized) return null;
|
||||
|
||||
// 清除变形历史
|
||||
this.deformHistory = [];
|
||||
|
||||
// 重置所有变形
|
||||
const deformations = new Float32Array(this.mesh.deformations.length);
|
||||
this.mesh.deformations = deformations;
|
||||
|
||||
// 更新变形缓冲区
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.deformBuffer);
|
||||
this.gl.bufferData(
|
||||
this.gl.ARRAY_BUFFER,
|
||||
deformations,
|
||||
this.gl.DYNAMIC_DRAW
|
||||
);
|
||||
|
||||
// 重新渲染
|
||||
this._render();
|
||||
|
||||
// 更新当前图像数据
|
||||
this.currentImageData = this._getImageData();
|
||||
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化模式
|
||||
* @param {String} mode 液化模式
|
||||
*/
|
||||
setMode(mode) {
|
||||
if (Object.values(this.modes).includes(mode)) {
|
||||
this.currentMode = mode;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化参数
|
||||
* @param {String} param 参数名
|
||||
* @param {Number} value 参数值
|
||||
*/
|
||||
setParam(param, value) {
|
||||
if (param in this.params) {
|
||||
this.params[param] = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前参数
|
||||
* @returns {Object} 当前参数
|
||||
*/
|
||||
getParams() {
|
||||
return { ...this.params };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置参数为默认值
|
||||
*/
|
||||
resetParams() {
|
||||
this.params = {
|
||||
size: 50,
|
||||
pressure: 0.5,
|
||||
distortion: 0,
|
||||
power: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始图像数据
|
||||
* @returns {ImageData} 原始图像数据
|
||||
*/
|
||||
getOriginalImageData() {
|
||||
return this.originalImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前图像数据
|
||||
* @returns {ImageData} 当前图像数据
|
||||
*/
|
||||
getCurrentImageData() {
|
||||
return this.currentImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
dispose() {
|
||||
if (!this.gl) return;
|
||||
|
||||
// 删除缓冲区
|
||||
if (this.vertexBuffer) this.gl.deleteBuffer(this.vertexBuffer);
|
||||
if (this.texCoordBuffer) this.gl.deleteBuffer(this.texCoordBuffer);
|
||||
if (this.deformBuffer) this.gl.deleteBuffer(this.deformBuffer);
|
||||
if (this.indexBuffer) this.gl.deleteBuffer(this.indexBuffer);
|
||||
|
||||
// 删除纹理
|
||||
if (this.texture) this.gl.deleteTexture(this.texture);
|
||||
|
||||
// 删除着色器程序
|
||||
if (this.program) this.gl.deleteProgram(this.program);
|
||||
if (this.deformProgram) this.gl.deleteProgram(this.deformProgram);
|
||||
|
||||
// 重置属性
|
||||
this.canvas = null;
|
||||
this.gl = null;
|
||||
this.program = null;
|
||||
this.deformProgram = null;
|
||||
this.texture = null;
|
||||
this.mesh = null;
|
||||
this.initialized = false;
|
||||
this.deformHistory = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持WebGL
|
||||
* @returns {Boolean} 是否支持WebGL
|
||||
*/
|
||||
static isSupported() {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
return !!(
|
||||
window.WebGLRenderingContext &&
|
||||
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user