lumina-slides 8.9.4 → 9.0.0
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/LUMINA_LLM_EXAMPLES.json +234 -0
- package/README.md +18 -18
- package/dist/lumina-slides.js +13207 -12659
- package/dist/lumina-slides.umd.cjs +215 -215
- package/dist/style.css +1 -1
- package/package.json +5 -4
- package/src/App.vue +16 -0
- package/src/animation/index.ts +11 -0
- package/src/animation/registry.ts +126 -0
- package/src/animation/stagger.ts +95 -0
- package/src/animation/types.ts +53 -0
- package/src/components/LandingPage.vue +229 -0
- package/src/components/LuminaDeck.vue +224 -0
- package/src/components/LuminaSpeakerNotes.vue +701 -0
- package/src/components/base/BaseSlide.vue +122 -0
- package/src/components/base/LuminaElement.vue +67 -0
- package/src/components/base/VideoPlayer.vue +204 -0
- package/src/components/layouts/LayoutAuto.vue +71 -0
- package/src/components/layouts/LayoutChart.vue +287 -0
- package/src/components/layouts/LayoutCustom.vue +92 -0
- package/src/components/layouts/LayoutDiagram.vue +253 -0
- package/src/components/layouts/LayoutFeatures.vue +121 -0
- package/src/components/layouts/LayoutFlex.vue +172 -0
- package/src/components/layouts/LayoutFree.vue +62 -0
- package/src/components/layouts/LayoutHalf.vue +127 -0
- package/src/components/layouts/LayoutStatement.vue +74 -0
- package/src/components/layouts/LayoutSteps.vue +106 -0
- package/src/components/layouts/LayoutTimeline.vue +104 -0
- package/src/components/layouts/LayoutVideo.vue +41 -0
- package/src/components/parts/FlexBullets.vue +45 -0
- package/src/components/parts/FlexButton.vue +132 -0
- package/src/components/parts/FlexImage.vue +54 -0
- package/src/components/parts/FlexOrdered.vue +44 -0
- package/src/components/parts/FlexSpacer.vue +13 -0
- package/src/components/parts/FlexStepper.vue +59 -0
- package/src/components/parts/FlexText.vue +29 -0
- package/src/components/parts/FlexTimeline.vue +67 -0
- package/src/components/parts/FlexTitle.vue +39 -0
- package/src/components/parts/LuminaBackground.vue +100 -0
- package/src/components/site/LivePreview.vue +101 -0
- package/src/components/site/SiteApi.vue +301 -0
- package/src/components/site/SiteDashboard.vue +604 -0
- package/src/components/site/SiteDocs.vue +3267 -0
- package/src/components/site/SiteExamples.vue +65 -0
- package/src/components/site/SiteFooter.vue +6 -0
- package/src/components/site/SiteHome.vue +362 -0
- package/src/components/site/SiteNavBar.vue +122 -0
- package/src/components/site/SitePlayground.vue +389 -0
- package/src/components/site/SitePromptBuilder.vue +266 -0
- package/src/components/site/SiteUserMenu.vue +90 -0
- package/src/components/studio/ActionEditor.vue +108 -0
- package/src/components/studio/ArrayEditor.vue +124 -0
- package/src/components/studio/CollapsibleSection.vue +33 -0
- package/src/components/studio/ColorField.vue +22 -0
- package/src/components/studio/EditorCanvas.vue +326 -0
- package/src/components/studio/EditorLayoutFeatures.vue +18 -0
- package/src/components/studio/EditorLayoutFixed.vue +46 -0
- package/src/components/studio/EditorLayoutFlex.vue +133 -0
- package/src/components/studio/EditorLayoutHalf.vue +18 -0
- package/src/components/studio/EditorLayoutStatement.vue +18 -0
- package/src/components/studio/EditorLayoutSteps.vue +18 -0
- package/src/components/studio/EditorLayoutTimeline.vue +18 -0
- package/src/components/studio/EditorNode.vue +89 -0
- package/src/components/studio/FieldEditor.vue +133 -0
- package/src/components/studio/IconPicker.vue +109 -0
- package/src/components/studio/LayerItem.vue +117 -0
- package/src/components/studio/LuminaStudio.vue +30 -0
- package/src/components/studio/SaveSuccessModal.vue +138 -0
- package/src/components/studio/SlideNavigator.vue +373 -0
- package/src/components/studio/SliderField.vue +44 -0
- package/src/components/studio/StudioInspector.vue +595 -0
- package/src/components/studio/StudioJsonEditor.vue +191 -0
- package/src/components/studio/StudioLayers.vue +145 -0
- package/src/components/studio/StudioSettings.vue +514 -0
- package/src/components/studio/StudioSidebar.vue +29 -0
- package/src/components/studio/StudioToolbar.vue +222 -0
- package/src/components/studio/fieldLabels.ts +224 -0
- package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
- package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
- package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
- package/src/composables/useAuth.ts +87 -0
- package/src/composables/useEditor.ts +224 -0
- package/src/composables/useElementState.ts +81 -0
- package/src/composables/useFlexLayout.ts +122 -0
- package/src/composables/useKeyboard.ts +45 -0
- package/src/composables/useLumina.ts +32 -0
- package/src/composables/useStudio.ts +87 -0
- package/src/composables/useSwipeNav.ts +53 -0
- package/src/composables/useTransition.ts +373 -0
- package/src/core/Lumina.ts +819 -0
- package/src/core/animationConfig.ts +251 -0
- package/src/core/compression.ts +34 -0
- package/src/core/elementController.ts +170 -0
- package/src/core/elementId.ts +27 -0
- package/src/core/elementResolver.ts +207 -0
- package/src/core/events.ts +53 -0
- package/src/core/fonts.ts +100 -0
- package/src/core/presets.ts +231 -0
- package/src/core/prompts.ts +272 -0
- package/src/core/schema.ts +478 -0
- package/src/core/speaker-channel.ts +250 -0
- package/src/core/store.ts +461 -0
- package/src/core/theme.ts +666 -0
- package/src/core/types.ts +1611 -0
- package/src/directives/vStudio.ts +45 -0
- package/src/index.ts +175 -0
- package/src/main.ts +17 -0
- package/src/router/index.ts +92 -0
- package/src/style/main.css +462 -0
- package/src/utils/deep.ts +127 -0
- package/src/utils/firebase.ts +184 -0
- package/src/utils/streaming.ts +134 -0
- package/src/views/DashboardView.vue +32 -0
- package/src/views/DeckView.vue +289 -0
- package/src/views/HomeView.vue +17 -0
- package/src/views/SiteLayout.vue +21 -0
- package/src/views/StudioView.vue +61 -0
- package/src/vite-env.d.ts +6 -0
- package/IMPLEMENTATION.md +0 -418
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { computed, inject } from 'vue';
|
|
2
|
+
import { StoreKey } from '../core/store';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LUMINA COMPOSABLE
|
|
6
|
+
*
|
|
7
|
+
* The primary way for Vue components (Slides, Layouts) to interact with the Engine.
|
|
8
|
+
* Provides typesafe access to state, getters, and actions.
|
|
9
|
+
*
|
|
10
|
+
* @throws Error if used outside of a <LuminaDeck> context (Dependency Injection).
|
|
11
|
+
* @returns {Object} { slide, index, total, next, prev, goto, options }
|
|
12
|
+
*/
|
|
13
|
+
export function useLumina() {
|
|
14
|
+
const store = inject(StoreKey);
|
|
15
|
+
|
|
16
|
+
if (!store) {
|
|
17
|
+
throw new Error("Lumina: Component must be used within a Lumina instance.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
// State
|
|
22
|
+
slide: computed(() => store.currentSlide()),
|
|
23
|
+
index: computed(() => store.state.currentIndex),
|
|
24
|
+
total: computed(() => store.state.deck?.slides.length || 0),
|
|
25
|
+
|
|
26
|
+
// Actions
|
|
27
|
+
next: store.next,
|
|
28
|
+
prev: store.prev,
|
|
29
|
+
goto: store.goto,
|
|
30
|
+
options: store.state.options
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { reactive, computed, inject, InjectionKey } from 'vue';
|
|
2
|
+
import { StoreKey } from '../core/store';
|
|
3
|
+
import { bus } from '../core/events';
|
|
4
|
+
|
|
5
|
+
export const StudioKey: InjectionKey<ReturnType<typeof createStudio>> = Symbol('StudioKey');
|
|
6
|
+
|
|
7
|
+
export function createStudio() {
|
|
8
|
+
const store = inject(StoreKey);
|
|
9
|
+
if (!store) {
|
|
10
|
+
throw new Error("Lumina Studio requires Lumina Store.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const state = reactive({
|
|
14
|
+
isEnabled: false,
|
|
15
|
+
selectedElementPath: null as string | null,
|
|
16
|
+
selectedElementSchema: null as string | null, // 'title', 'image', etc.
|
|
17
|
+
selectionRect: null as DOMRect | null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const toggle = () => {
|
|
21
|
+
state.isEnabled = !state.isEnabled;
|
|
22
|
+
if (!state.isEnabled) {
|
|
23
|
+
clearSelection();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const selectElement = (path: string, schema: string, rect?: DOMRect) => {
|
|
28
|
+
if (!state.isEnabled) return;
|
|
29
|
+
state.selectedElementPath = path;
|
|
30
|
+
state.selectedElementSchema = schema;
|
|
31
|
+
if (rect) state.selectionRect = rect;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const clearSelection = () => {
|
|
35
|
+
state.selectedElementPath = null;
|
|
36
|
+
state.selectedElementSchema = null;
|
|
37
|
+
state.selectionRect = null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const currentSlide = computed(() => store.currentSlide());
|
|
41
|
+
|
|
42
|
+
// Helper to get nested value by path string "elements.0.title"
|
|
43
|
+
const getValueByPath = (obj: any, path: string) => {
|
|
44
|
+
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const selectedData = computed(() => {
|
|
48
|
+
if (!state.selectedElementPath || !currentSlide.value) return null;
|
|
49
|
+
// If path is empty or 'root', return slide itself
|
|
50
|
+
if (state.selectedElementPath === 'root') return currentSlide.value;
|
|
51
|
+
return getValueByPath(currentSlide.value, state.selectedElementPath);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const updateSelected = (payload: any) => {
|
|
55
|
+
if (!state.selectedElementPath || !store.state.deck) return;
|
|
56
|
+
|
|
57
|
+
const slideIndex = store.state.currentIndex;
|
|
58
|
+
// If path is 'root', we are updating the slide itself
|
|
59
|
+
const relativePath = state.selectedElementPath === 'root' ? '' : '.' + state.selectedElementPath;
|
|
60
|
+
const fullPath = `slides.${slideIndex}${relativePath}`;
|
|
61
|
+
|
|
62
|
+
// Update the store directly
|
|
63
|
+
if (store.updateNode) {
|
|
64
|
+
store.updateNode(fullPath, payload);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Also emit for external listeners if needed
|
|
68
|
+
bus.emit('studio:update', { path: fullPath, value: payload });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
state,
|
|
73
|
+
toggle,
|
|
74
|
+
selectElement,
|
|
75
|
+
clearSelection,
|
|
76
|
+
selectedData,
|
|
77
|
+
updateSelected
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useStudio() {
|
|
82
|
+
const studio = inject(StudioKey);
|
|
83
|
+
if (!studio) {
|
|
84
|
+
throw new Error("useStudio must be used within LuminaStudio");
|
|
85
|
+
}
|
|
86
|
+
return studio;
|
|
87
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ref, inject } from 'vue';
|
|
2
|
+
import { StoreKey } from '../core/store';
|
|
3
|
+
|
|
4
|
+
const SWIPE_THRESHOLD = 50;
|
|
5
|
+
const SWIPE_HORIZONTAL_RATIO = 1.5;
|
|
6
|
+
const THROTTLE_MS = 400;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Touch swipe to navigate between slides on mobile.
|
|
10
|
+
* Swipe left -> next, swipe right -> prev.
|
|
11
|
+
* Only triggers when the gesture is clearly horizontal (avoids conflict with vertical scroll).
|
|
12
|
+
*/
|
|
13
|
+
export function useSwipeNav() {
|
|
14
|
+
const store = inject(StoreKey);
|
|
15
|
+
if (!store) return { onTouchStart: () => {}, onTouchEnd: () => {} };
|
|
16
|
+
|
|
17
|
+
const touchStart = ref<{ x: number; y: number } | null>(null);
|
|
18
|
+
let lastSwipeTime = 0;
|
|
19
|
+
|
|
20
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
21
|
+
if (e.touches.length !== 1) return;
|
|
22
|
+
touchStart.value = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const onTouchEnd = (e: TouchEvent) => {
|
|
26
|
+
const start = touchStart.value;
|
|
27
|
+
touchStart.value = null;
|
|
28
|
+
if (!start || e.changedTouches.length !== 1) return;
|
|
29
|
+
|
|
30
|
+
const { options } = store.state;
|
|
31
|
+
if (!options.navigation) return;
|
|
32
|
+
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
if (now - lastSwipeTime < THROTTLE_MS) return;
|
|
35
|
+
|
|
36
|
+
const end = e.changedTouches[0];
|
|
37
|
+
const dx = end.clientX - start.x;
|
|
38
|
+
const dy = end.clientY - start.y;
|
|
39
|
+
|
|
40
|
+
if (Math.abs(dx) < SWIPE_THRESHOLD) return;
|
|
41
|
+
if (Math.abs(dx) <= Math.abs(dy) * SWIPE_HORIZONTAL_RATIO) return;
|
|
42
|
+
|
|
43
|
+
if (dx < 0 && store.hasNext()) {
|
|
44
|
+
store.next();
|
|
45
|
+
lastSwipeTime = now;
|
|
46
|
+
} else if (dx > 0 && store.hasPrev()) {
|
|
47
|
+
store.prev();
|
|
48
|
+
lastSwipeTime = now;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return { onTouchStart, onTouchEnd };
|
|
53
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { onMounted, onUnmounted, nextTick, inject, watch, type Ref } from 'vue';
|
|
2
|
+
import gsap from 'gsap';
|
|
3
|
+
import { StoreKey } from '../core/store';
|
|
4
|
+
import { resolveTransitionConfig } from '../core/animationConfig';
|
|
5
|
+
import type { BaseSlideData } from '../core/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* REUSABLE TRANSITION SYSTEM
|
|
9
|
+
*
|
|
10
|
+
* Automatically applies entrance animations to elements with 'reveal-*' classes.
|
|
11
|
+
* Reads configuration from the store, prioritizing slide-level then deck-level meta.
|
|
12
|
+
*
|
|
13
|
+
* @param containerRef - Vue Ref pointing to the slide's root element.
|
|
14
|
+
* @param slideData - Optional slide-specific data for local overrides.
|
|
15
|
+
* @param onComplete - Optional callback fired when the entrance animation finishes.
|
|
16
|
+
*/
|
|
17
|
+
/** True if container itself or any [data-lumina-id] descendant has elementState.visible === false. */
|
|
18
|
+
function hasElementControlledHiddenDescendant(
|
|
19
|
+
container: Element,
|
|
20
|
+
elementState: Record<string, { visible?: boolean }>
|
|
21
|
+
): boolean {
|
|
22
|
+
const selfId = container.getAttribute?.('data-lumina-id');
|
|
23
|
+
if (selfId && elementState[selfId]?.visible === false) return true;
|
|
24
|
+
const nodes = container.querySelectorAll?.('[data-lumina-id]') || [];
|
|
25
|
+
for (const el of nodes) {
|
|
26
|
+
const id = el.getAttribute('data-lumina-id');
|
|
27
|
+
if (id && elementState[id]?.visible === false) return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useTransition(containerRef: Ref<HTMLElement | null>, slideData?: BaseSlideData, onComplete?: () => void) {
|
|
33
|
+
const store = inject(StoreKey);
|
|
34
|
+
let ctx: gsap.Context | null = null;
|
|
35
|
+
|
|
36
|
+
// Helper: Manually split text into spans for character/word animation
|
|
37
|
+
const splitText = (elements: NodeListOf<Element> | Element[]) => {
|
|
38
|
+
// Restore elements that are no longer intended for split text
|
|
39
|
+
containerRef.value?.querySelectorAll('[data-split-init]').forEach(el => {
|
|
40
|
+
const element = el as HTMLElement;
|
|
41
|
+
if (!element.hasAttribute('data-split-text')) {
|
|
42
|
+
element.innerHTML = element.dataset.splitTextValue || element.innerText;
|
|
43
|
+
delete element.dataset.splitInit;
|
|
44
|
+
delete element.dataset.splitTextValue;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
elements.forEach(el => {
|
|
49
|
+
const element = el as HTMLElement;
|
|
50
|
+
const text = element.innerText;
|
|
51
|
+
if (!text) return;
|
|
52
|
+
|
|
53
|
+
// If text changed, re-split
|
|
54
|
+
if (element.dataset.splitTextValue === text) return;
|
|
55
|
+
|
|
56
|
+
element.innerHTML = text.split('').map(char =>
|
|
57
|
+
char === ' ' ? ' ' : `<span class="inline-block reveal-char">${char}</span>`
|
|
58
|
+
).join('');
|
|
59
|
+
|
|
60
|
+
element.dataset.splitInit = 'true';
|
|
61
|
+
element.dataset.splitTextValue = text;
|
|
62
|
+
element.style.whiteSpace = 'normal';
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const animateIn = (immediate = false) => {
|
|
67
|
+
if (!containerRef.value) return;
|
|
68
|
+
|
|
69
|
+
ctx?.revert();
|
|
70
|
+
|
|
71
|
+
const config = resolveTransitionConfig(store, slideData);
|
|
72
|
+
const { enabled, type, entranceReveal } = config;
|
|
73
|
+
|
|
74
|
+
// Initial Properties
|
|
75
|
+
const isFlip = type === 'flip';
|
|
76
|
+
const isZoomRotate = type === 'zoom-rotate';
|
|
77
|
+
const isElastic = type === 'elastic-up';
|
|
78
|
+
const isBlur = type === 'blur';
|
|
79
|
+
const isSkew = type === 'skew';
|
|
80
|
+
const isSpring = type === 'spring' || isElastic;
|
|
81
|
+
|
|
82
|
+
// 1. NO REVEAL TIMELINE: when entranceReveal is off (default), animations disabled, or immediate
|
|
83
|
+
// The built-in GSAP reveal on .reveal-* runs only when meta.effects.entranceReveal or
|
|
84
|
+
// options.animation.entranceReveal is explicitly true. Otherwise we only set final state
|
|
85
|
+
// (excluded=opacity 0, toReveal=opacity 1) for any .reveal-* present; no stagger/timeline.
|
|
86
|
+
if (enabled === false || immediate || !entranceReveal) {
|
|
87
|
+
const allPossible = [
|
|
88
|
+
'.reveal-up', '.reveal-zoom', '.reveal-card', '.reveal-left', '.reveal-right',
|
|
89
|
+
'.reveal-content > *',
|
|
90
|
+
'.flex-layout > div > *',
|
|
91
|
+
'.reveal-img', '.reveal-char', '[data-split-text]', '.reveal-child'
|
|
92
|
+
].join(', ');
|
|
93
|
+
|
|
94
|
+
// Should also process split text if needed for immediate display
|
|
95
|
+
const textToSplit = containerRef.value.querySelectorAll('[data-split-text]');
|
|
96
|
+
if (textToSplit.length) splitText(textToSplit);
|
|
97
|
+
|
|
98
|
+
const es = store?.state?.elementState ?? {};
|
|
99
|
+
const allEls = Array.from(containerRef.value.querySelectorAll(allPossible));
|
|
100
|
+
const toReveal = allEls.filter(e => !hasElementControlledHiddenDescendant(e, es));
|
|
101
|
+
const excluded = allEls.filter(e => hasElementControlledHiddenDescendant(e, es));
|
|
102
|
+
gsap.set(excluded, { opacity: 0 });
|
|
103
|
+
gsap.set(toReveal, {
|
|
104
|
+
opacity: 1, y: 0, x: 0, scale: 1, filter: 'blur(0px)', visibility: 'visible', rotation: 0, rotationY: 0, skewY: 0
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Ensure split text chars are visible
|
|
108
|
+
const chars = containerRef.value.querySelectorAll('.reveal-char');
|
|
109
|
+
if (chars.length) {
|
|
110
|
+
gsap.set(chars, { y: '0%', opacity: 1, rotateX: 0 });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. ANIMATION CONTEXT
|
|
117
|
+
ctx = gsap.context(() => {
|
|
118
|
+
const el = containerRef.value!;
|
|
119
|
+
|
|
120
|
+
// 1. Handle Text Splitting FIRST
|
|
121
|
+
const textToSplit = el.querySelectorAll('[data-split-text]');
|
|
122
|
+
if (textToSplit.length) splitText(textToSplit);
|
|
123
|
+
|
|
124
|
+
// Initial Properties
|
|
125
|
+
const initY = (type === 'fade' || isBlur || isFlip) ? 0 : (isElastic ? 100 : 30);
|
|
126
|
+
const initRotationY = isFlip ? 90 : 0;
|
|
127
|
+
const initRotation = isZoomRotate ? -10 : 0;
|
|
128
|
+
const initScale = (type === 'zoom' || isZoomRotate) ? 0.7 : 1;
|
|
129
|
+
const initBlur = isBlur ? 15 : 0;
|
|
130
|
+
const initSkew = isSkew ? 10 : 0;
|
|
131
|
+
|
|
132
|
+
// Set Initial States
|
|
133
|
+
const standardSelector = [
|
|
134
|
+
'.reveal-up', '.reveal-zoom', '.reveal-card', '.reveal-left', '.reveal-right',
|
|
135
|
+
'.reveal-content > *',
|
|
136
|
+
'.flex-layout > div > *'
|
|
137
|
+
].join(', ');
|
|
138
|
+
|
|
139
|
+
const es = store?.state?.elementState ?? {};
|
|
140
|
+
const raw = Array.from(el.querySelectorAll(standardSelector)).filter(item => !item.hasAttribute('data-split-text'));
|
|
141
|
+
const standardElements = raw.filter(item => !hasElementControlledHiddenDescendant(item, es)) as HTMLElement[];
|
|
142
|
+
const excluded = raw.filter(item => hasElementControlledHiddenDescendant(item, es));
|
|
143
|
+
if (excluded.length) {
|
|
144
|
+
gsap.set(excluded, { opacity: 0 });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const chars = el.querySelectorAll('.reveal-char');
|
|
148
|
+
const images = el.querySelectorAll('.reveal-img');
|
|
149
|
+
const childReveals = el.querySelectorAll('.reveal-child');
|
|
150
|
+
|
|
151
|
+
// Ensure split-text containers themselves are visible (spans will handle the staggering)
|
|
152
|
+
if (textToSplit.length) {
|
|
153
|
+
gsap.set(textToSplit, { opacity: 1, visibility: 'visible' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Apply Initial States for standard elements
|
|
157
|
+
if (standardElements.length) {
|
|
158
|
+
gsap.set(standardElements, {
|
|
159
|
+
y: initY,
|
|
160
|
+
opacity: 0,
|
|
161
|
+
scale: initScale,
|
|
162
|
+
rotationY: initRotationY,
|
|
163
|
+
rotation: initRotation,
|
|
164
|
+
filter: `blur(${initBlur}px)`,
|
|
165
|
+
skewY: initSkew,
|
|
166
|
+
transformPerspective: 1200
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Directional Overrides
|
|
170
|
+
const lefts = standardElements.filter(e => e.classList.contains('reveal-left'));
|
|
171
|
+
const rights = standardElements.filter(e => e.classList.contains('reveal-right'));
|
|
172
|
+
if (lefts.length > 0) gsap.set(lefts, { x: -50, y: 0 });
|
|
173
|
+
if (rights.length > 0) gsap.set(rights, { x: 50, y: 0 });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (images.length) {
|
|
177
|
+
gsap.set(images, { scale: 1.1, opacity: 0, filter: 'blur(10px)' });
|
|
178
|
+
}
|
|
179
|
+
if (chars.length) {
|
|
180
|
+
gsap.set(chars, { y: '100%', opacity: 0, rotateX: -90 });
|
|
181
|
+
}
|
|
182
|
+
if (childReveals.length) {
|
|
183
|
+
gsap.set(childReveals, { y: 15, opacity: 0 });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Timeline
|
|
187
|
+
const dIn = config.durationIn || (isElastic ? config.durationInElastic : config.durationInStandard);
|
|
188
|
+
const tl = gsap.timeline({
|
|
189
|
+
defaults: {
|
|
190
|
+
ease: config.ease || (isElastic ? config.easeElastic : (isSpring ? config.easeSpring : config.ease)),
|
|
191
|
+
duration: dIn
|
|
192
|
+
},
|
|
193
|
+
onComplete
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Start Animation Sequence
|
|
197
|
+
// ------------------------
|
|
198
|
+
|
|
199
|
+
// 1. Images
|
|
200
|
+
if (images.length) {
|
|
201
|
+
tl.to(images, {
|
|
202
|
+
scale: 1, opacity: 1, filter: 'blur(0px)',
|
|
203
|
+
duration: dIn * config.imageDurationMultiplier,
|
|
204
|
+
ease: config.imageEase
|
|
205
|
+
}, 0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 2. Characters (Text)
|
|
209
|
+
if (chars.length) {
|
|
210
|
+
tl.to(chars, {
|
|
211
|
+
y: '0%', opacity: 1, rotateX: 0,
|
|
212
|
+
duration: dIn * config.characterDurationFactor,
|
|
213
|
+
stagger: config.characterStagger,
|
|
214
|
+
ease: config.characterEase
|
|
215
|
+
}, 0.05);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 3. Standard Elements (Staggered)
|
|
219
|
+
if (standardElements.length) {
|
|
220
|
+
tl.to(standardElements, {
|
|
221
|
+
y: 0, x: 0, opacity: 1, scale: 1,
|
|
222
|
+
rotationY: 0, rotation: 0,
|
|
223
|
+
filter: 'blur(0px)',
|
|
224
|
+
skewY: 0,
|
|
225
|
+
duration: dIn,
|
|
226
|
+
stagger: config.stagger
|
|
227
|
+
}, 0.1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 4. Reveal Children (Micro-stagger inside cards)
|
|
231
|
+
if (childReveals.length) {
|
|
232
|
+
tl.to(childReveals, {
|
|
233
|
+
y: 0, opacity: 1,
|
|
234
|
+
duration: dIn * config.childRevealDurationFactor,
|
|
235
|
+
stagger: config.childRevealStagger,
|
|
236
|
+
ease: config.childRevealEase
|
|
237
|
+
}, config.childRevealStart);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
}, containerRef.value);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const animateOut = (): Promise<void> => {
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
if (!containerRef.value) { resolve(); return; }
|
|
246
|
+
|
|
247
|
+
const config = resolveTransitionConfig(store, slideData);
|
|
248
|
+
const { enabled, type, durationOut, stagger } = config;
|
|
249
|
+
|
|
250
|
+
if (enabled === false) { resolve(); return; }
|
|
251
|
+
|
|
252
|
+
const el = containerRef.value;
|
|
253
|
+
const standardReveals = [
|
|
254
|
+
'.reveal-up', '.reveal-zoom', '.reveal-card', '.reveal-left', '.reveal-right',
|
|
255
|
+
'.reveal-content > *',
|
|
256
|
+
'.flex-layout > div > *',
|
|
257
|
+
].join(', ');
|
|
258
|
+
const elements = el.querySelectorAll(standardReveals);
|
|
259
|
+
const chars = el.querySelectorAll('.reveal-char');
|
|
260
|
+
const childReveals = el.querySelectorAll('.reveal-child');
|
|
261
|
+
const img = el.querySelector('.reveal-img');
|
|
262
|
+
|
|
263
|
+
const tl = gsap.timeline({
|
|
264
|
+
onComplete: resolve,
|
|
265
|
+
defaults: {
|
|
266
|
+
duration: durationOut,
|
|
267
|
+
ease: config.easeOut
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Exit Sequence (Reverse of Entry)
|
|
272
|
+
// -------------------------------
|
|
273
|
+
|
|
274
|
+
// 1. Elements
|
|
275
|
+
if (elements.length) {
|
|
276
|
+
const moveY = (type === 'fade' || type === 'blur') ? 0 : (type === 'cascade' ? -30 : 20);
|
|
277
|
+
tl.to(elements, {
|
|
278
|
+
opacity: 0,
|
|
279
|
+
y: moveY,
|
|
280
|
+
scale: type === 'zoom' ? 0.9 : 1,
|
|
281
|
+
stagger: stagger * config.exitStaggerFactor
|
|
282
|
+
}, 0);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 2. Characters
|
|
286
|
+
if (chars.length) {
|
|
287
|
+
const charY = type === 'fade' ? 0 : '-50%';
|
|
288
|
+
tl.to(chars, {
|
|
289
|
+
opacity: 0,
|
|
290
|
+
y: charY,
|
|
291
|
+
stagger: config.exitCharStagger,
|
|
292
|
+
ease: config.easeOut
|
|
293
|
+
}, 0.1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 3. Image
|
|
297
|
+
if (img) {
|
|
298
|
+
tl.to(img, {
|
|
299
|
+
opacity: 0,
|
|
300
|
+
scale: 1.05,
|
|
301
|
+
filter: 'blur(10px)',
|
|
302
|
+
duration: durationOut * config.exitImageDurationFactor
|
|
303
|
+
}, 0.1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (childReveals.length) {
|
|
307
|
+
tl.to(childReveals, { opacity: 0, y: 10, duration: durationOut * config.exitChildDurationFactor }, 0);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Fallback if no elements were found, ensure resolve is called
|
|
311
|
+
if (!elements.length && !chars.length && !img) {
|
|
312
|
+
resolve();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Live Update Watcher (for Studio)
|
|
318
|
+
watch(() => ([
|
|
319
|
+
slideData,
|
|
320
|
+
store?.state.deck?.meta?.effects,
|
|
321
|
+
store?.state.options.animation
|
|
322
|
+
]), () => {
|
|
323
|
+
nextTick(() => animateIn(true)); // Pass true for immediate update
|
|
324
|
+
}, { deep: true });
|
|
325
|
+
|
|
326
|
+
onMounted(() => {
|
|
327
|
+
// Initial trigger
|
|
328
|
+
animateIn();
|
|
329
|
+
|
|
330
|
+
// Watch for async content appearing (e.g. Charts after loading)
|
|
331
|
+
const observer = new MutationObserver(() => {
|
|
332
|
+
const el = containerRef.value;
|
|
333
|
+
if (!el) return;
|
|
334
|
+
|
|
335
|
+
const config = resolveTransitionConfig(store, slideData);
|
|
336
|
+
|
|
337
|
+
// If animations are DISABLED or entranceReveal is off, only set final state for late-appearing items (e.g. charts)
|
|
338
|
+
if (config.enabled === false || !config.entranceReveal) {
|
|
339
|
+
const allPossible = [
|
|
340
|
+
'.reveal-up', '.reveal-zoom', '.reveal-card', '.reveal-left', '.reveal-right',
|
|
341
|
+
'.reveal-content > *', '.flex-layout > div > *',
|
|
342
|
+
'.reveal-img', '.reveal-char', '[data-split-text]', '.reveal-child'
|
|
343
|
+
].join(', ');
|
|
344
|
+
const es = store?.state?.elementState ?? {};
|
|
345
|
+
const raw = Array.from(el.querySelectorAll(allPossible));
|
|
346
|
+
const items = raw.filter(e => !hasElementControlledHiddenDescendant(e, es));
|
|
347
|
+
const excluded = raw.filter(e => hasElementControlledHiddenDescendant(e, es));
|
|
348
|
+
if (excluded.length) gsap.set(excluded, { opacity: 0 });
|
|
349
|
+
if (items.length) {
|
|
350
|
+
gsap.set(items, { opacity: 1, y: 0, x: 0, scale: 1, filter: 'blur(0px)', visibility: 'visible' });
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If entranceReveal is ON, check for revealable elements that haven't been animated yet
|
|
356
|
+
if (config.entranceReveal) {
|
|
357
|
+
const lateElements = el.querySelectorAll('.reveal-up, .reveal-zoom, .reveal-card, .reveal-left, .reveal-right, .reveal-content > *, .flex-layout > div > *');
|
|
358
|
+
const hasInvisible = Array.from(lateElements).some(e => (e as HTMLElement).style.opacity === '' && getComputedStyle(e).opacity === '0');
|
|
359
|
+
if (hasInvisible) animateIn();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (containerRef.value) {
|
|
364
|
+
observer.observe(containerRef.value, { childList: true, subtree: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
onUnmounted(() => observer.disconnect());
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
onUnmounted(() => ctx?.revert());
|
|
371
|
+
|
|
372
|
+
return { animateIn, animateOut };
|
|
373
|
+
}
|