Canvas这玩意儿,很多前端新人刚接触时都会觉得,不就是个画图的标签吗?真到自己动手,画个矩形都手抖,更别提画星星了。但你真要能把一面蓝底金星的旗子从零画出来,那些什么坐标运算、绘图路径、像素适配的底层逻辑,基本就通透了。
这篇就从数学原理到代码落地,一步步拆解怎么用Canvas复刻一面经典蓝底金星徽章。适合刚学完JavaScript基础、想找实战练手,或者做可视化图表时被各种多边形折腾过的同学。我会把坐标怎么算、角度为什么老是偏、画出来为什么发虚这些常见坑,全部摆出来说清楚。
1. 整体设计与思路拆解
1.1 为什么选Canvas而不是SVG或直接贴图
先聊个很多人纠结的问题:画一个图形,用CSS、SVG、Canvas,甚至直接放一张图片,哪个更合适?
如果只是要一个展示用的成品图,直接切一张PNG当然最快,但那样你什么都学不到,后期想改颜色、改星星数量、做动效,全部得重新切图。SVG虽然也是矢量方案,但操作路径偏DOM思维,后续上动画往往依赖外部库,不如Canvas的像素级控制来得直接。
这次练手用Canvas的理由很单纯:第一,API足够底层,从绘制矩形、圆弧到多边形路径,每个步骤都需要自己算坐标,正好把图形学基础补上;第二,2D上下文(CanvasRenderingContext2D)的绘图状态机模型,适合讲清楚填充、描边、路径闭合这些概念的细节,这些都是做H5图表、游戏、图像处理的基本功。
1.2 蓝底金星图形的几何拆解
先把目标图形拆成可操作的几何元素。这面旗子或者叫徽章的图案,视觉上就三块:一块蓝色矩形背景、一个中心的视觉焦点、一圈均匀分布的五角星。
你要是只盯着“画星星”看,很容易卡住。换个思路:把环状分布的星星,抽象成“在一个半径固定的圆上,每隔相等角度取一个圆心点,然后在这个点上画一枚统一朝向的五角星”。这就变成了两个标准的子问题:
- 在圆上等分取点,需要用到三角函数,算出每个点的
x和y坐标。 - 在指定坐标处画一枚正五角星,需要确定五角星的5个外顶点和5个内顶点的坐标顺序。
这两个子问题一旦独立解决,组合起来就只是一层循环的事。
另外底色那个蓝色也不是正蓝,它带一点深沉的藏青调子;金色星星也不是明黄,而是偏亮的金黄。颜色参数看起来是小事,但真影响成品观感,后文会给出推荐值。
1.3 最终效果定位
这一版的目标效果是:画布长宽比2比3,深蓝背景,十二颗五角星沿中心圆环均匀分布,星星外套一个金色细圆环。为什么是十二颗星和圆环?纯从构图讲,十二等分圆周在视觉上最稳定,五角星的尖角朝外,再加上一个外圈圆环可以把散落的星星“收拢”成一个整体,视觉重心更集中。
实现语言只用原生JavaScript和Canvas 2D API,不引任何框架。整段代码压缩下来,核心逻辑不到一百行,适合当作模板反复改造。
2. 画布初始化与配色:别小看第一行代码
2.1 画布尺寸和高分辨率适配
很多教程让你直接写死canvas width="300" height="200",练手没问题,但一旦放到真实页面上,在高分屏(比如Retina屏)上会糊成一团,原因是画布的逻辑尺寸和物理像素不一致。
标准做法是三步:
// 逻辑尺寸,比如要一个 600x900 的画布(2:3) const logicalWidth = 900; const logicalHeight = 600; // 实际像素 = 逻辑尺寸 * 设备像素比 const dpr = window.devicePixelRatio || 1; canvas.width = logicalWidth * dpr; canvas.height = logicalHeight * dpr; // 关键!让绘图坐标重新映射回逻辑尺寸 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); canvas.style.width = logicalWidth + 'px'; canvas.style.height = logicalHeight + 'px';这里最容易漏的是setTransform这一步。如果你只设置了宽高不给ctx做变换,后续写坐标时就会特别割裂:物理像素是1200x1800,但你脑子里想的是900x600。先做变换之后,后面所有绘制的坐标直接按逻辑尺寸写就行。
注意:
ctx.setTransform会在每次调用时重置之前的变换,如果你之前用过translate或scale,调用顺序要在画每类图形之前统一规划好。
2.2 颜色参数:蓝色和金色的推荐值
常见的蓝色徽章背景,不是#0000FF那种纯蓝,纯蓝太艳,压不住金色的星星。推荐使用#003399附近的值,沉稳且对比度高。
金色推荐#FFCC00这一档。这个色相偏暖黄,在深蓝底上显得亮而不刺眼。再搭配同色系的描边,整体协调性会好很多。
| 用途 | 推荐色值 | 说明 |
|---|---|---|
| 背景蓝 | #003399 | 深蓝,传统徽章质感 |
| 主星金 | #FFCC00 | 明亮金黄,对比强 |
| 圆环金 | #FFCC00 | 与星星同色,整体统一 |
| 底边(可选) | #FFD700 | 比主色调更柔和的备选 |
实际调试时,可以先用亮色把辅助线画出来,确认几何结构对了再换成正式配色。这个技巧能省下不少反复改色的时间。
2.3 矩形的绘制与状态保存
画背景是最简单的部分,但有个原则必须讲:在填充整个画布之前,先想清楚你到底要清空的是整个画布,还是只重绘某一个区域。新手最常见的错误是每次刷新忘记clearRect,导致新旧图形叠在一起。
// 用背景色铺满整个画布,同时起到清屏作用 ctx.fillStyle = '#003399'; ctx.fillRect(0, 0, logicalWidth, logicalHeight);如果你后面需要绘制半透明叠加层,记得用ctx.save()和ctx.restore()把绘制状态包裹起来,否则颜色、线宽、变换参数会一路串到后面的代码里。这个习惯越早养成越好。
3. 星形数学:从圆上取点到五角星闭环
3.1 弧度制与角度制的一次彻底了断
Canvas的Math.sin、Math.cos、ctx.arc,统统使用弧度制。你脑子里习惯的是0到360度,代码里却写的是0到2π。很多星形错乱问题,根源就是这里。
一个角度制转弧度的公式只需要记死:
const radian = degree * Math.PI / 180;这次绘制十二颗星,如果从0度开始均匀分布,每颗星之间的角度间隔是:
360 / 12 = 30(度)也就是每转30度,画一颗星。
3.2 圆上均匀取点的坐标公式
圆心坐标用(centerX, centerY)表示,圆半径是ringRadius。那么第i颗星的中心点坐标就是:
const angle = (i * 30) * Math.PI / 180; const starCenterX = centerX + ringRadius * Math.cos(angle); const starCenterY = centerY + ringRadius * Math.sin(angle);这里还有一个新手经常踩的坑:默认0度在正右方,角度增加方向是顺时针。代码写出来之后,星星会从右边开始排列,而不是从正上方。如果想要从正上方开始,把起始角改成-90度即可:
const angle = (-90 + i * 30) * Math.PI / 180;千万别小看这个起始角偏移,很多教程代码跑出来星星位置不对,就是少了这一步“约定”。
3.3 五角星的顶点坐标计算
在圆心(starCenterX, starCenterY)处画一个五角星,可以看成由10个顶点围成的封闭多边形:5个外顶点在外接圆上,5个内顶点在内接圆上。
设外接圆半径为outerR,内接圆半径为innerR,通常取innerR = outerR * 0.382(这个比例就是五角星轮廓里内凹深度的近似值)。每个顶点的角度偏移以-90度为起点,这样星形朝上。
function drawStar(ctx, cx, cy, outerR, innerR) { const points = 5; const step = Math.PI / points; // 每两个顶点间的夹角是 pi/5 ctx.beginPath(); for (let i = 0; i < points * 2; i++) { const radius = i % 2 === 0 ? outerR : innerR; const angle = -Math.PI / 2 + i * step; const x = cx + radius * Math.cos(angle); const y = cy + radius * Math.sin(angle); if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.closePath(); ctx.fill(); }这里step写成Math.PI / points是关键中的关键。因为五角星的10个顶点是均匀分布的,相邻两个顶点之间的夹角是2π / 10 = π / 5,不可能用2π / 5,写错的话画出来就是一个五边形而不是五角星。
3.4 把12颗星画到圆环上
现在把两个子问题拼起来,主循环只需要一个for循环:
const starCount = 12; const ringRadius = 180; const starOuterR = 40; const starInnerR = starOuterR * 0.382; for (let i = 0; i < starCount; i++) { const angle = (-90 + i * 30) * Math.PI / 180; const starCenterX = centerX + ringRadius * Math.cos(angle); const starCenterY = centerY + ringRadius * Math.sin(angle); drawStar(ctx, starCenterX, starCenterY, starOuterR, starInnerR); }跑完这段,你就能看到12颗尖角向外的五角星整整齐齐围成一圈了。
4. 实操过程:从辅助线到成品
4.1 先画辅助线,再上正式图形
我这人调试图形有个习惯:第一次跑通功能之前,绝不直接上最终颜色。第一步全是半透明的灰线、辅助圆,先保证几何位置完全正确。
// 辅助:画一条水平中线和圆心十字 ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, centerY); ctx.lineTo(logicalWidth, centerY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(centerX, 0); ctx.lineTo(centerX, logicalHeight); ctx.stroke(); // 辅助:画星星中心点的环 ctx.beginPath(); ctx.arc(centerX, centerY, ringRadius, 0, Math.PI * 2); ctx.stroke();辅助线的作用很明显:星星不居中、角度偏了、环的大小不合适,一眼就能看穿,比盲调参数高效得多。等辅助线验证完毕,再把这部分代码注释掉或删除,替换成最终的样式绘制。
4.2 中心圆环与整体收尾
星星全部就位之后,加一个中心圆环能让画面立刻完整起来。圆环的半径一般取星星内切圆弧的中位线附近,也可以从辅助环的ringRadius向外推一段距离。
ctx.beginPath(); ctx.arc(centerX, centerY, ringRadius + starOuterR * 0.6, 0, Math.PI * 2); ctx.strokeStyle = '#FFCC00'; ctx.lineWidth = 4; ctx.stroke();这个圆环绕星星的外沿走,视觉上像是把十二颗星拴在一起。线宽别太细,太细显得单薄;也别太粗,太粗会盖住星星的尖角。在900x600左右的画布上,4像素是调试下来比较舒服的值。
4.3 完整代码模板
把以上所有逻辑拼在一起,一个可直接运行的完整页面如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>Canvas 蓝底金星徽章</title> <style> body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f5f5f5; } canvas { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); } </style> </head> <body> <canvas id="flagCanvas"></canvas> <script> const canvas = document.getElementById('flagCanvas'); const ctx = canvas.getContext('2d'); // 逻辑尺寸,900x600(3:2 横版),蓝底金星通常也是横版构图 const logicalWidth = 900; const logicalHeight = 600; const dpr = window.devicePixelRatio || 1; canvas.width = logicalWidth * dpr; canvas.height = logicalHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); canvas.style.width = logicalWidth + 'px'; canvas.style.height = logicalHeight + 'px'; // 颜色与几何常量 const BG_COLOR = '#003399'; const GOLD_COLOR = '#FFCC00'; const centerX = logicalWidth / 2; const centerY = logicalHeight / 2; const ringRadius = 180; const starCount = 12; const starOuterR = 40; const starInnerR = starOuterR * 0.382; // 1. 铺背景 ctx.fillStyle = BG_COLOR; ctx.fillRect(0, 0, logicalWidth, logicalHeight); // 2. 画十二颗五角星 for (let i = 0; i < starCount; i++) { const angle = (-90 + i * 30) * Math.PI / 180; const starCenterX = centerX + ringRadius * Math.cos(angle); const starCenterY = centerY + ringRadius * Math.sin(angle); drawStar(ctx, starCenterX, starCenterY, starOuterR, starInnerR); } // 3. 外圈圆环 ctx.beginPath(); ctx.arc(centerX, centerY, ringRadius + starOuterR * 0.6, 0, Math.PI * 2); ctx.strokeStyle = GOLD_COLOR; ctx.lineWidth = 4; ctx.stroke(); function drawStar(ctx, cx, cy, outerR, innerR) { const points = 5; const step = Math.PI / points; ctx.beginPath(); for (let i = 0; i < points * 2; i++) { const radius = i % 2 === 0 ? outerR : innerR; const angle = -Math.PI / 2 + i * step; const x = cx + radius * Math.cos(angle); const y = cy + radius * Math.sin(angle); if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.closePath(); ctx.fillStyle = GOLD_COLOR; ctx.fill(); } </script> </body> </html>这段代码没有引入任何外部依赖,直接保存为.html文件双击就能看到效果。跑通之后再根据自己的需求调参数。
5. 常见问题与排查技巧实录
5.1 星星角度不对、朝向乱
症状:有的星尖朝上,有的朝左,看起来像被风吹歪了。
原因几乎永远只有一个:在计算顶点坐标时,角度基准不一致。每次画星时,内部顶点的起始角都是-Math.PI / 2,这本身是固定的,出问题的是外层画星星位置的那一层角度。如果你在圆周取点时用了0度起始,而星星内部绘制用-90度起始,星尖指向就会随位置变化而旋转。
检查方法很简单:在画第一颗星之前,手动把圆周取点的起始角度固定在-90度,并且确认星内部的起始顶点也落在-90度。两边基准一致,所有星尖就会稳定朝外。
5.2 画出来是五边形不是五角星
症状:路径封闭后只有五个顶点,没有内凹的锯齿。
原因:step值写错,画成了正五边形的五个顶点。正五边形的顶点间隔是2π / 5,而五角星是两圈顶点的组合,相邻顶点间隔是π / 5。
排查时打印每个顶点的角度,看i从0到9时是不是按0, π/5, 2π/5……递增的。角度对,图形就一定对。
5.3 图形发虚、有锯齿
症状:屏幕上边缘和线条毛刺明显,整体模糊。
原因分两类。第一类是没有做高DPI适配,逻辑尺寸直接等于画布像素尺寸;第二类是缩放画布后没有调用ctx.setTransform,导致坐标和像素对应关系错位。
解决办法就是把2.1小节的适配代码原封不动搬过去。如果还有锯齿,检查是否给元素加了非整数倍的CSS缩放,比如transform: scale(1.1),会引发二次采样模糊。
5.4 星星之间距离不均,某几颗挤在一起
症状:圆环上十二颗星分布不均,有的区域空一大块。
原因:循环里的角度增量写错了,可能是用360 / 12但忘转弧度,或者把弧度的Math.PI * 2 / 12写成了Math.PI / 12,后者会让星星挤在半个圆里。
5.5 已知问题速查表
| 现象 | 排查方向 | 解决方案 |
|---|---|---|
| 星尖朝向不统一 | 起始角基准是否一致 | 圆环取点和星内部都从-90度开始 |
| 画出五边形 | step角度错误 | 改成Math.PI / points |
| 整体模糊 | 未适配devicePixelRatio | 物理尺寸乘DPR并setTransform |
| 分布不均 | 角度增量计算错误 | 确认是2π / 12而不是π / 12 |
| 星星被圆环遮住 | 圆环半径太贴近星尖 | 圆环半径增加星半径的0.5到0.7倍 |
| 背景和星星混在一起 | fillStyle重复设置错乱 | 绘制前明确设置,用save/restore包裹 |
5.6 调试心得
我习惯在一个工具函数里把所有几何常量集中放在顶部,颜色放一组,半径角度放一组,这样调参时不需要在代码里翻来翻去。另外,每次改动参数之后,记得清空画布再重绘,不然新旧图形叠加会严重误导判断。
如果项目里对矢量图形有更强的交互需求,后续可以在这个基础上加动画:让星星整体缓慢旋转,或者鼠标悬浮时某个星星高亮,核心逻辑只需要调整循环里的角度参数就行。
我个人实操中最大的体会是:Canvas绘图最怕的不是API不熟,而是坐标系和弧度制这关没过关。这两点一旦彻底想透,什么仪表盘、粒子系统、数据可视化的底层,都是一个套路。最后再分享一个小技巧:调试阶段给每个关键坐标点打一个半径两三像素的小圆圈标记,一眼就能看出所有点是不是落在预期的几何位置上,比反复改参数试错快得多。