kmcom-nuxt-layers 1.6.39 → 1.6.43
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/core/app/composables/useErrorLog.ts +6 -11
- package/layers/core/app/composables/useLoading.ts +15 -46
- package/layers/core/app/composables/useScrollGuard.ts +115 -129
- package/layers/forms/app/components/Form/Field.vue +2 -2
- package/layers/forms/app/composables/useFormSchema.ts +1 -3
- package/layers/forms/app/config/fields.ts +12 -6
- package/layers/forms/app/types/fields.ts +2 -20
- package/layers/forms/nuxt.config.ts +0 -10
- package/layers/motion/app/components/Motion/CountUp.vue +39 -0
- package/layers/motion/app/components/Motion/Cursor.vue +101 -0
- package/layers/motion/app/components/Motion/Magnetic.vue +22 -0
- package/layers/motion/app/components/Motion/Marquee.vue +130 -130
- package/layers/motion/app/components/Motion/MarqueeText.vue +147 -0
- package/layers/motion/app/components/Motion/Tilt.vue +22 -0
- package/layers/motion/app/components/Motion/VelocityEffect.vue +33 -71
- package/layers/motion/app/composables/useCountUp.ts +61 -0
- package/layers/motion/app/composables/useCursorFollower.ts +25 -0
- package/layers/motion/app/composables/useMagneticElement.ts +56 -0
- package/layers/motion/app/composables/useMarqueeCopies.ts +50 -0
- package/layers/motion/app/composables/useMarqueeVelocity.ts +44 -0
- package/layers/motion/app/composables/useSmoothScroll.ts +11 -0
- package/layers/motion/app/composables/useTiltEffect.ts +58 -0
- package/layers/motion/app/plugins/locomotive-scroll.client.ts +1 -1
- package/layers/motion/app/types/app-config.d.ts +11 -2
- package/layers/motion/tsconfig.json +1 -0
- package/layers/routing/app/middleware/02.governance.global.ts +7 -18
- package/layers/routing/app/types/routing.ts +10 -0
- package/layers/routing/app/utils/resolveRoute.ts +31 -0
- package/layers/shader/package.json +2 -1
- package/layers/theme/app/composables/useAccentColor.ts +1 -12
- package/layers/theme/app/composables/useThemeContrast.ts +0 -11
- package/layers/theme/app/composables/useThemeMotion.ts +0 -11
- package/layers/theme/app/composables/useThemeTransparency.ts +0 -14
- package/layers/theme/app/plugins/theme.client.ts +20 -3
- package/layers/theme/app/types/app-config.d.ts +2 -2
- package/layers/ui/app/components/Base/Modal.vue +0 -1
- package/layers/ui/app/composables/color.ts +1 -32
- package/layers/ui/app/utils/createModal.ts +11 -2
- package/package.json +28 -26
- package/layers/content/app/.DS_Store +0 -0
- package/layers/content/app/pages/blog/[slug].vue +0 -19
- package/layers/content/app/pages/blog/index.vue +0 -22
- package/layers/core/app/.DS_Store +0 -0
- package/layers/core/app/assets/.DS_Store +0 -0
- package/layers/forms/app/.DS_Store +0 -0
- package/layers/layout/app/.DS_Store +0 -0
- package/layers/motion/app/.DS_Store +0 -0
- package/layers/shader/app/.DS_Store +0 -0
- package/layers/theme/app/.DS_Store +0 -0
- package/layers/ui/app/.DS_Store +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
strength = 0.3,
|
|
4
|
+
radius = 100,
|
|
5
|
+
damping = 30,
|
|
6
|
+
stiffness = 300,
|
|
7
|
+
} = defineProps<{
|
|
8
|
+
strength?: number
|
|
9
|
+
radius?: number
|
|
10
|
+
damping?: number
|
|
11
|
+
stiffness?: number
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const el = ref<HTMLElement | null>(null)
|
|
15
|
+
const { isActive } = useMagneticElement(el, { strength, radius, damping, stiffness })
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div ref="el" class="will-change-transform">
|
|
20
|
+
<slot :is-active="isActive" />
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
@@ -1,142 +1,142 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const props = withDefaults(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
const { gsap } = useGsap()
|
|
56
|
-
const { velocity: scrollVelocity, direction: scrollDirection } = useSmoothScroll()
|
|
57
|
-
|
|
58
|
-
const containerRef = ref<HTMLElement | null>(null)
|
|
59
|
-
const contentRef = ref<HTMLElement | null>(null)
|
|
60
|
-
const tweenRef = ref<gsap.core.Tween | null>(null)
|
|
61
|
-
|
|
62
|
-
const isPaused = ref(false)
|
|
63
|
-
const currentTimeScale = ref(1)
|
|
64
|
-
|
|
65
|
-
onMounted(() => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
2
|
+
const props = withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
/**
|
|
5
|
+
* Base animation speed in pixels per second
|
|
6
|
+
*/
|
|
7
|
+
speed?: number
|
|
8
|
+
/**
|
|
9
|
+
* Direction of scroll
|
|
10
|
+
*/
|
|
11
|
+
direction?: 'left' | 'right'
|
|
12
|
+
/**
|
|
13
|
+
* Pause animation on hover
|
|
14
|
+
*/
|
|
15
|
+
pauseOnHover?: boolean
|
|
16
|
+
/**
|
|
17
|
+
* Gap between repeated items
|
|
18
|
+
*/
|
|
19
|
+
gap?: string
|
|
20
|
+
/**
|
|
21
|
+
* Enable velocity-based speed (scroll velocity affects marquee speed)
|
|
22
|
+
*/
|
|
23
|
+
velocityBased?: boolean
|
|
24
|
+
/**
|
|
25
|
+
* How much scroll velocity affects speed (0-1)
|
|
26
|
+
* 0 = no effect, 1 = velocity fully controls speed
|
|
27
|
+
*/
|
|
28
|
+
velocitySensitivity?: number
|
|
29
|
+
/**
|
|
30
|
+
* Reverse direction based on scroll direction
|
|
31
|
+
*/
|
|
32
|
+
velocityDirection?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Minimum speed multiplier when using velocity
|
|
35
|
+
*/
|
|
36
|
+
minSpeed?: number
|
|
37
|
+
/**
|
|
38
|
+
* Maximum speed multiplier when using velocity
|
|
39
|
+
*/
|
|
40
|
+
maxSpeed?: number
|
|
41
|
+
}>(),
|
|
42
|
+
{
|
|
43
|
+
speed: 50,
|
|
44
|
+
direction: 'left',
|
|
45
|
+
pauseOnHover: true,
|
|
46
|
+
gap: '2rem',
|
|
47
|
+
velocityBased: false,
|
|
48
|
+
velocitySensitivity: 0.5,
|
|
49
|
+
velocityDirection: false,
|
|
50
|
+
minSpeed: 0.2,
|
|
51
|
+
maxSpeed: 5,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const { gsap } = useGsap()
|
|
56
|
+
const { velocity: scrollVelocity, direction: scrollDirection } = useSmoothScroll()
|
|
57
|
+
|
|
58
|
+
const containerRef = ref<HTMLElement | null>(null)
|
|
59
|
+
const contentRef = ref<HTMLElement | null>(null)
|
|
60
|
+
const tweenRef = ref<gsap.core.Tween | null>(null)
|
|
61
|
+
|
|
62
|
+
const isPaused = ref(false)
|
|
63
|
+
const currentTimeScale = ref(1)
|
|
64
|
+
|
|
65
|
+
onMounted(() => {
|
|
66
|
+
if (!containerRef.value || !contentRef.value) return
|
|
67
|
+
|
|
68
|
+
const content = contentRef.value
|
|
69
|
+
const contentWidth = content.offsetWidth
|
|
70
|
+
|
|
71
|
+
// Set up the infinite scroll animation
|
|
72
|
+
tweenRef.value = gsap.to(content, {
|
|
73
|
+
x: props.direction === 'left' ? -contentWidth / 2 : contentWidth / 2,
|
|
74
|
+
duration: contentWidth / props.speed,
|
|
75
|
+
ease: 'none',
|
|
76
|
+
repeat: -1,
|
|
77
|
+
modifiers: {
|
|
78
|
+
x: gsap.utils.unitize((x) => {
|
|
79
|
+
const mod = contentWidth / 2
|
|
80
|
+
return props.direction === 'left' ? parseFloat(x) % mod : Math.abs(parseFloat(x) % mod)
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
})
|
|
83
84
|
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
// Watch velocity changes for velocity-based animation
|
|
87
|
-
watch(
|
|
88
|
-
[scrollVelocity, scrollDirection],
|
|
89
|
-
([vel, dir]) => {
|
|
90
|
-
if (!props.velocityBased || !tweenRef.value || isPaused.value) return
|
|
91
|
-
|
|
92
|
-
// Calculate velocity multiplier
|
|
93
|
-
const absVelocity = Math.abs(vel)
|
|
94
|
-
const velocityEffect = absVelocity * props.velocitySensitivity * 0.02
|
|
95
85
|
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
// Watch velocity changes for velocity-based animation
|
|
87
|
+
watch(
|
|
88
|
+
[scrollVelocity, scrollDirection],
|
|
89
|
+
([vel, dir]) => {
|
|
90
|
+
if (!props.velocityBased || !tweenRef.value || isPaused.value) return
|
|
91
|
+
|
|
92
|
+
// Calculate velocity multiplier
|
|
93
|
+
const absVelocity = Math.abs(vel)
|
|
94
|
+
const velocityEffect = absVelocity * props.velocitySensitivity * 0.02
|
|
95
|
+
|
|
96
|
+
// Base multiplier from velocity magnitude
|
|
97
|
+
let multiplier = 1 + velocityEffect
|
|
98
|
+
|
|
99
|
+
// Clamp to min/max
|
|
100
|
+
multiplier = Math.max(props.minSpeed, Math.min(props.maxSpeed, multiplier))
|
|
101
|
+
|
|
102
|
+
// Optionally reverse direction based on scroll direction
|
|
103
|
+
if (props.velocityDirection && dir !== 0) {
|
|
104
|
+
const baseDirection = props.direction === 'left' ? 1 : -1
|
|
105
|
+
multiplier *= dir * baseDirection
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Smoothly interpolate to target
|
|
109
|
+
gsap.to(currentTimeScale, {
|
|
110
|
+
value: multiplier,
|
|
111
|
+
duration: 0.3,
|
|
112
|
+
ease: 'power2.out',
|
|
113
|
+
onUpdate: () => {
|
|
114
|
+
if (tweenRef.value) {
|
|
115
|
+
tweenRef.value.timeScale(currentTimeScale.value)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
{ immediate: false }
|
|
121
|
+
)
|
|
98
122
|
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
onUnmounted(() => {
|
|
124
|
+
tweenRef.value?.kill()
|
|
125
|
+
})
|
|
101
126
|
|
|
102
|
-
|
|
103
|
-
if (props.
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
function handleMouseEnter() {
|
|
128
|
+
if (props.pauseOnHover && tweenRef.value) {
|
|
129
|
+
gsap.to(tweenRef.value, { timeScale: 0, duration: 0.5 })
|
|
130
|
+
isPaused.value = true
|
|
106
131
|
}
|
|
107
|
-
|
|
108
|
-
// Smoothly interpolate to target
|
|
109
|
-
gsap.to(currentTimeScale, {
|
|
110
|
-
value: multiplier,
|
|
111
|
-
duration: 0.3,
|
|
112
|
-
ease: 'power2.out',
|
|
113
|
-
onUpdate: () => {
|
|
114
|
-
if (tweenRef.value) {
|
|
115
|
-
tweenRef.value.timeScale(currentTimeScale.value)
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
})
|
|
119
|
-
},
|
|
120
|
-
{ immediate: false }
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
onUnmounted(() => {
|
|
124
|
-
tweenRef.value?.kill()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
function handleMouseEnter() {
|
|
128
|
-
if (props.pauseOnHover && tweenRef.value) {
|
|
129
|
-
gsap.to(tweenRef.value, { timeScale: 0, duration: 0.5 })
|
|
130
|
-
isPaused.value = true
|
|
131
132
|
}
|
|
132
|
-
}
|
|
133
133
|
|
|
134
|
-
function handleMouseLeave() {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
function handleMouseLeave() {
|
|
135
|
+
if (props.pauseOnHover && tweenRef.value) {
|
|
136
|
+
gsap.to(tweenRef.value, { timeScale: currentTimeScale.value, duration: 0.5 })
|
|
137
|
+
isPaused.value = false
|
|
138
|
+
}
|
|
138
139
|
}
|
|
139
|
-
}
|
|
140
140
|
</script>
|
|
141
141
|
|
|
142
142
|
<template>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ScrollTrigger as GSAPScrollTrigger } from 'gsap/ScrollTrigger'
|
|
3
|
+
|
|
4
|
+
interface VelocityMapping {
|
|
5
|
+
input: [number, number]
|
|
6
|
+
output: [number, number]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
texts = [],
|
|
11
|
+
velocity = 100,
|
|
12
|
+
className = '',
|
|
13
|
+
damping = 50,
|
|
14
|
+
stiffness = 400,
|
|
15
|
+
velocityMapping = { input: [0, 1000] as [number, number], output: [0, 5] as [number, number] },
|
|
16
|
+
pauseOnHover = false,
|
|
17
|
+
parallaxClassName = '',
|
|
18
|
+
scrollerClassName = '',
|
|
19
|
+
parallaxStyle = {},
|
|
20
|
+
scrollerStyle = {},
|
|
21
|
+
} = defineProps<{
|
|
22
|
+
texts?: string[]
|
|
23
|
+
velocity?: number
|
|
24
|
+
className?: string
|
|
25
|
+
damping?: number
|
|
26
|
+
stiffness?: number
|
|
27
|
+
velocityMapping?: VelocityMapping
|
|
28
|
+
pauseOnHover?: boolean
|
|
29
|
+
parallaxClassName?: string
|
|
30
|
+
scrollerClassName?: string
|
|
31
|
+
parallaxStyle?: Record<string, string | number>
|
|
32
|
+
scrollerStyle?: Record<string, string | number>
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const { gsap, ScrollTrigger } = useGsap()
|
|
36
|
+
const { velocityFactor } = useMarqueeVelocity({ damping, stiffness, velocityMapping })
|
|
37
|
+
|
|
38
|
+
const containerRef = ref<HTMLElement[]>([])
|
|
39
|
+
const copyRefs = ref<HTMLSpanElement[]>([])
|
|
40
|
+
|
|
41
|
+
const { copyWidths, calculatedCopies } = useMarqueeCopies(
|
|
42
|
+
containerRef,
|
|
43
|
+
copyRefs,
|
|
44
|
+
computed(() => texts.length)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const baseX = ref<number[]>([])
|
|
48
|
+
const directionFactors = ref<number[]>([])
|
|
49
|
+
const isPaused = ref(false)
|
|
50
|
+
|
|
51
|
+
let scrollTriggerInstance: GSAPScrollTrigger | null = null
|
|
52
|
+
let animLastTime = 0
|
|
53
|
+
|
|
54
|
+
const wrap = (min: number, max: number, v: number): number => {
|
|
55
|
+
const range = max - min
|
|
56
|
+
if (range === 0) return min
|
|
57
|
+
return ((((v - min) % range) + range) % range) + min
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const scrollTransforms = computed(() =>
|
|
61
|
+
texts.map((_, index) => {
|
|
62
|
+
const singleWidth = copyWidths.value[index]
|
|
63
|
+
if (!singleWidth) return '0px'
|
|
64
|
+
return `${wrap(-singleWidth, 0, baseX.value[index] ?? 0)}px`
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const setCopyRef = (el: Element | ComponentPublicInstance | null, index: number) => {
|
|
69
|
+
if (el instanceof HTMLSpanElement) copyRefs.value[index] = el
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function animate() {
|
|
73
|
+
if (isPaused.value) return
|
|
74
|
+
|
|
75
|
+
const now = performance.now()
|
|
76
|
+
const delta = animLastTime ? now - animLastTime : 16
|
|
77
|
+
animLastTime = now
|
|
78
|
+
|
|
79
|
+
texts.forEach((_, index) => {
|
|
80
|
+
const baseVelocity = index % 2 !== 0 ? -velocity : velocity
|
|
81
|
+
let moveBy = (directionFactors.value[index] ?? 1) * baseVelocity * (delta / 1000)
|
|
82
|
+
|
|
83
|
+
if (velocityFactor.value < 0) {
|
|
84
|
+
directionFactors.value[index] = -1
|
|
85
|
+
} else if (velocityFactor.value > 0) {
|
|
86
|
+
directionFactors.value[index] = 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
moveBy += (directionFactors.value[index] ?? 1) * moveBy * velocityFactor.value
|
|
90
|
+
baseX.value[index] = (baseX.value[index] ?? 0) + moveBy
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onMounted(async () => {
|
|
95
|
+
await nextTick()
|
|
96
|
+
|
|
97
|
+
baseX.value = new Array(texts.length).fill(0)
|
|
98
|
+
directionFactors.value = new Array(texts.length).fill(1)
|
|
99
|
+
animLastTime = performance.now()
|
|
100
|
+
|
|
101
|
+
const firstContainer = containerRef.value[0]
|
|
102
|
+
if (firstContainer) {
|
|
103
|
+
scrollTriggerInstance = ScrollTrigger.create({
|
|
104
|
+
trigger: firstContainer,
|
|
105
|
+
start: 'top bottom',
|
|
106
|
+
end: 'bottom top',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
gsap.ticker.add(animate)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
onUnmounted(() => {
|
|
114
|
+
gsap.ticker.remove(animate)
|
|
115
|
+
scrollTriggerInstance?.kill()
|
|
116
|
+
})
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<template>
|
|
120
|
+
<section
|
|
121
|
+
class="w-full overflow-x-hidden"
|
|
122
|
+
@mouseenter="isPaused = pauseOnHover"
|
|
123
|
+
@mouseleave="isPaused = false"
|
|
124
|
+
>
|
|
125
|
+
<div
|
|
126
|
+
v-for="(text, index) in texts"
|
|
127
|
+
:key="index"
|
|
128
|
+
ref="containerRef"
|
|
129
|
+
:class="`${parallaxClassName} relative w-full overflow-x-hidden overflow-y-visible`"
|
|
130
|
+
:style="parallaxStyle"
|
|
131
|
+
>
|
|
132
|
+
<div
|
|
133
|
+
:class="`${scrollerClassName} font-styles-logo flex py-2 text-center font-sans text-4xl font-medium tracking-[-0.02em] whitespace-nowrap text-neutral drop-shadow md:text-[5rem] md:leading-[5rem]`"
|
|
134
|
+
:style="{ transform: `translateX(${scrollTransforms[index] ?? '0px'})`, ...scrollerStyle }"
|
|
135
|
+
>
|
|
136
|
+
<span
|
|
137
|
+
v-for="spanIndex in calculatedCopies[index] ?? 15"
|
|
138
|
+
:key="spanIndex"
|
|
139
|
+
:ref="spanIndex === 1 ? (el) => setCopyRef(el, index) : undefined"
|
|
140
|
+
:class="`shrink-0 ${className} text-primary-500`"
|
|
141
|
+
>
|
|
142
|
+
{{ text }}
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
maxTilt = 15,
|
|
4
|
+
perspective = 800,
|
|
5
|
+
damping = 40,
|
|
6
|
+
stiffness = 250,
|
|
7
|
+
} = defineProps<{
|
|
8
|
+
maxTilt?: number
|
|
9
|
+
perspective?: number
|
|
10
|
+
damping?: number
|
|
11
|
+
stiffness?: number
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const el = ref<HTMLElement | null>(null)
|
|
15
|
+
useTiltEffect(el, { maxTilt, perspective, damping, stiffness })
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div ref="el" class="will-change-transform" style="transform-style: preserve-3d">
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
@@ -9,78 +9,43 @@ type EffectType =
|
|
|
9
9
|
| 'translateY'
|
|
10
10
|
| 'hueRotate'
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* Lerp factor for smoothing (0.1 = very smooth, 0.3 = responsive)
|
|
28
|
-
*/
|
|
29
|
-
smoothFactor?: number
|
|
30
|
-
/**
|
|
31
|
-
* Tag to render as
|
|
32
|
-
*/
|
|
33
|
-
as?: string
|
|
34
|
-
}>(),
|
|
35
|
-
{
|
|
36
|
-
smooth: true,
|
|
37
|
-
smoothFactor: 0.15,
|
|
38
|
-
as: 'div',
|
|
39
|
-
}
|
|
40
|
-
)
|
|
41
|
-
|
|
12
|
+
const {
|
|
13
|
+
effect,
|
|
14
|
+
intensity = undefined,
|
|
15
|
+
damping = 15,
|
|
16
|
+
stiffness = 150,
|
|
17
|
+
as = 'div',
|
|
18
|
+
} = defineProps<{
|
|
19
|
+
effect: EffectType
|
|
20
|
+
intensity?: number
|
|
21
|
+
damping?: number
|
|
22
|
+
stiffness?: number
|
|
23
|
+
as?: string
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const { gsap } = useGsap()
|
|
42
27
|
const { velocity } = useSmoothScroll()
|
|
43
28
|
|
|
44
|
-
// Smoothed velocity for stable animations
|
|
45
29
|
const smoothVelocity = ref(0)
|
|
46
30
|
const smoothAbsVelocity = ref(0)
|
|
47
|
-
let animationFrame: number | null = null
|
|
48
31
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
function tick() {
|
|
33
|
+
const d = damping / 1000
|
|
34
|
+
const s = stiffness / 1000
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
smoothVelocity.value
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
props.smoothFactor
|
|
59
|
-
)
|
|
36
|
+
smoothVelocity.value += (velocity.value - smoothVelocity.value) * s
|
|
37
|
+
smoothVelocity.value *= 1 - d
|
|
38
|
+
|
|
39
|
+
smoothAbsVelocity.value += (Math.abs(velocity.value) - smoothAbsVelocity.value) * s
|
|
40
|
+
smoothAbsVelocity.value *= 1 - d
|
|
60
41
|
|
|
61
42
|
if (Math.abs(smoothVelocity.value) < 0.01) smoothVelocity.value = 0
|
|
62
43
|
if (smoothAbsVelocity.value < 0.01) smoothAbsVelocity.value = 0
|
|
63
|
-
|
|
64
|
-
animationFrame = requestAnimationFrame(updateSmoothedValues)
|
|
65
44
|
}
|
|
66
45
|
|
|
67
|
-
onMounted(() =>
|
|
68
|
-
|
|
69
|
-
animationFrame = requestAnimationFrame(updateSmoothedValues)
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
onUnmounted(() => {
|
|
74
|
-
if (animationFrame) cancelAnimationFrame(animationFrame)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// Get the active velocity value
|
|
78
|
-
const activeVelocity = computed(() => (props.smooth ? smoothVelocity.value : velocity.value))
|
|
79
|
-
const activeAbsVelocity = computed(() =>
|
|
80
|
-
props.smooth ? smoothAbsVelocity.value : Math.abs(velocity.value)
|
|
81
|
-
)
|
|
46
|
+
onMounted(() => gsap.ticker.add(tick))
|
|
47
|
+
onUnmounted(() => gsap.ticker.remove(tick))
|
|
82
48
|
|
|
83
|
-
// Default intensities per effect type
|
|
84
49
|
const defaultIntensities: Record<EffectType, number> = {
|
|
85
50
|
skew: 3,
|
|
86
51
|
scale: 0.12,
|
|
@@ -92,15 +57,14 @@ const defaultIntensities: Record<EffectType, number> = {
|
|
|
92
57
|
hueRotate: 45,
|
|
93
58
|
}
|
|
94
59
|
|
|
95
|
-
const
|
|
60
|
+
const activeIntensity = computed(() => intensity ?? defaultIntensities[effect])
|
|
96
61
|
|
|
97
|
-
// Compute the style based on effect type
|
|
98
62
|
const effectStyle = computed(() => {
|
|
99
|
-
const vel =
|
|
100
|
-
const absVel =
|
|
101
|
-
const int =
|
|
63
|
+
const vel = smoothVelocity.value
|
|
64
|
+
const absVel = smoothAbsVelocity.value
|
|
65
|
+
const int = activeIntensity.value
|
|
102
66
|
|
|
103
|
-
switch (
|
|
67
|
+
switch (effect) {
|
|
104
68
|
case 'skew':
|
|
105
69
|
return { transform: `skewX(${Math.max(-15, Math.min(15, vel * int))}deg)` }
|
|
106
70
|
case 'scale':
|
|
@@ -122,13 +86,11 @@ const effectStyle = computed(() => {
|
|
|
122
86
|
}
|
|
123
87
|
})
|
|
124
88
|
|
|
125
|
-
// Classes for performance optimization
|
|
126
89
|
const effectClasses = computed(() => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
classes.push('will-change-transform')
|
|
90
|
+
if (['skew', 'scale', 'rotate', 'translateY'].includes(effect)) {
|
|
91
|
+
return ['will-change-transform']
|
|
130
92
|
}
|
|
131
|
-
return
|
|
93
|
+
return []
|
|
132
94
|
})
|
|
133
95
|
</script>
|
|
134
96
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import type { ScrollTrigger as GSAPScrollTrigger } from 'gsap/ScrollTrigger'
|
|
3
|
+
|
|
4
|
+
interface UseCountUpOptions {
|
|
5
|
+
to: number
|
|
6
|
+
from?: number
|
|
7
|
+
duration?: number
|
|
8
|
+
ease?: string
|
|
9
|
+
format?: (n: number) => string
|
|
10
|
+
once?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useCountUp(elementRef: MaybeRefOrGetter<HTMLElement | null>, opts: UseCountUpOptions) {
|
|
14
|
+
const { gsap, ScrollTrigger } = useGsap()
|
|
15
|
+
|
|
16
|
+
const displayValue = ref(String(opts.from ?? 0))
|
|
17
|
+
const isComplete = ref(false)
|
|
18
|
+
|
|
19
|
+
const format = opts.format ?? ((n: number) => n.toLocaleString())
|
|
20
|
+
const counter = { value: opts.from ?? 0 }
|
|
21
|
+
|
|
22
|
+
let scrollTriggerInstance: GSAPScrollTrigger | null = null
|
|
23
|
+
let tween: gsap.core.Tween | null = null
|
|
24
|
+
|
|
25
|
+
function startCount() {
|
|
26
|
+
tween = gsap.to(counter, {
|
|
27
|
+
value: opts.to,
|
|
28
|
+
duration: opts.duration ?? 2,
|
|
29
|
+
ease: opts.ease ?? 'power2.out',
|
|
30
|
+
onUpdate: () => {
|
|
31
|
+
displayValue.value = format(Math.round(counter.value))
|
|
32
|
+
},
|
|
33
|
+
onComplete: () => {
|
|
34
|
+
isComplete.value = true
|
|
35
|
+
displayValue.value = format(opts.to)
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onMounted(async () => {
|
|
41
|
+
await nextTick()
|
|
42
|
+
displayValue.value = format(opts.from ?? 0)
|
|
43
|
+
|
|
44
|
+
const el = toValue(elementRef)
|
|
45
|
+
if (!el) return
|
|
46
|
+
|
|
47
|
+
scrollTriggerInstance = ScrollTrigger.create({
|
|
48
|
+
trigger: el,
|
|
49
|
+
start: 'top 80%',
|
|
50
|
+
once: opts.once ?? true,
|
|
51
|
+
onEnter: startCount,
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
onUnmounted(() => {
|
|
56
|
+
tween?.kill()
|
|
57
|
+
scrollTriggerInstance?.kill()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return { displayValue, isComplete }
|
|
61
|
+
}
|