33 lines
1.1 KiB
Vue
33 lines
1.1 KiB
Vue
<script setup lang="ts">
|
|
const props = defineProps<{ value: string }>()
|
|
const root = ref<HTMLElement>()
|
|
const shown = ref(props.value)
|
|
let observer: IntersectionObserver | undefined
|
|
|
|
onMounted(() => {
|
|
const match = props.value.match(/^(\d+)(.*)$/)
|
|
if (!match || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
|
const target = Number(match[1])
|
|
const suffix = match[2]
|
|
shown.value = `0${suffix}`
|
|
observer = new IntersectionObserver(([entry]) => {
|
|
if (!entry?.isIntersecting) return
|
|
observer?.disconnect()
|
|
const startedAt = performance.now()
|
|
const duration = 620
|
|
const tick = (now: number) => {
|
|
const progress = Math.min(1, (now - startedAt) / duration)
|
|
const eased = 1 - Math.pow(1 - progress, 3)
|
|
shown.value = `${Math.round(target * eased)}${suffix}`
|
|
if (progress < 1) requestAnimationFrame(tick)
|
|
}
|
|
requestAnimationFrame(tick)
|
|
}, { threshold: 0.7 })
|
|
if (root.value) observer.observe(root.value)
|
|
})
|
|
|
|
onBeforeUnmount(() => observer?.disconnect())
|
|
</script>
|
|
|
|
<template><strong ref="root">{{ shown }}</strong></template>
|