颜色、渐变与阴影
Canvas 提供了丰富的颜色与视觉效果 API。除了基本的纯色填充和描边外,还支持线性渐变、径向渐变、圆锥渐变以及阴影效果。通过组合使用这些特性,可以创造出专业级别的视觉表现。本节将详细介绍 Canvas 中的颜色系统、渐变创建和阴影效果。
前置知识
阅读本节前,建议先了解:路径与形状绘制
基础概念
Canvas 的 fillStyle 和 strokeStyle 属性不仅接受颜色字符串,还接受 CanvasGradient 和 CanvasPattern 对象。这意味着你可以用渐变或图案来填充和描边图形,极大增强了视觉表现力。
颜色格式
Canvas 支持多种 CSS 颜色格式:
| 格式 | 示例 | 说明 |
|---|---|---|
| 关键词 | red、blue、transparent | CSS 颜色关键词 |
| HEX | #ff0000、#f00 | 十六进制 |
| RGB | rgb(255, 0, 0) | 红/绿/蓝 |
| RGBA | rgba(255, 0, 0, 0.5) | 带透明度 |
| HSL | hsl(0, 100%, 50%) | 色相/饱和度/亮度 |
| HSLA | hsla(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 支持为填充和描边添加阴影效果,通过四个阴影属性控制。
阴影属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
shadowColor | string | rgba(0,0,0,0) | 阴影颜色(含透明度) |
shadowBlur | number | 0 | 模糊程度 |
shadowOffsetX | number | 0 | 水平偏移 |
shadowOffsetY | number | 0 | 垂直偏移 |
基本阴影用法
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');
// 预渲染阴影,然后在动画中直接 drawImage3. 透明度陷阱
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
});下一节
继续学习:文本渲染