UNPKG

cheapskate

Version:

Low-budget user interface animation

244 lines (242 loc) 9.37 kB
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 };