feat. motion 적용
This commit is contained in:
@@ -119,6 +119,7 @@ const handleButtonClick = (button: PageDataResourceGroup) => {
|
||||
<template>
|
||||
<div
|
||||
v-if="buttonList.length"
|
||||
v-motion-stagger
|
||||
class="flex flex-wrap justify-center gap-3 md:gap-4"
|
||||
>
|
||||
<template v-for="(button, index) in buttonList" :key="index">
|
||||
|
||||
@@ -7,7 +7,7 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="description">
|
||||
<p v-motion-stagger class="description">
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,7 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>
|
||||
<h2 v-motion-stagger>
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="props.tag">
|
||||
<component :is="props.tag" v-motion-stagger>
|
||||
<BlocksVisualContent :resources-data="props.resourcesData" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -18,6 +18,7 @@ const handleVideoPlayClick = () => {
|
||||
|
||||
<template>
|
||||
<AtomsButtonPlay
|
||||
v-motion-stagger
|
||||
:tracking="props.resourcesData.tracking"
|
||||
@click="handleVideoPlayClick"
|
||||
/>
|
||||
|
||||
168
layers/plugins/motion-stagger.client.ts
Normal file
168
layers/plugins/motion-stagger.client.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { animate, stagger } from 'motion-v'
|
||||
import type { DOMKeyframesDefinition, AnimationOptions } from 'motion-v'
|
||||
|
||||
export default defineNuxtPlugin(nuxtApp => {
|
||||
// 전역 상태 관리
|
||||
const animatedItems = new Set<Element>()
|
||||
const sectionObservers = new Map<Element, IntersectionObserver>()
|
||||
const sectionItems = new Map<Element, Set<Element>>()
|
||||
|
||||
// 섹션의 motion-item들을 애니메이션
|
||||
const animateSectionItems = (section: Element) => {
|
||||
const items = sectionItems.get(section)
|
||||
if (!items || items.size === 0) return
|
||||
|
||||
const newItems = Array.from(items).filter(item => !animatedItems.has(item))
|
||||
if (newItems.length === 0) return
|
||||
|
||||
// 애니메이션 실행
|
||||
newItems.forEach(item => animatedItems.add(item))
|
||||
|
||||
animate(
|
||||
newItems,
|
||||
{ opacity: 1, y: 0 } as DOMKeyframesDefinition,
|
||||
{
|
||||
delay: stagger(0.2),
|
||||
duration: 0.5,
|
||||
easing: [0.22, 1, 0.36, 1],
|
||||
} as AnimationOptions
|
||||
)
|
||||
}
|
||||
|
||||
// 섹션에 IntersectionObserver 등록
|
||||
const observeSection = (section: Element) => {
|
||||
if (sectionObservers.has(section)) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
animateSectionItems(section)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.2 }
|
||||
)
|
||||
|
||||
observer.observe(section)
|
||||
sectionObservers.set(section, observer)
|
||||
}
|
||||
|
||||
// 섹션이 viewport에 있는지 확인
|
||||
const isSectionInViewport = (section: Element): boolean => {
|
||||
const rect = section.getBoundingClientRect()
|
||||
const windowHeight =
|
||||
window.innerHeight || document.documentElement.clientHeight
|
||||
// threshold 0.2를 고려한 체크
|
||||
const visibleHeight =
|
||||
Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0)
|
||||
const sectionHeight = rect.height
|
||||
return visibleHeight / sectionHeight >= 0.2
|
||||
}
|
||||
|
||||
// v-motion-stagger 디렉티브
|
||||
nuxtApp.vueApp.directive('motion-stagger', {
|
||||
mounted(el: HTMLElement) {
|
||||
// 초기 스타일 설정 (애니메이션 전)
|
||||
el.style.opacity = '0'
|
||||
el.style.transform = 'translateY(30px)'
|
||||
|
||||
// 디버깅: 요소 등록 확인
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[motion-stagger] Element mounted:', el.tagName)
|
||||
}
|
||||
|
||||
// 가장 가까운 section 찾기
|
||||
const section = el.closest('section')
|
||||
if (!section) {
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[motion-stagger] No section found for element:', el)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 섹션의 아이템 목록에 추가
|
||||
if (!sectionItems.has(section)) {
|
||||
sectionItems.set(section, new Set())
|
||||
observeSection(section)
|
||||
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[motion-stagger] New section registered')
|
||||
}
|
||||
}
|
||||
sectionItems.get(section)!.add(el)
|
||||
|
||||
// 이미 viewport에 있는 경우 즉시 체크 (여러 번 체크)
|
||||
const inViewport = isSectionInViewport(section)
|
||||
|
||||
if (import.meta.dev) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[motion-stagger] Section in viewport:', inViewport)
|
||||
}
|
||||
|
||||
if (inViewport) {
|
||||
// 즉시 실행
|
||||
requestAnimationFrame(() => {
|
||||
animateSectionItems(section)
|
||||
})
|
||||
|
||||
// 추가로 100ms 후 재확인 (비동기 렌더링 대응)
|
||||
setTimeout(() => {
|
||||
if (isSectionInViewport(section)) {
|
||||
animateSectionItems(section)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
// 추가로 300ms 후 재확인 (느린 비동기 데이터 대응)
|
||||
setTimeout(() => {
|
||||
if (isSectionInViewport(section)) {
|
||||
animateSectionItems(section)
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
|
||||
unmounted(el: Element) {
|
||||
const section = el.closest('section')
|
||||
if (!section) return
|
||||
|
||||
// 아이템 제거
|
||||
const items = sectionItems.get(section)
|
||||
if (items) {
|
||||
items.delete(el)
|
||||
// 섹션에 아이템이 없으면 observer 정리
|
||||
if (items.size === 0) {
|
||||
const observer = sectionObservers.get(section)
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
sectionObservers.delete(section)
|
||||
}
|
||||
sectionItems.delete(section)
|
||||
}
|
||||
}
|
||||
|
||||
animatedItems.delete(el)
|
||||
},
|
||||
})
|
||||
|
||||
// 페이지 이동 시 정리
|
||||
if (import.meta.client) {
|
||||
nuxtApp.hook('page:finish', () => {
|
||||
// 현재 페이지에 없는 섹션들 정리
|
||||
const currentSections = new Set<Element>(
|
||||
Array.from(document.querySelectorAll('section'))
|
||||
)
|
||||
|
||||
sectionObservers.forEach((observer, section) => {
|
||||
if (!currentSections.has(section)) {
|
||||
observer.disconnect()
|
||||
sectionObservers.delete(section)
|
||||
sectionItems.delete(section)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -124,6 +124,7 @@ const getArticleUrl = (articleId: string) => {
|
||||
<ClientOnly>
|
||||
<WidgetsSlideDefault
|
||||
v-if="slideLength > 0"
|
||||
v-motion-stagger
|
||||
v-bind="splideOptions"
|
||||
:slide-item-length="slideLength"
|
||||
:arrows-data="arrowsData"
|
||||
|
||||
@@ -91,6 +91,7 @@ const getVideoSrc = (item: PageDataTemplateComponent) => {
|
||||
/>
|
||||
<AtomsVideo
|
||||
v-if="hasComponentGroup(item, 'video')"
|
||||
v-motion-stagger
|
||||
:src="getVideoSrc(item)"
|
||||
:play="currentSlideIndex === index"
|
||||
class="aspect-[16/10] w-[258px] mt-8 md:w-[496px] md:mt-10"
|
||||
|
||||
@@ -89,6 +89,7 @@ onBeforeUnmount(() => {
|
||||
class="title-md max-w-[944px]"
|
||||
/>
|
||||
<WidgetsSlideThumbnail
|
||||
v-motion-stagger
|
||||
:thumbnail-data="slideData"
|
||||
:pagination-data="paginationData"
|
||||
:drag="false"
|
||||
|
||||
@@ -70,6 +70,7 @@ const handleSplideMove = (_splide: SplideType, newIndex: number) => {
|
||||
/>
|
||||
<WidgetsSlideCenterFocus
|
||||
v-if="slideData"
|
||||
v-motion-stagger
|
||||
:slide-item-size="slideItemSize"
|
||||
:slide-item-length="slideData?.length"
|
||||
:arrows-data="arrowsData"
|
||||
|
||||
@@ -80,6 +80,7 @@ const handleSplideMove = (
|
||||
/>
|
||||
<WidgetsSlideCenterHighlight
|
||||
v-if="slideData"
|
||||
v-motion-stagger
|
||||
:slide-item-size="slideItemSize"
|
||||
:slide-item-length="slideData?.length"
|
||||
:arrows-data="arrowsData"
|
||||
|
||||
@@ -101,6 +101,7 @@ const slideItemSize = {
|
||||
<WidgetsVideoPlay v-if="videoPlayData" :resources-data="videoPlayData" />
|
||||
<WidgetsSlideCenterHighlight
|
||||
v-if="slideData && slideData.length > 0"
|
||||
v-motion-stagger
|
||||
:slide-item-size="slideItemSize"
|
||||
:slide-item-length="slideData.length"
|
||||
:arrows-data="arrowsData"
|
||||
|
||||
Reference in New Issue
Block a user