kmcom-nuxt-layers 1.6.39 → 1.6.40

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.
@@ -1,142 +1,142 @@
1
1
  <script setup lang="ts">
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
- },
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
- // Base multiplier from velocity magnitude
97
- let multiplier = 1 + velocityEffect
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
- // Clamp to min/max
100
- multiplier = Math.max(props.minSpeed, Math.min(props.maxSpeed, multiplier))
123
+ onUnmounted(() => {
124
+ tweenRef.value?.kill()
125
+ })
101
126
 
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
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
- if (props.pauseOnHover && tweenRef.value) {
136
- gsap.to(tweenRef.value, { timeScale: currentTimeScale.value, duration: 0.5 })
137
- isPaused.value = false
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 }}&nbsp;
143
+ </span>
144
+ </div>
145
+ </div>
146
+ </section>
147
+ </template>
@@ -0,0 +1,50 @@
1
+ import type { MaybeRef } from 'vue'
2
+
3
+ export function useMarqueeCopies(
4
+ containerRefs: Ref<HTMLElement[]>,
5
+ copyRefs: Ref<HTMLSpanElement[]>,
6
+ rowCount: MaybeRef<number>,
7
+ ) {
8
+ const copyWidths = ref<number[]>([])
9
+ const calculatedCopies = ref<number[]>([])
10
+
11
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
12
+
13
+ function calculate() {
14
+ const n = unref(rowCount)
15
+ for (let i = 0; i < n; i++) {
16
+ const copy = copyRefs.value[i]
17
+ const container = containerRefs.value[i]
18
+ if (!copy || !container) continue
19
+ const singleWidth = copy.offsetWidth
20
+ if (singleWidth === 0) continue
21
+ const effectiveWidth = Math.max(container.offsetWidth, window.innerWidth)
22
+ const minCopies = Math.ceil((effectiveWidth * 2.5) / singleWidth)
23
+ copyWidths.value[i] = singleWidth
24
+ calculatedCopies.value[i] = Math.max(minCopies, 8)
25
+ }
26
+ }
27
+
28
+ function debouncedCalculate() {
29
+ if (debounceTimer) clearTimeout(debounceTimer)
30
+ debounceTimer = setTimeout(() => {
31
+ calculate()
32
+ debounceTimer = null
33
+ }, 150)
34
+ }
35
+
36
+ onMounted(() => {
37
+ nextTick(() => {
38
+ calculate()
39
+ setTimeout(calculate, 100)
40
+ })
41
+ window.addEventListener('resize', debouncedCalculate, { passive: true })
42
+ })
43
+
44
+ onUnmounted(() => {
45
+ if (debounceTimer) clearTimeout(debounceTimer)
46
+ window.removeEventListener('resize', debouncedCalculate)
47
+ })
48
+
49
+ return { copyWidths, calculatedCopies }
50
+ }
@@ -0,0 +1,44 @@
1
+ import type { MaybeRef } from 'vue'
2
+
3
+ interface VelocityMapping {
4
+ input: [number, number]
5
+ output: [number, number]
6
+ }
7
+
8
+ interface UseMarqueeVelocityOptions {
9
+ damping?: MaybeRef<number>
10
+ stiffness?: MaybeRef<number>
11
+ velocityMapping?: MaybeRef<VelocityMapping>
12
+ }
13
+
14
+ export function useMarqueeVelocity(opts: UseMarqueeVelocityOptions = {}) {
15
+ const { velocity: rawVelocity } = useSmoothScroll()
16
+ const { gsap } = useGsap()
17
+
18
+ const smoothVelocity = ref(0)
19
+ const velocityFactor = ref(0)
20
+
21
+ function tick() {
22
+ const damping = (unref(opts.damping) ?? 50) / 1000
23
+ const stiffness = (unref(opts.stiffness) ?? 400) / 1000
24
+ const mapping: VelocityMapping = unref(opts.velocityMapping) ?? {
25
+ input: [0, 1000],
26
+ output: [0, 5],
27
+ }
28
+
29
+ smoothVelocity.value += (rawVelocity.value - smoothVelocity.value) * stiffness
30
+ smoothVelocity.value *= 1 - damping
31
+
32
+ const inputRange = mapping.input[1] - mapping.input[0]
33
+ const outputRange = mapping.output[1] - mapping.output[0]
34
+ let t = (Math.abs(smoothVelocity.value) - mapping.input[0]) / inputRange
35
+ t = Math.max(0, Math.min(1, t))
36
+ velocityFactor.value = mapping.output[0] + t * outputRange
37
+ if (smoothVelocity.value < 0) velocityFactor.value *= -1
38
+ }
39
+
40
+ onMounted(() => gsap.ticker.add(tick))
41
+ onUnmounted(() => gsap.ticker.remove(tick))
42
+
43
+ return { velocityFactor }
44
+ }
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "extends": "../../apps/playground/tsconfig.json",
3
+
3
4
  "compilerOptions": {
4
5
  "strict": true
5
6
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kmcom-nuxt-layers",
3
3
  "private": false,
4
- "version": "1.6.39",
4
+ "version": "1.6.40",
5
5
  "description": "Composable Nuxt 4 layers for building scalable Vue applications",
6
6
  "exports": {
7
7
  "./layers/core": "./layers/core/nuxt.config.ts",