欢迎光临
我们一直在努力

小白前端速成:5分钟搞懂Canvas填色描边(附避坑指南)

小白前端速成:5分钟搞懂Canvas填色描边(附避坑指南)

  • 小白前端速成:5分钟搞懂Canvas填色描边(附避坑指南)
    • Canvas 是啥玩意儿
    • fillStyle 和 strokeStyle 到底有啥区别
    • 颜色怎么写才不翻车
    • 渐变色其实也没那么难
      • 线性渐变实战
      • 径向渐变实战
    • 图案填充?真有人用吗
    • 为什么我改了 strokeStyle 却没生效
    • 描边宽度和对齐方式的隐藏坑
    • 实战小例子:画个带阴影的彩色按钮
    • 调试时怎么快速定位颜色问题
    • 几个提升效率的小技巧

小白前端速成:5分钟搞懂Canvas填色描边(附避坑指南)

刚入门前端的你是不是一看到 Canvas 就头大?别慌,今天咱们就拿 fillStyle 和 strokeStyle 这俩“颜料桶”开刀,手把手教你画出能发朋友圈的图形!

Canvas 是啥玩意儿

别被术语吓到,Canvas 就是你网页里的“电子画布”,想画啥都行,但得靠 JavaScript 指挥。它不像 SVG 那样自带标签属性,一切颜色、线条、形状全靠代码“手搓”。

<!DOCTYPE html>
<html>
<head>
<title>Canvas 初体验</title>
<style>
canvas {
border: 1px solid #ccc;
display: block;
margin: 20px auto;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="400" height="300"></canvas>

<script>
// 获取画布和上下文
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 画个简单的矩形
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(50, 50, 100, 80);

// 再画个描边的圆
ctx.strokeStyle = '#4ecdc4';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(250, 100, 40, 0, Math.PI * 2);
ctx.stroke();
</script>
</body>
</html>

看,就这么简单!Canvas 的核心就是获取 2D 上下文,然后各种画就完事了。

fillStyle 和 strokeStyle 到底有啥区别

简单说:fillStyle 是“填肚子”的颜色,strokeStyle 是“描边框”的颜色。一个管里,一个管外。但很多人第一次用的时候,愣是把描边当成填充,结果画出来全是空心圈,自己还纳闷“颜色呢?”

// 错误示范:以为设置了 strokeStyle 就能填充
ctx.strokeStyle = '#ff0000';
ctx.fillRect(0, 0, 100, 100); // 结果啥颜色都没有!

// 正确姿势
ctx.fillStyle = '#ff0000'; // 这才是填充颜色
ctx.fillRect(0, 0, 100, 100); // 现在才有红色填充

来,看个完整的对比:

<canvas id="styleDemo" width="500" height="200"></canvas>
<script>
const canvas = document.getElementById('styleDemo');
const ctx = canvas.getContext('2d');

// 左边:只有填充
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(50, 50, 80, 80);

// 中间:只有描边
ctx.strokeStyle = '#4ecdc4';
ctx.lineWidth = 4;
ctx.strokeRect(200, 50, 80, 80);

// 右边:填充+描边
ctx.fillStyle = '#45b7d1';
ctx.fillRect(350, 50, 80, 80);
ctx.strokeStyle = '#f39c12';
ctx.lineWidth = 4;
ctx.strokeRect(350, 50, 80, 80);
</script>

颜色怎么写才不翻车

你以为只能写 “#ff0000”?Too young!除了十六进制,还能用 rgb()、rgba()、hsl(),甚至渐变和图案。但注意啊,一旦你用了 rgba 透明色当 fillStyle,后面没清掉状态,下一个图形可能也跟着半透明——Canvas 可不会自动重置!

// 各种颜色写法演示
const colors = [
'#ff0000', // 十六进制
'red', // 颜色名
'rgb(255, 0, 0)', // RGB
'rgba(255, 0, 0, 0.5)', // RGBA带透明
'hsl(0, 100%, 50%)', // HSL
'hsla(0, 100%, 50%, 0.3)' // HSLA带透明
];

// 画一排不同颜色的矩形
colors.forEach((color, index) => {
ctx.fillStyle = color;
ctx.fillRect(index * 60 + 10, 10, 50, 50);

// 标注颜色值
ctx.fillStyle = '#333';
ctx.font = '10px Arial';
ctx.fillText(color, index * 60 + 10, 80);
});

重要提醒:rgba 的透明度陷阱!

// 错误示范:透明度会累积
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(0, 0, 100, 100);
ctx.fillRect(50, 50, 100, 100); // 重叠部分透明度变成 0.75!

// 正确做法:每次画之前重置
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(0, 0, 100, 100);
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; // 重新设置
ctx.fillRect(50, 50, 100, 100);

渐变色其实也没那么难

线性渐变(createLinearGradient)和径向渐变(createRadialGradient)听着高大上,其实就几步:创建渐变对象 → 加颜色断点 → 把它赋给 fillStyle。但新手常犯的错是坐标写反了,比如从右往左画却设成 (0,0) 到 (100,0),结果颜色方向不对,还以为 API 坏了。

线性渐变实战

// 创建线性渐变
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, '#ff6b6b'); // 起点颜色
gradient.addColorStop(0.5, '#4ecdc4'); // 中间点
gradient.addColorStop(1, '#45b7d1'); // 终点颜色

