☰
Canvas绘制五角星:从几何坐标到动画的完整指南
2026/10/13 5:08:26 网站建设 项目流程

简介:一个基于C++语言实现的五角星绘图工程源码包,面向编程初学者和图形绘制入门人员,通过实际项目展示如何利用代码完成几何图形的绘制。整个资源包共包含六个文件,其中核心是一个C++源文件,配合Visual C++工程配置文件(如dsw、dsp)以及编译辅助文件(如plg、opt、ncb),压缩后大小仅为9KB,整体非常轻量,适合快速下载并查看工程结构。目前已有七百一十四人浏览学习,说明该示例对新手有一定吸引力。通过阅读和运行这份代码,可以直观理解五角星各条边的长度与角度关系、循环绘制结构以及绘图函数的基本用法;在掌握这个案例后,还可以尝试调整边长参数或绘制嵌套图形,进一步拓展图形编程能力。这份小工程的目录结构清晰,源码与工程配置分离,便于初学者逐项对照学习。

1. 绘制五角星:十行代码背后的几何与坐标陷阱

绘制五角星大概是图形编程里最容易被低估的入门题。很多人以为不过是找五个点连起来,等真的动手,第一笔就把尖角画歪了:要么星星倒立,要么胖瘦不均,要么 fill 完之后缺口还少一条边。原因不在那十行代码本身,而在坐标系方向、角度起点、内外半径比这三个看不见的默认值上。这篇文章按「几何建模 → Canvas 落地 → 典型翻车排查 → 动画进阶」的顺序,把绘制五角星从数学到代码拆到能直接照抄的程度。适合正在写 Canvas 图表、SVG 生成器、游戏 HUD 或任何需要星形标记的开发者,也适合拿这道题当热身的人,它能一次就暴露你对极坐标换屏幕坐标的基本功。

2. 五角星的几何建模:十个顶点、内外半径与黄金分割

2.1 画正五边形再连对角线还是十点多边形:两条路径的取舍

先破除一个常见的想当然:想画五角星,是不是先算出正五边形的五个顶点,再连对角线?数学上这个描述完全正确,但要落到画布填充上,这条路会给自己挖坑。正五边形的对角线互相交叉,交叉点的坐标你得额外再算一遍;更麻烦的是填充一个自相交路径时,Canvas 默认的 nonzero 环绕规则会把整块区域当成实心,你得先搞清楚填充规则才能把五条臂之间的空白留出来。这属于把简单问题做复杂。

我在实际项目里更常用的是「十点环」:外圆上每 72° 放一个尖点,内圆上每 72° 放一个凹点,两组点错开 36°,从外点开始内外交替连接。这个路径没有任何一条线交叉,填充规则对它完全不敏感,用 nonzero 还是 evenodd 结果都是同一块干净的星形。它还天然适合参数化,想改胖瘦改一个内半径就行,不用动任何角度。有人会问 SVG 或 CSS 里有没有内置星形?答案是都没有,只有 polygon,所以十点环这套计算几乎是做星形图形唯一要掌握的通用方案。

两条路径各有性格,落地难度差很多。对角线法生成的是严格意义上的正五角星,交叉点精确,适合做几何验证;十点环法生成的是「看起来就是五角星」的图形,胜在实现简单、控制直观。对 Canvas、SVG、CSS clip-path 这类渲染场景,十点环是更稳的选择。我只有做数学演示 Demo 的时候才去碰对角线法,日常画星形几乎全用十点环。

2.2 顶点坐标怎么算:36° 步进与 cos/sin 的屏幕坐标换算

十点环唯一的数学规律是:相邻两个顶点的圆心角永远相差 36°,也就是 π/5。原因是 5 个外点把 360° 分成 5 份每份 72°,内外点交替之后相当于把圆周切成了 10 份,每份 36°。这个步进角是所有坐标计算的地基,写错它画出来的直接就是另一张图。

有了步进角,任意一个顶点都能用极坐标换直角坐标的公式算出来:x = cx + radius * Math.cos(angle),y = cy + radius * Math.sin(angle)。radius 在 outerR 和 innerR 之间按奇偶交替,angle 从某个初始角开始每次递增 π/5。下面这段代码可以直接跑起来验证顶点位置:

