update at 2026-01-22 18:43:01

This commit is contained in:
douboer
2026-01-22 18:43:01 +08:00
parent c23c71eabf
commit a930a99a50
23 changed files with 2082 additions and 1186 deletions

137
src/degreeRing.ts Normal file
View File

@@ -0,0 +1,137 @@
import type { DegreeRingConfig, TickMark, DegreeLabel, DegreeRingData } from './types';
import { generateTextPath, polarToXY } from './utils';
// 根据刻度级别计算长度,后续由 clamp 处理最小值
const resolveTickLength = (config: DegreeRingConfig, type: TickMark['type']): number => {
const step = config.tickLengthStep ?? 0;
if (type === 'major') return config.tickLength;
if (type === 'minor') return config.tickLength - step;
return config.tickLength - 2 * step;
};
const clampTickLength = (value: number): number => (value < 1 ? 1 : value);
export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
const ticks: TickMark[] = [];
const { rInner, rOuter, mode } = config;
const labelFontSize = 8;
const majorTick = Math.max(1, config.majorTick);
const minorTick = Math.max(1, config.minorTick);
const microTick = Math.max(1, config.microTick);
for (let angle = 0; angle < 360; angle++) {
let type: TickMark['type'] | null = null;
if (angle % majorTick === 0) {
type = 'major';
} else if (angle % minorTick === 0) {
type = 'minor';
} else if (angle % microTick === 0) {
type = 'micro';
}
if (!type) continue;
const length = clampTickLength(resolveTickLength(config, type));
// 预计算坐标,避免渲染时重复三角计算
if (mode === 'inner') {
const start = polarToXY(angle, rInner);
const end = polarToXY(angle, rInner + length);
ticks.push({
angle,
type,
length,
startR: rInner,
endR: rInner + length,
x1: start.x,
y1: start.y,
x2: end.x,
y2: end.y,
});
continue;
}
if (mode === 'outer') {
const start = polarToXY(angle, rOuter - length);
const end = polarToXY(angle, rOuter);
ticks.push({
angle,
type,
length,
startR: rOuter - length,
endR: rOuter,
x1: start.x,
y1: start.y,
x2: end.x,
y2: end.y,
});
continue;
}
// both: 同角度生成内外两条刻度线
const innerStart = polarToXY(angle, rInner);
const innerEnd = polarToXY(angle, rInner + length);
ticks.push({
angle,
type,
length,
startR: rInner,
endR: rInner + length,
x1: innerStart.x,
y1: innerStart.y,
x2: innerEnd.x,
y2: innerEnd.y,
});
const outerStart = polarToXY(angle, rOuter - length);
const outerEnd = polarToXY(angle, rOuter);
ticks.push({
angle,
type,
length,
startR: rOuter - length,
endR: rOuter,
x1: outerStart.x,
y1: outerStart.y,
x2: outerEnd.x,
y2: outerEnd.y,
});
}
const labels: DegreeLabel[] = [];
if (config.showDegree === 1) {
for (let angle = 0; angle < 360; angle += majorTick) {
const r = (rInner + rOuter) / 2;
const text = angle.toString();
const estimatedArcLength = text.length * labelFontSize * 1.1;
const estimatedSpan = r > 0 ? (estimatedArcLength / r) * (180 / Math.PI) : 0;
const maxSpan = Math.max(majorTick * 0.9, 4);
const span = Math.min(Math.max(estimatedSpan, 4), maxSpan);
const aStart = angle - span / 2;
const aEnd = angle + span / 2;
// 使用 textPath 保持度数方向与扇区文字一致
labels.push({
angle,
text,
r,
fontSize: labelFontSize,
textPathId: `degree-label-${angle}`,
textPath: generateTextPath(r, r, aStart, aEnd, 'middle'),
});
}
}
return {
ticks,
tickColor: config.tickColor,
ring: {
rInner,
rOuter,
color: config.ringColor,
opacity: config.opacity,
},
labels: labels.length > 0 ? labels : undefined,
};
}