平面几何:如何绘制一个星形?

大家好,我是前端西瓜哥。

也是有一个月没写文章了。主要是 AI 太强了,简单的东西已经没有写的必要的,复杂的不好写。但多少还是要写点。

今天我们来绘制 Figma 的星形。

星形的绘制,比较简单,其实就是求两个同心圆的内接正多边形的点,将两组点两两连接即可。

方法的参数有:width、height、count、innerScale。方法签名为:

less 复制代码
(
  width: number,
  height: number,
  count: number,
  innerScale: number,
) => Point[];

首先是 归一,求出宽高为 1 的矩形下(其实也是半径为 0.5 的原型)的内接多边形的点集。

从最顶部的点开始,不断地旋转 360 / count 的角度,得到 count 个点。中心点是坐标原点。

ini 复制代码
const getInnerRegularPolygon = (radius: number, count: number): Point[] => {
const p = { x: 0, y: -radius };
const points: Point[] = [p];

const dAngle = (Math.PI * 2) / count;

for (let i = 1; i < count; i++) {
    points.push(rotate(p, dAngle * i));
  }

return points;
};

const rotate = (p: Point, rad: number) => {
return {
    x: p.x * Math.cos(rad) - p.y * Math.sin(rad),
    y: p.x * Math.sin(rad) + p.y * Math.cos(rad),
  };
};

这里是基于第一个点,不断地应用一个新的旋转角度。

还有一种方案是基于上一个点,做同样的增量旋转矩阵,但不是很建议,这是一种 "累加" 的策略,会导致误差的累加。

中间步骤越多,误差就累加的越大。类似的有对图形的移动,基于起点的位移会更可靠,基于 mousemove 的上一个点位移则会有很多问题。

接下来是绘制更小内接多边形。

半径改为 innerScale 就好了。innerScale 代表的小圆是相对大圆的大小。

不过起点的位置需要调整下,要顺时针旋转 360 / count / 2 的角度,然后再基于这个点去旋转。

否则你可能得到下面这样一个多边形环。

所以需要改造下 getInnerRegularPolygon,提供个起始角度。

ini 复制代码
const getInnerRegularPolygon = (
  radius: number,
  count: number,
  startAngle: number = 0,
): Point[] => {
let p = { x: 0, y: -radius };
if (startAngle) {
    p = rotate(p, startAngle);
  }
const points: Point[] = [p];

const dAngle = (Math.PI * 2) / count;

for (let i = 1; i < count; i++) {
    points.push(rotate(p, dAngle * i));
  }

return points;
};

然后我们得到一个大的正多边形,和一个小的歪了点的正多边形。

点是对了,就是点的顺序要调整下。我们对大多边形和小多边形的两组点,两两顺排。

ini 复制代码
const outerPoints = getInnerRegularPolygon(1, count);
const innerPoints = getInnerRegularPolygon(
  innerScale,
  count,
  Math.PI / count,
);

const points = [];
for (let i = 0; i < count; i++) {
  points.push(outerPoints[i]);
  points.push(innerPoints[i]);
}

到这里我们绘制了一个 2x2 圆的内接星形。(到这里才发现 1x1 要传入 0.5 或者改多边形算法实现才行,想了下 2x2 也问题不大)

后面我们给这些点放大和位移 就齐活了。scale(width/2, height/2) * translate(width/2, height/2)

ini 复制代码
for (let i = 0; i < points.length; i++) {
  points[i].x = points[i].x * halfWidth + halfWidth;
  points[i].y = points[i].y * halfHeight + halfHeight;
}

return points;

体验

线上体验地址:

geo-play-nv7v.vercel.app/src/page/st...

特殊的,innerScale 是 1 的话,就会让 n 角星形变成 2n 多边形。

另外,可以看到,包围盒其实是一个圆形,而不是矩形 ,这就是为什么 Figma 的 星形和多边形在矩形包围盒下会有空隙 的原因。因为包围盒它不是圆形的。

代码实现

ini 复制代码
interface Point {
  x: number;
  y: number;
}

const getInnerRegularPolygon = (
  radius: number,
  count: number,
  startAngle: number = 0,
): Point[] => {
let p = { x: 0, y: -radius };
if (startAngle) {
    p = rotate(p, startAngle);
  }
const points: Point[] = [p];

const dAngle = (Math.PI * 2) / count;

for (let i = 1; i < count; i++) {
    points.push(rotate(p, dAngle * i));
  }

return points;
};

const rotate = (p: Point, rad: number) => {
return {
    x: p.x * Math.cos(rad) - p.y * Math.sin(rad),
    y: p.x * Math.sin(rad) + p.y * Math.cos(rad),
  };
};

exportconst getStarPoints = (
  width: number,
  height: number,
  count: number,
  innerScale: number,
): Point[] => {
const outerPoints = getInnerRegularPolygon(1, count);
const innerPoints = getInnerRegularPolygon(
    innerScale,
    count,
    Math.PI / count,
  );

const points = [];
for (let i = 0; i < count; i++) {
    points.push(outerPoints[i]);
    points.push(innerPoints[i]);
  }

const halfWidth = width / 2;
const halfHeight = height / 2;

// scale(width/2, height/2) * translate(width/2, height/2)
for (let i = 0; i < points.length; i++) {
    points[i].x = points[i].x * halfWidth + halfWidth;
    points[i].y = points[i].y * halfHeight + halfHeight;
  }

return points;
};

结尾

星形,本质是两个多边形的点的交替连接。

几何算法很有趣吧。

我是前端西瓜哥,关注我,学习更多平面几何知识。


相关阅读,

平面几何:求内接或外切于圆的正多边形

平面几何:判断点是否在多边形内(射线法)

相关推荐
天天扭码6 小时前
解放双手!使用Cursor+Figma MCP 高效还原响应式设计稿
前端·mcp
今天不要写bug7 小时前
基于qrcode前端实现链接转二维码的生成与下载
前端·javascript·typescript·vue
JIngJaneIL7 小时前
基于Java + vue干洗店预约洗衣系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
搬山境KL攻城狮7 小时前
记-SPA单页面应用Chrome自动翻译导致中文错别字问题
前端·chrome
HIT_Weston7 小时前
61、【Ubuntu】【Gitlab】拉出内网 Web 服务:Gitlab 配置审视(五)
前端·ubuntu·gitlab
旺仔Sec7 小时前
2026年度河北省职业院校技能竞赛“Web技术”(高职组)赛项竞赛任务
运维·服务器·前端
用户4099322502127 小时前
Vue的Class绑定对象语法如何让动态类名切换变得直观高效?
前端·ai编程·trae
GIS之路8 小时前
GIS 数据转换:GDAL 实现将 CSV 转换为 Shp 数据(一)
前端
武清伯MVP8 小时前
深入了解Canvas:HTML5时代的绘图利器(一)
前端·html5·canvas