838 lines
36 KiB
JavaScript
838 lines
36 KiB
JavaScript
/* KUTE.js - The Light Tweening Engine
|
|
* by dnp_theme
|
|
* Licensed under MIT-License
|
|
*/
|
|
(((root, factory) => {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define([], factory); // AMD. Register as an anonymous module.
|
|
} else if (typeof exports == 'object') {
|
|
module.exports = factory(); // Node, not strict CommonJS
|
|
} else {
|
|
root.KUTE = factory();
|
|
}
|
|
})(this, () => {
|
|
// set a custom scope for KUTE.js
|
|
const g = typeof global !== 'undefined' ? global : window; // tick must be null!!
|
|
|
|
const time = g.performance;
|
|
let tweens = [];
|
|
let tick = null;
|
|
|
|
//supported properties
|
|
const // colors 'hex', 'rgb', 'rgba' -- #fff / rgb(0,0,0) / rgba(0,0,0,0)
|
|
_colors = ['color', 'backgroundColor']; //all properties default values
|
|
|
|
const // dimensions / box model
|
|
_boxModel = ['top', 'left', 'width', 'height'];
|
|
|
|
const // transform
|
|
_transform = ['translate3d', 'translateX', 'translateY', 'translateZ', 'rotate', 'translate', 'rotateX', 'rotateY', 'rotateZ', 'skewX', 'skewY', 'scale'];
|
|
|
|
const //scroll, it has no default value, it's calculated on tween start
|
|
_scroll = ['scroll'];
|
|
|
|
const // opacity
|
|
_opacity = ['opacity'];
|
|
|
|
const _all = _colors.concat( _opacity, _boxModel, _transform);
|
|
const al = _all.length;
|
|
const _defaults = {};
|
|
|
|
//populate default values object
|
|
for ( let i=0; i<al; i++ ){
|
|
let p = _all[i];
|
|
if (_colors.includes(p)){
|
|
_defaults[p] = 'rgba(0,0,0,0)'; // _defaults[p] = {r:0,g:0,b:0,a:1}; // no unit/suffix
|
|
} else if ( _boxModel.includes(p) ) {
|
|
_defaults[p] = 0;
|
|
} else if ( p === 'translate3d' ){ // px
|
|
_defaults[p] = [0,0,0];
|
|
} else if ( p === 'translate' ){ // px
|
|
_defaults[p] = [0,0];
|
|
} else if ( p === 'rotate' || /X|Y|Z/.test(p) ){ // deg
|
|
_defaults[p] = 0;
|
|
} else if ( p === 'scale' || p === 'opacity' ){ // unitless
|
|
_defaults[p] = 1;
|
|
}
|
|
p = null;
|
|
}
|
|
|
|
// default tween options, since 1.6.1
|
|
const defaultOptions = {
|
|
duration: 700,
|
|
delay: 0,
|
|
offset: 0,
|
|
repeat: 0,
|
|
repeatDelay: 0,
|
|
yoyo: false,
|
|
easing: 'linear',
|
|
keepHex: false,
|
|
}; // we optimize morph depending on device type
|
|
|
|
const // tools / utils
|
|
getPrefix = () => {
|
|
//returns browser prefix
|
|
let div = document.createElement('div');
|
|
|
|
var i = 0;
|
|
const pf = ['Moz', 'moz', 'Webkit', 'webkit', 'O', 'o', 'Ms', 'ms'];
|
|
const s = ['MozTransform', 'mozTransform', 'WebkitTransform', 'webkitTransform', 'OTransform', 'oTransform', 'MsTransform', 'msTransform'];
|
|
for (const i = 0, pl = pf.length; i < pl; i++) { if (s[i] in div.style) { return pf[i]; } }
|
|
div = null;
|
|
};
|
|
|
|
const property = p => { // returns prefixed property | property
|
|
const r = (!(p in document.body.style)) ? true : false, f = getPrefix(); // is prefix required for property | prefix
|
|
return r ? f + (p.charAt(0).toUpperCase() + p.slice(1)) : p;
|
|
};
|
|
|
|
const selector = (el, multi) => { // a public selector utility
|
|
let nl;
|
|
if (multi){
|
|
nl = el instanceof Object || typeof el === 'object' ? el : document.querySelectorAll(el);
|
|
} else {
|
|
nl = typeof el === 'object' ? el
|
|
: /^#/.test(el) ? document.getElementById(el.replace('#','')) : document.querySelector(el);
|
|
}
|
|
if (nl === null && el !== 'window') throw new TypeError(`Element not found or incorrect selector: ${el}`);
|
|
return nl;
|
|
};
|
|
|
|
const radToDeg = a => a*180/Math.PI;
|
|
|
|
const trueDimension = (d, p) => {
|
|
//true dimension returns { v = value, u = unit }
|
|
const x = parseInt(d) || 0;
|
|
|
|
const mu = ['px','%','deg','rad','em','rem','vh','vw'];
|
|
let y;
|
|
for (let i=0, l = mu.length; i<l; i++) { if ( typeof d === 'string' && d.includes(mu[i]) ) { y = mu[i]; break; } }
|
|
y = y !== undefined ? y : (p ? 'deg' : 'px');
|
|
return { v: x, u: y };
|
|
};
|
|
|
|
const trueColor = v => { // replace transparent and transform any color to rgba()/rgb()
|
|
if (/rgb|rgba/.test(v)) { // first check if it's a rgb string
|
|
const vrgb = v.replace(/\s|\)/,'').split('(')[1].split(','), y = vrgb[3] ? vrgb[3] : null;
|
|
if (!y) {
|
|
return { r: parseInt(vrgb[0]), g: parseInt(vrgb[1]), b: parseInt(vrgb[2]) };
|
|
} else {
|
|
return { r: parseInt(vrgb[0]), g: parseInt(vrgb[1]), b: parseInt(vrgb[2]), a: parseFloat(y) };
|
|
}
|
|
} else if (/^#/.test(v)) {
|
|
const fromHex = hexToRGB(v); return { r: fromHex.r, g: fromHex.g, b: fromHex.b };
|
|
} else if (/transparent|none|initial|inherit/.test(v)) {
|
|
return { r: 0, g: 0, b: 0, a: 0 };
|
|
} else if (!/^#|^rgb/.test(v) ) { // maybe we can check for web safe colors
|
|
const h = document.getElementsByTagName('head')[0]; h.style.color = v;
|
|
let webColor = g.getComputedStyle(h,null).color; webColor = /rgb/.test(webColor) ? webColor.replace(/[^\d,]/g, '').split(',') : [0,0,0];
|
|
h.style.color = ''; return { r: parseInt(webColor[0]), g: parseInt(webColor[1]), b: parseInt(webColor[2]) };
|
|
}
|
|
};
|
|
|
|
const rgbToHex = (r, g, b) => // transform rgb to hex or vice-versa | webkit browsers ignore HEX, always use RGB/RGBA
|
|
`#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
|
|
const hexToRGB = hex => {
|
|
const shr = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
|
hex = hex.replace(shr, (m, r, g, b) => r + r + g + g + b + b);
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
return result ? {
|
|
r: parseInt(result[1], 16),
|
|
g: parseInt(result[2], 16),
|
|
b: parseInt(result[3], 16)
|
|
} : null;
|
|
};
|
|
|
|
const getInlineStyle = (el, p) => { // get transform style for element from cssText for .to() method, the sp is for transform property
|
|
if (!el) return; // if the scroll applies to `window` it returns as it has no styling
|
|
const //the cssText
|
|
css = el.style.cssText.replace(/\s/g,'').split(';'),
|
|
trsf = {}; //the transform object
|
|
// if we have any inline style in the cssText attribute, usually it has higher priority
|
|
for ( let i=0, csl = css.length; i<csl; i++ ){
|
|
if ( /transform/i.test(css[i])) {
|
|
const tps = css[i].split(':')[1].split(')'); //all transform properties
|
|
for ( let k=0, tpl = tps.length-1; k< tpl; k++){
|
|
const tpv = tps[k].split('('), tp = tpv[0], tv = tpv[1]; //each transform property
|
|
if ( _transform.includes(tp) ){
|
|
trsf[tp] = /translate3d/.test(tp) ? tv.split(',') : tv;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return trsf;
|
|
};
|
|
|
|
const getCurrentStyle = (el, p) => { // get computed style property for element for .to() method
|
|
const styleAttribute = el.style,
|
|
computedStyle = g.getComputedStyle(el,null) || el.currentStyle,
|
|
//the computed style | prefixed property
|
|
pp = property(p),
|
|
styleValue = styleAttribute[p] && !/auto|initial|none|unset/.test(styleAttribute[p]) ? styleAttribute[p] : computedStyle[pp]; // s the property style value
|
|
if ( p !== 'transform' && (pp in computedStyle || pp in styleAttribute) ) {
|
|
if ( styleValue ){
|
|
if (pp==='filter') { // handle IE8 opacity
|
|
const filterValue = parseInt(styleValue.split('=')[1].replace(')',''));
|
|
return parseFloat(filterValue/100);
|
|
} else {
|
|
return styleValue;
|
|
}
|
|
} else {
|
|
return _defaults[p];
|
|
}
|
|
}
|
|
};
|
|
|
|
const //more internals
|
|
getAll = () => tweens;
|
|
|
|
const removeAll = () => { tweens = []; };
|
|
const add = tw => { tweens.push(tw); };
|
|
const remove = tw => { const i = tweens.indexOf(tw); if (i !== -1) { tweens.splice(i, 1); }};
|
|
const stop = () => { if (tick) { _cancelAnimationFrame(tick); tick = null; } };
|
|
|
|
const // support Touch?
|
|
canTouch = ('ontouchstart' in g || navigator && navigator.msMaxTouchPoints) || false;
|
|
|
|
const touchOrWheel = canTouch ? 'touchstart' : 'mousewheel';
|
|
|
|
const //events to prevent on scroll
|
|
mouseEnter = 'mouseenter';
|
|
|
|
const _requestAnimationFrame = g.requestAnimationFrame || g.webkitRequestAnimationFrame || (c => setTimeout(c, 16));
|
|
const _cancelAnimationFrame = g.cancelAnimationFrame || g.webkitCancelRequestAnimationFrame || (c => clearTimeout(c));
|
|
const transformProperty = property('transform');
|
|
|
|
const //true scroll container
|
|
body = document.body;
|
|
|
|
const html = document.getElementsByTagName('HTML')[0];
|
|
const scrollContainer = navigator && /webkit/i.test(navigator.userAgent) || document.compatMode == 'BackCompat' ? body : html;
|
|
const isIE = navigator && (new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})").exec(navigator.userAgent) !== null) ? parseFloat( RegExp.$1 ) : false;
|
|
|
|
const // check IE8/IE
|
|
isIE8 = isIE === 8;
|
|
|
|
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
|
|
// KUTE.js INTERPOLATORS
|
|
const interpolate = g.Interpolate = {};
|
|
|
|
// core easing functions
|
|
|
|
const number = interpolate.number = (a, b, v) => { // number1, number2, progress
|
|
a = +a; b -= a; return a + b * v;
|
|
};
|
|
|
|
const unit = interpolate.unit = (a, b, u, v) => { // number1, number2, unit, progress
|
|
a = +a; b -= a; return ( a + b * v ) + u;
|
|
};
|
|
|
|
const color = interpolate.color = (a, b, v, h) => {
|
|
// rgba1, rgba2, progress, convertToHex(true/false)
|
|
const _c = {};
|
|
|
|
let c;
|
|
const n = number;
|
|
const ep = ')';
|
|
const cm =',';
|
|
const r = 'rgb(';
|
|
const ra = 'rgba(';
|
|
for (c in b) { _c[c] = c !== 'a' ? (number(a[c],b[c],v)>>0 || 0) : (a[c] && b[c]) ? (number(a[c],b[c],v) * 100 >> 0 )/100 : null; }
|
|
return h ? rgbToHex( _c.r, _c.g, _c.b ) : !_c.a ? r + _c.r + cm + _c.g + cm + _c.b + ep : ra + _c.r + cm + _c.g + cm + _c.b + cm + _c.a + ep;
|
|
};
|
|
|
|
const translate = interpolate.translate = isMobile ? (a, b, u, v) => {
|
|
const translation = {};
|
|
for (const ax in b){
|
|
translation[ax] = ( a[ax]===b[ax] ? b[ax] : (a[ax] + ( b[ax] - a[ax] ) * v ) >> 0 ) + u;
|
|
}
|
|
return translation.x||translation.y ? `translate(${translation.x},${translation.y})` :
|
|
`translate3d(${translation.translateX},${translation.translateY},${translation.translateZ})`;
|
|
} : (a, b, u, v) => {
|
|
const translation = {};
|
|
for (const ax in b){
|
|
translation[ax] = ( a[ax]===b[ax] ? b[ax] : ( (a[ax] + ( b[ax] - a[ax] ) * v ) * 100 >> 0 ) / 100 ) + u;
|
|
}
|
|
return translation.x||translation.y ? `translate(${translation.x},${translation.y})` :
|
|
`translate3d(${translation.translateX},${translation.translateY},${translation.translateZ})`;
|
|
};
|
|
|
|
const rotate = interpolate.rotate = (a, b, u, v) => {
|
|
const rotation = {};
|
|
for ( const rx in b ){
|
|
rotation[rx] = rx === 'z' ? (`rotate(${((a[rx] + (b[rx] - a[rx]) * v) * 100 >> 0 ) / 100}${u})`)
|
|
: (`${rx}(${((a[rx] + (b[rx] - a[rx]) * v) * 100 >> 0 ) / 100}${u})`);
|
|
}
|
|
return rotation.z ? rotation.z : (rotation.rotateX||'') + (rotation.rotateY||'') + (rotation.rotateZ||'');
|
|
};
|
|
|
|
const skew = interpolate.skew = (a, b, u, v) => {
|
|
const skewProp = {};
|
|
for ( const sx in b ){
|
|
skewProp[sx] = `${sx}(${((a[sx] + (b[sx] - a[sx]) * v) * 10 >> 0) / 10}${u})`;
|
|
}
|
|
return (skewProp.skewX||'') + (skewProp.skewY||'');
|
|
};
|
|
|
|
const scale = interpolate.scale = (a, b, v) => `scale(${((a + (b - a) * v) * 1000 >> 0 ) / 1000})`;
|
|
|
|
const // KUTE.js DOM update functions
|
|
DOM = {};
|
|
|
|
const ticker = t => {
|
|
let i = 0;
|
|
while ( i < tweens.length ) {
|
|
if ( update.call(tweens[i],t) ) {
|
|
i++;
|
|
} else {
|
|
tweens.splice(i, 1);
|
|
}
|
|
}
|
|
tick = _requestAnimationFrame(ticker);
|
|
};
|
|
|
|
const update = function(t=time.now()) {
|
|
if ( t < this._startTime && this.playing ) { return true; }
|
|
|
|
const elapsed = Math.min(( t - this._startTime ) / this.options.duration, 1), progress = this.options.easing(elapsed); // calculate progress
|
|
|
|
for (const p in this.valuesEnd){ DOM[p](this.element,p,this.valuesStart[p],this.valuesEnd[p],progress,this.options); } //render the CSS update
|
|
|
|
if (this.options.update) { this.options.update.call(); } // fire the updateCallback
|
|
|
|
if (elapsed === 1) {
|
|
if (this.options.repeat > 0) {
|
|
if ( isFinite(this.options.repeat ) ) { this.options.repeat--; }
|
|
|
|
if (this.options.yoyo) { // handle yoyo
|
|
this.reversed = !this.reversed;
|
|
reverse.call(this);
|
|
}
|
|
|
|
this._startTime = (this.options.yoyo && !this.reversed) ? t + this.options.repeatDelay : t; //set the right time for delay
|
|
return true;
|
|
} else {
|
|
|
|
if (this.options.complete) { this.options.complete.call(); }
|
|
|
|
scrollOut.call(this); // unbind preventing scroll when scroll tween finished
|
|
|
|
for (let i = 0, ctl = this.options.chain.length; i < ctl; i++) { // start animating chained tweens
|
|
this.options.chain[i].start();
|
|
}
|
|
|
|
//stop ticking when finished
|
|
close.call(this);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const // applies the transform origin and perspective
|
|
perspective = function () {
|
|
const el = this.element, ops = this.options;
|
|
if ( ops.perspective !== undefined && transformProperty in this.valuesEnd ) { // element perspective
|
|
this.valuesStart[transformProperty]['perspective'] = this.valuesEnd[transformProperty]['perspective'];
|
|
}
|
|
// element transform origin / we filter it out for svgTransform to fix the Firefox transformOrigin bug https://bugzilla.mozilla.org/show_bug.cgi?id=923193
|
|
if ( ops.transformOrigin !== undefined && (!('svgTransform' in this.valuesEnd)) ) { el.style[property('transformOrigin')] = ops.transformOrigin; } // set transformOrigin for CSS3 transforms only
|
|
if ( ops.perspectiveOrigin !== undefined ) { el.style[property('perspectiveOrigin')] = ops.perspectiveOrigin; } // element perspective origin
|
|
if ( ops.parentPerspective !== undefined ) { el.parentNode.style[property('perspective')] = `${ops.parentPerspective}px`; } // parent perspective
|
|
if ( ops.parentPerspectiveOrigin !== undefined ) { el.parentNode.style[property('perspectiveOrigin')] = ops.parentPerspectiveOrigin; } // parent perspective origin
|
|
};
|
|
|
|
const // plugin connector objects
|
|
// check current property value when .to() method is used
|
|
prepareStart = {};
|
|
|
|
const // checks for differences between start and end value, try to make sure start unit and end unit are same as well as consistent, stack transforms, process SVG paths
|
|
crossCheck = {};
|
|
|
|
const // parse properties object
|
|
// string parsing and property specific value processing
|
|
parseProperty = { // we already start working on core supported properties
|
|
boxModel(p, v) {
|
|
if (!(p in DOM)){
|
|
DOM[p] = (l, p, a, b, v) => {
|
|
l.style[p] = `${v > 0.99 || v < 0.01 ? ((number(a,b,v)*10)>>0)/10 : (number(a,b,v) ) >> 0}px`;
|
|
}
|
|
}
|
|
const boxValue = trueDimension(v);
|
|
return boxValue.u === '%' ? boxValue.v * this.element.offsetWidth / 100 : boxValue.v;
|
|
},
|
|
transform(p, v) {
|
|
if (!(transformProperty in DOM)) {
|
|
DOM[transformProperty] = (l, p, a, b, v, o) => {
|
|
l.style[p] = (a.perspective||'')
|
|
+ ('translate' in a ? translate(a.translate,b.translate,'px',v):'')
|
|
+ ('rotate' in a ? rotate(a.rotate,b.rotate,'deg',v):'')
|
|
+ ('skew' in a ? skew(a.skew,b.skew,'deg',v):'')
|
|
+ ('scale' in a ? scale(a.scale,b.scale,v):'');
|
|
}
|
|
}
|
|
|
|
// process each transform property
|
|
if (/translate/.test(p)) {
|
|
if (p === 'translate3d') {
|
|
const t3d = v.split(','), t3d0 = trueDimension(t3d[0]), t3d1 = trueDimension(t3d[1], t3d2 = trueDimension(t3d[2]));
|
|
return {
|
|
translateX : t3d0.u === '%' ? (t3d0.v * this.element.offsetWidth / 100) : t3d0.v,
|
|
translateY : t3d1.u === '%' ? (t3d1.v * this.element.offsetHeight / 100) : t3d1.v,
|
|
translateZ : t3d2.u === '%' ? (t3d2.v * (this.element.offsetHeight + this.element.offsetWidth) / 200) : t3d2.v // to be changed with something like element and/or parent perspective
|
|
};
|
|
} else if (/^translate(?:[XYZ])$/.test(p)) {
|
|
const t1d = trueDimension(v), percentOffset = /X/.test(p) ? this.element.offsetWidth / 100 : /Y/.test(p) ? this.element.offsetHeight / 100 : (this.element.offsetWidth+this.element.offsetHeight) / 200;
|
|
|
|
return t1d.u === '%' ? (t1d.v * percentOffset) : t1d.v;
|
|
} else if (p === 'translate') {
|
|
const tv = typeof v === 'string' ? v.split(',') : v;
|
|
const t2d = {};
|
|
let t2dv;
|
|
const t2d0 = trueDimension(tv[0]);
|
|
const t2d1 = tv.length ? trueDimension(tv[1]) : {v: 0, u: 'px'};
|
|
if (tv instanceof Array) {
|
|
t2d.x = t2d0.u === '%' ? (t2d0.v * this.element.offsetWidth / 100) : t2d0.v,
|
|
t2d.y = t2d1.u === '%' ? (t2d1.v * this.element.offsetHeight / 100) : t2d1.v
|
|
} else {
|
|
t2dv = trueDimension(tv);
|
|
t2d.x = t2dv.u === '%' ? (t2dv.v * this.element.offsetWidth / 100) : t2dv.v,
|
|
t2d.y = 0
|
|
}
|
|
|
|
return t2d;
|
|
}
|
|
} else if (/rotate|skew/.test(p)) {
|
|
if (/^rotate(?:[XYZ])$|skew(?:[XY])$/.test(p)) {
|
|
const r3d = trueDimension(v,true);
|
|
return r3d.u === 'rad' ? radToDeg(r3d.v) : r3d.v;
|
|
} else if (p === 'rotate') {
|
|
const r2d = {}, r2dv = trueDimension(v,true);
|
|
r2d.z = r2dv.u === 'rad' ? radToDeg(r2dv.v) : r2dv.v;
|
|
return r2d;
|
|
}
|
|
} else if (p === 'scale') {
|
|
return parseFloat(v); // this must be parseFloat(v)
|
|
}
|
|
},
|
|
unitless(p, v) { // scroll | opacity
|
|
if (/scroll/.test(p) && !(p in DOM) ){
|
|
DOM[p] = (l, p, a, b, v) => {
|
|
l.scrollTop = (number(a,b,v))>>0;
|
|
};
|
|
} else if (p === 'opacity') {
|
|
if (!(p in DOM)) {
|
|
if (isIE8) {
|
|
DOM[p] = (l, p, a, b, v) => {
|
|
const st = "alpha(opacity=", ep = ')';
|
|
l.style.filter = st + ((number(a,b,v) * 100)>>0) + ep;
|
|
};
|
|
} else {
|
|
DOM[p] = (l, p, a, b, v) => {
|
|
l.style.opacity = ((number(a,b,v) * 100)>>0)/100;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return parseFloat(v);
|
|
},
|
|
colors(p, v) { // colors
|
|
if (!(p in DOM)) {
|
|
DOM[p] = (l, p, a, b, v, o) => {
|
|
l.style[p] = color(a,b,v,o.keepHex);
|
|
};
|
|
}
|
|
return trueColor(v);
|
|
}
|
|
};
|
|
|
|
const // process properties for endValues and startValues or one of them
|
|
preparePropertiesObject = function(obj, fn) { // this, props object, type: start/end
|
|
const element = this.element, propertiesObject = fn === 'start' ? this.valuesStart : this.valuesEnd, skewObject = {}, rotateObject = {}, translateObject = {}, transformObject = {};
|
|
|
|
for (const x in obj) {
|
|
if (_transform.includes(x)) { // transform object gets built here
|
|
if ( /^translate(?:[XYZ]|3d)$/.test(x) ) { //process translate3d
|
|
const ta = ['X', 'Y', 'Z']; //coordinates // translate[x] = pp(x, obj[x]);
|
|
|
|
for (let f = 0; f < 3; f++) {
|
|
const a = ta[f];
|
|
if ( /3d/.test(x) ) {
|
|
translateObject[`translate${a}`] = parseProperty.transform.call(this,`translate${a}`, obj[x][f]);
|
|
} else {
|
|
translateObject[`translate${a}`] = (`translate${a}` in obj) ? parseProperty.transform.call(this,`translate${a}`, obj[`translate${a}`]) : 0;
|
|
}
|
|
}
|
|
transformObject['translate'] = translateObject;
|
|
} else if ( /^rotate(?:[XYZ])$|^skew(?:[XY])$/.test(x) ) { //process rotation/skew
|
|
const ap = /rotate/.test(x) ? 'rotate' : 'skew', ra = ['X', 'Y', 'Z'], rtp = ap === 'rotate' ? rotateObject : skewObject;
|
|
for (let r = 0; r < 3; r++) {
|
|
const v = ra[r];
|
|
if ( obj[ap+v] !== undefined && x !== 'skewZ' ) {
|
|
rtp[ap+v] = parseProperty.transform.call(this,ap+v, obj[ap+v]);
|
|
}
|
|
}
|
|
transformObject[ap] = rtp;
|
|
} else if ( /(rotate|translate|scale)$/.test(x) ) { //process 2d translation / rotation
|
|
transformObject[x] = parseProperty.transform.call(this, x, obj[x]);
|
|
}
|
|
propertiesObject[transformProperty] = transformObject;
|
|
} else {
|
|
if ( _boxModel.includes(x) ) {
|
|
propertiesObject[x] = parseProperty.boxModel.call(this,x,obj[x]);
|
|
} else if (_opacity.includes(x) || x === 'scroll') {
|
|
propertiesObject[x] = parseProperty.unitless.call(this,x,obj[x]);
|
|
} else if (_colors.includes(x)) {
|
|
propertiesObject[x] = parseProperty.colors.call(this,x,obj[x]);
|
|
} else if (x in parseProperty) { // or any other property from css/ attr / svg / third party plugins
|
|
propertiesObject[x] = parseProperty[x].call(this,x,obj[x]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const reverse = function () {
|
|
if (this.options.yoyo) {
|
|
for (const p in this.valuesEnd) {
|
|
const tmp = this.valuesRepeat[p];
|
|
this.valuesRepeat[p] = this.valuesEnd[p];
|
|
this.valuesEnd[p] = tmp;
|
|
this.valuesStart[p] = this.valuesRepeat[p];
|
|
}
|
|
}
|
|
};
|
|
|
|
const close = function () { // when animation is finished reset repeat, yoyo&reversed tweens
|
|
if (this.repeat > 0) { this.options.repeat = this.repeat; }
|
|
if (this.options.yoyo && this.reversed===true) { reverse.call(this); this.reversed = false; }
|
|
this.playing = false;
|
|
|
|
setTimeout(() => { if (!tweens.length) { stop(); } }, 48); // when all animations are finished, stop ticking after ~3 frames
|
|
};
|
|
|
|
const preventScroll = e => { // prevent mousewheel or touch events while tweening scroll
|
|
const data = document.body.getAttribute('data-tweening');
|
|
if (data && data === 'scroll') { e.preventDefault(); }
|
|
};
|
|
|
|
const scrollOut = function(){ //prevent scroll when tweening scroll
|
|
if ( 'scroll' in this.valuesEnd && document.body.getAttribute('data-tweening')) {
|
|
document.removeEventListener(touchOrWheel, preventScroll, false);
|
|
document.removeEventListener(mouseEnter, preventScroll, false);
|
|
document.body.removeAttribute('data-tweening');
|
|
}
|
|
};
|
|
|
|
const scrollIn = function(){
|
|
if ( 'scroll' in this.valuesEnd && !document.body.getAttribute('data-tweening')) {
|
|
document.addEventListener(touchOrWheel, preventScroll, false);
|
|
document.addEventListener(mouseEnter, preventScroll, false);
|
|
document.body.setAttribute('data-tweening', 'scroll');
|
|
}
|
|
};
|
|
|
|
const processEasing = fn => { //process easing function
|
|
if ( typeof fn === 'function') {
|
|
return fn;
|
|
} else if ( typeof fn === 'string' ) {
|
|
return easing[fn]; // regular Robert Penner Easing Functions
|
|
}
|
|
};
|
|
|
|
const getStartValues = function () { // stack transform props for .to() chains
|
|
const startValues = {}, currentStyle = getInlineStyle(this.element,'transform'), deg = ['rotate','skew'], ax = ['X','Y','Z'];
|
|
|
|
for (var p in this.valuesStart){
|
|
if ( _transform.includes(p) ) {
|
|
const r2d = (/(rotate|translate|scale)$/.test(p));
|
|
if ( /translate/.test(p) && p !== 'translate' ) {
|
|
startValues['translate3d'] = currentStyle['translate3d'] || _defaults[p];
|
|
} else if ( r2d ) { // 2d transforms
|
|
startValues[p] = currentStyle[p] || _defaults[p];
|
|
} else if ( !r2d && /rotate|skew/.test(p) ) { // all angles
|
|
for (var d=0; d<2; d++) {
|
|
for (let a = 0; a<3; a++) {
|
|
const s = deg[d]+ax[a];
|
|
if (_transform.includes(s) && (s in this.valuesStart) ) { startValues[s] = currentStyle[s] || _defaults[s]; }
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if ( p !== 'scroll' ) {
|
|
if (p === 'opacity' && isIE8 ) { // handle IE8 opacity
|
|
const currentOpacity = getCurrentStyle(this.element,'filter');
|
|
startValues['opacity'] = typeof currentOpacity === 'number' ? currentOpacity : _defaults['opacity'];
|
|
} else {
|
|
if ( _all.includes(p) ) {
|
|
startValues[p] = getCurrentStyle(this.element,p) || d[p];
|
|
} else { // plugins register here
|
|
startValues[p] = p in prepareStart ? prepareStart[p].call(this,p,this.valuesStart[p]) : 0;
|
|
}
|
|
}
|
|
} else {
|
|
startValues[p] = this.element === scrollContainer ? (g.pageYOffset || scrollContainer.scrollTop) : this.element.scrollTop;
|
|
}
|
|
}
|
|
}
|
|
for ( var p in currentStyle ){ // also add to startValues values from previous tweens
|
|
if ( _transform.includes(p) && (!( p in this.valuesStart )) ) {
|
|
startValues[p] = currentStyle[p] || _defaults[p];
|
|
}
|
|
}
|
|
|
|
this.valuesStart = {};
|
|
preparePropertiesObject.call(this,startValues,'start');
|
|
|
|
if ( transformProperty in this.valuesEnd ) { // let's stack transform
|
|
for ( const sp in this.valuesStart[transformProperty]) { // sp is the object corresponding to the transform function objects translate / rotate / skew / scale
|
|
if ( sp !== 'perspective') {
|
|
if ( typeof this.valuesStart[transformProperty][sp] === 'object' ) {
|
|
for ( const spp in this.valuesStart[transformProperty][sp] ) { // 3rd level
|
|
if ( typeof this.valuesEnd[transformProperty][sp] === 'undefined' ) { this.valuesEnd[transformProperty][sp] = {}; }
|
|
if ( typeof this.valuesStart[transformProperty][sp][spp] === 'number' && typeof this.valuesEnd[transformProperty][sp][spp] === 'undefined' ) {
|
|
this.valuesEnd[transformProperty][sp][spp] = this.valuesStart[transformProperty][sp][spp];
|
|
}
|
|
}
|
|
} else if ( typeof this.valuesStart[transformProperty][sp] === 'number' ) {
|
|
if ( typeof this.valuesEnd[transformProperty][sp] === 'undefined' ) { // scale
|
|
this.valuesEnd[transformProperty][sp] = this.valuesStart[transformProperty][sp];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var easing = g.Easing = {};
|
|
easing.linear = t => t;
|
|
easing.easingSinusoidalIn = t => -Math.cos(t * Math.PI / 2) + 1;
|
|
easing.easingSinusoidalOut = t => Math.sin(t * Math.PI / 2);
|
|
easing.easingSinusoidalInOut = t => -0.5 * (Math.cos(Math.PI * t) - 1);
|
|
easing.easingQuadraticIn = t => t*t;
|
|
easing.easingQuadraticOut = t => t*(2-t);
|
|
easing.easingQuadraticInOut = t => t<.5 ? 2*t*t : -1+(4-2*t)*t;
|
|
easing.easingCubicIn = t => t*t*t;
|
|
easing.easingCubicOut = t => (--t)*t*t+1;
|
|
easing.easingCubicInOut = t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1;
|
|
easing.easingQuarticIn = t => t*t*t*t;
|
|
easing.easingQuarticOut = t => 1-(--t)*t*t*t;
|
|
easing.easingQuarticInOut = t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t;
|
|
easing.easingQuinticIn = t => t*t*t*t*t;
|
|
easing.easingQuinticOut = t => 1+(--t)*t*t*t*t;
|
|
easing.easingQuinticInOut = t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t;
|
|
easing.easingCircularIn = t => -(Math.sqrt(1 - (t * t)) - 1);
|
|
easing.easingCircularOut = t => Math.sqrt(1 - (t = t - 1) * t);
|
|
easing.easingCircularInOut = t => ((t*=2) < 1) ? -0.5 * (Math.sqrt(1 - t * t) - 1) : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1);
|
|
easing.easingExponentialIn = t => 2 ** (10 * (t - 1)) - 0.001;
|
|
easing.easingExponentialOut = t => 1 - 2 ** (-10 * t);
|
|
easing.easingExponentialInOut = t => (t *= 2) < 1 ? 0.5 * (2 ** (10 * (t - 1))) : 0.5 * (2 - 2 ** (-10 * (t - 1)));
|
|
easing.easingBackIn = t => { const s = 1.70158; return t * t * ((s + 1) * t - s); };
|
|
easing.easingBackOut = t => { const s = 1.70158; return --t * t * ((s + 1) * t + s) + 1; };
|
|
easing.easingBackInOut = t => { const s = 1.70158 * 1.525; if ((t *= 2) < 1) return 0.5 * (t * t * ((s + 1) * t - s)); return 0.5 * ((t -= 2) * t * ((s + 1) * t + s) + 2); };
|
|
easing.easingElasticIn = t => {
|
|
let s;
|
|
let _kea = 0.1;
|
|
const _kep = 0.4;
|
|
if ( t === 0 ) return 0;if ( t === 1 ) return 1;
|
|
if ( !_kea || _kea < 1 ) { _kea = 1; s = _kep / 4; } else s = _kep * Math.asin( 1 / _kea ) / Math.PI * 2;
|
|
return - ( _kea * (2 ** (10 * (t -= 1))) * Math.sin( ( t - s ) * Math.PI * 2 / _kep ) );
|
|
};
|
|
easing.easingElasticOut = t => {
|
|
let s;
|
|
let _kea = 0.1;
|
|
const _kep = 0.4;
|
|
if ( t === 0 ) return 0;if ( t === 1 ) return 1;
|
|
if ( !_kea || _kea < 1 ) { _kea = 1; s = _kep / 4; } else s = _kep * Math.asin( 1 / _kea ) / Math.PI * 2 ;
|
|
return ( _kea * (2 ** (- 10 * t)) * Math.sin( ( t - s ) * Math.PI * 2 / _kep ) + 1 );
|
|
};
|
|
easing.easingElasticInOut = t => {
|
|
let s;
|
|
let _kea = 0.1;
|
|
const _kep = 0.4;
|
|
if ( t === 0 ) return 0;if ( t === 1 ) return 1;
|
|
if ( !_kea || _kea < 1 ) { _kea = 1; s = _kep / 4; } else s = _kep * Math.asin( 1 / _kea ) / Math.PI * 2 ;
|
|
if ( ( t *= 2 ) < 1 ) return - 0.5 * ( _kea * (2 ** (10 * (t -= 1))) * Math.sin( ( t - s ) * Math.PI * 2 / _kep ) );
|
|
return _kea * (2 ** (-10 * (t -= 1))) * Math.sin( ( t - s ) * Math.PI * 2 / _kep ) * 0.5 + 1;
|
|
};
|
|
easing.easingBounceIn = t => 1 - easing.easingBounceOut( 1 - t );
|
|
easing.easingBounceOut = t => {
|
|
if ( t < ( 1 / 2.75 ) ) { return 7.5625 * t * t; }
|
|
else if ( t < ( 2 / 2.75 ) ) { return 7.5625 * ( t -= ( 1.5 / 2.75 ) ) * t + 0.75; }
|
|
else if ( t < ( 2.5 / 2.75 ) ) { return 7.5625 * ( t -= ( 2.25 / 2.75 ) ) * t + 0.9375; }
|
|
else {return 7.5625 * ( t -= ( 2.625 / 2.75 ) ) * t + 0.984375; }
|
|
};
|
|
easing.easingBounceInOut = t => { if ( t < 0.5 ) return easing.easingBounceIn( t * 2 ) * 0.5; return easing.easingBounceOut( t * 2 - 1 ) * 0.5 + 0.5;};
|
|
|
|
// single Tween object construct
|
|
const Tween = function (targetElement, startObject, endObject, options) {
|
|
this.element = 'scroll' in endObject && (targetElement === undefined || targetElement === null) ? scrollContainer : targetElement; // element animation is applied to
|
|
|
|
this.playing = false;
|
|
this.reversed = false;
|
|
this.paused = false;
|
|
|
|
this._startTime = null;
|
|
this._pauseTime = null;
|
|
|
|
this._startFired = false;
|
|
this.options = {}; for (const o in options) { this.options[o] = options[o]; }
|
|
this.options.rpr = options.rpr || false; // internal option to process inline/computed style at start instead of init true/false
|
|
|
|
this.valuesRepeat = {}; // internal valuesRepeat
|
|
this.valuesEnd = {}; // valuesEnd
|
|
this.valuesStart = {}; // valuesStart
|
|
|
|
preparePropertiesObject.call(this,endObject,'end'); // valuesEnd
|
|
if ( this.options.rpr ) { this.valuesStart = startObject; } else { preparePropertiesObject.call(this,startObject,'start'); } // valuesStart
|
|
|
|
if ( this.options.perspective !== undefined && transformProperty in this.valuesEnd ) { // element transform perspective
|
|
const perspectiveString = `perspective(${parseInt(this.options.perspective)}px)`;
|
|
this.valuesEnd[transformProperty].perspective = perspectiveString;
|
|
}
|
|
|
|
for ( const e in this.valuesEnd ) {
|
|
if (e in crossCheck && !this.options.rpr) crossCheck[e].call(this); // this is where we do the valuesStart and valuesEnd check for fromTo() method
|
|
}
|
|
|
|
this.options.chain = []; // chained Tweens
|
|
this.options.easing = options.easing && typeof processEasing(options.easing) === 'function' ? processEasing(options.easing) : easing[defaultOptions.easing]; // you can only set a core easing function as default
|
|
this.options.repeat = options.repeat || defaultOptions.repeat;
|
|
this.options.repeatDelay = options.repeatDelay || defaultOptions.repeatDelay;
|
|
this.options.yoyo = options.yoyo || defaultOptions.yoyo;
|
|
this.options.duration = options.duration || defaultOptions.duration; // duration option | default
|
|
this.options.delay = options.delay || defaultOptions.delay; // delay option | default
|
|
|
|
this.repeat = this.options.repeat; // we cache the number of repeats to be able to put it back after all cycles finish
|
|
};
|
|
|
|
const // tween control and chain
|
|
TweenProto = Tween.prototype = {
|
|
// queue tween object to main frame update
|
|
start(t) { // move functions that use the ticker outside the prototype to be in the same scope with it
|
|
scrollIn.call(this);
|
|
|
|
if ( this.options.rpr ) { getStartValues.apply(this); } // on start we reprocess the valuesStart for TO() method
|
|
perspective.apply(this); // apply the perspective and transform origin
|
|
|
|
for ( const e in this.valuesEnd ) {
|
|
if (e in crossCheck && this.options.rpr) crossCheck[e].call(this); // this is where we do the valuesStart and valuesEnd check for to() method
|
|
this.valuesRepeat[e] = this.valuesStart[e];
|
|
}
|
|
|
|
// now it's a good time to start
|
|
tweens.push(this);
|
|
this.playing = true;
|
|
this.paused = false;
|
|
this._startFired = false;
|
|
this._startTime = t || time.now();
|
|
this._startTime += this.options.delay;
|
|
|
|
if (!this._startFired) {
|
|
if (this.options.start) { this.options.start.call(); }
|
|
this._startFired = true;
|
|
}
|
|
!tick && ticker();
|
|
return this;
|
|
},
|
|
play() {
|
|
if (this.paused && this.playing) {
|
|
this.paused = false;
|
|
if (this.options.resume) { this.options.resume.call(); }
|
|
this._startTime += time.now() - this._pauseTime;
|
|
add(this);
|
|
!tick && ticker(); // restart ticking if stopped
|
|
}
|
|
return this;
|
|
},
|
|
resume() { return this.play(); },
|
|
pause() {
|
|
if (!this.paused && this.playing) {
|
|
remove(this);
|
|
this.paused = true;
|
|
this._pauseTime = time.now();
|
|
if (this.options.pause) { this.options.pause.call(); }
|
|
}
|
|
return this;
|
|
},
|
|
stop() {
|
|
if (!this.paused && this.playing) {
|
|
remove(this);
|
|
this.playing = false;
|
|
this.paused = false;
|
|
scrollOut.call(this);
|
|
|
|
if (this.options.stop) { this.options.stop.call(); }
|
|
this.stopChainedTweens();
|
|
close.call(this);
|
|
}
|
|
return this;
|
|
},
|
|
chain() { this.options.chain = arguments; return this; },
|
|
stopChainedTweens() {
|
|
for (let i = 0, ctl = this.options.chain.length; i < ctl; i++) {
|
|
this.options.chain[i].stop();
|
|
}
|
|
}
|
|
};
|
|
|
|
const // the multi elements Tween constructs
|
|
TweensTO = function (els, vE, o) { // .to
|
|
this.tweens = []; const options = [];
|
|
for ( let i = 0, tl = els.length; i < tl; i++ ) {
|
|
options[i] = o || {}; o.delay = o.delay || defaultOptions.delay;
|
|
options[i].delay = i>0 ? o.delay + (o.offset||defaultOptions.offset) : o.delay;
|
|
this.tweens.push( to(els[i], vE, options[i]) );
|
|
}
|
|
};
|
|
|
|
const TweensFT = function (els, vS, vE, o) { // .fromTo
|
|
this.tweens = []; const options = [];
|
|
for ( let i = 0, l = els.length; i < l; i++ ) {
|
|
options[i] = o || {}; o.delay = o.delay || defaultOptions.delay;
|
|
options[i].delay = i>0 ? o.delay + (o.offset||defaultOptions.offset) : o.delay;
|
|
this.tweens.push( fromTo(els[i], vS, vE, options[i]) );
|
|
}
|
|
};
|
|
|
|
const ws = TweensTO.prototype = TweensFT.prototype = {
|
|
start(t=time.now()) {
|
|
for ( let i = 0, tl = this.tweens.length; i < tl; i++ ) {
|
|
this.tweens[i].start(t);
|
|
}
|
|
return this;
|
|
},
|
|
stop() { for ( let i = 0, tl = this.tweens.length; i < tl; i++ ) { this.tweens[i].stop(); } return this; },
|
|
pause() { for ( let i = 0, tl = this.tweens.length; i < tl; i++ ) { this.tweens[i].pause(); } return this; },
|
|
chain() { this.tweens[this.tweens.length-1].options.chain = arguments; return this; },
|
|
play() { for ( let i = 0, tl = this.tweens.length; i < tl; i++ ) { this.tweens[i].play(); } return this; },
|
|
resume() {return this.play()}
|
|
};
|
|
|
|
const // main methods
|
|
to = (element, endObject, options) => {
|
|
options = options || {}; options.rpr = true;
|
|
return new Tween(selector(element), endObject, endObject, options);
|
|
};
|
|
|
|
const fromTo = (element, startObject, endObject, options) => {
|
|
options = options || {};
|
|
return new Tween(selector(element), startObject, endObject, options);
|
|
};
|
|
|
|
const // multiple elements tweening
|
|
allTo = (elements, endObject, options) => new TweensTO(selector(elements,true), endObject, options);
|
|
|
|
const allFromTo = (elements, f, endObject, options) => new TweensFT(selector(elements,true), f, endObject, options);
|
|
|
|
return { // export core methods to public for plugins
|
|
property, getPrefix, selector, processEasing, // utils
|
|
defaultOptions, // default tween options since 1.6.1
|
|
to, fromTo, allTo, allFromTo, // main methods
|
|
ticker, tick, tweens, update, dom : DOM, // update
|
|
parseProperty, prepareStart, crossCheck, Tween, // property parsing & preparation | Tween | crossCheck
|
|
truD: trueDimension, truC: trueColor, rth: rgbToHex, htr: hexToRGB, getCurrentStyle, // property parsing
|
|
};
|
|
}));
|