Skip to content

颜色、渐变与阴影

Canvas 提供了丰富的颜色与视觉效果 API。除了基本的纯色填充和描边外,还支持线性渐变、径向渐变、圆锥渐变以及阴影效果。通过组合使用这些特性,可以创造出专业级别的视觉表现。本节将详细介绍 Canvas 中的颜色系统、渐变创建和阴影效果。

前置知识

阅读本节前,建议先了解:路径与形状绘制

基础概念

Canvas 的 fillStylestrokeStyle 属性不仅接受颜色字符串,还接受 CanvasGradientCanvasPattern 对象。这意味着你可以用渐变或图案来填充和描边图形,极大增强了视觉表现力。

颜色格式

Canvas 支持多种 CSS 颜色格式:

格式示例说明
关键词redbluetransparentCSS 颜色关键词
HEX#ff0000#f00十六进制
RGBrgb(255, 0, 0)红/绿/蓝
RGBArgba(255, 0, 0, 0.5)带透明度
HSLhsl(0, 100%, 50%)色相/饱和度/亮度
HSLAhsla(0, 100%, 50%, 0.5)HSL 带透明度
javascript
const ctx = canvas.getContext('2d');

// 所有这些写法都有效
ctx.fillStyle = 'red';
ctx.fillStyle = '#ff0000';
ctx.fillStyle = '#f00';
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillStyle = 'rgba(255, 0, 0, 0.8)';
ctx.fillStyle = 'hsl(0, 100%, 50%)';
ctx.fillStyle = 'hsla(0, 100%, 50%, 0.5)';

渐变

线性渐变:createLinearGradient()

createLinearGradient(x0, y0, x1, y1) 创建沿直线方向的渐变。

javascript
/**
 * 线性渐变
 * @param {number} x0 - 起点X
 * @param {number} y0 - 起点Y
 * @param {number} x1 - 终点X
 * @param {number} y1 - 终点Y
 */
const gradient = ctx.createLinearGradient(0, 0, 200, 0);

// 添加颜色停止点(0~1)
gradient.addColorStop(0, '#4285f4');
gradient.addColorStop(0.5, '#34a853');
gradient.addColorStop(1, '#ea4335');

ctx.fillStyle = gradient;
ctx.fillRect(10, 10, 200, 100);

径向渐变:createRadialGradient()

createRadialGradient(x0, y0, r0, x1, y1, r1) 创建从内圆到外圆的渐变。

javascript
/**
 * 径向渐变
 * @param {number} x0 - 内圆中心X
 * @param {number} y0 - 内圆中心Y
 * @param {number} r0 - 内圆半径
 * @param {number} x1 - 外圆中心X
 * @param {number} y1 - 外圆中心Y
 * @param {number} r1 - 外圆半径
 */
const gradient = ctx.createRadialGradient(100, 100, 10, 100, 100, 80);

gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.5, '#4285f4');
gradient.addColorStop(1, '#1a237e');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 200);

圆锥渐变:createConicGradient()

createConicGradient(startAngle, x, y) 创建围绕中心点的圆锥渐变(较新的 API)。

javascript
/**
 * 圆锥渐变(需要较新的浏览器)
 * @param {number} startAngle - 起始角度(弧度)
 * @param {number} x - 中心X
 * @param {number} y - 中心Y
 */
const gradient = ctx.createConicGradient(0, 150, 150);

gradient.addColorStop(0, '#ea4335');
gradient.addColorStop(0.25, '#fbbc05');
gradient.addColorStop(0.5, '#34a853');
gradient.addColorStop(0.75, '#4285f4');
gradient.addColorStop(1, '#ea4335');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 300, 300);

渐变类型对比

类型方法适用场景
线性渐变createLinearGradient()按钮、进度条、背景条纹
径向渐变createRadialGradient()光球、聚光灯、3D 效果
圆锥渐变createConicGradient()色轮、饼图、旋转效果

addColorStop() 注意事项

javascript
// 颜色停止点必须在 0~1 范围内
gradient.addColorStop(0, 'red');     // 起始点
gradient.addColorStop(1, 'blue');    // 终止点
gradient.addColorStop(0.5, 'green'); // 中间点

