cheapskate
Version:
Low-budget user interface animation
244 lines (242 loc) • 9.37 kB
JavaScript
const DEFAULT_DURATION = 0.5;
const DEFAULT_EASING = "cubic-bezier(0.5, 0.0, 0.5, 1.0)";
class SiblingAnimator {
container_element;
delegate;
hash_to_animated_metrics_map;
hash_to_animated_items_map;
private_element;
discrete_hashes_set;
previous_hashes_set;
addition_items_array;
rendered_once;
highest_z_index;
constructor(delegate) {
this.container_element = null;
this.delegate = delegate;
this.discrete_hashes_set = new Set();
this.previous_hashes_set = new Set();
this.addition_items_array = new Array();
this.hash_to_animated_metrics_map = new Map();
this.hash_to_animated_items_map = new Map();
this.rendered_once = false;
this.highest_z_index = -1;
if (typeof document !== "undefined") {
this.private_element = document.createElement("div");
}
else {
this.private_element = null;
}
}
set_container_element(element) {
if (this.private_element === null && typeof document !== "undefined")
this.private_element = document.createElement("div");
if (element && this.container_element === null) {
this.container_element = element;
const resizeObserver = new ResizeObserver(() => {
this.layout();
});
resizeObserver.observe(element);
}
}
set_discrete_items(items) {
const animated_union_set = this.animated_hashes_set();
const discrete_set = new Set();
items.forEach(item => {
const hash = this.hash_for_item(item);
animated_union_set.add(hash);
discrete_set.add(hash);
this.hash_to_animated_items_map.set(hash, item);
});
this.previous_hashes_set = this.discrete_hashes_set;
this.discrete_hashes_set = discrete_set;
if (this.private_element) {
this.animate_set(this.previous_hashes_set, discrete_set);
}
this.inform_delegate();
}
hash_for_item(item) {
return (this.delegate.hash_for_item) ? this.delegate.hash_for_item(item) : item;
}
animated_hashes_set() {
let union = new Set(Array.from(this.discrete_hashes_set));
this.addition_items_array.forEach(set => {
for (let hash of set) {
union.add(hash);
}
});
return union;
}
get animated_items() {
const hash_array = Array.from(this.animated_hashes_set());
const item_array = hash_array.map(hash => {
return this.hash_to_animated_items_map.get(hash);
});
item_array.sort(this.delegate.sort);
return item_array;
}
get discrete_items() {
const hash_array = Array.from(this.discrete_hashes_set);
const item_array = hash_array.map(hash => {
return this.hash_to_animated_items_map.get(hash);
});
item_array.sort(this.delegate.sort);
return item_array;
}
set discrete_items(items) {
this.set_discrete_items(items);
}
inform_delegate() {
const animated_items = this.animated_items;
this.delegate.set_animated_items(animated_items);
}
animate_element(element, x, y, o) {
if (x === 0 && y === 0 && o === 0)
return;
const duration = (this.delegate.duration) ? this.delegate.duration() : DEFAULT_DURATION;
const easing = (this.delegate.easing) ? this.delegate.easing() : DEFAULT_EASING;
const timing = {
duration: duration * 1000,
easing: easing,
fill: "backwards",
composite: "accumulate"
};
const from = "translate3d(" + x + "px, " + y + "px, 0px)";
const to = "translate3d(0px, 0px, 0px)";
const frames = [
{ transform: from, opacity: o },
{ transform: to, opacity: 0 }
];
const effect = new KeyframeEffect(element, frames, timing);
const animation = new Animation(effect, document.timeline);
animation.play();
}
animate_set(old_items, new_items) {
let delta_items = new Set(old_items);
for (let item of new_items) {
delta_items.delete(item);
}
this.addition_items_array.push(delta_items);
const duration = (this.delegate.duration) ? this.delegate.duration() : DEFAULT_DURATION;
const timing = {
duration: duration * 1000,
easing: "linear",
fill: "none",
composite: "replace"
};
const frames = [{}];
const effect = new KeyframeEffect(this.private_element, frames, timing);
const animation = new Animation(effect, document.timeline);
animation.finished.then(() => {
const hashes = this.addition_items_array.shift();
if (hashes) {
const animated_hashes = this.animated_hashes_set();
hashes.forEach(hash => {
if (!this.discrete_hashes_set.has(hash) && !animated_hashes.has(hash)) {
this.hash_to_animated_metrics_map.delete(hash);
this.hash_to_animated_items_map.delete(hash);
}
});
}
this.inform_delegate();
});
animation.play();
}
numeric_value(value) {
const regular_expression = /(\d+)/;
const result = value.match(regular_expression);
if (result === null)
return 0;
return Number(result[0]);
}
layout(unused_animated_items) {
const container = this.container_element;
if (container) {
const animated_items = this.animated_items;
const children = container.children;
const length = animated_items.length;
if (children.length === length) {
for (let i = 0; i < length; i++) {
const item = animated_items[i];
const element = children[i];
if (element instanceof HTMLElement) {
const item_style = window.getComputedStyle(element);
let newX = element.offsetLeft;
let newY = element.offsetTop;
const extraX = this.numeric_value(item_style.marginLeft);
const extraY = this.numeric_value(item_style.marginTop);
const hash = this.hash_for_item(item);
const exiting = !this.discrete_hashes_set.has(hash);
const newO = exiting ? 0 : 1;
const newP = exiting ? "absolute" : "relative";
let newI = this.highest_z_index + 1;
const metrics = this.hash_to_animated_metrics_map.get(hash);
if (metrics) {
const oldX = metrics.x + extraX;
const oldY = metrics.y + extraY;
const oldO = metrics.o;
newI = metrics.i;
this.animate_element(element, oldX - newX, oldY - newY, oldO - newO);
}
else {
const oldO = this.previous_hashes_set.has(hash) ? 1 : 0;
this.animate_element(element, 0, 0, oldO - newO);
}
this.highest_z_index = Math.max(this.highest_z_index, newI);
const entry = {
x: newX - extraX,
y: newY - extraY,
o: newO,
ex: extraX,
ey: extraY,
p: newP,
i: newI
};
this.hash_to_animated_metrics_map.set(hash, entry);
}
}
}
}
if (!this.rendered_once) { // sneaky second render for first enter animation
this.rendered_once = true;
this.inform_delegate();
}
}
metrics_for_item(item) {
const hash = this.hash_for_item(item);
const raw = this.hash_to_animated_metrics_map.get(hash);
const exiting = !(this.discrete_hashes_set.has(hash));
if (!this.rendered_once)
return {
left: "auto",
top: "auto",
opacity: 0,
position: "relative",
zIndex: 0
};
if (raw === undefined)
return {
left: "auto",
top: "auto",
opacity: exiting ? 0 : 1,
position: exiting ? "absolute" : "relative",
zIndex: exiting ? -1 : 0
};
if (exiting)
return {
left: raw.x + "px",
top: raw.y + "px",
opacity: 0,
position: "absolute",
zIndex: raw.i
};
return {
left: "auto",
top: "auto",
opacity: 1,
position: "relative",
zIndex: raw.i
};
}
}
export { SiblingAnimator };