Component Boom

This commit is contained in:
David Haz
2025-07-10 15:36:38 +03:00
parent a4982577ad
commit 9b3465b04d
135 changed files with 16697 additions and 60 deletions

View File

@@ -0,0 +1,193 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'
interface CurvedLoopProps {
marqueeText?: string
speed?: number
className?: string
curveAmount?: number
direction?: 'left' | 'right'
interactive?: boolean
}
const props = withDefaults(defineProps<CurvedLoopProps>(), {
marqueeText: '',
speed: 2,
className: '',
curveAmount: 400,
direction: 'left',
interactive: true
})
const text = computed(() => {
const hasTrailing = /\s|\u00A0$/.test(props.marqueeText)
return (
(hasTrailing ? props.marqueeText.replace(/\s+$/, '') : props.marqueeText) + '\u00A0'
)
})
const measureRef = ref<SVGTextElement | null>(null)
const tspansRef = ref<SVGTSpanElement[]>([])
const pathRef = ref<SVGPathElement | null>(null)
const pathLength = ref(0)
const spacing = ref(0)
const uid = Math.random().toString(36).substr(2, 9)
const pathId = `curve-${uid}`
const pathD = computed(() => `M-100,40 Q500,${40 + props.curveAmount} 1540,40`)
const dragRef = ref(false)
const lastXRef = ref(0)
const dirRef = ref<'left' | 'right'>(props.direction)
const velRef = ref(0)
let animationFrame: number | null = null
const updateSpacing = () => {
if (measureRef.value) {
spacing.value = measureRef.value.getComputedTextLength()
}
}
const updatePathLength = () => {
if (pathRef.value) {
pathLength.value = pathRef.value.getTotalLength()
}
}
const animate = () => {
if (!spacing.value) return
const step = () => {
tspansRef.value.forEach((t) => {
if (!t) return
let x = parseFloat(t.getAttribute('x') || '0')
if (!dragRef.value) {
const delta = dirRef.value === 'right' ? Math.abs(props.speed) : -Math.abs(props.speed)
x += delta
}
const maxX = (tspansRef.value.length - 1) * spacing.value
if (x < -spacing.value) x = maxX
if (x > maxX) x = -spacing.value
t.setAttribute('x', x.toString())
})
animationFrame = requestAnimationFrame(step)
}
step()
}
const stopAnimation = () => {
if (animationFrame) {
cancelAnimationFrame(animationFrame)
animationFrame = null
}
}
const repeats = computed(() => {
return pathLength.value && spacing.value ? Math.ceil(pathLength.value / spacing.value) + 2 : 0
})
const ready = computed(() => pathLength.value > 0 && spacing.value > 0)
const onPointerDown = (e: PointerEvent) => {
if (!props.interactive) return
dragRef.value = true
lastXRef.value = e.clientX
velRef.value = 0
; (e.target as HTMLElement).setPointerCapture(e.pointerId)
}
const onPointerMove = (e: PointerEvent) => {
if (!props.interactive || !dragRef.value) return
const dx = e.clientX - lastXRef.value
lastXRef.value = e.clientX
velRef.value = dx
tspansRef.value.forEach((t) => {
if (!t) return
let x = parseFloat(t.getAttribute('x') || '0')
x += dx
const maxX = (tspansRef.value.length - 1) * spacing.value
if (x < -spacing.value) x = maxX
if (x > maxX) x = -spacing.value
t.setAttribute('x', x.toString())
})
}
const endDrag = () => {
if (!props.interactive) return
dragRef.value = false
dirRef.value = velRef.value > 0 ? 'right' : 'left'
}
const cursorStyle = computed(() => {
return props.interactive
? dragRef.value
? 'grabbing'
: 'grab'
: 'auto'
})
onMounted(() => {
nextTick(() => {
updateSpacing()
updatePathLength()
animate()
})
})
onUnmounted(() => {
stopAnimation()
})
watch([text, () => props.className], () => {
nextTick(() => {
updateSpacing()
})
})
watch(() => props.curveAmount, () => {
nextTick(() => {
updatePathLength()
})
})
watch([spacing, () => props.speed], () => {
stopAnimation()
if (spacing.value) {
animate()
}
})
watch(repeats, () => {
tspansRef.value = []
})
</script>
<template>
<div class="min-h-screen flex items-center justify-center w-full" :style="{
visibility: ready ? 'visible' : 'hidden',
cursor: cursorStyle
}" @pointerdown="onPointerDown" @pointermove="onPointerMove" @pointerup="endDrag" @pointerleave="endDrag">
<svg
class="select-none w-full overflow-visible block aspect-[100/12] text-[6rem] font-bold tracking-[5px] uppercase leading-none"
viewBox="0 0 1440 120">
<text ref="measureRef" xml:space="preserve" style="visibility: hidden; opacity: 0; pointer-events: none;">
{{ text }}
</text>
<defs>
<path ref="pathRef" :id="pathId" :d="pathD" fill="none" stroke="transparent" />
</defs>
<text v-if="ready" xml:space="preserve" :class="`fill-white ${className}`">
<textPath :href="`#${pathId}`" xml:space="preserve">
<tspan v-for="i in repeats" :key="i" :x="(i - 1) * spacing" :ref="(el) => {
if (el) tspansRef[i - 1] = el as SVGTSpanElement
}">
{{ text }}
</tspan>
</textPath>
</text>
</svg>
</div>
</template>