将canvas理解成一个画板
canvas 绘图的步骤:
- 建立canvas 画布。
- 通过canvas 画布获取上下文对象,也就是画笔。
- 设置画笔颜色。
- 设置图形形状。
- 绘制图形。
<canvas id="myCanvas"></canvas>
<script>
// 1.获取画布
const canvas = document.getElementById('myCanvas');
// js设置画布大小
canvas.width = 500;
canvas.height = 500;
// 2.获取画笔
const ctx = canvas.getContext('2d');
// 获取了上下文对象后,如何用它画画?
// 使用画笔在canvas 上画画,要考虑三个方面:颜色 形状 绘图
// 方法如,绘制一个红色的矩形:
ctx.fillStyle = 'red';//CanvasRenderingContext2D.fillStyle 属性指定用于形状内部的颜色、渐变或图案。默认样式为 #000
ctx.fillRect(50, 50, 100, 100);//执行顺序很重要:设置 fillStyle 属性不会立刻改变画布上已有的图形。你需要先设置 fillStyle,后调用 fillRect() 或 fill() 方法,新值才会生效
// strokeStyle 属性指定用于形状描边(轮廓)的颜色、渐变或图案。默认值是 #000(黑色)。
ctx.strokeStyle = '#ff0000';//先设置,后描边:必须先给 strokeStyle 赋值,再调用 stroke() 或 strokeRect(),否则新颜色不生效。
ctx.beginPath();
ctx.arc(200, 200, 100, 0, Math.PI * 2);
ctx.stroke();
</script>
1. fillStyle 属性核心速览
根据 MDN 的文档,fillStyle 主要用于设置图形内部的颜色、渐变或图案。
| 属性 | 描述 | 取值示例 |
|---|---|---|
| fillStyle | 设置或返回用于填充绘画的颜色、渐变或模式。 | 颜色值:"red", "#FFA500", "rgb(255 165 0)"。渐变对象: CanvasGradient。图案对象: CanvasPattern。 |
除了 fillStyle,Canvas 还有很多其他常用的样式属性,我也一并帮你列出来了:
| 分类 | 属性/方法 | 描述 |
|---|---|---|
| 颜色/样式 | strokeStyle | 设置或返回用于笔触的颜色、渐变或模式。 |
shadowColor / shadowBlur | 设置阴影颜色与模糊级别。 | |
shadowOffsetX / Y | 设置阴影与形状的水平/垂直距离。 | |
| 线条样式 | lineWidth | 设置当前线条的宽度。 |
lineCap | 设置线条端点样式(如圆形、方形)。 | |
lineJoin | 设置两线相交拐角类型(如尖角、圆角)。 | |
| 文本样式 | font | 设置文本字体属性(同 CSS font)。 |
textAlign / textBaseline | 设置文本对齐方式与基线。 |
注意事项
- 执行顺序很重要:设置
fillStyle属性不会立刻改变画布上已有的图形。你需要先设置fillStyle,后调用fillRect()或fill()方法,新值才会生效。 - 支持的颜色格式:
fillStyle支持 CSS 颜色规范中的所有格式,包括十六进制(#FF0000)、RGB(rgb(255,0,0))、RGBA(rgba(255,0,0,0.5)半透明)以及颜色名称(red)。
2. strokeStyle 属性核心速览
strokeStyle 属性指定用于形状描边(轮廓)的颜色、渐变或图案。默认值是 #000(黑色)。
与 fillStyle 的核心区别
| 属性 | 作用对象 | 默认值 | 必须搭配的方法 |
|---|---|---|---|
fillStyle | 形状的内部填充区域 | #000000 (黑色) | fillRect(), fill() |
strokeStyle | 形状的边界轮廓线 | #000000 (黑色) | strokeRect(), stroke() |
重要注意事项
- 先设置,后描边:必须先给
strokeStyle赋值,再调用stroke()或strokeRect(),否则新颜色不生效。 - 受
lineWidth影响:描边的粗细由lineWidth属性控制,默认 1 像素。 - 性能提示:频繁改变
strokeStyle(例如在循环中)会略微影响绘制性能。如果绘制大量不同颜色的独立形状,可以按颜色分组绘制。
3.官方文档查阅
如果需要查看所有细节(包括浏览器兼容性、渐变/图案的具体用法),推荐:
-
MDN Web Docs(最权威)
CanvasRenderingContext2D.strokeStyle -
简洁示例参考
W3Schools Canvas strokeStyle
按颜色分组绘制是Canvas性能优化和代码组织中的一个重要技巧。核心思想很简单:把相同颜色的绘制操作集中在一起,避免频繁切换 fillStyle 或 strokeStyle。
4.为什么需要按颜色分组?
// ❌ 不好的做法:频繁切换颜色
for (let i = 0; i < 100; i++) {
ctx.fillStyle = colors[i % 5]; // 每画一个就换一次
ctx.fillRect(x[i], y[i], 10, 10);
}
// ✅ 好的做法:按颜色分组
// 先画所有红色
ctx.fillStyle = 'red';
for (let i of redIndexes) ctx.fillRect(...);
// 再画所有蓝色
ctx.fillStyle = 'blue';
for (let i of blueIndexes) ctx.fillRect(...);
性能差异:切换 fillStyle 在底层会触发Canvas的状态变更,批量操作可减少这个开销,特别是绘制成百上千个图形时。
实际场景示例
场景1:绘制不同颜色的散点图
// 原始数据:每个点有 x, y, color
const points = [
{ x: 10, y: 20, color: 'red' },
{ x: 30, y: 15, color: 'blue' },
{ x: 50, y: 45, color: 'red' },
{ x: 70, y: 30, color: 'green' },
{ x: 90, y: 55, color: 'blue' },
// ... 可能上百个点
];
// 按颜色分组
const groups = new Map();
points.forEach(point => {
if (!groups.has(point.color)) {
groups.set(point.color, []);
}
groups.get(point.color).push(point);
});
// 按组绘制
for (const [color, pointsOfColor] of groups) {
ctx.fillStyle = color;
pointsOfColor.forEach(point => {
ctx.fillRect(point.x, point.y, 6, 6);
});
}
场景2:绘制饼图(按扇区颜色分组天然适用)
const sectors = [
{ value: 30, color: '#FF6B6B', label: 'A' },
{ value: 20, color: '#4ECDC4', label: 'B' },
{ value: 25, color: '#45B7D1', label: 'C' },
{ value: 25, color: '#96CEB4', label: 'D' }
];
let startAngle = 0;
sectors.forEach(sector => {
const angle = (sector.value / 100) * Math.PI * 2;
ctx.fillStyle = sector.color; // 每个扇区换一次,但总共只有4次,合理
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, startAngle + angle);
ctx.closePath();
ctx.fill();
startAngle += angle;
});
场景3:带描边的图形(需要同时分组 fill 和 stroke)
// 数据:每个形状有填充色和描边色
const shapes = [
{ type: 'rect', x: 10, y: 10, fill: 'red', stroke: 'black' },
{ type: 'rect', x: 50, y: 10, fill: 'blue', stroke: 'black' },
{ type: 'rect', x: 90, y: 10, fill: 'red', stroke: 'red' },
// ...
];
// 策略:按 fill+stroke 组合分组(把组合作为key)
const groups = new Map();
shapes.forEach(shape => {
const key = `${shape.fill}|${shape.stroke}`;
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(shape);
});
// 先画所有填充(可批量),再画所有描边
for (const [key, shapesOfGroup] of groups) {
const [fill, stroke] = key.split('|');
// 批量填充
ctx.fillStyle = fill;
shapesOfGroup.forEach(shape => {
ctx.fillRect(shape.x, shape.y, 30, 30);
});
// 批量描边
ctx.strokeStyle = stroke;
shapesOfGroup.forEach(shape => {
ctx.strokeRect(shape.x, shape.y, 30, 30);
});
}
进阶:性能优化考量
什么时候分组效果明显?
| 绘制数量 | 是否分组 | 建议 |
|---|---|---|
| < 50 | 不明显 | 代码可读性优先,不用刻意分组 |
| 50-500 | 轻微提升 | 简单分组即可 |
| 500-5000 | 明显提升 | 推荐分组 |
| > 5000 | 巨大差异 | 必须分组,甚至考虑离屏Canvas |
更高级的优化:离屏Canvas + 颜色分组
// 创建离屏Canvas预绘制同色图形
function createColorLayer(color, shapes) {
const offscreen = new OffscreenCanvas(800, 600);
const offCtx = offscreen.getContext('2d');
offCtx.fillStyle = color;
shapes.forEach(shape => {
offCtx.fillRect(shape.x, shape.y, shape.w, shape.h);
});
return offscreen;
}
// 主Canvas直接贴图层
const redLayer = createColorLayer('red', redShapes);
ctx.drawImage(redLayer, 0, 0);
实际项目中的取舍
不是所有情况都要严格按颜色分组,需要考虑:
- 代码复杂度:分组逻辑是否让代码更难理解?
- 绘制顺序:如果图形有重叠且需要特定前后顺序,分组可能破坏z-order
- 动态变化:如果颜色频繁变化,维护分组的开销可能超过收益
一个实用原则:
先写清晰可读的代码,如果性能不够,用浏览器性能工具(Performance tab)定位瓶颈。如果看到大量
fillStyle切换耗时,再按颜色分组优化。
需要我针对你的具体绘制场景,写一个更贴合实际的分组绘制示例吗?