// 错误示例
gradient.addColorStop(-0.1, 'red');  // INDEX_SIZE_ERR
gradient.addColorStop(1.1, 'blue');  // INDEX_SIZE_ERR

// 颜色停止点必须按顺序排列
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.8, 'green');
gradient.addColorStop(0.5, 'blue');  // 错误!0.5 < 0.8

阴影效果

Canvas 支持为填充和描边添加阴影效果,通过四个阴影属性控制。

阴影属性

属性类型默认值说明
shadowColorstringrgba(0,0,0,0)阴影颜色(含透明度)
shadowBlurnumber0模糊程度
shadowOffsetXnumber0水平偏移
shadowOffsetYnumber0垂直偏移

基本阴影用法

javascript
const ctx = canvas.getContext('2d');

// 设置阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;

// 绘制带阴影的矩形
ctx.fillStyle = '#4285f4';
ctx.fillRect(50, 50, 150, 100);

// 清除阴影(避免影响后续绘制)
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;

不同阴影效果

javascript
// 1. 投影效果(向右下方偏移)
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 4;
ctx.shadowOffsetY = 4;
ctx.fillStyle = '#4285f4';
ctx.fillRect(20, 20, 120, 80);

// 2. 内发光效果(配合裁剪和反向偏移)
ctx.shadowColor = 'rgba(66, 133, 244, 0.8)';
ctx.shadowBlur = 20;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.fillStyle = '#1a237e';
ctx.fillRect(180, 20, 120, 80);

// 3. 多层阴影效果
function drawWithMultipleShadows(ctx, x, y, w, h, color) {
  const shadows = [
    { color: 'rgba(0,0,0,0.2)', blur: 2, ox: 1, oy: 1 },
    { color: 'rgba(0,0,0,0.15)', blur: 5, ox: 3, oy: 3 },
    { color: 'rgba(0,0,0,0.1)', blur: 10, ox: 6, oy: 6 },
  ];

  shadows.forEach(s => {
    ctx.save();
    ctx.shadowColor = s.color;
    ctx.shadowBlur = s.blur;
    ctx.shadowOffsetX = s.ox;
    ctx.shadowOffsetY = s.oy;
    ctx.fillStyle = color;
    ctx.fillRect(x, y, w, h);
    ctx.restore();
  });
}

实战示例

