168 lines
4.0 KiB
Vue
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>
|