kmcom-nuxt-layers 1.3.1 → 1.5.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/layers/content/app/components/Blog/List.vue +5 -1
- package/layers/content/app/components/Gallery/AmbientImage.vue +5 -12
- package/layers/content/app/components/Gallery/Detail.vue +8 -6
- package/layers/content/app/components/Gallery/Grid.vue +11 -3
- package/layers/content/app/components/Portfolio/ColorPalette.vue +1 -4
- package/layers/content/app/components/Portfolio/Detail.vue +6 -1
- package/layers/content/app/components/Portfolio/List.vue +5 -1
- package/layers/content/app/components/content/Figure.vue +1 -7
- package/layers/content/nuxt.config.ts +16 -5
- package/layers/content/package.json +5 -5
- package/layers/core/app/assets/css/main.css +5 -0
- package/layers/core/app/composables/useCache.ts +8 -4
- package/layers/core/app/composables/useErrorLog.ts +9 -5
- package/layers/core/app/composables/useScrollGuard.ts +4 -2
- package/layers/core/app/plugins/feature-detection.client.ts +1 -1
- package/layers/core/app/plugins/init.ts +2 -1
- package/layers/core/app/plugins/scroll-guard.client.ts +4 -1
- package/layers/core/app.config.ts +0 -9
- package/layers/forms/app/components/Form/Contact.vue +16 -7
- package/layers/forms/nuxt.config.ts +18 -0
- package/layers/forms/package.json +2 -0
- package/layers/layout/app/components/Layout/Container.vue +1 -4
- package/layers/layout/app/components/Layout/Grid/Debug.vue +0 -1
- package/layers/layout/app/components/Layout/Grid/Item.vue +12 -6
- package/layers/layout/app/components/Layout/Main.vue +1 -4
- package/layers/layout/app/components/Layout/Page/Container.vue +3 -1
- package/layers/layout/app/components/Layout/Page/Header.vue +16 -7
- package/layers/layout/app/components/Layout/Section/Grid.vue +1 -4
- package/layers/layout/app/components/Layout/Section/Sidebar.vue +6 -1
- package/layers/layout/app/components/Layout/Section/Stack.vue +1 -1
- package/layers/layout/app/composables/useGridConfig.ts +6 -1
- package/layers/motion/app/components/Motion/HorizontalScroll.vue +61 -0
- package/layers/motion/app/components/Motion/PinnedSection.vue +77 -0
- package/layers/motion/app/components/Motion/ScrollProgress.vue +8 -56
- package/layers/motion/app/components/Motion/ScrollScene.vue +121 -0
- package/layers/motion/app/components/Motion/ScrollStep.vue +45 -0
- package/layers/motion/app/components/Motion/TextReveal.vue +28 -63
- package/layers/motion/app/composables/useScrollSteps.ts +41 -0
- package/layers/motion/app/composables/useSectionProgress.ts +58 -0
- package/layers/motion/app/composables/useSmoothScroll.ts +3 -2
- package/layers/motion/app/plugins/locomotive-scroll.client.ts +6 -6
- package/layers/motion/nuxt.config.ts +6 -0
- package/layers/motion/package.json +2 -1
- package/layers/routing/app/app.config.ts +20 -0
- package/layers/routing/app/composables/useFeatures.ts +12 -0
- package/layers/routing/app/composables/useMaintenance.ts +7 -0
- package/layers/routing/app/composables/useRoutingConfig.ts +20 -0
- package/layers/routing/app/middleware/01.maintenance.global.ts +6 -0
- package/layers/routing/app/middleware/02.governance.global.ts +25 -0
- package/layers/routing/app/plugins/feature-flags.client.ts +15 -0
- package/layers/routing/app/plugins/scroll-routing.client.ts +21 -0
- package/layers/routing/app/types/route-meta.d.ts +6 -0
- package/layers/routing/app/types/routing.ts +48 -0
- package/layers/routing/nuxt.config.ts +27 -0
- package/layers/routing/package.json +6 -0
- package/layers/shader/app/components/Preset/ThemeAurora.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeBubble.client.vue +87 -0
- package/layers/shader/app/components/Preset/ThemeFlow.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeGradient.client.vue +87 -0
- package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemePlasma.client.vue +86 -0
- package/layers/shader/app/components/Preset/ThemeWave.client.vue +86 -0
- package/layers/shader/app/components/Shader/Background.client.vue +15 -0
- package/layers/shader/app/composables/useAmbientMaterials.ts +306 -0
- package/layers/shader/app/composables/useThemeColors.ts +52 -0
- package/layers/shader/app/utils/tsl/oklch.ts +12 -6
- package/layers/theme/app/assets/css/theme.css +19 -14
- package/layers/theme/app/components/ThemePicker/AccentButton.vue +2 -2
- package/layers/theme/app/components/ThemePicker/Colors.vue +2 -4
- package/layers/theme/app/components/ThemePicker/Menu.vue +4 -13
- package/layers/theme/app/components/ThemePicker/MenuButton.vue +1 -7
- package/layers/theme/app/composables/useAccentColor.ts +38 -0
- package/layers/theme/app/composables/useTheme.ts +14 -0
- package/layers/theme/app/composables/useThemeContrast.ts +34 -0
- package/layers/theme/app/composables/useThemeMotion.ts +34 -0
- package/layers/theme/app/composables/useThemePreferences.ts +3 -156
- package/layers/theme/app/composables/useThemeTransparency.ts +41 -0
- package/layers/theme/app/plugins/theme.client.ts +3 -3
- package/layers/theme/app/types/theme.ts +4 -0
- package/layers/theme/nuxt.config.ts +7 -0
- package/layers/ui/app/app.config.ts +44 -0
- package/layers/ui/app/assets/css/main.css +14 -0
- package/layers/ui/app/components/Accent/Blob.vue +29 -0
- package/layers/ui/app/components/Accent/Scene.vue +38 -0
- package/layers/ui/app/components/Gradient/Background.vue +22 -0
- package/layers/ui/app/components/Gradient/Text.vue +22 -0
- package/layers/ui/app/components/Progress/Bar.vue +25 -0
- package/layers/ui/app/components/Progress/Circular.vue +69 -0
- package/layers/ui/app/components/Tint/Overlay.vue +25 -0
- package/layers/ui/app/components/Typography/CodeBlock.vue +2 -1
- package/layers/ui/app/components/Typography/Headline.vue +2 -1
- package/layers/ui/app/components/Typography/QuoteBlock.vue +2 -1
- package/layers/ui/app/components/Typography/TextStroke.vue +18 -16
- package/layers/ui/app/composables/accent.ts +51 -0
- package/layers/ui/app/composables/gradient.ts +79 -0
- package/layers/ui/app/composables/tint.ts +20 -0
- package/layers/ui/app/types/accent.ts +17 -0
- package/layers/ui/app/types/gradient.ts +27 -0
- package/layers/ui/app/types/tint.ts +25 -0
- package/package.json +37 -31
- package/layers/motion/app/utils/gsapAnimations.ts +0 -122
- package/layers/ui/app.config.ts +0 -12
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { scrub = 1 } = defineProps<{
|
|
3
|
+
/**
|
|
4
|
+
* Scrub smoothness (1 = default, higher = smoother/slower)
|
|
5
|
+
*/
|
|
6
|
+
scrub?: number
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits<{
|
|
10
|
+
'panel-change': [index: number]
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const { gsap } = useGsap()
|
|
14
|
+
|
|
15
|
+
const sectionRef = ref<HTMLElement | null>(null)
|
|
16
|
+
const trackRef = ref<HTMLElement | null>(null)
|
|
17
|
+
|
|
18
|
+
let cleanup: (() => void) | null = null
|
|
19
|
+
|
|
20
|
+
onMounted(() => {
|
|
21
|
+
if (!sectionRef.value || !trackRef.value) return
|
|
22
|
+
|
|
23
|
+
const track = trackRef.value
|
|
24
|
+
const trackWidth = track.scrollWidth - window.innerWidth
|
|
25
|
+
|
|
26
|
+
const tween = gsap.to(track, {
|
|
27
|
+
scrollTrigger: {
|
|
28
|
+
trigger: sectionRef.value,
|
|
29
|
+
start: 'top top',
|
|
30
|
+
end: `+=${trackWidth}`,
|
|
31
|
+
pin: true,
|
|
32
|
+
scrub,
|
|
33
|
+
anticipatePin: 1,
|
|
34
|
+
onUpdate: (self) => {
|
|
35
|
+
const panels = track.querySelectorAll(':scope > *')
|
|
36
|
+
if (panels.length > 0) {
|
|
37
|
+
const index = Math.round(self.progress * (panels.length - 1))
|
|
38
|
+
emit('panel-change', index)
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
x: -trackWidth,
|
|
43
|
+
ease: 'none',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
cleanup = () => tween.kill()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
onUnmounted(() => {
|
|
50
|
+
cleanup?.()
|
|
51
|
+
cleanup = null
|
|
52
|
+
})
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<section ref="sectionRef" class="motion-horizontal-scroll h-screen overflow-hidden">
|
|
57
|
+
<div ref="trackRef" class="h-full flex items-center will-change-transform">
|
|
58
|
+
<slot />
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
</template>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { duration = 200, scrub = 0.5 } = defineProps<{
|
|
3
|
+
/**
|
|
4
|
+
* Pin duration as a percentage of viewport height (200 = 2× viewport height)
|
|
5
|
+
*/
|
|
6
|
+
duration?: number
|
|
7
|
+
/**
|
|
8
|
+
* Scrub smoothness for card reveals
|
|
9
|
+
*/
|
|
10
|
+
scrub?: number
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const { gsap, ScrollTrigger } = useGsap()
|
|
14
|
+
|
|
15
|
+
const sectionRef = ref<HTMLElement | null>(null)
|
|
16
|
+
const contentRef = ref<HTMLElement | null>(null)
|
|
17
|
+
|
|
18
|
+
const cleanups: Array<() => void> = []
|
|
19
|
+
|
|
20
|
+
onMounted(() => {
|
|
21
|
+
if (!sectionRef.value || !contentRef.value) return
|
|
22
|
+
|
|
23
|
+
const endOffset = (duration * window.innerHeight) / 100
|
|
24
|
+
const endValue = `+=${endOffset}`
|
|
25
|
+
|
|
26
|
+
// Pin the section
|
|
27
|
+
const pinSt = ScrollTrigger.create({
|
|
28
|
+
trigger: sectionRef.value,
|
|
29
|
+
start: 'top top',
|
|
30
|
+
end: endValue,
|
|
31
|
+
pin: true,
|
|
32
|
+
pinSpacing: true,
|
|
33
|
+
})
|
|
34
|
+
cleanups.push(() => pinSt.kill())
|
|
35
|
+
|
|
36
|
+
// Animate each direct child
|
|
37
|
+
const items = Array.from(contentRef.value.querySelectorAll(':scope > *'))
|
|
38
|
+
const total = items.length
|
|
39
|
+
if (total === 0) return
|
|
40
|
+
|
|
41
|
+
items.forEach((item, i) => {
|
|
42
|
+
gsap.set(item, { opacity: 0, y: 60, scale: 0.9 })
|
|
43
|
+
|
|
44
|
+
const startProgress = i / total
|
|
45
|
+
const endProgress = (i + 0.5) / total
|
|
46
|
+
|
|
47
|
+
const tween = gsap.to(item, {
|
|
48
|
+
scrollTrigger: {
|
|
49
|
+
trigger: sectionRef.value,
|
|
50
|
+
start: 'top top',
|
|
51
|
+
end: endValue,
|
|
52
|
+
scrub,
|
|
53
|
+
},
|
|
54
|
+
keyframes: [
|
|
55
|
+
{ opacity: 0, y: 60, scale: 0.9, duration: startProgress },
|
|
56
|
+
{ opacity: 1, y: 0, scale: 1, duration: endProgress - startProgress },
|
|
57
|
+
{ opacity: 1, y: 0, scale: 1, duration: 1 - endProgress },
|
|
58
|
+
],
|
|
59
|
+
ease: 'none',
|
|
60
|
+
})
|
|
61
|
+
cleanups.push(() => tween.kill())
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
onUnmounted(() => {
|
|
66
|
+
cleanups.forEach((fn) => fn())
|
|
67
|
+
cleanups.length = 0
|
|
68
|
+
})
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<section ref="sectionRef" class="motion-pinned-section h-screen relative">
|
|
73
|
+
<div ref="contentRef" class="h-full flex items-center justify-center">
|
|
74
|
+
<slot />
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
</template>
|
|
@@ -44,16 +44,6 @@ const props = withDefaults(
|
|
|
44
44
|
const { progress } = useSmoothScroll()
|
|
45
45
|
|
|
46
46
|
const percentage = computed(() => Math.round(progress.value * 100))
|
|
47
|
-
|
|
48
|
-
// For circular progress
|
|
49
|
-
const radius = computed(() => (props.size - props.strokeWidth) / 2)
|
|
50
|
-
const circumference = computed(() => 2 * Math.PI * radius.value)
|
|
51
|
-
const strokeDasharray = computed(
|
|
52
|
-
() => `${progress.value * circumference.value} ${circumference.value}`
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
// Generate gradient ID
|
|
56
|
-
const gradientId = `scroll-progress-gradient-${Math.random().toString(36).slice(2, 9)}`
|
|
57
47
|
</script>
|
|
58
48
|
|
|
59
49
|
<template>
|
|
@@ -77,51 +67,13 @@ const gradientId = `scroll-progress-gradient-${Math.random().toString(36).slice(
|
|
|
77
67
|
</div>
|
|
78
68
|
|
|
79
69
|
<!-- Circular Progress -->
|
|
80
|
-
<
|
|
70
|
+
<ProgressCircular
|
|
81
71
|
v-else
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
:r="radius"
|
|
90
|
-
fill="none"
|
|
91
|
-
:stroke="bgColor"
|
|
92
|
-
:stroke-width
|
|
93
|
-
/>
|
|
94
|
-
<!-- Progress circle -->
|
|
95
|
-
<circle
|
|
96
|
-
:cx="size / 2"
|
|
97
|
-
:cy="size / 2"
|
|
98
|
-
:r="radius"
|
|
99
|
-
fill="none"
|
|
100
|
-
:stroke="`url(#${gradientId})`"
|
|
101
|
-
:stroke-width
|
|
102
|
-
stroke-linecap="round"
|
|
103
|
-
:stroke-dasharray
|
|
104
|
-
class="transition-[stroke-dasharray] duration-100"
|
|
105
|
-
/>
|
|
106
|
-
<defs>
|
|
107
|
-
<linearGradient :id="gradientId" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
108
|
-
<stop offset="0%" :style="`stop-color: ${colors[0]}`" />
|
|
109
|
-
<stop offset="50%" :style="`stop-color: ${colors[1]}`" />
|
|
110
|
-
<stop offset="100%" :style="`stop-color: ${colors[2]}`" />
|
|
111
|
-
</linearGradient>
|
|
112
|
-
</defs>
|
|
113
|
-
</svg>
|
|
114
|
-
<span
|
|
115
|
-
v-if="showPercentage"
|
|
116
|
-
class="absolute inset-0 flex items-center justify-center text-2xl font-bold tabular-nums"
|
|
117
|
-
>
|
|
118
|
-
{{ percentage }}%
|
|
119
|
-
</span>
|
|
120
|
-
</div>
|
|
72
|
+
:progress="progress"
|
|
73
|
+
:size
|
|
74
|
+
:stroke-width="strokeWidth"
|
|
75
|
+
:show-percentage="showPercentage"
|
|
76
|
+
:colors
|
|
77
|
+
:bg-color="bgColor"
|
|
78
|
+
/>
|
|
121
79
|
</template>
|
|
122
|
-
|
|
123
|
-
<style scoped>
|
|
124
|
-
.tabular-nums {
|
|
125
|
-
font-variant-numeric: tabular-nums;
|
|
126
|
-
}
|
|
127
|
-
</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ScrollSceneContext } from '../../composables/useScrollSteps'
|
|
3
|
+
import { SCROLL_SCENE_KEY } from '../../composables/useScrollSteps'
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
name,
|
|
7
|
+
start = 'top top',
|
|
8
|
+
end = '+=200%',
|
|
9
|
+
pin = true,
|
|
10
|
+
scrub = 1,
|
|
11
|
+
markers = false,
|
|
12
|
+
} = defineProps<{
|
|
13
|
+
/**
|
|
14
|
+
* Unique name for this scene (for debugging)
|
|
15
|
+
*/
|
|
16
|
+
name: string
|
|
17
|
+
/**
|
|
18
|
+
* ScrollTrigger start position
|
|
19
|
+
*/
|
|
20
|
+
start?: string
|
|
21
|
+
/**
|
|
22
|
+
* ScrollTrigger end position (e.g. '+=200%' = 2 viewport heights past start)
|
|
23
|
+
*/
|
|
24
|
+
end?: string
|
|
25
|
+
/**
|
|
26
|
+
* Pin the section while scroll advances
|
|
27
|
+
*/
|
|
28
|
+
pin?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Scrub smoothness: true | number | false
|
|
31
|
+
*/
|
|
32
|
+
scrub?: boolean | number
|
|
33
|
+
/**
|
|
34
|
+
* Show GSAP ScrollTrigger markers (debug)
|
|
35
|
+
*/
|
|
36
|
+
markers?: boolean
|
|
37
|
+
}>()
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits<{
|
|
40
|
+
enter: []
|
|
41
|
+
leave: []
|
|
42
|
+
progress: [value: number]
|
|
43
|
+
}>()
|
|
44
|
+
|
|
45
|
+
const { ScrollTrigger } = useGsap()
|
|
46
|
+
|
|
47
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
48
|
+
const progress = ref(0)
|
|
49
|
+
const active = ref(false)
|
|
50
|
+
|
|
51
|
+
// Step registration
|
|
52
|
+
const registeredSteps = ref(new Set<number>())
|
|
53
|
+
const stepCount = computed(() =>
|
|
54
|
+
registeredSteps.value.size > 0 ? Math.max(...registeredSteps.value) + 1 : 0
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
function registerStep(index: number) {
|
|
58
|
+
registeredSteps.value = new Set([...registeredSteps.value, index])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function unregisterStep(index: number) {
|
|
62
|
+
const next = new Set(registeredSteps.value)
|
|
63
|
+
next.delete(index)
|
|
64
|
+
registeredSteps.value = next
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
provide<ScrollSceneContext>(SCROLL_SCENE_KEY, {
|
|
68
|
+
progress,
|
|
69
|
+
active,
|
|
70
|
+
name,
|
|
71
|
+
stepCount,
|
|
72
|
+
registerStep,
|
|
73
|
+
unregisterStep,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
let st: { kill: () => void } | null = null
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
if (!containerRef.value) return
|
|
80
|
+
|
|
81
|
+
st = ScrollTrigger.create({
|
|
82
|
+
trigger: containerRef.value,
|
|
83
|
+
start,
|
|
84
|
+
end,
|
|
85
|
+
pin,
|
|
86
|
+
scrub,
|
|
87
|
+
markers,
|
|
88
|
+
onUpdate: (self) => {
|
|
89
|
+
progress.value = self.progress
|
|
90
|
+
emit('progress', self.progress)
|
|
91
|
+
},
|
|
92
|
+
onEnter: () => {
|
|
93
|
+
active.value = true
|
|
94
|
+
emit('enter')
|
|
95
|
+
},
|
|
96
|
+
onLeave: () => {
|
|
97
|
+
active.value = false
|
|
98
|
+
emit('leave')
|
|
99
|
+
},
|
|
100
|
+
onEnterBack: () => {
|
|
101
|
+
active.value = true
|
|
102
|
+
emit('enter')
|
|
103
|
+
},
|
|
104
|
+
onLeaveBack: () => {
|
|
105
|
+
active.value = false
|
|
106
|
+
emit('leave')
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
onUnmounted(() => {
|
|
112
|
+
st?.kill()
|
|
113
|
+
st = null
|
|
114
|
+
})
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<div ref="containerRef" class="motion-scroll-scene">
|
|
119
|
+
<slot :progress="progress" :active="active" />
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ScrollSceneContext } from '../../composables/useScrollSteps'
|
|
3
|
+
import { SCROLL_SCENE_KEY } from '../../composables/useScrollSteps'
|
|
4
|
+
|
|
5
|
+
const { index } = defineProps<{
|
|
6
|
+
/**
|
|
7
|
+
* Zero-based step index within the parent MotionScrollScene
|
|
8
|
+
*/
|
|
9
|
+
index: number
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
enter: []
|
|
14
|
+
leave: []
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const scene = inject<ScrollSceneContext>(SCROLL_SCENE_KEY)
|
|
18
|
+
|
|
19
|
+
onMounted(() => scene?.registerStep(index))
|
|
20
|
+
onUnmounted(() => scene?.unregisterStep(index))
|
|
21
|
+
|
|
22
|
+
const isActive = computed(() => {
|
|
23
|
+
if (!scene) return index === 0
|
|
24
|
+
const total = scene.stepCount.value
|
|
25
|
+
if (total === 0) return false
|
|
26
|
+
const stepSize = 1 / total
|
|
27
|
+
const start = index * stepSize
|
|
28
|
+
const end = (index + 1) * stepSize
|
|
29
|
+
// Last step stays active at progress === 1
|
|
30
|
+
return scene.progress.value >= start && (index === total - 1 || scene.progress.value < end)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const wasActive = ref(false)
|
|
34
|
+
watch(isActive, (active) => {
|
|
35
|
+
if (active && !wasActive.value) emit('enter')
|
|
36
|
+
else if (!active && wasActive.value) emit('leave')
|
|
37
|
+
wasActive.value = active
|
|
38
|
+
})
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div class="motion-scroll-step" :class="{ 'is-active': isActive }" :data-step="index">
|
|
43
|
+
<slot :is-active="isActive" />
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
@@ -17,73 +17,44 @@ const props = withDefaults(
|
|
|
17
17
|
* Animation duration
|
|
18
18
|
*/
|
|
19
19
|
duration?: number
|
|
20
|
-
/**
|
|
21
|
-
* Trigger on scroll
|
|
22
|
-
*/
|
|
23
|
-
scrollTrigger?: boolean
|
|
24
20
|
/**
|
|
25
21
|
* Start position for scroll trigger
|
|
26
22
|
*/
|
|
27
23
|
start?: string
|
|
28
24
|
}>(),
|
|
29
|
-
{
|
|
30
|
-
type: 'chars',
|
|
31
|
-
stagger: 0.03,
|
|
32
|
-
duration: 0.8,
|
|
33
|
-
scrollTrigger: true,
|
|
34
|
-
start: 'top 80%',
|
|
35
|
-
}
|
|
25
|
+
{ type: 'chars', stagger: 0.03, duration: 0.8, start: 'top 80%' }
|
|
36
26
|
)
|
|
37
27
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return [props.text]
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
onMounted(() => {
|
|
51
|
-
if (!containerRef.value) return
|
|
52
|
-
|
|
53
|
-
const elements = containerRef.value.querySelectorAll('.reveal-item')
|
|
54
|
-
|
|
55
|
-
const animationConfig: gsap.TweenVars = {
|
|
56
|
-
y: 40,
|
|
57
|
-
opacity: 0,
|
|
58
|
-
rotateX: -90,
|
|
59
|
-
duration: props.duration,
|
|
60
|
-
stagger: props.stagger,
|
|
61
|
-
ease: 'power3.out',
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (props.scrollTrigger) {
|
|
65
|
-
animationConfig.scrollTrigger = {
|
|
66
|
-
trigger: containerRef.value,
|
|
67
|
-
start: props.start,
|
|
68
|
-
toggleActions: 'play none none none',
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
gsap.from(elements, animationConfig)
|
|
73
|
-
})
|
|
28
|
+
const animBinding = computed(() => ({
|
|
29
|
+
y: 40,
|
|
30
|
+
opacity: 0,
|
|
31
|
+
rotateX: -90,
|
|
32
|
+
stagger: props.stagger,
|
|
33
|
+
duration: props.duration,
|
|
34
|
+
ease: 'power3.out',
|
|
35
|
+
start: props.start,
|
|
36
|
+
}))
|
|
74
37
|
</script>
|
|
75
38
|
|
|
76
39
|
<template>
|
|
77
|
-
<span
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
40
|
+
<span
|
|
41
|
+
v-if="type === 'chars'"
|
|
42
|
+
v-gsap.whenVisible.once.splitText.chars.from="animBinding"
|
|
43
|
+
class="motion-text-reveal"
|
|
44
|
+
>{{ text }}</span
|
|
45
|
+
>
|
|
46
|
+
<span
|
|
47
|
+
v-else-if="type === 'words'"
|
|
48
|
+
v-gsap.whenVisible.once.splitText.words.from="animBinding"
|
|
49
|
+
class="motion-text-reveal"
|
|
50
|
+
>{{ text }}</span
|
|
51
|
+
>
|
|
52
|
+
<span
|
|
53
|
+
v-else
|
|
54
|
+
v-gsap.whenVisible.once.splitText.lines.from="animBinding"
|
|
55
|
+
class="motion-text-reveal"
|
|
56
|
+
>{{ text }}</span
|
|
57
|
+
>
|
|
87
58
|
</template>
|
|
88
59
|
|
|
89
60
|
<style scoped>
|
|
@@ -92,10 +63,4 @@ onMounted(() => {
|
|
|
92
63
|
flex-wrap: wrap;
|
|
93
64
|
perspective: 1000px;
|
|
94
65
|
}
|
|
95
|
-
|
|
96
|
-
.reveal-item {
|
|
97
|
-
display: inline-block;
|
|
98
|
-
transform-origin: center bottom;
|
|
99
|
-
will-change: transform, opacity;
|
|
100
|
-
}
|
|
101
66
|
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ComputedRef, Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const SCROLL_SCENE_KEY = Symbol('motionScrollScene')
|
|
4
|
+
|
|
5
|
+
export interface ScrollSceneContext {
|
|
6
|
+
progress: Ref<number>
|
|
7
|
+
active: Ref<boolean>
|
|
8
|
+
name: string
|
|
9
|
+
stepCount: ComputedRef<number>
|
|
10
|
+
registerStep: (index: number) => void
|
|
11
|
+
unregisterStep: (index: number) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Step/waypoint state tracking — must be used inside a MotionScrollScene.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const { currentStep, isActive, stepProgress } = useScrollSteps(3)
|
|
19
|
+
* // currentStep: 0 | 1 | 2 based on scene progress
|
|
20
|
+
*/
|
|
21
|
+
export function useScrollSteps(total: number) {
|
|
22
|
+
const scene = inject<ScrollSceneContext>(SCROLL_SCENE_KEY)
|
|
23
|
+
|
|
24
|
+
const sceneProgress = computed(() => scene?.progress.value ?? 0)
|
|
25
|
+
|
|
26
|
+
const currentStep = computed(() =>
|
|
27
|
+
Math.min(Math.floor(sceneProgress.value * total), total - 1)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function isActive(index: number): boolean {
|
|
31
|
+
return currentStep.value === index
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stepProgress(index: number): number {
|
|
35
|
+
const stepSize = 1 / total
|
|
36
|
+
const start = index * stepSize
|
|
37
|
+
return Math.max(0, Math.min(1, (sceneProgress.value - start) / stepSize))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { currentStep, isActive, stepProgress }
|
|
41
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { MaybeRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
interface SectionProgressOptions {
|
|
4
|
+
start?: string
|
|
5
|
+
end?: string
|
|
6
|
+
markers?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Section-local scroll progress (0–1) scoped to a given element.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const sectionRef = ref<HTMLElement | null>(null)
|
|
14
|
+
* const { progress, active, entering, leaving } = useSectionProgress(sectionRef, {
|
|
15
|
+
* start: 'top top',
|
|
16
|
+
* end: '+=200%',
|
|
17
|
+
* })
|
|
18
|
+
*/
|
|
19
|
+
export function useSectionProgress(
|
|
20
|
+
trigger: MaybeRef<HTMLElement | null>,
|
|
21
|
+
options: SectionProgressOptions = {}
|
|
22
|
+
) {
|
|
23
|
+
const { ScrollTrigger } = useGsap()
|
|
24
|
+
const { direction } = useSmoothScroll()
|
|
25
|
+
|
|
26
|
+
const progress = ref(0)
|
|
27
|
+
const active = ref(false)
|
|
28
|
+
const entering = computed(() => active.value && direction.value > 0)
|
|
29
|
+
const leaving = computed(() => active.value && direction.value < 0)
|
|
30
|
+
|
|
31
|
+
let st: { kill: () => void } | null = null
|
|
32
|
+
|
|
33
|
+
onMounted(() => {
|
|
34
|
+
const el = unref(trigger)
|
|
35
|
+
if (!el) return
|
|
36
|
+
|
|
37
|
+
st = ScrollTrigger.create({
|
|
38
|
+
trigger: el,
|
|
39
|
+
start: options.start ?? 'top top',
|
|
40
|
+
end: options.end ?? '+=100%',
|
|
41
|
+
markers: options.markers ?? false,
|
|
42
|
+
onUpdate: (self) => {
|
|
43
|
+
progress.value = self.progress
|
|
44
|
+
},
|
|
45
|
+
onEnter: () => { active.value = true },
|
|
46
|
+
onLeave: () => { active.value = false },
|
|
47
|
+
onEnterBack: () => { active.value = true },
|
|
48
|
+
onLeaveBack: () => { active.value = false },
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
onUnmounted(() => {
|
|
53
|
+
st?.kill()
|
|
54
|
+
st = null
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return { progress, active, entering, leaving }
|
|
58
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
1
2
|
import type LocomotiveScroll from 'locomotive-scroll'
|
|
2
3
|
|
|
3
4
|
interface ScrollToOptions {
|
|
@@ -23,9 +24,9 @@ interface ScrollToOptions {
|
|
|
23
24
|
export function useSmoothScroll() {
|
|
24
25
|
const nuxtApp = useNuxtApp()
|
|
25
26
|
|
|
26
|
-
// Get the Locomotive Scroll instance
|
|
27
|
+
// Get the Locomotive Scroll instance from the reactive ref provided by the plugin
|
|
27
28
|
const locomotiveScroll = computed<LocomotiveScroll | undefined>(
|
|
28
|
-
() => nuxtApp.$locomotiveScroll as LocomotiveScroll | undefined
|
|
29
|
+
() => (nuxtApp.$locomotiveScroll as Ref<LocomotiveScroll | null>)?.value ?? undefined
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
// Reactive scroll state from the plugin
|
|
@@ -18,11 +18,11 @@ export default defineNuxtPlugin(() => {
|
|
|
18
18
|
progress: 0,
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const instance = shallowRef<LocomotiveScroll | null>(null)
|
|
22
22
|
|
|
23
23
|
function init() {
|
|
24
|
-
if (instance) return
|
|
25
|
-
instance = new LocomotiveScroll({
|
|
24
|
+
if (instance.value) return
|
|
25
|
+
instance.value = new LocomotiveScroll({
|
|
26
26
|
lenisOptions: {
|
|
27
27
|
lerp: 0.1,
|
|
28
28
|
smoothWheel: true,
|
|
@@ -40,8 +40,8 @@ export default defineNuxtPlugin(() => {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function destroy() {
|
|
43
|
-
instance?.destroy()
|
|
44
|
-
instance = null
|
|
43
|
+
instance.value?.destroy()
|
|
44
|
+
instance.value = null
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const router = useRouter()
|
|
@@ -62,7 +62,7 @@ export default defineNuxtPlugin(() => {
|
|
|
62
62
|
|
|
63
63
|
return {
|
|
64
64
|
provide: {
|
|
65
|
-
locomotiveScroll:
|
|
65
|
+
locomotiveScroll: instance,
|
|
66
66
|
scrollState,
|
|
67
67
|
},
|
|
68
68
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RoutingLayerConfig } from './types/routing'
|
|
2
|
+
|
|
3
|
+
export default defineAppConfig({
|
|
4
|
+
routingLayer: {
|
|
5
|
+
preset: 'simple',
|
|
6
|
+
strictDefaultDeny: false,
|
|
7
|
+
layerDefaultDeny: false,
|
|
8
|
+
runtimeFlags: false,
|
|
9
|
+
debug: false,
|
|
10
|
+
maintenance: { enabled: false, allowRoutes: ['/maintenance'] },
|
|
11
|
+
scrollRouting: { enabled: false, mode: 'replace' },
|
|
12
|
+
features: {},
|
|
13
|
+
} satisfies RoutingLayerConfig,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
declare module '@nuxt/schema' {
|
|
17
|
+
interface AppConfigInput {
|
|
18
|
+
routingLayer?: Partial<import('./types/routing').RoutingLayerConfig>
|
|
19
|
+
}
|
|
20
|
+
}
|