示例 1:渐变按钮

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>渐变与阴影</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>Canvas 渐变与阴影</h1>
  <canvas id="demoCanvas" width="500" height="400"></canvas>

  <script>
    const canvas = document.getElementById('demoCanvas');
    const ctx = canvas.getContext('2d');

    // === 线性渐变矩形 ===
    const linearGrad = ctx.createLinearGradient(20, 20, 200, 20);
    linearGrad.addColorStop(0, '#4285f4');
    linearGrad.addColorStop(1, '#34a853');
    ctx.fillStyle = linearGrad;
    ctx.fillRect(20, 20, 180, 60);

    // === 径向渐变圆形(光球效果) ===
    const radialGrad = ctx.createRadialGradient(300, 50, 5, 300, 50, 40);
    radialGrad.addColorStop(0, '#ffffff');
    radialGrad.addColorStop(0.3, '#fbbc05');
    radialGrad.addColorStop(1, '#e65100');
    ctx.beginPath();
    ctx.arc(300, 50, 40, 0, Math.PI * 2);
    ctx.fillStyle = radialGrad;
    ctx.fill();

    // === 带阴影的卡片 ===
    ctx.save();
    ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
    ctx.shadowBlur = 15;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 8;

    // 卡片背景
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(40, 120, 200, 140);

    ctx.restore();

    // 卡片顶部装饰条
    const cardGrad = ctx.createLinearGradient(40, 120, 240, 120);
    cardGrad.addColorStop(0, '#ea4335');
    cardGrad.addColorStop(1, '#fbbc05');
    ctx.fillStyle = cardGrad;
    ctx.fillRect(40, 120, 200, 8);

    // 卡片内容
    ctx.fillStyle = '#333';
    ctx.font = 'bold 16px sans-serif';
    ctx.fillText('卡片标题', 60, 160);
    ctx.fillStyle = '#666';
    ctx.font = '13px sans-serif';
    ctx.fillText('这是一张带有阴影和', 60, 190);
    ctx.fillText('渐变装饰的卡片', 60, 210);

    // === 圆锥渐变色轮 ===
    if (ctx.createConicGradient) {
      const conicGrad = ctx.createConicGradient(0, 400, 200);
      conicGrad.addColorStop(0, '#ea4335');
      conicGrad.addColorStop(0.17, '#fbbc05');
      conicGrad.addColorStop(0.33, '#34a853');
      conicGrad.addColorStop(0.5, '#4285f4');
      conicGrad.addColorStop(0.67, '#9c27b0');
      conicGrad.addColorStop(0.83, '#e91e63');
      conicGrad.addColorStop(1, '#ea4335');

      ctx.beginPath();
      ctx.arc(400, 200, 60, 0, Math.PI * 2);
      ctx.fillStyle = conicGrad;
      ctx.fill();

      ctx.fillStyle = '#333';
      ctx.font = '12px sans-serif';
      ctx.fillText('圆锥渐变', 370, 280);
    } else {
      ctx.fillStyle = '#999';
      ctx.font = '12px sans-serif';
      ctx.fillText('浏览器不支持圆锥渐变', 360, 200);
    }

    // === 阴影文字 ===
    ctx.save();
    ctx.shadowColor = 'rgba(66, 133, 244, 0.5)';
    ctx.shadowBlur = 10;
    ctx.fillStyle = '#4285f4';
    ctx.font = 'bold 20px sans-serif';
    ctx.fillText('阴影文字效果', 40, 360);
    ctx.restore();

    // === 标签 ===
    ctx.fillStyle = '#333';
    ctx.font = '12px sans-serif';
    ctx.fillText('线性渐变', 70, 100);
    ctx.fillText('径向渐变', 275, 100);
  </script>
</body>
</html>

示例 2:3D 按钮效果

javascript
/**
 * 绘制 3D 立体按钮
 */
function draw3DButton(ctx, x, y, width, height, text, baseColor) {
  // 按钮底部(深色部分,模拟立体感)
  const darkColor = shadeColor(baseColor, -30);
  ctx.fillStyle = darkColor;
  ctx.fillRect(x, y + 4, width, height);

  // 按钮主体渐变
  const gradient = ctx.createLinearGradient(x, y, x, y + height);
  gradient.addColorStop(0, lightenColor(baseColor, 20));
  gradient.addColorStop(0.5, baseColor);
  gradient.addColorStop(1, darkenColor(baseColor, 15));

  ctx.fillStyle = gradient;
  ctx.fillRect(x, y, width, height);

  // 高光
  const highlight = ctx.createLinearGradient(x, y, x, y + height / 2);
  highlight.addColorStop(0, 'rgba(255, 255, 255, 0.3)');
  highlight.addColorStop(1, 'rgba(255, 255, 255, 0)');

  ctx.fillStyle = highlight;
  ctx.fillRect(x, y, width, height / 2);

  // 文字
  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 14px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(text, x + width / 2, y + height / 2);
  ctx.textAlign = 'start';
  ctx.textBaseline = 'alphabetic';
}

// 颜色辅助函数
function shadeColor(color, percent) {
  const num = parseInt(color.replace('#', ''), 16);
  const amt = Math.round(2.55 * percent);
  const R = Math.min(255, Math.max(0, (num >> 16) + amt));
  const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + amt));
  const B = Math.min(255, Math.max(0, (num & 0x0000FF) + amt));
  return `rgb(${R},${G},${B})`;
}

// 使用
draw3DButton(ctx, 50, 50, 150, 40, '点击我', '#4285f4');
draw3DButton(ctx, 50, 110, 150, 40, '提交', '#34a853');
draw3DButton(ctx, 50, 170, 150, 40, '取消', '#ea4335');

示例 3:渐变背景图案

javascript
/**
 * 绘制渐变条纹图案
 */
