All files / src/core player.ts

100% Statements 45/45
100% Branches 21/21
100% Functions 14/14
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83                                                  44x 43x 40x 40x 40x     183x 40x 34x 34x     40x 42x 42x   27x 42x   40x 11x 11x     23x     8x 8x   40x 16x 15x 15x 15x   40x 6x 4x 4x 4x     40x   111x     19x 2x     4x        
export interface PlayerState {
  readonly index: number;
  readonly playing: boolean;
  /** Playback speed multiplier. 1 plays one step per interval, 2 twice as fast. */
  readonly speed: number;
}
 
export interface Player {
  readonly state: PlayerState;
  seek(index: number): void;
  next(): void;
  prev(): void;
  play(): void;
  pause(): void;
  toggle(): void;
  /** Sets the playback speed multiplier. Takes effect at once, even mid-playback. */
  setSpeed(speed: number): void;
}
 
/** Moves through `length` steps and reports every change. Has no DOM, so it works anywhere. */
export function createPlayer(
  length: number,
  onChange: (state: PlayerState) => void,
  intervalMs = 800,
): Player {
  if (length < 1) throw new RangeError("player needs at least one step");
  if (!(intervalMs > 0)) throw new RangeError(`intervalMs ${intervalMs} must be above 0`);
  const last = length - 1;
  let index = 0;
  let speed = 1;
  let timer: ReturnType<typeof setInterval> | undefined;
 
  const state = (): PlayerState => ({ index, playing: timer !== undefined, speed });
  const stop = () => {
    clearInterval(timer);
    timer = undefined;
  };
 
  const seek = (i: number) => {
    index = Math.min(Math.max(i, 0), last);
    if (index === last) stop();
    // Restart the clock, so the step the reader lands on gets a full interval.
    else if (timer !== undefined) restart();
    onChange(state());
  };
  const pause = () => {
    stop();
    onChange(state());
  };
  function start() {
    timer = setInterval(() => seek(index + 1), intervalMs / speed);
  }
  function restart() {
    stop();
    start();
  }
  const play = () => {
    if (timer !== undefined) return;
    if (index === last) index = 0;
    start();
    onChange(state());
  };
  const setSpeed = (next: number) => {
    if (!(next > 0) || !Number.isFinite(next)) throw new RangeError(`speed ${next} must be above 0`);
    speed = next;
    if (timer !== undefined) restart();
    onChange(state());
  };
 
  return {
    get state() {
      return state();
    },
    seek,
    next: () => seek(index + 1),
    prev: () => seek(index - 1),
    play,
    pause,
    toggle: () => (timer === undefined ? play() : pause()),
    setSpeed,
  };
}