水波荡漾滤镜是网页视觉特效中常见的效果类型,通过HTML5的相关图形处理能力可以实现非常自然的水波动态效果,适配不同的网页场景需求。实现该效果的核心逻辑是模拟水波传播的位移变化,结合像素处理或着色器计算完成最终渲染。
水波滤镜的核心实现原理
水波荡漾的本质是模拟液体表面受到扰动后,波纹向外扩散的过程。在HTML5技术体系中,主要有两种实现思路,一种是使用Canvas的2D上下文逐像素处理图像,另一种是使用WebGL结合着色器程序完成高效计算。
两种方案各有适用场景,Canvas方案兼容性更好,适合简单的静态图像水波效果;WebGL方案性能更强,适合需要实时交互、多波纹叠加的复杂场景。
Canvas 2D实现方案
Canvas实现水波滤镜的核心是操作ImageData对象,通过计算每个像素的偏移量,模拟波纹带来的视觉位移。以下是一段基础的水波效果实现代码:
// 获取Canvas元素和上下文
const canvas = document.getElementById('waveCanvas');
const ctx = canvas.getContext('2d');
// 设置画布尺寸
canvas.width = 800;
canvas.height = 400;
// 加载待处理的图像
const img = new Image();
img.src = 'https://ipipp.com/demo/water-bg.jpg';
img.onload = function() {
// 绘制原始图像
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
// 获取图像像素数据
const originalData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const originalPixels = originalData.data;
// 水波参数配置
const waveConfig = {
centerX: canvas.width / 2, // 波纹中心X坐标
centerY: canvas.height / 2, // 波纹中心Y坐标
amplitude: 15, // 波纹振幅
wavelength: 80, // 波纹波长
phase: 0 // 相位,用于动态效果
};
// 启动动画循环
function animateWave() {
// 创建新的像素数据对象
const newData = ctx.createImageData(canvas.width, canvas.height);
const newPixels = newData.data;
// 遍历每个像素计算偏移
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
// 计算当前像素到波纹中心的距离
const dx = x - waveConfig.centerX;
const dy = y - waveConfig.centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
// 计算波纹带来的偏移量
const offset = waveConfig.amplitude * Math.sin(distance / waveConfig.wavelength * 2 * Math.PI + waveConfig.phase);
// 计算原始像素坐标
const srcX = x + dx / distance * offset;
const srcY = y + dy / distance * offset;
// 边界判断,避免越界
if (srcX >= 0 && srcX < canvas.width && srcY >= 0 && srcY < canvas.height) {
const srcIdx = (Math.floor(srcY) * canvas.width + Math.floor(srcX)) * 4;
const dstIdx = (y * canvas.width + x) * 4;
// 复制像素值
newPixels[dstIdx] = originalPixels[srcIdx];
newPixels[dstIdx + 1] = originalPixels[srcIdx + 1];
newPixels[dstIdx + 2] = originalPixels[srcIdx + 2];
newPixels[dstIdx + 3] = originalPixels[srcIdx + 3];
}
}
}
// 将处理后的像素绘制到画布
ctx.putImageData(newData, 0, 0);
// 更新相位,实现动态效果
waveConfig.phase += 0.1;
// 循环调用动画
requestAnimationFrame(animateWave);
}
// 启动动画
animateWave();
};
WebGL结合着色器实现方案
WebGL方案通过GPU并行计算处理像素,性能远高于Canvas 2D方案,适合需要实时交互的场景。核心是通过顶点着色器和片元着色器实现水波位移计算,以下是对应的实现代码:
<canvas id="webglWave" width="800" height="400"></canvas>
<script>
// 获取WebGL上下文
const glCanvas = document.getElementById('webglWave');
const gl = glCanvas.getContext('webgl');
if (!gl) {
console.error('当前浏览器不支持WebGL');
}
// 顶点着色器代码
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_amplitude;
uniform float u_wavelength;
uniform float u_phase;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
// 将纹理坐标转换为画布像素坐标
vec2 pixelCoord = v_texCoord * u_resolution;
// 计算到波纹中心的距离
float dx = pixelCoord.x - u_center.x;
float dy = pixelCoord.y - u_center.y;
float distance = sqrt(dx * dx + dy * dy);
// 计算波纹偏移
float offset = u_amplitude * sin(distance / u_wavelength * 6.28318 + u_phase);
// 计算原始纹理坐标
vec2 srcCoord = v_texCoord + (vec2(dx, dy) / distance * offset) / u_resolution;
// 采样纹理颜色
gl_FragColor = texture2D(u_texture, srcCoord);
}
`;
// 创建着色器函数
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('着色器编译失败:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// 创建着色器程序
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('着色器程序链接失败:', gl.getProgramInfoLog(program));
}
gl.useProgram(program);
// 设置顶点和纹理坐标数据
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
]), gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1
]), gl.STATIC_DRAW);
const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// 加载纹理图像
const texture = gl.createTexture();
const textureImg = new Image();
textureImg.crossOrigin = 'anonymous';
textureImg.src = 'https://ipipp.com/demo/water-bg.jpg';
textureImg.onload = function() {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImg);
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);
// 设置uniform变量
const centerLocation = gl.getUniformLocation(program, 'u_center');
gl.uniform2f(centerLocation, glCanvas.width / 2, glCanvas.height / 2);
const amplitudeLocation = gl.getUniformLocation(program, 'u_amplitude');
gl.uniform1f(amplitudeLocation, 20);
const wavelengthLocation = gl.getUniformLocation(program, 'u_wavelength');
gl.uniform1f(wavelengthLocation, 100);
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
gl.uniform2f(resolutionLocation, glCanvas.width, glCanvas.height);
const phaseLocation = gl.getUniformLocation(program, 'u_phase');
let phase = 0;
// 动画循环
function render() {
phase += 0.1;
gl.uniform1f(phaseLocation, phase);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
render();
};
</script>
水波滤镜的实用应用技巧
水波滤镜在实际开发中有多种应用方式,可以根据场景需求调整参数和交互逻辑:
- 交互式水波效果:监听画布的
click或者mousemove事件,将点击位置作为波纹中心,实现点击水面产生涟漪的效果,提升用户交互体验。 - 多波纹叠加:维护一个波纹数组,记录每个波纹的中心、振幅、波长等参数,在每一帧计算时叠加所有波纹的偏移量,实现多个涟漪同时扩散的效果。
- 性能优化技巧:Canvas方案可以只处理波纹影响区域的像素,不需要遍历整个画布;WebGL方案可以通过调整着色器精度、减少uniform变量更新频率提升性能。
- 场景适配:游戏场景中的水波可以结合角色移动触发,视觉特效中可以调整振幅和波长匹配整体设计风格,表单交互中可以在输入框聚焦时添加轻微水波效果作为反馈。
两种方案的选择建议
如果项目只需要简单的静态水波效果,或者需要兼容低版本浏览器,优先选择Canvas 2D方案;如果需要实时交互、多波纹叠加、高性能渲染的场景,比如网页游戏、复杂视觉特效页面,优先选择WebGL方案。开发者可以根据项目的实际需求灵活选择实现路径,快速完成水波荡漾滤镜的开发和适配。