Files
takerofnotes-website/components/prismic/Media.vue
2026-05-29 11:22:56 -04:00

168 lines
4.0 KiB
Vue

<template>
<div
:class="[
'prismic-media',
{ 'fill-space': fillSpace },
`fit-${fit}`,
{ 'video-loaded': videoSrc && videoLoaded },
]"
:style="{ '--aspect': cmpAspect + '%' }"
>
<div class="image-sizer">
<transition :name="transition">
<img
v-show="loaded"
ref="image"
:src="imageSrc"
:srcset="srcset"
:width="width"
:height="height"
:alt="imageAlt"
@load="loaded = true"
/>
</transition>
<template v-if="videoSrc.length">
<video
v-show="videoLoaded"
:height="height"
:width="width"
:src="videoSrc"
:muted="muted"
:autoplay="autoplay"
ref="video"
playsinline
loop
@canplay="videoLoaded = true"
/>
</template>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
const props = defineProps({
image: {
type: Object,
default: () => {},
required: true,
},
video: [Object, String],
aspect: {
type: [String, Number],
default: () => -1,
},
transition: {
type: String,
default: () => 'fade',
},
fillSpace: {
type: Boolean,
default: () => false,
},
fit: {
type: String,
default: () => 'cover',
},
muted: {
type: Boolean,
default: () => true,
},
autoplay: {
type: Boolean,
default: () => true,
},
})
const image = ref()
const video = ref()
const loaded = ref(false)
const videoLoaded = ref(false)
// Set loaded to true if image is in cache
onMounted(async () => {
await new Promise((res) => setTimeout(res, 50))
if (image.value?.complete) loaded.value = true
if (video.value?.readyState >= 3) videoLoaded.value = true
})
const imageSrc = computed(() => props.image?.url || '')
const imageAlt = computed(() => props.image?.alt || '')
const videoSrc = computed(() => props.video?.url || props.video || '')
const width = computed(() => props.image?.dimensions?.width || 0)
const height = computed(() => props.image?.dimensions?.height || 0)
const cmpAspect = computed(() => {
// calculate if no aspect provided
if (props.aspect === -1) {
return (height.value / width.value) * 100
}
// otherwise, parse provided aspect, handling both 56.25 and 0.5625 style
const toParse = parseFloat(props.aspect)
return toParse <= 1 ? toParse * 100 : toParse
})
const srcset = computed(() => {
return [400, 800, 1024, 1280, 1536, 2048, 2560]
.map((size) => {
const w = size === null ? width.value : size
const h = Math.round(width.value / (cmpAspect.value / 100))
return imageSrc.value + `&w=${w} ${w}w`
})
.join(', ')
})
</script>
<style lang="scss">
.prismic-media {
position: relative;
width: 100%;
.image-sizer {
overflow: hidden;
padding-bottom: var(--aspect);
& > * {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
}
}
// fill space
&.fill-space {
position: absolute;
bottom: 0;
right: 0;
left: 0;
top: 0;
.image-sizer {
padding-bottom: 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
// fits
&.fit-cover .image-sizer > * {
object-fit: cover;
}
&.fit-contain .image-sizer > * {
object-fit: contain;
}
/* Hide background image on video load */
&.video-loaded img {
opacity: 0;
}
}
</style>