Stackedit/src/services/animationSvc.js

238 lines
6.5 KiB
JavaScript
Raw Normal View History

2017-07-23 18:42:08 +00:00
import bezierEasing from 'bezier-easing';
const easings = {
materialIn: bezierEasing(0.75, 0, 0.8, 0.25),
2017-07-25 18:20:52 +00:00
materialOut: bezierEasing(0.25, 0.8, 0.25, 1),
inOut: bezierEasing(0.25, 0.1, 0.67, 1),
2017-07-23 18:42:08 +00:00
};
const vendors = ['moz', 'webkit'];
for (let x = 0; x < vendors.length && !window.requestAnimationFrame; x += 1) {
window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`];
window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`] ||
window[`${vendors[x]}CancelRequestAnimationFrame`];
}
const transformStyles = [
'WebkitTransform',
'MozTransform',
'msTransform',
'OTransform',
'transform',
];
const transitionEndEvents = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
msTransition: 'MSTransitionEnd',
OTransition: 'oTransitionEnd',
transition: 'transitionend',
};
function getStyle(styles) {
const elt = document.createElement('div');
return styles.reduce((result, style) => {
if (elt.style[style] === undefined) {
return undefined;
}
return style;
}, undefined);
}
const transformStyle = getStyle(transformStyles);
const transitionStyle = getStyle(Object.keys(transitionEndEvents));
const transitionEndEvent = transitionEndEvents[transitionStyle];
function identity(x) {
return x;
}
function ElementAttribute(name) {
this.name = name;
this.setStart = (animation) => {
const value = animation.elt[name];
animation.$start[name] = value;
return value !== undefined && animation.$end[name] !== undefined;
};
this.applyCurrent = (animation) => {
animation.elt[name] = animation.$current[name];
};
}
function StyleAttribute(name, unit, defaultValue, wrap = identity) {
this.name = name;
this.setStart = (animation) => {
let value = parseFloat(animation.elt.style[name]);
2018-05-06 00:46:33 +00:00
if (Number.isNaN(value)) {
2017-07-23 18:42:08 +00:00
value = animation.$current[name] || defaultValue;
}
animation.$start[name] = value;
return animation.$end[name] !== undefined;
};
this.applyCurrent = (animation) => {
animation.elt.style[name] = wrap(animation.$current[name]) + unit;
};
}
function TransformAttribute(name, unit, defaultValue, wrap = identity) {
this.name = name;
this.setStart = (animation) => {
let value = animation.$current[name];
if (value === undefined) {
value = defaultValue;
}
animation.$start[name] = value;
if (animation.$end[name] === undefined) {
animation.$end[name] = value;
}
return value !== undefined;
};
this.applyCurrent = (animation) => {
const value = animation.$current[name];
return value !== defaultValue && `${name}(${wrap(value)}${unit})`;
};
}
const attributes = [
new ElementAttribute('scrollTop'),
new ElementAttribute('scrollLeft'),
new StyleAttribute('opacity', '', 1),
new StyleAttribute('zIndex', '', 0),
new TransformAttribute('translateX', 'px', 0, Math.round),
new TransformAttribute('translateY', 'px', 0, Math.round),
new TransformAttribute('scale', '', 1),
new TransformAttribute('rotate', 'deg', 0),
].concat([
'width',
'height',
'top',
'right',
'bottom',
'left',
].map(name => new StyleAttribute(name, 'px', 0, Math.round)));
class Animation {
constructor(elt) {
this.elt = elt;
this.$current = {};
this.$pending = {};
}
start(param1, param2, param3) {
let endCb = param1;
let stepCb = param2;
let useTransition = false;
if (typeof param1 === 'boolean') {
useTransition = param1;
endCb = param2;
stepCb = param3;
}
this.stop();
this.$start = {};
this.$end = this.$pending;
this.$pending = {};
this.$attributes = attributes.filter(attribute => attribute.setStart(this));
this.$end.duration = this.$end.duration || 0;
this.$end.delay = this.$end.delay || 0;
this.$end.easing = easings[this.$end.easing] || easings.materialOut;
this.$end.endCb = typeof endCb === 'function' && endCb;
this.$end.stepCb = typeof stepCb === 'function' && stepCb;
this.$startTime = Date.now() + this.$end.delay;
if (!this.$end.duration) {
this.loop(false);
} else if (useTransition) {
this.loop(true);
} else {
this.$requestId = window.requestAnimationFrame(() => this.loop(false));
}
2017-07-23 18:42:08 +00:00
return this.elt;
}
stop() {
window.cancelAnimationFrame(this.$requestId);
}
loop(useTransition) {
const onTransitionEnd = (evt) => {
if (evt.target === this.elt) {
this.elt.removeEventListener(transitionEndEvent, onTransitionEnd);
2018-05-06 00:46:33 +00:00
const { endCb } = this.$end;
2017-07-23 18:42:08 +00:00
this.$end.endCb = undefined;
if (endCb) {
endCb();
}
}
};
let progress = (Date.now() - this.$startTime) / this.$end.duration;
let transition = '';
if (useTransition) {
progress = 1;
const transitions = [
'all',
`${this.$end.duration}ms`,
this.$end.easing.toCSS(),
];
if (this.$end.delay) {
transitions.push(`${this.$end.duration}ms`);
}
transition = transitions.join(' ');
if (this.$end.endCb) {
this.elt.addEventListener(transitionEndEvent, onTransitionEnd);
}
} else if (progress < 1) {
this.$requestId = window.requestAnimationFrame(() => this.loop(false));
if (progress < 0) {
return;
}
} else if (this.$end.endCb) {
this.$requestId = window.requestAnimationFrame(this.$end.endCb);
}
const coeff = this.$end.easing.get(progress);
const transforms = this.$attributes.reduce((result, attribute) => {
if (progress < 1) {
const diff = this.$end[attribute.name] - this.$start[attribute.name];
this.$current[attribute.name] = this.$start[attribute.name] + (diff * coeff);
} else {
this.$current[attribute.name] = this.$end[attribute.name];
}
const transform = attribute.applyCurrent(this);
if (transform) {
result.push(transform);
}
return result;
}, []);
if (transforms.length) {
transforms.push('translateZ(0)'); // activate GPU
}
const transform = transforms.join(' ');
this.elt.style[transformStyle] = transform;
this.elt.style[transitionStyle] = transition;
if (this.$end.stepCb) {
this.$end.stepCb();
}
}
}
attributes.map(attribute => attribute.name).concat('duration', 'easing', 'delay')
.forEach((name) => {
Animation.prototype[name] = function setter(val) {
this.$pending[name] = val;
return this;
};
});
function animate(elt) {
if (!elt.$animation) {
elt.$animation = new Animation(elt);
}
return elt.$animation;
}
2017-07-25 18:20:52 +00:00
export default {
animate,
};