130 lines
4.2 KiB
TypeScript
130 lines
4.2 KiB
TypeScript
/**
|
||
* 为正多边形添加圆角
|
||
* 将每个角转换为 segmentsPerCorner 条线段,线段的端点在圆角的圆弧上
|
||
*
|
||
* 对每个角:
|
||
* 1. 寻找圆心
|
||
* 2. 将圆心投影到两条边上
|
||
* 3. 以圆弧连接投影点
|
||
* @param vertices 多边形顶点数组(顺时针或逆时针)
|
||
* @param cornerRadius 圆角半径
|
||
* @param segmentsPerCorner 每个圆角的分段数
|
||
* @returns 带圆角的多边形轮廓点
|
||
*/
|
||
export function getRoundedPolygonPoints(
|
||
vertices: [number, number][],
|
||
cornerRadius: number,
|
||
segmentsPerCorner: number = 4
|
||
): [number, number][] {
|
||
if (vertices.length < 3) {
|
||
return [...vertices];
|
||
}
|
||
|
||
const result: [number, number][] = [];
|
||
|
||
for (let i = 0; i < vertices.length; i++) {
|
||
const prev = vertices[(i - 1 + vertices.length) % vertices.length];
|
||
const curr = vertices[i];
|
||
const next = vertices[(i + 1) % vertices.length];
|
||
|
||
// 获取圆角圆心
|
||
const center = getTangentCircleCenter(prev, curr, next, cornerRadius);
|
||
|
||
// 获取圆心到两条边的投影点(圆角的起点和终点)
|
||
const start = getProjectedPoint(center, prev, curr);
|
||
const end = getProjectedPoint(center, curr, next);
|
||
|
||
// 计算起始角度和结束角度
|
||
const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
|
||
const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
|
||
|
||
// 判断方向(顺时针或逆时针)
|
||
const cross = (curr[0] - prev[0]) * (next[1] - prev[1]) - (curr[1] - prev[1]) * (next[0] - prev[0]);
|
||
const isClockwise = cross < 0;
|
||
|
||
// 计算角度差,确保沿着正确的方向
|
||
let angleDiff = endAngle - startAngle;
|
||
if (isClockwise) {
|
||
if (angleDiff > 0) angleDiff -= Math.PI * 2;
|
||
} else {
|
||
if (angleDiff < 0) angleDiff += Math.PI * 2;
|
||
}
|
||
|
||
// 生成圆弧上的点
|
||
for (let j = 0; j <= segmentsPerCorner; j++) {
|
||
const t = j / segmentsPerCorner;
|
||
const angle = startAngle + angleDiff * t;
|
||
const x = center[0] + cornerRadius * Math.cos(angle);
|
||
const y = center[1] + cornerRadius * Math.sin(angle);
|
||
result.push([x, y]);
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 对于给定的角 abc,获取与 ab 和 bc 相切的半径为 radius 的圆的圆心位置。
|
||
* @param va
|
||
* @param vb
|
||
* @param vc
|
||
* @param radius
|
||
*/
|
||
export function getTangentCircleCenter(
|
||
va: [number, number],
|
||
vb: [number, number],
|
||
vc: [number, number],
|
||
radius: number
|
||
): [number, number] {
|
||
// 计算两条边的单位向量
|
||
const ba: [number, number] = [va[0] - vb[0], va[1] - vb[1]];
|
||
const bc: [number, number] = [vc[0] - vb[0], vc[1] - vb[1]];
|
||
|
||
const baLen = Math.sqrt(ba[0] ** 2 + ba[1] ** 2);
|
||
const bcLen = Math.sqrt(bc[0] ** 2 + bc[1] ** 2);
|
||
|
||
const baUnit: [number, number] = [ba[0] / baLen, ba[1] / baLen];
|
||
const bcUnit: [number, number] = [bc[0] / bcLen, bc[1] / bcLen];
|
||
|
||
// 角平分线方向
|
||
const bisector: [number, number] = [baUnit[0] + bcUnit[0], baUnit[1] + bcUnit[1]];
|
||
const bisectorLen = Math.sqrt(bisector[0] ** 2 + bisector[1] ** 2);
|
||
const bisectorUnit: [number, number] = [bisector[0] / bisectorLen, bisector[1] / bisectorLen];
|
||
|
||
// 计算半角的正弦值
|
||
const halfAngle = Math.acos((baUnit[0] * bcUnit[0] + baUnit[1] * bcUnit[1]));
|
||
const sinHalfAngle = Math.sin(halfAngle / 2);
|
||
|
||
// 圆心到顶点的距离
|
||
const centerDist = radius / sinHalfAngle;
|
||
|
||
// 圆心位置(从顶点沿角平分线向外)
|
||
return [
|
||
vb[0] + bisectorUnit[0] * centerDist,
|
||
vb[1] + bisectorUnit[1] * centerDist
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 将 v 投影到 ab 线段上
|
||
* @param v
|
||
* @param a
|
||
* @param b
|
||
*/
|
||
export function getProjectedPoint(
|
||
v: [number, number],
|
||
a: [number, number],
|
||
b: [number, number],
|
||
): [number, number] {
|
||
const ab: [number, number] = [b[0] - a[0], b[1] - a[1]];
|
||
const av: [number, number] = [v[0] - a[0], v[1] - a[1]];
|
||
|
||
const abLenSq = ab[0] ** 2 + ab[1] ** 2;
|
||
const t = (av[0] * ab[0] + av[1] * ab[1]) / abLenSq;
|
||
|
||
return [
|
||
a[0] + ab[0] * t,
|
||
a[1] + ab[1] * t
|
||
];
|
||
}
|