Timeline.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  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 '../../parts/Globals.js';
  14. import U from '../../parts/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. // We need to sort our events by time
  186. this.sortEvents();
  187. // Get map from event ID to index
  188. this.updateEventIdMap();
  189. // Signal events to fire
  190. this.signalHandler = new utilities.SignalHandler(['playOnEnd', 'masterOnEnd', 'onStart', 'onEventStart', 'onEventEnd']);
  191. this.signalHandler.registerSignalCallbacks(merge(options, { masterOnEnd: options.onEnd }));
  192. };
  193. /**
  194. * Sort the internal event list by time.
  195. * @private
  196. */
  197. TimelinePath.prototype.sortEvents = function () {
  198. this.events = this.events.sort(function (a, b) {
  199. return a.time - b.time;
  200. });
  201. };
  202. /**
  203. * Update the internal eventId to index map.
  204. * @private
  205. */
  206. TimelinePath.prototype.updateEventIdMap = function () {
  207. this.eventIdMap = this.events.reduce(function (acc, cur, i) {
  208. acc[cur.id] = i;
  209. return acc;
  210. }, {});
  211. };
  212. /**
  213. * Add events to the path. Should not be done while the path is playing.
  214. * The new events are inserted according to their time property.
  215. * @private
  216. * @param {Array<Highcharts.TimelineEvent>} newEvents - The new timeline events
  217. * to add.
  218. */
  219. TimelinePath.prototype.addTimelineEvents = function (newEvents) {
  220. this.events = this.events.concat(newEvents);
  221. this.sortEvents(); // Sort events by time
  222. this.updateEventIdMap(); // Update the event ID to index map
  223. };
  224. /**
  225. * Get the current TimelineEvent under the cursor.
  226. * @private
  227. * @return {Highcharts.TimelineEvent} The current timeline event.
  228. */
  229. TimelinePath.prototype.getCursor = function () {
  230. return this.events[this.cursor];
  231. };
  232. /**
  233. * Set the current TimelineEvent under the cursor.
  234. * @private
  235. * @param {string} eventId
  236. * The ID of the timeline event to set as current.
  237. * @return {boolean}
  238. * True if there is an event with this ID in the path. False otherwise.
  239. */
  240. TimelinePath.prototype.setCursor = function (eventId) {
  241. var ix = this.eventIdMap[eventId];
  242. if (typeof ix !== 'undefined') {
  243. this.cursor = ix;
  244. return true;
  245. }
  246. return false;
  247. };
  248. /**
  249. * Play the timeline from the current cursor.
  250. * @private
  251. * @param {Function} onEnd
  252. * Callback to call when play finished. Does not override other onEnd callbacks.
  253. * @return {void}
  254. */
  255. TimelinePath.prototype.play = function (onEnd) {
  256. this.pause();
  257. this.signalHandler.emitSignal('onStart');
  258. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  259. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  260. this.playEvents(1);
  261. };
  262. /**
  263. * Play the timeline backwards from the current cursor.
  264. * @private
  265. * @param {Function} onEnd
  266. * Callback to call when play finished. Does not override other onEnd callbacks.
  267. * @return {void}
  268. */
  269. TimelinePath.prototype.rewind = function (onEnd) {
  270. this.pause();
  271. this.signalHandler.emitSignal('onStart');
  272. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  273. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  274. this.playEvents(-1);
  275. };
  276. /**
  277. * Reset the cursor to the beginning.
  278. * @private
  279. */
  280. TimelinePath.prototype.resetCursor = function () {
  281. this.cursor = 0;
  282. };
  283. /**
  284. * Reset the cursor to the end.
  285. * @private
  286. */
  287. TimelinePath.prototype.resetCursorEnd = function () {
  288. this.cursor = this.events.length - 1;
  289. };
  290. /**
  291. * Cancel current playing. Leaves the cursor intact.
  292. * @private
  293. * @param {boolean} [fadeOut=false] - Whether or not to fade out as we stop. If
  294. * false, the path is cancelled synchronously.
  295. */
  296. TimelinePath.prototype.pause = function (fadeOut) {
  297. var timelinePath = this;
  298. // Cancel next scheduled play
  299. clearTimeout(timelinePath.nextScheduledPlay);
  300. // Cancel currently playing events
  301. Object.keys(timelinePath.eventsPlaying).forEach(function (id) {
  302. if (timelinePath.eventsPlaying[id]) {
  303. timelinePath.eventsPlaying[id].cancel(fadeOut);
  304. }
  305. });
  306. timelinePath.eventsPlaying = {};
  307. };
  308. /**
  309. * Play the events, starting from current cursor, and going in specified
  310. * direction.
  311. * @private
  312. * @param {number} direction
  313. * The direction to play, 1 for forwards and -1 for backwards.
  314. * @return {void}
  315. */
  316. TimelinePath.prototype.playEvents = function (direction) {
  317. var timelinePath = this, curEvent = timelinePath.events[this.cursor], nextEvent = timelinePath.events[this.cursor + direction], timeDiff, onEnd = function (signalData) {
  318. timelinePath.signalHandler.emitSignal('masterOnEnd', signalData);
  319. timelinePath.signalHandler.emitSignal('playOnEnd', signalData);
  320. };
  321. // Store reference to path on event
  322. curEvent.timelinePath = timelinePath;
  323. // Emit event, cancel if returns false
  324. if (timelinePath.signalHandler.emitSignal('onEventStart', curEvent) === false) {
  325. onEnd({
  326. event: curEvent,
  327. cancelled: true
  328. });
  329. return;
  330. }
  331. // Play the current event
  332. timelinePath.eventsPlaying[curEvent.id] = curEvent;
  333. curEvent.play({
  334. onEnd: function (cancelled) {
  335. var signalData = {
  336. event: curEvent,
  337. cancelled: !!cancelled
  338. };
  339. // Keep track of currently playing events for cancelling
  340. delete timelinePath.eventsPlaying[curEvent.id];
  341. // Handle onEventEnd
  342. timelinePath.signalHandler.emitSignal('onEventEnd', signalData);
  343. // Reached end of path?
  344. if (!nextEvent) {
  345. onEnd(signalData);
  346. }
  347. }
  348. });
  349. // Schedule next
  350. if (nextEvent) {
  351. timeDiff = Math.abs(nextEvent.time - curEvent.time);
  352. if (timeDiff < 1) {
  353. // Play immediately
  354. timelinePath.cursor += direction;
  355. timelinePath.playEvents(direction);
  356. }
  357. else {
  358. // Schedule after the difference in ms
  359. this.nextScheduledPlay = setTimeout(function () {
  360. timelinePath.cursor += direction;
  361. timelinePath.playEvents(direction);
  362. }, timeDiff);
  363. }
  364. }
  365. };
  366. /* ************************************************************************** *
  367. * TIMELINE *
  368. * ************************************************************************** */
  369. /**
  370. * A set of options for the Timeline class.
  371. *
  372. * @requires module:modules/sonification
  373. *
  374. * @private
  375. * @interface Highcharts.TimelineOptionsObject
  376. */ /**
  377. * List of TimelinePaths to play. Multiple paths can be grouped together and
  378. * played simultaneously by supplying an array of paths in place of a single
  379. * path.
  380. * @name Highcharts.TimelineOptionsObject#paths
  381. * @type {Array<(Highcharts.TimelinePath|Array<Highcharts.TimelinePath>)>}
  382. */ /**
  383. * Callback function to call before a path plays.
  384. * @name Highcharts.TimelineOptionsObject#onPathStart
  385. * @type {Function|undefined}
  386. */ /**
  387. * Callback function to call after a path has stopped playing.
  388. * @name Highcharts.TimelineOptionsObject#onPathEnd
  389. * @type {Function|undefined}
  390. */ /**
  391. * Callback called when the whole path is finished.
  392. * @name Highcharts.TimelineOptionsObject#onEnd
  393. * @type {Function|undefined}
  394. */
  395. /**
  396. * The Timeline class. Represents a sonification timeline with a list of
  397. * timeline paths with events to play at certain times relative to each other.
  398. *
  399. * @requires module:modules/sonification
  400. *
  401. * @private
  402. * @class
  403. * @name Highcharts.Timeline
  404. *
  405. * @param {Highcharts.TimelineOptionsObject} options
  406. * Options for the Timeline.
  407. */
  408. function Timeline(options) {
  409. this.init(options || {});
  410. }
  411. Timeline.prototype.init = function (options) {
  412. this.options = options;
  413. this.cursor = 0;
  414. this.paths = options.paths;
  415. this.pathsPlaying = {};
  416. this.signalHandler = new utilities.SignalHandler(['playOnEnd', 'masterOnEnd', 'onPathStart', 'onPathEnd']);
  417. this.signalHandler.registerSignalCallbacks(merge(options, { masterOnEnd: options.onEnd }));
  418. };
  419. /**
  420. * Play the timeline forwards from cursor.
  421. * @private
  422. * @param {Function} [onEnd]
  423. * Callback to call when play finished. Does not override other onEnd callbacks.
  424. * @return {void}
  425. */
  426. Timeline.prototype.play = function (onEnd) {
  427. this.pause();
  428. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  429. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  430. this.playPaths(1);
  431. };
  432. /**
  433. * Play the timeline backwards from cursor.
  434. * @private
  435. * @param {Function} onEnd
  436. * Callback to call when play finished. Does not override other onEnd callbacks.
  437. * @return {void}
  438. */
  439. Timeline.prototype.rewind = function (onEnd) {
  440. this.pause();
  441. this.signalHandler.clearSignalCallbacks(['playOnEnd']);
  442. this.signalHandler.registerSignalCallbacks({ playOnEnd: onEnd });
  443. this.playPaths(-1);
  444. };
  445. /**
  446. * Play the timeline in the specified direction.
  447. * @private
  448. * @param {number} direction
  449. * Direction to play in. 1 for forwards, -1 for backwards.
  450. * @return {void}
  451. */
  452. Timeline.prototype.playPaths = function (direction) {
  453. var curPaths = splat(this.paths[this.cursor]), nextPaths = this.paths[this.cursor + direction], timeline = this, signalHandler = this.signalHandler, pathsEnded = 0,
  454. // Play a path
  455. playPath = function (path) {
  456. // Emit signal and set playing state
  457. signalHandler.emitSignal('onPathStart', path);
  458. timeline.pathsPlaying[path.id] = path;
  459. // Do the play
  460. path[direction > 0 ? 'play' : 'rewind'](function (callbackData) {
  461. // Play ended callback
  462. // Data to pass to signal callbacks
  463. var cancelled = callbackData && callbackData.cancelled, signalData = {
  464. path: path,
  465. cancelled: cancelled
  466. };
  467. // Clear state and send signal
  468. delete timeline.pathsPlaying[path.id];
  469. signalHandler.emitSignal('onPathEnd', signalData);
  470. // Handle next paths
  471. pathsEnded++;
  472. if (pathsEnded >= curPaths.length) {
  473. // We finished all of the current paths for cursor.
  474. if (nextPaths && !cancelled) {
  475. // We have more paths, move cursor along
  476. timeline.cursor += direction;
  477. // Reset upcoming path cursors before playing
  478. splat(nextPaths).forEach(function (nextPath) {
  479. nextPath[direction > 0 ? 'resetCursor' : 'resetCursorEnd']();
  480. });
  481. // Play next
  482. timeline.playPaths(direction);
  483. }
  484. else {
  485. // If it is the last path in this direction, call onEnd
  486. signalHandler.emitSignal('playOnEnd', signalData);
  487. signalHandler.emitSignal('masterOnEnd', signalData);
  488. }
  489. }
  490. });
  491. };
  492. // Go through the paths under cursor and play them
  493. curPaths.forEach(function (path) {
  494. if (path) {
  495. // Store reference to timeline
  496. path.timeline = timeline;
  497. // Leave a timeout to let notes fade out before next play
  498. setTimeout(function () {
  499. playPath(path);
  500. }, H.sonification.fadeOutDuration);
  501. }
  502. });
  503. };
  504. /**
  505. * Stop the playing of the timeline. Cancels all current sounds, but does not
  506. * affect the cursor.
  507. * @private
  508. * @param {boolean} [fadeOut=false]
  509. * Whether or not to fade out as we stop. If false, the timeline is cancelled
  510. * synchronously.
  511. * @return {void}
  512. */
  513. Timeline.prototype.pause = function (fadeOut) {
  514. var timeline = this;
  515. // Cancel currently playing events
  516. Object.keys(timeline.pathsPlaying).forEach(function (id) {
  517. if (timeline.pathsPlaying[id]) {
  518. timeline.pathsPlaying[id].pause(fadeOut);
  519. }
  520. });
  521. timeline.pathsPlaying = {};
  522. };
  523. /**
  524. * Reset the cursor to the beginning of the timeline.
  525. * @private
  526. * @return {void}
  527. */
  528. Timeline.prototype.resetCursor = function () {
  529. this.paths.forEach(function (paths) {
  530. splat(paths).forEach(function (path) {
  531. path.resetCursor();
  532. });
  533. });
  534. this.cursor = 0;
  535. };
  536. /**
  537. * Reset the cursor to the end of the timeline.
  538. * @private
  539. * @return {void}
  540. */
  541. Timeline.prototype.resetCursorEnd = function () {
  542. this.paths.forEach(function (paths) {
  543. splat(paths).forEach(function (path) {
  544. path.resetCursorEnd();
  545. });
  546. });
  547. this.cursor = this.paths.length - 1;
  548. };
  549. /**
  550. * Set the current TimelineEvent under the cursor. If multiple paths are being
  551. * played at the same time, this function only affects a single path (the one
  552. * that contains the eventId that is passed in).
  553. * @private
  554. * @param {string} eventId
  555. * The ID of the timeline event to set as current.
  556. * @return {boolean}
  557. * True if the cursor was set, false if no TimelineEvent was found for this ID.
  558. */
  559. Timeline.prototype.setCursor = function (eventId) {
  560. return this.paths.some(function (paths) {
  561. return splat(paths).some(function (path) {
  562. return path.setCursor(eventId);
  563. });
  564. });
  565. };
  566. /**
  567. * Get the current TimelineEvents under the cursors. This function will return
  568. * the event under the cursor for each currently playing path, as an object
  569. * where the path ID is mapped to the TimelineEvent under that path's cursor.
  570. * @private
  571. * @return {Highcharts.Dictionary<Highcharts.TimelineEvent>}
  572. * The TimelineEvents under each path's cursors.
  573. */
  574. Timeline.prototype.getCursor = function () {
  575. return this.getCurrentPlayingPaths().reduce(function (acc, cur) {
  576. acc[cur.id] = cur.getCursor();
  577. return acc;
  578. }, {});
  579. };
  580. /**
  581. * Check if timeline is reset or at start.
  582. * @private
  583. * @return {boolean}
  584. * True if timeline is at the beginning.
  585. */
  586. Timeline.prototype.atStart = function () {
  587. return !this.getCurrentPlayingPaths().some(function (path) {
  588. return path.cursor;
  589. });
  590. };
  591. /**
  592. * Get the current TimelinePaths being played.
  593. * @private
  594. * @return {Array<Highcharts.TimelinePath>}
  595. * The TimelinePaths currently being played.
  596. */
  597. Timeline.prototype.getCurrentPlayingPaths = function () {
  598. return splat(this.paths[this.cursor]);
  599. };
  600. // Export the classes
  601. var timelineClasses = {
  602. TimelineEvent: TimelineEvent,
  603. TimelinePath: TimelinePath,
  604. Timeline: Timeline
  605. };
  606. export default timelineClasses;