Skip to content
Snippets Groups Projects
Commit be46f1de authored by Martin Ring's avatar Martin Ring
Browse files

add paint example

parent f35e3703
No related branches found
No related tags found
No related merge requests found
ThisBuild / scalaVersion := "3.1.2"
lazy val root = project.in(file("."))
.aggregate(lecture08.js, lecture08.jvm)
.settings(
publish := {},
publishLocal := {}
)
lazy val lecture08 = crossProject(JSPlatform,JVMPlatform).in(file("."))
.settings(
Compile / scalaSource := baseDirectory.value,
name := "lecture-08",
)
.jvmSettings(
libraryDependencies += ("com.typesafe.akka" %% "akka-stream" % "2.6.14").cross(CrossVersion.for3Use2_13)
//libraryDependencies += "org.scala-js" %% "scalajs-dom" % "2.2.0"
)
.jsSettings(
libraryDependencies += ("org.akka-js" %%% "akkajsactorstream" % "2.2.6.14").cross(CrossVersion.for3Use2_13),
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.2.0",
scalaJSUseMainModuleInitializer := true
)
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=devide-width, initial-scale=1.0">
<title>Reactive Paint</title>
</head>
<body>
<script src="target/scala-3.1.2/lecture-08-fastopt.js"></script>
</body>
</html>
\ No newline at end of file
import akka.NotUsed
import akka.actor._
import akka.stream._
import akka.stream.scaladsl._
import org.scalajs.dom._
import org.scalajs.dom.ext.KeyCode
import org.scalajs.dom.EventListenerOptions
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Future, Promise}
implicit class EventSource(val target: org.scalajs.dom.EventTarget):
def on[E <: Event](event: String): Source[E, NotUsed] =
Source.queue[E](2048, OverflowStrategy.fail).mapMaterializedValue { queue =>
val handler: scalajs.js.Function1[E, Unit] = (e: E) => queue.offer(e)
target.addEventListener(event, handler)
handler
}.watchTermination() {
case (h, d) =>
d.onComplete(_ => target.removeEventListener(event, h))
NotUsed
}
@main def main(): Unit =
import PaintCommand._
given system: ActorSystem = ActorSystem("paint")
given Materializer = Materializer(system)
val canvas = document.createElement("canvas").asInstanceOf[html.Canvas]
document.body.appendChild(canvas)
canvas.style.position = "fixed"
canvas.style.left = "0"
canvas.style.right = "0"
canvas.style.top = "0"
canvas.style.bottom = "0"
canvas.width = window.innerWidth.toInt
canvas.height = window.innerHeight.toInt
val mouseDown =
canvas.on[MouseEvent]("mousedown")
.map(e => BeginDrag(e.clientX, e.clientY))
val mouseUp =
canvas.on[MouseEvent]("mouseup")
.map(e => EndDrag(e.clientX, e.clientY))
val mouseLeave =
canvas.on[MouseEvent]("mouseleave")
.map(e => EndDrag(e.clientX, e.clientY))
val mouseMove =
canvas.on[MouseEvent]("mousemove")
.map(e => Drag(e.clientX, e.clientY))
val mouseCommands = mouseDown.flatMapConcat { b =>
Source.single(b).concat(
mouseMove.merge(mouseUp merge mouseLeave).takeWhile(_.isInstanceOf[Drag], inclusive = true)
)
}
val keyboardCommands =
window.on[KeyboardEvent]("keydown")
.map(e => e.keyCode)
.collect {
case KeyCode.B => ChooseColor("blue")
case KeyCode.G => ChooseColor("green")
case KeyCode.R => ChooseColor("red")
case KeyCode.Up => IncStrokeWidth
case KeyCode.Down => DecStrokeWidth
case KeyCode.Left => SelectPrev
case KeyCode.Right => SelectNext
case KeyCode.Backspace => Cut
case KeyCode.Enter => Paste
case KeyCode.C => Copy
}
val commands: Source[PaintCommand, NotUsed] =
mouseCommands merge keyboardCommands
val paint: Flow[PaintCommand, PaintState, NotUsed] =
Flow[PaintCommand].scan(PaintState.initial) {
case (state, ChooseColor(c)) =>
if (state.isSelected) state.modifySelected(_.copy(color = c))
else state.copy(color = c)
case (state, IncStrokeWidth) =>
if (state.isSelected) state.modifySelected(p => p.copy(width = p.width + 1))
else state.copy(width = state.width + 1)
case (state, DecStrokeWidth) =>
if (state.isSelected) state.modifySelected(p => p.copy(width = Math.max(1,p.width - 1)))
else state.copy(width = Math.max(1,state.width - 1))
case (state, BeginDrag(x, y)) =>
val s = state.paths.indexWhere(_.hits(x,y))
if (s > -1) state.copy(
selected = s,
dragStart = (x,y)
) else state.copy(
selected = -1,
currentPath = Some(Path(state.color, state.width, Vector(Point(x, y)))))
case (state, Drag(x, y)) =>
if (state.isSelected) state.modifySelected(p => p.copy(points = p.points.map {
case Point(px,py) => Point(px + x - state.dragStart._1,py + y - state.dragStart._2)
})).copy(dragStart = (x,y)) else state.copy(
currentPath = state.currentPath.map(_.append(x, y)))
case (state, EndDrag(x, y)) =>
if (state.isSelected) state.modifySelected(p => p.copy(points = p.points.map {
case Point(px,py) => Point(px + x - state.dragStart._1,py + y - state.dragStart._2)
})) else state.copy(
paths = state.currentPath.filter(!_.isEmpty).map(_.append(x,y)).toList ++ state.paths,
currentPath = None)
case (state, SelectPrev) =>
if (state.selected > 0 && state.selected < state.paths.length) {
state.copy(selected = state.selected - 1)
} else state.copy(selected = state.paths.length - 1)
case (state, SelectNext) =>
if (state.selected > -1 && state.selected < state.paths.length - 1) {
state.copy(selected = state.selected + 1)
} else state.copy(selected = 0)
case (state, Copy) =>
if (state.isSelected) state.copy(
clipboard = Some(state.paths(state.selected)))
else state
case (state, Cut) =>
if (state.isSelected) {
val (before,deleted :: after) = state.paths.splitAt(state.selected)
state.copy(
paths = before ++ after,
clipboard = Some(deleted),
selected = -1
)
} else state
case (state, Paste) =>
if (state.clipboard.isDefined) state.copy(
paths = state.clipboard.toList ++ state.paths,
selected = 0
) else state
}
def visualize = Sink.foreach[PaintState] {
case s@PaintState(color, width, paths, _, currentPath, selected, _) =>
val context = canvas.getContext("2d").asInstanceOf[raw.CanvasRenderingContext2D]
context.lineJoin = "round"
context.lineCap = "round"
context.clearRect(0,0,canvas.width,canvas.height)
paths.zipWithIndex.reverse.foreach {
case (p@Path(color, width, points),i) =>
if (i == selected) {
context.shadowColor = "blue"
context.shadowBlur = width
}
context.strokeStyle = color
context.lineWidth = width
context.beginPath()
p.controlPoints.headOption.foreach {
case Point(x, y) => context.moveTo(x, y)
}
p.controlPoints.drop(1).grouped(3).foreach {
case Seq(c1,c2,p) => context.bezierCurveTo(c1.x,c1.y,c2.x,c2.y,p.x,p.y)
}
context.stroke()
context.shadowColor = null
context.shadowBlur = 0
}
currentPath.foreach {
case Path(color, width, points) =>
context.strokeStyle = color
context.lineWidth = width
context.beginPath()
points.headOption.foreach {
case Point(x, y) => context.moveTo(x, y)
}
points.drop(1).foreach {
case Point(x, y) => context.lineTo(x, y)
}
context.stroke()
}
context.shadowColor = null
context.shadowBlur = 0
// visualize state
context.strokeStyle = color
context.lineWidth = width
context.beginPath()
context.moveTo(10 + width / 2,10 + width)
context.lineTo(10 + width / 2,50)
context.stroke()
}
(commands via paint to visualize).run()
\ No newline at end of file
import akka.actor.ActorSystem
import akka.stream.Materializer
import akka.stream.scaladsl._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import akka.NotUsed
import scala.concurrent.duration._
import akka.stream.OverflowStrategy
given system: ActorSystem = ActorSystem("paint")
given ExecutionContext = system.dispatcher
given Materializer = Materializer(system)
def sum =
val source1 = Source.tick(1.second,1.second,0)
val source2 = Source(Stream.iterate(0)(_ + 1))
val source = source1.zip(source2)
val sink = Sink.foreach[Any](i => println("step: " + i))
val sum = source runWith sink
sum
def error1 =
val source = Source(0 to 5).map(100 / _)
val result = source.runWith(Sink.fold(0)(_ + _))
result
def error2 =
val source = Source(0 to 5).map(100 / _).recover {
case _: ArithmeticException => 0
}
val result = source.runWith(Sink.fold(0)(_ + _))
result
\ No newline at end of file
sbt.version=1.6.2
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0")
\ No newline at end of file
case class Point(
x: Double,
y: Double
):
def +(that: Point) = Point(this.x + that.x, this.y + that.y)
def unary_- : Point = Point(-x,-y)
def -(that: Point) = this + (- that)
def *(scalar: Double) = Point(x*scalar,y*scalar)
def magnitude: Double = Math.sqrt(Math.pow(x,2) + Math.pow(y,2))
def phi: Double = Math.atan2(y,x)
def normalized: Point = Point(Math.cos(phi),Math.sin(phi))
object Point:
def distance(a: Point, b: Point): Double = (a - b).magnitude
case class Path(
color: String,
width: Int,
points: Vector[Point]
):
def append(x: Double, y: Double): Path = {
if (points.length > 1 && Point.distance(points.last,points.init.last) <= width * 2)
copy(points = points.init :+ Point(x,y))
else
copy(points = points :+ Point(x,y))
}
val scale = 0.5
lazy val controlPoints: Seq[Point] = (for (i <- 0 until points.length) yield {
if (i == 0) {
val p1 = points(i)
val p2 = points(i+1)
val tangent = p2 - p1;
val q1 = p1 + tangent * scale;
Seq(p1,q1)
} else if (i == points.length - 1) {
val p0 = points(i - 1)
val p1 = points(i)
val tangent = (p1 - p0);
val q0 = p1 - tangent * scale;
Seq(q0,p1)
} else {
val p0 = points(i - 1)
val p1 = points(i)
val p2 = points(i + 1)
val tangent = (p2 - p0).normalized;
val q0 = p1 - tangent * scale * (p1 - p0).magnitude;
val q1 = p1 + tangent * scale * (p2 - p1).magnitude;
Seq(q0,p1,q1)
}
}).flatten
def hits(x: Double, y: Double): Boolean = points.exists(p => Point.distance(p,Point(x,y)) <= width)
def isEmpty: Boolean = points.length < 2
case class PaintState(
color: String,
width: Int,
paths: List[Path],
clipboard: Option[Path],
currentPath: Option[Path],
selected: Int,
dragStart: (Double,Double)
):
def isSelected: Boolean = selected > -1 && selected < paths.length
def modifySelected(f: Path => Path): PaintState = {
val (before,s :: after) = paths.splitAt(selected)
copy(paths = before ++ (f(s) :: after))
}
object PaintState:
def initial = PaintState("black",5,List.empty,Option.empty,None,-1,(0,0))
enum PaintCommand:
case ChooseColor(color: String)
case IncStrokeWidth
case DecStrokeWidth
case BeginDrag(x: Double, y: Double)
case Drag(x: Double, y: Double)
case EndDrag(x: Double, y: Double)
case Cut
case Copy
case Paste
case SelectNext
case SelectPrev
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment