Skip to content

Instantly share code, notes, and snippets.

@yongjun21
Created May 28, 2019 09:04
Show Gist options
  • Save yongjun21/0ea46bdf94a20b41f94316038e63b501 to your computer and use it in GitHub Desktop.
Save yongjun21/0ea46bdf94a20b41f94316038e63b501 to your computer and use it in GitHub Desktop.
Vue component to polyfill object-fit CSS property on videos
<template>
<video class="object-fit-video" v-bind="$attrs" v-on="$listeners" :style="videoStyle">
<slot></slot>
</video>
</template>
<script>
const supportsObjectFit = window.CSS && window.CSS.supports &&
window.CSS.supports('object-fit', 'cover') &&
!/Edge/.test(window.navigator.userAgent)
export default {
props: {
objectFit: {
type: String,
default: 'cover'
},
objectPosition: {
type: String,
default: '50% 50%'
}
},
data () {
return {
videoWidth: null,
videoHeigth: null,
containerWidth: null,
containerHeight: null
}
},
computed: {
ready () {
return this.videoWidth > 0 && this.containerWidth > 0
},
parsedPosition () {
const parsed = this.objectPosition.split(' ')
if (parsed.length < 2) parsed.push('center')
if (parsed[0] === 'top' || parsed[0] === 'bottom' ||
parsed[1] === 'left' || parsed[1] === 'right') parsed.reverse()
if (parsed[0] === 'left') parsed[0] = '0%'
if (parsed[0] === 'center') parsed[0] = '50%'
if (parsed[0] === 'right') parsed[0] = '100%'
if (parsed[1] === 'top') parsed[1] = '0%'
if (parsed[1] === 'center') parsed[1] = '50%'
if (parsed[1] === 'bottom') parsed[1] = '100%'
return parsed
},
videoDimension () {
const {videoWidth, videoHeigth, containerWidth, containerHeight} = this
const containerAspectRatio = containerHeight / containerWidth
const videoAspectRatio = videoHeigth / videoWidth
switch (this.objectFit) {
case 'fill':
return {
width: containerWidth,
height: containerHeight
}
case 'contain':
return containerAspectRatio >= videoAspectRatio ? {
width: containerWidth,
height: videoAspectRatio * containerWidth
} : {
width: containerHeight && containerHeight / videoAspectRatio,
height: containerHeight
}
case 'cover':
return containerAspectRatio >= videoAspectRatio ? {
width: containerHeight && containerHeight / videoAspectRatio,
height: containerHeight
} : {
width: containerWidth,
height: videoAspectRatio * containerWidth
}
case 'scale-down':
const minWidth = Math.min(containerWidth, videoWidth)
const minHeight = Math.min(containerHeight, videoHeigth)
return containerAspectRatio >= videoAspectRatio ? {
width: minWidth,
height: videoAspectRatio * minWidth
} : {
width: minHeight && minHeight / videoAspectRatio,
height: minHeight
}
default:
return {
width: videoWidth,
height: videoHeigth
}
}
},
videoStyle () {
if (supportsObjectFit) {
return {
objectFit: this.objectFit,
objectPosition: this.objectPosition
}
}
if (!this.ready) return {visibility: 'hidden'}
return this.applyPosition(this.videoDimension)
}
},
methods: {
measure () {
this.containerWidth = this.$el.parentElement.clientWidth
this.containerHeight = this.$el.parentElement.clientHeight
},
applyPosition (dimension) {
let [marginLeft, marginTop] = this.parsedPosition
if (marginLeft[marginLeft.length - 1] === '%') {
marginLeft = +marginLeft.slice(0, -1) / 100 * (this.containerWidth - dimension.width) + 'px'
}
if (marginTop[marginTop.length - 1] === '%') {
marginTop = +marginTop.slice(0, -1) / 100 * (this.containerHeight - dimension.height) + 'px'
}
return {
width: dimension.width + 'px',
height: dimension.height + 'px',
marginLeft,
marginTop
}
}
},
mounted () {
if (supportsObjectFit) return
this.measure()
this.measure = frameRateLimited(this.measure)
window.addEventListener('resize', this.measure, {capture: true, passive: true})
this.observer = new MutationObserver(this.measure)
this.observer.observe(this.$el.parentElement, {
attributes: true,
attributeFilter: ['class', 'style']
})
this.$el.addEventListener('loadedmetadata', e => {
this.videoWidth = this.$el.videoWidth
this.videoHeigth = this.$el.videoHeight
})
},
beforeDestroy () {
if (supportsObjectFit) return
window.removeEventListener('resize', this.measure)
this.observer.disconnect()
}
}
function frameRateLimited (cb, context) {
let ready = true
function wrapped () {
if (!ready) return
ready = false
window.requestAnimationFrame(() => {
cb.apply(this, arguments)
ready = true
})
}
return context ? wrapped.bind(context) : wrapped
}
</script>
<style>
.object-fit-video {
display: block;
width: 100%;
height: 100%;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment