Timeline.js 20 KB

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