update at 2026-01-23 12:07:22

This commit is contained in:
douboer@gmail.com
2026-01-23 12:07:22 +08:00
parent 405576c7c3
commit f0234d1d8a
9 changed files with 45 additions and 25 deletions

View File

@@ -3,9 +3,9 @@
"description": "luopan demo config with named color palettes", "description": "luopan demo config with named color palettes",
"background": "白", "background": "白",
"strokeWidth": 1, "strokeWidth": 1,
"strokeColor": "冷", "strokeOpacity": 1,
"strokeColor": "黑",
"insetDistance": 1, "insetDistance": 1,
"strokeOpacity": 0.5,
"theme": { "theme": {
"name": "五行配色主题", "name": "五行配色主题",
"colorPalettes": { "colorPalettes": {
@@ -34,8 +34,8 @@
}, },
{ {
"divisions": 2, "divisions": 2,
"rInner": 90, "rInner": 60,
"rOuter": 120, "rOuter": 100,
"startAngle": 0, "startAngle": 0,
"sectors": [ "sectors": [
{ {
@@ -53,6 +53,7 @@
"rInner": 120, "rInner": 120,
"rOuter": 160, "rOuter": 160,
"startAngle": 0, "startAngle": 0,
"colorRef": "水",
"sectors": [ "sectors": [
{ "content": "乾", "innerFill": 1 }, { "content": "乾", "innerFill": 1 },
{ "content": "兑", "innerFill": 0 }, { "content": "兑", "innerFill": 0 },

View File

@@ -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); const textRadialPosition = ref<TextRadialPosition>(DEFAULT_TEXT_RADIAL_POSITION);
// 缩放和平移状态 // 缩放和平移状态
@@ -362,7 +362,7 @@ const {
error, error,
} = useLuopan(configInput, textRadialPosition); } = useLuopan(configInput, textRadialPosition);
// viewBox 以实际外半径为准,确保完整显示配置中的大半径罗盘 // 以实际外半径作为 `viewBox`,确保完整显示配置中的大半径罗盘
const viewBoxSize = computed(() => { const viewBoxSize = computed(() => {
const radius = outerMost.value > 0 ? outerMost.value : props.size / 2; const radius = outerMost.value > 0 ? outerMost.value : props.size / 2;
return radius * 2; return radius * 2;
@@ -388,7 +388,7 @@ const boundaryRings = computed(() => {
return Array.from(set); return Array.from(set);
}); });
// 使用 rAF 合并缩放/拖拽更新,减少渲染频率 // 使用 `rAF` 合并缩放/拖拽更新,减少渲染频率
const scheduleTransform = () => { const scheduleTransform = () => {
if (rafId !== null) return; if (rafId !== null) return;
const requestFrame = const requestFrame =
@@ -416,7 +416,7 @@ const setPan = (x: number, y: number) => {
}; };
const getUnitSvgBox = (sector: Sector, unit: TextUnit) => { const getUnitSvgBox = (sector: Sector, unit: TextUnit) => {
// SVG 图标与文字共享布局规则,使用单元角度范围计算形心位置 // `SVG` 图标与文字共享布局规则,使用单元角度范围计算形心位置
const centroid = annularSectorCentroid({ const centroid = annularSectorCentroid({
rInner: sector.rInner, rInner: sector.rInner,
rOuter: sector.rOuter, rOuter: sector.rOuter,

View File

@@ -19,6 +19,7 @@ export const applyPatternColoring = (
return colorMap; return colorMap;
} }
// 规律填色:连续着色 `num` 个扇区,然后跳过 `interval` 个扇区。
let currentIndex = 0; let currentIndex = 0;
while (currentIndex < divisions) { while (currentIndex < divisions) {
for (let i = 0; i < num && currentIndex < divisions; i++) { for (let i = 0; i < num && currentIndex < divisions; i++) {
@@ -70,6 +71,7 @@ export class ColorResolver {
sector: SectorConfig | undefined, sector: SectorConfig | undefined,
sectorIndex: number sectorIndex: number
): string { ): string {
// 优先级:扇区 `colorRef` > 层级规律色 > 背景。
if (sector?.colorRef) { if (sector?.colorRef) {
return this.resolveColor(sector.colorRef); return this.resolveColor(sector.colorRef);
} }

View File

@@ -19,6 +19,7 @@ import { SectorBuilder } from '../sectorBuilder';
import { buildDegreeRing } from '../degreeRing'; import { buildDegreeRing } from '../degreeRing';
import { loadCenterIcon } from '../centerIcon'; import { loadCenterIcon } from '../centerIcon';
// 只有扇区层会生成扇区几何,其它层仍参与层级顺序。
const isSectorLayer = (layer: LayerConfig): layer is SectorLayerConfig => const isSectorLayer = (layer: LayerConfig): layer is SectorLayerConfig =>
layer.type !== 'centerIcon' && layer.type !== 'degreeRing'; layer.type !== 'centerIcon' && layer.type !== 'degreeRing';
@@ -67,8 +68,10 @@ export function useLuopan(
textRadialPosition: textRadialPosition.value, textRadialPosition: textRadialPosition.value,
insetDistance: configObj.insetDistance, insetDistance: configObj.insetDistance,
}); });
const sectorLayers = configObj.layers.filter(isSectorLayer); // 层索引与配置顺序一致(`centerIcon`/`degreeRing` 仍占位)。
return sectorLayers.flatMap((layer, index) => builder.buildLayer(layer, index)); return configObj.layers.flatMap((layer, index) =>
isSectorLayer(layer) ? builder.buildLayer(layer, index) : []
);
}; };
const loadConfig = async () => { const loadConfig = async () => {
@@ -156,6 +159,7 @@ export function useLuopan(
const outerMost = computed(() => { const outerMost = computed(() => {
if (!config.value) return 0; if (!config.value) return 0;
// 取扇区层与刻度环中的最大半径。
const radii = sectorLayers.value.map((layer) => layer.rOuter); const radii = sectorLayers.value.map((layer) => layer.rOuter);
const degreeRingLayer = findDegreeRingLayer(config.value.layers); const degreeRingLayer = findDegreeRingLayer(config.value.layers);
if (degreeRingLayer) { if (degreeRingLayer) {

View File

@@ -1,7 +1,7 @@
import type { DegreeRingConfig, TickMark, DegreeLabel, DegreeRingData } from './types'; import type { DegreeRingConfig, TickMark, DegreeLabel, DegreeRingData } from './types';
import { generateTextPath, polarToXY } from './utils'; import { generateTextPath, polarToXY } from './utils';
// 根据刻度级别计算长度,后续由 clamp 处理最小值 // 根据刻度级别计算长度,最小值`clampTickLength` 兜底
const resolveTickLength = (config: DegreeRingConfig, type: TickMark['type']): number => { const resolveTickLength = (config: DegreeRingConfig, type: TickMark['type']): number => {
const step = config.tickLengthStep ?? 0; const step = config.tickLengthStep ?? 0;
if (type === 'major') return config.tickLength; if (type === 'major') return config.tickLength;
@@ -70,7 +70,7 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
continue; continue;
} }
// both: 同角度生成内外两条刻度线 // 模式为 `both` 时,同角度生成内外两条刻度线
const innerStart = polarToXY(angle, rInner); const innerStart = polarToXY(angle, rInner);
const innerEnd = polarToXY(angle, rInner + length); const innerEnd = polarToXY(angle, rInner + length);
ticks.push({ ticks.push({
@@ -111,7 +111,7 @@ export function buildDegreeRing(config: DegreeRingConfig): DegreeRingData {
const aStart = angle - span / 2; const aStart = angle - span / 2;
const aEnd = angle + span / 2; const aEnd = angle + span / 2;
// 使用 textPath 保持度数方向与扇区文字一致 // 使用 `textPath` 保持度数方向与扇区文字一致
labels.push({ labels.push({
angle, angle,
text, text,

View File

@@ -37,7 +37,7 @@ export {
getTextColorForBackground, getTextColorForBackground,
} from './utils'; } from './utils';
// Composables 导出 // 组合式函数导出
export { useLuopan } from './composables/useLuopan'; export { useLuopan } from './composables/useLuopan';
export type { UseLuopanReturn } from './composables/useLuopan'; export type { UseLuopanReturn } from './composables/useLuopan';

View File

@@ -53,12 +53,13 @@ export class SectorBuilder {
const rawContent = typeof sectorConfig?.content === 'string' ? sectorConfig.content.trim() : ''; const rawContent = typeof sectorConfig?.content === 'string' ? sectorConfig.content.trim() : '';
const isMultiText = rawContent.includes('|'); const isMultiText = rawContent.includes('|');
// 颜色优先级:sector > layer pattern > background // 颜色优先级:扇区 > 规律填色 > 背景
const fillColor = this.colorResolver.resolveSectorColor(layerColorMap, sectorConfig, i); const fillColor = this.colorResolver.resolveSectorColor(layerColorMap, sectorConfig, i);
const layerColor = layer.colorRef ? this.colorResolver.resolveColor(layer.colorRef) : undefined; const layerColor = layer.colorRef ? this.colorResolver.resolveColor(layer.colorRef) : undefined;
const sectorColor = sectorConfig?.colorRef const sectorColor = sectorConfig?.colorRef
? this.colorResolver.resolveColor(sectorConfig.colorRef) ? this.colorResolver.resolveColor(sectorConfig.colorRef)
: undefined; : undefined;
// 扇区的 `innerFill` 优先级高于层级的 `innerFill`。
const innerFill = (sectorConfig?.innerFill ?? layer.innerFill ?? 0) === 1; const innerFill = (sectorConfig?.innerFill ?? layer.innerFill ?? 0) === 1;
const innerFillPath = innerFill const innerFillPath = innerFill
? annularSectorInsetPath( ? annularSectorInsetPath(
@@ -72,6 +73,7 @@ export class SectorBuilder {
const normalizedInnerFillPath = const normalizedInnerFillPath =
innerFillPath && innerFillPath.length > 0 ? innerFillPath : undefined; innerFillPath && innerFillPath.length > 0 ? innerFillPath : undefined;
const hasInnerFillPath = Boolean(normalizedInnerFillPath); const hasInnerFillPath = Boolean(normalizedInnerFillPath);
// `innerFill` 开启时:外圈保持白色,仅填充内缩块。
const baseFillColor = hasInnerFillPath ? '#ffffff' : fillColor; const baseFillColor = hasInnerFillPath ? '#ffffff' : fillColor;
const innerFillColor = hasInnerFillPath ? sectorColor ?? layerColor ?? fillColor : undefined; const innerFillColor = hasInnerFillPath ? sectorColor ?? layerColor ?? fillColor : undefined;
const textBaseColor = hasInnerFillPath ? innerFillColor ?? fillColor : fillColor; const textBaseColor = hasInnerFillPath ? innerFillColor ?? fillColor : fillColor;
@@ -79,6 +81,7 @@ export class SectorBuilder {
const sectorKey = `L${layerIndex}-P${i}`; const sectorKey = `L${layerIndex}-P${i}`;
const textPathId = `text-path-${sectorKey}`; const textPathId = `text-path-${sectorKey}`;
// 最内层使用形心位置,保证文字可读性。
const effectiveTextRadialPosition = const effectiveTextRadialPosition =
layerIndex === 0 ? 'centroid' : this.textRadialPosition; layerIndex === 0 ? 'centroid' : this.textRadialPosition;
@@ -222,13 +225,14 @@ export class SectorBuilder {
} }
private shouldShowGroupSplit(layer: SectorLayerConfig, sectorIndex: number): boolean { private shouldShowGroupSplit(layer: SectorLayerConfig, sectorIndex: number): boolean {
// groupSplit 关闭时,仅保留分组边界线 // `groupSplit` 关闭时,仅保留分组边界线
if (layer.groupSplit !== false) return true; if (layer.groupSplit !== false) return true;
if (!layer.num) return true; if (!layer.num) return true;
const cycleLength = layer.num + (layer.interval ?? 0); const cycleLength = layer.num + (layer.interval ?? 0);
const posInCycle = sectorIndex % cycleLength; const posInCycle = sectorIndex % cycleLength;
// 仅保留每个着色分组的末尾分割线。
return posInCycle >= layer.num - 1; return posInCycle >= layer.num - 1;
} }
} }

View File

@@ -92,7 +92,7 @@ export function annularSectorCentroid(params: AnnularSectorParams): CentroidResu
return { cx: 0, cy: 0, rho: 0, aMidDeg, aMidRad, deltaDeg }; 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 delta = (deltaDeg * Math.PI) / 180;
const radialFactor = const radialFactor =
(2 / 3) * ((rOuter ** 3 - rInner ** 3) / (rOuter ** 2 - rInner ** 2)); (2 / 3) * ((rOuter ** 3 - rInner ** 3) / (rOuter ** 2 - rInner ** 2));
@@ -124,7 +124,7 @@ export function annularSectorPath(
let delta = a2 - a1; let delta = a2 - a1;
if (delta < 0) delta += 360; if (delta < 0) delta += 360;
// SVG arc flags // `SVG` 圆弧标记位
const largeArc = delta > 180 ? 1 : 0; const largeArc = delta > 180 ? 1 : 0;
const sweepOuter = 1; const sweepOuter = 1;
const sweepInner = 0; const sweepInner = 0;
@@ -134,7 +134,7 @@ export function annularSectorPath(
const p3 = polarToXY(a2, rInner); const p3 = polarToXY(a2, rInner);
const p4 = polarToXY(a1, rInner); const p4 = polarToXY(a1, rInner);
// 如果 rInner=0内弧退化为点 // 如果 `rInner`=0内弧退化为点
if (rInner <= 0.000001) { if (rInner <= 0.000001) {
return [ return [
`M ${p1.x} ${p1.y}`, `M ${p1.x} ${p1.y}`,

View File

@@ -69,7 +69,7 @@
| rOuter | number | 是 | 层外半径,单位:像素 | 200 | | rOuter | number | 是 | 层外半径,单位:像素 | 200 |
| startAngle | number | 否 | 第一个扇区的起始角度0度为正北顺时针默认0 | 0 | | startAngle | number | 否 | 第一个扇区的起始角度0度为正北顺时针默认0 | 0 |
| colorRef | string | 否 | 层级颜色引用引用theme.colorPalettes中的颜色名 | "土" | | colorRef | string | 否 | 层级颜色引用引用theme.colorPalettes中的颜色名 | "土" |
| innerFill | number | 否 | 内缩设置0=不内缩1=内缩1像素,用于规律填色的扇区 | 1 | | innerFill | number | 否 | 内缩设置0=不内缩1=内缩1像素 | 1 |
| num | number | 否 | 规律填色连续着色的扇区数量与interval配合使用 | 3 | | num | number | 否 | 规律填色连续着色的扇区数量与interval配合使用 | 3 |
| interval | number | 否 | 规律填色着色后间隔的扇区数量0表示无间隔 | 1 | | interval | number | 否 | 规律填色着色后间隔的扇区数量0表示无间隔 | 1 |
| groupSplit | boolean | 否 | 是否显示同组扇区间分割线false隐藏默认true | false | | 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个以上平均 - 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可能在layer或者sector。
第二优先级colorRef规律填色也就是说如果同一个sector中指定了colorRef该sector也指定了layer级别的colorRef前者为准innerFill使用相同规则 如果同一个sector中指定了colorRef该sector也指定了layer级别的colorRefsector的colorRef为准
sector colorRef > layer colorRef > 全局background颜色
参数: 参数:
startAngle表示第一个扇区的起始角度以度为单位0度为正北方向顺时针增加 startAngle表示第一个扇区的起始角度以度为单位0度为正北方向顺时针增加
innerfill对numinterval定义的着色扇区生效 innerfill对num/interval定义的着色扇区生效(目前layer中的innerFill对所有sector生效暂保持这个逻辑)
-- start表示着色起始扇区已废弃统一从第1个扇区开始 -- start表示着色起始扇区已废弃统一从第1个扇区开始
num表示连接几个单元着色 num表示连接几个单元着色
interval表示中间间隔几个单元 interval表示中间间隔几个单元
比如num=3,interval=1,意思是从第1个扇区开始着色对1、2、3扇区着色colorref4扇区全局背景5、6、7着色colorref…… 比如num=3,interval=1,意思是从第1个扇区开始着色对1、2、3扇区着色colorref4扇区全局背景5、6、7着色colorref……
groupSplit: 隐藏同组扇区之间的分割线, false表示不显示group中间分割线。如该参数不设置取默认值true显示。 groupSplit: 隐藏同组扇区之间的分割线, false表示不显示group中间分割线。如该参数不设置取默认值true显示。
规律填色有个特殊情况divisions mod (num+interval) 不等于 0比如divisions=16num=2interval=116mod3=1多出来的1个应该使用num同样的colorRef和innerFill设定。
”layers“: ”layers“:
{ {
-- ======================================== -- ========================================
@@ -146,8 +155,8 @@
"innerFill": 0, -- 着色区域的内缩设置 "innerFill": 0, -- 着色区域的内缩设置
"colorRef": "火", -- 着色使用的颜色引用 "colorRef": "火", -- 着色使用的颜色引用
"num": 3, -- 连续着色3个扇区每个区域跨3度 "num": 3, -- 连续着色3个扇区每个区域跨3度
"interval": 2, -- 着色后间隔1个扇区 "interval": 2, -- 着色后间隔1个扇区
"groupSplit": false -- 隐藏同组扇区之间的分割线, false表示不显示group中间分割线该参数不设置默认显示。 "groupSplit": false -- 隐藏同组扇区之间的分割线, false表示不显示group中间分割线该参数不设置默认显示。
}, },
-- ======================================== -- ========================================