Skip to content

Instantly share code, notes, and snippets.

@kitlangton
Created April 19, 2020 22:09
Show Gist options
  • Save kitlangton/b1ac192761f8caf8e164b30da4d1b80a to your computer and use it in GitHub Desktop.
Save kitlangton/b1ac192761f8caf8e164b30da4d1b80a to your computer and use it in GitHub Desktop.
Laminaranimator
package animator
import com.raquo.airstream.eventstream.EventStream
import com.raquo.airstream.signal.{Signal, Var}
import magnolia.{CaseClass, Magnolia}
import org.scalajs.dom
import scala.language.experimental.macros
object Time {
private def start(): Int = {
dom.window.requestAnimationFrame(step)
}
private def step(t: Double) = {
timeVar.set(t)
start()
}
private val timeVar = Var(0.0)
lazy val time: EventStream[Double] = {
start()
timeVar.signal.changes
}
}
object Animator {
// Translated from the `react-motion` library
case class Animation(value: Double = 0, velocity: Double = 0, target: Double = 0) {
val stiffness = 170
val damping = 26
val precision = 0.01
def tick(speed: Double = 1): Animation = {
val fSpring = -stiffness * (value - target);
val fDamper = -damping * velocity;
val a = fSpring + fDamper;
val newVelocity = velocity + a * (1.0 / 80.0) * speed;
val newValue = value + newVelocity * (1.0 / 80.0);
if (Math.abs(newVelocity) < precision && Math.abs(newValue - target) < precision)
copy(value = target, velocity = 0);
else
copy(value = newValue, velocity = newVelocity);
}
}
object Animation {
def fromValue(value: Double): Animation = Animation(value = value, target = value)
}
// TODO: Certainly there could be a cleaner safer representation than a Map.
trait Animatable[A] {
def toAnimations(value: A): Map[String, Animation]
def fromAnimations(animations: Map[String, Animation]): A
}
// Magnolia derivation for Case Classes
// TODO: Implement for Sealed Traits using Magnolia's `dispatch`
object Animatable {
type Typeclass[T] = Animatable[T]
def combine[T](ctx: CaseClass[Animatable, T]): Animatable[T] = new Animatable[T] {
override def toAnimations(value: T): Map[String, Animation] =
ctx.parameters.flatMap { param =>
param.typeclass.toAnimations(param.dereference(value)).toList.map {
case (key, value) =>
s"${param.label}__$key" -> value
}
}.toMap
override def fromAnimations(animations: Map[String, Animation]): T = {
ctx.construct { param =>
val paramMap = animations
.filter(_._1.startsWith(param.label))
.map { case (key, value) => key.drop(param.label.length + 2) -> value }
param.typeclass.fromAnimations(paramMap)
}
}
}
implicit def gen[T]: Animatable[T] = macro Magnolia.gen[T]
}
// Built in for doubles
implicit val doubleAnimatable: Animatable[Double] = new Animatable[Double] {
override def toAnimations(value: Double): Map[String, Animation] = Map("double" -> Animation.fromValue(value))
override def fromAnimations(animations: Map[String, Animation]): Double = animations("double").value
}
// Pass a signal of your animatable value into `animate`, then pass the resulting `EventStream`
// into a method that creates some ReactiveElement with it.
def animate[A]($value: Signal[A], speed: Double = 1)(implicit animatable: Animatable[A]): EventStream[A] = {
Time.time
.withCurrentValueOf($value)
.fold[Map[String, Animation]](Map.empty) {
case (map, (_,value)) if map.isEmpty =>
animatable.toAnimations(value)
case (animations, (_,target)) =>
val next = animatable.toAnimations(target)
animations.map {
case (key, animation) =>
(key, animation.copy(target = next(key).target).tick(speed))
}
}
.changes
.filter(_.nonEmpty)
.map(animatable.fromAnimations)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment