mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Landing Page
This commit is contained in:
192
src/content/TextAnimations/SplitText/SplitText.vue
Normal file
192
src/content/TextAnimations/SplitText/SplitText.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<p
|
||||
ref="textRef"
|
||||
:class="`split-parent overflow-hidden inline-block whitespace-normal ${className}`"
|
||||
:style="{
|
||||
textAlign,
|
||||
wordWrap: 'break-word',
|
||||
}"
|
||||
>
|
||||
{{ text }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import { SplitText as GSAPSplitText } from 'gsap/SplitText'
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger, GSAPSplitText)
|
||||
|
||||
export interface SplitTextProps {
|
||||
text: string
|
||||
className?: string
|
||||
delay?: number
|
||||
duration?: number
|
||||
ease?: string | ((t: number) => number)
|
||||
splitType?: 'chars' | 'words' | 'lines' | 'words, chars'
|
||||
from?: gsap.TweenVars
|
||||
to?: gsap.TweenVars
|
||||
threshold?: number
|
||||
rootMargin?: string
|
||||
textAlign?: 'left' | 'center' | 'right' | 'justify'
|
||||
onLetterAnimationComplete?: () => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SplitTextProps>(), {
|
||||
className: '',
|
||||
delay: 100,
|
||||
duration: 0.6,
|
||||
ease: 'power3.out',
|
||||
splitType: 'chars',
|
||||
from: () => ({ opacity: 0, y: 40 }),
|
||||
to: () => ({ opacity: 1, y: 0 }),
|
||||
threshold: 0.1,
|
||||
rootMargin: '-100px',
|
||||
textAlign: 'center'
|
||||
})
|
||||
|
||||
const textRef = ref<HTMLParagraphElement | null>(null)
|
||||
const animationCompletedRef = ref(false)
|
||||
const scrollTriggerRef = ref<ScrollTrigger | null>(null)
|
||||
const timelineRef = ref<gsap.core.Timeline | null>(null)
|
||||
const splitterRef = ref<GSAPSplitText | null>(null)
|
||||
|
||||
const initializeAnimation = async () => {
|
||||
if (typeof window === 'undefined' || !textRef.value || !props.text) return
|
||||
|
||||
// Wait for DOM to be fully updated
|
||||
await nextTick()
|
||||
|
||||
const el = textRef.value
|
||||
|
||||
animationCompletedRef.value = false
|
||||
|
||||
const absoluteLines = props.splitType === 'lines'
|
||||
if (absoluteLines) el.style.position = 'relative'
|
||||
|
||||
let splitter: GSAPSplitText
|
||||
try {
|
||||
splitter = new GSAPSplitText(el, {
|
||||
type: props.splitType,
|
||||
absolute: absoluteLines,
|
||||
linesClass: 'split-line',
|
||||
})
|
||||
splitterRef.value = splitter
|
||||
} catch (error) {
|
||||
console.error('Failed to create SplitText:', error)
|
||||
return
|
||||
}
|
||||
|
||||
let targets: Element[]
|
||||
switch (props.splitType) {
|
||||
case 'lines':
|
||||
targets = splitter.lines
|
||||
break
|
||||
case 'words':
|
||||
targets = splitter.words
|
||||
break
|
||||
case 'chars':
|
||||
targets = splitter.chars
|
||||
break
|
||||
default:
|
||||
targets = splitter.chars
|
||||
}
|
||||
|
||||
if (!targets || targets.length === 0) {
|
||||
console.warn('No targets found for SplitText animation')
|
||||
splitter.revert()
|
||||
return
|
||||
}
|
||||
|
||||
targets.forEach((t) => {
|
||||
;(t as HTMLElement).style.willChange = 'transform, opacity'
|
||||
})
|
||||
|
||||
const startPct = (1 - props.threshold) * 100
|
||||
const marginMatch = /^(-?\d+(?:\.\d+)?)(px|em|rem|%)?$/.exec(props.rootMargin)
|
||||
const marginValue = marginMatch ? parseFloat(marginMatch[1]) : 0
|
||||
const marginUnit = marginMatch ? (marginMatch[2] || 'px') : 'px'
|
||||
const sign = marginValue < 0 ? `-=${Math.abs(marginValue)}${marginUnit}` : `+=${marginValue}${marginUnit}`
|
||||
const start = `top ${startPct}%${sign}`
|
||||
|
||||
const tl = gsap.timeline({
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start,
|
||||
toggleActions: 'play none none none',
|
||||
once: true,
|
||||
onToggle: (self) => {
|
||||
scrollTriggerRef.value = self
|
||||
},
|
||||
},
|
||||
smoothChildTiming: true,
|
||||
onComplete: () => {
|
||||
animationCompletedRef.value = true
|
||||
gsap.set(targets, {
|
||||
...props.to,
|
||||
clearProps: 'willChange',
|
||||
immediateRender: true,
|
||||
})
|
||||
props.onLetterAnimationComplete?.()
|
||||
},
|
||||
})
|
||||
|
||||
timelineRef.value = tl
|
||||
|
||||
tl.set(targets, { ...props.from, immediateRender: false, force3D: true })
|
||||
tl.to(targets, {
|
||||
...props.to,
|
||||
duration: props.duration,
|
||||
ease: props.ease,
|
||||
stagger: props.delay / 1000,
|
||||
force3D: true,
|
||||
})
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
if (timelineRef.value) {
|
||||
timelineRef.value.kill()
|
||||
timelineRef.value = null
|
||||
}
|
||||
if (scrollTriggerRef.value) {
|
||||
scrollTriggerRef.value.kill()
|
||||
scrollTriggerRef.value = null
|
||||
}
|
||||
if (splitterRef.value) {
|
||||
gsap.killTweensOf(textRef.value)
|
||||
splitterRef.value.revert()
|
||||
splitterRef.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeAnimation()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
// Watch for prop changes and reinitialize animation
|
||||
watch(
|
||||
[
|
||||
() => props.text,
|
||||
() => props.delay,
|
||||
() => props.duration,
|
||||
() => props.ease,
|
||||
() => props.splitType,
|
||||
() => props.from,
|
||||
() => props.to,
|
||||
() => props.threshold,
|
||||
() => props.rootMargin,
|
||||
() => props.onLetterAnimationComplete,
|
||||
],
|
||||
() => {
|
||||
cleanup()
|
||||
initializeAnimation()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user