You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
6.0 KiB
JavaScript
153 lines
6.0 KiB
JavaScript
var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout;
|
|
var nextFrame = function(fn) { raf(function() { raf(fn); }); };
|
|
|
|
function setNextFrame(obj, prop, val) {
|
|
nextFrame(function() { obj[prop] = val; });
|
|
}
|
|
|
|
function getTextNodeRect(textNode) {
|
|
var rect;
|
|
if (document.createRange) {
|
|
var range = document.createRange();
|
|
range.selectNodeContents(textNode);
|
|
if (range.getBoundingClientRect) {
|
|
rect = range.getBoundingClientRect();
|
|
}
|
|
}
|
|
return rect;
|
|
}
|
|
|
|
function calcTransformOrigin(isTextNode, textRect, boundingRect) {
|
|
if (isTextNode) {
|
|
if (textRect) {
|
|
//calculate pixels to center of text from left edge of bounding box
|
|
var relativeCenterX = textRect.left + textRect.width/2 - boundingRect.left;
|
|
var relativeCenterY = textRect.top + textRect.height/2 - boundingRect.top;
|
|
return relativeCenterX + 'px ' + relativeCenterY + 'px';
|
|
}
|
|
}
|
|
return '0 0'; //top left
|
|
}
|
|
|
|
function getTextDx(oldTextRect, newTextRect) {
|
|
if (oldTextRect && newTextRect) {
|
|
return ((oldTextRect.left + oldTextRect.width/2) - (newTextRect.left + newTextRect.width/2));
|
|
}
|
|
return 0;
|
|
}
|
|
function getTextDy(oldTextRect, newTextRect) {
|
|
if (oldTextRect && newTextRect) {
|
|
return ((oldTextRect.top + oldTextRect.height/2) - (newTextRect.top + newTextRect.height/2));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function isTextElement(elm) {
|
|
return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3;
|
|
}
|
|
|
|
var removed, created;
|
|
|
|
function pre(oldVnode, vnode) {
|
|
removed = {};
|
|
created = [];
|
|
}
|
|
|
|
function create(oldVnode, vnode) {
|
|
var hero = vnode.data.hero;
|
|
if (hero && hero.id) {
|
|
created.push(hero.id);
|
|
created.push(vnode);
|
|
}
|
|
}
|
|
|
|
function destroy(vnode) {
|
|
var hero = vnode.data.hero;
|
|
if (hero && hero.id) {
|
|
var elm = vnode.elm;
|
|
vnode.isTextNode = isTextElement(elm); //is this a text node?
|
|
vnode.boundingRect = elm.getBoundingClientRect(); //save the bounding rectangle to a new property on the vnode
|
|
vnode.textRect = vnode.isTextNode ? getTextNodeRect(elm.childNodes[0]) : null; //save bounding rect of inner text node
|
|
var computedStyle = window.getComputedStyle(elm, null); //get current styles (includes inherited properties)
|
|
vnode.savedStyle = JSON.parse(JSON.stringify(computedStyle)); //save a copy of computed style values
|
|
removed[hero.id] = vnode;
|
|
}
|
|
}
|
|
|
|
function post() {
|
|
var i, id, newElm, oldVnode, oldElm, hRatio, wRatio,
|
|
oldRect, newRect, dx, dy, origTransform, origTransition,
|
|
newStyle, oldStyle, newComputedStyle, isTextNode,
|
|
newTextRect, oldTextRect;
|
|
for (i = 0; i < created.length; i += 2) {
|
|
id = created[i];
|
|
newElm = created[i+1].elm;
|
|
oldVnode = removed[id];
|
|
if (oldVnode) {
|
|
isTextNode = oldVnode.isTextNode && isTextElement(newElm); //Are old & new both text?
|
|
newStyle = newElm.style;
|
|
newComputedStyle = window.getComputedStyle(newElm, null); //get full computed style for new element
|
|
oldElm = oldVnode.elm;
|
|
oldStyle = oldElm.style;
|
|
//Overall element bounding boxes
|
|
newRect = newElm.getBoundingClientRect();
|
|
oldRect = oldVnode.boundingRect; //previously saved bounding rect
|
|
//Text node bounding boxes & distances
|
|
if (isTextNode) {
|
|
newTextRect = getTextNodeRect(newElm.childNodes[0]);
|
|
oldTextRect = oldVnode.textRect;
|
|
dx = getTextDx(oldTextRect, newTextRect);
|
|
dy = getTextDy(oldTextRect, newTextRect);
|
|
} else {
|
|
//Calculate distances between old & new positions
|
|
dx = oldRect.left - newRect.left;
|
|
dy = oldRect.top - newRect.top;
|
|
}
|
|
hRatio = newRect.height / (Math.max(oldRect.height, 1));
|
|
wRatio = isTextNode ? hRatio : newRect.width / (Math.max(oldRect.width, 1)); //text scales based on hRatio
|
|
// Animate new element
|
|
origTransform = newStyle.transform;
|
|
origTransition = newStyle.transition;
|
|
if (newComputedStyle.display === 'inline') //inline elements cannot be transformed
|
|
newStyle.display = 'inline-block'; //this does not appear to have any negative side effects
|
|
newStyle.transition = origTransition + 'transform 0s';
|
|
newStyle.transformOrigin = calcTransformOrigin(isTextNode, newTextRect, newRect);
|
|
newStyle.opacity = '0';
|
|
newStyle.transform = origTransform + 'translate('+dx+'px, '+dy+'px) ' +
|
|
'scale('+1/wRatio+', '+1/hRatio+')';
|
|
setNextFrame(newStyle, 'transition', origTransition);
|
|
setNextFrame(newStyle, 'transform', origTransform);
|
|
setNextFrame(newStyle, 'opacity', '1');
|
|
// Animate old element
|
|
for (var key in oldVnode.savedStyle) { //re-apply saved inherited properties
|
|
if (parseInt(key) != key) {
|
|
var ms = key.substring(0,2) === 'ms';
|
|
var moz = key.substring(0,3) === 'moz';
|
|
var webkit = key.substring(0,6) === 'webkit';
|
|
if (!ms && !moz && !webkit) //ignore prefixed style properties
|
|
oldStyle[key] = oldVnode.savedStyle[key];
|
|
}
|
|
}
|
|
oldStyle.position = 'absolute';
|
|
oldStyle.top = oldRect.top + 'px'; //start at existing position
|
|
oldStyle.left = oldRect.left + 'px';
|
|
oldStyle.width = oldRect.width + 'px'; //Needed for elements who were sized relative to their parents
|
|
oldStyle.height = oldRect.height + 'px'; //Needed for elements who were sized relative to their parents
|
|
oldStyle.margin = 0; //Margin on hero element leads to incorrect positioning
|
|
oldStyle.transformOrigin = calcTransformOrigin(isTextNode, oldTextRect, oldRect);
|
|
oldStyle.transform = '';
|
|
oldStyle.opacity = '1';
|
|
document.body.appendChild(oldElm);
|
|
setNextFrame(oldStyle, 'transform', 'translate('+ -dx +'px, '+ -dy +'px) scale('+wRatio+', '+hRatio+')'); //scale must be on far right for translate to be correct
|
|
setNextFrame(oldStyle, 'opacity', '0');
|
|
oldElm.addEventListener('transitionend', function(ev) {
|
|
if (ev.propertyName === 'transform')
|
|
document.body.removeChild(ev.target);
|
|
});
|
|
}
|
|
}
|
|
removed = created = undefined;
|
|
}
|
|
|
|
module.exports = {pre: pre, create: create, destroy: destroy, post: post};
|