@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,876 @@
|
|
|
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 { resolvePyramidCardPositions } from '../../../utils/pyramid/shared';
|
|
17
|
+
import {
|
|
18
|
+
clampSquareCount,
|
|
19
|
+
createSquarePyramidGeometry,
|
|
20
|
+
DEFAULT_SQUARE_PYRAMID_COLOR,
|
|
21
|
+
DEFAULT_SQUARE_PYRAMID_STACK_COLORS,
|
|
22
|
+
darkenColor,
|
|
23
|
+
lightenColor,
|
|
24
|
+
resolveSquarePyramidPalette,
|
|
25
|
+
SQUARE_PYRAMID_ROOT_KEY,
|
|
26
|
+
} from '../../../utils/pyramid/square-pyramid';
|
|
27
|
+
import { useDiagramAutoScale } from '../../../utils/useDiagramAutoScale';
|
|
28
|
+
import { useMergedUnoAttrs } from '../../../utils/useMergedUnoAttrs';
|
|
29
|
+
import SquarePyramidStack from './SquarePyramidStack.vue';
|
|
30
|
+
|
|
31
|
+
defineOptions({
|
|
32
|
+
inheritAttrs: false,
|
|
33
|
+
name: 'SquarePyramid',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export interface ISquarePyramidProps {
|
|
37
|
+
/**
|
|
38
|
+
* Number of square layers in the pyramid (1 to 6).
|
|
39
|
+
* Automatically inferred from slotted `<SquarePyramidStack>` components if omitted.
|
|
40
|
+
* Default: 4
|
|
41
|
+
*/
|
|
42
|
+
readonly count?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Index of the active square 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_SQUARE_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<ISquarePyramidProps>(), {
|
|
104
|
+
count: undefined,
|
|
105
|
+
activeIndex: -1,
|
|
106
|
+
color: DEFAULT_SQUARE_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 === 'SquarePyramidStack' ||
|
|
187
|
+
comp?.displayName === 'SquarePyramidStack'
|
|
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 === 'SquarePyramidStack' ||
|
|
209
|
+
comp?.displayName === 'SquarePyramidStack'
|
|
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_SQUARE_PYRAMID_STACK_COLORS[
|
|
278
|
+
Math.abs(index) % DEFAULT_SQUARE_PYRAMID_STACK_COLORS.length
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
return stackColor ?? props.color ?? DEFAULT_SQUARE_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 clampSquareCount(props.count);
|
|
295
|
+
}
|
|
296
|
+
if (slottedStackCount.value > 0) {
|
|
297
|
+
return clampSquareCount(slottedStackCount.value);
|
|
298
|
+
}
|
|
299
|
+
if (registeredStackTokens.value.length > 0) {
|
|
300
|
+
return clampSquareCount(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 createSquarePyramidGeometry({
|
|
316
|
+
count: resolvedCount.value,
|
|
317
|
+
viewBoxWidth: 630,
|
|
318
|
+
viewBoxHeight: vbHeight,
|
|
319
|
+
cx: 315,
|
|
320
|
+
bottomSize: 180,
|
|
321
|
+
topSize:
|
|
322
|
+
resolvedCount.value <= 2
|
|
323
|
+
? 100
|
|
324
|
+
: Math.max(70, 95 - (resolvedCount.value - 3) * 6),
|
|
325
|
+
slabHeight: resolvedCount.value >= 5 ? 8 : 10,
|
|
326
|
+
ratio: 0.46,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const palette = computed(() => {
|
|
331
|
+
return resolveSquarePyramidPalette(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
|
+
const CONTENT_RIGHT_OFFSET_PX = 0;
|
|
343
|
+
|
|
344
|
+
const titleWidthPx = computed(() => TITLE_WIDTH_PX);
|
|
345
|
+
const contentWidthPx = computed(() => CONTENT_WIDTH_PX);
|
|
346
|
+
const contentRightOffsetPx = computed(() => CONTENT_RIGHT_OFFSET_PX);
|
|
347
|
+
|
|
348
|
+
const cardHeights = ref<number[]>([]);
|
|
349
|
+
const cardDeltaYList = ref<number[]>([]);
|
|
350
|
+
|
|
351
|
+
const initialDeltaYList = computed(() => {
|
|
352
|
+
const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
|
|
353
|
+
const estimatedHeights = Array(targetY.length).fill(48);
|
|
354
|
+
const resolvedY = resolvePyramidCardPositions({
|
|
355
|
+
targetYList: targetY,
|
|
356
|
+
heights: estimatedHeights,
|
|
357
|
+
gap: 10,
|
|
358
|
+
minY: 16,
|
|
359
|
+
maxY: geo.value.viewBoxHeight - 16,
|
|
360
|
+
});
|
|
361
|
+
return resolvedY.map((y, i) => y - (targetY[i] ?? y));
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
function getCardDeltaY(index: number): number {
|
|
365
|
+
if (cardDeltaYList.value[index] !== undefined) {
|
|
366
|
+
return cardDeltaYList.value[index] ?? 0;
|
|
367
|
+
}
|
|
368
|
+
return initialDeltaYList.value[index] ?? 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let cardAnimationFrame: number | undefined;
|
|
372
|
+
|
|
373
|
+
function updateCardPositions(): void {
|
|
374
|
+
if (typeof window === 'undefined' || !rootRef.value) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const cardEls = rootRef.value.querySelectorAll<HTMLElement>(
|
|
378
|
+
'.alpha-square-pyramid-stack-content',
|
|
379
|
+
);
|
|
380
|
+
if (cardEls.length === 0) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const heights: number[] = [];
|
|
385
|
+
cardEls.forEach((el) => {
|
|
386
|
+
heights.push(el.offsetHeight || 48);
|
|
387
|
+
});
|
|
388
|
+
cardHeights.value = heights;
|
|
389
|
+
|
|
390
|
+
const targetY = geo.value.layers.map((l) => l.slabCenterY ?? l.cy);
|
|
391
|
+
const resolvedY = resolvePyramidCardPositions({
|
|
392
|
+
targetYList: targetY,
|
|
393
|
+
heights,
|
|
394
|
+
gap: 10,
|
|
395
|
+
minY: 16,
|
|
396
|
+
maxY: geo.value.viewBoxHeight - 16,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
cardDeltaYList.value = resolvedY.map((y, i) => y - (targetY[i] ?? y));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function scheduleCardUpdate(): void {
|
|
403
|
+
if (typeof window === 'undefined') {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (cardAnimationFrame !== undefined) {
|
|
407
|
+
cancelAnimationFrame(cardAnimationFrame);
|
|
408
|
+
}
|
|
409
|
+
cardAnimationFrame = requestAnimationFrame(() => {
|
|
410
|
+
cardAnimationFrame = undefined;
|
|
411
|
+
updateCardPositions();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let cardResizeObserver: ResizeObserver | null = null;
|
|
416
|
+
let cardMutationObserver: MutationObserver | null = null;
|
|
417
|
+
|
|
418
|
+
onMounted(() => {
|
|
419
|
+
scheduleCardUpdate();
|
|
420
|
+
if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
|
|
421
|
+
cardResizeObserver = new ResizeObserver(scheduleCardUpdate);
|
|
422
|
+
cardResizeObserver.observe(rootRef.value);
|
|
423
|
+
}
|
|
424
|
+
if (typeof MutationObserver !== 'undefined' && rootRef.value) {
|
|
425
|
+
cardMutationObserver = new MutationObserver(scheduleCardUpdate);
|
|
426
|
+
cardMutationObserver.observe(rootRef.value, {
|
|
427
|
+
childList: true,
|
|
428
|
+
subtree: true,
|
|
429
|
+
characterData: true,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
onUnmounted(() => {
|
|
435
|
+
if (cardAnimationFrame !== undefined) {
|
|
436
|
+
cancelAnimationFrame(cardAnimationFrame);
|
|
437
|
+
}
|
|
438
|
+
cardResizeObserver?.disconnect();
|
|
439
|
+
cardMutationObserver?.disconnect();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
provide(SQUARE_PYRAMID_ROOT_KEY, {
|
|
443
|
+
count: resolvedCount,
|
|
444
|
+
activeIndex: currentActiveIndex,
|
|
445
|
+
color: computed(() => props.color),
|
|
446
|
+
geo,
|
|
447
|
+
palette,
|
|
448
|
+
animation: isAnimated,
|
|
449
|
+
interactive: computed(() => props.interactive),
|
|
450
|
+
titleWidthPx,
|
|
451
|
+
contentWidthPx,
|
|
452
|
+
contentRightOffsetPx,
|
|
453
|
+
registerStack,
|
|
454
|
+
getLayerColor,
|
|
455
|
+
setActiveIndex,
|
|
456
|
+
getCardDeltaY,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Render from bottom layer up to top layer for natural overlapping perspective
|
|
460
|
+
const stackedLayers = computed(() => {
|
|
461
|
+
return [...geo.value.layers].reverse();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const lowestLayer = computed(() => {
|
|
465
|
+
const layers = geo.value.layers;
|
|
466
|
+
return layers.length > 0 ? layers[layers.length - 1] : undefined;
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const uid = Math.random().toString(36).slice(2, 8);
|
|
470
|
+
const filterActiveShadowId = `alpha-square-pyramid-shadow-${uid}`;
|
|
471
|
+
const filterAmbientShadowId = `alpha-square-pyramid-ambient-${uid}`;
|
|
472
|
+
|
|
473
|
+
const activeTopGradId = `alpha-sq-grad-act-top-${uid}`;
|
|
474
|
+
const activeSideGradId = `alpha-sq-grad-act-side-${uid}`;
|
|
475
|
+
const defaultTopGradId = `alpha-sq-grad-def-top-${uid}`;
|
|
476
|
+
const defaultSideGradId = `alpha-sq-grad-def-side-${uid}`;
|
|
477
|
+
|
|
478
|
+
function getInactiveTopGradId(index: number): string {
|
|
479
|
+
return `alpha-sq-grad-inact-top-${uid}-${index}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function getInactiveSideGradId(index: number): string {
|
|
483
|
+
return `alpha-sq-grad-inact-side-${uid}-${index}`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function getActiveTopGradId(index: number): string {
|
|
487
|
+
return `alpha-sq-grad-act-top-${uid}-${index}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getActiveSideGradId(index: number): string {
|
|
491
|
+
return `alpha-sq-grad-act-side-${uid}-${index}`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function getActiveShadowId(index: number): string {
|
|
495
|
+
return `alpha-sq-shadow-${uid}-${index}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const { resolvedScale } = useDiagramAutoScale({
|
|
499
|
+
rootRef,
|
|
500
|
+
baseWidth: props.viewBoxWidth,
|
|
501
|
+
baseHeight: props.viewBoxHeight,
|
|
502
|
+
scale: () => props.scale,
|
|
503
|
+
autoScale: () => props.autoScale,
|
|
504
|
+
maxScale: () => props.maxScale,
|
|
505
|
+
height: () => props.height,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const stageTransform = computed(() => {
|
|
509
|
+
if (resolvedScale.value !== 1) {
|
|
510
|
+
return `scale(${resolvedScale.value})`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return undefined;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const resolvedWidthStyle = computed(() => {
|
|
517
|
+
if (props.width !== undefined) {
|
|
518
|
+
return typeof props.width === 'number' ? `${props.width}px` : props.width;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return '100%';
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const resolvedHeightStyle = computed(() => {
|
|
525
|
+
if (props.height !== undefined) {
|
|
526
|
+
return typeof props.height === 'number'
|
|
527
|
+
? `${props.height}px`
|
|
528
|
+
: props.height;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (resolvedCount.value >= 6) {
|
|
532
|
+
return '480px';
|
|
533
|
+
}
|
|
534
|
+
if (resolvedCount.value >= 5) {
|
|
535
|
+
return '450px';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return '440px';
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
function handleLayerClick(index: number, _event: MouseEvent): void {
|
|
542
|
+
if (props.interactive) {
|
|
543
|
+
if (internalActiveIndex.value === index) {
|
|
544
|
+
setActiveIndex(-1);
|
|
545
|
+
} else {
|
|
546
|
+
setActiveIndex(index);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function handleContainerClick(event: MouseEvent): void {
|
|
552
|
+
emit('click', event);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const { className, forwardedAttrs } = useMergedUnoAttrs(
|
|
556
|
+
'alpha-square-pyramid w-full flex items-center justify-center select-none relative',
|
|
557
|
+
);
|
|
558
|
+
</script>
|
|
559
|
+
|
|
560
|
+
<template>
|
|
561
|
+
<div
|
|
562
|
+
ref="rootRef"
|
|
563
|
+
v-bind="forwardedAttrs()"
|
|
564
|
+
:class="className()"
|
|
565
|
+
:data-count="resolvedCount"
|
|
566
|
+
:data-active-index="currentActiveIndex"
|
|
567
|
+
:style="{
|
|
568
|
+
width: resolvedWidthStyle,
|
|
569
|
+
height: resolvedHeightStyle,
|
|
570
|
+
'--square-pyramid-start-delay': `${resolvedStartDelay}ms`,
|
|
571
|
+
'--square-pyramid-title-width': `${titleWidthPx}px`,
|
|
572
|
+
'--square-pyramid-content-width': `${contentWidthPx}px`,
|
|
573
|
+
}"
|
|
574
|
+
@click="handleContainerClick"
|
|
575
|
+
>
|
|
576
|
+
<div
|
|
577
|
+
class="alpha-square-pyramid__stage relative flex items-center justify-center overflow-visible flex-shrink-0"
|
|
578
|
+
:style="{
|
|
579
|
+
width: `${props.viewBoxWidth ?? 960}px`,
|
|
580
|
+
height: `${geo.viewBoxHeight}px`,
|
|
581
|
+
transform: stageTransform,
|
|
582
|
+
transformOrigin: 'center center',
|
|
583
|
+
}"
|
|
584
|
+
>
|
|
585
|
+
<!-- Central Square Pyramid SVG Graphic -->
|
|
586
|
+
<div
|
|
587
|
+
class="alpha-square-pyramid__middle absolute inset-y-0 flex items-center justify-center pointer-events-none overflow-visible"
|
|
588
|
+
style="left: 150px; right: 180px;"
|
|
589
|
+
>
|
|
590
|
+
<svg
|
|
591
|
+
class="alpha-square-pyramid__svg w-full h-full overflow-visible pointer-events-none"
|
|
592
|
+
:viewBox="`0 0 ${geo.viewBoxWidth} ${geo.viewBoxHeight}`"
|
|
593
|
+
preserveAspectRatio="xMidYMid meet"
|
|
594
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
595
|
+
>
|
|
596
|
+
<defs>
|
|
597
|
+
<!-- Per-layer Gradients: Inactive Frosted Glass & Active Luminous -->
|
|
598
|
+
<template v-for="layer in geo.layers" :key="layer.index">
|
|
599
|
+
<!-- Inactive Frosted Glass Top Face Gradient -->
|
|
600
|
+
<linearGradient
|
|
601
|
+
:id="getInactiveTopGradId(layer.index)"
|
|
602
|
+
x1="90%"
|
|
603
|
+
y1="10%"
|
|
604
|
+
x2="10%"
|
|
605
|
+
y2="90%"
|
|
606
|
+
>
|
|
607
|
+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
|
|
608
|
+
<stop offset="45%" stop-color="#ffffff" stop-opacity="0.7" />
|
|
609
|
+
<stop
|
|
610
|
+
offset="80%"
|
|
611
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
|
|
612
|
+
stop-opacity="0.5"
|
|
613
|
+
/>
|
|
614
|
+
<stop
|
|
615
|
+
offset="100%"
|
|
616
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.78 })"
|
|
617
|
+
stop-opacity="0.35"
|
|
618
|
+
/>
|
|
619
|
+
</linearGradient>
|
|
620
|
+
<!-- Inactive Frosted Glass Rim Gradient -->
|
|
621
|
+
<linearGradient
|
|
622
|
+
:id="getInactiveSideGradId(layer.index)"
|
|
623
|
+
x1="0%"
|
|
624
|
+
y1="0%"
|
|
625
|
+
x2="100%"
|
|
626
|
+
y2="0%"
|
|
627
|
+
>
|
|
628
|
+
<stop
|
|
629
|
+
offset="0%"
|
|
630
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.65 })"
|
|
631
|
+
stop-opacity="0.65"
|
|
632
|
+
/>
|
|
633
|
+
<stop
|
|
634
|
+
offset="50%"
|
|
635
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.75 })"
|
|
636
|
+
stop-opacity="0.5"
|
|
637
|
+
/>
|
|
638
|
+
<stop
|
|
639
|
+
offset="100%"
|
|
640
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.88 })"
|
|
641
|
+
stop-opacity="0.75"
|
|
642
|
+
/>
|
|
643
|
+
</linearGradient>
|
|
644
|
+
<!-- Active Luminous Top Face Gradient -->
|
|
645
|
+
<linearGradient
|
|
646
|
+
:id="getActiveTopGradId(layer.index)"
|
|
647
|
+
x1="90%"
|
|
648
|
+
y1="10%"
|
|
649
|
+
x2="15%"
|
|
650
|
+
y2="90%"
|
|
651
|
+
>
|
|
652
|
+
<stop
|
|
653
|
+
offset="0%"
|
|
654
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.92 })"
|
|
655
|
+
stop-opacity="1"
|
|
656
|
+
/>
|
|
657
|
+
<stop
|
|
658
|
+
offset="28%"
|
|
659
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.55 })"
|
|
660
|
+
stop-opacity="1"
|
|
661
|
+
/>
|
|
662
|
+
<stop
|
|
663
|
+
offset="65%"
|
|
664
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.15 })"
|
|
665
|
+
stop-opacity="1"
|
|
666
|
+
/>
|
|
667
|
+
<stop
|
|
668
|
+
offset="100%"
|
|
669
|
+
:stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.18 })"
|
|
670
|
+
stop-opacity="1"
|
|
671
|
+
/>
|
|
672
|
+
</linearGradient>
|
|
673
|
+
<!-- Active 3D Rim Gradient -->
|
|
674
|
+
<linearGradient
|
|
675
|
+
:id="getActiveSideGradId(layer.index)"
|
|
676
|
+
x1="0%"
|
|
677
|
+
y1="0%"
|
|
678
|
+
x2="100%"
|
|
679
|
+
y2="0%"
|
|
680
|
+
>
|
|
681
|
+
<stop
|
|
682
|
+
offset="0%"
|
|
683
|
+
:stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.25 })"
|
|
684
|
+
/>
|
|
685
|
+
<stop
|
|
686
|
+
offset="50%"
|
|
687
|
+
:stop-color="darkenColor({ hex: getLayerColor(layer.index), ratio: 0.08 })"
|
|
688
|
+
/>
|
|
689
|
+
<stop
|
|
690
|
+
offset="100%"
|
|
691
|
+
:stop-color="lightenColor({ hex: getLayerColor(layer.index), ratio: 0.45 })"
|
|
692
|
+
/>
|
|
693
|
+
</linearGradient>
|
|
694
|
+
<!-- Active Drop Shadow / Glow Filter -->
|
|
695
|
+
<filter
|
|
696
|
+
:id="getActiveShadowId(layer.index)"
|
|
697
|
+
x="-40%"
|
|
698
|
+
y="-40%"
|
|
699
|
+
width="180%"
|
|
700
|
+
height="200%"
|
|
701
|
+
>
|
|
702
|
+
<feDropShadow
|
|
703
|
+
dx="0"
|
|
704
|
+
dy="8"
|
|
705
|
+
stdDeviation="10"
|
|
706
|
+
:flood-color="getLayerColor(layer.index)"
|
|
707
|
+
flood-opacity="0.35"
|
|
708
|
+
/>
|
|
709
|
+
</filter>
|
|
710
|
+
</template>
|
|
711
|
+
<!-- Fallback Global Gradients for Test Spec Compatibility -->
|
|
712
|
+
<linearGradient :id="activeTopGradId" x1="90%" y1="10%" x2="15%" y2="90%">
|
|
713
|
+
<stop offset="0%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.92 })" stop-opacity="1" />
|
|
714
|
+
<stop offset="100%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.18 })" stop-opacity="1" />
|
|
715
|
+
</linearGradient>
|
|
716
|
+
<linearGradient :id="activeSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
717
|
+
<stop offset="0%" :stop-color="darkenColor({ hex: activeLayerColor, ratio: 0.25 })" />
|
|
718
|
+
<stop offset="100%" :stop-color="lightenColor({ hex: activeLayerColor, ratio: 0.45 })" />
|
|
719
|
+
</linearGradient>
|
|
720
|
+
<linearGradient :id="defaultTopGradId" x1="90%" y1="10%" x2="10%" y2="90%">
|
|
721
|
+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9" />
|
|
722
|
+
<stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.78 })" stop-opacity="0.35" />
|
|
723
|
+
</linearGradient>
|
|
724
|
+
<linearGradient :id="defaultSideGradId" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
725
|
+
<stop offset="0%" :stop-color="lightenColor({ hex: props.color, ratio: 0.65 })" stop-opacity="0.65" />
|
|
726
|
+
<stop offset="100%" :stop-color="lightenColor({ hex: props.color, ratio: 0.88 })" stop-opacity="0.75" />
|
|
727
|
+
</linearGradient>
|
|
728
|
+
<filter :id="filterActiveShadowId" x="-40%" y="-40%" width="180%" height="200%">
|
|
729
|
+
<feDropShadow dx="0" dy="8" stdDeviation="10" :flood-color="activeLayerColor" flood-opacity="0.35" />
|
|
730
|
+
</filter>
|
|
731
|
+
<!-- Ambient Bottom Surface Shadow -->
|
|
732
|
+
<filter
|
|
733
|
+
:id="filterAmbientShadowId"
|
|
734
|
+
x="-30%"
|
|
735
|
+
y="-30%"
|
|
736
|
+
width="160%"
|
|
737
|
+
height="160%"
|
|
738
|
+
>
|
|
739
|
+
<feGaussianBlur stdDeviation="14" />
|
|
740
|
+
</filter>
|
|
741
|
+
</defs>
|
|
742
|
+
<!-- Ambient Ground Shadow (under the lowest pyramid slab) -->
|
|
743
|
+
<ellipse
|
|
744
|
+
v-if="lowestLayer"
|
|
745
|
+
:cx="geo.viewBoxWidth / 2"
|
|
746
|
+
:cy="lowestLayer.cy + lowestLayer.slabHeight + 14"
|
|
747
|
+
:rx="lowestLayer.rx * 0.92"
|
|
748
|
+
:ry="lowestLayer.ry * 0.3"
|
|
749
|
+
fill="#0f172a"
|
|
750
|
+
opacity="0.1"
|
|
751
|
+
:filter="`url(#${filterAmbientShadowId})`"
|
|
752
|
+
/>
|
|
753
|
+
<!-- Stacked Floating Square Slabs (Rendered from bottom to top) -->
|
|
754
|
+
<g
|
|
755
|
+
class="alpha-square-pyramid__stack"
|
|
756
|
+
:class="{ 'alpha-square-pyramid--animated': isAnimated }"
|
|
757
|
+
>
|
|
758
|
+
<g
|
|
759
|
+
v-for="layer in stackedLayers"
|
|
760
|
+
:key="layer.index"
|
|
761
|
+
class="alpha-square-pyramid__layer transition-all duration-300 ease-out pointer-events-auto"
|
|
762
|
+
:class="[
|
|
763
|
+
layer.index === currentActiveIndex
|
|
764
|
+
? 'alpha-square-pyramid__layer--active'
|
|
765
|
+
: 'alpha-square-pyramid__layer--default',
|
|
766
|
+
`alpha-square-pyramid__layer--${layer.index}`,
|
|
767
|
+
{ 'cursor-pointer': props.interactive },
|
|
768
|
+
]"
|
|
769
|
+
:data-layer-index="layer.index"
|
|
770
|
+
:data-is-active="layer.index === currentActiveIndex"
|
|
771
|
+
:filter="
|
|
772
|
+
layer.index === currentActiveIndex
|
|
773
|
+
? `url(#${getActiveShadowId(layer.index)})`
|
|
774
|
+
: undefined
|
|
775
|
+
"
|
|
776
|
+
@click.stop="handleLayerClick(layer.index, $event)"
|
|
777
|
+
>
|
|
778
|
+
<!-- 1. 3D Square Slab Thickness Rim (Mantle) -->
|
|
779
|
+
<path
|
|
780
|
+
class="alpha-square-pyramid__rim transition-all duration-300"
|
|
781
|
+
:d="layer.sidePath"
|
|
782
|
+
:fill="
|
|
783
|
+
layer.index === currentActiveIndex
|
|
784
|
+
? `url(#${getActiveSideGradId(layer.index)})`
|
|
785
|
+
: `url(#${getInactiveSideGradId(layer.index)})`
|
|
786
|
+
"
|
|
787
|
+
/>
|
|
788
|
+
<!-- 2. Slab Top Face Isometric Rounded Diamond -->
|
|
789
|
+
<path
|
|
790
|
+
class="alpha-square-pyramid__face transition-all duration-300"
|
|
791
|
+
:d="layer.topFacePath"
|
|
792
|
+
:fill="
|
|
793
|
+
layer.index === currentActiveIndex
|
|
794
|
+
? `url(#${getActiveTopGradId(layer.index)})`
|
|
795
|
+
: `url(#${getInactiveTopGradId(layer.index)})`
|
|
796
|
+
"
|
|
797
|
+
:stroke="
|
|
798
|
+
layer.index === currentActiveIndex
|
|
799
|
+
? 'none'
|
|
800
|
+
: 'rgba(255, 255, 255, 0.5)'
|
|
801
|
+
"
|
|
802
|
+
stroke-width="0.5"
|
|
803
|
+
/>
|
|
804
|
+
<!-- 3. Inner Inset Highlight (matches translucent glassmorphic plate) -->
|
|
805
|
+
<path
|
|
806
|
+
class="alpha-square-pyramid__inner transition-all duration-300 pointer-events-none"
|
|
807
|
+
:d="layer.innerFacePath"
|
|
808
|
+
fill="#ffffff"
|
|
809
|
+
:fill-opacity="layer.index === currentActiveIndex ? 0.35 : 0.2"
|
|
810
|
+
/>
|
|
811
|
+
</g>
|
|
812
|
+
</g>
|
|
813
|
+
</svg>
|
|
814
|
+
</div>
|
|
815
|
+
<!-- Slotted or Default Stack Items -->
|
|
816
|
+
<slot v-if="slots.default" />
|
|
817
|
+
<template v-else>
|
|
818
|
+
<SquarePyramidStack
|
|
819
|
+
v-for="idx in resolvedCount"
|
|
820
|
+
:key="idx - 1"
|
|
821
|
+
:index="idx - 1"
|
|
822
|
+
/>
|
|
823
|
+
</template>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
</template>
|
|
827
|
+
|
|
828
|
+
<style scoped>
|
|
829
|
+
.alpha-square-pyramid {
|
|
830
|
+
box-sizing: border-box;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.alpha-square-pyramid__layer {
|
|
834
|
+
transform-box: fill-box;
|
|
835
|
+
transform-origin: center;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.alpha-square-pyramid__layer:hover {
|
|
839
|
+
filter: brightness(1.05);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.alpha-square-pyramid--animated .alpha-square-pyramid__layer--0 {
|
|
843
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 60ms) both;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.alpha-square-pyramid--animated .alpha-square-pyramid__layer--1 {
|
|
847
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 140ms) both;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.alpha-square-pyramid--animated .alpha-square-pyramid__layer--2 {
|
|
851
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 220ms) both;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.alpha-square-pyramid--animated .alpha-square-pyramid__layer--3 {
|
|
855
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 300ms) both;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.alpha-square-pyramid--animated .alpha-square-pyramid__layer--4 {
|
|
859
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 380ms) both;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.alpha-square-pyramid--animated .alpha-square-pyramid__layer--5 {
|
|
863
|
+
animation: alpha-pyramid-layer-enter 450ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--square-pyramid-start-delay, 0ms) + 460ms) both;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
@keyframes alpha-pyramid-layer-enter {
|
|
867
|
+
0% {
|
|
868
|
+
opacity: 0;
|
|
869
|
+
transform: translateY(12px) scale(0.97);
|
|
870
|
+
}
|
|
871
|
+
100% {
|
|
872
|
+
opacity: 1;
|
|
873
|
+
transform: translateY(0) scale(1);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
</style>
|