第4章:样式与颜色系统
4.1 章节概述
在前几章中,我们学习了如何绑制各种图形——矩形、圆形、路径、曲线等。但这些图形都是单色的,显得有些单调。在实际应用中,我们需要为图形添加丰富的视觉效果:漂亮的颜色、渐变、图案、阴影、各种线条样式……
本章将深入探索 Canvas 的样式系统,让你的图形从"能用"变成"好看"。
我们将学习:
- 颜色表示法:RGB、RGBA、HSL、十六进制等
- 填充与描边样式:fillStyle 和 strokeStyle 的各种用法
- 线条样式:线宽、线端、连接、虚线
- 渐变效果:线性渐变、径向渐变、锥形渐变
- 图案填充:使用图像作为填充图案
- 阴影效果:为图形添加阴影
学完本章后,你将能够创建视觉效果丰富的 Canvas 应用。
4.2 颜色表示法
在开始学习样式之前,我们需要了解 Canvas 中可以使用的各种颜色表示方法。
4.2.1 颜色的本质
在计算机图形学中,颜色通常用三原色(红、绿、蓝)的组合来表示。这称为 RGB 颜色模型。
RGB 颜色模型:
红 (Red)
│
│
●───────────── 绿 (Green)
╱
╱
蓝 (Blue)
三种原色混合可以产生任意颜色:
- 红 + 绿 = 黄
- 红 + 蓝 = 品红
- 绿 + 蓝 = 青
- 红 + 绿 + 蓝 = 白
- 无颜色 = 黑每种原色的强度通常用 0-255 的整数表示,所以一个颜色可以表示为三个数字的组合,比如 (255, 0, 0) 表示纯红色。
4.2.2 Canvas 支持的颜色格式
Canvas 的 fillStyle 和 strokeStyle 属性支持多种颜色格式:
1. 颜色名称
CSS 定义了 147 种标准颜色名称,可以直接使用:
ctx.fillStyle = 'red';
ctx.fillStyle = 'blue';
ctx.fillStyle = 'green';
ctx.fillStyle = 'coral';
ctx.fillStyle = 'dodgerblue';
ctx.fillStyle = 'mediumseagreen';
ctx.fillStyle = 'transparent'; // 完全透明常用颜色名称示例:
| 颜色名称 | 效果 | 颜色名称 | 效果 |
|---|---|---|---|
| red | 红色 | blue | 蓝色 |
| green | 绿色 | yellow | 黄色 |
| orange | 橙色 | purple | 紫色 |
| pink | 粉色 | cyan | 青色 |
| black | 黑色 | white | 白色 |
| gray | 灰色 | silver | 银色 |
2. 十六进制格式(Hexadecimal)
这是最常用的颜色表示法,格式为 #RRGGBB 或简写 #RGB:
// 完整格式:#RRGGBB
ctx.fillStyle = '#FF0000'; // 红色
ctx.fillStyle = '#00FF00'; // 绿色
ctx.fillStyle = '#0000FF'; // 蓝色
ctx.fillStyle = '#FFFFFF'; // 白色
ctx.fillStyle = '#000000'; // 黑色
ctx.fillStyle = '#4D7CFF'; // 自定义蓝色
// 简写格式:#RGB(每个字符重复一次)
ctx.fillStyle = '#F00'; // 等同于 #FF0000
ctx.fillStyle = '#0F0'; // 等同于 #00FF00
ctx.fillStyle = '#00F'; // 等同于 #0000FF
// 带透明度:#RRGGBBAA 或 #RGBA
ctx.fillStyle = '#FF000080'; // 50% 透明的红色
ctx.fillStyle = '#F008'; // 简写形式理解十六进制:
十六进制使用 0-9 和 A-F 表示数字 0-15
十进制:0 1 2 ... 9 10 11 12 13 14 15
十六进制:0 1 2 ... 9 A B C D E F
两位十六进制可以表示 0-255:
00 = 0
FF = 15 × 16 + 15 = 255
80 = 8 × 16 + 0 = 128(约50%)
#RRGGBB 的结构:
# FF 00 00
│ │ │
│ │ └── Blue(蓝色分量)
│ └── Green(绿色分量)
└── Red(红色分量)3. RGB 函数格式
使用 rgb() 函数,参数为 0-255 的整数:
ctx.fillStyle = 'rgb(255, 0, 0)'; // 红色
ctx.fillStyle = 'rgb(0, 255, 0)'; // 绿色
ctx.fillStyle = 'rgb(0, 0, 255)'; // 蓝色
ctx.fillStyle = 'rgb(128, 128, 128)'; // 灰色
// 也可以使用百分比
ctx.fillStyle = 'rgb(100%, 0%, 0%)'; // 红色
ctx.fillStyle = 'rgb(50%, 50%, 50%)'; // 灰色4. RGBA 函数格式(带透明度)
rgba() 函数在 RGB 基础上增加了 Alpha(透明度)通道:
// 最后一个参数是透明度,范围 0-1
ctx.fillStyle = 'rgba(255, 0, 0, 1)'; // 完全不透明的红色
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; // 50% 透明的红色
ctx.fillStyle = 'rgba(255, 0, 0, 0.1)'; // 90% 透明的红色
ctx.fillStyle = 'rgba(255, 0, 0, 0)'; // 完全透明
// 透明度的常见值
// 1.0 = 100% 不透明
// 0.75 = 75% 不透明
// 0.5 = 50% 不透明(半透明)
// 0.25 = 25% 不透明
// 0 = 完全透明透明度的视觉效果:
不同透明度的叠加效果:
alpha = 1.0 alpha = 0.5 alpha = 0.25
██████████ ▒▒▒▒▒▒▒▒▒▒ ░░░░░░░░░░
██████████ ▒▒▒▒▒▒▒▒▒▒ ░░░░░░░░░░
██████████ ▒▒▒▒▒▒▒▒▒▒ ░░░░░░░░░░
完全不透明 半透明 近乎透明5. HSL 格式
HSL 是另一种颜色模型,代表 Hue(色相)、Saturation(饱和度)、Lightness(亮度):
// hsl(色相, 饱和度, 亮度)
ctx.fillStyle = 'hsl(0, 100%, 50%)'; // 红色
ctx.fillStyle = 'hsl(120, 100%, 50%)'; // 绿色
ctx.fillStyle = 'hsl(240, 100%, 50%)'; // 蓝色
ctx.fillStyle = 'hsl(60, 100%, 50%)'; // 黄色
// hsla 带透明度
ctx.fillStyle = 'hsla(0, 100%, 50%, 0.5)'; // 半透明红色HSL 颜色模型详解:
色相(Hue):颜色的种类,0-360 度
0° / 360° = 红色
60° = 黄色
120° = 绿色
180° = 青色
240° = 蓝色
300° = 品红
0°(红)
│
300° ●──● 60°(黄)
(品红)│╲│
│ ╲│
240° ●──● 120°(绿)
(蓝) │
180°(青)
饱和度(Saturation):颜色的鲜艳程度,0-100%
0% = 灰色(无彩色)
50% = 较淡
100% = 最鲜艳
亮度(Lightness):颜色的明暗程度,0-100%
0% = 黑色
50% = 正常颜色
100% = 白色HSL vs RGB 的优势:
HSL 更符合人类对颜色的直觉理解,便于做颜色变换:
// 使用 HSL 创建颜色变体非常容易
// 基础颜色:蓝色
const baseHue = 210;
// 调整饱和度创建系列颜色
const colors = [
`hsl(${baseHue}, 100%, 50%)`, // 鲜艳
`hsl(${baseHue}, 70%, 50%)`, // 较淡
`hsl(${baseHue}, 40%, 50%)`, // 更淡
`hsl(${baseHue}, 10%, 50%)`, // 接近灰色
];
// 调整亮度创建明暗系列
const shades = [
`hsl(${baseHue}, 70%, 80%)`, // 浅色
`hsl(${baseHue}, 70%, 50%)`, // 正常
`hsl(${baseHue}, 70%, 30%)`, // 深色
];4.2.3 颜色工具函数
在实际开发中,我们经常需要进行颜色转换和计算:
/**
* RGB 转十六进制
*/
function rgbToHex(r, g, b) {
return '#' + [r, g, b]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
/**
* 十六进制转 RGB
*/
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
/**
* RGB 转 HSL
*/
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
/**
* 颜色混合
*/
function blendColors(color1, color2, ratio) {
const c1 = hexToRgb(color1);
const c2 = hexToRgb(color2);
return rgbToHex(
Math.round(c1.r * (1 - ratio) + c2.r * ratio),
Math.round(c1.g * (1 - ratio) + c2.g * ratio),
Math.round(c1.b * (1 - ratio) + c2.b * ratio)
);
}
/**
* 获取对比色
*/
function getContrastColor(hex) {
const rgb = hexToRgb(hex);
// 计算亮度
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
return luminance > 0.5 ? '#000000' : '#FFFFFF';
}
// 使用示例
console.log(rgbToHex(255, 128, 0)); // '#ff8000'
console.log(hexToRgb('#4D7CFF')); // { r: 77, g: 124, b: 255 }
console.log(blendColors('#FF0000', '#0000FF', 0.5)); // 紫色4.3 填充与描边样式
4.3.1 fillStyle 属性
fillStyle 设置填充图形时使用的颜色或样式:
// 设置填充颜色
ctx.fillStyle = '#4D7CFF';
// 绘制填充矩形
ctx.fillRect(50, 50, 200, 100);
// 绘制填充圆形
ctx.beginPath();
ctx.arc(300, 100, 50, 0, Math.PI * 2);
ctx.fill();fillStyle 可以接受:
- 颜色字符串:任何有效的 CSS 颜色值
- 渐变对象:
CanvasGradient(稍后详细介绍) - 图案对象:
CanvasPattern(稍后详细介绍)
4.3.2 strokeStyle 属性
strokeStyle 设置描边时使用的颜色或样式:
// 设置描边颜色
ctx.strokeStyle = '#FF6B6B';
ctx.lineWidth = 3;
// 绘制描边矩形
ctx.strokeRect(50, 50, 200, 100);
// 绘制描边圆形
ctx.beginPath();
ctx.arc(300, 100, 50, 0, Math.PI * 2);
ctx.stroke();4.3.3 样式的作用域
fillStyle 和 strokeStyle 是状态属性,一旦设置就会一直生效,直到被修改或被 restore() 恢复:
// 设置蓝色填充
ctx.fillStyle = '#4D7CFF';
ctx.fillRect(50, 50, 100, 100); // 蓝色
ctx.fillRect(200, 50, 100, 100); // 仍然是蓝色!
// 修改为红色
ctx.fillStyle = '#FF6B6B';
ctx.fillRect(350, 50, 100, 100); // 红色使用 save/restore 管理样式:
// 保存当前状态
ctx.save();
ctx.fillStyle = '#FF6B6B';
ctx.fillRect(50, 50, 100, 100);
// 恢复之前的状态
ctx.restore();
// fillStyle 恢复为之前的值4.3.4 全局透明度
globalAlpha 属性设置全局透明度,影响后续所有绑制操作:
// 默认值是 1(完全不透明)
ctx.globalAlpha = 1;
// 设置 50% 透明
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#FF0000';
ctx.fillRect(50, 50, 100, 100); // 半透明红色
// 设置 25% 透明
ctx.globalAlpha = 0.25;
ctx.fillRect(100, 100, 100, 100); // 更透明的红色
// 记得恢复
ctx.globalAlpha = 1;globalAlpha vs rgba:
| 属性 | 作用范围 | 使用场景 |
|---|---|---|
globalAlpha | 影响所有后续绘制 | 需要临时改变所有内容的透明度 |
rgba() | 只影响设置的颜色 | 需要精确控制单个颜色的透明度 |
// globalAlpha 影响所有内容
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#FF0000';
ctx.fillRect(50, 50, 100, 100); // 半透明
ctx.fillStyle = '#0000FF';
ctx.fillRect(200, 50, 100, 100); // 也是半透明
// rgba 只影响设置的颜色
ctx.globalAlpha = 1; // 恢复
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(50, 200, 100, 100); // 半透明红色
ctx.fillStyle = '#0000FF';
ctx.fillRect(200, 200, 100, 100); // 不透明蓝色4.4 线条样式
Canvas 提供了丰富的线条样式控制。
4.4.1 lineWidth - 线条宽度
lineWidth 设置线条的粗细,单位是像素:
// 不同线宽示例
const widths = [1, 2, 4, 8, 16];
widths.forEach((width, index) => {
ctx.lineWidth = width;
ctx.beginPath();
ctx.moveTo(50, 50 + index * 40);
ctx.lineTo(300, 50 + index * 40);
ctx.stroke();
// 标注
ctx.fillStyle = '#333';
ctx.font = '14px sans-serif';
ctx.fillText(`lineWidth: ${width}`, 310, 55 + index * 40);
});线宽的注意事项:
- 默认值是 1
- 可以是小数:如
0.5、1.5等 - 必须大于 0:设置为 0 或负数无效
- 线条以路径为中心扩展:一半在内,一半在外
lineWidth = 10 的线条:
路径位置(无宽度):────────────
实际渲染:
│ 5px
════════════════
│ 5px
线条向两边各扩展 lineWidth/24.4.2 lineCap - 线端样式
lineCap 控制线条端点的形状:
const caps = ['butt', 'round', 'square'];
caps.forEach((cap, index) => {
ctx.lineCap = cap;
ctx.lineWidth = 20;
ctx.strokeStyle = '#4D7CFF';
ctx.beginPath();
ctx.moveTo(100, 50 + index * 60);
ctx.lineTo(300, 50 + index * 60);
ctx.stroke();
// 显示实际线条位置
ctx.strokeStyle = '#FF6B6B';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(100, 50 + index * 60);
ctx.lineTo(300, 50 + index * 60);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#333';
ctx.font = '14px sans-serif';
ctx.fillText(cap, 320, 55 + index * 60);
});三种 lineCap 的区别:
butt(默认):
端点与线条端点齐平
──────────────────
100 300
round:
端点是半圆
●──────────────────●
90 310
(向两端各延伸 lineWidth/2)
square:
端点是方形
■──────────────────■
90 310
(向两端各延伸 lineWidth/2)什么时候使用不同的 lineCap?
| 值 | 效果 | 适用场景 |
|---|---|---|
butt | 平直端点 | 精确对齐、网格线 |
round | 圆形端点 | 手绘风格、平滑感 |
square | 方形端点 | 类似 round 但保持直角 |
4.4.3 lineJoin - 连接样式
lineJoin 控制两条线段连接处的形状:
const joins = ['miter', 'round', 'bevel'];
joins.forEach((join, index) => {
ctx.lineJoin = join;
ctx.lineWidth = 20;
ctx.strokeStyle = '#4D7CFF';
const y = 100 + index * 120;
ctx.beginPath();
ctx.moveTo(50, y);
ctx.lineTo(150, y - 50);
ctx.lineTo(250, y);
ctx.stroke();
ctx.fillStyle = '#333';
ctx.font = '14px sans-serif';
ctx.fillText(join, 270, y);
});三种 lineJoin 的区别:
miter(默认)- 尖角:
╱╲
╱ ╲
╱ ╲
round - 圆角:
╱╲
╱ ╲
( )
bevel - 斜角:
╱╲
╱__╲4.4.4 miterLimit - 斜接限制
当 lineJoin 为 miter 时,如果两条线的夹角很小,尖角会变得非常长。miterLimit 用于限制尖角的最大长度:
ctx.lineWidth = 10;
ctx.lineJoin = 'miter';
// miterLimit 的作用
// 当尖角长度 / 线宽 > miterLimit 时,自动改用 bevel
ctx.miterLimit = 10; // 默认值
// 如果尖角过长,会自动变成 bevel 样式
ctx.miterLimit = 1; // 几乎总是 bevel
ctx.miterLimit = 100; // 允许很长的尖角miterLimit 的数学含义:
╱ 尖角长度
╱
╱
───╱───
╲│╱
│
│ 线宽
miterLimit = 尖角长度 / 线宽
当这个比值超过 miterLimit 时,使用 bevel 替代 miter4.4.5 虚线 - setLineDash
setLineDash(segments) 方法设置虚线样式:
// segments 是一个数组,描述实线和空白的长度交替
// 简单虚线:10px 实线,10px 空白
ctx.setLineDash([10, 10]);
// 不同实线和空白长度:20px 实线,5px 空白
ctx.setLineDash([20, 5]);
// 复杂模式:长-短-长模式
ctx.setLineDash([20, 5, 5, 5]);
// 取消虚线(恢复实线)
ctx.setLineDash([]);虚线模式详解:
// [实线, 空白] 交替重复
// [10, 10]:
// ████████──────────████████──────────
// [20, 5]:
// ████████████████████─────████████████████████─────
// [20, 5, 5, 5]:
// ████████████████████─────█████─────████████████████████─────█████─────
// [15, 5, 5, 5, 5, 5]:
// 长-短-短 模式获取当前虚线设置:
ctx.setLineDash([10, 5]);
const currentDash = ctx.getLineDash();
console.log(currentDash); // [10, 5]4.4.6 lineDashOffset - 虚线偏移
lineDashOffset 设置虚线的起始偏移量,可用于创建动画效果:
// 静态偏移
ctx.setLineDash([10, 10]);
ctx.lineDashOffset = 5; // 虚线向后偏移 5px
// 动画效果:蚂蚁线
let offset = 0;
function animateDash() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setLineDash([10, 5]);
ctx.lineDashOffset = offset;
ctx.strokeRect(50, 50, 200, 100);
offset++;
if (offset > 15) offset = 0; // 重置
requestAnimationFrame(animateDash);
}
animateDash();蚂蚁线(Marching Ants)效果:
这种效果常用于选区边框,看起来像一群蚂蚁在边缘行走:
class MarchingAnts {
constructor(ctx) {
this.ctx = ctx;
this.offset = 0;
this.dashPattern = [4, 4];
this.speed = 0.5;
}
draw(x, y, width, height) {
const { ctx } = this;
ctx.save();
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.setLineDash(this.dashPattern);
ctx.lineDashOffset = this.offset;
ctx.strokeRect(x, y, width, height);
ctx.restore();
// 更新偏移
this.offset -= this.speed;
}
}
const ants = new MarchingAnts(ctx);
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ants.draw(100, 100, 200, 150);
requestAnimationFrame(animate);
}
animate();4.4.7 综合示例:自定义线条
/**
* 绘制带样式的线条
*/
function drawStyledLine(ctx, x1, y1, x2, y2, options = {}) {
const {
color = '#333',
width = 1,
cap = 'butt',
dash = [],
dashOffset = 0
} = options;
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.lineCap = cap;
ctx.setLineDash(dash);
ctx.lineDashOffset = dashOffset;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.restore();
}
// 实线
drawStyledLine(ctx, 50, 50, 300, 50, {
color: '#4D7CFF',
width: 3
});
// 虚线
drawStyledLine(ctx, 50, 80, 300, 80, {
color: '#FF6B6B',
width: 2,
dash: [10, 5]
});
// 点线
drawStyledLine(ctx, 50, 110, 300, 110, {
color: '#4ECDC4',
width: 3,
cap: 'round',
dash: [0, 10]
});
// 点划线
drawStyledLine(ctx, 50, 140, 300, 140, {
color: '#9B59B6',
width: 2,
dash: [20, 5, 5, 5]
});4.5 渐变效果
渐变是从一种颜色平滑过渡到另一种颜色的效果。Canvas 支持三种渐变类型。
4.5.1 线性渐变(Linear Gradient)
线性渐变沿一条直线方向变化颜色。
创建线性渐变:
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);参数定义了渐变的方向和范围:
| 参数 | 含义 |
|---|---|
| x0, y0 | 渐变起点坐标 |
| x1, y1 | 渐变终点坐标 |
添加颜色停止点:
gradient.addColorStop(offset, color);| 参数 | 含义 |
|---|---|
| offset | 位置,0-1 之间的数值 |
| color | 该位置的颜色 |
基础示例:
// 创建从左到右的渐变
const gradient = ctx.createLinearGradient(0, 0, 400, 0);
// 添加颜色停止点
gradient.addColorStop(0, '#FF6B6B'); // 起点:红色
gradient.addColorStop(1, '#4D7CFF'); // 终点:蓝色
// 使用渐变填充
ctx.fillStyle = gradient;
ctx.fillRect(50, 50, 400, 100);渐变方向图解:
// 从左到右
createLinearGradient(0, 0, 400, 0);
// ████████████████████████████████████
// 红 蓝
// 从上到下
createLinearGradient(0, 0, 0, 200);
// ████ 红
// ████
// ████
// ████
// ████ 蓝
// 对角线(左上到右下)
createLinearGradient(0, 0, 400, 200);
// 红 ╲
// ╲╲
// ╲╲
// ╲╲ 蓝多色渐变:
const rainbow = ctx.createLinearGradient(0, 0, 600, 0);
rainbow.addColorStop(0, '#FF0000'); // 红
rainbow.addColorStop(0.17, '#FF7F00'); // 橙
rainbow.addColorStop(0.33, '#FFFF00'); // 黄
rainbow.addColorStop(0.5, '#00FF00'); // 绿
rainbow.addColorStop(0.67, '#0000FF'); // 蓝
rainbow.addColorStop(0.83, '#4B0082'); // 靛
rainbow.addColorStop(1, '#9400D3'); // 紫
ctx.fillStyle = rainbow;
ctx.fillRect(50, 50, 600, 100);4.5.2 径向渐变(Radial Gradient)
径向渐变从一个圆向外扩散(或从外向内收缩)。
创建径向渐变:
const gradient = ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);| 参数 | 含义 |
|---|---|
| x0, y0, r0 | 内圆的圆心和半径 |
| x1, y1, r1 | 外圆的圆心和半径 |
基础示例:
// 创建同心圆渐变
const gradient = ctx.createRadialGradient(200, 200, 0, 200, 200, 150);
gradient.addColorStop(0, '#FF6B6B'); // 中心:红色
gradient.addColorStop(1, '#4D7CFF'); // 边缘:蓝色
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(200, 200, 150, 0, Math.PI * 2);
ctx.fill();径向渐变图解:
同心圆渐变(常见):
╭──────────╮
╱ ╭────╮ ╲
╱ ╱ ●红 ╲ ╲
│ │ 中心 │ │
╲ ╲ ╱ ╱
╲ ╰────╯ ╱ 蓝
╰──────────╯
内外圆不同心(光照效果):
╭──────────╮
╱ ●红 ╲
╱ ↘ ╲
│ ↘ │
╲ ↘ ╱ 蓝
╲ ↘ ╱
╰──────────╯创建 3D 球体效果:
function draw3DSphere(ctx, cx, cy, radius, color) {
// 计算高光位置(左上方)
const highlightX = cx - radius * 0.3;
const highlightY = cy - radius * 0.3;
// 创建径向渐变
const gradient = ctx.createRadialGradient(
highlightX, highlightY, radius * 0.1, // 内圆:高光点
cx, cy, radius // 外圆:球体边缘
);
// 从白色高光到颜色到深色
gradient.addColorStop(0, '#FFFFFF');
gradient.addColorStop(0.2, color);
gradient.addColorStop(1, darkenColor(color, 0.3));
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
}
// 辅助函数:颜色变暗
function darkenColor(hex, amount) {
const rgb = hexToRgb(hex);
return rgbToHex(
Math.round(rgb.r * (1 - amount)),
Math.round(rgb.g * (1 - amount)),
Math.round(rgb.b * (1 - amount))
);
}
// 绘制一组 3D 球体
draw3DSphere(ctx, 100, 150, 60, '#FF6B6B');
draw3DSphere(ctx, 250, 150, 60, '#4ECDC4');
draw3DSphere(ctx, 400, 150, 60, '#45B7D1');4.5.3 锥形渐变(Conic Gradient)
锥形渐变围绕中心点旋转变化颜色,类似于色轮。
注意:createConicGradient 是较新的 API,需要检查浏览器支持。
// 检查支持
if (typeof ctx.createConicGradient === 'function') {
const gradient = ctx.createConicGradient(startAngle, cx, cy);
gradient.addColorStop(0, '#FF0000');
gradient.addColorStop(0.33, '#00FF00');
gradient.addColorStop(0.66, '#0000FF');
gradient.addColorStop(1, '#FF0000'); // 回到起始颜色
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(200, 200, 100, 0, Math.PI * 2);
ctx.fill();
}锥形渐变图解:
0° (红)
│
紫 ────────●──────── 绿
│
180° (蓝)
颜色围绕中心点旋转分布手动实现锥形渐变(兼容性方案):
function drawConicGradient(ctx, cx, cy, radius, colors) {
const segmentAngle = (Math.PI * 2) / colors.length;
colors.forEach((color, index) => {
const startAngle = index * segmentAngle - Math.PI / 2;
const endAngle = startAngle + segmentAngle;
// 为每个扇形创建径向渐变,模拟平滑过渡
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, endAngle);
ctx.closePath();
ctx.fill();
});
}
// 创建色轮
const hueColors = [];
for (let i = 0; i < 360; i += 10) {
hueColors.push(`hsl(${i}, 100%, 50%)`);
}
drawConicGradient(ctx, 200, 200, 100, hueColors);4.5.4 渐变的高级技巧
1. 渐变是相对于 Canvas 坐标系的
渐变的坐标是相对于整个 Canvas 的,而不是相对于绘制的图形:
// 创建一个覆盖整个 Canvas 的渐变
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
// 绘制两个矩形,它们显示渐变的不同部分
ctx.fillRect(50, 50, 100, 100); // 显示渐变的左边部分(偏红)
ctx.fillRect(350, 50, 100, 100); // 显示渐变的右边部分(偏蓝)2. 为每个图形创建独立渐变
如果希望每个图形都有完整的渐变效果:
function fillWithGradient(ctx, x, y, width, height, colors) {
// 为这个图形创建专属渐变
const gradient = ctx.createLinearGradient(x, y, x + width, y);
colors.forEach((color, index) => {
gradient.addColorStop(index / (colors.length - 1), color);
});
ctx.fillStyle = gradient;
ctx.fillRect(x, y, width, height);
}
// 每个矩形都有完整的红到蓝渐变
fillWithGradient(ctx, 50, 50, 100, 100, ['red', 'blue']);
fillWithGradient(ctx, 200, 50, 100, 100, ['red', 'blue']);
fillWithGradient(ctx, 350, 50, 100, 100, ['red', 'blue']);3. 创建发光效果
function drawGlow(ctx, x, y, radius, color) {
// 创建多层渐变,模拟发光
const layers = 5;
for (let i = layers; i >= 0; i--) {
const layerRadius = radius * (1 + i * 0.2);
const alpha = 1 / (i + 1);
const gradient = ctx.createRadialGradient(
x, y, 0,
x, y, layerRadius
);
gradient.addColorStop(0, `rgba(${hexToRgbString(color)}, ${alpha})`);
gradient.addColorStop(1, `rgba(${hexToRgbString(color)}, 0)`);
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, layerRadius, 0, Math.PI * 2);
ctx.fill();
}
}
function hexToRgbString(hex) {
const rgb = hexToRgb(hex);
return `${rgb.r}, ${rgb.g}, ${rgb.b}`;
}
// 绘制发光效果
drawGlow(ctx, 200, 200, 50, '#4D7CFF');4.6 图案填充
除了纯色和渐变,Canvas 还支持使用图像作为填充图案。
4.6.1 createPattern 方法
const pattern = ctx.createPattern(image, repetition);| 参数 | 说明 |
|---|---|
| image | 图像源(Image、Canvas、Video 等) |
| repetition | 重复方式 |
重复方式选项:
| 值 | 效果 |
|---|---|
'repeat' | 水平和垂直方向都重复(默认) |
'repeat-x' | 只在水平方向重复 |
'repeat-y' | 只在垂直方向重复 |
'no-repeat' | 不重复 |
4.6.2 使用图像作为图案
// 加载图像
const img = new Image();
img.onload = function() {
// 创建图案
const pattern = ctx.createPattern(img, 'repeat');
// 使用图案填充
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
img.src = 'pattern.png';完整示例:
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
async function drawWithPattern() {
const img = await loadImage('texture.png');
const pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
// 填充矩形
ctx.fillRect(50, 50, 300, 200);
// 填充圆形
ctx.beginPath();
ctx.arc(500, 150, 100, 0, Math.PI * 2);
ctx.fill();
}
drawWithPattern();4.6.3 使用 Canvas 作为图案
可以用另一个 Canvas 作为图案源,这允许创建动态图案:
// 创建图案 Canvas
function createPatternCanvas(size, color1, color2) {
const patternCanvas = document.createElement('canvas');
patternCanvas.width = size;
patternCanvas.height = size;
const pCtx = patternCanvas.getContext('2d');
// 绘制棋盘格图案
pCtx.fillStyle = color1;
pCtx.fillRect(0, 0, size, size);
pCtx.fillStyle = color2;
pCtx.fillRect(0, 0, size / 2, size / 2);
pCtx.fillRect(size / 2, size / 2, size / 2, size / 2);
return patternCanvas;
}
// 使用图案
const checkerCanvas = createPatternCanvas(20, '#FFFFFF', '#CCCCCC');
const pattern = ctx.createPattern(checkerCanvas, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(50, 50, 300, 200);4.6.4 图案变换
图案可以通过 setTransform 方法进行变换:
const img = await loadImage('texture.png');
const pattern = ctx.createPattern(img, 'repeat');
// 旋转图案
const matrix = new DOMMatrix();
matrix.rotateSelf(45); // 旋转 45 度
pattern.setTransform(matrix);
ctx.fillStyle = pattern;
ctx.fillRect(50, 50, 300, 200);4.6.5 实用图案生成器
/**
* 创建条纹图案
*/
function createStripePattern(ctx, width, color1, color2, angle = 0) {
const patternCanvas = document.createElement('canvas');
patternCanvas.width = width * 2;
patternCanvas.height = width * 2;
const pCtx = patternCanvas.getContext('2d');
pCtx.fillStyle = color1;
pCtx.fillRect(0, 0, width * 2, width * 2);
pCtx.fillStyle = color2;
pCtx.fillRect(0, 0, width, width * 2);
const pattern = ctx.createPattern(patternCanvas, 'repeat');
if (angle !== 0) {
const matrix = new DOMMatrix();
matrix.rotateSelf(angle);
pattern.setTransform(matrix);
}
return pattern;
}
/**
* 创建圆点图案
*/
function createDotPattern(ctx, spacing, radius, bgColor, dotColor) {
const patternCanvas = document.createElement('canvas');
patternCanvas.width = spacing;
patternCanvas.height = spacing;
const pCtx = patternCanvas.getContext('2d');
pCtx.fillStyle = bgColor;
pCtx.fillRect(0, 0, spacing, spacing);
pCtx.fillStyle = dotColor;
pCtx.beginPath();
pCtx.arc(spacing / 2, spacing / 2, radius, 0, Math.PI * 2);
pCtx.fill();
return ctx.createPattern(patternCanvas, 'repeat');
}
/**
* 创建网格图案
*/
function createGridPattern(ctx, size, lineWidth, bgColor, lineColor) {
const patternCanvas = document.createElement('canvas');
patternCanvas.width = size;
patternCanvas.height = size;
const pCtx = patternCanvas.getContext('2d');
pCtx.fillStyle = bgColor;
pCtx.fillRect(0, 0, size, size);
pCtx.strokeStyle = lineColor;
pCtx.lineWidth = lineWidth;
pCtx.beginPath();
pCtx.moveTo(size, 0);
pCtx.lineTo(size, size);
pCtx.moveTo(0, size);
pCtx.lineTo(size, size);
pCtx.stroke();
return ctx.createPattern(patternCanvas, 'repeat');
}
// 使用示例
ctx.fillStyle = createStripePattern(ctx, 10, '#4D7CFF', '#FFFFFF', 45);
ctx.fillRect(50, 50, 150, 150);
ctx.fillStyle = createDotPattern(ctx, 20, 4, '#FFFFFF', '#FF6B6B');
ctx.fillRect(220, 50, 150, 150);
ctx.fillStyle = createGridPattern(ctx, 20, 1, '#F5F5F5', '#CCCCCC');
ctx.fillRect(390, 50, 150, 150);4.7 阴影效果
Canvas 可以为绘制的图形添加阴影效果。
4.7.1 阴影属性
| 属性 | 说明 | 默认值 |
|---|---|---|
shadowColor | 阴影颜色 | 'rgba(0, 0, 0, 0)'(透明) |
shadowBlur | 模糊程度 | 0 |
shadowOffsetX | 水平偏移 | 0 |
shadowOffsetY | 垂直偏移 | 0 |
4.7.2 基础阴影
// 设置阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; // 半透明黑色
ctx.shadowBlur = 10; // 模糊半径
ctx.shadowOffsetX = 5; // 水平偏移
ctx.shadowOffsetY = 5; // 垂直偏移
// 绘制带阴影的矩形
ctx.fillStyle = '#4D7CFF';
ctx.fillRect(50, 50, 200, 100);
// 注意:阴影会影响后续所有绘制
// 需要重置阴影
ctx.shadowColor = 'transparent';
// 或
ctx.shadowBlur = 0;4.7.3 阴影参数详解
shadowBlur - 模糊程度:
// 不同模糊程度的效果
const blurs = [0, 5, 10, 20, 40];
blurs.forEach((blur, index) => {
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = blur;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#4D7CFF';
ctx.fillRect(50 + index * 120, 50, 80, 80);
// 重置
ctx.shadowColor = 'transparent';
// 标注
ctx.fillStyle = '#333';
ctx.font = '12px sans-serif';
ctx.fillText(`blur: ${blur}`, 50 + index * 120, 160);
});shadowBlur 效果对比:
blur: 0 blur: 5 blur: 10 blur: 20
┌────┐ ┌────┐ ┌────┐ ┌────┐
│ │▌ │ │░░ │ │▒▒▒ │ │▓▓▓▓
│ │▌ │ │░░ │ │▒▒▒ │ │▓▓▓▓
└────┘▌ └────┘░░ └────┘▒▒▒ └────┘▓▓▓▓
▀▀▀▀▀ ░░░░░░ ▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓
锐利边缘 轻微模糊 中等模糊 大幅模糊shadowOffset - 偏移方向:
// 不同偏移方向的效果
// 右下阴影(常见)
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
// 左下阴影
ctx.shadowOffsetX = -5;
ctx.shadowOffsetY = 5;
// 右上阴影
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = -5;
// 四周阴影(偏移为 0,只有模糊)
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 15;4.7.4 彩色阴影
阴影不一定是黑色的,可以使用任何颜色:
function drawColorfulShadow(ctx, x, y, size, color) {
ctx.shadowColor = color;
ctx.shadowBlur = 20;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 10;
ctx.fillStyle = color;
ctx.fillRect(x, y, size, size);
ctx.shadowColor = 'transparent';
}
drawColorfulShadow(ctx, 50, 50, 100, '#FF6B6B');
drawColorfulShadow(ctx, 200, 50, 100, '#4ECDC4');
drawColorfulShadow(ctx, 350, 50, 100, '#FFD93D');4.7.5 内阴影效果
Canvas 原生不支持内阴影,但可以通过裁剪技巧模拟:
function drawInnerShadow(ctx, x, y, width, height, shadowBlur, shadowColor) {
ctx.save();
// 创建裁剪区域
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.clip();
// 绘制大于裁剪区域的阴影
ctx.shadowColor = shadowColor;
ctx.shadowBlur = shadowBlur;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
// 绘制一个包围裁剪区域的路径
ctx.beginPath();
ctx.rect(x - shadowBlur * 2, y - shadowBlur * 2,
width + shadowBlur * 4, shadowBlur * 2); // 上
ctx.rect(x - shadowBlur * 2, y + height,
width + shadowBlur * 4, shadowBlur * 2); // 下
ctx.rect(x - shadowBlur * 2, y,
shadowBlur * 2, height); // 左
ctx.rect(x + width, y,
shadowBlur * 2, height); // 右
ctx.fillStyle = shadowColor;
ctx.fill();
ctx.restore();
}
// 使用
ctx.fillStyle = '#F5F5F5';
ctx.fillRect(50, 50, 200, 100);
drawInnerShadow(ctx, 50, 50, 200, 100, 15, 'rgba(0,0,0,0.3)');4.7.6 阴影性能考虑
阴影渲染是相对耗费性能的操作。在高频绑制场景(如动画)中需要注意:
// 不推荐:每帧都绘制阴影
function badAnimate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 20;
ctx.fillRect(x, y, 100, 100); // 每帧计算阴影
requestAnimationFrame(badAnimate);
}
// 推荐:预渲染带阴影的图形到离屏 Canvas
const shadowCache = document.createElement('canvas');
shadowCache.width = 140; // 100 + 阴影范围
shadowCache.height = 140;
const sCtx = shadowCache.getContext('2d');
sCtx.shadowColor = 'rgba(0,0,0,0.5)';
sCtx.shadowBlur = 20;
sCtx.fillStyle = '#4D7CFF';
sCtx.fillRect(20, 20, 100, 100);
function goodAnimate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(shadowCache, x - 20, y - 20); // 直接绘制缓存
requestAnimationFrame(goodAnimate);
}4.8 综合实战
4.8.1 创建按钮组件
function drawButton(ctx, x, y, width, height, text, options = {}) {
const {
bgColor = '#4D7CFF',
textColor = '#FFFFFF',
borderRadius = 8,
shadow = true,
gradient = true
} = options;
ctx.save();
// 阴影
if (shadow) {
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 10;
ctx.shadowOffsetY = 4;
}
// 创建圆角路径
roundRect(ctx, x, y, width, height, borderRadius);
// 渐变背景
if (gradient) {
const grad = ctx.createLinearGradient(x, y, x, y + height);
grad.addColorStop(0, lightenColor(bgColor, 0.1));
grad.addColorStop(1, bgColor);
ctx.fillStyle = grad;
} else {
ctx.fillStyle = bgColor;
}
ctx.fill();
// 重置阴影
ctx.shadowColor = 'transparent';
// 文字
ctx.fillStyle = textColor;
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, x + width / 2, y + height / 2);
ctx.restore();
}
// 辅助函数
function lightenColor(hex, amount) {
const rgb = hexToRgb(hex);
return rgbToHex(
Math.min(255, Math.round(rgb.r + (255 - rgb.r) * amount)),
Math.min(255, Math.round(rgb.g + (255 - rgb.g) * amount)),
Math.min(255, Math.round(rgb.b + (255 - rgb.b) * amount))
);
}
// 使用
drawButton(ctx, 50, 50, 120, 44, '确定');
drawButton(ctx, 190, 50, 120, 44, '取消', { bgColor: '#FF6B6B' });
drawButton(ctx, 330, 50, 120, 44, '禁用', { bgColor: '#CCCCCC', shadow: false });4.8.2 创建卡片组件
function drawCard(ctx, x, y, width, height, options = {}) {
const {
backgroundColor = '#FFFFFF',
shadowColor = 'rgba(0, 0, 0, 0.1)',
shadowBlur = 20,
borderRadius = 12,
title = '',
content = ''
} = options;
ctx.save();
// 卡片阴影
ctx.shadowColor = shadowColor;
ctx.shadowBlur = shadowBlur;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 4;
// 卡片背景
roundRect(ctx, x, y, width, height, borderRadius);
ctx.fillStyle = backgroundColor;
ctx.fill();
// 重置阴影
ctx.shadowColor = 'transparent';
// 标题
if (title) {
ctx.fillStyle = '#333';
ctx.font = 'bold 18px sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(title, x + 20, y + 20);
// 分割线
ctx.strokeStyle = '#EEEEEE';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x + 20, y + 50);
ctx.lineTo(x + width - 20, y + 50);
ctx.stroke();
}
// 内容
if (content) {
ctx.fillStyle = '#666';
ctx.font = '14px sans-serif';
wrapText(ctx, content, x + 20, y + 70, width - 40, 20);
}
ctx.restore();
}
// 文本换行
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split('');
let line = '';
let currentY = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i];
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && i > 0) {
ctx.fillText(line, x, currentY);
line = words[i];
currentY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, currentY);
}
// 使用
drawCard(ctx, 50, 50, 300, 200, {
title: '卡片标题',
content: '这是卡片的内容区域,可以显示一些描述文字。支持自动换行功能。'
});4.8.3 创建数据可视化图表
function drawPieChart(ctx, cx, cy, radius, data, options = {}) {
const {
showLabels = true,
showLegend = true,
legendX = cx + radius + 50,
legendY = cy - radius
} = options;
const total = data.reduce((sum, item) => sum + item.value, 0);
let currentAngle = -Math.PI / 2;
data.forEach((item, index) => {
const sliceAngle = (item.value / total) * Math.PI * 2;
// 绘制扇形
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
ctx.shadowBlur = 5;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, currentAngle, currentAngle + sliceAngle);
ctx.closePath();
ctx.fillStyle = item.color;
ctx.fill();
ctx.restore();
// 标签
if (showLabels) {
const labelAngle = currentAngle + sliceAngle / 2;
const labelRadius = radius * 0.7;
const labelX = cx + Math.cos(labelAngle) * labelRadius;
const labelY = cy + Math.sin(labelAngle) * labelRadius;
const percentage = Math.round(item.value / total * 100);
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${percentage}%`, labelX, labelY);
}
currentAngle += sliceAngle;
});
// 图例
if (showLegend) {
data.forEach((item, index) => {
const y = legendY + index * 25;
// 颜色块
ctx.fillStyle = item.color;
ctx.fillRect(legendX, y, 16, 16);
// 文字
ctx.fillStyle = '#333';
ctx.font = '14px sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(item.label, legendX + 24, y + 8);
});
}
}
// 使用
const pieData = [
{ label: '产品A', value: 35, color: '#FF6B6B' },
{ label: '产品B', value: 25, color: '#4ECDC4' },
{ label: '产品C', value: 20, color: '#45B7D1' },
{ label: '产品D', value: 15, color: '#96CEB4' },
{ label: '其他', value: 5, color: '#CCCCCC' }
];
drawPieChart(ctx, 200, 200, 120, pieData);4.9 本章小结
本章详细介绍了 Canvas 的样式系统:
核心知识
| 主题 | 要点 |
|---|---|
| 颜色格式 | 颜色名、十六进制、rgb()、rgba()、hsl()、hsla() |
| 填充/描边 | fillStyle、strokeStyle、globalAlpha |
| 线条样式 | lineWidth、lineCap、lineJoin、miterLimit |
| 虚线 | setLineDash()、lineDashOffset |
| 渐变 | 线性、径向、锥形渐变 |
| 图案 | createPattern、重复模式、变换 |
| 阴影 | shadowColor、shadowBlur、shadowOffset |
颜色选择建议
| 场景 | 推荐格式 |
|---|---|
| 简单颜色 | 十六进制 #RRGGBB |
| 需要透明度 | rgba() |
| 颜色计算/动画 | hsl() / hsla() |
| CSS 兼容 | 任意格式 |
性能提示
- 阴影开销大:尽量使用离屏 Canvas 缓存
- 渐变复用:相同渐变可以复用
- 图案缓存:使用 Canvas 作为图案源可动态更新
4.10 练习题
基础练习
创建一个颜色选择器,显示 HSL 色轮
绘制一组不同线条样式的示例图
创建一个渐变按钮,hover 时颜色变化
进阶练习
实现一个卡片组件:
- 支持阴影
- 支持渐变背景
- 支持圆角
创建一个图案生成器:
- 支持条纹、圆点、网格
- 支持自定义颜色和大小
挑战练习
- 实现一个完整的饼图组件:
- 支持动画效果
- 支持点击交互
- 支持图例和标签
下一章预告:在第5章中,我们将学习 Canvas 的变换系统——平移、旋转、缩放和矩阵变换,这是创建复杂动画和交互的基础。
文档版本:v1.0
字数统计:约 18,000 字
代码示例:50+ 个
