src/core/core.animator.js
import {requestAnimFrame} from '../helpers/helpers.extras.js';
/**
* @typedef { import('./core.animation.js').default } Animation
* @typedef { import('./core.controller.js').default } Chart
*/
/**
* Please use the module's default export which provides a singleton instance
* Note: class is export for typedoc
*/
export class Animator {
constructor() {
this._request = null;
this._charts = new Map();
this._running = false;
this._lastDate = undefined;
}
/**
* @private
*/
_notify(chart, anims, date, type) {
const callbacks = anims.listeners[type];
const numSteps = anims.duration;
callbacks.forEach(fn => fn({
chart,
initial: anims.initial,
numSteps,
currentStep: Math.min(date - anims.start, numSteps)
}));
}
/**
* @private
*/
_refresh() {
if (this._request) {
return;
}
this._running = true;
this._request = requestAnimFrame.call(window, () => {
this._update();
this._request = null;
if (this._running) {
this._refresh();
}
});
}
/**
* @private
*/
_update(date = Date.now()) {
let remaining = 0;
this._charts.forEach((anims, chart) => {
if (!anims.running || !anims.items.length) {
return;
}
const items = anims.items;
let i = items.length - 1;
let draw = false;
let item;
for (; i >= 0; --i) {
item = items[i];
if (item._active) {
if (item._total > anims.duration) {
// if the animation has been updated and its duration prolonged,
// update to total duration of current animations run (for progress event)
anims.duration = item._total;
}
item.tick(date);
draw = true;
} else {
// Remove the item by replacing it with last item and removing the last
// A lot faster than splice.
items[i] = items[items.length - 1];
items.pop();
}
}
if (draw) {
chart.draw();
this._notify(chart, anims, date, 'progress');
}
if (!items.length) {
anims.running = false;
this._notify(chart, anims, date, 'complete');
anims.initial = false;
}
remaining += items.length;
});
this._lastDate = date;
if (remaining === 0) {
this._running = false;
}
}
/**
* @private
*/
_getAnims(chart) {
const charts = this._charts;
let anims = charts.get(chart);
if (!anims) {
anims = {
running: false,
initial: true,
items: [],
listeners: {
complete: [],
progress: []
}
};
charts.set(chart, anims);
}
return anims;
}
/**
* @param {Chart} chart
* @param {string} event - event name
* @param {Function} cb - callback
*/
listen(chart, event, cb) {
this._getAnims(chart).listeners[event].push(cb);
}
/**
* Add animations
* @param {Chart} chart
* @param {Animation[]} items - animations
*/
add(chart, items) {
if (!items || !items.length) {
return;
}
this._getAnims(chart).items.push(...items);
}
/**
* Counts number of active animations for the chart
* @param {Chart} chart
*/
has(chart) {
return this._getAnims(chart).items.length > 0;
}
/**
* Start animating (all charts)
* @param {Chart} chart
*/
start(chart) {
const anims = this._charts.get(chart);
if (!anims) {
return;
}
anims.running = true;
anims.start = Date.now();
anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0);
this._refresh();
}
running(chart) {
if (!this._running) {
return false;
}
const anims = this._charts.get(chart);
if (!anims || !anims.running || !anims.items.length) {
return false;
}
return true;
}
/**
* Stop all animations for the chart
* @param {Chart} chart
*/
stop(chart) {
const anims = this._charts.get(chart);
if (!anims || !anims.items.length) {
return;
}
const items = anims.items;
let i = items.length - 1;
for (; i >= 0; --i) {
items[i].cancel();
}
anims.items = [];
this._notify(chart, anims, Date.now(), 'complete');
}
/**
* Remove chart from Animator
* @param {Chart} chart
*/
remove(chart) {
return this._charts.delete(chart);
}
}
// singleton instance
export default /* #__PURE__ */ new Animator();