// 应用渐变
ctx.fillStyle = gradient;
ctx.fillRect(50, 50, 200, 100);

// 渐变方向演示
const verticalGradient = ctx.createLinearGradient(0, 200, 0, 300);
verticalGradient.addColorStop(0, '#ff0000');
verticalGradient.addColorStop(1, '#0000ff');
ctx.fillStyle = verticalGradient;
ctx.fillRect(300, 200, 100, 100);

径向渐变实战

// 创建径向渐变
const radialGradient = ctx.createRadialGradient(150, 250, 20, 150, 250, 80);
radialGradient.addColorStop(0, '#fff');
radialGradient.addColorStop(0.5, '#ff6b6b');
radialGradient.addColorStop(1, '#000');

ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(150, 250, 80, 0, Math.PI * 2);
ctx.fill();

避坑指南:渐变坐标一定要和实际图形匹配!

// 错误:渐变范围太小,颜色显示不全
const badGradient = ctx.createLinearGradient(0, 0, 50, 0);
badGradient.addColorStop(0, '#ff0000');
badGradient.addColorStop(1, '#0000ff');
ctx.fillStyle = badGradient;
ctx.fillRect(0, 0, 200, 100); // 只有前50px有渐变,后面全是蓝色!

// 正确:渐变范围覆盖整个图形
const goodGradient = ctx.createLinearGradient(0, 0, 200, 0);
goodGradient.addColorStop(0, '#ff0000');
goodGradient.addColorStop(1, '#0000ff');
ctx.fillStyle = goodGradient;
ctx.fillRect(0, 0, 200, 100);

图案填充?真有人用吗

当然有!用 createPattern 能把图片当"瓷砖"铺满图形。不过注意:图片必须加载完再用,否则直接报错或者画不出。建议加个 img.onload 再执行绘图逻辑,不然调试到怀疑人生。

// 创建图案填充
const img = new Image();
img.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHJlY3Qgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiBmaWxsPSIjZjBmMGYwIi8+CiAgPGNpcmNsZSBjeD0iMTAiIGN5PSIxMCIgcj0iMiIgZmlsbD0iI2NjYyIvPgo8L3N2Zz4=';

img.onload = function() {
// 创建图案,repeat 表示平铺
const pattern = ctx.createPattern(img, 'repeat');

ctx.fillStyle = pattern;
ctx.fillRect(50, 50, 200, 200);

// 也可以设置其他平铺方式
const pattern2 = ctx.createPattern(img, 'repeat-x');
ctx.fillStyle = pattern2;
ctx.fillRect(300, 50, 200, 100);
};

实战技巧:用 Canvas 自己生成图案!

// 创建一个小的离屏 Canvas 作为图案
const patternCanvas = document.createElement('canvas');
patternCanvas.width = 10;
patternCanvas.height = 10;
const pctx = patternCanvas.getContext('2d');

// 画一个简单的点状图案
pctx.fillStyle = '#fff';
pctx.fillRect(0, 0, 10, 10);
pctx.fillStyle = '#ff6b6b';
pctx.beginPath();
pctx.arc(5, 5, 2, 0, Math.PI * 2);
pctx.fill();

// 用这个 Canvas 创建图案
const pattern = ctx.createPattern(patternCanvas, 'repeat');
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 300, 300);

为什么我改了 strokeStyle 却没生效

Canvas 是"状态机"!你设置的颜色会一直保留,直到下次覆盖。所以每次画不同颜色前,务必重新赋值 fillStyle/strokeStyle。别偷懒,该写就写,不然上一个图形的颜色会"传染"给下一个。

// 错误示范:颜色被"传染"
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 50, 50);

ctx.fillRect(60, 0, 50, 50); // 还是红色,但可能你想要别的颜色!

// 正确做法:每次重新设置
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 50, 50);

ctx.fillStyle = '#00ff00'; // 明确设置新颜色
ctx.fillRect(60, 0, 50, 50);

ctx.fillStyle = '#0000ff'; // 再设置一个
ctx.fillRect(120, 0, 50, 50);

状态管理神器:save() 和 restore()

// 使用 save/restore 管理状态
ctx.fillStyle = '#ff0000';
ctx.save(); // 保存当前状态

