小白前端也能画:用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} 星!`);
});

鼠标滑过实时点亮,产品经理看了直呼“有内味”。


踩坑实录:为什么我的星星像被门夹过?

  1. 角度忘转弧度
    Math.cos(72) 算出来不是你想的 72°,先 * Math.PI / 180

  2. innerR 太大
    内半径 > 外半径 * 0.5 时,凹进去的部分会鼓包,像海星吃多了。

  3. 路径没关
    beginPath 爽了,不 closePath,星星缺一边;批量绘制更惨,所有星星连成长城。

  4. 状态没 save/restore
    translate、rotate、globalAlpha 全叠加,画第二颗星星时人直接傻掉。

  5. 高清屏没处理 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 真的太丑了。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值