Permalink
Browse files

Mobile support: Before: Instant move response via timer updates

Problem: When updating many elements mobile device queues slow changes. So even after touch up, you can se changes caching up.

Showing debug information for Frames per second (fps) and count of objects being freshly rendered (R).
So it could be easier to test, if skipping rendering actually works and witch parts need more optimisation.

Standard ScalaJs library did not had developer-friendly setTimeout function, so added my own wrapper.

Added some styling when moving elements.
  • Loading branch information...
aurelijusb committed Oct 16, 2016
1 parent c2423aa commit 50c81bb83ad06e9192cef6733086bd694cf49433
@@ -0,0 +1,12 @@
package com.auginte.eventsourced
import org.scalajs.dom._
object Async {
def timer(interval: Int)(f: => Boolean): Unit = {
val continue = f
if (continue) {
window.setTimeout(() => timer(interval)(f), interval)
}
}
}
@@ -21,6 +21,8 @@ case class LoadElement(text: String) extends Action
case class ElementMouseDown(aggregateId: AggregateId, x: Int, y: Int) extends Action
case class MouseMove(x: Int, y: Int) extends Action
case class ElementMouseUp(aggregateId: AggregateId, x: Int, y: Int) extends Action
case class MouseUp(x: Int, y: Int) extends Action
@@ -45,49 +47,60 @@ object ElementsApp extends Circuit[ElementsModel]{
val event = Generic.Event.created(text)
val data = Pickle.intoString(event) + "\n"
sendAjax(Config.url, data)
updated(value)
noChange // Depending on backend, while inner cache not implemented
case LoadElement(raw) =>
Unpickle[Generic.Event].fromString(raw) match {
case Success(e) =>
updated(value.copy(elements = value.elements.updated(e.aggregateId, e.data)))
case Failure(err) =>
dom.console.warn("Unable to unmarshal event", err.toString, raw)
updated(value)
noChange
}
case ElementMouseDown(id, x, y) =>
updated(value.copy(lastMousePosition = MousePosition(x, y), selectedElementId = Some(id)))
case MouseMove(x, y) =>
val mousePosition = MousePosition(x, y)
value.selectedElementId match {
case Some(id) => value.elements.get(id) match {
case Some(textElement: Text) =>
val updatedElement = moveElement(textElement, value.lastMousePosition, mousePosition)
movedElement(id, updatedElement, mousePosition, value.selectedElementId)
case Some(element) =>
dom.console.warn(s"Element to drag not supported $id: $element")
noChange
case None =>
dom.console.warn("Element to drag not found", id)
noChange
}
case None => noChange // No element selected
}
case ElementMouseUp(id, x, y) =>
val mousePosition = MousePosition(x, y)
value.elements.get(id) match {
case Some(textElement: Text) =>
val newElement = moveElement(textElement, value.lastMousePosition, mousePosition)
storeUpdated(id, newElement)
updated(value.copy(elements = value.elements.updated(id, newElement), lastMousePosition = mousePosition, selectedElementId = None))
val updatedElement = moveElement(textElement, value.lastMousePosition, mousePosition)
storedElement(id, updatedElement, mousePosition, selected = None)
case other =>
dom.console.warn(s"Trying to mouse up not stored element: $id: $other")
updated(value)
noChange
}
case MouseUp(x, y) =>
val mousePosition = MousePosition(x, y)
value.selectedElementId match {
case Some(id) => value.elements.get(id) match {
case Some(textElement: Text) =>
val mousePosition = MousePosition(x, y)
val newElement = moveElement(textElement, value.lastMousePosition, mousePosition)
storeUpdated(id, newElement)
updated(value.copy(elements = value.elements.updated(id, newElement), lastMousePosition = mousePosition, selectedElementId = None))
val updatedElement = moveElement(textElement, value.lastMousePosition, mousePosition)
storedElement(id, updatedElement, mousePosition, selected = None)
case other =>
dom.console.warn(s"Trying to move not stored selected element: $id: $other")
updated(value)
noChange
}
case None => updated(value)
case None => noChange
}
}
@@ -100,6 +113,15 @@ object ElementsApp extends Circuit[ElementsModel]{
private def moveElement(element: Text, oldPos: MousePosition, newPos: MousePosition): Generic.Data = {
element.copy(x = element.x + (newPos.x - oldPos.x), y = element.y + (newPos.y - oldPos.y))
}
private def movedElement(id: AggregateId, element: Generic.Data, mousePosition: MousePosition, selected: Option[AggregateId]) =
updated(value.copy(elements = value.elements.updated(id, element), lastMousePosition = mousePosition, selectedElementId = selected))
private def storedElement(id: AggregateId, element: Generic.Data, mousePosition: MousePosition, selected: Option[AggregateId]) = {
storeUpdated(id, element)
movedElement(id, element, mousePosition, selected)
}
}
private def handleAjax(url: String, input: String): Future[String] =
@@ -4,7 +4,6 @@ import com.auginte.eventsourced.Generic.Text
import com.auginte.eventsourced.vdom.Implicits._
import com.auginte.eventsourced.vdom.{Div, Elements}
import diode._
import org.scalajs.dom
import org.scalajs.dom.{MouseEvent, Touch, TouchEvent}
import scalatags.JsDom.all.{style => _}
@@ -20,6 +19,8 @@ class ElementsView(elements: ModelR[_, Map[Generic.AggregateId, Generic.Data]],
onMouseUp := { (e: MouseEvent) => dispatch(MouseUp(e.screenX.toInt, e.screenY.toInt)) },
onTouchEnd := { (e: TouchEvent) => dispatch(MouseUp(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
onTouchCancel := { (e: TouchEvent) => dispatch(MouseUp(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
onMouseMove := { (e: MouseEvent) => dispatch(MouseMove(e.screenX.toInt, e.screenY.toInt)) },
onTouchMove := { (e: TouchEvent) => dispatch(MouseMove(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
Elements(innerElements)
)
)
@@ -29,12 +30,13 @@ class ElementsView(elements: ModelR[_, Map[Generic.AggregateId, Generic.Data]],
case (id: Generic.AggregateId, t@Text(text, x, y, scale)) =>
Div(
className := "element",
style := s"""position: absolute; left: $x; top: $y; display: inline; border: 1px solid red;""",
style := s"""position: absolute; left: $x; top: $y;""",
onMouseDown := { (e: MouseEvent) => dispatch(ElementMouseDown(id, e.screenX.toInt, e.screenY.toInt)) },
onMouseUp := { (e: MouseEvent) => dispatch(ElementMouseUp(id, e.screenX.toInt, e.screenY.toInt)) },
onTouchStart := { (e: TouchEvent) => dispatch(ElementMouseDown(id, firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
onTouchEnd := { (e: TouchEvent) => dispatch(ElementMouseUp(id, firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
onMouseMove := { (e: MouseEvent) => dispatch(MouseMove(e.screenX.toInt, e.screenY.toInt)) },
onTouchMove := { (e: TouchEvent) => dispatch(MouseMove(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
innerHtml := text
)
case other =>
@@ -1,18 +1,55 @@
package com.auginte.eventsourced
import com.auginte.eventsourced.vdom.Input
import org.scalajs.dom
import org.scalajs.dom._
import org.scalajs.dom.html.{Element => _, _}
import scala.scalajs.js
import scala.scalajs.js.JSApp
import scala.scalajs.js.{Date, JSApp}
import scalatags.JsDom.all._
object MainJs extends JSApp {
val elements = new ElementsView(ElementsApp.zoom(_.elements), ElementsApp)
val controls = new ControlsView(ElementsApp)
object DEBUG {
var guiRepainted = 0
var frames = 0
var started: scala.Option[Double] = None
def repainted() = {
guiRepainted += 1
val debug = document.getElementById("debugUpdates").asInstanceOf[dom.html.Span]
debug.innerHTML = s"R(${DEBUG.guiRepainted})"
}
def framesPerSecond() = {
Async.timer(1000) {
val fpsElement = document.getElementById("debugFps")
val now = new Date().getTime()
started match {
case Some(s: Double) =>
val fps = frames / (now - s) * 1000
if (fps > 1) {
fpsElement.innerHTML = Math.floor(fps) + " fps"
} else {
fpsElement.innerHTML = (Math.floor(fps * 10) / 10) + " fps"
}
case None =>
started = Some(now)
fpsElement.innerHTML = "Starting"
}
started = Some(now)
frames = 0
true
}
}
}
var needUpdate = false
def main(): Unit = {
var loaded = false
@@ -26,34 +63,39 @@ object MainJs extends JSApp {
}
}
onReady{
onReady {
val updatesPerSecond = 60
Async.timer(1000 / updatesPerSecond) {
DEBUG.frames += 1
if (needUpdate) {
needUpdate = false
}
true
}
DEBUG.framesPerSecond()
// Data model
val root = dom.document.getElementById("root")
ElementsApp.subscribe(ElementsApp.zoom(identity))(_ => render(root))
ElementsApp(Empty)
// Updates from server
val source = new dom.EventSource(Config.url)
val messageHandler = (e: MessageEvent) => {
ElementsApp(LoadElement(e.data.asInstanceOf[String]))
}
source.addEventListener[MessageEvent]("message", messageHandler, useCapture = false)
}
}
def render(root: Element) = {
def focus(): Unit = {
val element = dom.document.getElementById("newElement").asInstanceOf[Input]
element.focus()
def clearChilds(root: Element): Unit = {
root.innerHTML = ""
}
clearChilds(root)
root.appendChild(div(controls.render).render)
root.appendChild(elements.domElements.createDomElement(dom.document))
focus()
}
private def clearChilds(root: Element): Unit ={
root.innerHTML = ""
DEBUG.repainted()
}
}
@@ -6,4 +6,5 @@ trait MouseEvents {
val onClick = new GenericEvent[dom.MouseEvent]("click")
val onMouseDown = new GenericEvent[dom.MouseEvent]("mousedown")
val onMouseUp = new GenericEvent[dom.MouseEvent]("mouseup")
val onMouseMove = new GenericEvent[dom.MouseEvent]("mousemove")
}
@@ -1,4 +1,38 @@
body {
background: #eff5f7;
color: #325464;
}
.noSelectable {
webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently not supported by any browser */
}
.elements .element {
display: inline;
border: 1px solid green;
cursor: move;
cursor: grab;
.noSelectable
}
#debug {
#debugFps {
position: fixed;
top: 0;
right: 0;
z-order: -1000;
border: 1px solid red;
}
#debugUpdates {
position: fixed;
top: 20;
right: 0;
z-order: -2000;
border: 1px solid brown;
}
}
@@ -14,5 +14,9 @@
</head>
<body>
<div id="root">Loading...</div>
<div id="debug">
<span id="debugFps">-1</span>
<span id="debugUpdates">R(-1)</span>
</div>
</body>
</html>

0 comments on commit 50c81bb

Please sign in to comment.