ctx.fillStyle = '#00ff00';
ctx.fillRect(0, 0, 50, 50);

ctx.restore(); // 恢复之前保存的状态
ctx.fillRect(60, 0, 50, 50); // 现在是红色!

描边宽度和对齐方式的隐藏坑

strokeWidth(其实是 lineWidth)默认是 1px,但描边是"居中对齐"的!也就是说,如果你画一条 10px 宽的线在 x=50 的位置,实际会占 45~55 的像素区域。这在做精准布局时特别容易偏移,解决办法?要么调整坐标,要么用 save/restore 包裹局部状态。

// 描边对齐问题演示
ctx.lineWidth = 10;
ctx.strokeStyle = '#ff0000';

// 画一条理论上在 x=100 的线
ctx.beginPath();
ctx.moveTo(100, 50);
ctx.lineTo(100, 150);
ctx.stroke();

// 实际这条线占据了 95-105 的区域!
// 如果你需要精确对齐,需要调整坐标
ctx.strokeStyle = '#00ff00';
ctx.beginPath();
ctx.moveTo(100.5, 50); // 加 0.5 像素对齐
ctx.lineTo(100.5, 150);
ctx.stroke();

线帽和连接样式:

// 线帽样式
ctx.lineWidth = 20;
ctx.strokeStyle = '#ff6b6b';

// butt(默认)
ctx.lineCap = 'butt';
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(150, 50);
ctx.stroke();

// round
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(50, 100);
ctx.lineTo(150, 100);
ctx.stroke();

// square
ctx.lineCap = 'square';
ctx.beginPath();
ctx.moveTo(50, 150);
ctx.lineTo(150, 150);
ctx.stroke();

// 连接线样式
ctx.lineJoin = 'round'; // 还可以是 'miter'(默认)、'bevel'
ctx.beginPath();
ctx.moveTo(200, 50);
ctx.lineTo(250, 100);
ctx.lineTo(200, 150);
ctx.stroke();

实战小例子:画个带阴影的彩色按钮

别光看理论,来点实在的:用 fillStyle 填个圆角矩形,strokeStyle 描个细边,再加个浅灰色 shadowColor,瞬间就有 UI 感了。关键是把样式设置顺序理清楚——先设 shadow,再设 fill,最后 stroke,顺序乱了效果就崩。

function drawButton(ctx, x, y, width, height, text) {
// 保存状态
ctx.save();

// 1. 先设置阴影(在填充之前)
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 5;

// 2. 创建渐变填充
const gradient = ctx.createLinearGradient(x, y, x, y + height);
gradient.addColorStop(0, '#667eea');
gradient.addColorStop(1, '#764ba2');

// 3. 填充圆角矩形
ctx.fillStyle = gradient;
roundRect(ctx, x, y, width, height, 10);
ctx.fill();

// 4. 描边
ctx.shadowColor = 'transparent'; // 去掉阴影,不然描边也有阴影
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
roundRect(ctx, x, y, width, height, 10);
ctx.stroke();

// 5. 画文字
ctx.fillStyle = '#fff';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, x + width / 2, y + height / 2);

// 恢复状态
ctx.restore();
}

// 圆角矩形辅助函数
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height radius);
ctx.quadraticCurveTo(x + width, y + height, x + width radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}

// 使用示例
drawButton(ctx, 100, 100, 150, 50, '点击我');
drawButton(ctx, 300, 100, 150, 50, '再来一个');

调试时怎么快速定位颜色问题

打开浏览器控制台,把每一步的 fillStyle 打印出来;或者临时把图形放大十倍,看边缘是否溢出。更狠一点:用纯色背景 + 极简图形,排除干扰项。记住,Canvas 不报错不代表你画对了,可能只是"静默失败"。

// 调试辅助函数
function debugStyle(ctx, label) {
console.log(`${label} – fillStyle:`, ctx.fillStyle);
console.log(`${label} – strokeStyle:`, ctx.strokeStyle);
console.log(`${label} – globalAlpha:`, ctx.globalAlpha);
}

// 使用示例
ctx.fillStyle = '#ff0000';
debugStyle(ctx, '设置红色填充后');

ctx.globalAlpha = 0.5;
ctx.fillStyle = 'rgba(0, 255, 0, 0.8)';
debugStyle(ctx, '设置透明绿色后');

// 可视化调试:画网格
function drawDebugGrid(ctx, width, height, spacing = 50) {
ctx.save();
ctx.strokeStyle = 'rgba(200, 200, 200, 0.5)';
ctx.lineWidth = 1;

// 垂直线
for (let x = 0; x <= width; x += spacing) {
ctx.beginPath();
ctx.moveTo(x + 0.5, 0); // +0.5 像素对齐
ctx.lineTo(x + 0.5, height);
ctx.stroke();
}

// 水平线
for (let y = 0; y <= height; y += spacing) {
ctx.beginPath();
ctx.moveTo(0, y + 0.5);
ctx.lineTo(width, y + 0.5);
ctx.stroke();
}

ctx.restore();
}

