Skip to content

Commit 50c81bb

Browse files
committed
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.
1 parent c2423aa commit 50c81bb

File tree

7 files changed

+150
-33
lines changed

7 files changed

+150
-33
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.auginte.eventsourced
2+
3+
import org.scalajs.dom._
4+
5+
object Async {
6+
def timer(interval: Int)(f: => Boolean): Unit = {
7+
val continue = f
8+
if (continue) {
9+
window.setTimeout(() => timer(interval)(f), interval)
10+
}
11+
}
12+
}

js/src/main/scala/com/auginte/eventsourced/ElementsApp.scala

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ case class LoadElement(text: String) extends Action
2121

2222
case class ElementMouseDown(aggregateId: AggregateId, x: Int, y: Int) extends Action
2323

24+
case class MouseMove(x: Int, y: Int) extends Action
25+
2426
case class ElementMouseUp(aggregateId: AggregateId, x: Int, y: Int) extends Action
2527

2628
case class MouseUp(x: Int, y: Int) extends Action
@@ -45,49 +47,60 @@ object ElementsApp extends Circuit[ElementsModel]{
4547
val event = Generic.Event.created(text)
4648
val data = Pickle.intoString(event) + "\n"
4749
sendAjax(Config.url, data)
48-
updated(value)
50+
noChange // Depending on backend, while inner cache not implemented
4951

5052
case LoadElement(raw) =>
5153
Unpickle[Generic.Event].fromString(raw) match {
5254
case Success(e) =>
5355
updated(value.copy(elements = value.elements.updated(e.aggregateId, e.data)))
5456
case Failure(err) =>
5557
dom.console.warn("Unable to unmarshal event", err.toString, raw)
56-
updated(value)
58+
noChange
5759
}
5860

5961
case ElementMouseDown(id, x, y) =>
6062
updated(value.copy(lastMousePosition = MousePosition(x, y), selectedElementId = Some(id)))
6163

64+
case MouseMove(x, y) =>
65+
val mousePosition = MousePosition(x, y)
66+
value.selectedElementId match {
67+
case Some(id) => value.elements.get(id) match {
68+
case Some(textElement: Text) =>
69+
val updatedElement = moveElement(textElement, value.lastMousePosition, mousePosition)
70+
movedElement(id, updatedElement, mousePosition, value.selectedElementId)
71+
case Some(element) =>
72+
dom.console.warn(s"Element to drag not supported $id: $element")
73+
noChange
74+
case None =>
75+
dom.console.warn("Element to drag not found", id)
76+
noChange
77+
}
78+
case None => noChange // No element selected
79+
}
80+
6281
case ElementMouseUp(id, x, y) =>
6382
val mousePosition = MousePosition(x, y)
6483
value.elements.get(id) match {
6584
case Some(textElement: Text) =>
66-
val newElement = moveElement(textElement, value.lastMousePosition, mousePosition)
67-
68-
storeUpdated(id, newElement)
69-
70-
updated(value.copy(elements = value.elements.updated(id, newElement), lastMousePosition = mousePosition, selectedElementId = None))
85+
val updatedElement = moveElement(textElement, value.lastMousePosition, mousePosition)
86+
storedElement(id, updatedElement, mousePosition, selected = None)
7187
case other =>
7288
dom.console.warn(s"Trying to mouse up not stored element: $id: $other")
73-
updated(value)
89+
noChange
7490
}
7591

7692
case MouseUp(x, y) =>
93+
val mousePosition = MousePosition(x, y)
7794
value.selectedElementId match {
7895
case Some(id) => value.elements.get(id) match {
7996
case Some(textElement: Text) =>
80-
val mousePosition = MousePosition(x, y)
81-
val newElement = moveElement(textElement, value.lastMousePosition, mousePosition)
82-
83-
storeUpdated(id, newElement)
84-
85-
updated(value.copy(elements = value.elements.updated(id, newElement), lastMousePosition = mousePosition, selectedElementId = None))
97+
val updatedElement = moveElement(textElement, value.lastMousePosition, mousePosition)
98+
storedElement(id, updatedElement, mousePosition, selected = None)
8699
case other =>
87100
dom.console.warn(s"Trying to move not stored selected element: $id: $other")
88-
updated(value)
101+
noChange
89102
}
90-
case None => updated(value)
103+
case None => noChange
91104
}
92105
}
93106

