小白也能画出炫酷六角星:Canvas实战+源码直接抄
- 小白也能画出炫酷六角星:Canvas实战+源码直接抄
-
- 为啥突然要画六角星?别问,问就是UI设计师又整活了
- Canvas 是啥玩意儿?先搞明白再动手
- 六角星到底长啥样?数学原理简单说两句
- 从零开始手搓一个基础六角星
- 想换个风格?试试这些花里胡哨的样式
-
- 1. 渐变填充
- 2. 阴影发光
- 3. 镂空效果
- 动态绘制?让六角星转起来、缩放、甚至呼吸
-
- 旋转 + 缩放
- 呼吸灯
- 组合技:旋转 + 呼吸 + 变色
- 性能别翻车:批量画几十个六角星怎么办?
-
- 离屏缓存模板
- 调试时星星画歪了?常见翻车现场和自救指南
- 开发老鸟私藏技巧:封装成组件、支持配置、还能导出图片
- 附赠可直接跑的完整源码(带注释版)
小白也能画出炫酷六角星:Canvas实战+源码直接抄
为啥突然要画六角星?别问,问就是UI设计师又整活了
凌晨一点半,我刚把热乎的 React 打包上线,产品经理甩来一张图:“哥,明天上线,就要这个会转的六角星,带呼吸灯,能导出 PNG,最好还能批量复制,性能别卡,颜色要可配置,谢谢。” 我盯着屏幕,脑子里只有一句话:我谢谢你全家。
可吐槽归吐槽,饭还是要吃。于是我把咖啡续上,打开 VSCode,决定把这次踩坑的全过程写下来,顺带把能复用的代码一股脑儿打包给你。复制粘贴就能跑,注释比我高中时抄的周杰伦歌词还密,谁再说 Canvas 难,就把这篇文章糊他脸上。
Canvas 是啥玩意儿?先搞明白再动手
有些教程一上来就让你 getContext('2d'),结果你连画布长啥样都没看清,就稀里糊涂开始画线,画完发现——咦,怎么是上下颠倒的?别问我怎么知道,我第一次画箭头,结果箭头冲着我自己,仿佛在嘲笑:你行你上啊。
说白了,Canvas 就是一块“透明胶片”,你拿支 JS 做的“笔”在上面乱涂。涂错了只能整块擦掉重画,没有“撤回”按钮,像极了人生。 它有两个尺寸:一个是元素本身大小(width/height 属性),另一个是 CSS 大小。如果你把两者搞混,就会出现“我明明画的 100px,怎么变 150px 了”的灵异事件。记住一句话:属性尺寸是画布像素,CSS 尺寸只是拉伸显示。 所以第一步,先锁死画布,别让浏览器乱缩放:
<canvas id="starCanvas" width="400" height="400"></canvas>
<style>
/* 千万别在这里写 width/height,不然你会哭 */
canvas {
border: 1px solid #ddd;
display: block;
margin: 0 auto;
}
</style>
六角星到底长啥样?数学原理简单说两句
小时候我以为星星都是五角的,直到 UI 妹子给我甩了这张图,我才发现自己天真得可爱。 六角星其实就是两个等边三角形,一个正着,一个倒着,叠在一起,像两个披萨切片交叉。 把圆周六等分,就能得到六个顶点。第一个三角形取第 0、2、4 个点,第二个三角形取第 1、3、5 个点,完事儿。 角度怎么算?别被“弧度”吓到,一句话:弧度 = 角度 * Math.PI / 180。 如果你非要用角度,也随你,反正 Canvas 只认弧度,就像后端只认 JSON,你丢 XML 过去,他内心是崩溃的。
从零开始手搓一个基础六角星
先把画布抢过来:
const canvas = document.getElementById('starCanvas');
const ctx = canvas.getContext('2d');
接下来封装一个“画六角星”函数,参数想多细就多细,反正以后老板说要改颜色,你改一行数字就行,不用连夜加班:
/**
* 画一个六角星
* @param {CanvasRenderingContext2D} ctx – 画布上下文
* @param {number} cx – 中心 x
* @param {number} cy – 中心 y
* @param {number} outerRadius – 外圈半径(尖角)
* @param {number} [innerRatio=0.5] – 内圈半径比例,越小越瘦
* @param {string} [fillStyle='#f39c12'] – 填充色
* @param {string} [strokeStyle='#fff'] – 描边色
* @param {number} [lineWidth=2] – 线宽
*/
function drawSixStar(
ctx,
cx,
cy,
outerRadius,
innerRatio = 0.5,
fillStyle = '#f39c12',
strokeStyle = '#fff',
lineWidth = 2
) {
const angleStep = Math.PI / 3; // 60°
ctx.save(); // 保存上下文,防止污染
ctx.translate(cx, cy); // 把坐标系原点移到星星中心,后面好算
ctx.beginPath();
for (let i = 0; i < 6; i++) {
// 外角尖
const outerAngle = i * angleStep – Math.PI / 2; // 从正上方开始,好看
const x1 = Math.cos(outerAngle) * outerRadius;
const y1 = Math.sin(outerAngle) * outerRadius;
// 内角凹
const innerAngle = outerAngle + angleStep / 2;
const x2 = Math.cos(innerAngle) * outerRadius * innerRatio;
const y2 = Math.sin(innerAngle) * outerRadius * innerRatio;
if (i === 0) ctx.moveTo(x1, y1);
else ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
}
ctx.closePath();
// 填充 & 描边
ctx.fillStyle = fillStyle;
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
ctx.fill();
ctx.stroke();
ctx.restore(); // 把状态弹回去,别影响别人
}
调用一下,看看效果:
drawSixStar(ctx, 200, 200, 80, 0.5, '#f39c12', '#fff', 2);
跑起来,一颗黄灿灿的六角星就躺在画布中央,像极了你第一次跑通 Hello World 时的激动。 如果你想画个“瘦星”,就把 innerRatio 调到 0.3;想画个“胖星”,就拉到 0.7,像极了我疫情期间的体重变化。
想换个风格?试试这些花里胡哨的样式
1. 渐变填充
CSS 里写 linear-gradient 很爽,Canvas 里也不差:
const grad = ctx.createLinearGradient(–80, –80, 80, 80);
grad.addColorStop(0, '#ffecd2');
grad.addColorStop(1, '#fcb69f');
drawSixStar(ctx, 200, 200, 80, 0.5, grad);
2. 阴影发光
别再用 box-shadow 了,Canvas 里叫 shadowBlur,记得画完关掉,不然全场都是阿凡达:
ctx.save();
ctx.shadowColor = 'rgba(255, 100, 200, 0.8)';
ctx.shadowBlur = 20;
drawSixStar(ctx, 200, 200, 80);
ctx.restore(); // 用完立刻关,别像冰箱门不关被妈骂
3. 镂空效果
想要“描边空心”?把 fill() 删了,只留 stroke(),再调粗线宽,瞬间高冷风:
drawSixStar(ctx, 200, 200, 80, 0.5, 'transparent', '#000', 6);
动态绘制?让六角星转起来、缩放、甚至呼吸
静态图老板看不上,非要“动画”。行,给他整一个。
旋转 + 缩放
核心就是 requestAnimationFrame + clearRect + 改 rotate 和 scale。 先整一个“旋转木马”版:
let rotation = 0;
function animate() {
const cx = 200;
const cy = 200;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(rotation);
// 画星星
drawSixStar(ctx, 0, 0, 60, 0.5, '#f39c12');
ctx.restore();
rotation += 0.02;
requestAnimationFrame(animate);
}
animate();
呼吸灯
把半径做成正弦波动,像熬夜程序员的心跳:
let frame = 0;
function breathe() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const scale = 1 + Math.sin(frame * 0.05) * 0.1; // 0.9 ~ 1.1
drawSixStar(ctx, 200, 200, 60 * scale);
frame++;
requestAnimationFrame(breathe);
}
breathe();
组合技:旋转 + 呼吸 + 变色
把上面代码揉一起,再加个 HSL 色相漂移,老板看了直呼“赛博朋克”:
let hue = 0;
function combo() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(200, 200);
ctx.rotate(rotation);
const scale = 1 + Math.sin(frame * 0.05) * 0.1;
const color = `hsl(${hue}, 80%, 60%)`;
drawSixStar(ctx, 0, 0, 60 * scale, 0.5, color);
ctx.restore();
rotation += 0.02;
frame++;
hue = (hue + 1) % 360;
requestAnimationFrame(combo);
}
combo();
性能别翻车:批量画几十个六角星怎么办?
老板突然说:“一颗不够,我要一屏,最好 60fps,手机也不能卡。” 你心里骂娘,手上还是得写。 记住三句话:
离屏缓存模板
// 1. 离屏 canvas,内存里画好
const off = document.createElement('canvas');
off.width = off.height = 120; // 比星星大一点
const offCtx = off.getContext('2d');
drawSixStar(offCtx, 60, 60, 50, 0.5, '#f39c12'); // 画一次
// 2. 主屏只负责 drawImage
const stars = Array.from({ length: 50 }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() – 0.5) * 2,
vy: (Math.random() – 0.5) * 2,
scale: 0.5 + Math.random() * 0.5,
rotation: Math.random() * Math.PI * 2
}));
function move() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
stars.forEach(s => {
s.x += s.vx;
s.y += s.vy;
if (s.x < 0 || s.x > canvas.width) s.vx *= –1;
if (s.y < 0 || s.y > canvas.height) s.vy *= –1;
ctx.save();
ctx.translate(s.x, s.y);
ctx.rotate(s.rotation);
ctx.scale(s.scale, s.scale);
ctx.drawImage(off, –60, –60); // 把离屏 canvas 当素材贴过来
ctx.restore();
});
requestAnimationFrame(move);
}
move();
上面这段,50 颗星星,手机老年代步机都能跑 60fps。 如果你再狠一点,可以用 requestIdleCallback 分帧更新位置,或者上 WebGL,但那就属于“卷王”领域了,本文不卷。
调试时星星画歪了?常见翻车现场和自救指南
坐标系上下颠倒 你发现星星倒着长,八成是 translate 之后忘了 rotate 方向,或者 Math.sin 用反了。Canvas 的 Y 轴向下,不是向上,记得初中数学老师的忠告吗?
路径没闭合 少写了 closePath(),结果星星缺一条边,像被老鼠啃了一口。 解决:画完 lineTo 最后回到起点,或者直接 closePath(),一键回家。
角度单位用错 rotate(30) 你以为 30°,实际上 Canvas 直接当成 30 弧度,星星转得比风扇还快。 解决:统一用 angle * Math.PI / 180,或者写个工具函数:
const DEG = d => d * Math.PI / 180;
ctx.rotate(DEG(30));
状态污染 你画完星星,下一帧整个画布都带闪粉,原因是 shadowBlur 没关。 解决:永远成对出现 save() 和 restore(),像穿袜子,别只穿一只。
开发老鸟私藏技巧:封装成组件、支持配置、还能导出图片
写到这儿,你已经可以画星星、做动画、防性能翻车。但老板又提需求:“能不能像 Ant Design 一样,一句 <star color="#f00" size="100" spin /> 就能用?” 行,给他整一个 Web Component,不用 React,不用 Vue,原生就能跑:
class HexStar extends HTMLElement {
static get observedAttributes() {
return ['size', 'color', 'spin', 'export'];
}
constructor() {
super();
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(this.canvas);
}
connectedCallback() {
this.render();
if (this.hasAttribute('spin')) this.startSpin();
}
attributeChangedCallback() {
this.render();
}
render() {
const size = parseInt(this.getAttribute('size') || '100', 10);
this.canvas.width = this.canvas.height = size;
const color = this.getAttribute('color') || '#f39c12';
drawSixStar(this.ctx, size / 2, size / 2, size * 0.4, 0.5, color);
}
startSpin() {
let r = 0;
const spin = () => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.save();
this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2);
this.ctx.rotate(r);
const color = this.getAttribute('color') || '#f39c12';
drawSixStar(this.ctx, 0, 0, this.canvas.width * 0.4, 0.5, color);
this.ctx.restore();
r += 0.02;
requestAnimationFrame(spin);
};
spin();
}
// 导出 PNG,老板最爱
exportPNG() {
return this.canvas.toDataURL('image/png');
}
}
customElements.define('hex-star', HexStar);
用的时候,跟写 HTML 一样随意:
<hex-star size="200" color="#ff0066" spin export></hex-star>
<script>
const star = document.querySelector('hex-star');
// 三秒后下载
setTimeout(() => {
const link = document.createElement('a');
link.download = 'star.png';
link.href = star.exportPNG();
link.click();
}, 3000);
</script>
封装完,你可以:
- 传参调颜色、大小、动画;
- 一键导出 PNG,发给设计师做 PPT;
- 直接丢 CDN,全公司复用,年终总结写“沉淀通用组件”,绩效稳了。
附赠可直接跑的完整源码(带注释版)
下面这份代码,复制粘贴到本地 index.html,双击即可跑。注释比我大学时抄的高数笔记还细,谁再说看不懂,就把电脑送他:
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>六角星全家桶</title>
<style>
body { background: #f7f7f7; display: flex; flex-direction: column; align-items: center; }
canvas { background: #fff; margin: 10px; box-shadow: 0 2px 8px rgba(0,0,0,.1); }
button { margin: 10px; padding: 6px 12px; }
</style>
</head>
<body>
<canvas id="stage" width="400" height="400"></canvas>
<button id="exportBtn">导出 PNG</button>
<script>
// 工具:角度转弧度
const DEG = d => d * Math.PI / 180;
// 画六角星函数,超全参数版
function drawSixStar(ctx, cx, cy, outerR, innerRatio = 0.5, fill, stroke, lineW) {
const step = Math.PI / 3;
ctx.save();
ctx.translate(cx, cy);
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const outerA = i * step – Math.PI / 2; // 从头顶开始
const x1 = Math.cos(outerA) * outerR;
const y1 = Math.sin(outerA) * outerR;
const innerA = outerA + step / 2;
const x2 = Math.cos(innerA) * outerR * innerRatio;
const y2 = Math.sin(innerA) * outerR * innerRatio;
if (i === 0) ctx.moveTo(x1, y1);
else ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
}
ctx.closePath();
if (fill) { ctx.fillStyle = fill; ctx.fill(); }
if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = lineW; ctx.stroke(); }
ctx.restore();
}
// 动画:旋转 + 呼吸 + 色相漂移
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
let frame = 0;
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const scale = 1 + Math.sin(frame * 0.05) * 0.1;
const hue = frame % 360;
const color = `hsl(${hue}, 80%, 60%)`;
ctx.save();
ctx.translate(200, 200);
ctx.rotate(DEG(frame * 0.5));
drawSixStar(ctx, 0, 0, 60 * scale, 0.5, color, '#fff', 2);
ctx.restore();
frame++;
requestAnimationFrame(animate);
}
animate();
// 导出 PNG
document.getElementById('exportBtn').onclick = () => {
const a = document.createElement('a');
a.download = 'hexStar.png';
a.href = canvas.toDataURL('image/png');
a.click();
};
</script>
</body>
</html>
写到这里,天已经蒙蒙亮,咖啡凉了,但星星在屏幕里转得正欢。 我把这份代码和踩坑笔记一并交到你手里,下次产品经理再提“炫酷六角星”,你直接把文件甩过去,附带一句:“跑不通算我输。” 然后合上电脑,去楼下买份热豆浆,让星星自己转去吧。

网硕互联帮助中心





评论前必须登录!
注册