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",
|
"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 },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
17
todolist.md
17
todolist.md
@@ -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级别的colorRef,以sector的colorRef为准。
|
||||||
|
sector colorRef > layer colorRef > 全局background颜色
|
||||||
参数:
|
参数:
|
||||||
startAngle表示第一个扇区的起始角度(以度为单位,0度为正北方向,顺时针增加)
|
startAngle表示第一个扇区的起始角度(以度为单位,0度为正北方向,顺时针增加)
|
||||||
innerfill对num、interval定义的着色扇区生效
|
innerfill仅对num/interval定义的着色扇区生效(目前layer中的innerFill对所有sector生效,暂保持这个逻辑)
|
||||||
-- start表示着色起始扇区(已废弃,统一从第1个扇区开始)
|
-- start表示着色起始扇区(已废弃,统一从第1个扇区开始)
|
||||||
num表示连接几个单元着色
|
num表示连接几个单元着色
|
||||||
interval表示中间间隔几个单元
|
interval表示中间间隔几个单元
|
||||||
比如num=3,interval=1,意思是从第1个扇区开始着色,对1、2、3扇区着色colorref,4扇区全局背景,5、6、7着色colorref……
|
比如num=3,interval=1,意思是从第1个扇区开始着色,对1、2、3扇区着色colorref,4扇区全局背景,5、6、7着色colorref……
|
||||||
groupSplit: 隐藏同组扇区之间的分割线, false表示不显示group中间分割线。如该参数不设置,取默认值true,显示。
|
groupSplit: 隐藏同组扇区之间的分割线, false表示不显示group中间分割线。如该参数不设置,取默认值true,显示。
|
||||||
|
|
||||||
|
规律填色有个特殊情况,divisions mod (num+interval) 不等于 0,比如divisions=16,num=2,interval=1,16mod3=1,多出来的1个应该使用num同样的colorRef和innerFill设定。
|
||||||
|
|
||||||
”layers“:
|
”layers“:
|
||||||
{
|
{
|
||||||
-- ========================================
|
-- ========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user