@@ -100,6 +113,15 @@ object ElementsApp extends Circuit[ElementsModel]{
100113
private def moveElement(element: Text, oldPos: MousePosition, newPos: MousePosition): Generic.Data = {
101114
element.copy(x = element.x + (newPos.x - oldPos.x), y = element.y + (newPos.y - oldPos.y))
102115
}
116+
117+
private def movedElement(id: AggregateId, element: Generic.Data, mousePosition: MousePosition, selected: Option[AggregateId]) =
118+
updated(value.copy(elements = value.elements.updated(id, element), lastMousePosition = mousePosition, selectedElementId = selected))
119+
120+
private def storedElement(id: AggregateId, element: Generic.Data, mousePosition: MousePosition, selected: Option[AggregateId]) = {
121+
storeUpdated(id, element)
122+
movedElement(id, element, mousePosition, selected)
123+
}
124+
103125
}
104126

105127
private def handleAjax(url: String, input: String): Future[String] =

js/src/main/scala/com/auginte/eventsourced/ElementsView.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.auginte.eventsourced.Generic.Text
44
import com.auginte.eventsourced.vdom.Implicits._
55
import com.auginte.eventsourced.vdom.{Div, Elements}
66
import diode._
7-
import org.scalajs.dom
87
import org.scalajs.dom.{MouseEvent, Touch, TouchEvent}
98

