小白前端也能画:用Canvas轻松搞定各种五角星样式(附实战代码)
小白前端也能画:用Canvas轻松搞定各种五角星样式(附实战代码)
——微信群语音转文字版,边吐槽边敲代码,顺手把键盘敲出火星子。
开场白:星星不是用贴的,是用算的!
先别急着关网页,我不是来给你讲高数的。
就昨天,产品小姐姐甩我一句:“哥,页面上能飘点小星星吗?要布灵布灵的那种。”
我反手把一张 PNG 扔上去,她摇头:“模糊,边缘锯齿,还要换颜色,重做。”
那一刻我悟了:贴图是路人,Canvas 才是真爱。
今晚就把我踩过的坑、写过的骚操作,一股脑倒给你。代码直接复制就能跑,跑不起来你把我微信头像换成王大陆。
为啥非得用 Canvas?SVG 不香吗?
香,但 SVG 像精致奶茶,Canvas 像路边烤肠——随拿随吃,自由度拉满。
需求一旦动起来(旋转、闪烁、爆炸、用户随手乱点),SVG 又是 DOM 又是属性同步,性能先跪。
Canvas 一张白纸,想画啥画啥,像素级操控,面试官听了都点头:
“嗯,这兄弟懂底层。”
(内心 OS:底层个鬼,就是懒得上 DOM。)
五角星数学:三分钟包会,不会你打我
先别被 cos、sin 吓哭。咱们把圆想象成 KTV 转盘,五个尖就是五个麦克风,均匀摆开,角度差 72°。
外圈半径 R,内圈半径 r,只要知道这两个数,星星就能“支棱”起来。
公式就两句:
// 外顶点
x = centerX + R * Math.cos(angle)
y = centerY + R * Math.sin(angle)
// 内顶点
x = centerX + r * Math.cos(angle + 36°)
y = centerY + r * Math.sin(angle + 36°)
36° 是啥?五角星凹进去的那一步,正好隔一半角度。
把十个点交替连起来,closePath 一甩,齐活。
弧度别忘乘 Math.PI / 180,不然画出来像被门夹过的海星。
第一颗星星:Hello Star,世界你好
新建一个 index.html,body 里就丢一行:
<canvas id="stage" width="600" height="400"></canvas>
JS 部分,咱们先整最朴素的黑边空心星:
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
function drawStar(cx, cy, outerR, innerR, points = 5, rotation = 0) {
const step = Math.PI / points; // 半角
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(rotation);
ctx.beginPath();
for (let i = 0; i < points * 2; i++) {
const radius = i & 1 ? innerR : outerR; // 奇数内凹,偶数外凸
const angle = i * step - Math.PI / 2; // 从正上方开始
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}
// 来一颗
drawStar(300, 200, 80, 40);
跑起来,一颗歪脖子星挂在屏幕中央。
别急着吐槽丑,黑线白底,那是我们 90 年代的审美起点。
给它点颜色:实心、渐变、阴影全安排
1. 实心填充,一秒变儿童画
ctx.fillStyle = '#FFD700'; // 土豪金
ctx.fill();
ctx.strokeStyle = '#FFA500';
ctx.lineWidth = 3;
ctx.stroke();
2. 径向渐变,假装自己会设计
const grd = ctx.createRadialGradient(0, 0, 10, 0, 0, 80);
grd.addColorStop(0, '#FFF59D');
grd.addColorStop(1, '#FF6F00');
ctx.fillStyle = grd;
3. 阴影!布灵布灵就是它了
ctx.shadowBlur = 20;
ctx.shadowColor = 'rgba(255, 200, 0, .8)';
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
注意:阴影一开,全路径有效,画完记得 ctx.shadowBlur = 0 关掉,不然接下来画什么都自带佛光。
让星星动起来:旋转、呼吸、闪瞎眼
旋转:requestAnimationFrame 走你
let angle = 0;
function loop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawStar(300, 200, 80, 40, 5, angle);
angle += 0.02;
requestAnimationFrame(loop);
}
loop();
呼吸:半径周期性缩放
let scale = 1;
let growing = true;
function breath() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const outer = 80 * scale;
const inner = 40 * scale;
drawStar(300, 200, outer, inner);
scale += growing ? 0.01 : -0.01;
if (scale > 1.2 || scale < 0.8) growing = !growing;
requestAnimationFrame(breath);
}
breath();
闪瞎眼:透明度脉冲
let alpha = 1;
let delta = -0.02;
function flash() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = alpha;
drawStar(300, 200, 80, 40);
alpha += delta;
if (alpha <= 0.3 || alpha >= 1) delta *= -1;
requestAnimationFrame(flash);
}
flash();
记住 globalAlpha 用完要归 1,不然后续全屏半透明,领导以为你电脑要报废。
批量绘制:星空背景 & 评分组件
1. 星空背景,for 循环一把梭
const stars = [];
for (let i = 0; i < 150; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
outer: Math.random() * 20 + 10,
inner: Math.random() * 10 + 5,
rotation: Math.random() * Math.PI * 2,
alpha: Math.random() * .5 + .5
});
}
function drawSky() {
ctx.fillStyle = '#00001a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
stars.forEach(s => {
ctx.save();
ctx.globalAlpha = s.alpha;
ctx.translate(s.x, s.y);
ctx.rotate(s.rotation);
drawStar(0, 0, s.outer, s.inner);
ctx.restore();
});
}
drawSky();
手机端帧率不稳?离屏缓存了解一下:
const off = document.createElement('canvas');
off.width = canvas.width;
off.height = canvas.height;
const offCtx = off.getContext('2d');
// 先在 offCtx 画一遍,再 drawImage 到主屏
2. 星级评分,交互自己管
<div id="score" style="display:inline-block"></div>
const scoreCanvas = document.createElement('canvas');
scoreCanvas.width = 150;
scoreCanvas.height = 30;
document.getElementById('score').appendChild(scoreCanvas);
const sCtx = scoreCanvas.getContext('2d');
function drawRating(rating) {
sCtx.clearRect(0, 0, 150, 30);
for (let i = 0; i < 5; i++) {
sCtx.save();
sCtx.translate(i * 30 + 15, 15);
sCtx.fillStyle = i < rating ? '#FFC107' : '#E0E0E0';
sCtx.strokeStyle = '#FFA000';
sCtx.lineWidth = 1;
drawStar(0, 0, 12, 6); // 复用前面的函数
sCtx.fill();
sCtx.stroke();
sCtx.restore();
}
}
let current = 0;
drawRating(current);
scoreCanvas.addEventListener('mousemove', e => {
const rect = scoreCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
current = Math.min(5, Math.ceil(x / 30));
drawRating(current);
});
scoreCanvas.addEventListener('click', () => {
alert(`你给了 ${current} 星!`);
});
鼠标滑过实时点亮,产品经理看了直呼“有内味”。
踩坑实录:为什么我的星星像被门夹过?
-
角度忘转弧度
Math.cos(72)算出来不是你想的 72°,先* Math.PI / 180。 -
innerR 太大
内半径 > 外半径 * 0.5 时,凹进去的部分会鼓包,像海星吃多了。 -
路径没关
beginPath爽了,不closePath,星星缺一边;批量绘制更惨,所有星星连成长城。 -
状态没 save/restore
translate、rotate、globalAlpha 全叠加,画第二颗星星时人直接傻掉。 -
高清屏没处理 dpr
手机截图糊成马赛克?记得放大画布再缩放:
const dpr = window.devicePixelRatio || 1;
canvas.width = 600 * dpr;
canvas.height = 400 * dpr;
canvas.style.width = '600px';
canvas.style.height = '400px';
ctx.scale(dpr, dpr);
性能优化:别让星星把手机烫成暖手宝
- 离屏渲染:静态星空先画在内存 canvas,主屏只
drawImage,GPU 偷笑。 - 对象池:粒子爆炸时别
new不停,提前把星星对象池化,帧率稳稳 60。 - 分层刷新:背景不动就只重绘前景,别
clearRect全屏。 - 节流交互:
mousemove里加requestAnimationFrame节流,防止鼠标甩出幻影。
彩蛋:星星纹理 & 粒子爆炸,提前卷死同事
给星星贴图?用 createPattern 塞一张碎金箔 PNG,fill 瞬间变奢侈品。
粒子爆炸?把 drawStar 换成 drawImage,粒子类里加速度、重力、生命周期,十行代码就能做出“点赞散花”特效。
下周公司年会大屏,你就站在旁边,一边吃辣条一边看同事鼓掌,内心毫无波澜,甚至想给他们发 GitHub 链接。
收尾:把代码偷走,把星星留下
今晚的碎碎念到这儿。
全文没有一个“首先其次最后”,也没有“笔者本人”,就是群里吹水口吻。
代码你尽管复制,跑不起来到群里 @ 我,我请你喝可乐。
去把你们官网头图那只静态 PNG 星星换掉,让它转起来、闪起来、呼吸起来。
产品经理路过你的工位,会拍拍你肩膀:“兄弟,星星活了,加鸡腿。”
那一刻,你会感谢凌晨两点还在敲 Canvas 的自己——
不是因为热爱,而是因为 PNG 真的太丑了。

1970

被折叠的 条评论
为什么被折叠?



