From cd7cc5fe19485d60d21c7a58023ba1c2eb9005b6 Mon Sep 17 00:00:00 2001 From: Joris Date: Mon, 13 Feb 2017 15:51:53 +0100 Subject: Animate book detail show and hide --- src/main/scala/reading/Route.scala | 13 ++--- .../scala/reading/component/widget/Animate.scala | 55 ++++++++++++++++++++++ .../scala/reading/component/widget/Modal.scala | 36 ++++++++++++-- .../reading/component/widget/Transition.scala | 12 +++++ .../reading/component/widget/style/Modal.scala | 24 ++++------ 5 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 src/main/scala/reading/component/widget/Animate.scala create mode 100644 src/main/scala/reading/component/widget/Transition.scala diff --git a/src/main/scala/reading/Route.scala b/src/main/scala/reading/Route.scala index c1f993e..9295d49 100644 --- a/src/main/scala/reading/Route.scala +++ b/src/main/scala/reading/Route.scala @@ -1,6 +1,7 @@ package reading -import org.scalajs.dom +import org.scalajs.dom.window +import org.scalajs.dom.raw.PopStateEvent import scala.scalajs.js.URIUtils import rx.Var @@ -12,10 +13,10 @@ sealed trait Route object Route { case class Books(filters: Seq[Filter]) extends Route - val current: Var[Route] = Var(parse(dom.window.location.hash)) + val current: Var[Route] = Var(parse(window.location.hash)) - dom.window.onpopstate = (e: dom.raw.PopStateEvent) => { - current() = parse(dom.window.location.hash) + window.onpopstate = (e: PopStateEvent) => { + current() = parse(window.location.hash) } def parse(hash: String): Route = @@ -50,7 +51,7 @@ object Route { case Books(filters) => "/books" ++ (if (filters.nonEmpty) filters.map(filter => s"${filter.kind}=${filter.nonFormattedName}").mkString("?", "&", "") else "") case _ => "/books" } - dom.window.location.origin + dom.window.location.pathname + "#" + URIUtils.encodeURI(hash) + window.location.origin + window.location.pathname + "#" + URIUtils.encodeURI(hash) } def goTo(route: Route): Unit = { @@ -59,6 +60,6 @@ object Route { } def push(route: Route): Unit = { - dom.window.history.pushState(null, "", url(route)); + window.history.pushState(null, "", url(route)); } } diff --git a/src/main/scala/reading/component/widget/Animate.scala b/src/main/scala/reading/component/widget/Animate.scala new file mode 100644 index 0000000..0e848aa --- /dev/null +++ b/src/main/scala/reading/component/widget/Animate.scala @@ -0,0 +1,55 @@ +package reading.component.widget + +import scala.collection.mutable.Map + +import org.scalajs.dom.{ window, document } +import org.scalajs.dom.raw.HTMLElement + +object Animate { + val animationFrames: Map[String, Int] = Map.empty + + def apply( + id: String, + duration: Double, + transition: (Double, Double) => Double, + animate: (Double, HTMLElement) => Unit, + onEnd: => Unit = () + ): Unit = { + animationFrames.get(id) match { + case Some(animationFrame) => window.cancelAnimationFrame(animationFrame) + case None => () + } + val animationFrame = window.requestAnimationFrame(ts => + frame(id, ts, duration, transition, animate, onEnd)(ts)) + animationFrames.put(id, animationFrame) + () + } + + private def frame( + id: String, + start: Double, + duration: Double, + transition: (Double, Double) => Double, + animate: (Double, HTMLElement) => Unit, + onEnd: => Unit + )( + timestamp: Double + ): Unit = { + document.getElementById(id).asInstanceOf[HTMLElement] match { + case element: HTMLElement => + val elapsed = timestamp - start + if (elapsed < duration) { + animate(Transition.easeOut(elapsed, duration), element) + val animationFrame = window.requestAnimationFrame(frame(id, start, duration, transition, animate, onEnd)) + animationFrames.put(id, animationFrame) + } else { + animate(1, element) + onEnd + } + case _ => + val animationFrame = window.requestAnimationFrame(ts => frame(id, ts, duration, transition, animate, onEnd)(ts)) + animationFrames.put(id, animationFrame) + } + () + } +} diff --git a/src/main/scala/reading/component/widget/Modal.scala b/src/main/scala/reading/component/widget/Modal.scala index fe10d1f..02c42be 100644 --- a/src/main/scala/reading/component/widget/Modal.scala +++ b/src/main/scala/reading/component/widget/Modal.scala @@ -1,24 +1,41 @@ package reading.component.widget -import rx._ -import Ctx.Owner.Unsafe._ +import scala.util.Random -import scalatags.JsDom.all._ +import org.scalajs.dom.raw.HTMLElement +import rx._ +import rx.Ctx.Owner.Unsafe._ import scalacss.Defaults._ import scalacss.ScalatagsCss._ +import scalatags.JsDom.all._ import reading.component.widget.style.{ Modal => ModalStyle } import reading.utils.{ RxAttr } object Modal { def apply(onClose: => Unit)(content: HtmlTag): HtmlTag = { + val modalId = s"modal${Random.nextInt}" + + Animate( + id = modalId, + duration = 300, + transition = Transition.easeOut, + animate = (progress, element) => { + element.style.opacity = s"$progress" + element.childNodes(2) match { + case e: HTMLElement => e.style.transform = s"translateY(${40 * (progress - 1)}px)" + } + } + ) + div( ModalStyle.render, ModalStyle.modal, + id := modalId, div( ModalStyle.curtain, - RxAttr(onclick, Rx(() => onClose)) + RxAttr(onclick, Rx(() => close(modalId, onClose))) ), div( @@ -26,10 +43,19 @@ object Modal { content, button( ModalStyle.close, - RxAttr(onclick, Rx(() => onClose)), + RxAttr(onclick, Rx(() => close(modalId, onClose))), "Fermer" ) ) ) } + + private def close(modalId: String, onClose: => Unit): Unit = + Animate( + id = modalId, + duration = 300, + transition = Transition.linear, + onEnd = onClose, + animate = (progress, element) => element.style.opacity = s"${1 - progress}" + ) } diff --git a/src/main/scala/reading/component/widget/Transition.scala b/src/main/scala/reading/component/widget/Transition.scala new file mode 100644 index 0000000..aa8ff3d --- /dev/null +++ b/src/main/scala/reading/component/widget/Transition.scala @@ -0,0 +1,12 @@ +package reading.component.widget + +object Transition { + def linear(progress: Double, total: Double): Double = + progress / total + + def easeIn(progress: Double, total: Double): Double = + math.pow(progress, 2) / math.pow(total, 2) + + def easeOut(progress: Double, total: Double): Double = + (-1) * (progress / total) * (progress / total - 2) +} diff --git a/src/main/scala/reading/component/widget/style/Modal.scala b/src/main/scala/reading/component/widget/style/Modal.scala index bfcc276..ae37c4b 100644 --- a/src/main/scala/reading/component/widget/style/Modal.scala +++ b/src/main/scala/reading/component/widget/style/Modal.scala @@ -19,23 +19,19 @@ object Modal extends StyleSheet.Inline { right(0.px), bottom(0.px), left(0.px), - overflowY.scroll + overflowY.scroll, + opacity(0) ) val curtain = style( - Media.desktop( - width(100.%%), - height(100.%%), - position.absolute, - top(0.px), - left(0.px), - backgroundColor(C.black.value), - opacity(0.5), - cursor.pointer - ), - Media.mobile( - display.none - ) + width(100.%%), + height(100.%%), + position.absolute, + top(0.px), + left(0.px), + backgroundColor(C.black.value), + opacity(0.7), + cursor.pointer ) val content = style( -- cgit v1.2.3