109
import scalatags.JsDom.all.{style => _}
@@ -20,6 +19,8 @@ class ElementsView(elements: ModelR[_, Map[Generic.AggregateId, Generic.Data]],
2019
onMouseUp := { (e: MouseEvent) => dispatch(MouseUp(e.screenX.toInt, e.screenY.toInt)) },
2120
onTouchEnd := { (e: TouchEvent) => dispatch(MouseUp(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
2221
onTouchCancel := { (e: TouchEvent) => dispatch(MouseUp(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
22+
onMouseMove := { (e: MouseEvent) => dispatch(MouseMove(e.screenX.toInt, e.screenY.toInt)) },
23+
onTouchMove := { (e: TouchEvent) => dispatch(MouseMove(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
2324
Elements(innerElements)
2425
)
2526
)
@@ -29,12 +30,13 @@ class ElementsView(elements: ModelR[_, Map[Generic.AggregateId, Generic.Data]],
2930
case (id: Generic.AggregateId, t@Text(text, x, y, scale)) =>
3031
Div(
3132
className := "element",
32-
style := s"""position: absolute; left: $x; top: $y; display: inline; border: 1px solid red;""",
33+
style := s"""position: absolute; left: $x; top: $y;""",
3334
onMouseDown := { (e: MouseEvent) => dispatch(ElementMouseDown(id, e.screenX.toInt, e.screenY.toInt)) },
3435
onMouseUp := { (e: MouseEvent) => dispatch(ElementMouseUp(id, e.screenX.toInt, e.screenY.toInt)) },
3536
onTouchStart := { (e: TouchEvent) => dispatch(ElementMouseDown(id, firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
3637
onTouchEnd := { (e: TouchEvent) => dispatch(ElementMouseUp(id, firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
37-
38+
onMouseMove := { (e: MouseEvent) => dispatch(MouseMove(e.screenX.toInt, e.screenY.toInt)) },
39+
onTouchMove := { (e: TouchEvent) => dispatch(MouseMove(firstTouch(e).screenX.toInt, firstTouch(e).screenY.toInt)) },
3840
innerHtml := text
3941
)
4042
case other =>
Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,55 @@
11
package com.auginte.eventsourced
2-
import com.auginte.eventsourced.vdom.Input
2+
33
import org.scalajs.dom
44
import org.scalajs.dom._
55
import org.scalajs.dom.html.{Element => _, _}
66

7-
import scala.scalajs.js
8-
import scala.scalajs.js.JSApp
7+
import scala.scalajs.js.{Date, JSApp}
98
import scalatags.JsDom.all._
109

1110
object MainJs extends JSApp {
1211

1312
val elements = new ElementsView(ElementsApp.zoom(_.elements), ElementsApp)
1413
val controls = new ControlsView(ElementsApp)
1514

15+
object DEBUG {
16+
var guiRepainted = 0
17+
18+
var frames = 0
19+
20+
var started: scala.Option[Double] = None
21+
22+
def repainted() = {
23+
guiRepainted += 1
24+
val debug = document.getElementById("debugUpdates").asInstanceOf[dom.html.Span]
25+
debug.innerHTML = s"R(${DEBUG.guiRepainted})"
26+
}
27+
28+
def framesPerSecond() = {
29+
Async.timer(1000) {
30+
val fpsElement = document.getElementById("debugFps")
31+
val now = new Date().getTime()
32+
started match {
33+
case Some(s: Double) =>
34+
val fps = frames / (now - s) * 1000
35+
if (fps > 1) {
36+
fpsElement.innerHTML = Math.floor(fps) + " fps"
37+
} else {
38+
fpsElement.innerHTML = (Math.floor(fps * 10) / 10) + " fps"
39+
}
40+
41+
case None =>
42+
started = Some(now)
43+
fpsElement.innerHTML = "Starting"
44+
}
45+
started = Some(now)
46+
frames = 0
47+
true
48+
}
49+
}
50+
}
51+
52+
var needUpdate = false
1653

1754
def main(): Unit = {
1855
var loaded = false
@@ -26,34 +63,39 @@ object MainJs extends JSApp {
2663
}
2764
}
2865

29-
onReady{
66+
onReady {
67+
val updatesPerSecond = 60
68+
Async.timer(1000 / updatesPerSecond) {
69+
DEBUG.frames += 1
70+
if (needUpdate) {
71+
needUpdate = false
72+
}
73+
true
74+
}
75+
DEBUG.framesPerSecond()
76+
77+
// Data model
3078
val root = dom.document.getElementById("root")
3179
ElementsApp.subscribe(ElementsApp.zoom(identity))(_ => render(root))
3280
ElementsApp(Empty)
3381

82+
// Updates from server
3483
val source = new dom.EventSource(Config.url)
3584
val messageHandler = (e: MessageEvent) => {
3685
ElementsApp(LoadElement(e.data.asInstanceOf[String]))
3786
}
3887
source.addEventListener[MessageEvent]("message", messageHandler, useCapture = false)
39-
4088
}
4189
}
4290

4391
def render(root: Element) = {
44-
def focus(): Unit = {
45-
val element = dom.document.getElementById("newElement").asInstanceOf[Input]
46-
element.focus()
92+
def clearChilds(root: Element): Unit = {
93+
root.innerHTML = ""
4794
}
4895

4996
clearChilds(root)
5097
root.appendChild(div(controls.render).render)
5198
root.appendChild(elements.domElements.createDomElement(dom.document))
52-
53-
focus()
54-
}
55-
56-
private def clearChilds(root: Element): Unit ={
57-
root.innerHTML = ""
99+
DEBUG.repainted()
58100
}
59101
}

js/src/main/scala/com/auginte/eventsourced/vdom/MouseEvents.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ trait MouseEvents {
66
val onClick = new GenericEvent[dom.MouseEvent]("click")
77
val onMouseDown = new GenericEvent[dom.MouseEvent]("mousedown")
88
val onMouseUp = new GenericEvent[dom.MouseEvent]("mouseup")
9+
val onMouseMove = new GenericEvent[dom.MouseEvent]("mousemove")
910
}

jvm/src/main/assets/main.less

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,38 @@
11
body {
22
background: #eff5f7;
33
color: #325464;
4+
}
5+
6+
.noSelectable {
7+
webkit-touch-callout: none; /* iOS Safari */
8+
-webkit-user-select: none; /* Chrome/Safari/Opera */
9+
-khtml-user-select: none; /* Konqueror */
10+
-moz-user-select: none; /* Firefox */
11+
-ms-user-select: none; /* Internet Explorer/Edge */
12+
user-select: none; /* Non-prefixed version, currently not supported by any browser */
13+
}
14+
15+
.elements .element {
16+
display: inline;
17+
border: 1px solid green;
18+
cursor: move;
19+
cursor: grab;
20+
.noSelectable
21+
}
22+
23+
#debug {
24+
#debugFps {
25+
position: fixed;
26+
top: 0;
27+
right: 0;
28+
z-order: -1000;
29+
border: 1px solid red;
30+
}
31+
#debugUpdates {
32+
position: fixed;
33+
top: 20;
34+
right: 0;
35+
z-order: -2000;
36+
border: 1px solid brown;
37+
}
438
}

jvm/src/main/resources/com/auginte/eventsourced/project.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@
1414
</head>
1515
<body>
1616
<div id="root">Loading...</div>
17+
<div id="debug">
18+
<span id="debugFps">-1</span>
19+
<span id="debugUpdates">R(-1)</span>
20+
</div>
1721
</body>
1822
</html>

0 commit comments

Comments
 (0)