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.
207 lines
6.4 KiB
JavaScript
207 lines
6.4 KiB
JavaScript
const raf =
|
|
(typeof window !== "undefined" && window.requestAnimationFrame) || setTimeout;
|
|
|
|
const nextFrame = function (fn) {
|
|
raf(function () {
|
|
raf(fn);
|
|
});
|
|
};
|
|
|
|
function setNextFrame(obj, prop, val) {
|
|
nextFrame(function () {
|
|
obj[prop] = val;
|
|
});
|
|
}
|
|
|
|
function getTextNodeRect(textNode) {
|
|
let rect;
|
|
if (document.createRange) {
|
|
const 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
|
|
const relativeCenterX =
|
|
textRect.left + textRect.width / 2 - boundingRect.left;
|
|
const 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;
|
|
}
|
|
|
|
let removed, created;
|
|
function pre() {
|
|
removed = {};
|
|
created = [];
|
|
}
|
|
|
|
function create(oldVnode, vnode) {
|
|
const hero = vnode.data.hero;
|
|
if (hero && hero.id) {
|
|
created.push(hero.id);
|
|
created.push(vnode);
|
|
}
|
|
}
|
|
|
|
function destroy(vnode) {
|
|
const hero = vnode.data.hero;
|
|
if (hero && hero.id) {
|
|
const 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
|
|
const computedStyle = window.getComputedStyle(elm, undefined); // 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() {
|
|
let 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, undefined); // 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 (const key in oldVnode.savedStyle) {
|
|
// re-apply saved inherited properties
|
|
if (String(parseInt(key)) !== key) {
|
|
const ms = key.substring(0, 2) === "ms";
|
|
const moz = key.substring(0, 3) === "moz";
|
|
const 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;
|
|
}
|
|
|
|
export const heroModule = { pre, create, destroy, post };
|