Timeline.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. /* *
  2. *
  3. * (c) 2009-2020 Øystein Moseng
  4. *
  5. * TimelineEvent class definition.
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  10. *
  11. * */
  12. 'use strict';
  13. import H from '../../Core/Globals.js';
  14. import U from '../../Core/Utilities.js';
  15. var merge = U.merge, splat = U.splat, uniqueKey = U.uniqueKey;
  16. /**
  17. * A set of options for the TimelineEvent class.
  18. *
  19. * @requires module:modules/sonification
  20. *
  21. * @private
  22. * @interface Highcharts.TimelineEventOptionsObject
  23. */ /**
  24. * The object we want to sonify when playing the TimelineEvent. Can be any
  25. * object that implements the `sonify` and `cancelSonify` functions. If this is
  26. * not supplied, the TimelineEvent is considered a silent event, and the onEnd
  27. * event is immediately called.
  28. * @name Highcharts.TimelineEventOptionsObject#eventObject
  29. * @type {*}
  30. */ /**
  31. * Options to pass on to the eventObject when playing it.
  32. * @name Highcharts.TimelineEventOptionsObject#playOptions
  33. * @type {object|undefined}
  34. */ /**
  35. * The time at which we want this event to play (in milliseconds offset). This
  36. * is not used for the TimelineEvent.play function, but rather intended as a
  37. * property to decide when to call TimelineEvent.play. Defaults to 0.
  38. * @name Highcharts.TimelineEventOptionsObject#time
  39. * @type {number|undefined}
  40. */ /**
  41. * Unique ID for the event. Generated automatically if not supplied.
  42. * @name Highcharts.TimelineEventOptionsObject#id
  43. * @type {string|undefined}
  44. */ /**
  45. * Callback called when the play has finished.
  46. * @name Highcharts.TimelineEventOptionsObject#onEnd
  47. * @type {Function|undefined}
  48. */
  49. import utilities from './utilities.js';
  50. /* eslint-disable no-invalid-this, valid-jsdoc */
  51. /**
  52. * The TimelineEvent class. Represents a sound event on a timeline.
  53. *
  54. * @requires module:modules/sonification
  55. *
  56. * @private
  57. * @class
  58. * @name Highcharts.TimelineEvent
  59. *
  60. * @param {Highcharts.TimelineEventOptionsObject} options
  61. * Options for the TimelineEvent.
  62. */
  63. function TimelineEvent(options) {
  64. this.init(options || {});
  65. }
  66. TimelineEvent.prototype.init = function (options) {
  67. this.options = options;
  68. this.time = options.time || 0;
  69. this.id = this.options.id = options.id || uniqueKey();
  70. };
  71. /**
  72. * Play the event. Does not take the TimelineEvent.time option into account,
  73. * and plays the event immediately.
  74. *
  75. * @function Highcharts.TimelineEvent#play
  76. *
  77. * @param {Highcharts.TimelineEventOptionsObject} [options]
  78. * Options to pass in to the eventObject when playing it.
  79. *
  80. * @return {void}
  81. */
  82. TimelineEvent.prototype.play = function (options) {
  83. var eventObject = this.options.eventObject, masterOnEnd = this.options.onEnd, playOnEnd = options && options.onEnd, playOptionsOnEnd = this.options.playOptions &&
  84. this.options.playOptions.onEnd, playOptions = merge(this.options.playOptions, options);
  85. if (eventObject && eventObject.sonify) {
  86. // If we have multiple onEnds defined, use all
  87. playOptions.onEnd = masterOnEnd || playOnEnd || playOptionsOnEnd ?
  88. function () {
  89. var args = arguments;
  90. [masterOnEnd, playOnEnd, playOptionsOnEnd].forEach(function (onEnd) {
  91. if (onEnd) {
  92. onEnd.apply(this, args);
  93. }
  94. });
  95. } : void 0;
  96. eventObject.sonify(playOptions);
  97. }
  98. else {
  99. if (playOnEnd) {
  100. playOnEnd();
  101. }
  102. if (masterOnEnd) {
  103. masterOnEnd();
  104. }
  105. }
  106. };
  107. /**
  108. * Cancel the sonification of this event. Does nothing if the event is not
  109. * currently sonifying.
  110. *
  111. * @function Highcharts.TimelineEvent#cancel
  112. *
  113. * @param {boolean} [fadeOut=false]
  114. * Whether or not to fade out as we stop. If false, the event is
  115. * cancelled synchronously.
  116. */
  117. TimelineEvent.prototype.cancel = function (fadeOut) {
  118. this.options.eventObject.cancelSonify(fadeOut);
  119. };
  120. /**
  121. * A set of options for the TimelinePath class.
  122. *
  123. * @requires module:modules/
  124. *
  125. * @private
  126. * @interface Highcharts.TimelinePathOptionsObject
  127. */ /**
  128. * List of TimelineEvents to play on this track.
  129. * @name Highcharts.TimelinePathOptionsObject#events
  130. * @type {Array<Highcharts.TimelineEvent>}
  131. */ /**
  132. * If this option is supplied, this path ignores all events and just waits for
  133. * the specified number of milliseconds before calling onEnd.
  134. * @name Highcharts.TimelinePathOptionsObject#silentWait
  135. * @type {number|undefined}
  136. */ /**
  137. * Unique ID for this timeline path. Automatically generated if not supplied.
  138. * @name Highcharts.TimelinePathOptionsObject#id
  139. * @type {string|undefined}
  140. */ /**
  141. * Callback called before the path starts playing.
  142. * @name Highcharts.TimelinePathOptionsObject#onStart
  143. * @type {Function|undefined}
  144. */ /**
  145. * Callback function to call before an event plays.
  146. * @name Highcharts.TimelinePathOptionsObject#onEventStart
  147. * @type {Function|undefined}
  148. */ /**
  149. * Callback function to call after an event has stopped playing.
  150. * @name Highcharts.TimelinePathOptionsObject#onEventEnd
  151. * @type {Function|undefined}
  152. */ /**
  153. * Callback called when the whole path is finished.
  154. * @name Highcharts.TimelinePathOptionsObject#onEnd
  155. * @type {Function|undefined}
  156. */
  157. /**
  158. * The TimelinePath class. Represents a track on a timeline with a list of
  159. * sound events to play at certain times relative to each other.
  160. *
  161. * @requires module:modules/sonification
  162. *
  163. * @private
  164. * @class
  165. * @name Highcharts.TimelinePath
  166. *
  167. * @param {Highcharts.TimelinePathOptionsObject} options
  168. * Options for the TimelinePath.
  169. */
  170. function TimelinePath(options) {
  171. this.init(options);
  172. }
  173. TimelinePath.prototype.init = function (options) {
  174. this.options = options;
  175. this.id = this.options.id = options.id || uniqueKey();
  176. this.cursor = 0;
  177. this.eventsPlaying = {};
  178. // Handle silent wait, otherwise use events from options
  179. this.events = options.silentWait ?
  180. [
  181. new TimelineEvent({ time: 0 }),
  182. new TimelineEvent({ time: options.silentWait })
  183. ] :
  184. this.options.events;
  185. // Reference optionally provided by the user that indicates the intended
  186. // duration of the path. Unused by TimelinePath itself.
  187. this.targetDuration = options.targetDuration || options.silentWait;
  188. // We need to sort our events by time
  189. this.sortEvents();
  190. // Get map from event ID to index
  191. this.updateEventIdMap();
  192. // Signal events to fire
  193. this.signalHandler = new utilities.SignalHandler(['playOnEnd', 'masterOnEnd', 'onStart', 'onEventStart', 'onEventEnd']);
  194. this.signalHandler.registerSignalCallbacks(merge(options, { masterOnEnd: options.onEnd }));
  195. };
  196. /**
  197. * Sort the internal event list by time.
  198. * @private
  199. */
  200. TimelinePath.prototype.sortEvents = function () {
  201. this.events = this.events.sort(function (a, b) {
  202. return a.time - b.time;
  203. });
  204. };
  205. /**
  206. * Update the internal eventId to index map.
  207. * @private
  208. */
  209. TimelinePath.prototype.updateEventIdMap = function () {
  210. this.eventIdMap = this.events.reduce(function (acc, cur, i) {
  211. acc[cur.id] = i;
  212. return acc;
  213. }, {});
  214. };
  215. /**
  216. * Add events to the path. Should not be done while the path is playing.
  217. * The new events are inserted according to their time property.
  218. * @private
  219. * @param {Array<Highcharts.TimelineEvent>} newEvents - The new timeline events
  220. * to add.
  221. */
  222. TimelinePath.prototype.addTimelineEvents = function (newEvents) {
  223. this.events = this.events.concat(newEvents);
  224. this.sortEvents(); // Sort events by time
  225. this.updateEventIdMap(); // Update the event ID to index map
  226. };
  227. /**
  228. * Get the current TimelineEvent under the cursor.
  229. * @private
  230. * @return {Highcharts.TimelineEvent} The current timeline event.
  231. */
  232. TimelinePath.prototype.getCursor = function () {
  233. return this.events[this.cursor];
  234. };
  235. /**
  236. * Set the current TimelineEvent under the cursor.
  237. * @private
  238. * @param {string} eventId
  239. * The ID of the timeline event to set as current.
  240. * @return {boolean}
  241. * True if there is an event with this ID in the path. False otherwise.
  242. */
  243. TimelinePath.prototype.setCursor = function (eventId) {
  244. var ix = this.eventIdMap[eventId];
  245. if (typeof ix !== 'undefined') {
  246. this.cursor = ix;
  247. return true;
  248. }
  249. return false;
  250. };
  251. /**
  252. * Play the timeline from the current cursor.
  253. * @private
  254. * @param {Function} onEnd
  255. * Callback to call when play finished. Does not override other onEnd callbacks.
  256. * @return {void}
  257. */
  258. TimelinePath.prototype.play = function (onEnd) {
  259. this.pause();
  260. this.signalHandler.emitSignal('onStart');
  261. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  262. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  263. this.playEvents(1);
  264. };
  265. /**
  266. * Play the timeline backwards from the current cursor.
  267. * @private
  268. * @param {Function} onEnd
  269. * Callback to call when play finished. Does not override other onEnd callbacks.
  270. * @return {void}
  271. */
  272. TimelinePath.prototype.rewind = function (onEnd) {
  273. this.pause();
  274. this.signalHandler.emitSignal('onStart');
  275. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  276. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  277. this.playEvents(-1);
  278. };
  279. /**
  280. * Reset the cursor to the beginning.
  281. * @private
  282. */
  283. TimelinePath.prototype.resetCursor = function () {
  284. this.cursor = 0;
  285. };
  286. /**
  287. * Reset the cursor to the end.
  288. * @private
  289. */
  290. TimelinePath.prototype.resetCursorEnd = function () {
  291. this.cursor = this.events.length - 1;
  292. };
  293. /**
  294. * Cancel current playing. Leaves the cursor intact.
  295. * @private
  296. * @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If
  297. * false, the path is cancelled synchronously.
  298. */
  299. TimelinePath.prototype.pause = function (fadeOut) {
  300. var timelinePath = this;
  301. // Cancel next scheduled play
  302. clearTimeout(timelinePath.nextScheduledPlay);
  303. // Cancel currently playing events
  304. Object.keys(timelinePath.eventsPlaying).forEach(function (id) {
  305. if (timelinePath.eventsPlaying[id]) {
  306. timelinePath.eventsPlaying[id].cancel(fadeOut);
  307. }
  308. });
  309. timelinePath.eventsPlaying = {};
  310. };
  311. /**
  312. * Play the events, starting from current cursor, and going in specified
  313. * direction.
  314. * @private
  315. * @param {number} direction
  316. * The direction to play, 1 for forwards and -1 for backwards.
  317. * @return {void}
  318. */
  319. TimelinePath.prototype.playEvents = function (direction) {
  320. var timelinePath = this, curEvent = timelinePath.events[this.cursor], nextEvent = timelinePath.events[this.cursor + direction], timeDiff, onEnd = function (signalData) {
  321. timelinePath.signalHandler.emitSignal('masterOnEnd', signalData);
  322. timelinePath.signalHandler.emitSignal('playOnEnd', signalData);
  323. };
  324. // Store reference to path on event
  325. curEvent.timelinePath = timelinePath;
  326. // Emit event, cancel if returns false
  327. if (timelinePath.signalHandler.emitSignal('onEventStart', curEvent) === false) {
  328. onEnd({
  329. event: curEvent,
  330. cancelled: true
  331. });
  332. return;
  333. }
  334. // Play the current event
  335. timelinePath.eventsPlaying[curEvent.id] = curEvent;
  336. curEvent.play({
  337. onEnd: function (cancelled) {
  338. var signalData = {
  339. event: curEvent,
  340. cancelled: !!cancelled
  341. };
  342. // Keep track of currently playing events for cancelling
  343. delete timelinePath.eventsPlaying[curEvent.id];
  344. // Handle onEventEnd
  345. timelinePath.signalHandler.emitSignal('onEventEnd', signalData);
  346. // Reached end of path?
  347. if (!nextEvent) {
  348. onEnd(signalData);
  349. }
  350. }
  351. });
  352. // Schedule next
  353. if (nextEvent) {
  354. timeDiff = Math.abs(nextEvent.time - curEvent.time);
  355. if (timeDiff < 1) {
  356. // Play immediately
  357. timelinePath.cursor += direction;
  358. timelinePath.playEvents(direction);
  359. }
  360. else {
  361. // Schedule after the difference in ms
  362. this.nextScheduledPlay = setTimeout(function () {
  363. timelinePath.cursor += direction;
  364. timelinePath.playEvents(direction);
  365. }, timeDiff);
  366. }
  367. }
  368. };
  369. /* ************************************************************************** *
  370. * TIMELINE *
  371. * ************************************************************************** */
  372. /**
  373. * A set of options for the Timeline class.
  374. *
  375. * @requires module:modules/sonification
  376. *
  377. * @private
  378. * @interface Highcharts.TimelineOptionsObject
  379. */ /**
  380. * List of TimelinePaths to play. Multiple paths can be grouped together and
  381. * played simultaneously by supplying an array of paths in place of a single
  382. * path.
  383. * @name Highcharts.TimelineOptionsObject#paths
  384. * @type {Array<(Highcharts.TimelinePath|Array<Highcharts.TimelinePath>)>}
  385. */ /**
  386. * Callback function to call before a path plays.
  387. * @name Highcharts.TimelineOptionsObject#onPathStart
  388. * @type {Function|undefined}
  389. */ /**
  390. * Callback function to call after a path has stopped playing.
  391. * @name Highcharts.TimelineOptionsObject#onPathEnd
  392. * @type {Function|undefined}
  393. */ /**
  394. * Callback called when the whole path is finished.
  395. * @name Highcharts.TimelineOptionsObject#onEnd
  396. * @type {Function|undefined}
  397. */
  398. /**
  399. * The Timeline class. Represents a sonification timeline with a list of
  400. * timeline paths with events to play at certain times relative to each other.
  401. *
  402. * @requires module:modules/sonification
  403. *
  404. * @private
  405. * @class
  406. * @name Highcharts.Timeline
  407. *
  408. * @param {Highcharts.TimelineOptionsObject} options
  409. * Options for the Timeline.
  410. */
  411. function Timeline(options) {
  412. this.init(options || {});
  413. }
  414. Timeline.prototype.init = function (options) {
  415. this.options = options;
  416. this.cursor = 0;
  417. this.paths = options.paths || [];
  418. this.pathsPlaying = {};
  419. this.signalHandler = new utilities.SignalHandler(['playOnEnd', 'masterOnEnd', 'onPathStart', 'onPathEnd']);
  420. this.signalHandler.registerSignalCallbacks(merge(options, { masterOnEnd: options.onEnd }));
  421. };
  422. /**
  423. * Play the timeline forwards from cursor.
  424. * @private
  425. * @param {Function} [onEnd]
  426. * Callback to call when play finished. Does not override other onEnd callbacks.
  427. * @return {void}
  428. */
  429. Timeline.prototype.play = function (onEnd) {
  430. this.pause();
  431. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  432. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  433. this.playPaths(1);
  434. };
  435. /**
  436. * Play the timeline backwards from cursor.
  437. * @private
  438. * @param {Function} onEnd
  439. * Callback to call when play finished. Does not override other onEnd callbacks.
  440. * @return {void}
  441. */
  442. Timeline.prototype.rewind = function (onEnd) {
  443. this.pause();
  444. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  445. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  446. this.playPaths(-1);
  447. };
  448. /**
  449. * Play the timeline in the specified direction.
  450. * @private
  451. * @param {number} direction
  452. * Direction to play in. 1 for forwards, -1 for backwards.
  453. * @return {void}
  454. */
  455. Timeline.prototype.playPaths = function (direction) {
  456. var timeline = this;
  457. var signalHandler = timeline.signalHandler;
  458. if (!timeline.paths.length) {
  459. var emptySignal = {
  460. cancelled: false
  461. };
  462. signalHandler.emitSignal('playOnEnd', emptySignal);
  463. signalHandler.emitSignal('masterOnEnd', emptySignal);
  464. return;
  465. }
  466. var curPaths = splat(this.paths[this.cursor]), nextPaths = this.paths[this.cursor + direction], pathsEnded = 0,
  467. // Play a path
  468. playPath = function (path) {
  469. // Emit signal and set playing state
  470. signalHandler.emitSignal('onPathStart', path);
  471. timeline.pathsPlaying[path.id] = path;
  472. // Do the play
  473. path[direction > 0 ? 'play' : 'rewind'](function (callbackData) {
  474. // Play ended callback
  475. // Data to pass to signal callbacks
  476. var cancelled = callbackData && callbackData.cancelled, signalData = {
  477. path: path,
  478. cancelled: cancelled
  479. };
  480. // Clear state and send signal
  481. delete timeline.pathsPlaying[path.id];
  482. signalHandler.emitSignal('onPathEnd', signalData);
  483. // Handle next paths
  484. pathsEnded++;
  485. if (pathsEnded >= curPaths.length) {
  486. // We finished all of the current paths for cursor.
  487. if (nextPaths && !cancelled) {
  488. // We have more paths, move cursor along
  489. timeline.cursor += direction;
  490. // Reset upcoming path cursors before playing
  491. splat(nextPaths).forEach(function (nextPath) {
  492. nextPath[direction > 0 ? 'resetCursor' : 'resetCursorEnd']();
  493. });
  494. // Play next
  495. timeline.playPaths(direction);
  496. }
  497. else {
  498. // If it is the last path in this direction, call onEnd
  499. signalHandler.emitSignal('playOnEnd', signalData);
  500. signalHandler.emitSignal('masterOnEnd', signalData);
  501. }
  502. }
  503. });
  504. };
  505. // Go through the paths under cursor and play them
  506. curPaths.forEach(function (path) {
  507. if (path) {
  508. // Store reference to timeline
  509. path.timeline = timeline;
  510. // Leave a timeout to let notes fade out before next play
  511. setTimeout(function () {
  512. playPath(path);
  513. }, H.sonification.fadeOutDuration);
  514. }
  515. });
  516. };
  517. /**
  518. * Stop the playing of the timeline. Cancels all current sounds, but does not
  519. * affect the cursor.
  520. * @private
  521. * @param {boolean} [fadeOut=false]
  522. * Whether or not to fade out as we stop. If false, the timeline is cancelled
  523. * synchronously.
  524. * @return {void}
  525. */
  526. Timeline.prototype.pause = function (fadeOut) {
  527. var timeline = this;
  528. // Cancel currently playing events
  529. Object.keys(timeline.pathsPlaying).forEach(function (id) {
  530. if (timeline.pathsPlaying[id]) {
  531. timeline.pathsPlaying[id].pause(fadeOut);
  532. }
  533. });
  534. timeline.pathsPlaying = {};
  535. };
  536. /**
  537. * Reset the cursor to the beginning of the timeline.
  538. * @private
  539. * @return {void}
  540. */
  541. Timeline.prototype.resetCursor = function () {
  542. this.paths.forEach(function (paths) {
  543. splat(paths).forEach(function (path) {
  544. path.resetCursor();
  545. });
  546. });
  547. this.cursor = 0;
  548. };
  549. /**
  550. * Reset the cursor to the end of the timeline.
  551. * @private
  552. * @return {void}
  553. */
  554. Timeline.prototype.resetCursorEnd = function () {
  555. this.paths.forEach(function (paths) {
  556. splat(paths).forEach(function (path) {
  557. path.resetCursorEnd();
  558. });
  559. });
  560. this.cursor = this.paths.length - 1;
  561. };
  562. /**
  563. * Set the current TimelineEvent under the cursor. If multiple paths are being
  564. * played at the same time, this function only affects a single path (the one
  565. * that contains the eventId that is passed in).
  566. * @private
  567. * @param {string} eventId
  568. * The ID of the timeline event to set as current.
  569. * @return {boolean}
  570. * True if the cursor was set, false if no TimelineEvent was found for this ID.
  571. */
  572. Timeline.prototype.setCursor = function (eventId) {
  573. return this.paths.some(function (paths) {
  574. return splat(paths).some(function (path) {
  575. return path.setCursor(eventId);
  576. });
  577. });
  578. };
  579. /**
  580. * Get the current TimelineEvents under the cursors. This function will return
  581. * the event under the cursor for each currently playing path, as an object
  582. * where the path ID is mapped to the TimelineEvent under that path's cursor.
  583. * @private
  584. * @return {Highcharts.Dictionary<Highcharts.TimelineEvent>}
  585. * The TimelineEvents under each path's cursors.
  586. */
  587. Timeline.prototype.getCursor = function () {
  588. return this.getCurrentPlayingPaths().reduce(function (acc, cur) {
  589. acc[cur.id] = cur.getCursor();
  590. return acc;
  591. }, {});
  592. };
  593. /**
  594. * Check if timeline is reset or at start.
  595. * @private
  596. * @return {boolean}
  597. * True if timeline is at the beginning.
  598. */
  599. Timeline.prototype.atStart = function () {
  600. if (this.cursor) {
  601. return false;
  602. }
  603. return !splat(this.paths[0]).some(function (path) {
  604. return path.cursor;
  605. });
  606. };
  607. /**
  608. * Get the current TimelinePaths being played.
  609. * @private
  610. * @return {Array<Highcharts.TimelinePath>}
  611. * The TimelinePaths currently being played.
  612. */
  613. Timeline.prototype.getCurrentPlayingPaths = function () {
  614. if (!this.paths.length) {
  615. return [];
  616. }
  617. return splat(this.paths[this.cursor]);
  618. };
  619. // Export the classes
  620. var timelineClasses = {
  621. TimelineEvent: TimelineEvent,
  622. TimelinePath: TimelinePath,
  623. Timeline: Timeline
  624. };
  625. export default timelineClasses;