scene.js

/** @typedef {import("./tweenable").Tweenable} Tweenable */

export class Scene {
  #tweenables = []

  /**
   * The {@link Scene} class provides a way to control groups of {@link
   * Tweenable}s. It is lightweight, minimalistic, and meant to provide
   * performant {@link Tweenable} batch control that users of Shifty
   * might otherwise have to implement themselves. It is **not** a robust
   * timeline solution, and it does **not** provide utilities for sophisticated
   * animation sequencing or orchestration. If that is what you need for your
   * project, consider using a more robust tool such as
   * [Rekapi](http://jeremyckahn.github.io/rekapi/doc/) (a timeline layer built
   * on top of Shifty).
   *
   * Please be aware that {@link Scene} does **not** perform any
   * automatic cleanup. If you want to remove a {@link Tweenable} from a
   * {@link Scene}, you must do so explicitly with either {@link
   * Scene#remove} or {@link Scene#empty}.
   *
   * <p class="codepen" data-height="677" data-theme-id="0" data-default-tab="js,result" data-user="jeremyckahn" data-slug-hash="qvZKbe" style="height: 677px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="Shifty Scene Demo">
   * <span>See the Pen <a href="https://codepen.io/jeremyckahn/pen/qvZKbe/">
   * Shifty Scene Demo</a> by Jeremy Kahn (<a href="https://codepen.io/jeremyckahn">@jeremyckahn</a>)
   * on <a href="https://codepen.io">CodePen</a>.</span>
   * </p>
   * <script async src="https://static.codepen.io/assets/embed/ei.js"></script>
   * @param {...Tweenable} tweenables
   * @see https://codepen.io/jeremyckahn/pen/qvZKbe
   * @constructs Scene
   * @memberof shifty
   */
  constructor(...tweenables) {
    tweenables.forEach(this.add.bind(this))
  }

  /**
   * A copy of the internal {@link Tweenable}s array.
   * @member Scene#tweenables
   * @type {Array.<Tweenable>}
   */
  get tweenables() {
    return [...this.#tweenables]
  }

  /**
   * A list of {@link Tweenable}s in the scene that have not yet ended (playing
   * or not).
   * @member Scene#playingTweenables
   * @type {Array.<Tweenable>}
   */
  get playingTweenables() {
    return this.#tweenables.filter(tweenable => !tweenable.hasEnded())
  }

  /**
   * The {@link external:Promise}s for all {@link Tweenable}s in this
   * {@link Scene} that have been configured with {@link
   * Tweenable#setConfig}. Note that each call of {@link
   * Scene#play} or {@link Scene#pause} creates new {@link
   * external:Promise}s:
   *
   *     const scene = new Scene(new Tweenable());
   *     scene.play();
   *
   *     Promise.all(scene.promises).then(() =>
   *       // Plays the scene again upon completion, but a new promise is
   *       // created so this line only runs once.
   *       scene.play()
   *     );
   *
   * @member Scene#promises
   * @type {Array.<Promise<any>>}
   */
  get promises() {
    return this.#tweenables.map(tweenable => tweenable.then())
  }

  /**
   * Add a {@link Tweenable} to be controlled by this {@link
   * Scene}.
   * @method Scene#add
   * @param {Tweenable} tweenable
   * @return {Tweenable} The {@link Tweenable} that was added.
   */
  add(tweenable) {
    this.#tweenables.push(tweenable)

    return tweenable
  }

  /**
   * Remove a {@link Tweenable} that is controlled by this {@link
   * Scene}.
   * @method Scene#remove
   * @param {Tweenable} tweenable
   * @return {Tweenable} The {@link Tweenable} that was removed.
   */
  remove(tweenable) {
    const index = this.#tweenables.indexOf(tweenable)

    if (~index) {
      this.#tweenables.splice(index, 1)
    }

    return tweenable
  }

  /**
   * [Remove]{@link Scene#remove} all {@link Tweenable}s in this {@link
   * Scene}.
   * @method Scene#empty
   * @return {Array.<Tweenable>} The {@link Tweenable}s that were
   * removed.
   */
  empty() {
    // Deliberate of the tweenables getter here to create a temporary array
    return this.tweenables.map(this.remove.bind(this))
  }

  /**
   * Is `true` if any {@link Tweenable} in this {@link Scene} is
   * playing.
   * @method Scene#isPlaying
   * @return {boolean}
   */
  isPlaying() {
    return this.#tweenables.some(tweenable => tweenable.isPlaying())
  }

  /**
   * Play all {@link Tweenable}s from their beginning.
   * @method Scene#play
   * @return {Scene}
   */
  play() {
    this.#tweenables.forEach(tweenable => tweenable.tween())

    return this
  }

  /**
   * {@link Tweenable#pause} all {@link Tweenable}s in this
   * {@link Scene}.
   * @method Scene#pause
   * @return {Scene}
   */
  pause() {
    this.#tweenables.forEach(tweenable => tweenable.pause())
    return this
  }

  /**
   * {@link Tweenable#resume} all paused {@link Tweenable}s.
   * @method Scene#resume
   * @return {Scene}
   */
  resume() {
    this.playingTweenables.forEach(tweenable => tweenable.resume())

    return this
  }

  /**
   * {@link Tweenable#stop} all {@link Tweenable}s in this {@link
   * Scene}.
   * @method Scene#stop
   * @param {boolean} [gotoEnd]
   * @return {Scene}
   */
  stop(gotoEnd) {
    this.#tweenables.forEach(tweenable => tweenable.stop(gotoEnd))
    return this
  }
}