532 lines
15 KiB
JavaScript
532 lines
15 KiB
JavaScript
/**
|
||
* WebGL版本的液化管理器 - 暂时不兼容WebGL的版本 后续看情况支持
|
||
* 使用GPU加速进行高性能液化变形处理,支持持续按压效果
|
||
*/
|
||
export class LiquifyWebGLManager {
|
||
constructor(options = {}) {
|
||
this.config = {
|
||
meshResolution: options.meshResolution || 64, // WebGL网格精度
|
||
maxStrength: options.maxStrength || 100,
|
||
useFloatTextures: options.useFloatTextures !== false, // 默认启用浮点纹理
|
||
};
|
||
|
||
this.params = {
|
||
size: 50,
|
||
pressure: 0.5,
|
||
distortion: 0,
|
||
power: 0.5,
|
||
};
|
||
|
||
this.modes = {
|
||
PUSH: "push",
|
||
CLOCKWISE: "clockwise",
|
||
COUNTERCLOCKWISE: "counterclockwise",
|
||
PINCH: "pinch",
|
||
EXPAND: "expand",
|
||
CRYSTAL: "crystal",
|
||
EDGE: "edge",
|
||
RECONSTRUCT: "reconstruct",
|
||
};
|
||
|
||
this.currentMode = this.modes.PUSH;
|
||
this.initialized = false;
|
||
|
||
// WebGL相关
|
||
this.canvas = null;
|
||
this.gl = null;
|
||
this.program = null;
|
||
this.meshBuffer = null;
|
||
this.originalTexture = null;
|
||
this.currentTexture = null;
|
||
this.framebuffer = null;
|
||
|
||
// 持续按压相关状态
|
||
this.pressStartTime = 0;
|
||
this.pressDuration = 0;
|
||
this.accumulatedRotation = 0;
|
||
this.accumulatedScale = 0;
|
||
this.isHolding = false;
|
||
this.continuousTimer = null;
|
||
this.continuousApplyInterval = 50; // 50ms间隔
|
||
|
||
// 初始点坐标
|
||
this.initialMouseX = 0;
|
||
this.initialMouseY = 0;
|
||
this.isDragging = false;
|
||
}
|
||
|
||
/**
|
||
* 检查WebGL支持
|
||
*/
|
||
static isSupported() {
|
||
try {
|
||
const canvas = document.createElement("canvas");
|
||
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
||
return !!gl;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化WebGL液化管理器
|
||
*/
|
||
initialize(imageSource) {
|
||
try {
|
||
// 创建WebGL画布
|
||
this.canvas = document.createElement("canvas");
|
||
this.gl = this.canvas.getContext("webgl") || this.canvas.getContext("experimental-webgl");
|
||
|
||
if (!this.gl) {
|
||
throw new Error("WebGL不可用");
|
||
}
|
||
|
||
// 处理图像源
|
||
if (imageSource instanceof HTMLImageElement) {
|
||
this.canvas.width = imageSource.width;
|
||
this.canvas.height = imageSource.height;
|
||
this._initWebGL(imageSource);
|
||
} else {
|
||
throw new Error("WebGL版本目前只支持HTMLImageElement");
|
||
}
|
||
|
||
this.initialized = true;
|
||
return true;
|
||
} catch (error) {
|
||
console.error("WebGL液化管理器初始化失败:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化WebGL环境
|
||
*/
|
||
_initWebGL(image) {
|
||
const gl = this.gl;
|
||
|
||
// 创建着色器程序
|
||
this._createShaderProgram();
|
||
|
||
// 创建纹理
|
||
this.originalTexture = this._createTexture(image);
|
||
this.currentTexture = this._createTexture(image);
|
||
|
||
// 创建帧缓冲
|
||
this.framebuffer = gl.createFramebuffer();
|
||
|
||
// 创建网格缓冲
|
||
this._createMeshBuffer();
|
||
|
||
console.log("WebGL液化环境初始化完成");
|
||
}
|
||
|
||
/**
|
||
* 创建着色器程序
|
||
*/
|
||
_createShaderProgram() {
|
||
const gl = this.gl;
|
||
|
||
// 顶点着色器
|
||
const vertexShaderSource = `
|
||
attribute vec2 a_position;
|
||
attribute vec2 a_texCoord;
|
||
varying vec2 v_texCoord;
|
||
void main() {
|
||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||
v_texCoord = a_texCoord;
|
||
}
|
||
`;
|
||
|
||
// 片段着色器 - 支持多种液化模式
|
||
const fragmentShaderSource = `
|
||
precision mediump float;
|
||
uniform sampler2D u_texture;
|
||
uniform vec2 u_center;
|
||
uniform float u_radius;
|
||
uniform float u_strength;
|
||
uniform int u_mode;
|
||
uniform float u_time;
|
||
uniform float u_distortion;
|
||
uniform vec2 u_dragVector;
|
||
varying vec2 v_texCoord;
|
||
|
||
void main() {
|
||
vec2 coord = v_texCoord;
|
||
vec2 center = u_center;
|
||
float dist = distance(coord, center);
|
||
|
||
if (dist < u_radius && dist > 0.0) {
|
||
float factor = 1.0 - (dist / u_radius);
|
||
factor = factor * factor * factor * u_strength;
|
||
|
||
if (u_mode == 0) { // PUSH
|
||
vec2 direction = normalize(u_dragVector);
|
||
coord += direction * factor * 0.1;
|
||
}
|
||
else if (u_mode == 1 || u_mode == 2) { // CLOCKWISE/COUNTERCLOCKWISE
|
||
float angle = atan(coord.y - center.y, coord.x - center.x);
|
||
float rotationSpeed = u_mode == 1 ? 0.1 : -0.1;
|
||
angle += rotationSpeed * factor * (1.0 + u_time * 0.001);
|
||
coord = center + dist * vec2(cos(angle), sin(angle));
|
||
}
|
||
else if (u_mode == 3) { // PINCH
|
||
vec2 direction = center - coord;
|
||
coord += direction * factor * (0.5 + u_time * 0.0005);
|
||
}
|
||
else if (u_mode == 4) { // EXPAND
|
||
vec2 direction = coord - center;
|
||
coord += direction * factor * (0.5 + u_time * 0.0005);
|
||
}
|
||
else if (u_mode == 5) { // CRYSTAL
|
||
float crystalAngle = atan(coord.y - center.y, coord.x - center.x);
|
||
float wave1 = sin(crystalAngle * 8.0 + u_time * 0.005) * 0.6;
|
||
float wave2 = cos(crystalAngle * 12.0 + u_time * 0.003) * 0.4;
|
||
crystalAngle += (wave1 + wave2) * u_distortion * factor;
|
||
coord = center + dist * vec2(cos(crystalAngle), sin(crystalAngle));
|
||
}
|
||
else if (u_mode == 6) { // EDGE
|
||
float edgeAngle = atan(coord.y - center.y, coord.x - center.x);
|
||
float edgeWave = sin(dist * 3.14159 * 4.0 + u_time * 0.004) *
|
||
cos(edgeAngle * 6.0 + u_time * 0.002);
|
||
float perpAngle = edgeAngle + 3.14159 * 0.5;
|
||
coord += vec2(cos(perpAngle), sin(perpAngle)) * edgeWave * factor * u_distortion * 0.5;
|
||
}
|
||
}
|
||
|
||
gl_FragColor = texture2D(u_texture, coord);
|
||
}
|
||
`;
|
||
|
||
this.program = this._createProgram(vertexShaderSource, fragmentShaderSource);
|
||
}
|
||
|
||
/**
|
||
* 创建着色器
|
||
*/
|
||
_createShader(type, source) {
|
||
const gl = this.gl;
|
||
const shader = gl.createShader(type);
|
||
gl.shaderSource(shader, source);
|
||
gl.compileShader(shader);
|
||
|
||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||
throw new Error("着色器编译失败: " + gl.getShaderInfoLog(shader));
|
||
}
|
||
|
||
return shader;
|
||
}
|
||
|
||
/**
|
||
* 创建着色器程序
|
||
*/
|
||
_createProgram(vertexSource, fragmentSource) {
|
||
const gl = this.gl;
|
||
const vertexShader = this._createShader(gl.VERTEX_SHADER, vertexSource);
|
||
const fragmentShader = this._createShader(gl.FRAGMENT_SHADER, fragmentSource);
|
||
|
||
const program = gl.createProgram();
|
||
gl.attachShader(program, vertexShader);
|
||
gl.attachShader(program, fragmentShader);
|
||
gl.linkProgram(program);
|
||
|
||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||
throw new Error("程序链接失败: " + gl.getProgramInfoLog(program));
|
||
}
|
||
|
||
return program;
|
||
}
|
||
|
||
/**
|
||
* 创建纹理
|
||
*/
|
||
_createTexture(image) {
|
||
const gl = this.gl;
|
||
const texture = gl.createTexture();
|
||
|
||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||
|
||
return texture;
|
||
}
|
||
|
||
/**
|
||
* 创建网格缓冲
|
||
*/
|
||
_createMeshBuffer() {
|
||
const gl = this.gl;
|
||
const vertices = new Float32Array([-1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1]);
|
||
|
||
this.meshBuffer = gl.createBuffer();
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, this.meshBuffer);
|
||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
||
}
|
||
|
||
/**
|
||
* 设置液化模式
|
||
*/
|
||
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 };
|
||
}
|
||
|
||
/**
|
||
* 开始液化操作
|
||
*/
|
||
startDeformation(x, y) {
|
||
this.initialMouseX = x;
|
||
this.initialMouseY = y;
|
||
this.isDragging = true;
|
||
|
||
// 初始化持续按压状态
|
||
this.pressStartTime = Date.now();
|
||
this.pressDuration = 0;
|
||
this.accumulatedRotation = 0;
|
||
this.accumulatedScale = 0;
|
||
this.isHolding = true;
|
||
|
||
// 启动持续效果定时器
|
||
this.startContinuousEffect();
|
||
|
||
console.log(`WebGL液化开始,初始点: (${x}, ${y})`);
|
||
}
|
||
|
||
/**
|
||
* 结束液化操作
|
||
*/
|
||
endDeformation() {
|
||
this.isDragging = false;
|
||
this.isHolding = false;
|
||
this.pressStartTime = 0;
|
||
this.pressDuration = 0;
|
||
this.accumulatedRotation = 0;
|
||
this.accumulatedScale = 0;
|
||
|
||
// 停止持续效果定时器
|
||
this.stopContinuousEffect();
|
||
|
||
console.log("WebGL液化结束");
|
||
}
|
||
|
||
/**
|
||
* 启动持续效果
|
||
*/
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用持续变形效果
|
||
*/
|
||
applyContinuousDeformation() {
|
||
if (!this.isHolding || !this.initialized) return;
|
||
|
||
return this.applyDeformation(this.initialMouseX, this.initialMouseY);
|
||
}
|
||
|
||
/**
|
||
* 应用液化变形
|
||
*/
|
||
applyDeformation(x, y) {
|
||
if (!this.initialized || !this.gl) return null;
|
||
|
||
const gl = this.gl;
|
||
const { size, pressure, power, distortion } = this.params;
|
||
|
||
// 设置视口
|
||
gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
||
|
||
// 使用着色器程序
|
||
gl.useProgram(this.program);
|
||
|
||
// 绑定属性
|
||
const positionLoc = gl.getAttribLocation(this.program, "a_position");
|
||
const texCoordLoc = gl.getAttribLocation(this.program, "a_texCoord");
|
||
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, this.meshBuffer);
|
||
gl.enableVertexAttribArray(positionLoc);
|
||
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 16, 0);
|
||
gl.enableVertexAttribArray(texCoordLoc);
|
||
gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 16, 8);
|
||
|
||
// 设置uniform变量
|
||
const centerLoc = gl.getUniformLocation(this.program, "u_center");
|
||
const radiusLoc = gl.getUniformLocation(this.program, "u_radius");
|
||
const strengthLoc = gl.getUniformLocation(this.program, "u_strength");
|
||
const modeLoc = gl.getUniformLocation(this.program, "u_mode");
|
||
const timeLoc = gl.getUniformLocation(this.program, "u_time");
|
||
const distortionLoc = gl.getUniformLocation(this.program, "u_distortion");
|
||
const dragVectorLoc = gl.getUniformLocation(this.program, "u_dragVector");
|
||
|
||
// 计算参数
|
||
const centerX = x / this.canvas.width;
|
||
const centerY = 1.0 - y / this.canvas.height; // WebGL坐标系Y轴翻转
|
||
const radius = (size / Math.max(this.canvas.width, this.canvas.height)) * 2;
|
||
|
||
// 计算强度,考虑持续按压效果
|
||
let strength = pressure * power;
|
||
if (this.isHolding) {
|
||
const timeFactor = Math.min(this.pressDuration / 1000, 3.0);
|
||
if (this.currentMode === this.modes.PUSH) {
|
||
strength *= 0.2 + timeFactor * 0.3;
|
||
} else {
|
||
strength *= 0.5 + timeFactor * 0.5;
|
||
}
|
||
}
|
||
|
||
// 计算拖拽向量(用于推拉模式)
|
||
const dragX = this.isDragging ? (x - this.initialMouseX) / this.canvas.width : 0;
|
||
const dragY = this.isDragging ? -(y - this.initialMouseY) / this.canvas.height : 0;
|
||
|
||
// 获取模式索引
|
||
const modeIndex = Object.values(this.modes).indexOf(this.currentMode);
|
||
|
||
// 设置uniform值
|
||
gl.uniform2f(centerLoc, centerX, centerY);
|
||
gl.uniform1f(radiusLoc, radius);
|
||
gl.uniform1f(strengthLoc, strength);
|
||
gl.uniform1i(modeLoc, modeIndex);
|
||
gl.uniform1f(timeLoc, this.pressDuration);
|
||
gl.uniform1f(distortionLoc, distortion);
|
||
gl.uniform2f(dragVectorLoc, dragX, dragY);
|
||
|
||
// 绑定纹理
|
||
gl.activeTexture(gl.TEXTURE0);
|
||
gl.bindTexture(gl.TEXTURE_2D, this.currentTexture);
|
||
gl.uniform1i(gl.getUniformLocation(this.program, "u_texture"), 0);
|
||
|
||
// 渲染到帧缓冲
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||
gl.framebufferTexture2D(
|
||
gl.FRAMEBUFFER,
|
||
gl.COLOR_ATTACHMENT0,
|
||
gl.TEXTURE_2D,
|
||
this.currentTexture,
|
||
0
|
||
);
|
||
|
||
// 绘制
|
||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||
|
||
// 读取结果并转换为ImageData
|
||
const pixels = new Uint8Array(this.canvas.width * this.canvas.height * 4);
|
||
gl.readPixels(0, 0, this.canvas.width, this.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||
|
||
// 翻转Y轴以匹配ImageData格式
|
||
const flippedPixels = new Uint8ClampedArray(pixels.length);
|
||
for (let y = 0; y < this.canvas.height; y++) {
|
||
for (let x = 0; x < this.canvas.width; x++) {
|
||
const srcIndex = ((this.canvas.height - 1 - y) * this.canvas.width + x) * 4;
|
||
const dstIndex = (y * this.canvas.width + x) * 4;
|
||
flippedPixels[dstIndex] = pixels[srcIndex];
|
||
flippedPixels[dstIndex + 1] = pixels[srcIndex + 1];
|
||
flippedPixels[dstIndex + 2] = pixels[srcIndex + 2];
|
||
flippedPixels[dstIndex + 3] = pixels[srcIndex + 3];
|
||
}
|
||
}
|
||
|
||
return new ImageData(flippedPixels, this.canvas.width, this.canvas.height);
|
||
}
|
||
|
||
/**
|
||
* 重置到原始状态
|
||
*/
|
||
reset() {
|
||
if (!this.initialized) return null;
|
||
|
||
const gl = this.gl;
|
||
|
||
// 将原始纹理复制到当前纹理
|
||
gl.bindTexture(gl.TEXTURE_2D, this.currentTexture);
|
||
gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, this.canvas.width, this.canvas.height, 0);
|
||
|
||
// 读取原始纹理数据
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||
gl.framebufferTexture2D(
|
||
gl.FRAMEBUFFER,
|
||
gl.COLOR_ATTACHMENT0,
|
||
gl.TEXTURE_2D,
|
||
this.originalTexture,
|
||
0
|
||
);
|
||
|
||
const pixels = new Uint8Array(this.canvas.width * this.canvas.height * 4);
|
||
gl.readPixels(0, 0, this.canvas.width, this.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||
|
||
return new ImageData(new Uint8ClampedArray(pixels), this.canvas.width, this.canvas.height);
|
||
}
|
||
|
||
/**
|
||
* 重置参数为默认值
|
||
*/
|
||
resetParams() {
|
||
this.params = {
|
||
size: 50,
|
||
pressure: 0.5,
|
||
distortion: 0,
|
||
power: 0.5,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 释放资源
|
||
*/
|
||
dispose() {
|
||
if (this.gl) {
|
||
if (this.program) this.gl.deleteProgram(this.program);
|
||
if (this.meshBuffer) this.gl.deleteBuffer(this.meshBuffer);
|
||
if (this.originalTexture) this.gl.deleteTexture(this.originalTexture);
|
||
if (this.currentTexture) this.gl.deleteTexture(this.currentTexture);
|
||
if (this.framebuffer) this.gl.deleteFramebuffer(this.framebuffer);
|
||
}
|
||
|
||
this.stopContinuousEffect();
|
||
this.initialized = false;
|
||
this.canvas = null;
|
||
this.gl = null;
|
||
|
||
console.log("WebGL液化管理器资源已释放");
|
||
}
|
||
}
|