UNPKG

background-video

Version:

backgroundVideo makes your HTML5 `<video>` behave like the CSS property `background-size: cover`, making it fully responsive and scaling to aspect ratio. backgroundVideo also has parallax options.

394 lines (336 loc) 11.5 kB
/*! * backgroundVideo v2.0.1 * https://github.com/linnett/backgroundVideo * Use HTML5 video to create an effect like the CSS property, * 'background-size: cover'. Includes parallax option. * * Copyright 2016 Sam Linnett <linnettsam@gmail.com> * @license http://www.opensource.org/licenses/mit-license.html MIT License */ 'use strict'; (function(root, factory) { const pluginName = 'BackgroundVideo'; if (typeof define === 'function' && define.amd) { define([], factory(pluginName)); } else if (typeof exports === 'object') { module.exports = factory(pluginName); } else { root[pluginName] = factory(pluginName); } }((window || module || {}), function(pluginName) { /** * Default options */ const defaults = { parallax: { effect: 1.5 }, pauseVideoOnViewLoss: false, preventContextMenu: false, minimumVideoWidth: 400, // Callback functions onBeforeReady: function() {}, onReady: function() {} }; /** * Some private helper function */ const addClass = function (el, className) { if (el.classList) { el.classList.add(className); } else { el.className += ' ' + className; } }; /** * @class Plugin * * BackgroundVideo class */ class BackgroundVideo { /** * Class constructor method * * @method constructor * @params {object} options - object passed in to override default class options */ constructor(element, options) { this.element = document.querySelectorAll(element); this.options = Object.assign({}, defaults, options); // Set browser prefix option this.options.browserPrexix = this.detectBrowser(); // Ensure requestAnimationFrame is available this.shimRequestAnimationFrame(); // Detect 3d transforms this.options.has3d = this.detect3d(); // Loop through each video and init for(let i = 0; i < this.element.length; i++) { this.init(this.element[i], i); } } /** * Init the plugin * * @method init * @params element * @params {number} iteration */ init(element, iteration) { this.el = element; this.playEvent = this.videoReadyCallback.bind(this); this.setVideoWrap(iteration); this.setVideoProperties() this.insertVideos(); // Trigger beforeReady() event if (this.options && this.options.onBeforeReady()) this.options.onBeforeReady(); // If video is cached, the video will already be ready so // canplay/canplaythrough event will not fire. if (this.el.readyState > 3) { this.videoReadyCallback(); } else { // Add event listener to detect when the video can play through this.el.addEventListener('canplaythrough', this.playEvent, false); this.el.addEventListener('canplay', this.playEvent, false); } // Prevent context menu on right click for object if (this.options.preventContextMenu) { this.el.addEventListener('contextmenu', () => false); } } /** * Function is triggered when the video is ready to be played * * @method videoReadyCallback */ videoReadyCallback() { // Prevent event from being repeatedly called this.el.removeEventListener('canplaythrough', this.playEvent, false); this.el.removeEventListener('canplay', this.playEvent, false); // Set original video height and width for resize and initial calculations this.options.originalVideoW = this.el.videoWidth; this.options.originalVideoH = this.el.videoHeight; // Bind events for scroll, reize and parallax this.bindEvents(); // Request first tick this.requestTick(); // Trigger onReady() event if (this.options && this.options.onReady()) this.options.onReady(); } /** * Bind class events * * @method bindEvents */ bindEvents() { this.ticking = false; if (this.options.parallax) { window.addEventListener('scroll', this.requestTick.bind(this)); } window.addEventListener('resize', this.requestTick.bind(this)); } /** * When the user scrolls, check if !ticking and requestAnimationFrame * * @method bindEvents */ requestTick() { if (!this.ticking) { this.ticking = true; window.requestAnimationFrame(this.positionObject.bind(this)); } this.ticking = false; } /** * Position the video and apply transform styles * * @method positionObject */ positionObject() { const scrollPos = window.pageYOffset; let {xPos, yPos} = this.scaleObject(); // Check for parallax if (this.options.parallax) { // Prevent parallax when scroll position is negative to the window if (scrollPos >= 0) { yPos = this.calculateYPos(yPos, scrollPos); } else { yPos = this.calculateYPos(yPos, 0); } } else { yPos = -yPos; } const transformStyle = (this.options.has3d) ? `translate3d(${xPos}px, ${yPos}px, 0)` : `translate(${xPos}px, ${yPos}px)`; // Style with prefix this.el.style[`${this.options.browserPrexix}`] = transformStyle; // Style without prefix this.el.style.transform = transformStyle; } /** * Scale video and wrapper, ensures video stays central and maintains aspect * ratio * * @method scaleObject */ scaleObject() { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const heightScale = windowWidth / this.options.originalVideoW; const widthScale = windowHeight / this.options.originalVideoH; let scaleFactor; this.options.bvVideoWrap.style.width = `${windowWidth}px`; this.options.bvVideoWrap.style.height = `${windowHeight}px`; scaleFactor = heightScale > widthScale ? heightScale : widthScale; if (scaleFactor * this.options.originalVideoW < this.options.minimumVideoWidth) { scaleFactor = this.options.minimumVideoWidth / this.options.originalVideoW; } const videoWidth = scaleFactor * this.options.originalVideoW; const videoHeight = scaleFactor * this.options.originalVideoH; this.el.style.width = `${videoWidth}px`; this.el.style.height = `${videoHeight}px`; return { xPos: -(parseInt((videoWidth - windowWidth) / 2)), yPos: parseInt(videoHeight - windowHeight) / 2 } } calculateYPos(yPos, scrollPos) { const videoPosition = parseInt(this.options.bvVideoWrap.offsetTop); const videoOffset = videoPosition - scrollPos; yPos = -((videoOffset / this.options.parallax.effect) + yPos); return yPos; } /** * Create a container around the video tag * * @method setVideoWrap * @params {number} - iteration of video */ setVideoWrap(iteration) { const wrapper = document.createElement('div'); // Set video wrap class for later use in calculations this.options.bvVideoWrapClass = `${this.el.className}-wrap-${iteration}`; addClass(wrapper, 'bv-video-wrap'); addClass(wrapper, this.options.bvVideoWrapClass); wrapper.style.position = 'relative'; wrapper.style.overflow = 'hidden'; wrapper.style.zIndex = '10'; this.el.parentNode.insertBefore(wrapper, this.el); wrapper.appendChild(this.el); // Set wrapper element for class wide use this.options.bvVideoWrap = document.querySelector(`.${this.options.bvVideoWrapClass}`); } /** * Set attributes and styles for video * * @method setVideoProperties */ setVideoProperties() { this.el.setAttribute('preload', 'metadata'); this.el.setAttribute('loop', 'true'); this.el.setAttribute('autoplay', 'true'); this.el.style.position = 'absolute'; this.el.style.zIndex = '1'; } /** * Insert videos from `src` property defined * * @method insertVideos */ insertVideos() { for(let i = 0; i < this.options.src.length; i++) { let videoTypeArr = this.options.src[i].split('.'); let videoType = videoTypeArr[videoTypeArr.length - 1]; this.addSourceToVideo(this.options.src[i], `video/${videoType}`); } } /** * Insert videos from `src` property defined * * @method insertVideos * @params {string} src - source of the video * @params {string} type - type of video */ addSourceToVideo(src, type) { const source = document.createElement('source'); source.src = src; source.type = type; this.el.appendChild(source); } /** * Detect browser and return browser prefix for CSS * * @method detectBrowser */ detectBrowser() { const val = navigator.userAgent.toLowerCase(); let browserPrexix; if (val.indexOf('chrome') > -1 || val.indexOf('safari') > -1) { browserPrexix = 'webkitTransform'; } else if (val.indexOf('firefox') > -1) { browserPrexix = 'MozTransform'; } else if (val.indexOf('MSIE') !== -1 || val.indexOf('Trident/') > 0) { browserPrexix = 'msTransform'; } else if (val.indexOf('Opera') > -1) { browserPrexix = 'OTransform'; } return browserPrexix; } /** * Shim requestAnimationFrame to ensure it is available to all browsers * * @method shimRequestAnimationFrame */ shimRequestAnimationFrame() { /* Paul Irish rAF.js: https://gist.github.com/paulirish/1579671 */ var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } /** * Detect if 3D transforms are avilable in the browser * * @method detect3d */ detect3d() { var el = document.createElement('p'), t, has3d, transforms = { 'WebkitTransform': '-webkit-transform', 'OTransform': '-o-transform', 'MSTransform': '-ms-transform', 'MozTransform': '-moz-transform', 'transform': 'transform' }; document.body.insertBefore(el, document.body.lastChild); for (t in transforms) { if (el.style[t] !== undefined) { el.style[t] = 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'; has3d = window.getComputedStyle(el).getPropertyValue(transforms[t]); } } el.parentNode.removeChild(el); if (has3d !== undefined) { return has3d !== 'none'; } else { return false; } } } return BackgroundVideo; }));