Skip to content

渐变色实现原理(基于shader)

渐变色是通过在两个或多个颜色之间平滑过渡来创建视觉效果。本文将深入探讨线性渐变和径向渐变的底层实现原理。

线性渐变

线性渐变是沿着一条直线(渐变线)在颜色之间进行插值。渐变线由起点和终点定义,颜色沿着这条线进行过渡。

基于shader实现

使用 WebGL 的片段着色器(Fragment Shader)可以实现更高性能的线性渐变,特别适合大面积渲染和动画场景。

GLSL 实现:

glsl
// 片段着色器(Fragment Shader)
precision mediump float;

varying vec2 v_position;
uniform vec2 u_gradientStart; // 渐变起点
uniform vec2 u_gradientEnd; // 渐变终点
uniform vec3 u_color1; // 起始颜色
uniform vec3 u_color2; // 结束颜色

void main() {
  // 计算渐变方向向量
  vec2 gradientVec = u_gradientEnd - u_gradientStart;
  float gradientLength = length(gradientVec);
  vec2 gradientDir = normalize(gradientVec);

  // 计算当前像素相对于起点的向量
  vec2 pixelVec = v_position - u_gradientStart;

  // 计算投影:当前像素在渐变线上的位置比例
  float t = dot(pixelVec, gradientDir) / gradientLength;

  // 限制在 [0, 1] 范围内
  t = clamp(t, 0.0, 1.0);

  // 线性插值计算颜色
  vec3 color = mix(u_color1, u_color2, t);

  gl_FragColor = vec4(color, 1.0);
}
glsl
// 顶点着色器(Vertex Shader)
attribute vec2 a_position;
varying vec2 v_position;

void main() {
  v_position = a_position;
  gl_Position = vec4(a_position, 0.0, 1.0);
}

TIP

可以看到基于shader实现的渐变色与css中的渐变色产生的视觉效果是不同的,根本原因是

  • shader是在非线性空间中计算的,即sRGB空间
  • css有完整的计算流程,sRGB -> linear RGB -> sRGB

多色标实现:

对于多个色标的情况,可以使用纹理或条件判断:

glsl
// 使用纹理存储色标(推荐方式)
uniform sampler2D u_gradientTexture; // 1D 渐变纹理

void main() {
  // ... 计算 t 值 ...

  // 从纹理中采样颜色
  vec4 color = texture2D(u_gradientTexture, vec2(t, 0.5));
  gl_FragColor = color;
}
glsl
// 使用条件判断(适合少量色标)
uniform vec3 u_colors[4]; // 最多4个颜色
uniform float u_stops[4]; // 对应的位置

void main() {
  // ... 计算 t 值 ...

  vec3 color;
  if (t < u_stops[1]) {
    float localT = (t - u_stops[0]) / (u_stops[1] - u_stops[0]);
    color = mix(u_colors[0], u_colors[1], localT);
  } else if (t < u_stops[2]) {
    float localT = (t - u_stops[1]) / (u_stops[2] - u_stops[1]);
    color = mix(u_colors[1], u_colors[2], localT);
  } else {
    float localT = (t - u_stops[2]) / (u_stops[3] - u_stops[2]);
    color = mix(u_colors[2], u_colors[3], localT);
  }

  gl_FragColor = vec4(color, 1.0);
}

JavaScript 调用代码:

javascript
// 初始化 WebGL
const canvas = document.getElementById('webgl-canvas');
const gl = canvas.getContext('webgl');

// 编译着色器并创建程序(省略标准代码)
const program = createProgram(gl, vertexShaderSource, fragmentShaderSource);

// 设置渐变参数
const startLoc = gl.getUniformLocation(program, 'u_gradientStart');
const endLoc = gl.getUniformLocation(program, 'u_gradientEnd');
const color1Loc = gl.getUniformLocation(program, 'u_color1');
const color2Loc = gl.getUniformLocation(program, 'u_color2');

gl.uniform2f(startLoc, 0.0, 0.0); // 起点
gl.uniform2f(endLoc, 1.0, 0.0); // 终点(水平渐变)
gl.uniform3f(color1Loc, 1.0, 0.0, 0.0); // 红色
gl.uniform3f(color2Loc, 0.0, 0.0, 1.0); // 蓝色

// 绘制
gl.drawArrays(gl.TRIANGLES, 0, 6);

径向渐变

径向渐变从中心点向外辐射,颜色沿着半径方向进行过渡。可以创建圆形或椭圆形的渐变效果。

基于shader实现

使用片段着色器实现径向渐变可以获得更好的性能和灵活性。

基础圆形渐变(同心圆):

glsl
// 片段着色器
precision mediump float;

varying vec2 v_position;
uniform vec2 u_center; // 渐变中心
uniform float u_innerRadius; // 内半径
uniform float u_outerRadius; // 外半径
uniform vec3 u_innerColor; // 内圈颜色
uniform vec3 u_outerColor; // 外圈颜色

void main() {
  // 计算当前像素到中心的距离
  float distance = length(v_position - u_center);

  // 计算渐变位置比例
  float t = (distance - u_innerRadius) / (u_outerRadius - u_innerRadius);
  t = clamp(t, 0.0, 1.0);

  // 线性插值颜色
  vec3 color = mix(u_innerColor, u_outerColor, t);

  gl_FragColor = vec4(color, 1.0);
}

