nicwands.com/src/components/MosaicViewport.vue
2026-06-26 12:02:56 -04:00

143 lines
3.6 KiB
Vue

<template>
<div
class="mosaic-viewport"
v-intersect:once="{
callback: onIntersect,
rootMargin: '-25% 0px -25% 0px',
}"
>
<div class="mosaic-container">
<router-link
v-for="(cell, i) in content.cells"
:class="[
'mosaic-cell',
`layout-${cell.width * wWidth >= cell.height * wHeight ? 'landscape' : 'portrait'}`,
]"
:style="{
left: cell.x * 100 + 'vw',
top: cell.y * 100 + 'vh',
width: cell.width * 100 + 'vw',
height: cell.height * 100 + 'vh',
}"
:to="{
name: 'mosaic-item',
params: { id: cell.content.slug },
}"
:key="`${viewport.index}-${cell.id}`"
@mouseenter="onMouseEnter(i)"
ref="cells"
>
<div class="hover-bg" :style="{ background: hoverColors[i] }" />
<strapi-media
:image="cell.content.image"
:aspect="100"
fit="contain"
/>
</router-link>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import gsap from 'gsap'
import { useWindowSize } from '@vueuse/core'
const THEME_S = 90
const THEME_L = 50
const props = defineProps({
viewport: {
type: Object,
required: true,
},
content: {
type: Object,
required: true,
},
})
const cells = ref([])
const hoverColors = ref(
cells.value.map(
() => `hsl(${Math.random() * 360}deg ${THEME_S}% ${THEME_L}%)`,
),
)
const { width: wWidth, height: wHeight } = useWindowSize()
const onMouseEnter = (i) => {
hoverColors.value[i] =
`hsl(${Math.random() * 360}deg ${THEME_S}% ${THEME_L}%)`
}
const onIntersect = (isIntersecting) => {
if (isIntersecting) {
if (!cells.value) return
const cellGifs = cells.value
.map((cell) => cell.$el?.children?.[1])
.filter(Boolean)
gsap.to(cellGifs, { opacity: 1, duration: 0, stagger: 0.1 })
}
}
</script>
<style lang="scss">
.mosaic-viewport {
height: calc(100 * var(--vh));
display: flex;
position: relative;
overflow: hidden;
.mosaic-container {
flex: 1;
position: relative;
.mosaic-cell {
position: absolute;
overflow: hidden;
border: 0.5px solid var(--theme-fg);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
background: var(--theme-bg);
&.layout-portrait {
.strapi-media {
width: 40%;
aspect-ratio: 1;
}
}
&.layout-landscape {
.strapi-media {
height: 40%;
aspect-ratio: 1;
}
}
.hover-bg {
position: absolute;
inset: 0;
clip-path: circle(0% at 50% 50%);
transition: clip-path 0.3s ease-in-out;
}
.strapi-media {
opacity: 0;
position: relative;
z-index: 5;
}
&:hover {
.strapi-media {
@include bonk-animation(0s, 1s, 10deg);
}
.hover-bg {
clip-path: circle(100% at 50% 50%);
}
}
}
}
}
</style>