Landing Page

This commit is contained in:
David Haz
2025-07-08 12:39:14 +03:00
parent fa9392fa47
commit 9ddb731258
41 changed files with 4584 additions and 8 deletions

View 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>