import animator from './core.animator.js'; import Animation from './core.animation.js'; import defaults from './core.defaults.js'; import {isArray, isObject} from '../helpers/helpers.core.js'; export default class Animations { constructor(chart, config) { this._chart = chart; this._properties = new Map(); this.configure(config); } configure(config) { if (!isObject(config)) { return; } const animationOptions = Object.keys(defaults.animation); const animatedProps = this._properties; Object.getOwnPropertyNames(config).forEach(key => { const cfg = config[key]; if (!isObject(cfg)) { return; } const resolved = {}; for (const option of animationOptions) { resolved[option] = cfg[option]; } (isArray(cfg.properties) && cfg.properties || [key]).forEach((prop) => { if (prop === key || !animatedProps.has(prop)) { animatedProps.set(prop, resolved); } }); }); } /** * Utility to handle animation of `options`. * @private */ _animateOptions(target, values) { const newOptions = values.options; const options = resolveTargetOptions(target, newOptions); if (!options) { return []; } const animations = this._createAnimations(options, newOptions); if (newOptions.$shared) { // Going to shared options: // After all animations are done, assign the shared options object to the element // So any new updates to the shared options are observed awaitAll(target.options.$animations, newOptions).then(() => { target.options = newOptions; }, () => { // rejected, noop }); } return animations; } /** * @private */ _createAnimations(target, values) { const animatedProps = this._properties; const animations = []; const running = target.$animations || (target.$animations = {}); const props = Object.keys(values); const date = Date.now(); let i; for (i = props.length - 1; i >= 0; --i) { const prop = props[i]; if (prop.charAt(0) === '$') { continue; } if (prop === 'options') { animations.push(...this._animateOptions(target, values)); continue; } const value = values[prop]; let animation = running[prop]; const cfg = animatedProps.get(prop); if (animation) { if (cfg && animation.active()) { // There is an existing active animation, let's update that animation.update(cfg, value, date); continue; } else { animation.cancel(); } } if (!cfg || !cfg.duration) { // not animated, set directly to new value target[prop] = value; continue; } running[prop] = animation = new Animation(cfg, target, prop, value); animations.push(animation); } return animations; } /** * Update `target` properties to new values, using configured animations * @param {object} target - object to update * @param {object} values - new target properties * @returns {boolean|undefined} - `true` if animations were started **/ update(target, values) { if (this._properties.size === 0) { // Nothing is animated, just apply the new values. Object.assign(target, values); return; } const animations = this._createAnimations(target, values); if (animations.length) { animator.add(this._chart, animations); return true; } } } function awaitAll(animations, properties) { const running = []; const keys = Object.keys(properties); for (let i = 0; i < keys.length; i++) { const anim = animations[keys[i]]; if (anim && anim.active()) { running.push(anim.wait()); } } // @ts-ignore return Promise.all(running); } function resolveTargetOptions(target, newOptions) { if (!newOptions) { return; } let options = target.options; if (!options) { target.options = newOptions; return; } if (options.$shared) { // Going from shared options to distinct one: // Create new options object containing the old shared values and start updating that. target.options = options = Object.assign({}, options, {$shared: false, $animations: {}}); } return options; }