timer.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. /**
  2. * EventTarget has ~97% browser support
  3. */
  4. export class Timer extends EventTarget {
  5. private _id: number | null = null;
  6. private _active: boolean = false;
  7. private _start: number = 0;
  8. private _time: number = 0;
  9. private _pausedAt: number = 0;
  10. private _additionalTime: number = 0;
  11. private _callbacks: Map<number, (() => void)[]> = new Map();
  12. private _speed: number = 1;
  13. step = () => {
  14. if (!this._active) {
  15. return;
  16. }
  17. this._time = (window.performance.now() - this._start) * this._speed;
  18. // We don't expect _callbacks to be very large, so we can deal with a
  19. // linear search
  20. this._callbacks.forEach((value, key, callbacks) => {
  21. if (this._time >= key) {
  22. // Call every callback and then clear the callbacks at offset
  23. value.forEach(callback => callback());
  24. callbacks.set(key, []);
  25. }
  26. });
  27. this._id = window.requestAnimationFrame(this.step);
  28. };
  29. /**
  30. * @param seconds The number of seconds to start at
  31. */
  32. start(seconds?: number) {
  33. this._pausedAt = 0;
  34. // we're dividing by the speed here because we multiply
  35. // by the same factor up in step(). doing the math,
  36. // you'll see that this._time turns out to be just `seconds`.
  37. this._start = window.performance.now() - (seconds ?? 0) / this._speed;
  38. this.resume();
  39. this.step();
  40. }
  41. /**
  42. * Stops/pauses timer, can use `resume` to restart
  43. */
  44. stop() {
  45. if (!this._active) {
  46. // already paused, do nothing
  47. return;
  48. }
  49. this._pausedAt = window.performance.now();
  50. if (this._id) {
  51. window.cancelAnimationFrame(this._id);
  52. }
  53. this._active = false;
  54. }
  55. resume() {
  56. if (this._active) {
  57. // already active, do nothing
  58. return;
  59. }
  60. this._active = true;
  61. // adjust for time passing since pausing
  62. if (this._pausedAt) {
  63. this._start += window.performance.now() - this._pausedAt;
  64. this._pausedAt = 0;
  65. }
  66. this._id = window.requestAnimationFrame(this.step);
  67. }
  68. reset() {
  69. this.stop();
  70. this._start = 0;
  71. this._time = 0;
  72. this._pausedAt = 0;
  73. }
  74. /**
  75. * Allow updating `_time` directly. Needed for replay in the case
  76. * where we seek to a place in the replay before we start the
  77. * replay. `play()` in ReplayContext will call `this.getTime()`
  78. * when the play button is pressed, so we need to have the correct
  79. * playtime to start at.
  80. */
  81. setTime(time: number) {
  82. this._time = time;
  83. }
  84. getTime() {
  85. return this._time + this._additionalTime;
  86. }
  87. isActive() {
  88. return this._active;
  89. }
  90. addNotificationAtTime(offset: number, callback: () => void) {
  91. // Can't notify going backwards in time
  92. if (offset <= this._time) {
  93. return;
  94. }
  95. if (!this._callbacks.has(offset)) {
  96. this._callbacks.set(offset, []);
  97. }
  98. const callbacksAtOffset = this._callbacks.get(offset)!;
  99. callbacksAtOffset.push(callback);
  100. this._callbacks.set(offset, callbacksAtOffset);
  101. }
  102. setSpeed(speed: number) {
  103. this._speed = speed;
  104. }
  105. }