CSS 高级动效与生成艺术实战利用 WebGL 与 Canvas 打造西湖光影视觉屏住在西湖附近、常在傍晚去湖边散步的这些年里最令我震撼的视觉体验莫过于西湖水面上随微风起伏、折射着夕阳金光的自然光影。美术学院的背景让我对光影的明暗渐变、色调漫反射与波纹流动感充满偏执。而在现代 Web 前端开发中传统的 CSS 静态渐变linear-gradient或预录制的视频背景根本无法展现这种随机、自然、永远不会重复的动态光影美感。要将自然的流动感搬进网页大屏与交互界面我们需要借助Canvas 2D 与 WebGL Fragment Shader片段着色器结合Perlin Noise柏林噪声与 Simplex Noise 算法打造程序化生成艺术Generative Art。通过用数学公式去模拟水波的干涉与光线的漫反射在浏览器中绘制出如西湖水光般柔和、充满温度的生成艺术背景。噪声函数与 WebGL 渲染管线拓扑生成艺术的核心在于“打破机械的几何规则引入连续有规律的随机性”。flowchart TD ShaderInput[Uniform 变量: u_time 动态时间 u_resolution 分辨率] -- GLSL_Pipeline[WebGL 渲染管线] subgraph GPU 片段着色器 (Fragment Shader) GLSL_Pipeline -- UV_Calc[1. UV 坐标归一化: (gl_FragCoord / u_resolution)] UV_Calc -- NoiseFunc[2. 2D Simplex / Perlin 噪声梯度生成算法] NoiseFunc -- FBM[3. Fractional Brownian Motion (fBm) 分形分层合成] FBM -- ColorPalette[4. 柔和色调漫反射与 Palette 映射 (西湖夕阳金/黛青)] end ColorPalette -- FrameBuffer[帧缓冲区 FrameBuffer 物理像素输出] FrameBuffer --|GPU 60FPS 极速渲染| ScreenDisplay[西湖光影动态生成背景]1. 为什么选择 Perlin / Simplex 噪声而非Math.random()真正的Math.random()生成的是完全不连续的白噪声White Noise展示在画面上就像老旧电视机的雪花屏缺乏视觉连贯性。而Perlin 噪声产生的是平滑连续的渐变随机值。相邻的像素点在数值上平缓过渡能够完美模拟水波、云雾、火焰与光影的自然流动。2. 分形分层合成fBm, Fractional Brownian Motion单层的 Perlin 噪声显得过于平淡。通过将多个不同频率Frequency和振幅Amplitude的噪声叠加在一起称为 Octaves 叠加能够制造出宏观有波浪大趋势、微观有细小波纹褶皱的极具真实感的水光质感。生产级 HTML5 / WebGL 代码西湖光影生成艺术平滑渲染器下面是一套可以在任何现代浏览器中直接运行的生产级 WebGL / Canvas 源码。它内嵌了 GLSL 片段着色器利用平滑噪声算法在 GPU 中实时绘制光影!DOCTYPE html html langzh-CN head meta charsetUTF-8 title西湖光影生成艺术屏 - 李慕杰 (Leo)/title style * { margin: 0; padding: 0; box-sizing: border-box; } body, html { width: 100%; height: 100%; overflow: hidden; background-color: #0d1117; } canvas { display: block; width: 100vw; height: 100vh; } /style /head body canvas idgenerative-canvas/canvas script /** * 生产级 WebGL 西湖光影生成艺术渲染器 * 作者: 李慕杰 (Leo) */ const canvas document.getElementById(generative-canvas); const gl canvas.getContext(webgl) || canvas.getContext(experimental-webgl); if (!gl) { alert(您的浏览器不支持 WebGL 硬件加速); } // 1. 顶点着色器 (Vertex Shader) const vsSource attribute vec2 a_position; void main() { gl_Position vec4(a_position, 0.0, 1.0); } ; // 2. 片段着色器 (Fragment Shader - 光影噪声数学核心) const fsSource precision mediump float; uniform vec2 u_resolution; uniform float u_time; // 2D 随机伪梯度 vec2 random2(vec2 st){ st vec2( dot(st,vec2(127.1,311.7)), dot(st,vec2(269.5,183.3)) ); return -1.0 2.0*fract(sin(st)*43758.5453123); } // 2D Perlin 噪声算法 float noise(vec2 st) { vec2 i floor(st); vec2 f fract(st); vec2 u f*f*(3.0-2.0*f); // Cubic 曲线平滑插值 return mix( mix( dot( random2(i vec2(0.0,0.0) ), f - vec2(0.0,0.0) ), dot( random2(i vec2(1.0,0.0) ), f - vec2(1.0,0.0) ), u.x), mix( dot( random2(i vec2(0.0,1.0) ), f - vec2(0.0,1.0) ), dot( random2(i vec2(1.0,1.0) ), f - vec2(1.0,1.0) ), u.x), u.y); } // 分形分层叠加 (fBm) float fbm(vec2 st) { float value 0.0; float amplitude 0.5; for (int i 0; i 4; i) { value amplitude * noise(st); st * 2.0; amplitude * 0.5; } return value; } void main() { vec2 st gl_FragCoord.xy / u_resolution.xy; st.x * u_resolution.x / u_resolution.y; // 随时间流逝制作波纹水光流动 vec2 q vec2(0.0); q.x fbm( st 0.10 * u_time ); q.y fbm( st vec2(1.0) ); vec2 r vec2(0.0); r.x fbm( st 1.0 * q vec2(1.7, 9.2) 0.15 * u_time ); r.y fbm( st 1.0 * q vec2(8.3, 2.8) 0.126 * u_time ); float f fbm(st r); // 西湖夕阳光影配色: 黛青底色 ➔ 暖金光芒 ➔ 深紫水波 vec3 colorA vec3(0.05, 0.12, 0.22); // 黛青 vec3 colorB vec3(0.95, 0.58, 0.25); // 夕阳金 vec3 colorC vec3(0.35, 0.15, 0.45); // 紫霞 vec3 color mix(colorA, colorB, clamp(f*f*4.0, 0.0, 1.0)); color mix(color, colorC, clamp(length(q), 0.0, 1.0)); gl_FragColor vec4((f*f*f 0.6*f*f 0.5*f)*color, 1.0); } ; // 初始化编译 Shader 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, vsSource); const fragmentShader createShader(gl, gl.FRAGMENT_SHADER, fsSource); const program gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(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 resolutionLocation gl.getUniformLocation(program, u_resolution); const timeLocation gl.getUniformLocation(program, u_time); function resize() { canvas.width window.innerWidth; canvas.height window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); } window.addEventListener(resize, resize); resize(); // 60FPS 循环渲染 function render(time) { gl.uniform2f(resolutionLocation, canvas.width, canvas.height); gl.uniform1f(timeLocation, time * 0.001); gl.drawArrays(gl.TRIANGLES, 0, 6); requestAnimationFrame(render); } requestAnimationFrame(render); /script /body /html视觉与性能权衡Trade-offs在将生成艺术应用到 Web 商业界面中时我们需要做出如下工程权衡背景视觉实现传统静态 Image预渲染 MP4 视频背景WebGL / Canvas 生成艺术视觉品质与动态美感静态死板无动态生命力动态但会重复占用数 MB 带宽极佳无限随机、永不重复自然水光网络包体积开销500KB ~ 2MB5MB ~ 20MB (加载慢)极小仅几 KB 纯数学着色器代码GPU 硬件性能占用0%中等视频解码开销低中交由 GPU 着色器绘制主线程 0 压力用极其轻量的代码在 GPU 中实时绘制出无限自然的生成艺术光影是美学与技术最高境界的结合。总结技术可以很硬核但呈现在用户面前的界面应当带有自然的温度。搞懂 Perlin 噪声与分形分层fBm合成的数学原理掌握 WebGL 片段着色器在 GPU 中的并行渲染管线才能把西湖的光影与自然的韵律搬进网页做出兼具极高艺术美感与工程品味的前端作品。参考资料The Book of Shaders - Patricio Gonzalez Vivo Jen LoweWebGL Specification and Shader Language (GLSL) GuidePerlin Noise Algorithm and Fractal Brownian Motion
CSS 高级动效与生成艺术实战:利用 WebGL 与 Canvas 打造西湖光影视觉屏
CSS 高级动效与生成艺术实战利用 WebGL 与 Canvas 打造西湖光影视觉屏住在西湖附近、常在傍晚去湖边散步的这些年里最令我震撼的视觉体验莫过于西湖水面上随微风起伏、折射着夕阳金光的自然光影。美术学院的背景让我对光影的明暗渐变、色调漫反射与波纹流动感充满偏执。而在现代 Web 前端开发中传统的 CSS 静态渐变linear-gradient或预录制的视频背景根本无法展现这种随机、自然、永远不会重复的动态光影美感。要将自然的流动感搬进网页大屏与交互界面我们需要借助Canvas 2D 与 WebGL Fragment Shader片段着色器结合Perlin Noise柏林噪声与 Simplex Noise 算法打造程序化生成艺术Generative Art。通过用数学公式去模拟水波的干涉与光线的漫反射在浏览器中绘制出如西湖水光般柔和、充满温度的生成艺术背景。噪声函数与 WebGL 渲染管线拓扑生成艺术的核心在于“打破机械的几何规则引入连续有规律的随机性”。flowchart TD ShaderInput[Uniform 变量: u_time 动态时间 u_resolution 分辨率] -- GLSL_Pipeline[WebGL 渲染管线] subgraph GPU 片段着色器 (Fragment Shader) GLSL_Pipeline -- UV_Calc[1. UV 坐标归一化: (gl_FragCoord / u_resolution)] UV_Calc -- NoiseFunc[2. 2D Simplex / Perlin 噪声梯度生成算法] NoiseFunc -- FBM[3. Fractional Brownian Motion (fBm) 分形分层合成] FBM -- ColorPalette[4. 柔和色调漫反射与 Palette 映射 (西湖夕阳金/黛青)] end ColorPalette -- FrameBuffer[帧缓冲区 FrameBuffer 物理像素输出] FrameBuffer --|GPU 60FPS 极速渲染| ScreenDisplay[西湖光影动态生成背景]1. 为什么选择 Perlin / Simplex 噪声而非Math.random()真正的Math.random()生成的是完全不连续的白噪声White Noise展示在画面上就像老旧电视机的雪花屏缺乏视觉连贯性。而Perlin 噪声产生的是平滑连续的渐变随机值。相邻的像素点在数值上平缓过渡能够完美模拟水波、云雾、火焰与光影的自然流动。2. 分形分层合成fBm, Fractional Brownian Motion单层的 Perlin 噪声显得过于平淡。通过将多个不同频率Frequency和振幅Amplitude的噪声叠加在一起称为 Octaves 叠加能够制造出宏观有波浪大趋势、微观有细小波纹褶皱的极具真实感的水光质感。生产级 HTML5 / WebGL 代码西湖光影生成艺术平滑渲染器下面是一套可以在任何现代浏览器中直接运行的生产级 WebGL / Canvas 源码。它内嵌了 GLSL 片段着色器利用平滑噪声算法在 GPU 中实时绘制光影!DOCTYPE html html langzh-CN head meta charsetUTF-8 title西湖光影生成艺术屏 - 李慕杰 (Leo)/title style * { margin: 0; padding: 0; box-sizing: border-box; } body, html { width: 100%; height: 100%; overflow: hidden; background-color: #0d1117; } canvas { display: block; width: 100vw; height: 100vh; } /style /head body canvas idgenerative-canvas/canvas script /** * 生产级 WebGL 西湖光影生成艺术渲染器 * 作者: 李慕杰 (Leo) */ const canvas document.getElementById(generative-canvas); const gl canvas.getContext(webgl) || canvas.getContext(experimental-webgl); if (!gl) { alert(您的浏览器不支持 WebGL 硬件加速); } // 1. 顶点着色器 (Vertex Shader) const vsSource attribute vec2 a_position; void main() { gl_Position vec4(a_position, 0.0, 1.0); } ; // 2. 片段着色器 (Fragment Shader - 光影噪声数学核心) const fsSource precision mediump float; uniform vec2 u_resolution; uniform float u_time; // 2D 随机伪梯度 vec2 random2(vec2 st){ st vec2( dot(st,vec2(127.1,311.7)), dot(st,vec2(269.5,183.3)) ); return -1.0 2.0*fract(sin(st)*43758.5453123); } // 2D Perlin 噪声算法 float noise(vec2 st) { vec2 i floor(st); vec2 f fract(st); vec2 u f*f*(3.0-2.0*f); // Cubic 曲线平滑插值 return mix( mix( dot( random2(i vec2(0.0,0.0) ), f - vec2(0.0,0.0) ), dot( random2(i vec2(1.0,0.0) ), f - vec2(1.0,0.0) ), u.x), mix( dot( random2(i vec2(0.0,1.0) ), f - vec2(0.0,1.0) ), dot( random2(i vec2(1.0,1.0) ), f - vec2(1.0,1.0) ), u.x), u.y); } // 分形分层叠加 (fBm) float fbm(vec2 st) { float value 0.0; float amplitude 0.5; for (int i 0; i 4; i) { value amplitude * noise(st); st * 2.0; amplitude * 0.5; } return value; } void main() { vec2 st gl_FragCoord.xy / u_resolution.xy; st.x * u_resolution.x / u_resolution.y; // 随时间流逝制作波纹水光流动 vec2 q vec2(0.0); q.x fbm( st 0.10 * u_time ); q.y fbm( st vec2(1.0) ); vec2 r vec2(0.0); r.x fbm( st 1.0 * q vec2(1.7, 9.2) 0.15 * u_time ); r.y fbm( st 1.0 * q vec2(8.3, 2.8) 0.126 * u_time ); float f fbm(st r); // 西湖夕阳光影配色: 黛青底色 ➔ 暖金光芒 ➔ 深紫水波 vec3 colorA vec3(0.05, 0.12, 0.22); // 黛青 vec3 colorB vec3(0.95, 0.58, 0.25); // 夕阳金 vec3 colorC vec3(0.35, 0.15, 0.45); // 紫霞 vec3 color mix(colorA, colorB, clamp(f*f*4.0, 0.0, 1.0)); color mix(color, colorC, clamp(length(q), 0.0, 1.0)); gl_FragColor vec4((f*f*f 0.6*f*f 0.5*f)*color, 1.0); } ; // 初始化编译 Shader 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, vsSource); const fragmentShader createShader(gl, gl.FRAGMENT_SHADER, fsSource); const program gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(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 resolutionLocation gl.getUniformLocation(program, u_resolution); const timeLocation gl.getUniformLocation(program, u_time); function resize() { canvas.width window.innerWidth; canvas.height window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); } window.addEventListener(resize, resize); resize(); // 60FPS 循环渲染 function render(time) { gl.uniform2f(resolutionLocation, canvas.width, canvas.height); gl.uniform1f(timeLocation, time * 0.001); gl.drawArrays(gl.TRIANGLES, 0, 6); requestAnimationFrame(render); } requestAnimationFrame(render); /script /body /html视觉与性能权衡Trade-offs在将生成艺术应用到 Web 商业界面中时我们需要做出如下工程权衡背景视觉实现传统静态 Image预渲染 MP4 视频背景WebGL / Canvas 生成艺术视觉品质与动态美感静态死板无动态生命力动态但会重复占用数 MB 带宽极佳无限随机、永不重复自然水光网络包体积开销500KB ~ 2MB5MB ~ 20MB (加载慢)极小仅几 KB 纯数学着色器代码GPU 硬件性能占用0%中等视频解码开销低中交由 GPU 着色器绘制主线程 0 压力用极其轻量的代码在 GPU 中实时绘制出无限自然的生成艺术光影是美学与技术最高境界的结合。总结技术可以很硬核但呈现在用户面前的界面应当带有自然的温度。搞懂 Perlin 噪声与分形分层fBm合成的数学原理掌握 WebGL 片段着色器在 GPU 中的并行渲染管线才能把西湖的光影与自然的韵律搬进网页做出兼具极高艺术美感与工程品味的前端作品。参考资料The Book of Shaders - Patricio Gonzalez Vivo Jen LoweWebGL Specification and Shader Language (GLSL) GuidePerlin Noise Algorithm and Fractal Brownian Motion