椭圆渐变:

glsl
precision mediump float;

varying vec2 v_position;
uniform vec2 u_center;
uniform vec2 u_radii; // 椭圆的 x 和 y 半径
uniform vec3 u_innerColor;
uniform vec3 u_outerColor;

void main() {
  // 归一化坐标到椭圆空间
  vec2 normalized = (v_position - u_center) / u_radii;

  // 计算椭圆距离
  float distance = length(normalized);

  // 渐变插值
  float t = clamp(distance, 0.0, 1.0);
  vec3 color = mix(u_innerColor, u_outerColor, t);

  gl_FragColor = vec4(color, 1.0);
}

多色标径向渐变(使用纹理):

glsl
precision mediump float;

varying vec2 v_position;
uniform vec2 u_center;
uniform float u_radius;
uniform sampler2D u_gradientTexture; // 1D 渐变纹理

void main() {
  // 计算距离比例
  float distance = length(v_position - u_center);
  float t = distance / u_radius;
  t = clamp(t, 0.0, 1.0);

  // 从纹理采样颜色
  vec4 color = texture2D(u_gradientTexture, vec2(t, 0.5));

  gl_FragColor = color;
}

偏心径向渐变(两圆):

这是最接近 Canvas createRadialGradient() 的实现:

glsl
precision mediump float;

varying vec2 v_position;
uniform vec2 u_start; // 起始圆心
uniform float u_startRadius; // 起始半径
uniform vec2 u_end; // 结束圆心
uniform float u_endRadius; // 结束半径
uniform vec3 u_color1;
uniform vec3 u_color2;

void main() {
  // 计算点到两个圆的关系
  vec2 startVec = v_position - u_start;
  vec2 endVec = v_position - u_end;

  float distToStart = length(startVec);
  float distToEnd = length(endVec);

  // 简化算法:基于到两个圆心距离的加权
  vec2 centerVec = u_end - u_start;
  float centerDist = length(centerVec);

  // 投影到连接两圆心的直线上
  vec2 centerDir = centerDist > 0.0 ? centerVec / centerDist : vec2(1.0, 0.0);
  float projection = dot(v_position - u_start, centerDir);
  float t = projection / centerDist;

  // 考虑半径变化
  float radiusAtT = u_startRadius + (u_endRadius - u_startRadius) * t;
  vec2 pointOnLine = u_start + centerDir * projection;
  float distToLine = length(v_position - pointOnLine);

  // 计算最终的渐变位置
  float gradientT = clamp(distToLine / radiusAtT, 0.0, 1.0);

  // 颜色插值
  vec3 color = mix(u_color1, u_color2, gradientT);

  gl_FragColor = vec4(color, 1.0);
}

高级效果 - 动态径向渐变:

glsl
precision mediump float;

varying vec2 v_position;
uniform vec2 u_center;
uniform float u_time; // 时间参数,用于动画
uniform vec3 u_colors[3];

void main() {
  float distance = length(v_position - u_center);

  // 添加动画效果
  float animatedDistance = distance + sin(u_time * 2.0) * 0.1;

  // 创建脉动效果
  float t = fract(animatedDistance * 3.0 - u_time);

  // 多色渐变
  vec3 color;
  if (t < 0.5) {
    color = mix(u_colors[0], u_colors[1], t * 2.0);
  } else {
    color = mix(u_colors[1], u_colors[2], (t - 0.5) * 2.0);
  }

  gl_FragColor = vec4(color, 1.0);
}

JavaScript 使用示例:

javascript
const gl = canvas.getContext('webgl');

// 设置 uniform 变量
const centerLoc = gl.getUniformLocation(program, 'u_center');
const innerRadiusLoc = gl.getUniformLocation(program, 'u_innerRadius');
const outerRadiusLoc = gl.getUniformLocation(program, 'u_outerRadius');
const innerColorLoc = gl.getUniformLocation(program, 'u_innerColor');
const outerColorLoc = gl.getUniformLocation(program, 'u_outerColor');

gl.uniform2f(centerLoc, 0.5, 0.5); // 中心点
gl.uniform1f(innerRadiusLoc, 0.1); // 内半径
gl.uniform1f(outerRadiusLoc, 0.5); // 外半径
gl.uniform3f(innerColorLoc, 1.0, 1.0, 1.0); // 白色
gl.uniform3f(outerColorLoc, 1.0, 0.0, 0.0); // 红色

// 渲染
gl.drawArrays(gl.TRIANGLES, 0, 6);

// 动画循环
function animate(time) {
  gl.uniform1f(gl.getUniformLocation(program, 'u_time'), time * 0.001);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

总结

Canvas vs Shader 对比:

特性Canvas APIWebGL Shader
易用性简单,API 直观复杂,需要理解 GPU 编程
性能适合中小型渲染高性能,适合大量像素
灵活性功能固定完全可定制
动画需要重绘可通过 uniform 实时更新
兼容性广泛支持需要 WebGL 支持

最佳实践:

  1. 简单场景:使用 Canvas API,代码简洁易维护
  2. 高性能需求:使用 Shader,特别是大面积渐变或实时动画
  3. 复杂效果:Shader 提供更多控制,可实现各种自定义渐变
  4. 优化技巧:使用纹理存储色标数据,避免在着色器中使用过多条件判断