diff --git a/public/demo.json b/public/demo.json index 506272f..9c6828c 100644 --- a/public/demo.json +++ b/public/demo.json @@ -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 }, diff --git a/src/Luopan.vue b/src/Luopan.vue index de10f46..7627c5e 100644 --- a/src/Luopan.vue +++ b/src/Luopan.vue @@ -321,7 +321,7 @@ const props = withDefaults(defineProps(), { /** * 状态 */ -const showGuides = ref(true); +const showGuides = ref(false); const textRadialPosition = ref(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, diff --git a/src/colorResolver.ts b/src/colorResolver.ts index 5aca46c..cf23d0e 100644 --- a/src/colorResolver.ts +++ b/src/colorResolver.ts @@ -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); } diff --git a/src/composables/useLuopan.ts b/src/composables/useLuopan.ts index 23b34e3..d85de12 100644 --- a/src/composables/useLuopan.ts +++ b/src/composables/useLuopan.ts @@ -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) { diff --git a/src/degreeRing.ts b/src/degreeRing.ts index c2958cd..1530df7 100644 --- a/src/degreeRing.ts +++ b/src/degreeRing.ts @@ -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, diff --git a/src/index.ts b/src/index.ts index 1df9317..67d5773 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,7 @@ export { getTextColorForBackground, } from './utils'; -// Composables 导出 +// 组合式函数导出 export { useLuopan } from './composables/useLuopan'; export type { UseLuopanReturn } from './composables/useLuopan'; diff --git a/src/sectorBuilder.ts b/src/sectorBuilder.ts index b903943..2ef5e71 100644 --- a/src/sectorBuilder.ts +++ b/src/sectorBuilder.ts @@ -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; } } diff --git a/src/utils.ts b/src/utils.ts index 19e434f..3d9dcd7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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}`, diff --git a/todolist.md b/todolist.md index a0254ae..db63f5b 100644 --- a/todolist.md +++ b/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“: { -- ======================================== @@ -146,8 +155,8 @@ "innerFill": 0, -- 着色区域的内缩设置 "colorRef": "火", -- 着色使用的颜色引用 "num": 3, -- 连续着色3个扇区,每个区域跨3度 - "interval": 2, -- 着色后间隔1个扇区 - "groupSplit": false -- 隐藏同组扇区之间的分割线, false表示不显示group中间分割线,该参数不设置,默认显示。 + "interval": 2, -- 着色后间隔1个扇区 + "groupSplit": false -- 隐藏同组扇区之间的分割线, false表示不显示group中间分割线,该参数不设置,默认显示。 }, -- ========================================