update at 2026-01-23 12:07:22
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
"description": "luopan demo config with named color palettes",
|
||||
"background": "白",
|
||||
"strokeWidth": 1,
|
||||
"strokeColor": "冷",
|
||||
"strokeOpacity": 1,
|
||||
"strokeColor": "黑",
|
||||
"insetDistance": 1,
|
||||
"strokeOpacity": 0.5,
|
||||
"theme": {
|
||||
"name": "五行配色主题",
|
||||
"colorPalettes": {
|
||||
@@ -34,8 +34,8 @@
|
||||
},
|
||||
{
|
||||
"divisions": 2,
|
||||
"rInner": 90,
|
||||
"rOuter": 120,
|
||||
"rInner": 60,
|
||||
"rOuter": 100,
|
||||
"startAngle": 0,
|
||||
"sectors": [
|
||||
{
|
||||
@@ -53,6 +53,7 @@
|
||||
"rInner": 120,
|
||||
"rOuter": 160,
|
||||
"startAngle": 0,
|
||||
"colorRef": "水",
|
||||
"sectors": [
|
||||
{ "content": "乾", "innerFill": 1 },
|
||||
{ "content": "兑", "innerFill": 0 },
|
||||
|
||||
@@ -321,7 +321,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
const showGuides = ref(true);
|
||||
const showGuides = ref(false);
|
||||
const textRadialPosition = ref<TextRadialPosition>(DEFAULT_TEXT_RADIAL_POSITION);
|
||||
|
||||
// 缩放和平移状态
|
||||
@@ -362,7 +362,7 @@ const {
|
||||
error,
|
||||
} = useLuopan(configInput, textRadialPosition);
|
||||
|
||||
// viewBox 以实际外半径为准,确保完整显示配置中的大半径罗盘
|
||||
// 以实际外半径作为 `viewBox`,确保完整显示配置中的大半径罗盘
|
||||
const viewBoxSize = computed(() => {
|
||||
const radius = outerMost.value > 0 ? outerMost.value : props.size / 2;
|
||||
return radius * 2;
|
||||
@@ -388,7 +388,7 @@ const boundaryRings = computed(() => {
|
||||
return Array.from(set);
|
||||
});
|
||||
|
||||
// 使用 rAF 合并缩放/拖拽更新,减少渲染频率
|
||||
// 使用 `rAF` 合并缩放/拖拽更新,减少渲染频率
|
||||
const scheduleTransform = () => {
|
||||
if (rafId !== null) return;
|
||||
const requestFrame =
|
||||
@@ -416,7 +416,7 @@ const setPan = (x: number, y: number) => {
|
||||
};
|
||||
|
||||
const getUnitSvgBox = (sector: Sector, unit: TextUnit) => {
|
||||
// SVG 图标与文字共享布局规则,使用单元角度范围计算形心位置
|
||||
// `SVG` 图标与文字共享布局规则,使用单元角度范围计算形心位置
|
||||
const centroid = annularSectorCentroid({
|
||||
rInner: sector.rInner,
|
||||
rOuter: sector.rOuter,
|
||||
|
||||
@@ -19,6 +19,7 @@ export const applyPatternColoring = (
|
||||
return colorMap;
|
||||
}
|
||||
|
||||
// 规律填色:连续着色 `num` 个扇区,然后跳过 `interval` 个扇区。
|
||||
let currentIndex = 0;
|
||||
while (currentIndex < divisions) {
|
||||
for (let i = 0; i < num && currentIndex < divisions; i++) {
|
||||
@@ -70,6 +71,7 @@ export class ColorResolver {
|
||||
sector: SectorConfig | undefined,
|
||||
sectorIndex: number
|
||||
): string {
|
||||
// 优先级:扇区 `colorRef` > 层级规律色 > 背景。
|
||||
if (sector?.colorRef) {
|
||||
return this.resolveColor(sector.colorRef);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SectorBuilder } from '../sectorBuilder';
|
||||
import { buildDegreeRing } from '../degreeRing';
|
||||
import { loadCenterIcon } from '../centerIcon';
|
||||
|
||||
// 只有扇区层会生成扇区几何,其它层仍参与层级顺序。
|
||||
const isSectorLayer = (layer: LayerConfig): layer is SectorLayerConfig =>
|
||||
layer.type !== 'centerIcon' && layer.type !== 'degreeRing';
|
||||
|
||||
@@ -67,8 +68,10 @@ export function useLuopan(
|
||||
textRadialPosition: textRadialPosition.value,
|
||||
insetDistance: configObj.insetDistance,
|
||||
});
|
||||
const sectorLayers = configObj.layers.filter(isSectorLayer);
|
||||
return sectorLayers.flatMap((layer, index) => builder.buildLayer(layer, index));
|
||||
// 层索引与配置顺序一致(`centerIcon`/`degreeRing` 仍占位)。
|
||||
return configObj.layers.flatMap((layer, index) =>
|
||||
isSectorLayer(layer) ? builder.buildLayer(layer, index) : []
|
||||
);
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
@@ -156,6 +159,7 @@ export function useLuopan(
|
||||
const outerMost = computed(() => {
|
||||
if (!config.value) return 0;
|
||||
|
||||
// 取扇区层与刻度环中的最大半径。
|
||||
const radii = sectorLayers.value.map((layer) => layer.rOuter);
|
||||
const degreeRingLayer = findDegreeRingLayer(config.value.layers);
|
||||
if (degreeRingLayer) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DegreeRingConfig, TickMark, DegreeLabel, DegreeRingData } from './types';
|
||||
import { generateTextPath, polarToXY } from './utils';
|
||||
|
||||
// 根据刻度级别计算长度,后续由 clamp 处理最小值
|
||||
// 根据刻度级别计算长度,最小值由 `clampTickLength` 兜底
|
||||
const resolveTickLength = (config: DegreeRingConfig, type: TickMark['type']): number => {
|
||||
const step = config.tickLengthStep ?? 0;
|
||||
if (type === 'major') return config.tickLength;
|
||||
@@ -70,7 +70,7 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
|
||||
continue;
|
||||
}
|
||||
|
||||
// both: 同角度生成内外两条刻度线
|
||||
// 模式为 `both` 时,同角度生成内外两条刻度线
|
||||
const innerStart = polarToXY(angle, rInner);
|
||||
const innerEnd = polarToXY(angle, rInner + length);
|
||||
ticks.push({
|
||||
@@ -111,7 +111,7 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
|
||||
const aStart = angle - span / 2;
|
||||
const aEnd = angle + span / 2;
|
||||
|
||||
// 使用 textPath 保持度数方向与扇区文字一致
|
||||
// 使用 `textPath` 保持度数方向与扇区文字一致
|
||||
labels.push({
|
||||
angle,
|
||||
text,
|
||||
|
||||
@@ -37,7 +37,7 @@ export {
|
||||
getTextColorForBackground,
|
||||
} from './utils';
|
||||
|
||||
// Composables 导出
|
||||
// 组合式函数导出
|
||||
export { useLuopan } from './composables/useLuopan';
|
||||
export type { UseLuopanReturn } from './composables/useLuopan';
|
||||
|
||||
|
||||
@@ -53,12 +53,13 @@ export class SectorBuilder {
|
||||
const rawContent = typeof sectorConfig?.content === 'string' ? sectorConfig.content.trim() : '';
|
||||
const isMultiText = rawContent.includes('|');
|
||||
|
||||
// 颜色优先级:sector > layer pattern > background
|
||||
// 颜色优先级:扇区 > 规律填色 > 背景
|
||||
const fillColor = this.colorResolver.resolveSectorColor(layerColorMap, sectorConfig, i);
|
||||
const layerColor = layer.colorRef ? this.colorResolver.resolveColor(layer.colorRef) : undefined;
|
||||
const sectorColor = sectorConfig?.colorRef
|
||||
? this.colorResolver.resolveColor(sectorConfig.colorRef)
|
||||
: undefined;
|
||||
// 扇区的 `innerFill` 优先级高于层级的 `innerFill`。
|
||||
const innerFill = (sectorConfig?.innerFill ?? layer.innerFill ?? 0) === 1;
|
||||
const innerFillPath = innerFill
|
||||
? annularSectorInsetPath(
|
||||
@@ -72,6 +73,7 @@ export class SectorBuilder {
|
||||
const normalizedInnerFillPath =
|
||||
innerFillPath && innerFillPath.length > 0 ? innerFillPath : undefined;
|
||||
const hasInnerFillPath = Boolean(normalizedInnerFillPath);
|
||||
// `innerFill` 开启时:外圈保持白色,仅填充内缩块。
|
||||
const baseFillColor = hasInnerFillPath ? '#ffffff' : fillColor;
|
||||
const innerFillColor = hasInnerFillPath ? sectorColor ?? layerColor ?? fillColor : undefined;
|
||||
const textBaseColor = hasInnerFillPath ? innerFillColor ?? fillColor : fillColor;
|
||||
@@ -79,6 +81,7 @@ export class SectorBuilder {
|
||||
|
||||
const sectorKey = `L${layerIndex}-P${i}`;
|
||||
const textPathId = `text-path-${sectorKey}`;
|
||||
// 最内层使用形心位置,保证文字可读性。
|
||||
const effectiveTextRadialPosition =
|
||||
layerIndex === 0 ? 'centroid' : this.textRadialPosition;
|
||||
|
||||
@@ -222,13 +225,14 @@ export class SectorBuilder {
|
||||
}
|
||||
|
||||
private shouldShowGroupSplit(layer: SectorLayerConfig, sectorIndex: number): boolean {
|
||||
// groupSplit 关闭时,仅保留分组边界线
|
||||
// `groupSplit` 关闭时,仅保留分组边界线
|
||||
if (layer.groupSplit !== false) return true;
|
||||
if (!layer.num) return true;
|
||||
|
||||
const cycleLength = layer.num + (layer.interval ?? 0);
|
||||
const posInCycle = sectorIndex % cycleLength;
|
||||
|
||||
// 仅保留每个着色分组的末尾分割线。
|
||||
return posInCycle >= layer.num - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function annularSectorCentroid(params: AnnularSectorParams): CentroidResu
|
||||
return { cx: 0, cy: 0, rho: 0, aMidDeg, aMidRad, deltaDeg };
|
||||
}
|
||||
|
||||
// rho = (2/3) * (r2^3 - r1^3)/(r2^2 - r1^2) * sinc(delta/2)
|
||||
// 形心径向距离公式:`rho = (2/3) * (r2^3 - r1^3)/(r2^2 - r1^2) * sinc(delta/2)`
|
||||
const delta = (deltaDeg * Math.PI) / 180;
|
||||
const radialFactor =
|
||||
(2 / 3) * ((rOuter ** 3 - rInner ** 3) / (rOuter ** 2 - rInner ** 2));
|
||||
@@ -124,7 +124,7 @@ export function annularSectorPath(
|
||||
let delta = a2 - a1;
|
||||
if (delta < 0) delta += 360;
|
||||
|
||||
// SVG arc flags
|
||||
// `SVG` 圆弧标记位
|
||||
const largeArc = delta > 180 ? 1 : 0;
|
||||
const sweepOuter = 1;
|
||||
const sweepInner = 0;
|
||||
@@ -134,7 +134,7 @@ export function annularSectorPath(
|
||||
const p3 = polarToXY(a2, rInner);
|
||||
const p4 = polarToXY(a1, rInner);
|
||||
|
||||
// 如果 rInner=0,内弧退化为点
|
||||
// 如果 `rInner`=0,内弧退化为点
|
||||
if (rInner <= 0.000001) {
|
||||
return [
|
||||
`M ${p1.x} ${p1.y}`,
|
||||
|
||||
17
todolist.md
17
todolist.md
@@ -69,7 +69,7 @@
|
||||
| rOuter | number | 是 | 层外半径,单位:像素 | 200 |
|
||||
| startAngle | number | 否 | 第一个扇区的起始角度(度,0度为正北,顺时针),默认0 | 0 |
|
||||
| colorRef | string | 否 | 层级颜色引用,引用theme.colorPalettes中的颜色名 | "土" |
|
||||
| innerFill | number | 否 | 内缩设置:0=不内缩,1=内缩1像素,用于规律填色的扇区 | 1 |
|
||||
| innerFill | number | 否 | 内缩设置:0=不内缩,1=内缩1像素 | 1 |
|
||||
| num | number | 否 | 规律填色:连续着色的扇区数量,与interval配合使用 | 3 |
|
||||
| interval | number | 否 | 规律填色:着色后间隔的扇区数量,0表示无间隔 | 1 |
|
||||
| groupSplit | boolean | 否 | 是否显示同组扇区间分割线,false隐藏,默认true | false |
|
||||
@@ -87,18 +87,27 @@
|
||||
- 1个单元:100%;2个:[0.5, 0.5];3个:[0.25, 0.5, 0.25];4个:[0.2, 0.3, 0.3, 0.2];5个以上:平均
|
||||
- 单元内自动计算字体大小,单元间无分割线
|
||||
|
||||
### 扇区内缩规则
|
||||
innerFill表示扇区内缩,可能在layer或者sector。layer中配置作用于整个层,sector配置作用于单个扇区。当innerfill=1,内缩1px。
|
||||
内缩块的填色规则与不内缩相同,内缩块边界和扇区边界之间的区域填白色。
|
||||
如果同某个layer指定了inner Fill,该layer下的某sector中也指定了innerFill,且两者不同,以sector innerFill为准。也就是说sector配置可以对layer配置做修正。
|
||||
sector innerFill > layer innerFill
|
||||
|
||||
### 扇区背景色着色原则:
|
||||
最高优先级,在layer中指定colorRef
|
||||
第二优先级:colorRef规律填色,也就是说,如果同一个sector中指定了colorRef,该sector也指定了layer级别的colorRef,以前者为准,innerFill使用相同规则。
|
||||
colorRef可能在layer或者sector。
|
||||
如果同一个sector中指定了colorRef,该sector也指定了layer级别的colorRef,以sector的colorRef为准。
|
||||
sector colorRef > layer colorRef > 全局background颜色
|
||||
参数:
|
||||
startAngle表示第一个扇区的起始角度(以度为单位,0度为正北方向,顺时针增加)
|
||||
innerfill对num、interval定义的着色扇区生效
|
||||
innerfill仅对num/interval定义的着色扇区生效(目前layer中的innerFill对所有sector生效,暂保持这个逻辑)
|
||||
-- start表示着色起始扇区(已废弃,统一从第1个扇区开始)
|
||||
num表示连接几个单元着色
|
||||
interval表示中间间隔几个单元
|
||||
比如num=3,interval=1,意思是从第1个扇区开始着色,对1、2、3扇区着色colorref,4扇区全局背景,5、6、7着色colorref……
|
||||
groupSplit: 隐藏同组扇区之间的分割线, false表示不显示group中间分割线。如该参数不设置,取默认值true,显示。
|
||||
|
||||
规律填色有个特殊情况,divisions mod (num+interval) 不等于 0,比如divisions=16,num=2,interval=1,16mod3=1,多出来的1个应该使用num同样的colorRef和innerFill设定。
|
||||
|
||||
”layers“:
|
||||
{
|
||||
-- ========================================
|
||||
|
||||
Reference in New Issue
Block a user