// 在调试时使用
drawDebugGrid(ctx, canvas.width, canvas.height);

几个提升效率的小技巧

  • 把常用配色封装成函数,比如 getBrandFill() 返回品牌主色
  • 用 ctx.save() 和 ctx.restore() 保护绘图状态,避免全局污染
  • 别在动画循环里重复创建渐变对象,提前生成好复用
  • 想预览颜色?直接在 console.log 里输出当前 fillStyle,配合 Chrome 的颜色预览功能超方便

// 1. 颜色管理器
const ColorManager = {
brand: {
primary: '#667eea',
secondary: '#764ba2',
success: '#48bb78',
warning: '#ed8936',
error: '#f56565'
},

getBrandColor(type, alpha = 1) {
const color = this.brand[type];
if (alpha < 1) {
// 转换十六进制为 rgba
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
return color;
}
};

// 使用示例
ctx.fillStyle = ColorManager.getBrandColor('primary');
ctx.fillRect(0, 0, 100, 100);

ctx.fillStyle = ColorManager.getBrandColor('success', 0.5);
ctx.fillRect(120, 0, 100, 100);

// 2. 渐变缓存
const GradientCache = new Map();

function getCachedGradient(ctx, id, x1, y1, x2, y2, colorStops) {
const key = `${id}_${x1}_${y1}_${x2}_${y2}_${JSON.stringify(colorStops)}`;

if (!GradientCache.has(key)) {
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
colorStops.forEach(stop => {
gradient.addColorStop(stop.position, stop.color);
});
GradientCache.set(key, gradient);
}

return GradientCache.get(key);
}

// 使用示例
const gradient = getCachedGradient(ctx, 'button', 0, 0, 0, 50, [
{ position: 0, color: '#667eea' },
{ position: 1, color: '#764ba2' }
]);

// 3. 绘图状态管理器
class DrawingState {
constructor(ctx) {
this.ctx = ctx;
this.states = [];
}

push() {
this.states.push({
fillStyle: this.ctx.fillStyle,
strokeStyle: this.ctx.strokeStyle,
lineWidth: this.ctx.lineWidth,
globalAlpha: this.ctx.globalAlpha,
shadowColor: this.ctx.shadowColor,
shadowBlur: this.ctx.shadowBlur
});
}

pop() {
if (this.states.length > 0) {
const state = this.states.pop();
Object.assign(this.ctx, state);
}
}
}

// 使用示例
const state = new DrawingState(ctx);
state.push(); // 保存当前状态

// 修改各种属性
ctx.fillStyle = '#ff0000';
ctx.globalAlpha = 0.5;
// … 绘图操作

state.pop(); // 恢复之前的状态

下次老板让你画个动态进度环,你就能笑着掏出这段代码:“这不就是 fillStyle + arc + requestAnimationFrame 的事儿嘛!”

// bonus:动态进度环
function drawProgressRing(ctx, x, y, radius, progress, options = {}) {
const {
backgroundColor = '#e0e0e0',
progressColor = '#667eea',
lineWidth = 10,
shadowBlur = 0,
shadowColor = 'transparent'
} = options;

ctx.save();

// 背景圆环
ctx.strokeStyle = backgroundColor;
ctx.lineWidth = lineWidth;
ctx.shadowBlur = shadowBlur;
ctx.shadowColor = shadowColor;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.stroke();

// 进度圆环
ctx.strokeStyle = progressColor;
ctx.beginPath();
ctx.arc(x, y, radius, Math.PI / 2, Math.PI / 2 + (Math.PI * 2 * progress));
ctx.stroke();

ctx.restore();
}

// 动画示例
let progress = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

drawProgressRing(ctx, 200, 200, 80, progress, {
progressColor: ColorManager.getBrandColor('primary'),
lineWidth: 15,
shadowBlur: 10,
shadowColor: 'rgba(102, 126, 234, 0.5)'
});

// 显示百分比
ctx.fillStyle = '#333';
ctx.font = '24px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${Math.round(progress * 100)}%`, 200, 200);

if (progress < 1) {
progress += 0.01;
requestAnimationFrame(animate);
}
}

// 开始动画
animate();

好了,Canvas 的填色和描边就聊到这儿。记住:多写代码多踩坑,踩得多了自然就成大佬了!有啥问题,咱们评论区见~

在这里插入图片描述

赞(0)
未经允许不得转载:171主机测评 » 小白前端速成:5分钟搞懂Canvas填色描边(附避坑指南)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址