@alphacifer/slidev-addon-theme 0.0.8 → 0.0.9
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 +80 -0
- package/components/pyramid/circular-pyramid/CircularPyramid.vue +871 -0
- package/components/pyramid/circular-pyramid/CircularPyramidStack.vue +375 -0
- package/components/pyramid/circular-pyramid/CircularPyramidStackContent.vue +208 -0
- package/components/pyramid/circular-pyramid/CircularPyramidStackTitle.vue +138 -0
- package/components/pyramid/square-pyramid/SquarePyramid.vue +876 -0
- package/components/pyramid/square-pyramid/SquarePyramidStack.vue +366 -0
- package/components/pyramid/square-pyramid/SquarePyramidStackContent.vue +204 -0
- package/components/pyramid/square-pyramid/SquarePyramidStackTitle.vue +134 -0
- package/package.json +2 -1
- package/utils/pyramid/circular-pyramid/context.ts +46 -0
- package/utils/pyramid/circular-pyramid/createCircularPyramidGeometry.ts +143 -0
- package/utils/pyramid/circular-pyramid/index.ts +3 -0
- package/utils/pyramid/circular-pyramid/types.ts +34 -0
- package/utils/pyramid/index.ts +3 -0
- package/utils/pyramid/shared/colors.ts +92 -0
- package/utils/pyramid/shared/connector.ts +111 -0
- package/utils/pyramid/shared/constants.ts +8 -0
- package/utils/pyramid/shared/index.ts +5 -0
- package/utils/pyramid/shared/stacking.ts +76 -0
- package/utils/pyramid/shared/types.ts +36 -0
- package/utils/pyramid/square-pyramid/context.ts +46 -0
- package/utils/pyramid/square-pyramid/createSquarePyramidGeometry.ts +265 -0
- package/utils/pyramid/square-pyramid/index.ts +3 -0
- package/utils/pyramid/square-pyramid/types.ts +39 -0
- package/utils/useDiagramAutoScale.ts +6 -8
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
type ComputedRef,
|
|
4
|
+
computed,
|
|
5
|
+
Fragment,
|
|
6
|
+
inject,
|
|
7
|
+
onMounted,
|
|
8
|
+
onUnmounted,
|
|
9
|
+
provide,
|
|
10
|
+
ref,
|
|
11
|
+
useSlots,
|
|
12
|
+
type VNode,
|
|
13
|
+
watch,
|
|
14
|
+
} from 'vue';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
CIRCULAR_PYRAMID_ROOT_KEY,
|
|
18
|
+
clampCircularCount,
|
|
19
|
+
createCircularPyramidGeometry,
|
|
20
|
+
DEFAULT_CIRCULAR_PYRAMID_COLOR,
|
|
21
|
+
DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS,
|
|
22
|
+
darkenColor,
|
|
23
|
+
lightenColor,
|
|
24
|
+
resolveCircularPyramidPalette,
|
|
25
|
+
} from '../../../utils/pyramid/circular-pyramid';
|
|
26
|
+
import { resolvePyramidCardPositions } from '../../../utils/pyramid/shared';
|
|
27
|
+
import { useDiagramAutoScale } from '../../../utils/useDiagramAutoScale';
|
|
28
|
+
import { useMergedUnoAttrs } from '../../../utils/useMergedUnoAttrs';
|
|
29
|
+
import CircularPyramidStack from './CircularPyramidStack.vue';
|
|
30
|
+
|
|
31
|
+
defineOptions({
|
|
32
|
+
inheritAttrs: false,
|
|
33
|
+
name: 'CircularPyramid',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export interface ICircularPyramidProps {
|
|
37
|
+
/**
|
|
38
|
+
* Number of circular layers in the pyramid (1 to 6).
|
|
39
|
+
* Automatically inferred from slotted `<CircularPyramidStack>` components if omitted.
|
|
40
|
+
* Default: 4
|
|
41
|
+
*/
|
|
42
|
+
readonly count?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Index of the active circular layer (0 = top, count - 1 = bottom).
|
|
45
|
+
* Default: -1 (none active)
|
|
46
|
+
*/
|
|
47
|
+
readonly activeIndex?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Primary fallback theme color for the pyramid.
|
|
50
|
+
* Default: '#3b82f6'
|
|
51
|
+
*/
|
|
52
|
+
readonly color?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Optional custom palette for stacks/layers.
|
|
55
|
+
* If omitted, distinct defaults from DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS are used.
|
|
56
|
+
*/
|
|
57
|
+
readonly colors?: readonly string[] | string[];
|
|
58
|
+
/**
|
|
59
|
+
* Container width. Supports numeric px or CSS unit strings.
|
|
60
|
+
*/
|
|
61
|
+
readonly width?: number | string;
|
|
62
|
+
/**
|
|
63
|
+
* Container height. Supports numeric px or CSS unit strings.
|
|
64
|
+
*/
|
|
65
|
+
readonly height?: number | string;
|
|
66
|
+
/**
|
|
67
|
+
* Manual zoom/scale factor.
|
|
68
|
+
*/
|
|
69
|
+
readonly scale?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Enable responsive auto-scaling to fit container.
|
|
72
|
+
* Default: true
|
|
73
|
+
*/
|
|
74
|
+
readonly autoScale?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Maximum allowed auto-scale factor.
|
|
77
|
+
*/
|
|
78
|
+
readonly maxScale?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Whether clicking a layer or stack changes the active layer.
|
|
81
|
+
* Default: true
|
|
82
|
+
*/
|
|
83
|
+
readonly interactive?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Enable entrance and state transition animations.
|
|
86
|
+
* Default: true
|
|
87
|
+
*/
|
|
88
|
+
readonly animation?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Start delay (ms) for entrance animations.
|
|
91
|
+
*/
|
|
92
|
+
readonly startDelay?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Custom viewBox width. Default: 960
|
|
95
|
+
*/
|
|
96
|
+
readonly viewBoxWidth?: number;
|
|
97
|
+
/**
|
|
98
|
+
* Custom viewBox height. Default: 440
|
|
99
|
+
*/
|
|
100
|
+
readonly viewBoxHeight?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const props = withDefaults(defineProps<ICircularPyramidProps>(), {
|
|
104
|
+
count: undefined,
|
|
105
|
+
activeIndex: -1,
|
|
106
|
+
color: DEFAULT_CIRCULAR_PYRAMID_COLOR,
|
|
107
|
+
colors: undefined,
|
|
108
|
+
width: undefined,
|
|
109
|
+
height: undefined,
|
|
110
|
+
scale: undefined,
|
|
111
|
+
autoScale: true,
|
|
112
|
+
maxScale: undefined,
|
|
113
|
+
interactive: true,
|
|
114
|
+
animation: true,
|
|
115
|
+
startDelay: undefined,
|
|
116
|
+
viewBoxWidth: 960,
|
|
117
|
+
viewBoxHeight: 440,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const emit = defineEmits<{
|
|
121
|
+
(e: 'update:activeIndex', index: number): void;
|
|
122
|
+
(e: 'select', index: number): void;
|
|
123
|
+
(e: 'click', event: MouseEvent): void;
|
|
124
|
+
}>();
|
|
125
|
+
|
|
126
|
+
const slots = useSlots();
|
|
127
|
+
const rootRef = ref<HTMLElement | null>(null);
|
|
128
|
+
|
|
129
|
+
const shiftingIntro = inject<
|
|
130
|
+
| {
|
|
131
|
+
active: ComputedRef<boolean>;
|
|
132
|
+
isShifting?: ComputedRef<boolean>;
|
|
133
|
+
}
|
|
134
|
+
| undefined
|
|
135
|
+
>('SLIDEV_LAYOUT_SHIFTING_INTRO', undefined);
|
|
136
|
+
|
|
137
|
+
const isAnimated = computed(() => {
|
|
138
|
+
if (!props.animation) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (shiftingIntro) {
|
|
143
|
+
return shiftingIntro.active.value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const resolvedStartDelay = computed(() => {
|
|
150
|
+
if (props.startDelay !== undefined) {
|
|
151
|
+
return props.startDelay;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (shiftingIntro) {
|
|
155
|
+
return shiftingIntro.isShifting?.value ? 500 : 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return 0;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const internalActiveIndex = ref(props.activeIndex);
|
|
162
|
+
|
|
163
|
+
watch(
|
|
164
|
+
() => props.activeIndex,
|
|
165
|
+
(newVal) => {
|
|
166
|
+
internalActiveIndex.value = newVal;
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const currentActiveIndex = computed(() => internalActiveIndex.value);
|
|
171
|
+
|
|
172
|
+
interface INamedComponent {
|
|
173
|
+
readonly type?: {
|
|
174
|
+
readonly name?: string;
|
|
175
|
+
readonly displayName?: string;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function countSlottedStacks(slotNodes: VNode[] | undefined): number {
|
|
180
|
+
if (!slotNodes) return 0;
|
|
181
|
+
let count = 0;
|
|
182
|
+
for (const node of slotNodes) {
|
|
183
|
+
if (typeof node.type === 'object' && node.type !== null) {
|
|
184
|
+
const comp = node.type as INamedComponent['type'];
|
|
185
|
+
if (
|
|
186
|
+
comp?.name === 'CircularPyramidStack' ||
|
|
187
|
+
comp?.displayName === 'CircularPyramidStack'
|
|
188
|
+
) {
|
|
189
|
+
count++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (node.type === Fragment && Array.isArray(node.children)) {
|
|
194
|
+
count += countSlottedStacks(node.children as VNode[]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return count;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const slottedStackCount = computed(() => {
|
|
201
|
+
const defaultSlotNodes = slots.default?.();
|
|
202
|
+
if (!defaultSlotNodes) return 0;
|
|
203
|
+
let count = 0;
|
|
204
|
+
for (const node of defaultSlotNodes) {
|
|
205
|
+
if (typeof node.type === 'object' && node.type !== null) {
|
|
206
|
+
const comp = node.type as INamedComponent['type'];
|
|
207
|
+
if (
|
|
208
|
+
comp?.name === 'CircularPyramidStack' ||
|
|
209
|
+
comp?.displayName === 'CircularPyramidStack'
|
|
210
|
+
) {
|
|
211
|
+
count++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (node.type === Fragment && Array.isArray(node.children)) {
|
|
216
|
+
count += countSlottedStacks(node.children as VNode[]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return count;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const registeredStackTokens = ref<symbol[]>([]);
|
|
223
|
+
const registeredStackColors = ref<Map<symbol, string | undefined>>(new Map());
|
|
224
|
+
|
|
225
|
+
function registerStack(
|
|
226
|
+
id?: symbol,
|
|
227
|
+
colorRef?: ComputedRef<string | undefined>,
|
|
228
|
+
) {
|
|
229
|
+
const token = id ?? Symbol();
|
|
230
|
+
if (!registeredStackTokens.value.includes(token)) {
|
|
231
|
+
registeredStackTokens.value.push(token);
|
|
232
|
+
}
|
|
233
|
+
let stopWatch: (() => void) | undefined;
|
|
234
|
+
if (colorRef) {
|
|
235
|
+
stopWatch = watch(
|
|
236
|
+
colorRef,
|
|
237
|
+
(newColor) => {
|
|
238
|
+
if (newColor) {
|
|
239
|
+
registeredStackColors.value.set(token, newColor);
|
|
240
|
+
} else {
|
|
241
|
+
registeredStackColors.value.delete(token);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{ immediate: true },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const index = computed(() => registeredStackTokens.value.indexOf(token));
|
|
248
|
+
return {
|
|
249
|
+
index,
|
|
250
|
+
unregister: () => {
|
|
251
|
+
stopWatch?.();
|
|
252
|
+
registeredStackTokens.value = registeredStackTokens.value.filter(
|
|
253
|
+
(t) => t !== token,
|
|
254
|
+
);
|
|
255
|
+
registeredStackColors.value.delete(token);
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getLayerColor(index: number): string {
|
|
261
|
+
if (index >= 0 && index < registeredStackTokens.value.length) {
|
|
262
|
+
const token = registeredStackTokens.value[index];
|
|
263
|
+
if (token) {
|
|
264
|
+
const custom = registeredStackColors.value.get(token);
|
|
265
|
+
if (custom) {
|
|
266
|
+
return custom;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const customPaletteColor = props.colors?.[index];
|
|
272
|
+
if (customPaletteColor) {
|
|
273
|
+
return customPaletteColor;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const stackColor =
|
|
277
|
+
DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS[
|
|
278
|
+
Math.abs(index) % DEFAULT_CIRCULAR_PYRAMID_STACK_COLORS.length
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
return stackColor ?? props.color ?? DEFAULT_CIRCULAR_PYRAMID_COLOR;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const activeLayerColor = computed<string>(() => {
|
|
285
|
+
if (currentActiveIndex.value >= 0) {
|
|
286
|
+
return getLayerColor(currentActiveIndex.value);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return props.color;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const resolvedCount = computed(() => {
|
|
293
|
+
if (props.count !== undefined) {
|
|
294
|
+
return clampCircularCount(props.count);
|
|
295
|
+
}
|
|
296
|
+
if (slottedStackCount.value > 0) {
|
|
297
|
+
return clampCircularCount(slottedStackCount.value);
|
|
298
|
+
}
|
|
299
|
+
if (registeredStackTokens.value.length > 0) {
|
|
300
|
+
return clampCircularCount(registeredStackTokens.value.length);
|
|
301
|
+
}
|
|
302
|
+
return 4;
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const geo = computed(() => {
|
|
306
|
+
const vbHeight =
|
|
307
|
+
props.viewBoxHeight !== 440
|
|
308
|
+
? props.viewBoxHeight
|
|
309
|
+
: resolvedCount.value >= 6
|
|
310
|
+
? 480
|
|
311
|
+
: resolvedCount.value >= 5
|
|
312
|
+
? 450
|
|
313
|
+
: 440;
|
|
314
|
+
|
|
315
|
+
return createCircularPyramidGeometry({
|
|
316
|
+
count: resolvedCount.value,
|
|
317
|
+
viewBoxWidth: 630,
|
|
318
|
+
viewBoxHeight: vbHeight,
|
|
319
|
+
cx: 315,
|
|
320
|
+
bottomRadius: 180,
|
|
321
|
+
topRadius:
|
|
322
|
+
resolvedCount.value <= 2
|
|
323
|
+
? 100
|
|
324
|
+
: Math.max(70, 95 - (resolvedCount.value - 3) * 6),
|
|
325
|
+
cylinderHeight: resolvedCount.value >= 5 ? 7 : 8,
|
|
326
|
+
ratio: 0.45,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const palette = computed(() => {
|
|
331
|
+
return resolveCircularPyramidPalette(props.color);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
function setActiveIndex(index: number): void {
|
|
335
|
+
internalActiveIndex.value = index;
|
|
336
|
+
emit('update:activeIndex', index);
|
|
337
|
+
emit('select', index);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const TITLE_WIDTH_PX = 150;
|
|
341
|
+
const CONTENT_WIDTH_PX = 240;
|
|
342
|
+
// Content card extends to the right boundary limit (0px offset from right edge)
|
|
343
|
+
const CONTENT_RIGHT_OFFSET_PX = 0;
|
|
344
|
+
|
|
345
|
+
const titleWidthPx = computed(() => TITLE_WIDTH_PX);
|
|
346
|
+
const contentWidthPx = computed(() => CONTENT_WIDTH_PX);
|
|
347
|
+
const contentRightOffsetPx = computed(() => CONTENT_RIGHT_OFFSET_PX);
|
|
348
|
+
|
|
349
|
+
const cardHeights = ref<number[]>([]);
|
|
350
|
+
const cardDeltaYList = ref<number[]>([]);
|
|
351
|
+
|
|
352
|
+
const initialDeltaYList = computed(() => {
|
|
353
|
+
const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
|
|
354
|
+
const estimatedHeights = Array(targetY.length).fill(48);
|
|
355
|
+
const resolvedY = resolvePyramidCardPositions({
|
|
356
|
+
targetYList: targetY,
|
|
357
|
+
heights: estimatedHeights,
|
|
358
|
+
gap: 10,
|
|
359
|
+
minY: 16,
|
|
360
|
+
maxY: geo.value.viewBoxHeight - 16,
|
|
361
|
+
});
|
|
362
|
+
return resolvedY.map((y, i) => y - (targetY[i] ?? y));
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
function getCardDeltaY(index: number): number {
|
|
366
|
+
if (cardDeltaYList.value[index] !== undefined) {
|
|
367
|
+
return cardDeltaYList.value[index] ?? 0;
|
|
368
|
+
}
|
|
369
|
+
return initialDeltaYList.value[index] ?? 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let cardAnimationFrame: number | undefined;
|
|
373
|
+
|
|
374
|
+
function updateCardPositions(): void {
|
|
375
|
+
if (typeof window === 'undefined' || !rootRef.value) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const cardEls = rootRef.value.querySelectorAll<HTMLElement>(
|
|
379
|
+
'.alpha-circular-pyramid-stack-content',
|
|
380
|
+
);
|
|
381
|
+
if (cardEls.length === 0) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const heights: number[] = [];
|
|
386
|
+
cardEls.forEach((el) => {
|
|
387
|
+
heights.push(el.offsetHeight || 48);
|
|
388
|
+
});
|
|
389
|
+
cardHeights.value = heights;
|
|
390
|
+
|
|
391
|
+
const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
|
|
392
|
+
const resolvedY = resolvePyramidCardPositions({
|
|
393
|
+
targetYList: targetY,
|
|
394
|
+
heights,
|
|
395
|
+
gap: 10,
|
|
396
|
+
minY: 16,
|
|
397
|
+
maxY: geo.value.viewBoxHeight - 16,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
cardDeltaYList.value = resolvedY.map((y, i) => y - (targetY[i] ?? y));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function scheduleCardUpdate(): void {
|
|
404
|
+
if (typeof window === 'undefined') {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (cardAnimationFrame !== undefined) {
|
|
408
|
+
cancelAnimationFrame(cardAnimationFrame);
|
|
409
|
+
}
|
|
410
|
+
cardAnimationFrame = requestAnimationFrame(() => {
|
|
411
|
+
cardAnimationFrame = undefined;
|
|
412
|
+
updateCardPositions();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let cardResizeObserver: ResizeObserver | null = null;
|
|
417
|
+
let cardMutationObserver: MutationObserver | null = null;
|
|
418
|
+
|
|
419
|
+
onMounted(() => {
|
|
420
|
+
scheduleCardUpdate();
|
|
421
|
+
if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
|
|
422
|
+
cardResizeObserver = new ResizeObserver(scheduleCardUpdate);
|
|
423
|
+
cardResizeObserver.observe(rootRef.value);
|
|
424
|
+
}
|
|
425
|
+
if (typeof MutationObserver !== 'undefined' && rootRef.value) {
|
|
426
|
+
cardMutationObserver = new MutationObserver(scheduleCardUpdate);
|
|
427
|
+
cardMutationObserver.observe(rootRef.value, {
|
|
428
|
+
childList: true,
|
|
429
|
+
subtree: true,
|
|
430
|
+
characterData: true,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
onUnmounted(() => {
|
|
436
|
+
if (cardAnimationFrame !== undefined) {
|
|
437
|
+
cancelAnimationFrame(cardAnimationFrame);
|
|
438
|
+
}
|
|
439
|
+
cardResizeObserver?.disconnect();
|
|
440
|
+
cardMutationObserver?.disconnect();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
provide(CIRCULAR_PYRAMID_ROOT_KEY, {
|
|
444
|
+
count: resolvedCount,
|
|
445
|
+
activeIndex: currentActiveIndex,
|
|
446
|
+
color: computed(() => props.color),
|
|
447
|
+
geo,
|
|
448
|
+
palette,
|
|
449
|
+
animation: isAnimated,
|
|
450
|
+
interactive: computed(() => props.interactive),
|
|
451
|
+
titleWidthPx,
|
|
452
|
+
contentWidthPx,
|
|
453
|
+
contentRightOffsetPx,
|
|
454
|
+
registerStack,
|
|
455
|
+
getLayerColor,
|
|
456
|
+
setActiveIndex,
|
|
457
|
+
getCardDeltaY,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Render from bottom layer up to top layer for natural overlapping perspective
|
|
461
|
+
const stackedLayers = computed(() => {
|
|
462
|
+
return [...geo.value.layers].reverse();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const lowestLayer = computed(() => {
|
|
466
|
+
const layers = geo.value.layers;
|
|
467
|
+
return layers.length > 0 ? layers[layers.length - 1] : undefined;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const uid = Math.random().toString(36).slice(2, 8);
|
|
471
|
+
const filterBlurId = `alpha-circular-pyramid-blur-${uid}`;
|
|
472
|
+
const filterActiveShadowId = `alpha-circular-pyramid-shadow-${uid}`;
|
|
473
|
+
const filterAmbientShadowId = `alpha-circular-pyramid-ambient-${uid}`;
|
|
474
|
+
|
|
475
|
+
const activeTopGradId = `alpha-circ-grad-act-top-${uid}`;
|
|
476
|
+
const activeSideGradId = `alpha-circ-grad-act-side-${uid}`;
|
|
477
|
+
const defaultTopGradId = `alpha-circ-grad-def-top-${uid}`;
|
|
478
|
+
const defaultSideGradId = `alpha-circ-grad-def-side-${uid}`;
|
|
479
|
+
|
|
480
|
+
function getInactiveTopGradId(index: number): string {
|
|
481
|
+
return `alpha-circ-grad-inact-top-${uid}-${index}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function getInactiveSideGradId(index: number): string {
|
|
485
|
+
return `alpha-circ-grad-inact-side-${uid}-${index}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function getActiveTopGradId(index: number): string {
|
|
489
|
+
return `alpha-circ-grad-act-top-${uid}-${index}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function getActiveSideGradId(index: number): string {
|
|
493
|
+
return `alpha-circ-grad-act-side-${uid}-${index}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function getActiveShadowId(index: number): string {
|
|
497
|
+
return `alpha-circ-shadow-${uid}-${index}`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { resolvedScale, containerHeight } = useDiagramAutoScale({
|
|
501
|
+
rootRef,
|
|
502
|
+
baseWidth: props.viewBoxWidth,
|
|
503
|
+
baseHeight: props.viewBoxHeight,
|
|
504
|
+
scale: () => props.scale,
|
|
505
|
+
autoScale: () => props.autoScale,
|
|
506
|
+
maxScale: () => props.maxScale,
|
|
507
|
+
height: () => props.height,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const stageTransform = computed(() => {
|
|
511
|
+
if (resolvedScale.value !== 1) {
|
|
512
|
+
return `scale(${resolvedScale.value})`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return undefined;
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const resolvedWidthStyle = computed(() => {
|
|
519
|
+
if (props.width !== undefined) {
|
|
520
|
+
return typeof props.width === 'number' ? `${props.width}px` : props.width;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return '100%';
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const resolvedHeightStyle = computed(() => {
|
|
527
|
+
if (props.height !== undefined) {
|
|
528
|
+
return typeof props.height === 'number'
|
|
529
|
+
? `${props.height}px`
|
|
530
|
+
: props.height;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (resolvedCount.value >= 6) {
|
|
534
|
+
return '480px';
|
|
535
|
+
}
|
|
536
|
+
if (resolvedCount.value >= 5) {
|
|
537
|
+
return '450px';
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return '440px';
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
function handleLayerClick(index: number, _event: MouseEvent): void {
|
|
544
|
+
if (props.interactive) {
|
|
545
|
+
if (internalActiveIndex.value === index) {
|
|
546
|
+
setActiveIndex(-1);
|
|
547
|
+
} else {
|
|
548
|
+
setActiveIndex(index);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function handleContainerClick(event: MouseEvent): void {
|
|
554
|
+
emit('click', event);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const { className, forwardedAttrs } = useMergedUnoAttrs(
|
|
558
|
+
'alpha-circular-pyramid w-full flex items-center justify-center select-none relative',
|
|
559
|
+
);
|
|
560
|
+
</script>
|
|
561
|
+
|
|
562
|
+
<template>
|
|
563
|
+
<div
|
|
564
|
+
ref="rootRef"
|
|
565
|
+
v-bind="forwardedAttrs()"
|
|
566
|
+
:class="className()"
|
|
567
|
+
:data-count="resolvedCount"
|
|
568
|
+
:data-active-index="currentActiveIndex"
|
|
569
|
+
:style="{
|
|
570
|
+
width: resolvedWidthStyle,
|
|
571
|
+
height: resolvedHeightStyle,
|
|
572
|
+
'--circular-pyramid-start-delay': `${resolvedStartDelay}ms`,
|
|
573
|
+
'--circular-pyramid-title-width': `${titleWidthPx}px`,
|
|
574
|
+
'--circular-pyramid-content-width': `${contentWidthPx}px`,
|
|
575
|
+
}"
|
|
576
|
+
@click="handleContainerClick"
|
|
577
|
+
>
|
|
578
|
+
<div
|
|
579
|
+
class="alpha-circular-pyramid__stage relative flex items-center justify-center overflow-visible flex-shrink-0"
|
|
580
|
+
:style="{
|
|
581
|
+
width: `${props.viewBoxWidth ?? 960}px`,
|
|
582
|
+
height: `${geo.viewBoxHeight}px`,
|
|
583
|
+
transform: stageTransform,
|
|
584
|
+
transformOrigin: 'center center',
|
|
585
|
+
}"
|
|
586
|
+
>
|
|
587
|
+
<!-- Central Circular Pyramid SVG Graphic (Strictly in middle between 150px and 180px) -->
|
|
588
|
+
<div
|
|
589
|
+
class="alpha-circular-pyramid__middle absolute inset-y-0 flex items-center justify-center pointer-events-none overflow-visible"
|
|
590
|
+
style="left: 150px; right: 180px;"
|
|
591
|
+
>
|
|
592
|
+
<svg
|
|
593
|
+
class="alpha-circular-pyramid__svg w-full h-full overflow-visible pointer-events-none"
|
|
594
|
+
:viewBox="`0 0 ${geo.viewBoxWidth} ${geo.viewBoxHeight}`"
|
|
595
|
+
preserveAspectRatio="xMidYMid meet"
|
|
596
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
597
|
+
>
|
|
598
|
+
<defs>
|
|
599
|
+
<!-- Per-layer Gradients: Inactive Frosted Glass & Active Luminous -->
|
|
600
|
+
<template v-for="layer in geo.layers" :key="layer.index">
|
|
601
|
+
<!-- Inactive Frosted Glass Top Face Gradient (Exact formula matching Image 1 & 2) -->
|
|
602
|
+
<linearGradient
|
|
603
|
+
:id="getInactiveTopGradId(layer.index)"
|
|
604
|
+
x1="90%"
|
|
605
|
+
y1="10%"
|
|
606
|
+
x2="10%"
|
|
607
|
+
y2="90%"
|
|
608
|
+
>
|
|
609
|
+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
|
|
610
|
+
<stop offset="45%" stop-color="#ffffff" stop-opacity="0.7" />
|
|
611
|
+
<stop
|
|
612
|
+
offset="80%"
|
|
613
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
|
|
614
|
+
stop-opacity="0.5"
|
|
615
|
+
/>
|
|
616
|
+
<stop
|
|
617
|
+
offset="100%"
|
|
618
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.78 })"
|
|
619
|
+
stop-opacity="0.35"
|
|
620
|
+
/>
|
|
621
|
+
</linearGradient>
|
|
622
|
+
<!-- Inactive Frosted Glass Rim Gradient (Exact formula matching Image 1 & 2) -->
|
|
623
|
+
<linearGradient
|
|
624
|
+
:id="getInactiveSideGradId(layer.index)"
|
|
625
|
+
x1="0%"
|
|
626
|
+
y1="0%"
|
|
627
|
+
x2="100%"
|
|
628
|
+
y2="0%"
|
|
629
|
+
>
|
|
630
|
+
<stop
|
|
631
|
+
offset="0%"
|
|
632
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.65 })"
|
|
633
|
+
stop-opacity="0.65"
|
|
634
|
+
/>
|
|
635
|
+
<stop
|
|
636
|
+
offset="50%"
|
|
637
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.75 })"
|
|
638
|
+
stop-opacity="0.5"
|
|
639
|
+
/>
|
|
640
|
+
<stop
|
|
641
|
+
offset="100%"
|
|
642
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
|
|
643
|
+
stop-opacity="0.75"
|
|
644
|
+
/>
|
|
645
|
+
</linearGradient>
|
|
646
|
+
<!-- Active Luminous Top Face Gradient -->
|
|
647
|
+
<linearGradient
|
|
648
|
+
:id="getActiveTopGradId(layer.index)"
|
|
649
|
+
x1="90%"
|
|
650
|
+
y1="10%"
|
|
651
|
+
x2="15%"
|
|
652
|
+
y2="90%"
|
|
653
|
+
>
|
|
654
|
+
<stop
|
|
655
|
+
offset="0%"
|
|
656
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.92 })"
|
|
657
|
+
stop-opacity="1"
|
|
658
|
+
/>
|
|
659
|
+
<stop
|
|
660
|
+
offset="28%"
|
|
661
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.55 })"
|
|
662
|
+
stop-opacity="1"
|
|
663
|
+
/>
|
|
664
|
+
<stop
|
|
665
|
+
offset="65%"
|
|
666
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.15 })"
|
|
667
|
+
stop-opacity="1"
|
|
668
|
+
/>
|
|
669
|
+
<stop
|
|
670
|
+
offset="100%"
|
|
671
|
+
:stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.18 })"
|
|
672
|
+
stop-opacity="1"
|
|
673
|
+
/>
|
|
674
|
+
</linearGradient>
|
|
675
|
+
<!-- Active 3D Rim Gradient -->
|
|
676
|
+
<linearGradient
|
|
677
|
+
:id="getActiveSideGradId(layer.index)"
|
|
678
|
+
x1="0%"
|
|
679
|
+
y1="0%"
|
|
680
|
+
x2="100%"
|
|
681
|
+
y2="0%"
|
|
682
|
+
>
|
|
683
|
+
<stop
|
|
684
|
+
offset="0%"
|
|
685
|
+
:stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.25 })"
|
|
686
|
+
/>
|
|
687
|
+
<stop
|
|
688
|
+
offset="50%"
|
|
689
|
+
:stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.08 })"
|
|
690
|
+
/>
|
|
691
|
+
<stop
|
|
692
|
+
offset="100%"
|
|
693
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.45 })"
|
|
694
|
+
/>
|
|
695
|
+
</linearGradient>
|
|
696
|
+
<!-- Active Drop Shadow / Glow Filter -->
|
|
697
|
+
<filter
|
|
698
|
+
:id="getActiveShadowId(layer.index)"
|
|
699
|
+
x="-40%"
|
|
700
|
+
y="-40%"
|
|
701
|
+
width="180%"
|
|
702
|
+
height="200%"
|
|
703
|
+
>
|
|
704
|
+
<feDropShadow
|
|
705
|
+
dx="0"
|
|
706
|
+
dy="8"
|
|
707
|
+
stdDeviation="10"
|
|
708
|
+
:flood-color="getLayerColor(layer.index)"
|
|
709
|
+
flood-opacity="0.35"
|
|
710
|
+
/>
|
|
711
|
+
</filter>
|
|
712
|
+
</template>
|
|
713
|
+
<!-- Fallback Global Gradients for Test Spec Compatibility -->
|
|
714
|
+
<linearGradient :id="activeTopGradId" x1="90%" y1="10%" x2="15%" y2="90%">
|
|
715
|
+
<stop offset="0%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.92 })" stop-opacity="1" />
|
|
716
|
+
<stop offset="100%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.18 })" stop-opacity="1" />
|
|
717
|
+
</linearGradient>
|
|
718
|
+
<linearGradient :id="activeSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
719
|
+
<stop offset="0%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.25 })" />
|
|
720
|
+
<stop offset="100%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.45 })" />
|
|
721
|
+
</linearGradient>
|
|
722
|
+
<linearGradient :id="defaultTopGradId" x1="90%" y1="10%" x2="10%" y2="90%">
|
|
723
|
+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
|
|
724
|
+
<stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.78 })" stop-opacity="0.35" />
|
|
725
|
+
</linearGradient>
|
|
726
|
+
<linearGradient :id="defaultSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
727
|
+
<stop offset="0%" :stop-color="lightenColor({ hex: props.color, ratio: 0.65 })" stop-opacity="0.65" />
|
|
728
|
+
<stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.88 })" stop-opacity="0.75" />
|
|
729
|
+
</linearGradient>
|
|
730
|
+
<filter :id="filterActiveShadowId" x="-40%" y="-40%" width="180%" height="200%">
|
|
731
|
+
<feDropShadow dx="0" dy="8" stdDeviation="10" :flood-color="activeLayerColor" flood-opacity="0.35" />
|
|
732
|
+
</filter>
|
|
733
|
+
<!-- Ambient Bottom Surface Shadow -->
|
|
734
|
+
<filter
|
|
735
|
+
:id="filterAmbientShadowId"
|
|
736
|
+
x="-30%"
|
|
737
|
+
y="-30%"
|
|
738
|
+
width="160%"
|
|
739
|
+
height="160%"
|
|
740
|
+
>
|
|
741
|
+
<feGaussianBlur stdDeviation="14" />
|
|
742
|
+
</filter>
|
|
743
|
+
</defs>
|
|
744
|
+
<!-- Ambient Ground Shadow (under the lowest pyramid disc) -->
|
|
745
|
+
<ellipse
|
|
746
|
+
v-if="lowestLayer"
|
|
747
|
+
:cx="geo.viewBoxWidth / 2"
|
|
748
|
+
:cy="lowestLayer.cy + lowestLayer.cylinderHeight + 14"
|
|
749
|
+
:rx="lowestLayer.rx * 0.92"
|
|
750
|
+
:ry="lowestLayer.ry * 0.28"
|
|
751
|
+
fill="#0f172a"
|
|
752
|
+
opacity="0.1"
|
|
753
|
+
:filter="`url(#${filterAmbientShadowId})`"
|
|
754
|
+
/>
|
|
755
|
+
<!-- Stacked Floating Circular Discs (Rendered from bottom to top) -->
|
|
756
|
+
<g
|
|
757
|
+
class="alpha-circular-pyramid__stack"
|
|
758
|
+
:class="{ 'alpha-circular-pyramid--animated': isAnimated }"
|
|
759
|
+
>
|
|
760
|
+
<g
|
|
761
|
+
v-for="layer in stackedLayers"
|
|
762
|
+
:key="layer.index"
|
|
763
|
+
class="alpha-circular-pyramid__layer transition-all duration-300 ease-out pointer-events-auto"
|
|
764
|
+
:class="[
|
|
765
|
+
layer.index === currentActiveIndex
|
|
766
|
+
? 'alpha-circular-pyramid__layer--active'
|
|
767
|
+
: 'alpha-circular-pyramid__layer--default',
|
|
768
|
+
`alpha-circular-pyramid__layer--${layer.index}`,
|
|
769
|
+
{ 'cursor-pointer': props.interactive },
|
|
770
|
+
]"
|
|
771
|
+
:data-layer-index="layer.index"
|
|
772
|
+
:data-is-active="layer.index === currentActiveIndex"
|
|
773
|
+
:filter="
|
|
774
|
+
layer.index === currentActiveIndex
|
|
775
|
+
? `url(#${getActiveShadowId(layer.index)})`
|
|
776
|
+
: undefined
|
|
777
|
+
"
|
|
778
|
+
@click.stop="handleLayerClick(layer.index, $event)"
|
|
779
|
+
>
|
|
780
|
+
<!-- 1. 3D Cylindrical Rim (Mantle) -->
|
|
781
|
+
<path
|
|
782
|
+
class="alpha-circular-pyramid__rim transition-all duration-300"
|
|
783
|
+
:d="layer.cylinderPath"
|
|
784
|
+
:fill="
|
|
785
|
+
layer.index === currentActiveIndex
|
|
786
|
+
? `url(#${getActiveSideGradId(layer.index)})`
|
|
787
|
+
: `url(#${getInactiveSideGradId(layer.index)})`
|
|
788
|
+
"
|
|
789
|
+
/>
|
|
790
|
+
<!-- 2. Disc Top Face Ellipse -->
|
|
791
|
+
<path
|
|
792
|
+
class="alpha-circular-pyramid__face transition-all duration-300"
|
|
793
|
+
:d="layer.topEllipsePath"
|
|
794
|
+
:fill="
|
|
795
|
+
layer.index === currentActiveIndex
|
|
796
|
+
? `url(#${getActiveTopGradId(layer.index)})`
|
|
797
|
+
: `url(#${getInactiveTopGradId(layer.index)})`
|
|
798
|
+
"
|
|
799
|
+
:stroke="
|
|
800
|
+
layer.index === currentActiveIndex
|
|
801
|
+
? 'none'
|
|
802
|
+
: 'rgba(255, 255, 255, 0.5)'
|
|
803
|
+
"
|
|
804
|
+
stroke-width="0.5"
|
|
805
|
+
/>
|
|
806
|
+
</g>
|
|
807
|
+
</g>
|
|
808
|
+
</svg>
|
|
809
|
+
</div>
|
|
810
|
+
<!-- Slotted or Default Stack Items -->
|
|
811
|
+
<slot v-if="slots.default" />
|
|
812
|
+
<template v-else>
|
|
813
|
+
<CircularPyramidStack
|
|
814
|
+
v-for="idx in resolvedCount"
|
|
815
|
+
:key="idx - 1"
|
|
816
|
+
:index="idx - 1"
|
|
817
|
+
/>
|
|
818
|
+
</template>
|
|
819
|
+
</div>
|
|
820
|
+
</div>
|
|
821
|
+
</template>
|
|
822
|
+
|
|
823
|
+
<style scoped>
|
|
824
|
+
.alpha-circular-pyramid {
|
|
825
|
+
box-sizing: border-box;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.alpha-circular-pyramid__layer {
|
|
829
|
+
transform-box: fill-box;
|
|
830
|
+
transform-origin: center;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.alpha-circular-pyramid__layer:hover {
|
|
834
|
+
filter: brightness(1.05);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--0 {
|
|
838
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 60ms) both;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--1 {
|
|
842
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 140ms) both;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--2 {
|
|
846
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 220ms) both;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--3 {
|
|
850
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 300ms) both;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--4 {
|
|
854
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 380ms) both;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.alpha-circular-pyramid--animated .alpha-circular-pyramid__layer--5 {
|
|
858
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--circular-pyramid-start-delay, 0ms) + 460ms) both;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
@keyframes alpha-pyramid-layer-enter {
|
|
862
|
+
0% {
|
|
863
|
+
opacity: 0;
|
|
864
|
+
transform: translateY(12px) scale(0.97);
|
|
865
|
+
}
|
|
866
|
+
100% {
|
|
867
|
+
opacity: 1;
|
|
868
|
+
transform: translateY(0) scale(1);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
</style>
|