
1. 项目概述我用Canvas画了100个五角星竟然这么上头前几天整理旧代码翻到一个很久以前写的小页面——用Canvas在屏幕上随机撒了100个五角星。当时只是随手做着玩没想到后来很多朋友跑来问我要源码还有人问我能不能加个小人形象、能不能做成动态旋转的星星。这让我意识到Canvas入门这件事看起来高大上其实只要抓住几个核心点完全可以让小白在半小时内做出一个既好看又有点交互感的作品。这个“100个五角星随机撒屏”的项目本质上是把Canvas的三大能力串起来了一是绘制基础图形五角星的路径计算二是随机数驱动的坐标分布三是循环渲染与动画帧刷新。你用三分钟画一个圆也能学到Canvas的入门写法但画100个五角星并且让它们随机分布你就能自然理解为什么需要beginPath()、为什么每次重绘之前要clearRect()、为什么性能上要避免频繁改动DOM。这些坑我在初学的时候踩了无数遍所以写这篇文章就是想把从零到完整效果的全过程说清楚。适合谁看如果你刚接触Canvas看完能独立写出这个实战页面如果你已经能用Canvas画点东西我后面讲的性能优化和常见坑也会对你有帮助。整个项目不依赖任何框架一个HTML文件就能跑所见即所得。2. 为什么是“随机撒屏”拆解这个项目背后的核心思路2.1 随机性带来的“活”效果很多人画Canvas第一个感觉是“太死板了”。画个方块它就在那儿画个圆它也不动。而随机撒屏这个需求恰好打破了这种呆板。100个五角星每个都有独立的x、y坐标每个的旋转角度、大小甚至颜色都不同整个画面看起来就像是你打破了调色盘星星自己蹦到了屏幕上。随机性还有一个好处每次刷新页面呈现的画面都不一样。这就意味着同样一段代码每次打开都有新鲜感。我在写这个项目的时候还专门加了“点击页面重新随机分布”的逻辑每次点击都是一幅新画面有朋友说这像某种减压神器哈哈确实有点解压。2.2 为什么选五角星而不是圆或方块五角星是最适合Canvas入门的图形之一因为它逼着你理解“路径”的概念。画圆有现成的arc()方法画方块有rect()但五角星没有现成API你得自己算十个顶点然后连线。这一步搞定了你对Canvas的moveTo()、lineTo()、closePath()的理解会非常深刻以后画任何不规则图形思路都一样。而且五角星视觉效果够炫撒满屏幕很有成就感换成圆就没有那种“哇”的效果了。2.3 从“撒屏”延伸到“canvas小人形象”的基础有朋友看完这个五角星页面问我能不能在星星旁边画点小人。其实完全可以因为五角星的路径绘制逻辑和小人的四肢绘制逻辑本质是相通的——都是点、线、面的组合。你学会控制顶点坐标就学会了组合图形。换句话说五角星项目是一个隐蔽的“通用绘图引擎”入门它不依赖API而是依赖你的数学思维。掌握这个后面画小人、画房子、画场景都只需要重复“找点、连线”的过程。3. 动手前的准备环境与Canvas基础概念扫盲3.1 一个HTML文件就够了这个实战不需要安装任何开发环境不需要Node.js不需要构建工具。你只需要新建一个文本文件后缀改成.html用浏览器打开就能运行。这是Canvas最友好的地方它本身就是浏览器内置的绘图能力没有额外的依赖。我的习惯是先建一个index.html然后在这个文件里同时写HTML、CSS和JavaScript。作为Demo足够了后面要扩展再拆文件。3.2 Canvas元素和2D绘图上下文代码里首先要有一个canvas标签这是画布。它有两个关键属性width和height。注意这两个属性和CSS里的宽高不同Canvas的width/height决定的是画布的分辨率也就是绘图坐标系的范围。然后是获取“画笔”的代码const canvas document.getElementById(canvas); const ctx canvas.getContext(2d);这里的ctx就是绘图上下文所有绘制方法都在它上面调用。说句大白话canvas是纸ctx是笔。你拿笔在纸上画画的东西就在canvas上显示。3.3 坐标系从左上角开始Canvas坐标系和数学坐标系不一样——它的原点在左上角x轴向右y轴向下。这意味着y值越大图形位置越靠下。很多新手一开始会在这里出错比如想画在右上角写了x: 400, y: 0结果跑到了最顶上。没关系记住“往下为正”就好。我个人建议第一次写Canvas之前先花两分钟在纸上画一个200x200的网格标出原点、几个点的坐标然后对应到Canvas上。这个小习惯能避免后面80%的坐标混乱。4. 核心实现100个五角星的随机撒屏全流程4.1 先来一个五角星顶点的数学计算五角星有5个外角和5个内角实际上就是10个顶点交替排列。假设五角星的中心在(cx, cy)外半径为R中心到外顶点的距离内半径为r中心到内顶点的距离那么每个顶点的坐标可以这样算外顶点角度从-90度开始每隔72度一个因为一圈360度5个外顶点内顶点角度从-90度加36度开始同样每隔72度一个用弧度制表示角度转弧度是degree * Math.PI / 180。外顶点坐标公式x cx R * Math.cos(angle) y cy R * Math.sin(angle)内顶点同理只不过半径换成r角度加上36度也就是半个72度。为了让五角星体型匀称r一般取R * 0.382这个比例是数学推导出来的黄金比例近似值。我封装了一个绘制单个五角星的函数function drawStar(ctx, cx, cy, R, r, rotation) { ctx.beginPath(); for (let i 0; i 5; i) { // 外顶点 let outerAngle rotation (i * 72 - 90) * Math.PI / 180; let x1 cx R * Math.cos(outerAngle); let y1 cy R * Math.sin(outerAngle); if (i 0) { ctx.moveTo(x1, y1); } else { ctx.lineTo(x1, y1); } // 内顶点 let innerAngle rotation ((i * 72) 36 - 90) * Math.PI / 180; let x2 cx r * Math.cos(innerAngle); let y2 cy r * Math.sin(innerAngle); ctx.lineTo(x2, y2); } ctx.closePath(); ctx.fill(); ctx.stroke(); }rotation参数用来控制五角星的旋转角度这样每个星星都能有不同的姿态。如果不要旋转传0就行。4.2 生成100个随机位置与随机参数五角星本身画出来之后剩下的事情就简单了循环100次每次生成随机的中心坐标、半径、颜色、旋转角度然后调用drawStar。随机坐标要注意边界。如果画布宽度是window.innerWidth那么x的范围不能是0 ~ width因为星星有半径太靠近边缘会被截掉一半。我给每个星星留了R 20的边距x Math.random() * (canvas.width - 2 * padding) padding;这样星星就不会跑出画布。颜色也做成随机的但纯随机容易出现对比度很低的颜色所以我用HSL格式固定饱和度和亮度只随机色相。这样整体看起来鲜艳又和谐。let hue Math.floor(Math.random() * 360); let color hsl(${hue}, 70%, 60%);4.3 刷新与重绘为什么每次要清空画布如果你直接画100个星星不清空画布那么图形会层层叠加。在某些效果里这很酷比如拖尾但这个项目我们要的是干净的“撒屏”效果所以每次绘制前必须清空。清空有两个常用方法ctx.clearRect(0, 0, canvas.width, canvas.height);或者用canvas.width canvas.width这个技巧重置画布。两者区别不大clearRect更语义化推荐。画布尺寸要注意如果浏览器窗口大小变了画布不会自动调整。我在代码里加入了resize事件监听重新设置canvas.width和canvas.height然后重新绘制星星。不然窗口变大后会出现大片空白窗口变小后星星会被裁掉。4.4 加入点击重绘与动画雏形为了让页面更好玩我在window上绑定了点击事件。每次点击就重新生成所有星星的数据并重绘。这个交互看起来简单但它背后有一个重要思路数据与绘制分离。你可以把所有星星的参数存在一个数组里let stars []; function generateStars(count) { stars []; for (let i 0; i count; i) { let R Math.random() * 30 10; stars.push({ x: ..., y: ..., R: R, r: R * 0.382, rotation: Math.random() * 360, color: ... }); } }绘制时遍历数组function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); stars.forEach(s { ctx.fillStyle s.color; ctx.strokeStyle s.color; drawStar(ctx, s.x, s.y, s.R, s.r, s.rotation); }); }以后如果你想让星星动起来只需要把render()放进requestAnimationFrame循环然后在每次渲染时更新星星的坐标或旋转角度。这就是从“静态撒屏”到“动态星空”的最自然演进路径。5. 完整实操手把手把代码跑起来5.1 最终代码一个文件搞定下面是我整理后的完整代码你可以直接复制成一个HTML文件浏览器打开就能看到效果。我加了比较详细的注释方便你对照理解。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title100个五角星随机撒屏/title style body { margin: 0; overflow: hidden; background: #0b0b1a; font-family: Arial, sans-serif; } canvas { display: block; cursor: pointer; } .tips { position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); color: rgba(255, 255, 255, 0.6); background: rgba(0, 0, 0, 0.3); padding: 6px 16px; border-radius: 20px; font-size: 14px; pointer-events: none; } /style /head body canvas idcanvas/canvas div classtips点击页面任意位置重新随机分布/div script const canvas document.getElementById(canvas); const ctx canvas.getContext(2d); // 星星数据数组 let stars []; const STAR_COUNT 100; // 初始化画布尺寸 function resizeCanvas() { canvas.width window.innerWidth; canvas.height window.innerHeight; } // 生成一个星星对象 function generateStar() { const R Math.random() * 30 10; const padding R 20; return { x: Math.random() * (canvas.width - padding * 2) padding, y: Math.random() * (canvas.height - padding * 2) padding, R: R, r: R * 0.382, rotation: Math.random() * Math.PI * 2, hue: Math.floor(Math.random() * 360) }; } // 生成指定数量的星星 function generateStars(count) { stars []; for (let i 0; i count; i) { stars.push(generateStar()); } } // 绘制单个五角星 function drawStar(star) { const { x, y, R, r, rotation } star; const ctx canvas.getContext(2d); ctx.beginPath(); for (let i 0; i 5; i) { // 外顶点 let outerAngle rotation (i * 72 - 90) * Math.PI / 180; let x1 x R * Math.cos(outerAngle); let y1 y R * Math.sin(outerAngle); if (i 0) { ctx.moveTo(x1, y1); } else { ctx.lineTo(x1, y1); } // 内顶点 let innerAngle rotation (i * 72 36 - 90) * Math.PI / 180; let x2 x r * Math.cos(innerAngle); let y2 y r * Math.sin(innerAngle); ctx.lineTo(x2, y2); } ctx.closePath(); ctx.fillStyle hsl(${star.hue}, 70%, 60%); ctx.strokeStyle hsl(${star.hue}, 80%, 40%); ctx.lineWidth 2; ctx.fill(); ctx.stroke(); } // 渲染全部星星 function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 加一点背景渐变更有夜空的质感 const gradient ctx.createLinearGradient(0, 0, canvas.width, canvas.height); gradient.addColorStop(0, #0b0b1a); gradient.addColorStop(1, #1a1a3a); ctx.fillStyle gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); stars.forEach(star drawStar(star)); } // 点击页面重新撒屏 window.addEventListener(click, function () { generateStars(STAR_COUNT); render(); }); // 窗口尺寸变化时重新生成并绘制保持星星不越界 window.addEventListener(resize, function () { resizeCanvas(); generateStars(STAR_COUNT); render(); }); // 初始化 resizeCanvas(); generateStars(STAR_COUNT); render(); /script /body /html5.2 代码逐段说明你为什么应该这样写先看resizeCanvas()。window.innerWidth和window.innerHeight分别是浏览器窗口的宽高。我把canvas的分辨率设成跟窗口一样大这样星星能铺满整个屏幕。如果你希望画布固定大小比如800x600也可以但那样星星只能在这个区域里“撒”。generateStar()里的padding为什么是R 20因为星星有大小中心坐标如果贴着画布边缘外顶点就会出界。我把R最大设为40所以padding最大为60最小也有30。这样即使碰到屏幕边缘星星也能完整显示。drawStar()函数里我重新获取了ctx虽然可以用全局的ctx但这样写的可读性更好而且方便日后把drawStar抽到单独模块。lineWidth设成2因为星星轮廓太细会显得廉价太粗又盖住内部颜色。2刚刚好。背景渐变我用了createLinearGradient这是一个非常实用的Canvas API它创建的是一个线性渐变对象。注意addColorStop(0, #0b0b1a)表示渐变的起点颜色addColorStop(1, #1a1a3a)是终点颜色。背景深色偏蓝紫能衬托出星星的鲜艳色彩。5.3 为什么用hsl而不是rgb来随机颜色这是一个很实用的小技巧。如果你用rgb(Math.random()*255, ...)去随机很容易出现灰色、暗色、或者和背景融为一体的蓝色。而用HSL我们可以固定亮度和饱和度只随机色相这样每个颜色都能保证鲜艳且可辨识。hsl(色相, 饱和度, 亮度)里色相是0到360度的颜色环饱和度70%让颜色有足够浓度亮度60%让颜色在深色背景上既有存在感又不会刺眼。我在调试时发现如果把亮度设成50%以下星星会显得很沉闷设成80%以上又会出现大量类似于荧光的效果看久了眼睛累。60%是我试下来最舒服的数值当然你可以根据喜好调整。6. 常见问题排查与性能优化实战6.1 我刚运行屏幕上什么都没有是怎么回事最大的可能canvas的width或height为0。常见于你把canvas的尺寸写在了CSS里而没有设置HTML属性。比如canvas idcanvas stylewidth: 300px; height: 200px;/canvas这样画布的内部分辨率依然是默认的300x150。更阴险的是如果CSS里设了100%宽高但父元素没有高度canvas就变成0。所以最稳妥的方案就是像我这样用JavaScript动态设置canvas.width window.innerWidth不要依赖CSS。第二种可能是代码里Math.random()生成的x、y全是NaN这通常是因为canvas.width是字符串或者没定义。记得用window.innerWidth或数字字面量。第三种可能是你绘制的时候没有调用ctx.beginPath()。如果没有beginPath多次绘制会共用同一条路径导致你看到的不是五角星而是一堆连在一起的杂乱线条。我之前就犯过这个错误当时画了三个五角星结果屏幕上出现了一个“大怪物”。6.2 星星被裁剪或者跑出屏幕了这个问题90%是因为你没有考虑边界。我用padding解决但还有一个坑如果窗口大小变化原来的padding可能不够。比如窗口变窄了但某个星星的R没变x还是老坐标就可能超出新边界。所以我在resize事件里重新生成了所有星星而不是简单地缩放坐标。如果你不想重新生成也可以做坐标钳制——把超出边界的星星坐标强制设置到边界内。但这样星星的分布会变得不均匀所以重新生成更干脆。6.3 100个星星太卡聊聊Canvas性能优化说实话100个星星在现代浏览器里几乎不可能卡。但如果你以后要画1000个、10000个或者让星星旋转性能就得注意了。第一个优化点减少Canvas API调用次数。比如fillStyle和strokeStyle赋值很便宜但beginPath、moveTo、lineTo都是有开销的。如果整批星星使用相同的颜色可以把beginPath放在循环外面只调用一次路径定义最后统一fill()。不过这样会丢失每个星星独立颜色的能力。第二个优化点使用requestAnimationFrame而不是setInterval来做动画。requestAnimationFrame会跟随屏幕刷新率通常60fps而且页面不可见时会自动暂停节省CPU。如果你只需要静态显示一次render()就够了不需要循环。第三个优化点离屏Canvas。如果你有很多相同的五角星——同一个形状、同一个大小可以先把一个五角星画到一个隐藏的Canvas上然后用drawImage把它贴到主画布的各个位置。drawImage是硬件加速的性能远高于重新计算路径。这个技巧在粒子系统里非常常用。不过我们这个项目里每个星星大小、旋转角度都不同离屏缓存收益不大所以没有采用。6.4 常见问题速查表问题现象可能原因解决方案页面空白canvas尺寸为0用JS设置canvas.width/height图形错乱、乱线没有beginPath每次绘制前调用ctx.beginPath()星星缺角或被切坐标越界用padding防越界颜色太暗/看不清随机RGB导致低对比度改用HSL固定饱和度和亮度窗口变化后布局乱canvas尺寸未自适应监听resize重设画布并重新生成动画或刷新卡顿频繁调用路径API用离屏Canvas缓存图形6.5 一个小技巧把星星变成动态旋转的这个项目做完之后我最喜欢的一个扩展是让星星慢慢旋转。只需要在render里每次增加rotation然后用requestAnimationFrame驱动。function update() { stars.forEach(star { star.rotation 0.01; }); } function loop() { clearAndDrawBackground(); stars.forEach(star drawStar(star)); update(); requestAnimationFrame(loop); }当100个星星以不同的转速旋转时整个屏幕像是一片流动的星河效果比静态的好很多。而且你不需要改变绘制函数因为旋转角度本来就是参数。这就是“数据驱动绘制”的最大好处。7. 经验总结从五角星到任意Canvas图形的迁移能力我个人在实际操作中的一个强烈体会是Canvas入门最大的门槛不是API而是“把图形拆解成坐标点”的思维。五角星项目完美训练了这个能力。当你亲手动笔算出10个顶点再看着屏幕出现一个五角星那种“数学变成画面”的成就感会支撑你继续画很多有意思的东西。这套五角星也可以作为你日后的组件库基础。比如把drawStar改造一下传入不同的外顶点数和内半径比例你就能画出任意星星、齿轮、多边形。如果再结合Canvas的translate和rotate你甚至可以让星星围绕任意中心旋转。这些能力都会在后续的canvas小游戏、数据可视化、动画海报项目中反复用到。最后再分享一个我在调试时常用的土方法在drawStar函数里临时加上一行console.log(x1, y1)然后刷新页面看控制台输出的坐标是否正确。比如我当初发现有一个星星画到了左上角外面就是靠这个办法查出来的——原来是rotation传来的是360度而不是角度单位导致一个顶点位置异常。及时打印坐标比单纯盯着屏幕猜会高效很多。你也可以在坐标计算函数里加个断点步进看每一行代码执行后的值这对新手排查问题非常友好。如果你照着这篇文章做出来一个能动态刷新五角星的小页面那你的Canvas基础就过关了一半。后面无论是想画小人、实现3D效果Canvas也有实现3D文字的库原理类似或者做一个绘图引擎都是从这些基础坐标系和路径操作延伸出去的。希望这份笔记对你有点用。