@alphacifer/slidev-addon-theme 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -2
- package/components/arc-arrow-process/ArcArrowProcess.vue +361 -0
- package/components/arc-arrow-process/ArcArrowProcessArrow.vue +143 -0
- package/components/arc-arrow-process/ArcArrowProcessCallout.vue +361 -0
- package/components/arc-arrow-process/ArcArrowProcessContent.vue +38 -0
- package/components/arc-arrow-process/ArcArrowProcessHeading.vue +43 -0
- package/components/arc-arrow-process/ArcArrowProcessIcon.vue +154 -0
- package/components/arc-compare/ArcCompare.vue +443 -0
- package/components/arc-compare/ArcCompareBadge.vue +63 -0
- package/components/arc-compare/ArcCompareCallout.vue +386 -0
- package/components/arc-compare/ArcCompareContent.vue +52 -0
- package/components/arc-compare/ArcCompareContents.vue +18 -0
- package/components/arc-compare/ArcCompareHeading.vue +52 -0
- package/components/arc-compare/ArcCompareLeft.vue +183 -0
- package/components/arc-compare/ArcCompareRight.vue +183 -0
- package/components/arc-compare/ArcCompareTitle.vue +47 -0
- package/components/arrow-triad/ArrowTriad.vue +368 -0
- package/components/arrow-triad/ArrowTriadCallout.vue +300 -0
- package/components/arrow-triad/ArrowTriadContent.vue +38 -0
- package/components/arrow-triad/ArrowTriadHeading.vue +38 -0
- package/components/arrow-triad/ArrowTriadIcon.vue +96 -0
- package/components/chevron-card/ChevronCard.vue +237 -0
- package/components/chevron-card/ChevronCardBadge.vue +74 -0
- package/components/chevron-card/ChevronCardContent.vue +39 -0
- package/components/chevron-card/ChevronCardHeading.vue +39 -0
- package/components/hex-triad/HexTriad.vue +438 -0
- package/components/hex-triad/HexTriadBadge.vue +110 -0
- package/components/hex-triad/HexTriadCallout.vue +200 -0
- package/components/hex-triad/HexTriadCenterBadge.vue +51 -0
- package/components/hex-triad/HexTriadContent.vue +20 -0
- package/components/hex-triad/HexTriadHeading.vue +28 -0
- package/components/hex-triad/HexTriadLayer.vue +28 -0
- package/components/horiz-card/HorizCard.vue +318 -0
- package/components/horiz-card/HorizCardBadge.vue +75 -0
- package/components/horiz-card/HorizCardContent.vue +46 -0
- package/components/horiz-card/HorizCardHeading.vue +47 -0
- package/components/horiz-card/HorizCardIcon.vue +118 -0
- package/components/quad-hub/QuadHub.vue +642 -0
- package/components/quad-hub/QuadHubCallout.vue +267 -0
- package/components/quad-hub/QuadHubCenter.vue +53 -0
- package/components/quad-hub/QuadHubContent.vue +39 -0
- package/components/quad-hub/QuadHubHeading.vue +44 -0
- package/components/quad-hub/QuadHubIcon.vue +112 -0
- package/components/shifting-heading/TransitionHeading.vue +3 -3
- package/components/vert-card/VertCard.vue +389 -0
- package/components/vert-card/VertCardBadge.vue +72 -0
- package/components/vert-card/VertCardContent.vue +45 -0
- package/components/vert-card/VertCardHeading.vue +68 -0
- package/components/vert-card/VertCardIcon.vue +126 -0
- package/components/vert-card/VertCardTitle.vue +76 -0
- package/layouts/core/arc-toc.vue +76 -113
- package/layouts/core/bg-center.vue +32 -4
- package/layouts/shifting-heading/shifting-intro.vue +35 -7
- package/package.json +3 -2
- package/utils/arcArrowProcess/context.ts +28 -0
- package/utils/arcArrowProcess/createArcArrowProcessGeometry.ts +137 -0
- package/utils/arcArrowProcess/index.ts +3 -0
- package/utils/arcArrowProcess/resolveArcArrowProcessItems.ts +189 -0
- package/utils/arcCompare/context.ts +36 -0
- package/utils/arcCompare/createArcCompareGeometry.ts +254 -0
- package/utils/arcCompare/index.ts +3 -0
- package/utils/arcCompare/resolveArcCompareItems.ts +123 -0
- package/utils/arrowTriad/context.ts +38 -0
- package/utils/arrowTriad/createArrowTriadGeometry.ts +63 -0
- package/utils/arrowTriad/index.ts +3 -0
- package/utils/arrowTriad/resolveArrowTriadItems.ts +121 -0
- package/utils/chevronCard/context.ts +12 -0
- package/utils/chevronCard/createChevronCardGeometry.ts +48 -0
- package/utils/chevronCard/index.ts +2 -0
- package/utils/hexTriad/context.ts +25 -0
- package/utils/hexTriad/createHexTriadGeometry.ts +182 -0
- package/utils/hexTriad/index.ts +3 -0
- package/utils/hexTriad/resolveHexTriadItems.ts +63 -0
- package/utils/horizCard/context.ts +13 -0
- package/utils/horizCard/createHorizCardGeometry.ts +243 -0
- package/utils/horizCard/index.ts +2 -0
- package/utils/quadHub/context.ts +26 -0
- package/utils/quadHub/createQuadHubGeometry.ts +331 -0
- package/utils/quadHub/index.ts +3 -0
- package/utils/quadHub/resolveQuadHubItems.ts +214 -0
- package/utils/useDiagramAutoScale.ts +300 -0
- package/utils/vertCard/context.ts +18 -0
- package/utils/vertCard/createVertCardGeometry.ts +242 -0
- package/utils/vertCard/index.ts +2 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue';
|
|
2
|
+
|
|
3
|
+
export interface IHorizCardContext {
|
|
4
|
+
readonly color: ComputedRef<string>;
|
|
5
|
+
readonly cardBg: ComputedRef<string>;
|
|
6
|
+
readonly lightColor: ComputedRef<string>;
|
|
7
|
+
readonly titleColor: ComputedRef<string>;
|
|
8
|
+
readonly step: ComputedRef<string | number>;
|
|
9
|
+
readonly textColor: ComputedRef<string | undefined>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const HORIZ_CARD_KEY: InjectionKey<IHorizCardContext> =
|
|
13
|
+
Symbol('HORIZ_CARD');
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
export interface ICreateHorizCardPathParams {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
readonly chamferX?: number;
|
|
5
|
+
readonly chamferY?: number;
|
|
6
|
+
readonly borderRadius?: number;
|
|
7
|
+
readonly chamferFillet?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ICreateHorizCardAccentPathParams {
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
readonly chamferX?: number;
|
|
14
|
+
readonly chamferY?: number;
|
|
15
|
+
readonly accentEndX?: number;
|
|
16
|
+
readonly accentStartY?: number;
|
|
17
|
+
readonly chamferFillet?: number;
|
|
18
|
+
readonly accentOutsetX?: number;
|
|
19
|
+
readonly accentThickness?: number;
|
|
20
|
+
readonly accentTopFillet?: number;
|
|
21
|
+
readonly accentBottomFillet?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ICreateHorizCardBadgePathParams {
|
|
25
|
+
readonly width?: number;
|
|
26
|
+
readonly height?: number;
|
|
27
|
+
readonly topRadius?: number;
|
|
28
|
+
readonly tipRadius?: number;
|
|
29
|
+
readonly sideHeight?: number;
|
|
30
|
+
readonly bottomRadius?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IHexToRgbaParams {
|
|
34
|
+
readonly hex: string;
|
|
35
|
+
readonly alpha?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates an SVG path for the main HorizCard body.
|
|
40
|
+
* Features rounded top-left, top-right, and bottom-right corners,
|
|
41
|
+
* and a smooth filleted chamfer at the bottom-left corner matching the design.
|
|
42
|
+
*/
|
|
43
|
+
export function createHorizCardPath({
|
|
44
|
+
width,
|
|
45
|
+
height,
|
|
46
|
+
chamferX = Math.min(60, Math.round(width * 0.17)),
|
|
47
|
+
chamferY = Math.min(76, Math.round(height * 0.29)),
|
|
48
|
+
borderRadius = 24,
|
|
49
|
+
chamferFillet = 22,
|
|
50
|
+
}: ICreateHorizCardPathParams): string {
|
|
51
|
+
const safeR = Math.max(
|
|
52
|
+
0,
|
|
53
|
+
Math.min(borderRadius, width / 2, (height - chamferY) / 2),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const diagLen = Math.hypot(chamferX, chamferY);
|
|
57
|
+
const ux = diagLen > 0 ? chamferX / diagLen : 0;
|
|
58
|
+
const uy = diagLen > 0 ? chamferY / diagLen : 0;
|
|
59
|
+
|
|
60
|
+
const p1Y = height - chamferY - chamferFillet;
|
|
61
|
+
const p2X = chamferFillet * ux;
|
|
62
|
+
const p2Y = height - chamferY + chamferFillet * uy;
|
|
63
|
+
|
|
64
|
+
const p3X = chamferX - chamferFillet * ux;
|
|
65
|
+
const p3Y = height - chamferFillet * uy;
|
|
66
|
+
const p4X = chamferX + chamferFillet;
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
`M ${safeR} 0`,
|
|
70
|
+
`H ${width - safeR}`,
|
|
71
|
+
`A ${safeR} ${safeR} 0 0 1 ${width} ${safeR}`,
|
|
72
|
+
`V ${height - safeR}`,
|
|
73
|
+
`A ${safeR} ${safeR} 0 0 1 ${width - safeR} ${height}`,
|
|
74
|
+
`H ${p4X.toFixed(2)}`,
|
|
75
|
+
`Q ${chamferX.toFixed(2)} ${height.toFixed(2)} ${p3X.toFixed(2)} ${p3Y.toFixed(2)}`,
|
|
76
|
+
`L ${p2X.toFixed(2)} ${p2Y.toFixed(2)}`,
|
|
77
|
+
`Q 0 ${(height - chamferY).toFixed(2)} 0 ${p1Y.toFixed(2)}`,
|
|
78
|
+
`V ${safeR}`,
|
|
79
|
+
`A ${safeR} ${safeR} 0 0 1 ${safeR} 0`,
|
|
80
|
+
'Z',
|
|
81
|
+
].join(' ');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generates an SVG path for the organic accent shape at the bottom-left of the card.
|
|
86
|
+
* Curves in parallel along with the white card body, featuring 2 distinct rounded curves
|
|
87
|
+
* (a top fillet and a bottom fillet) flanking the diagonal segment, perfectly mirroring
|
|
88
|
+
* the white card's dual filleted transitions.
|
|
89
|
+
*/
|
|
90
|
+
export function createHorizCardAccentPath({
|
|
91
|
+
width,
|
|
92
|
+
height,
|
|
93
|
+
chamferX = Math.min(60, Math.round(width * 0.17)),
|
|
94
|
+
chamferY = Math.min(76, Math.round(height * 0.29)),
|
|
95
|
+
accentEndX,
|
|
96
|
+
accentStartY = Math.round(height * 0.28),
|
|
97
|
+
chamferFillet = 22,
|
|
98
|
+
accentOutsetX,
|
|
99
|
+
accentThickness = 16,
|
|
100
|
+
accentTopFillet = 22,
|
|
101
|
+
accentBottomFillet = 22,
|
|
102
|
+
}: ICreateHorizCardAccentPathParams): string {
|
|
103
|
+
const diagLen = Math.hypot(chamferX, chamferY);
|
|
104
|
+
const ux = diagLen > 0 ? chamferX / diagLen : 0;
|
|
105
|
+
const uy = diagLen > 0 ? chamferY / diagLen : 0;
|
|
106
|
+
|
|
107
|
+
const p1Y = height - chamferY - chamferFillet;
|
|
108
|
+
const p2X = chamferFillet * ux;
|
|
109
|
+
const p2Y = height - chamferY + chamferFillet * uy;
|
|
110
|
+
|
|
111
|
+
const p3X = chamferX - chamferFillet * ux;
|
|
112
|
+
const p3Y = height - chamferFillet * uy;
|
|
113
|
+
const p4X = chamferX + chamferFillet;
|
|
114
|
+
|
|
115
|
+
// Normal vector pointing outward/down-left
|
|
116
|
+
const nx = -uy;
|
|
117
|
+
const ny = ux;
|
|
118
|
+
|
|
119
|
+
// Parallel offset thickness
|
|
120
|
+
const o2X = p2X + accentThickness * nx;
|
|
121
|
+
const o2Y = p2Y + accentThickness * ny;
|
|
122
|
+
const o3X = p3X + accentThickness * nx;
|
|
123
|
+
const o3Y = p3Y + accentThickness * ny;
|
|
124
|
+
|
|
125
|
+
// 1. Bottom vertex & curve (matching white bottom fillet)
|
|
126
|
+
const tBottom = (height - o3Y) / uy;
|
|
127
|
+
const vBottomX = o3X + tBottom * ux;
|
|
128
|
+
const vBottomY = height;
|
|
129
|
+
const bStartX = vBottomX - accentBottomFillet * ux;
|
|
130
|
+
const bStartY = vBottomY - accentBottomFillet * uy;
|
|
131
|
+
const bEndX = vBottomX + accentBottomFillet;
|
|
132
|
+
|
|
133
|
+
// 2. Top vertex & curve (matching white top fillet)
|
|
134
|
+
const outsetX = accentOutsetX ?? accentThickness;
|
|
135
|
+
const vTopX = -outsetX;
|
|
136
|
+
const tTop = (vTopX - o2X) / ux;
|
|
137
|
+
const vTopY = o2Y + tTop * uy;
|
|
138
|
+
const tStartX = vTopX;
|
|
139
|
+
const tStartY = vTopY - accentTopFillet;
|
|
140
|
+
const tEndX = vTopX + accentTopFillet * ux;
|
|
141
|
+
const tEndY = vTopY + accentTopFillet * uy;
|
|
142
|
+
|
|
143
|
+
const startY = accentStartY;
|
|
144
|
+
const endX = accentEndX ?? Math.round(p4X + 4);
|
|
145
|
+
|
|
146
|
+
return [
|
|
147
|
+
`M 0 ${startY}`,
|
|
148
|
+
`C -8 ${startY}, ${vTopX} ${startY + 20}, ${tStartX.toFixed(2)} ${tStartY.toFixed(2)}`,
|
|
149
|
+
// Curve 1: Top fillet transitioning vertical edge into diagonal
|
|
150
|
+
`Q ${vTopX.toFixed(2)} ${vTopY.toFixed(2)} ${tEndX.toFixed(2)} ${tEndY.toFixed(2)}`,
|
|
151
|
+
// Straight diagonal running parallel to the white card
|
|
152
|
+
`L ${bStartX.toFixed(2)} ${bStartY.toFixed(2)}`,
|
|
153
|
+
// Curve 2: Bottom fillet transitioning diagonal into horizontal edge
|
|
154
|
+
`Q ${vBottomX.toFixed(2)} ${vBottomY.toFixed(2)} ${bEndX.toFixed(2)} ${height}`,
|
|
155
|
+
`H ${endX}`,
|
|
156
|
+
`L ${p4X.toFixed(2)} ${height}`,
|
|
157
|
+
`Q ${chamferX.toFixed(2)} ${height.toFixed(2)} ${p3X.toFixed(2)} ${p3Y.toFixed(2)}`,
|
|
158
|
+
`L ${p2X.toFixed(2)} ${p2Y.toFixed(2)}`,
|
|
159
|
+
`Q 0 ${(height - chamferY).toFixed(2)} 0 ${p1Y.toFixed(2)}`,
|
|
160
|
+
`L 0 ${startY}`,
|
|
161
|
+
'Z',
|
|
162
|
+
].join(' ');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generates an SVG path for the top ribbon step badge.
|
|
167
|
+
* Features 5 distinct rounded curves matching the design:
|
|
168
|
+
* 1. Top-Left rounded corner
|
|
169
|
+
* 2. Top-Right rounded corner
|
|
170
|
+
* 3. Bottom-Right rounded corner
|
|
171
|
+
* 4. Bottom-Center pointed tip curve
|
|
172
|
+
* 5. Bottom-Left rounded corner
|
|
173
|
+
*/
|
|
174
|
+
export function createHorizCardBadgePath({
|
|
175
|
+
width = 84,
|
|
176
|
+
height = 58,
|
|
177
|
+
topRadius = 12,
|
|
178
|
+
tipRadius = 8,
|
|
179
|
+
sideHeight = 46,
|
|
180
|
+
bottomRadius = 8,
|
|
181
|
+
}: ICreateHorizCardBadgePathParams = {}): string {
|
|
182
|
+
const halfW = width / 2;
|
|
183
|
+
const dx = halfW;
|
|
184
|
+
const dy = height - sideHeight;
|
|
185
|
+
const len = Math.hypot(dx, dy);
|
|
186
|
+
const ux = len > 0 ? dx / len : 0;
|
|
187
|
+
const uy = len > 0 ? dy / len : 0;
|
|
188
|
+
|
|
189
|
+
return [
|
|
190
|
+
// 1. Top-Left curve
|
|
191
|
+
`M ${topRadius} 0`,
|
|
192
|
+
// Top horizontal edge
|
|
193
|
+
`H ${width - topRadius}`,
|
|
194
|
+
// 2. Top-Right curve
|
|
195
|
+
`A ${topRadius} ${topRadius} 0 0 1 ${width} ${topRadius}`,
|
|
196
|
+
// Right vertical edge
|
|
197
|
+
`V ${sideHeight - bottomRadius}`,
|
|
198
|
+
// 3. Bottom-Right curve
|
|
199
|
+
`Q ${width} ${sideHeight} ${(width - bottomRadius * ux).toFixed(2)} ${(sideHeight + bottomRadius * uy).toFixed(2)}`,
|
|
200
|
+
// Bottom-right diagonal edge
|
|
201
|
+
`L ${(halfW + tipRadius * ux).toFixed(2)} ${(height - tipRadius * uy).toFixed(2)}`,
|
|
202
|
+
// 4. Bottom-Center tip curve
|
|
203
|
+
`Q ${halfW} ${height} ${(halfW - tipRadius * ux).toFixed(2)} ${(height - tipRadius * uy).toFixed(2)}`,
|
|
204
|
+
// Bottom-left diagonal edge
|
|
205
|
+
`L ${(bottomRadius * ux).toFixed(2)} ${(sideHeight + bottomRadius * uy).toFixed(2)}`,
|
|
206
|
+
// 5. Bottom-Left curve
|
|
207
|
+
`Q 0 ${sideHeight} 0 ${sideHeight - bottomRadius}`,
|
|
208
|
+
// Left vertical edge
|
|
209
|
+
`V ${topRadius}`,
|
|
210
|
+
`A ${topRadius} ${topRadius} 0 0 1 ${topRadius} 0`,
|
|
211
|
+
'Z',
|
|
212
|
+
].join(' ');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Converts a hex color string to an RGBA string with the given alpha transparency.
|
|
217
|
+
*/
|
|
218
|
+
export function hexToRgba({ hex, alpha = 0.15 }: IHexToRgbaParams): string {
|
|
219
|
+
let sanitized = hex.trim().replace(/^#/, '');
|
|
220
|
+
|
|
221
|
+
if (sanitized.length === 3) {
|
|
222
|
+
sanitized = sanitized
|
|
223
|
+
.split('')
|
|
224
|
+
.map((char) => {
|
|
225
|
+
return char + char;
|
|
226
|
+
})
|
|
227
|
+
.join('');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (sanitized.length !== 6) {
|
|
231
|
+
return hex;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const r = Number.parseInt(sanitized.slice(0, 2), 16);
|
|
235
|
+
const g = Number.parseInt(sanitized.slice(2, 4), 16);
|
|
236
|
+
const b = Number.parseInt(sanitized.slice(4, 6), 16);
|
|
237
|
+
|
|
238
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
|
|
239
|
+
return hex;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
243
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { IQuadHubItem, IResolvedQuadHubItem } from './resolveQuadHubItems';
|
|
4
|
+
|
|
5
|
+
export interface IQuadHubRegistration {
|
|
6
|
+
readonly index: number;
|
|
7
|
+
readonly item: ComputedRef<IResolvedQuadHubItem>;
|
|
8
|
+
readonly setCustomBadge?: (text: string | number) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IQuadHubRootContext {
|
|
12
|
+
readonly animation?: ComputedRef<boolean>;
|
|
13
|
+
readonly registerCallout: (props?: IQuadHubItem) => IQuadHubRegistration;
|
|
14
|
+
readonly items: ComputedRef<readonly IResolvedQuadHubItem[]>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IQuadHubCalloutContext {
|
|
18
|
+
readonly index: number;
|
|
19
|
+
readonly item: ComputedRef<IResolvedQuadHubItem>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const QUAD_HUB_ROOT_KEY: InjectionKey<IQuadHubRootContext> =
|
|
23
|
+
Symbol('QUAD_HUB_ROOT_KEY');
|
|
24
|
+
|
|
25
|
+
export const QUAD_HUB_CALLOUT_KEY: InjectionKey<IQuadHubCalloutContext> =
|
|
26
|
+
Symbol('QUAD_HUB_CALLOUT_KEY');
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
export interface IQuadPoint {
|
|
2
|
+
readonly x: number;
|
|
3
|
+
readonly y: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface IQuadBox {
|
|
7
|
+
readonly x: number;
|
|
8
|
+
readonly y: number;
|
|
9
|
+
readonly rx: number;
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IQuadHubCoordinates {
|
|
15
|
+
readonly centerX: number;
|
|
16
|
+
readonly centerY: number;
|
|
17
|
+
readonly arcPaths: readonly [string, string, string, string];
|
|
18
|
+
readonly arcRadius: number;
|
|
19
|
+
readonly hubRadius: number;
|
|
20
|
+
readonly quadrants: readonly [IQuadBox, IQuadBox, IQuadBox, IQuadBox];
|
|
21
|
+
readonly viewBoxSize: number;
|
|
22
|
+
readonly iconPositions: readonly [
|
|
23
|
+
IQuadPoint,
|
|
24
|
+
IQuadPoint,
|
|
25
|
+
IQuadPoint,
|
|
26
|
+
IQuadPoint,
|
|
27
|
+
];
|
|
28
|
+
readonly arcBorderWidth: number;
|
|
29
|
+
readonly numberPositions: readonly [
|
|
30
|
+
IQuadPoint,
|
|
31
|
+
IQuadPoint,
|
|
32
|
+
IQuadPoint,
|
|
33
|
+
IQuadPoint,
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createArcPath(
|
|
38
|
+
cx: number,
|
|
39
|
+
cy: number,
|
|
40
|
+
radius: number,
|
|
41
|
+
startAngleDeg: number,
|
|
42
|
+
endAngleDeg: number,
|
|
43
|
+
): string {
|
|
44
|
+
const startRad = (startAngleDeg * Math.PI) / 180;
|
|
45
|
+
const endRad = (endAngleDeg * Math.PI) / 180;
|
|
46
|
+
|
|
47
|
+
const x1 = Number((cx + radius * Math.cos(startRad)).toFixed(2));
|
|
48
|
+
const y1 = Number((cy + radius * Math.sin(startRad)).toFixed(2));
|
|
49
|
+
const x2 = Number((cx + radius * Math.cos(endRad)).toFixed(2));
|
|
50
|
+
const y2 = Number((cy + radius * Math.sin(endRad)).toFixed(2));
|
|
51
|
+
|
|
52
|
+
const largeArcFlag = Math.abs(endAngleDeg - startAngleDeg) > 180 ? 1 : 0;
|
|
53
|
+
|
|
54
|
+
return `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ICalculateQuadHubCoordinatesParams {
|
|
58
|
+
readonly gap?: number;
|
|
59
|
+
readonly width?: number;
|
|
60
|
+
readonly height?: number;
|
|
61
|
+
readonly arcRadius?: number;
|
|
62
|
+
readonly hubRadius?: number;
|
|
63
|
+
readonly arcInsetDeg?: number;
|
|
64
|
+
readonly viewBoxSize?: number;
|
|
65
|
+
readonly cornerRadius?: number;
|
|
66
|
+
readonly iconPaddingX?: number;
|
|
67
|
+
readonly iconPaddingY?: number;
|
|
68
|
+
readonly arcBorderWidth?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Calculates geometric coordinates for the QuadHub layout.
|
|
73
|
+
* Indices (Clockwise starting Top-Left):
|
|
74
|
+
* 0: Top-Left (01)
|
|
75
|
+
* 1: Top-Right (02)
|
|
76
|
+
* 2: Bottom-Right (03)
|
|
77
|
+
* 3: Bottom-Left (04)
|
|
78
|
+
*/
|
|
79
|
+
export function calculateQuadHubCoordinates({
|
|
80
|
+
gap = 14,
|
|
81
|
+
width = 136,
|
|
82
|
+
height = 150,
|
|
83
|
+
hubRadius = 58,
|
|
84
|
+
arcRadius = 104,
|
|
85
|
+
arcInsetDeg = 11,
|
|
86
|
+
viewBoxSize = 360,
|
|
87
|
+
cornerRadius = 26,
|
|
88
|
+
iconPaddingX = 30,
|
|
89
|
+
iconPaddingY = 32,
|
|
90
|
+
arcBorderWidth = 5,
|
|
91
|
+
}: ICalculateQuadHubCoordinatesParams = {}): IQuadHubCoordinates {
|
|
92
|
+
const centerX = viewBoxSize / 2;
|
|
93
|
+
const centerY = viewBoxSize / 2;
|
|
94
|
+
const halfGap = gap / 2;
|
|
95
|
+
|
|
96
|
+
// Quadrants:
|
|
97
|
+
// 0: Top-Left (1)
|
|
98
|
+
const q0: IQuadBox = {
|
|
99
|
+
x: centerX - halfGap - width,
|
|
100
|
+
y: centerY - halfGap - height,
|
|
101
|
+
rx: cornerRadius,
|
|
102
|
+
width,
|
|
103
|
+
height,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// 1: Top-Right (2)
|
|
107
|
+
const q1: IQuadBox = {
|
|
108
|
+
x: centerX + halfGap,
|
|
109
|
+
y: centerY - halfGap - height,
|
|
110
|
+
rx: cornerRadius,
|
|
111
|
+
width,
|
|
112
|
+
height,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// 2: Bottom-Right (3)
|
|
116
|
+
const q2: IQuadBox = {
|
|
117
|
+
x: centerX + halfGap,
|
|
118
|
+
y: centerY + halfGap,
|
|
119
|
+
rx: cornerRadius,
|
|
120
|
+
width,
|
|
121
|
+
height,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// 3: Bottom-Left (4)
|
|
125
|
+
const q3: IQuadBox = {
|
|
126
|
+
x: centerX - halfGap - width,
|
|
127
|
+
y: centerY + halfGap,
|
|
128
|
+
rx: cornerRadius,
|
|
129
|
+
width,
|
|
130
|
+
height,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Number positions (centered in the arc ribbon between hubRadius and arcRadius)
|
|
134
|
+
const numberRadius = (hubRadius + arcRadius) / 2;
|
|
135
|
+
const rad = (38 * Math.PI) / 180;
|
|
136
|
+
const offsetX = Number((numberRadius * Math.cos(rad)).toFixed(2));
|
|
137
|
+
const offsetY = Number((numberRadius * Math.sin(rad)).toFixed(2));
|
|
138
|
+
|
|
139
|
+
// 0: Top-Left (1)
|
|
140
|
+
const num0: IQuadPoint = {
|
|
141
|
+
x: centerX - offsetX,
|
|
142
|
+
y: centerY - offsetY,
|
|
143
|
+
};
|
|
144
|
+
// 1: Top-Right (2)
|
|
145
|
+
const num1: IQuadPoint = {
|
|
146
|
+
x: centerX + offsetX,
|
|
147
|
+
y: centerY - offsetY,
|
|
148
|
+
};
|
|
149
|
+
// 2: Bottom-Right (3)
|
|
150
|
+
const num2: IQuadPoint = {
|
|
151
|
+
x: centerX + offsetX,
|
|
152
|
+
y: centerY + offsetY,
|
|
153
|
+
};
|
|
154
|
+
// 3: Bottom-Left (4)
|
|
155
|
+
const num3: IQuadPoint = {
|
|
156
|
+
x: centerX - offsetX,
|
|
157
|
+
y: centerY + offsetY,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Icon positions (in outer corner of each block)
|
|
161
|
+
// 0: Top-Left (1)
|
|
162
|
+
const icon0: IQuadPoint = {
|
|
163
|
+
x: q0.x + iconPaddingX,
|
|
164
|
+
y: q0.y + iconPaddingY,
|
|
165
|
+
};
|
|
166
|
+
// 1: Top-Right (2)
|
|
167
|
+
const icon1: IQuadPoint = {
|
|
168
|
+
x: q1.x + q1.width - iconPaddingX,
|
|
169
|
+
y: q1.y + iconPaddingY,
|
|
170
|
+
};
|
|
171
|
+
// 2: Bottom-Right (3)
|
|
172
|
+
const icon2: IQuadPoint = {
|
|
173
|
+
x: q2.x + q2.width - iconPaddingX,
|
|
174
|
+
y: q2.y + q2.height - iconPaddingY,
|
|
175
|
+
};
|
|
176
|
+
// 3: Bottom-Left (4)
|
|
177
|
+
const icon3: IQuadPoint = {
|
|
178
|
+
x: q3.x + iconPaddingX,
|
|
179
|
+
y: q3.y + q3.height - iconPaddingY,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// In SVG, stroke of width W centered on radius R extends [R - W/2, R + W/2].
|
|
183
|
+
// By placing the arc path center at arcRadius + arcBorderWidth / 2, the inner edge
|
|
184
|
+
// of the stroke is exactly arcRadius, touching the pastel circle with zero overlay.
|
|
185
|
+
const arcStrokeRadius = arcRadius + arcBorderWidth / 2;
|
|
186
|
+
|
|
187
|
+
// 0: Top-Left (1)
|
|
188
|
+
const p0 = createArcPath(
|
|
189
|
+
centerX,
|
|
190
|
+
centerY,
|
|
191
|
+
arcStrokeRadius,
|
|
192
|
+
-180 + arcInsetDeg,
|
|
193
|
+
-90 - arcInsetDeg,
|
|
194
|
+
);
|
|
195
|
+
// 1: Top-Right (2)
|
|
196
|
+
const p1 = createArcPath(
|
|
197
|
+
centerX,
|
|
198
|
+
centerY,
|
|
199
|
+
arcStrokeRadius,
|
|
200
|
+
-90 + arcInsetDeg,
|
|
201
|
+
0 - arcInsetDeg,
|
|
202
|
+
);
|
|
203
|
+
// 2: Bottom-Right (3)
|
|
204
|
+
const p2 = createArcPath(
|
|
205
|
+
centerX,
|
|
206
|
+
centerY,
|
|
207
|
+
arcStrokeRadius,
|
|
208
|
+
0 + arcInsetDeg,
|
|
209
|
+
90 - arcInsetDeg,
|
|
210
|
+
);
|
|
211
|
+
// 3: Bottom-Left (4)
|
|
212
|
+
const p3 = createArcPath(
|
|
213
|
+
centerX,
|
|
214
|
+
centerY,
|
|
215
|
+
arcStrokeRadius,
|
|
216
|
+
90 + arcInsetDeg,
|
|
217
|
+
180 - arcInsetDeg,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
centerX,
|
|
222
|
+
centerY,
|
|
223
|
+
arcPaths: [p0, p1, p2, p3],
|
|
224
|
+
arcRadius,
|
|
225
|
+
hubRadius,
|
|
226
|
+
quadrants: [q0, q1, q2, q3],
|
|
227
|
+
viewBoxSize,
|
|
228
|
+
iconPositions: [icon0, icon1, icon2, icon3],
|
|
229
|
+
arcBorderWidth,
|
|
230
|
+
numberPositions: [num0, num1, num2, num3],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface ILightenColorParams {
|
|
235
|
+
readonly hex: string;
|
|
236
|
+
readonly ratio?: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Automatically lightens a base hex color by mixing it with white.
|
|
241
|
+
* ratio: 0 = original color, 1 = pure white.
|
|
242
|
+
*/
|
|
243
|
+
export function lightenColor({
|
|
244
|
+
hex,
|
|
245
|
+
ratio = 0.5,
|
|
246
|
+
}: ILightenColorParams): string {
|
|
247
|
+
const raw = hex.trim().replace(/^#/, '');
|
|
248
|
+
const sanitized =
|
|
249
|
+
raw.length === 3
|
|
250
|
+
? raw
|
|
251
|
+
.split('')
|
|
252
|
+
.map((char) => {
|
|
253
|
+
return char + char;
|
|
254
|
+
})
|
|
255
|
+
.join('')
|
|
256
|
+
: raw;
|
|
257
|
+
|
|
258
|
+
if (sanitized.length !== 6) {
|
|
259
|
+
return hex;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const r = Number.parseInt(sanitized.slice(0, 2), 16);
|
|
263
|
+
const g = Number.parseInt(sanitized.slice(2, 4), 16);
|
|
264
|
+
const b = Number.parseInt(sanitized.slice(4, 6), 16);
|
|
265
|
+
|
|
266
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
|
|
267
|
+
return hex;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const clamp = (val: number): number => {
|
|
271
|
+
return Math.min(255, Math.max(0, Math.round(val)));
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const lr = clamp(r + (255 - r) * ratio);
|
|
275
|
+
const lg = clamp(g + (255 - g) * ratio);
|
|
276
|
+
const lb = clamp(b + (255 - b) * ratio);
|
|
277
|
+
|
|
278
|
+
const toHex = (n: number): string => {
|
|
279
|
+
return n.toString(16).padStart(2, '0');
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return `#${toHex(lr)}${toHex(lg)}${toHex(lb)}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface IDarkenColorParams {
|
|
286
|
+
readonly hex: string;
|
|
287
|
+
readonly ratio?: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Automatically darkens a base hex color by mixing it with black.
|
|
292
|
+
* ratio: 0 = original color, 1 = pure black.
|
|
293
|
+
*/
|
|
294
|
+
export function darkenColor({ hex, ratio = 0.18 }: IDarkenColorParams): string {
|
|
295
|
+
const raw = hex.trim().replace(/^#/, '');
|
|
296
|
+
const sanitized =
|
|
297
|
+
raw.length === 3
|
|
298
|
+
? raw
|
|
299
|
+
.split('')
|
|
300
|
+
.map((char) => {
|
|
301
|
+
return char + char;
|
|
302
|
+
})
|
|
303
|
+
.join('')
|
|
304
|
+
: raw;
|
|
305
|
+
|
|
306
|
+
if (sanitized.length !== 6) {
|
|
307
|
+
return hex;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const r = Number.parseInt(sanitized.slice(0, 2), 16);
|
|
311
|
+
const g = Number.parseInt(sanitized.slice(2, 4), 16);
|
|
312
|
+
const b = Number.parseInt(sanitized.slice(4, 6), 16);
|
|
313
|
+
|
|
314
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
|
|
315
|
+
return hex;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const clamp = (val: number): number => {
|
|
319
|
+
return Math.min(255, Math.max(0, Math.round(val)));
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const dr = clamp(r * (1 - ratio));
|
|
323
|
+
const dg = clamp(g * (1 - ratio));
|
|
324
|
+
const db = clamp(b * (1 - ratio));
|
|
325
|
+
|
|
326
|
+
const toHex = (n: number): string => {
|
|
327
|
+
return n.toString(16).padStart(2, '0');
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return `#${toHex(dr)}${toHex(dg)}${toHex(db)}`;
|
|
331
|
+
}
|