mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
7.7 KiB
JSON
1 line
7.7 KiB
JSON
{"name":"ScrollVelocity","title":"ScrollVelocity","description":"Text marquee animatio - speed and distortion scale with user's scroll velocity.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <section>\n <div\n v-for=\"(text, index) in texts\"\n :key=\"index\"\n ref=\"containerRef\"\n :class=\"`${parallaxClassName} relative overflow-hidden`\"\n :style=\"parallaxStyle\"\n >\n <div\n ref=\"scrollerRef\"\n :class=\"`${scrollerClassName} flex whitespace-nowrap text-center font-sans text-4xl font-bold tracking-[-0.02em] drop-shadow md:text-[5rem] md:leading-[5rem]`\"\n :style=\"{ transform: `translateX(${scrollTransforms[index] || '0px'})`, ...scrollerStyle }\"\n >\n <span\n v-for=\"spanIndex in calculatedCopies[index] || 15\"\n :key=\"spanIndex\"\n :class=\"`flex-shrink-0 ${className}`\"\n :ref=\"spanIndex === 1 ? el => setCopyRef(el, index) : undefined\"\n >\n {{ text }} \n </span>\n </div>\n </div>\n </section>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onUnmounted, nextTick, type ComponentPublicInstance } from 'vue';\nimport { gsap } from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\n\ngsap.registerPlugin(ScrollTrigger);\n\ninterface VelocityMapping {\n input: [number, number];\n output: [number, number];\n}\n\ninterface ScrollVelocityProps {\n scrollContainerRef?: HTMLElement | null;\n texts?: string[];\n velocity?: number;\n className?: string;\n damping?: number;\n stiffness?: number;\n velocityMapping?: VelocityMapping;\n parallaxClassName?: string;\n scrollerClassName?: string;\n parallaxStyle?: Record<string, string | number>;\n scrollerStyle?: Record<string, string | number>;\n}\n\nconst props = withDefaults(defineProps<ScrollVelocityProps>(), {\n texts: () => [],\n velocity: 100,\n className: '',\n damping: 50,\n stiffness: 400,\n velocityMapping: () => ({ input: [0, 1000], output: [0, 5] }),\n parallaxClassName: '',\n scrollerClassName: '',\n parallaxStyle: () => ({}),\n scrollerStyle: () => ({})\n});\n\nconst containerRef = ref<HTMLDivElement[]>([]);\nconst scrollerRef = ref<HTMLDivElement[]>([]);\nconst copyRefs = ref<HTMLSpanElement[]>([]);\n\nconst baseX = ref<number[]>([]);\nconst scrollVelocity = ref(0);\nconst smoothVelocity = ref(0);\nconst velocityFactor = ref(0);\nconst copyWidths = ref<number[]>([]);\nconst directionFactors = ref<number[]>([]);\nconst calculatedCopies = ref<number[]>([]);\n\nlet rafId: number | null = null;\nlet scrollTriggerInstance: ScrollTrigger | null = null;\nlet lastScrollY = 0;\nlet lastTime = 0;\nlet resizeTimeout: number | null = null;\n\nconst setCopyRef = (el: Element | ComponentPublicInstance | null, index: number) => {\n if (el && el instanceof HTMLSpanElement) {\n copyRefs.value[index] = el;\n }\n};\n\nconst updateWidths = () => {\n props.texts.forEach((_, index) => {\n if (copyRefs.value[index] && containerRef.value[index]) {\n const singleCopyWidth = copyRefs.value[index].offsetWidth;\n const containerWidth = containerRef.value[index].offsetWidth;\n const viewportWidth = window.innerWidth;\n\n const effectiveWidth = Math.max(containerWidth, viewportWidth);\n const minCopies = Math.ceil((effectiveWidth * 2.5) / singleCopyWidth);\n const optimalCopies = Math.max(minCopies, 8);\n\n copyWidths.value[index] = singleCopyWidth;\n calculatedCopies.value[index] = optimalCopies;\n }\n });\n};\n\nconst debouncedUpdateWidths = () => {\n if (resizeTimeout) {\n clearTimeout(resizeTimeout);\n }\n resizeTimeout = window.setTimeout(() => {\n updateWidths();\n resizeTimeout = null;\n }, 150);\n};\n\nconst wrap = (min: number, max: number, v: number): number => {\n const range = max - min;\n if (range === 0) return min;\n const mod = (((v - min) % range) + range) % range;\n return mod + min;\n};\n\nconst scrollTransforms = computed(() => {\n return props.texts.map((_, index) => {\n const singleWidth = copyWidths.value[index];\n if (singleWidth === undefined || singleWidth === 0) return '0px';\n return `${wrap(-singleWidth, 0, baseX.value[index] || 0)}px`;\n });\n});\n\nconst updateSmoothVelocity = () => {\n const dampingFactor = props.damping / 1000;\n const stiffnessFactor = props.stiffness / 1000;\n\n const velocityDiff = scrollVelocity.value - smoothVelocity.value;\n smoothVelocity.value += velocityDiff * stiffnessFactor;\n smoothVelocity.value *= 1 - dampingFactor;\n};\n\nconst updateVelocityFactor = () => {\n const { input, output } = props.velocityMapping;\n const inputRange = input[1] - input[0];\n const outputRange = output[1] - output[0];\n\n let normalizedVelocity = (Math.abs(smoothVelocity.value) - input[0]) / inputRange;\n normalizedVelocity = Math.max(0, Math.min(1, normalizedVelocity));\n\n velocityFactor.value = output[0] + normalizedVelocity * outputRange;\n if (smoothVelocity.value < 0) velocityFactor.value *= -1;\n};\n\nconst animate = (currentTime: number) => {\n if (lastTime === 0) lastTime = currentTime;\n const delta = currentTime - lastTime;\n lastTime = currentTime;\n\n updateSmoothVelocity();\n updateVelocityFactor();\n\n props.texts.forEach((_, index) => {\n const baseVelocity = index % 2 !== 0 ? -props.velocity : props.velocity;\n\n let moveBy = (directionFactors.value[index] || 1) * baseVelocity * (delta / 1000);\n\n if (velocityFactor.value < 0) {\n directionFactors.value[index] = -1;\n } else if (velocityFactor.value > 0) {\n directionFactors.value[index] = 1;\n }\n\n moveBy += (directionFactors.value[index] || 1) * moveBy * velocityFactor.value;\n baseX.value[index] = (baseX.value[index] || 0) + moveBy;\n });\n\n rafId = requestAnimationFrame(animate);\n};\n\nconst updateScrollVelocity = () => {\n const container = props.scrollContainerRef || window;\n const currentScrollY = container === window ? window.scrollY : (container as HTMLElement).scrollTop;\n\n const currentTime = performance.now();\n const timeDelta = currentTime - lastTime;\n\n if (timeDelta > 0) {\n const scrollDelta = currentScrollY - lastScrollY;\n scrollVelocity.value = (scrollDelta / timeDelta) * 1000;\n }\n\n lastScrollY = currentScrollY;\n};\n\nonMounted(async () => {\n await nextTick();\n\n baseX.value = new Array(props.texts.length).fill(0);\n copyWidths.value = new Array(props.texts.length).fill(0);\n calculatedCopies.value = new Array(props.texts.length).fill(15);\n directionFactors.value = new Array(props.texts.length).fill(1);\n\n setTimeout(() => {\n updateWidths();\n }, 100);\n\n updateWidths();\n\n if (containerRef.value && containerRef.value.length > 0) {\n scrollTriggerInstance = ScrollTrigger.create({\n trigger: containerRef.value[0],\n start: 'top bottom',\n end: 'bottom top',\n onUpdate: updateScrollVelocity,\n ...(props.scrollContainerRef && { scroller: props.scrollContainerRef })\n });\n }\n\n rafId = requestAnimationFrame(animate);\n\n window.addEventListener('resize', debouncedUpdateWidths, { passive: true });\n});\n\nonUnmounted(() => {\n if (rafId) {\n cancelAnimationFrame(rafId);\n }\n if (scrollTriggerInstance) {\n scrollTriggerInstance.kill();\n }\n if (resizeTimeout) {\n clearTimeout(resizeTimeout);\n }\n window.removeEventListener('resize', debouncedUpdateWidths);\n});\n</script>\n","path":"ScrollVelocity/ScrollVelocity.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"gsap","version":"^3.13.0"}],"devDependencies":[],"categories":["TextAnimations"]} |