function drawGradientStripes(ctx, x, y, width, height, color1, color2, count) {
  const stripeWidth = width / count;

  for (let i = 0; i < count; i++) {
    const gradient = ctx.createLinearGradient(
      x + i * stripeWidth, y,
      x + (i + 1) * stripeWidth, y
    );
    const t = i / (count - 1);
    gradient.addColorStop(0, t % 2 === 0 ? color1 : color2);
    gradient.addColorStop(1, t % 2 === 0 ? color2 : color1);

    ctx.fillStyle = gradient;
    ctx.fillRect(
      x + i * stripeWidth, y,
      stripeWidth, height
    );
  }
}

// 使用
drawGradientStripes(ctx, 0, 0, 400, 50, '#4285f4', '#34a853', 8);

注意事项

1. 渐变坐标与变换

渐变坐标是在变换前的"用户空间"中定义的,如果你在创建渐变后修改了变换矩阵,渐变方向会随之改变:

javascript
// 渐变方向固定(不受变换影响)
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
ctx.save();
ctx.rotate(Math.PI / 4);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 200);
ctx.restore();

2. 阴影性能开销

阴影效果非常消耗性能,在动画循环中应谨慎使用:

javascript
// 不好的做法:动画中每帧设置阴影
function animate() {
  ctx.shadowColor = 'rgba(0,0,0,0.3)';
  ctx.shadowBlur = 10;
  // ... 绘制 ...
  requestAnimationFrame(animate);
}

// 好的做法:静态阴影预渲染到离屏 Canvas
const shadowCanvas = document.createElement('canvas');
// 预渲染阴影,然后在动画中直接 drawImage

3. 透明度陷阱

shadowColor 必须设置非透明颜色,否则阴影不可见:

javascript
// 错误:没有设置阴影颜色
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillRect(50, 50, 100, 100);  // 无阴影效果

// 正确:必须设置阴影颜色
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';  // 或其他颜色

最佳实践

1. 颜色停止点规范化

javascript
/**
 * 安全的 addColorStop,确保值在 0~1 范围内
 */
function safeColorStop(gradient, offset, color) {
  const clampedOffset = Math.max(0, Math.min(1, offset));
  gradient.addColorStop(clampedOffset, color);
}

2. 渐变缓存

对于不变的渐变,应缓存渐变对象:

javascript
// 不好的做法:每帧创建新渐变
function render() {
  const grad = ctx.createLinearGradient(0, 0, 200, 0);
  grad.addColorStop(0, '#4285f4');
  grad.addColorStop(1, '#34a853');
  ctx.fillStyle = grad;
  requestAnimationFrame(render);
}

// 好的做法:创建一次,重复使用
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, '#4285f4');
gradient.addColorStop(1, '#34a853');

function render() {
  ctx.fillStyle = gradient;
  requestAnimationFrame(render);
}

3. 阴影管理工具

javascript
/**
 * 阴影管理器:安全地使用阴影
 */
class ShadowManager {
  constructor(ctx) {
    this.ctx = ctx;
    this.defaults = {
      shadowColor: 'transparent',
      shadowBlur: 0,
      shadowOffsetX: 0,
      shadowOffsetY: 0
    };
  }

  /**
   * 带阴影绘制
   */
  withShadow(drawFn, options) {
    const { shadowColor, shadowBlur, shadowOffsetX, shadowOffsetY } = {
      ...this.defaults,
      ...options
    };

    this.ctx.save();
    this.ctx.shadowColor = shadowColor;
    this.ctx.shadowBlur = shadowBlur;
    this.ctx.shadowOffsetX = shadowOffsetX;
    this.ctx.shadowOffsetY = shadowOffsetY;
    drawFn();
    this.ctx.restore();
  }
}

// 使用
const shadow = new ShadowManager(ctx);
shadow.withShadow(() => {
  ctx.fillStyle = '#4285f4';
  ctx.fillRect(50, 50, 100, 60);
}, {
  shadowColor: 'rgba(0, 0, 0, 0.3)',
  shadowBlur: 10,
  shadowOffsetX: 3,
  shadowOffsetY: 3
});

下一节

继续学习:文本渲染

参考链接