62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
// v-reveal 디렉티브 — 요소가 뷰포트에 들어오면 등장 애니메이션 트리거
|
|
// 사용법:
|
|
// v-reveal → 기본(아래→위 fade-up)
|
|
// v-reveal:scale → 확대 등장
|
|
// v-reveal:fade → 투명도만
|
|
// v-reveal="{ delay: 120 }" → 등장 지연(ms) — 스태거용
|
|
// v-reveal:up="{ delay: i*90 }" → 타입 + 지연 조합
|
|
//
|
|
// 디렉티브는 서버/클라이언트 양쪽에 등록(미등록 시 SSR 렌더 크래시)하되,
|
|
// IntersectionObserver 등 실제 동작은 클라이언트에서만 수행한다.
|
|
// getSSRProps 를 제공해 SSR 단계에서 안전하게 통과시킨다.
|
|
type RevealType = 'up' | 'scale' | 'fade'
|
|
interface RevealValue {
|
|
type?: RevealType
|
|
delay?: number
|
|
}
|
|
|
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
let observer: IntersectionObserver | null = null
|
|
let reduceMotion = false
|
|
|
|
if (import.meta.client) {
|
|
reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
observer = new IntersectionObserver(
|
|
(entries) => {
|
|
for (const entry of entries) {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('is-revealed')
|
|
observer?.unobserve(entry.target)
|
|
}
|
|
}
|
|
},
|
|
// 요소가 화면 하단에서 약간 올라온 시점에 트리거
|
|
{ threshold: 0.15, rootMargin: '0px 0px -8% 0px' },
|
|
)
|
|
}
|
|
|
|
nuxtApp.vueApp.directive<HTMLElement, RevealValue | number | undefined>('reveal', {
|
|
// SSR: 추가 속성 없이 통과 (디렉티브 미등록으로 인한 크래시 방지)
|
|
getSSRProps: () => ({}),
|
|
mounted(el, binding) {
|
|
// 서버에서는 호출되지 않음. 모션 최소화 선호 시 애니메이션 없이 그대로 노출.
|
|
if (reduceMotion || !observer) return
|
|
|
|
const value = binding.value
|
|
const type: RevealType =
|
|
(binding.arg as RevealType) ||
|
|
(typeof value === 'object' && value?.type) ||
|
|
'up'
|
|
const delay = typeof value === 'number' ? value : value?.delay ?? 0
|
|
|
|
el.classList.add('reveal', `reveal-${type}`)
|
|
if (delay) el.style.setProperty('--reveal-delay', `${delay}ms`)
|
|
|
|
observer.observe(el)
|
|
},
|
|
unmounted(el) {
|
|
observer?.unobserve(el)
|
|
},
|
|
})
|
|
})
|