const cx = 200, cy = 200; const outerR = 150, innerR = 58; const rotation = -Math.PI / 2; for (let i = 0; i < 10; i++) { const radius = i % 2 === 0 ? outerR : innerR; // 偶数点是尖角,奇数点是凹角 const angle = rotation + (i * Math.PI) / 5; // 从正上方开始,每次递增36度 const x = cx + radius * Math.cos(angle); const y = cy + radius * Math.sin(angle); console.log(i, x.toFixed(1), y.toFixed(1)); }

跑一遍你会发现第 0 个点落在 (200, 50),也就是圆心正上方;第 2 个点落在右上方的外圆上;第 4、6、8 个点依次绕下去,五个外点组成的正是五个尖角。这里有两个高频错误:radius 的奇偶取反,画出来不是星形而是一圈点;步进写成 2π/5,画出来只有五个点,连成一个正五边形。想快速自检,可以心算第 0 个点坐标,它应该在 (cx, cy - outerR),不对就先查 rotation,再查奇偶。

2.3 内半径比决定胖瘦:0.382 与 0.5 之间的选择

五角星没有唯一标准长相,innerR 与 outerR 的比值是全部的形状开关。数学意义上的正五角星要求 r/R = 1/φ² ≈ 0.382,φ 是黄金分割比 (1+√5)/2。这个结论可以由正五角星边的黄金分割性质推出来:每条臂被交点分成长短两段,比值恰好是 φ,代入化简就得到 0.382。干活时没人现场推这个,记住结论就好。这个比例下星臂细长、尖角锐利,旗帜、徽章、印章上最常见。

但 UI 图标很少直接用 0.382,细臂在 24px、32px 的小尺寸下尖角会糊成一团,视觉上星的「臂」直接消失。我在图标场景一般用 0.45,游戏里的奖励星星用 0.5,画出来饱满、有体积感。比值超过 0.6 之后星形退化成均匀一圈锯齿,像齿轮不像星星,基本用不上。

innerR / outerR观感适用场景
0.382细臂锐角,标准五角星旗帜、徽章、印章
0.45饱满适中,抗模糊UI 图标、评分控件
0.5圆润胖星游戏奖励、儿童插画
> 0.6齿轮化,星感消失不推荐

实际调参我的习惯是:先定 outerR(由版面尺寸决定),再用一个 ratio 变量控制 innerR = outerR * ratio。ratio 放到调试面板上拖动就能实时看效果,比把 innerR 写死聪明得多。第 3 章会把参数化思路落成完整的 drawStar 函数。

3. 用 Canvas 把五角星画在浏览器里:最小代码与参数解读

3.1 最小可运行代码:beginPath、交替半径与 closePath

先给一份能直接贴进 HTML 跑通的完整代码。核心就是第 2 章的十点环,加上 Canvas 路径三件套 beginPath、lineTo、closePath:

<!DOCTYPE html> <html lang="zh-CN"> <body> <canvas id="star" width="400" height="400"></canvas> <script> const canvas = document.getElementById('star'); const ctx = canvas.getContext('2d'); const cx = 200, cy = 200; const outerR = 150, innerR = 58; const rotation = -Math.PI / 2; ctx.beginPath(); for (let i = 0; i < 10; i++) { const radius = i % 2 === 0 ? outerR : innerR; const angle = rotation + (i * Math.PI) / 5; const x = cx + radius * Math.cos(angle); const y = cy + radius * Math.sin(angle); if (i === 0) { ctx.moveTo(x, y); // 第0个点是起点,必须抬笔移动 } else { ctx.lineTo(x, y); // 其余9个点依次连线 } } ctx.closePath(); ctx.fillStyle = '#FFC107'; ctx.fill(); ctx.strokeStyle = '#E65100'; ctx.lineWidth = 4; ctx.lineJoin = 'round'; ctx.stroke(); </script> </body> </html>

逻辑拆开看:第 0 个点是路径起点,必须用 moveTo 落笔,否则 lineTo 会从坐标系原点拉一条线过来;之后 9 个点全部 lineTo;最后 closePath 把最后一个点连回起点形成闭合路径。fill 和 stroke 之间没有硬性先后,但先 fill 再 stroke 是惯用顺序,描边压着填充边缘走,视觉更干净。

两个概念值得点一下。beginPath 的作用是开启一条新路径,忘记它会导致本帧路径累积上一次绘制的线段,图形越画越脏。moveTo 与 lineTo 的区别是抬笔与落笔,漏掉 moveTo 几乎必然多一条从原点射出的斜线。这两个小问题也是第 4 章避坑的高频对象。

3.2 封装成 drawStar 函数:四个参数怎么控制位置、大小与朝向

最小实现跑通后,下一步是把线性脚本改成可复用函数。常见做法是封装成下面的签名,把几何计算全部收敛到函数内部:

function drawStar(ctx, cx, cy, outerR, innerR, rotation = -Math.PI / 2) { ctx.beginPath(); for (let i = 0; i < 10; i++) { const radius = i % 2 === 0 ? outerR : innerR; const angle = rotation + (i * Math.PI) / 5; 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(); }

调用时把样式留在外部控制:

drawStar(ctx, 200, 200, 150, 58); ctx.fillStyle = '#FFC107'; ctx.fill(); ctx.strokeStyle = '#8D6E63'; ctx.lineWidth = 3; ctx.stroke();

五个参数里,cx、cy 控制位置,outerR 控制整体大小,innerR 控制胖瘦,rotation 控制朝向。rotation 默认给 -π/2,因为绝大多数场景都要尖角朝上。参数不是越多越好,我见过有人把每个顶点角度都暴露成入参,那纯属给自己找麻烦,五角星的对称性决定了角度计算不需要外部干预。

注意:这个函数不负责 fill 和 stroke。把「生成路径」和「应用样式」拆开,是后续做动画、复用、批量绘制的前提。

这个拆分还有个好处:旋转动画只需要递增 rotation,函数内部一行不用改。第 5 章直接在这个函数基础上加动画。

3.3 SVG 与 Python turtle 对照:同一套几何在不同环境的写法

Canvas 不是唯一选择。如果项目需要服务端输出 SVG 或生成静态图,可以用同样的十点环逻辑生成 polygon 的 points 字符串:

function starPoints(cx, cy, outerR, innerR, rotation = -Math.PI / 2) { const pts = []; for (let i = 0; i < 10; i++) { const radius = i % 2 === 0 ? outerR : innerR; const angle = rotation + (i * Math.PI) / 5; const x = (cx + radius * Math.cos(angle)).toFixed(2); const y = (cy + radius * Math.sin(angle)).toFixed(2); pts.push(x + ',' + y); } return pts.join(' '); } // 模板里使用 // <polygon points="${starPoints(100, 100, 90, 35)}" // fill="gold" stroke="#E65100" stroke-width="2" stroke-linejoin="round"/>

SVG 的 y 轴同样向下,rotation 规则跟 Canvas 完全一致,代码几乎不用改直接平移。这里要注意 stroke-linejoin 设成 round,否则尖角处的描边会带一个突兀的尖刺,Canvas 里对应的是 lineJoin 属性,两个环境名字不同、作用相同。

Python turtle 是另一类典型写法。它画五角星不需要算十个点,用一只画笔一笔画五次就能出形:

import turtle t = turtle.Turtle() t.speed(5) for _ in range(5): t.forward(200) # 画一条边 t.right(144) # 右转144度:180 - 36 t.hideturtle() turtle.done()

turtle 版为什么不需要内半径?因为它走的是自相交的 pentagram 路径,每条边直接跨越到隔一个的顶点,交叉点由线段自然产生。这个写法代码最短,但它画的路径是自交的,要填充时会遇到 2.1 节说的环绕规则问题,需要显式控制填充规则。教学演示用 turtle 很合适,生产环境我仍然推荐十点环。

4. 绘制五角星避坑指南:四个典型翻车点与排查方法

这四个坑我按出现频率排序。前两个新手必踩,后两个是换了高分屏或不同设计稿才会遇到。排查时按顺序过一遍,比瞎猜快很多。

4.1 尖角朝下:Canvas 坐标系 Y 轴方向与角度起点

现象:照抄极坐标公式,画出来的星星尖角朝下,或者整个横躺在右边。

原因:数学坐标系 y 轴向上,Canvas 的 y 轴向下,同样的角度在屏幕上位置是镜像的;同时角度 0 从圆心正右方开始而不是正上方,不调整 rotation,第 0 个顶点就落在右侧,画出来就是横躺的星。

解决:起始角设为 -Math.PI / 2,第 0 个顶点落在正上方,五个尖角自然朝上。另一种修法是改成 y = cy - radius * Math.sin(angle),也能让尖角朝上,但不推荐这么干,因为你把角度方向改反了,后续做旋转动画时方向会跟直觉相反,排查起来更费劲。统一用 rotation 参数,保持公式不动。

排查最快的办法是打印前两个顶点:第 0 点应该在 (cx, cy - outerR)。如果它出现在 (cx + outerR, cy),就是 rotation 没设置对;如果第 0 点位置正确但图形像镜像,再检查是不是有人把 sin 的减号写到了 y 之外的地方。

4.2 fill 出来缺一条边:closePath 与 beginPath 的配对问题

现象:fill 之后五角星缺一条边,缺口总在最后一段;或者上一个图形残留在新图形里,越画越脏。

原因:漏了 closePath。fill 只填充当前路径覆盖的区域,路径没闭合,最后一段边就不会被连上。残留图形则是漏了 beginPath,新路径把上一次调用的线段全串进来了。

解决:养成 beginPath 和 closePath 成对出现的习惯。drawStar 函数第一行 beginPath,循环结束立即 closePath。封装成函数后还要注意每次调用都重新 beginPath,否则连续画两颗星会把两条路径缝在一起,fill 出奇怪的重叠形状。

还有一个隐蔽版本:循环里 i === 0 时误用 lineTo 而不是 moveTo,第一条边会从坐标系原点拉出来,fill 后多一条从 (0,0) 到星形的放射线。别问我怎么知道的,这个坑的血泪在于报错信息看起来完全不像坐标问题,排查方向很容易跑偏。

4.3 星星太胖或太瘦:innerR 比值区间的选择

现象:innerR 设成 outerR 的 80%,画出来一圈锯齿,像齿轮;设成 10%,又变成一把细针,没有星形感。

原因:innerR 决定凹点深度。比值太接近 1,凹点几乎贴在外圆上,星形凹口消失;比值太小,凹点挤向圆心,五条臂细长如针。

解决:回到 2.3 节的比值表。最省事的做法是代码里不写死 innerR,定义 ratio 参数,innerR = outerR * ratio,默认 0.45。这样改形状只动一个数,也避免调 outerR 时忘了同步 innerR 导致的胖瘦突变。

这里还有一个和描边线宽叠加的坑:描边是沿路径居中的,lineWidth 加大会让星形整体向外扩约线宽的一半。如果设计稿对星星外包络尺寸有硬性要求,outerR 要预先减去 lineWidth / 2,否则星星总会比预期大一圈,反复返工。

4.4 高分屏下边缘发糊:devicePixelRatio 不是玄学

现象:同一份代码在普通屏幕上清晰,在高分屏上五角星边缘发虚。

原因:Canvas 的 width/height 属性决定绘图缓冲区像素数,CSS 里的 width/height 决定显示尺寸。高分屏一个 CSS 像素对应多个物理像素,缓冲区像素不够,浏览器只能插值放大,于是发糊。这个坑与五角星无关,所有 Canvas 绘制都逃不掉,提前知道能省一晚上的排查时间。

解决:绘制前按 devicePixelRatio 缩放画布:

const canvas = document.getElementById('star'); const dpr = window.devicePixelRatio || 1; const cssW = 400, cssH = 400; canvas.width = cssW * dpr; canvas.height = cssH * dpr; canvas.style.width = cssW + 'px'; canvas.style.height = cssH + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr);

注意这段必须在任何绘制之前执行,ctx.scale(dpr, dpr) 之后所有坐标按 CSS 像素理解,drawStar 内部一行不用改。如果缩放后还是糊,检查 CSS 里有没有外层 transform: scale,优先去掉外层缩放而不是继续加倍 dpr,否则会进入越乘越糊的死循环。顺带一提,SVG 方案不存在这个问题,矢量渲染天然适配任意 DPR,这也是为什么服务端出图我经常选 SVG。

5. 让五角星动起来:旋转动画、径向渐变与圆角描边

drawStar 把路径生成和样式拆开之后,动画几乎是白送的。最常见的需求是星星绕自己旋转,用 requestAnimationFrame 每帧递增 rotation 并重绘:

let rot = -Math.PI / 2; const gradient = ctx.createRadialGradient(180, 180, 20, 200, 200, 150); gradient.addColorStop(0, '#FFE082'); // 中心高亮 gradient.addColorStop(1, '#FF8F00'); // 边缘深橙 function starAnimation() { ctx.clearRect(0, 0, 400, 400); drawStar(ctx, 200, 200, 150, 58, rot); ctx.fillStyle = gradient; ctx.fill(); ctx.strokeStyle = '#E65100'; ctx.lineWidth = 3; ctx.stroke(); rot += 0.01; requestAnimationFrame(starAnimation); }

rot 每帧加 0.01 弧度,约 0.57°,一秒 60 帧下来是 34°,视觉平滑。想转快就把增量调大,想逆时针就改为负值。gradient 要放在循环外面创建,每帧重建渐变对象既慢又制造垃圾回收压力。径向渐变的六个参数分别是内圆圆心、内圆半径、外圆圆心、外圆半径,把光源放在左上角靠里的位置,星星看起来更有立体感。描边记得保留 lineJoin = 'round',尖角处才不会长出刺。

如果场景里有多颗星,我一般这样批量画:

const stars = [ [80, 80, 30], [200, 200, 60], [320, 120, 40] ]; for (const [sx, sy, sr] of stars) { drawStar(ctx, sx, sy, sr, sr * 0.45); // 每颗星独立位置与大小 ctx.fillStyle = '#FFC107'; ctx.fill(); ctx.stroke(); // 复用上一步的描边样式 }

多星场景有个隐藏细节:每颗星 fill 前都要重新设置 fillStyle,否则后画的星会沿用上一个颜色,排错时看着就非常困惑。我的习惯是把颜色、线宽、圆角这些收进一个样式对象传给 drawStar 的调用封装,页面代码保持干净。最后一条踩坑心得:画星星出问题,八成在角度起点和内外半径比,先查这两个,再查路径闭合,比大海捞针快得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询