All Downloads are FREE. Search and download functionalities are using the official Maven repository.

indigo.shared.animation.timeline.Timeline.scala Maven / Gradle / Ivy

The newest version!
package indigo.shared.animation.timeline

import indigo.shared.collections.Batch
import indigo.shared.temporal.Signal
import indigo.shared.temporal.SignalFunction
import indigo.shared.time.Seconds

import scala.annotation.tailrec
import scala.annotation.targetName

opaque type Timeline[A] = Batch[TimeWindow[A]]

object Timeline:
  inline def apply[A](timeWindows: Batch[TimeWindow[A]]): Timeline[A] =
    timeWindows

  inline def apply[A](timeWindows: TimeWindow[A]*): Timeline[A] =
    Timeline(Batch.fromSeq(timeWindows))

  @targetName("timeline_apply_slots")
  inline def apply[A](timeSlots: TimeSlot[A]*): Timeline[A] =
    Timeline(Batch.fromSeq(timeSlots).flatMap(_.toWindows))

  @targetName("timeline_apply_animations")
  inline def apply[A](animations: TimelineAnimation[A]*): Timeline[A] =
    Timeline(Batch.fromSeq(animations).flatMap(_.compile.toWindows))

  def empty[A]: Timeline[A] =
    Timeline(Batch.empty[TimeWindow[A]])

  extension [A](tl: Timeline[A])
    /** Add a `TimeSlot[A]` to the animation as a `TimeWindow`. Time slots are the building block of animation and can
      * be combined together to build up an animation, for example: `startAfter(2.seconds) andThen
      * show(5.seconds)(_.withAlpha(0.5))`
      *
      * @param animation
      *   the `TimeSlot[A]` to add
      * @return
      *   `Timeline[A]`
      */
    def add(animation: TimeSlot[A]): Timeline[A] =
      addWindows(animation.toWindows)

    /** Add a time window to the animation.
      *
      * @param nextWindow
      *   `TimeWindow[A]`
      * @return
      *   `Timeline[A]`
      */
    def addWindow(nextWindow: TimeWindow[A]): Timeline[A] =
      tl :+ nextWindow

    /** Add a batch of time windows to the animation.
      *
      * @param nextWindows
      *   `Batch[TimeWindow[A]]`
      * @return
      *   `Timeline[A]`
      */
    def addWindows(nextWindows: Batch[TimeWindow[A]]): Timeline[A] =
      tl ++ nextWindows

    /** Samples the animation at the given time to produce a value. The return type is ultimately optional because you
      * could sample the animation outside of the animations time range.
      *
      * @param time
      *   the current running time in `Second`s to sample the animation at.
      * @return
      *   `A => Option[A]` where `A` is the 'subject', the thing to be animated.
      */
    def at(time: Seconds): A => Option[A] = subject =>
      @tailrec
      def rec(remaining: Batch[TimeWindow[A]], acc: Option[A]): Option[A] =
        if remaining.isEmpty then acc
        else
          val x  = remaining.head
          val xs = remaining.tail

          if x.within(time) then
            val value = acc.getOrElse(subject)
            val next  = (Signal.Time |> x.modifier(value)).at(time - x.start)
            rec(xs, Some(next))
          else rec(xs, acc)

      rec(tl, None)

    /** Calls `at` but always returns a value, using the original subject `A` if no other value would be produced by the
      * animation.
      *
      * @param time
      *   the current running time in `Second`s to sample the animation at.
      * @return
      *   `A`
      */
    def atOrElse(time: Seconds): A => A =
      subject => at(time)(subject).getOrElse(subject)

    /** Calls `at` but always returns a value, using the given default subject `A` if no other value would be produced
      * by the animation.
      *
      * @param time
      *   the current running time in `Second`s to sample the animation at.
      * @param default
      *   the subject to use if no other value is produced
      * @return
      *   `A`
      */
    def atOrElse(time: Seconds, default: A): A => A =
      subject => at(time)(subject).getOrElse(default)

    /** Clamps the time to be within the animation's duration, ensuring that the last frame is present even when over
      * the time windows. Still produces an optional value because you may have a delay at the beginning of you
      * animation, for example.
      *
      * @param time
      *   the current running time in `Second`s to sample the animation at.
      * @return
      *   `Option[A]`
      */
    def atOrLast(time: Seconds): A => Option[A] =
      val d = duration
      subject => at(if time < d then time else d)(subject)

    /** Give the time windows as a Batch.
      *
      * @return
      *   `Batch[TimeWindow[A]]`
      */
    def toWindows: Batch[TimeWindow[A]] =
      tl

    /** Return the total time duration of the animation in seconds, including any initial delays.
      *
      * @return
      *   `Seconds`
      */
    def duration: Seconds =
      tl.maxByOption(_.end.toDouble).fold(Seconds.zero)(_.end)

    /** Alias for duration, returns the total time duration of the animation in seconds, including any initial delays.
      *
      * @return
      *   `Seconds`
      */
    def length: Seconds =
      duration




© 2015 - 2024 Weber Informatics LLC | Privacy Policy