
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>液体玻璃效果演示</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
/* 控制面板 */
#controls {
position: absolute;
top: 10px;
left: 10px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
font-family: sans-serif;
border-radius: 5px;
user-select: none;
}
#controls label {
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="glslCanvas"></canvas>
<img
id="backgroundImage"
src="https://pic2.zhimg.com/v2-a53c740141ab75eb4fe16af3ef8c35c5_r.jpg"
crossorigin="anonymous"
style="display: none"
/>
<div id="controls">
<label>
<input type="checkbox" id="useCircleCheckbox" />
使用圆形
</label>
</div>
<script id="vertex-shader" type="x-shader/x-vertex">
// 将顶点位置传递出去,这样片段着色器就可以在构成平面的两个三角形上运行
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float; // 为浮点数设置中等精度
// 从 JavaScript 传入的变量
uniform vec3 iResolution;
uniform float iTime;
uniform vec4 iMouse;
uniform vec3 iImageResolution;
uniform sampler2D iImage1;
uniform float useCircle;
// 已适配 WebGL ---
vec2 R;
const float PI = 3.14159265;
// 创建旋转矩阵
mat2 Rot(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
// 像素归一化处理
float PX(float a) {
return a / R.y;
}
// 矩形距离场
float Box(vec2 p, vec2 b) {
vec2 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
// 圆形距离场
float Circle(vec2 p, float r) {
return length(p) - r;
}
// 根据 useCircle 选择形状(圆形或圆角矩形)
float Shape(vec2 p, vec2 b, float r) {
return useCircle > 0.5 ? Circle(p, r) : Box(p, b);
}
// 液体玻璃效果
vec4 LiquidGlass(vec2 uv, float direction, float quality, float size) {
vec2 radius = size / R;
vec4 color = texture2D(iImage1, uv);
float d_step = PI / direction; // 方向步长
float i_step = 1.0 / quality; // 质量步长
float d = 0.0;
// 循环被展开以兼容一些旧的GPU
for (int j = 0; j < 10; j++) {
if (float(j) >= direction) break;
float i = i_step;
for (int k = 0; k < 10; k++) {
if (float(k) >= quality) break;
color += texture2D(iImage1, uv + vec2(cos(d), sin(d)) * radius * i);
i += i_step;
}
d += d_step;
}
color /= quality * direction + 1.0; // 归一化
return color;
}
// 形状扭曲效果
vec4 Distortion(vec2 uv) {
float shape = Shape(uv, vec2(PX(50.0)), PX(50.0));
float shapeShape = smoothstep(PX(1.5), 0.0, shape - PX(50.0)); // 形状平滑过渡
float shapeDisp = smoothstep(PX(75.0), 0.0, shape - PX(25.0)); // 边框宽度
float shapeLight = shapeShape * smoothstep(0.0, PX(20.0), shape - PX(40.0)); // 光照强度
return vec4(shapeShape, shapeDisp, shapeLight, 0.0);
}
void main() {
R = iResolution.xy;
vec2 uv = gl_FragCoord.xy / R; // 归一化UV坐标
vec2 st = (gl_FragCoord.xy - 0.5 * R) / R.y; // 屏幕空间坐标
vec2 M = iMouse.xy == vec2(0.0) ? vec2(0.0) : (iMouse.xy - 0.5 * R) / R.y; // 鼠标位置
// 如果鼠标没有移动过,则将效果定位在中心
if (iMouse.x == 0.0 && iMouse.y == 0.0) {
M = vec2(0.0);
}
vec4 dist = Distortion(st - M); // 计算扭曲效果
vec2 uv2 = uv;
uv2 *= 0.5 + 0.5 * smoothstep(0.5, 1.0, dist.y); // 缩放UV
vec3 col = mix(vec3(0.0), // 透明黑色背景
0.2 + LiquidGlass(uv2, 10.0, 10.0, 5.0).rgb * 0.7, // 应用液体玻璃效果
dist.x); // 根据图标形状混合
col += dist.z * 0.9 + dist.w; // 添加图标光照和图案
// 应用阴影效果
col *= 1.0 - 0.2 * smoothstep(PX(80.0), 0.0, Shape(st - M + vec2(0.0, PX(40.0)), vec2(PX(50.0)), PX(50.0)));
// 使用 dist.x 控制透明度:图标区域不透明,其他区域透明
float alpha = dist.x;
gl_FragColor = vec4(col, alpha); // 返回最终颜色和透明度
}
</script>
<script>
const canvas = document.getElementById('glslCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('抱歉,您的浏览器不支持 WebGL。');
}
// 获取 DOM 元素
const backgroundImage = document.getElementById('backgroundImage');
const useCircleCheckbox = document.getElementById('useCircleCheckbox');
// 创建着色器程序
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
const vertexShaderSource = document.getElementById('vertex-shader').text;
const fragmentShaderSource =
document.getElementById('fragment-shader').text;
const vertexShader = createShader(
gl,
gl.VERTEX_SHADER,
vertexShaderSource
);
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
fragmentShaderSource
);
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
// 准备数据 获取 uniform 和 attribute 的位置
const positionAttributeLocation = gl.getAttribLocation(
program,
'a_position'
);
const resolutionUniformLocation = gl.getUniformLocation(
program,
'iResolution'
);
const timeUniformLocation = gl.getUniformLocation(program, 'iTime');
const mouseUniformLocation = gl.getUniformLocation(program, 'iMouse');
const imageResolutionUniformLocation = gl.getUniformLocation(
program,
'iImageResolution'
);
const imageSamplerUniformLocation = gl.getUniformLocation(
program,
'iImage1'
);
const useCircleUniformLocation = gl.getUniformLocation(
program,
'useCircle'
);
// 创建一个缓冲区来放置一个覆盖整个画布的矩形
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1];
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(positions),
gl.STATIC_DRAW
);
gl.enableVertexAttribArray(positionAttributeLocation);
gl.vertexAttribPointer(
positionAttributeLocation,
2,
gl.FLOAT,
false,
0,
0
);
// 设置纹理
let imageTexture;
backgroundImage.onload = function () {
imageTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, imageTexture);
// 设置纹理参数
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);
// 将图片数据上传到纹理
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
backgroundImage
);
// 设置图像分辨率 uniform
gl.uniform3f(
imageResolutionUniformLocation,
backgroundImage.width,
backgroundImage.height,
0
);
// 启动渲染
requestAnimationFrame(render);
};
// 如果图片已经加载完成 (例如从缓存加载)
if (backgroundImage.complete) {
backgroundImage.onload();
}
// 渲染循环
let startTime = Date.now();
let mouseX = 0;
let mouseY = 0;
function render(time) {
// 调整画布大小以匹配显示大小
const displayWidth = gl.canvas.clientWidth;
const displayHeight = gl.canvas.clientHeight;
if (
gl.canvas.width !== displayWidth ||
gl.canvas.height !== displayHeight
) {
gl.canvas.width = displayWidth;
gl.canvas.height = displayHeight;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
}
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// 更新 uniforms
gl.uniform3f(
resolutionUniformLocation,
gl.canvas.width,
gl.canvas.height,
1.0
);
gl.uniform1f(timeUniformLocation, (Date.now() - startTime) * 0.001);
gl.uniform4f(
mouseUniformLocation,
mouseX,
gl.canvas.height - mouseY,
0,
0
); // y坐标需要翻转
gl.uniform1f(
useCircleUniformLocation,
useCircleCheckbox.checked ? 1.0 : 0.0
);
// 绑定纹理
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, imageTexture);
gl.uniform1i(imageSamplerUniformLocation, 0);
// 绘制
gl.drawArrays(gl.TRIANGLES, 0, 6);
// 请求下一帧
requestAnimationFrame(render);
}
// 事件监听
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
</script>
</body>
</html>