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

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

也是有一个月没写文章了。主要是 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;
};

结尾

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

几何算法很有趣吧。

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


相关阅读,

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

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

相关推荐
百度地图汽车版1 分钟前
【智图译站】基于 LightGBM 与 GNSS 多特征驱动的 NLOS 误差可靠识别方法
前端
有意义7 分钟前
用心写好一个登录页:代码、体验与细节的平衡
前端·react.js·交互设计
程序员Agions8 分钟前
React 自定义 Hooks 生存指南:7 个让你少加班的"偷懒"神器
前端·javascript
爱学习的小康11 分钟前
js 文件读取 修改 创建
前端·javascript·vue.js
2501_9418705612 分钟前
从配置频繁变动到动态配置体系落地的互联网系统工程实践随笔与多语言语法思考
java·前端·python
百度地图汽车版28 分钟前
【AI地图 Tech说】第二期:一文解码百度地图ETA
前端
恋猫de小郭30 分钟前
罗技鼠标因为服务器证书过期无法使用?我是如何解决 SSL 证书问题
android·前端·flutter
Sailing32 分钟前
AI 流式对话该怎么做?SSE、fetch、axios 一次讲清楚
前端·javascript·面试
橙露38 分钟前
Vue3 组件通信全解析:技术细节、适用场景与性能优化
前端·javascript·vue.js