1.绘制

6 阅读6分钟

将canvas理解成一个画板

canvas 绘图的步骤:

  1. 建立canvas 画布。
  2. 通过canvas 画布获取上下文对象,也就是画笔。
  3. 设置画笔颜色。
  4. 设置图形形状。
  5. 绘制图形。
    <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.官方文档查阅

如果需要查看所有细节(包括浏览器兼容性、渐变/图案的具体用法),推荐:

按颜色分组绘制是Canvas性能优化和代码组织中的一个重要技巧。核心思想很简单:把相同颜色的绘制操作集中在一起,避免频繁切换 fillStylestrokeStyle

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);

实际项目中的取舍

不是所有情况都要严格按颜色分组,需要考虑:

  1. 代码复杂度:分组逻辑是否让代码更难理解?
  2. 绘制顺序:如果图形有重叠且需要特定前后顺序,分组可能破坏z-order
  3. 动态变化:如果颜色频繁变化,维护分组的开销可能超过收益

一个实用原则

先写清晰可读的代码,如果性能不够,用浏览器性能工具(Performance tab)定位瓶颈。如果看到大量 fillStyle 切换耗时,再按颜色分组优化。

需要我针对你的具体绘制场景,写一个更贴合实际的分组绘制示例吗?