Created
April 19, 2020 22:09
-
-
Save kitlangton/b1ac192761f8caf8e164b30da4d1b80a to your computer and use it in GitHub Desktop.
Laminaranimator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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