scroll-parallax
Version:
ES6/ES2015 unobstrusive modern HW accelerated scrollable images parallax
619 lines (535 loc) • 16.5 kB
JavaScript
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define('Parallax', ['module'], factory);
} else if (typeof exports !== "undefined") {
factory(module);
} else {
var mod = {
exports: {}
};
factory(mod);
global.Parallax = mod.exports;
}
})(this, function (module) {
'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function $$(selector, ctx) {
var els = void 0;
if (typeof selector == 'string') {
els = (ctx || document).querySelectorAll(selector);
} else {
els = selector;
}
return Array.prototype.slice.call(els);
}
function extend(src) {
var obj = void 0,
args = arguments;
for (var i = 1; i < args.length; ++i) {
if (obj = args[i]) {
for (var key in obj) {
src[key] = obj[key];
}
}
}
return src;
}
function isUndefined(val) {
return typeof val == 'undefined';
}
function toCamel(string) {
return string.replace(/-(\w)/g, function (_, c) {
return c.toUpperCase();
});
}
function elementData(el, attr) {
if (attr) return el.dataset[attr] || el.getAttribute('data-' + attr);else return el.dataset || Array.prototype.slice.call(el.attributes).reduce(function (ret, attribute) {
if (/data-/.test(attribute.name)) ret[toCamel(attribute.name)] = attribute.value;
return ret;
}, {});
}
function prefix(obj, prop) {
var prefixes = ['ms', 'o', 'Moz', 'webkit'];
var i = prefixes.length;
while (i--) {
var _prefix = prefixes[i],
p = _prefix ? _prefix + prop[0].toUpperCase() + prop.substr(1) : prop.toLowerCase() + prop.substr(1);
if (p in obj) {
return p;
}
}
return '';
}
var observable = function observable(el) {
el = el || {};
var callbacks = {},
slice = Array.prototype.slice;
Object.defineProperties(el, {
on: {
value: function value(event, fn) {
if (typeof fn == 'function') (callbacks[event] = callbacks[event] || []).push(fn);
return el;
},
enumerable: false,
writable: false,
configurable: false
},
off: {
value: function value(event, fn) {
if (event == '*' && !fn) callbacks = {};else {
if (fn) {
var arr = callbacks[event];
for (var i = 0, cb; cb = arr && arr[i]; ++i) {
if (cb == fn) arr.splice(i--, 1);
}
} else delete callbacks[event];
}
return el;
},
enumerable: false,
writable: false,
configurable: false
},
one: {
value: function value(event, fn) {
function on() {
el.off(event, on);
fn.apply(el, arguments);
}
return el.on(event, on);
},
enumerable: false,
writable: false,
configurable: false
},
trigger: {
value: function value(event) {
var arglen = arguments.length - 1,
args = new Array(arglen),
fns,
fn,
i;
for (i = 0; i < arglen; i++) {
args[i] = arguments[i + 1];
}
fns = slice.call(callbacks[event] || [], 0);
for (i = 0; fn = fns[i]; ++i) {
fn.apply(el, args);
}
if (callbacks['*'] && event != '*') el.trigger.apply(el, ['*', event].concat(args));
return el;
},
enumerable: false,
writable: false,
configurable: false
}
});
return el;
};
var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function (cb) {
setTimeout(cb, 1000 / 60);
};
var RESIZE_DELAY = 20;
var Stage = function () {
function Stage() {
_classCallCheck(this, Stage);
observable(this);
this.resizeTimer = null;
this.tick = false;
this.bind();
}
_createClass(Stage, [{
key: 'bind',
value: function bind() {
var _this = this;
window.addEventListener('scroll', function () {
return _this.scroll();
}, true);
window.addEventListener('mousewheel', function () {
return _this.scroll();
}, true);
window.addEventListener('touchmove', function () {
return _this.scroll();
}, true);
window.addEventListener('resize', function () {
return _this.resize();
}, true);
window.addEventListener('orientationchange', function () {
return _this.resize();
}, true);
window.onload = function () {
return _this.scroll();
};
return this;
}
}, {
key: 'scroll',
value: function scroll() {
var _this2 = this;
if (this.tick) return this;
this.tick = !this.tick;
rAF(function () {
return _this2.update();
});
return this;
}
}, {
key: 'update',
value: function update() {
this.trigger('scroll', this.scrollTop);
this.tick = !this.tick;
return this;
}
}, {
key: 'resize',
value: function resize() {
var _this3 = this;
if (this.resizeTimer) clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(function () {
return _this3.trigger('resize', _this3.size);
}, RESIZE_DELAY);
return this;
}
}, {
key: 'scrollTop',
get: function get() {
var top = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0);
return window.isNaN(top) ? 0 : top;
}
}, {
key: 'height',
get: function get() {
return window.innerHeight;
}
}, {
key: 'width',
get: function get() {
return window.innerWidth;
}
}, {
key: 'size',
get: function get() {
return {
width: this.width,
height: this.height
};
}
}]);
return Stage;
}();
var TRANSFORM_PREFIX = function (div) {
return prefix(div.style, 'transform');
}(document.createElement('div'));
var HAS_MATRIX = function (div) {
div.style[TRANSFORM_PREFIX] = 'matrix(1, 0, 0, 1, 0, 0)';
return (/matrix/g.test(div.style.cssText)
);
}(document.createElement('div'));
var Canvas = function () {
function Canvas(element, opts) {
_classCallCheck(this, Canvas);
observable(this);
this.opts = opts;
this.element = element;
this.wrapper = element.parentNode;
this.isLoaded = false;
this.initial = element.cloneNode(true);
}
_createClass(Canvas, [{
key: 'load',
value: function load() {
var _this4 = this;
var isImage = this.element.complete !== undefined;
var isVideo = this.element.oncanplay !== undefined;
if (isImage && !this.element.complete) {
this.element.onload = function () {
return _this4.onElementLoaded();
};
} else if (isVideo && !this.element.oncanplay) {
this.element.onload = this.element.oncanplay = function () {
return _this4.onElementLoaded();
};
} else {
this.onElementLoaded();
}
return this;
}
}, {
key: 'destroy',
value: function destroy() {
this.element.parentNode.replaceChild(this.initial, this.element);
this.off('*');
}
}, {
key: 'onElementLoaded',
value: function onElementLoaded() {
this.isLoaded = true;
this.update();
this.element.style.willChange = 'transform';
this.trigger('loaded', this.element);
return this;
}
}, {
key: 'update',
value: function update() {
var iw = this.element.naturalWidth || this.element.width || this.element.offsetWidth,
ih = this.element.naturalHeight || this.element.height || this.element.offsetHeight,
ratio = iw / ih,
size = this.size;
var nh = void 0,
nw = void 0,
offsetTop = void 0,
offsetLeft = void 0;
if (size.width / ratio <= size.height) {
nw = size.height * ratio;
nh = size.height;
} else {
nw = size.width;
nh = size.width / ratio;
}
if (nh <= size.height + size.height * this.opts.safeHeight) {
nw += nw * this.opts.safeHeight;
nh += nh * this.opts.safeHeight;
}
offsetTop = -~~((nh - size.height) / 2);
offsetLeft = -~~((nw - size.width) / 2);
this.element.width = nw;
this.element.height = nh;
this.element.style.top = offsetTop + 'px';
this.element.style.left = offsetLeft + 'px';
return this;
}
}, {
key: 'draw',
value: function draw(_ref) {
var scrollTop = _ref.scrollTop,
width = _ref.width,
height = _ref.height;
var size = this.size,
perc = (this.offset.top + size.height * this.opts.center + height / 2 - scrollTop) / height - 1,
offset = ~~(perc * (this.element.height / size.height / 2 * this.opts.intensity) * 10);
this.element.style[TRANSFORM_PREFIX] = HAS_MATRIX ? 'matrix(1,0,0,1, 0, ' + -offset + ')' : 'translate(0, ' + -offset + 'px)';
return this;
}
}, {
key: 'bounds',
get: function get() {
return this.wrapper.getBoundingClientRect();
}
}, {
key: 'offset',
get: function get() {
return {
top: this.wrapper.offsetTop,
left: this.wrapper.offsetLeft
};
}
}, {
key: 'size',
get: function get() {
var props = this.bounds;
return {
height: props.height | 0,
width: props.width | 0
};
}
}]);
return Canvas;
}();
var stage = void 0;
var Parallax = function () {
function Parallax() {
var selector = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, Parallax);
observable(this);
this.opts = opts;
this.selector = selector;
this.canvases = [];
this.bound = false;
if (selector !== null) {
this.add(selector);
}
if (!stage) stage = new Stage();
return this;
}
_createClass(Parallax, [{
key: 'init',
value: function init() {
if (this.bound) {
throw 'The parallax instance has already been initialized';
}
if (!this.canvases.length && this.selector !== null) {
console.warn('No elements were found with the selector "' + this.selector + '"');
} else {
this.elementsLoaded = 0;
this.bind();
}
return this;
}
}, {
key: 'bind',
value: function bind() {
var _this5 = this;
this._onResize = function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _this5.resize.apply(_this5, args);
};
this._onScroll = function () {
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _this5.scroll.apply(_this5, args);
};
stage.on('resize', this._onResize);
stage.on('scroll', this._onScroll);
this.canvases.forEach(function (canvas) {
canvas.one('loaded', function () {
return _this5.onCanvasLoaded(canvas);
});
canvas.load();
});
this.bound = true;
return this;
}
}, {
key: 'refresh',
value: function refresh() {
this._onResize(stage.size);
this._onScroll(stage.scrollTop);
return this;
}
}, {
key: 'onCanvasLoaded',
value: function onCanvasLoaded(canvas) {
this.trigger('element:loaded', canvas.element, canvas);
this.elementsLoaded++;
canvas.draw(stage);
if (this.elementsLoaded == this.canvases.length) this.trigger('elements:loaded');
return this;
}
}, {
key: 'scroll',
value: function scroll(scrollTop) {
var offsetYBounds = this.opts.offsetYBounds,
_stage = stage,
height = _stage.height,
width = _stage.width;
var i = this.canvases.length;
while (i--) {
var canvas = this.canvases[i],
canvasHeight = canvas.size.height,
canvasOffset = canvas.offset;
if (canvas.isLoaded && scrollTop + stage.height + offsetYBounds > canvasOffset.top && canvasOffset.top + canvasHeight > scrollTop - offsetYBounds) {
canvas.draw({ height: height, scrollTop: scrollTop, width: width });
this.trigger('draw', canvas.element);
}
}
this.trigger('update', scrollTop);
return this;
}
}, {
key: 'add',
value: function add(els) {
var _this6 = this;
var canvases = this.createCanvases($$(els));
if (this.bound) {
canvases.forEach(function (canvas) {
canvas.one('loaded', function () {
return _this6.onCanvasLoaded(canvas);
});
canvas.load();
});
}
this.canvases = this.canvases.concat(canvases);
return this;
}
}, {
key: 'remove',
value: function remove(els) {
var _this7 = this;
$$(els).forEach(function (el) {
var i = _this7.canvases.length;
while (i--) {
if (el == _this7.canvases[i].element) {
_this7.canvases[i].destroy();
_this7.canvases.splice(i, 1);
break;
}
}
});
return this;
}
}, {
key: 'destroy',
value: function destroy() {
this.off('*');
this.canvases = [];
stage.off('resize', this._onResize).off('scroll', this._onScroll);
return this;
}
}, {
key: 'resize',
value: function resize(size) {
var i = this.canvases.length;
while (i--) {
var canvas = this.canvases[i];
if (!canvas.isLoaded) return;
canvas.update().draw(stage);
}
this.trigger('resize');
return this;
}
}, {
key: 'createCanvases',
value: function createCanvases(els) {
var _this8 = this;
return els.map(function (el) {
var data = elementData(el);
return new Canvas(el, {
intensity: !isUndefined(data.intensity) ? +data.intensity : _this8.opts.intensity,
center: !isUndefined(data.center) ? +data.center : _this8.opts.center,
safeHeight: !isUndefined(data.safeHeight) ? +data.safeHeight : _this8.opts.safeHeight
});
});
}
}, {
key: 'opts',
set: function set(opts) {
this._defaults = {
offsetYBounds: 50,
intensity: 30,
center: 0.5,
safeHeight: 0.15 };
extend(this._defaults, opts);
},
get: function get() {
return this._defaults;
}
}]);
return Parallax;
}();
module.exports = Parallax;
});