viewport cells intersect animation

This commit is contained in:
nicwands 2026-06-19 15:32:09 -04:00
parent 4b79a5af0f
commit a99dfdb8ff
4 changed files with 63 additions and 3 deletions

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="infinite-scroll-container"> <div class="infinite-scroll-container">
<transition-group name="fade" tag="div" class="viewport-stage"> <div class="viewport-stage">
<div <div
v-for="viewport in visibleViewports" v-for="viewport in visibleViewports"
class="viewport" class="viewport"
@ -24,7 +24,7 @@
</div> </div>
</slot> </slot>
</div> </div>
</transition-group> </div>
</div> </div>
</template> </template>
@ -97,6 +97,7 @@ watch(currentViewportIndex, (newIndex, oldIndex) => {
emit('viewport-change', { newIndex, oldIndex }) emit('viewport-change', { newIndex, oldIndex })
}) })
// Expose to parent
defineExpose({ defineExpose({
reset, reset,
retryViewport, retryViewport,

View file

@ -1,5 +1,11 @@
<template> <template>
<div class="mosaic-viewport"> <div
class="mosaic-viewport"
v-intersect:once="{
callback: onIntersect,
rootMargin: '-25% 0px -25% 0px',
}"
>
<div class="mosaic-container"> <div class="mosaic-container">
<router-link <router-link
v-for="cell in content.cells" v-for="cell in content.cells"
@ -18,6 +24,7 @@
params: { id: cell.content.slug }, params: { id: cell.content.slug },
}" }"
:key="`${viewport.index}-${cell.id}`" :key="`${viewport.index}-${cell.id}`"
ref="cells"
> >
<strapi-media <strapi-media
:image="cell.content.image" :image="cell.content.image"
@ -30,6 +37,9 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'
import gsap from 'gsap'
const props = defineProps({ const props = defineProps({
viewport: { viewport: {
type: Object, type: Object,
@ -40,6 +50,20 @@ const props = defineProps({
required: true, required: true,
}, },
}) })
const cells = ref([])
const onIntersect = (isIntersecting) => {
if (isIntersecting) {
if (!cells.value) return
const cellGifs = cells.value
.map((cell) => cell.$el?.children?.[0])
.filter(Boolean)
gsap.to(cellGifs, { opacity: 1, duration: 0, stagger: 0.1 })
}
}
</script> </script>
<style lang="scss"> <style lang="scss">
@ -76,6 +100,7 @@ const props = defineProps({
} }
.strapi-media { .strapi-media {
mix-blend-mode: darken; mix-blend-mode: darken;
opacity: 0;
} }
} }
} }

View file

@ -3,11 +3,15 @@ import App from './App.vue'
import { ViteSSG } from 'vite-ssg' import { ViteSSG } from 'vite-ssg'
import routes from './routes' import routes from './routes'
import strapiPlugin from './plugins/strapi' import strapiPlugin from './plugins/strapi'
import gsapPlugin from './plugins/gsap'
import intersectPlugin from './plugins/intersect'
import { strapi } from '@strapi/client' import { strapi } from '@strapi/client'
export const createApp = ViteSSG(App, routes, ({ app }) => { export const createApp = ViteSSG(App, routes, ({ app }) => {
// Plugins // Plugins
app.use(strapiPlugin) app.use(strapiPlugin)
app.use(gsapPlugin)
app.use(intersectPlugin)
}) })
// Prerender mosaic item routes // Prerender mosaic item routes

30
src/plugins/intersect.js Normal file
View file

@ -0,0 +1,30 @@
import { useIntersectionObserver } from '@vueuse/core'
export default {
install: (app) => {
app.directive('intersect', {
mounted(el, binding) {
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
binding.value?.callback?.(isIntersecting)
if (isIntersecting) {
el.classList.add('intersected')
if (binding.arg === 'once') {
stop()
}
} else {
el.classList.remove('intersected')
}
},
{
threshold: binding.value?.threshold || 0,
rootMargin: binding.value?.rootMargin || '0px',
},
)
},
})
},
}