UNPKG

highcharts

Version:
389 lines (388 loc) 12.9 kB
/* * * * (c) 2010-2025 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import Color from '../Color/Color.js'; const { parse: color } = Color; import H from '../Globals.js'; const { win } = H; import U from '../Utilities.js'; const { isNumber, objectEach } = U; /* eslint-disable no-invalid-this, valid-jsdoc */ /* * * * Class * * */ /** * An animator object used internally. One instance applies to one property * (attribute or style prop) on one element. Animation is always initiated * through {@link SVGElement#animate}. * * @example * let rect = renderer.rect(0, 0, 10, 10).add(); * rect.animate({ width: 100 }); * * @private * @class * @name Highcharts.Fx * * @param {Highcharts.HTMLDOMElement|Highcharts.SVGElement} elem * The element to animate. * * @param {Partial<Highcharts.AnimationOptionsObject>} options * Animation options. * * @param {string} prop * The single attribute or CSS property to animate. */ class Fx { /* * * * Constructors * * */ constructor(elem, options, prop) { this.pos = NaN; this.options = options; this.elem = elem; this.prop = prop; } /* * * * Functions * * */ /** * Set the current step of a path definition on SVGElement. * * @function Highcharts.Fx#dSetter * */ dSetter() { const paths = this.paths, start = paths?.[0], end = paths?.[1], now = this.now || 0; let path = []; // Land on the final path without adjustment points appended in the ends if (now === 1 || !start || !end) { path = this.toD || []; } else if (start.length === end.length && now < 1) { for (let i = 0; i < end.length; i++) { // Tween between the start segment and the end segment. Start // with a copy of the end segment and tween the appropriate // numerics const startSeg = start[i]; const endSeg = end[i]; const tweenSeg = []; for (let j = 0; j < endSeg.length; j++) { const startItem = startSeg[j]; const endItem = endSeg[j]; // Tween numbers if (isNumber(startItem) && isNumber(endItem) && // Arc boolean flags !(endSeg[0] === 'A' && (j === 4 || j === 5))) { tweenSeg[j] = startItem + now * (endItem - startItem); // Strings, take directly from the end segment } else { tweenSeg[j] = endItem; } } path.push(tweenSeg); } // If animation is finished or length not matching, land on right value } else { path = end; } this.elem.attr('d', path, void 0, true); } /** * Update the element with the current animation step. * * @function Highcharts.Fx#update * */ update() { const elem = this.elem, prop = this.prop, // If destroyed, it is null now = this.now, step = this.options.step; // Animation setter defined from outside if (this[prop + 'Setter']) { this[prop + 'Setter'](); // Other animations on SVGElement } else if (elem.attr) { if (elem.element) { elem.attr(prop, now, null, true); } // HTML styles, raw HTML content like container size } else { elem.style[prop] = now + this.unit; } if (step) { step.call(elem, now, this); } } /** * Run an animation. * * @function Highcharts.Fx#run * * @param {number} from * The current value, value to start from. * * @param {number} to * The end value, value to land on. * * @param {string} unit * The property unit, for example `px`. * */ run(from, to, unit) { const self = this, options = self.options, timer = function (gotoEnd) { return timer.stopped ? false : self.step(gotoEnd); }, requestAnimationFrame = win.requestAnimationFrame || function (step) { setTimeout(step, 13); }, step = function () { for (let i = 0; i < Fx.timers.length; i++) { if (!Fx.timers[i]()) { Fx.timers.splice(i--, 1); } } if (Fx.timers.length) { requestAnimationFrame(step); } }; if (from === to && !this.elem['forceAnimate:' + this.prop]) { delete options.curAnim[this.prop]; if (options.complete && Object.keys(options.curAnim).length === 0) { options.complete.call(this.elem); } } else { // #7166 this.startTime = +new Date(); this.start = from; this.end = to; this.unit = unit; this.now = this.start; this.pos = 0; timer.elem = this.elem; timer.prop = this.prop; if (timer() && Fx.timers.push(timer) === 1) { requestAnimationFrame(step); } } } /** * Run a single step in the animation. * * @function Highcharts.Fx#step * * @param {boolean} [gotoEnd] * Whether to go to the endpoint of the animation after abort. * * @return {boolean} * Returns `true` if animation continues. */ step(gotoEnd) { const t = +new Date(), options = this.options, elem = this.elem, complete = options.complete, duration = options.duration, curAnim = options.curAnim; let ret, done; if (!!elem.attr && !elem.element) { // #2616, element is destroyed ret = false; } else if (gotoEnd || t >= duration + this.startTime) { this.now = this.end; this.pos = 1; this.update(); curAnim[this.prop] = true; done = true; objectEach(curAnim, function (val) { if (val !== true) { done = false; } }); if (done && complete) { complete.call(elem); } ret = false; } else { this.pos = options.easing((t - this.startTime) / duration); this.now = this.start + ((this.end - this.start) * this.pos); this.update(); ret = true; } return ret; } /** * Prepare start and end values so that the path can be animated one to one. * * @function Highcharts.Fx#initPath * * @param {Highcharts.SVGElement} elem * The SVGElement item. * * @param {Highcharts.SVGPathArray|undefined} fromD * Starting path definition. * * @param {Highcharts.SVGPathArray} toD * Ending path definition. * * @return {Array<Highcharts.SVGPathArray,Highcharts.SVGPathArray>} * An array containing start and end paths in array form so that * they can be animated in parallel. */ initPath(elem, fromD, toD) { const startX = elem.startX, endX = elem.endX, end = toD.slice(), // Copy isArea = elem.isArea, positionFactor = isArea ? 2 : 1, disableAnimation = fromD && toD.length > fromD.length && toD.hasStackedCliffs; // #16925 let shift, fullLength, i, reverse, start = fromD?.slice(); // Copy if (!start || disableAnimation) { return [end, end]; } /** * If shifting points, prepend a dummy point to the end path. * @private */ function prepend(arr, other) { while (arr.length < fullLength) { // Move to, line to or curve to? const moveSegment = arr[0], otherSegment = other[fullLength - arr.length]; if (otherSegment && moveSegment[0] === 'M') { if (otherSegment[0] === 'C') { arr[0] = [ 'C', moveSegment[1], moveSegment[2], moveSegment[1], moveSegment[2], moveSegment[1], moveSegment[2] ]; } else { arr[0] = ['L', moveSegment[1], moveSegment[2]]; } } // Prepend a copy of the first point arr.unshift(moveSegment); // For areas, the bottom path goes back again to the left, so we // need to append a copy of the last point. if (isArea) { const z = arr.pop(); arr.push(arr[arr.length - 1], z); // Append point and the Z } } } /** * Copy and append last point until the length matches the end length. * @private */ function append(arr) { while (arr.length < fullLength) { // Pull out the slice that is going to be appended or inserted. // In a line graph, the positionFactor is 1, and the last point // is sliced out. In an area graph, the positionFactor is 2, // causing the middle two points to be sliced out, since an area // path starts at left, follows the upper path then turns and // follows the bottom back. const segmentToAdd = arr[Math.floor(arr.length / positionFactor) - 1].slice(); // Disable the first control point of curve segments if (segmentToAdd[0] === 'C') { segmentToAdd[1] = segmentToAdd[5]; segmentToAdd[2] = segmentToAdd[6]; } if (!isArea) { arr.push(segmentToAdd); } else { const lowerSegmentToAdd = arr[Math.floor(arr.length / positionFactor)].slice(); arr.splice(arr.length / 2, 0, segmentToAdd, lowerSegmentToAdd); } } } // For sideways animation, find out how much we need to shift to get the // start path Xs to match the end path Xs. if (startX && endX && endX.length) { for (i = 0; i < startX.length; i++) { // Moving left, new points coming in on right if (startX[i] === endX[0]) { shift = i; break; // Moving right } else if (startX[0] === endX[endX.length - startX.length + i]) { shift = i; reverse = true; break; // Fixed from the right side, "scaling" left } else if (startX[startX.length - 1] === endX[endX.length - startX.length + i]) { shift = startX.length - i; break; } } if (typeof shift === 'undefined') { start = []; } } if (start.length && isNumber(shift)) { // The common target length for the start and end array, where both // arrays are padded in opposite ends fullLength = end.length + shift * positionFactor; if (!reverse) { prepend(end, start); append(start); } else { prepend(start, end); append(end); } } return [start, end]; } /** * Handle animation of the color attributes directly. * * @function Highcharts.Fx#fillSetter * */ fillSetter() { Fx.prototype.strokeSetter.apply(this, arguments); } /** * Handle animation of the color attributes directly. * * @function Highcharts.Fx#strokeSetter * */ strokeSetter() { this.elem.attr(this.prop, color(this.start).tweenTo(color(this.end), this.pos), void 0, true); } } /* * * * Static Properties * * */ Fx.timers = []; /* * * * Default Export * * */ export default Fx;