livelyjs
Version:
Animation library for javascript
528 lines (496 loc) • 21.6 kB
JavaScript
(function (context, configure) {
context.lively = configure();
}(this, () => {
// Easing functions
const easings = {
'default' : (currentTime, initialValue, changeInValue, duration) => {
return changeInValue * (currentTime / duration) + initialValue;
},
'easeInQuad' : (currentTime, initialValue, changeInValue, duration) => {
return changeInValue*(currentTime/=duration)*currentTime + initialValue;
},
'easeOutQuad' : (t, b, c, d) => {
return -c * (t/=d)*(t-2) + b;
},
'easeInOutQuad': function (t, b, c, d) {
if ((t/=d/2) < 1) return c/2*t*t + b;
return -c/2 * ((--t)*(t-2) - 1) + b;
},
};
let customEasings = {};
function stringContains(string, text) {
return string.indexOf(text) > -1;
}
//Helpers
const is = {
dom: a => a instanceof Element,
svg: a => a instanceof SVGElement,
obj: a => stringContains(Object.prototype.toString.call(a), 'Object'),
empty: a => Object.keys(a).length === 0 || (is.array(a) && a.length === 0),
array: a => Array.isArray(a),
nodeList : a => stringContains(Object.prototype.toString.call(a), 'NodeList'),
str: a => typeof a === 'string',
und: a => a === undefined,
fnc: a => typeof a === 'function',
rgb: a => /^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i.test(a),
rgba: a => /^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,[\d.]\)$/i.test(a),
hex: a => /(^#[0-9a-f]{3}$)|(^#[0-9a-f]{6}$)/i.test(a),
hex3: a => /(^#[0-9a-f]{3}$)/i.test(a),
hex6: a => /(^#[0-9a-f]{6}$)/i.test(a),
col : a => is.rgb(a) || is.rgba(a) || is.hex(a)
};
function getFirstKey(obj) {
for (let key in obj) { return key; }
}
const livelyProperties = ['update', 'done', 'targets', 'eases', 'preserve', 'keyframes'];
const cssTransformProperties = ['transform', 'translateX', 'translateY', 'rotate', 'scaleX', 'scaleY', 'skewX', 'skewY'];
const cssColorProperties = ['color', 'background-color', 'border-color', 'fill'];
function parseValue(value, property) {
if (is.col(value)) return parseColor(value);
let result = parseTransform(value, property);
if (!is.und(result) && !isNaN(result)) return result;
result = parseFloat(parseCssValue(value));
if (!is.und(result) && !isNaN(result)) return result;
return value;
}
function parseCssValue(value) {
if (!is.str(value)) return undefined;
return value.replace(/[^0-9]/g, '');
}
function parseTransform(value, property) {
if (!property || !cssTransformProperties.includes(property)) return undefined;
let currentTransform = {};
let match;
let regex = /(\w+)\(([^)]*)\)/g;
while ((match = regex.exec(value)) !== null) {
let number = match[2].replace(/[^0-9]/g, '');
if (number) currentTransform[match[1]] = parseFloat(number);
}
if (!currentTransform[property] && stringContains(property, 'scale')) return 1;
else if (!currentTransform[property]) return 0;
else return currentTransform[property];
}
function parseColor(color) {
if (is.rgb(color)) {
let rgb = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
if (rgb) {
return {
r : parseInt(rgb[1]),
g : parseInt(rgb[2]),
b : parseInt(rgb[3])
};
}
}
else if (is.rgba(color)) {
let rgba = color.match(/rgba\((\s*\d+\s*,){3}[\d.]+\)/i);
if (rgba) {
console.log(rgba);
}
}
else if (is.hex3(color)) {
let rgb = color.match(/^#([0-9a-f]{3})$/i)[1];
if (rgb) {
return {
r : parseInt(rgb.charAt(0), 16) * 0x11,
g : parseInt(rgb.charAt(1), 16) * 0x11,
b : parseInt(rgb.charAt(2), 16) * 0x11,
};
}
}
else if (is.hex6(color)) {
let rgb = color.match(/^#([0-9a-f]{6})$/i)[1];
if (rgb) {
return {
r : parseInt(rgb.substr(0,2), 16),
g : parseInt(rgb.substr(2,2), 16),
b : parseInt(rgb.substr(4,2), 16),
};
}
}
}
function parsePercentage(str) {
if (str.endsWith('%')) return parseFloat(str.replace('%', ''));
}
function parseUnit(target, property, value) {
if (!is.dom(target)) return undefined;
return parseCssUnit(target, property, value);
}
function parseCssUnit(element, property, value) {
if (is.dom(element) && cssColorProperties.includes(property)) return undefined;
else if (is.dom(element)) {
if (stringContains(property, 'opacity') || stringContains(property, 'scale')) return undefined;
let unit = /(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|px|pt|pc|deg)$/.exec(value);
if (!unit && stringContains(property, 'rotate') || stringContains(property, 'skew')) return 'deg';
if (!unit) return 'px';
return unit[0];
}
}
function getTargetValue(target, property) {
if (is.dom(target)) {
return getCssValue(target, property);
}
return target[property];
}
function getCssValue(element, property) {
if (is.dom(element) && cssTransformProperties.includes(property)) {
return element.style.transform;
}
else if (is.dom(element)) {
return getComputedStyle(element).getPropertyValue(property);
}
}
function setTargetPropertyValue(target, property, value) {
if (is.dom(target) && cssTransformProperties.includes(property)) {
setTransformValue(target, property, value);
}
else if (is.dom(target) && cssColorProperties.includes(property)) {
setRgbValue(target, property, value);
}
else if (is.dom(target)) {
target.style[property] = value;
}
else if (is.obj(target)) {
target[property] = value;
}
}
function setRgbValue(element, property, rgb) {
if (is.dom(element)) {
element.style[property] = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
}
}
function setTransformValue(element, property, transform) {
if (is.dom(element)) {
let scaleX = !transform.scaleX ? '' : 'scaleX('+ transform.scaleX +') ';
let skewY = !transform.skewY ? '' : 'skewY('+ transform.skewY +') ';
let skewX = !transform.skewX ? '' : 'skewX('+ transform.skewX +') ';
let scaleY = !transform.scaleY ? '' : 'scaleY('+ transform.scaleY +') ';
let translateX = !transform.translateX ? '' : 'translateX('+ transform.translateX +') ';
let translateY = !transform.translateY ? '' : 'translateY('+ transform.translateY +') ';
let rotate = !transform.rotate ? '' : 'rotate('+ transform.rotate +') ';
element.style.transform = translateX + translateY + scaleX + scaleY + rotate + skewY + skewX;
}
}
// Animation related constants
const animationFactory = (() => {
let factory = {};
function getEases(eases) {
let animationEases = {
'default' : easings.default
};
if (is.fnc(eases)) {
animationEases.default = eases;
}
else if (is.str(eases) && (eases in easings)) {
animationEases.default = easings[eases];
}
else if (is.str(eases) && (eases in customEasings)) {
animationEases.default = customEasings[eases];
}
else if (is.array(eases)) {
for (let i = 0; i < eases.length; i++) {
for (let property in eases[i]) {
if (is.str(eases[i][property]) && (eases[i][property] in easings) || (eases[i][property] in customEasings)) {
let easingName = eases[i][property];
animationEases[property] = easings[easingName] || customEasings[easingName];
}
break;
}
}
}
return animationEases;
}
function getKeyframes(animatedProperties, duration, target) {
let propertyKeyframes = {};
for (let propertyName in animatedProperties) {
let keyframes = [];
let animatedProperty = animatedProperties[propertyName];
if (!is.obj(animatedProperty) && !is.array(animatedProperty) && !is.nodeList(animatedProperty)) {
keyframes.push({
startTime : 0,
startValue : parseValue(getTargetValue(target, propertyName), propertyName),
endTime : duration,
endValue : parseValue(animatedProperty),
unit : parseUnit(target, propertyName, animatedProperty),
isLast : true
});
}
else {
if (is.obj(animatedProperty)) {
animatedProperty = [].concat(animatedProperty);
}
for (let i = 0; i < animatedProperty.length; i++) {
let key = getFirstKey(animatedProperty[i]);
let endTime = (parsePercentage(key) / 100) * duration;
keyframes.push({
startTime : 0,
startValue : parseValue(getTargetValue(target, propertyName), propertyName),
endTime : endTime,
endValue : parseValue(animatedProperty[i][key]),
unit : parseUnit(target, propertyName, animatedProperty[i][key]),
isLast : false
});
}
keyframes.sort((a, b) => {
return a.startTime > b.startTime;
});
keyframes[keyframes.length - 1].isLast = true;
for (let i = 1; i < keyframes.length; i++) {
keyframes[i].startTime = keyframes[i - 1].endTime;
keyframes[i].startValue = keyframes[i - 1].endValue;
}
}
propertyKeyframes[propertyName] = keyframes;
}
return propertyKeyframes;
}
function getTargets(animateObj, duration){
let targets = animateObj.targets;
let animatedTargets = [];
let animatedProperties = {};
for (let property in animateObj) {
if (!livelyProperties.includes(property)) {
animatedProperties[property] = animateObj[property];
}
}
if (is.str(targets)) {
let createdTargets = document.querySelectorAll(targets);
for (let i = 0; i < createdTargets.length; i++) {
animatedTargets.push({
target : createdTargets[i],
keyframes : getKeyframes(animatedProperties, duration, createdTargets[i])
});
}
}
else if (is.array(targets) || is.nodeList(targets)) {
for (let i = 0; i < targets.length; i++) {
animatedTargets.push({
target : targets[i],
keyframes: getKeyframes(animatedProperties, duration, targets[i])
});
}
}
else if (is.obj(targets)) {
animatedTargets.push({
target : targets,
keyframes : getKeyframes(animatedProperties, duration, targets)
});
}
return animatedTargets;
}
factory.createAnimation = (animateObj, durationMs) => {
return {
duration : durationMs,
eases : getEases(animateObj.eases),
targets : getTargets(animateObj, durationMs),
preserve : animateObj.preserve,
onDone : animateObj.done,
onUpdate : animateObj.update
};
};
return factory;
})();
const animator = (() => {
function getTween(eases, property) {
return (eases[property] ? eases[property] : eases['default']);
}
function getKeyframe(currentTime, propertyKeyframes) {
let length = propertyKeyframes.length;
let keyframe = propertyKeyframes.filter(function (keyframe) {
return currentTime >= keyframe.startTime && currentTime <= keyframe.endTime;
})[0];
if (keyframe) return keyframe;
else return propertyKeyframes[length - 1];
}
function animateKeyframe(currentTime, target, property, keyframe, tween) {
if (keyframe.isLast && currentTime >= keyframe.endTime) {
return keyframe.endValue;
}
else if (is.dom(target) && cssColorProperties.includes(property)) {
return {
r : tween((currentTime - keyframe.startTime), keyframe.startValue.r, (keyframe.endValue.r - keyframe.startValue.r), (keyframe.endTime - keyframe.startTime)),
g : tween((currentTime - keyframe.startTime), keyframe.startValue.g, (keyframe.endValue.g - keyframe.startValue.g), (keyframe.endTime - keyframe.startTime)),
b : tween((currentTime - keyframe.startTime), keyframe.startValue.b, (keyframe.endValue.b - keyframe.startValue.b), (keyframe.endTime - keyframe.startTime)),
};
}
else if (is.dom(target) || is.obj(target)) {
return tween((currentTime - keyframe.startTime), keyframe.startValue, (keyframe.endValue - keyframe.startValue), (keyframe.endTime - keyframe.startTime));
}
}
function tick(currentTime, animation) {
const duration = animation.duration;
const finished = (currentTime >= duration) || (currentTime < 0);
if (currentTime < 0) return finished;
let eases = animation.eases;
for (let i = 0; i < animation.targets.length; i++) {
let target = animation.targets[i].target;
let keyframeProperties = animation.targets[i].keyframes;
let transform = {};
for (let property in keyframeProperties) {
let tween = getTween(eases, property);
let keyframe = getKeyframe(currentTime, keyframeProperties[property]);
let currentVal = animateKeyframe(currentTime, target, property, keyframe, tween);
if (keyframe.unit) currentVal = currentVal + keyframe.unit;
if (cssTransformProperties.includes(property)) transform[property] = currentVal;
else setTargetPropertyValue(target, property, currentVal);
}
if (!is.empty(transform)) {
setTargetPropertyValue(target, 'transform', transform);
}
}
return finished;
}
return {
tick : tick
};
})();
let queuedAnimations = [];
let runningAnimations = [];
let finishedAnimations = [];
let raf = 0;
let lively = {};
let config = {};
lively.configure = (options) => {
config.onRenderTick = options.onRenderTick;
config.preserveAll = options.preserveAll;
config.onAllAnimationsComplete = options.onAllAnimationsComplete;
};
const animationEngine = (() => {
let renderer = animator.tick;
let startTime = 0;
let elapsedTime = 0;
let pauseTime = 0;
let reverseTime = 0;
let isPaused = false;
let isRewinding = false;
let isPlaying = false;
let resolve;
function play(res) {
isRewinding = isPaused = false;
resolve = res;
runningAnimations = runningAnimations.concat(queuedAnimations);
if (!isPlaying) {
isPlaying = true;
raf = requestAnimationFrame(tick);
}
}
function rewind() {
if (isPlaying) {
isRewinding = true;
}
}
function pause() {
isPaused = true;
isRewinding = false;
isPlaying = false;
}
function stop() {
isPlaying = false;
isRewinding = false;
startTime = undefined;
elapsedTime = 0;
pauseTime = 0;
reverseTime = 0;
raf = 0;
if (finishedAnimations.length) {
queuedAnimations = [].concat(finishedAnimations);
finishedAnimations = [];
}
runningAnimations = [];
if (resolve) {
resolve();
}
}
function reset() {
startTime = undefined;
elapsedTime = undefined;
isPaused = false;
isPlaying = false;
resolve = undefined;
raf = 0;
}
function renderAnimations(elapsedTime) {
for (let i = 0; i < runningAnimations.length; i++) {
if (runningAnimations[i]) {
let finished = renderer(elapsedTime, runningAnimations[i]);
if (runningAnimations[i].onUpdate && !isPaused) {
runningAnimations[i].onUpdate(runningAnimations[i].target);
}
if (finished) {
if (runningAnimations[i].onDone) {
runningAnimations[i].onDone(runningAnimations[i].target);
}
if (runningAnimations[i].preserve) {
finishedAnimations.push(runningAnimations[i]);
}
runningAnimations.splice(i, 1);
}
}
}
}
function tick(timeStamp) {
if (runningAnimations.length && isPlaying) {
if (!startTime) {
startTime = timeStamp;
}
if (isPaused) {
pauseTime = (timeStamp - startTime) - elapsedTime;
elapsedTime = (timeStamp - startTime) - pauseTime;
}
if (isRewinding) {
let currentTime = (timeStamp - startTime) - pauseTime;
reverseTime = (elapsedTime - (currentTime - elapsedTime));
renderAnimations(reverseTime);
}
else {
elapsedTime = (timeStamp - startTime) - pauseTime;
renderAnimations(elapsedTime - reverseTime);
}
if (config.onRenderTick) {
config.onRenderTick();
}
raf = requestAnimationFrame(tick);
}
else {
cancelAnimationFrame(raf);
stop();
if (config.onAllAnimationsComplete) {
config.onAllAnimationsComplete();
}
}
}
return {
play : play,
rewind : rewind,
pause : pause,
stop : stop,
reset : reset
};
})();
lively.reset = () => {
queuedAnimations = [];
runningAnimations = [];
finishedAnimations = [];
animationEngine.reset();
};
lively.easings = customEasings;
lively.animate = (animateObj, durationMs) => {
let animation = animationFactory.createAnimation(animateObj, durationMs);
queuedAnimations.push(animation);
};
lively.promise = {};
lively.promise.play = () => {
return new Promise((resolve, reject) => {
try {
animationEngine.play(resolve);
}
catch (e) {
reject(e);
}
});
};
lively.play = () => animationEngine.play();
lively.rewind = () => animationEngine.rewind();
lively.pause = () => animationEngine.pause();
lively.stop = () => animationEngine.stop();
return lively;
}));