Skip to content

Commit

Permalink
First working functionality for Canvas backend
Browse files Browse the repository at this point in the history
  • Loading branch information
noelwelsh committed Jun 6, 2024
1 parent 4771bf2 commit 4d768ba
Show file tree
Hide file tree
Showing 17 changed files with 518 additions and 9 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,6 @@ lazy val examples = crossProject(JSPlatform, JVMPlatform)
)
.jsConfigure(
_.settings(mimaPreviousArtifacts := Set.empty)
.dependsOn(core.js, image.js, interact.js)
.dependsOn(core.js, canvas, image.js, interact.js)
)
.dependsOn(svg)
19 changes: 19 additions & 0 deletions canvas/src/main/scala/doodle/canvas/Algebra.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package doodle.canvas

type Algebra = doodle.canvas.algebra.Algebra
20 changes: 20 additions & 0 deletions canvas/src/main/scala/doodle/canvas/Drawing.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package doodle.canvas

type Drawing[A] =
doodle.algebra.generic.Finalized[doodle.canvas.algebra.CanvasDrawing, A]
20 changes: 20 additions & 0 deletions canvas/src/main/scala/doodle/canvas/Frame.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package doodle.canvas

type Frame = doodle.canvas.effect.Frame
val Frame = doodle.canvas.effect.Frame
27 changes: 27 additions & 0 deletions canvas/src/main/scala/doodle/canvas/Picture.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package doodle.canvas

import doodle.algebra.BaseConstructor
import doodle.algebra.ShapeConstructor

type Picture[A] = doodle.algebra.Picture[Algebra, A]
object Picture extends BaseConstructor, ShapeConstructor {

type Algebra = doodle.canvas.Algebra
type Drawing[A] = doodle.canvas.Drawing[A]
}
70 changes: 70 additions & 0 deletions canvas/src/main/scala/doodle/canvas/algebra/Algebra.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package doodle.canvas.algebra

import cats.Apply
import cats.Eval
import cats.Functor
import cats.Monad
import doodle.algebra.generic._
import doodle.core.BoundingBox
import org.scalajs.dom.CanvasRenderingContext2D

final case class Algebra(
ctx: CanvasRenderingContext2D,
applyDrawing: Apply[CanvasDrawing] = Apply.apply[CanvasDrawing],
functorDrawing: Functor[CanvasDrawing] = Apply.apply[CanvasDrawing]
) extends Shape,
GenericDebug[CanvasDrawing],
GenericLayout[CanvasDrawing],
GenericSize[CanvasDrawing],
GenericStyle[CanvasDrawing],
GenericTransform[CanvasDrawing],
GivenApply[CanvasDrawing],
GivenFunctor[CanvasDrawing],
doodle.algebra.Algebra {
type Drawing[A] = doodle.canvas.Drawing[A]
implicit val drawingInstance: Monad[Drawing] =
new Monad[Drawing] {
def pure[A](x: A): Drawing[A] =
Finalized.leaf(_ =>
(
BoundingBox.empty,
Renderable.apply(_ => Eval.now(CanvasDrawing.pure(x)))
)
)

def flatMap[A, B](fa: Drawing[A])(f: A => Drawing[B]): Drawing[B] =
fa.flatMap { (bb, rdr) =>
val canvasDrawing = rdr.runA(doodle.core.Transform.identity).value
val a = canvasDrawing(ctx)
f(a)
}

def tailRecM[A, B](a: A)(f: A => Drawing[Either[A, B]]): Drawing[B] = {
// TODO: This implementation is not tail recursive but I don't think we need it for what we use in Doodle
val dAB = f(a)
flatMap(dAB)(either =>
either match {
case Left(a) => tailRecM(a)(f)
case Right(b) => dAB.asInstanceOf[Drawing[B]]
}
)
}
}

}
62 changes: 60 additions & 2 deletions canvas/src/main/scala/doodle/canvas/algebra/CanvasDrawing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,83 @@

package doodle.canvas.algebra

import doodle.core.Color
import cats.Apply
import doodle.algebra.generic.Fill
import doodle.algebra.generic.Fill.ColorFill
import doodle.algebra.generic.Fill.GradientFill
import doodle.algebra.generic.Stroke
import doodle.core.Cap
import doodle.core.Color
import doodle.core.Join
import org.scalajs.dom.CanvasRenderingContext2D

/** A canvas `Drawing` is a function that, when applied, produces a value of
* type `A` and has the side-effect of drawing on the canvas.
*/
opaque type CanvasDrawing[A] = Function[CanvasRenderingContext2D, A]
object CanvasDrawing {
given Apply[CanvasDrawing] with {
def ap[A, B](ff: CanvasDrawing[A => B])(
fa: CanvasDrawing[A]
): CanvasDrawing[B] =
CanvasDrawing(ctx => ff(ctx)(fa(ctx)))

def map[A, B](fa: CanvasDrawing[A])(f: A => B): CanvasDrawing[B] =
CanvasDrawing(ctx => f(fa(ctx)))
}

def pure[A](value: A): CanvasDrawing[A] =
CanvasDrawing(_ => value)

/** CanvasDrawing that does nothing */
val unit: CanvasDrawing[Unit] =
pure(())

extension [A](drawing: CanvasDrawing[A]) {
def apply(ctx: CanvasRenderingContext2D): A =
drawing(ctx)

def >>[B](that: CanvasDrawing[B]): CanvasDrawing[B] =
CanvasDrawing { ctx =>
drawing(ctx)
that(ctx)
}
}

def apply[A](f: CanvasRenderingContext2D => A): CanvasDrawing[A] = f

def colorToCSS(color: Color): String =
s"rgb(${color.red} ${color.green} ${color.blue} / ${color.alpha})"

def setFill(fill: Fill): CanvasDrawing[Unit] = {
CanvasDrawing { ctx =>
// TODO: Implement
fill match {
case ColorFill(color) => ()
case GradientFill(gradient) => ()
}
}
}

def setStroke(stroke: Option[Stroke]): CanvasDrawing[Unit] =
stroke.map(setStroke).getOrElse(unit)

def setStroke(stroke: Stroke): CanvasDrawing[Unit] = {
val Stroke(color, width, cap, join, dash) = stroke
CanvasDrawing{ ctx =>
CanvasDrawing { ctx =>
ctx.strokeStyle = colorToCSS(color)
ctx.lineWidth = width
ctx.lineCap = cap match {
case Cap.Butt => "butt"
case Cap.Round => "round"
case Cap.Square => "square"
}
ctx.lineJoin = join match {
case Join.Bevel => "bevel"
case Join.Round => "round"
case Join.Miter => "miter"
}
// TODO: Implement dash
}
}
}
27 changes: 22 additions & 5 deletions canvas/src/main/scala/doodle/canvas/algebra/Shape.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
package doodle.canvas.algebra

import doodle.algebra.Algebra
import doodle.algebra.generic.*
import doodle.algebra.generic._
import doodle.core.PathElement
import doodle.core.PathElement.BezierCurveTo
import doodle.core.PathElement.LineTo
import doodle.core.PathElement.MoveTo
import doodle.core.Point
import doodle.core.{Transform => Tx}

trait Shape extends GenericShape[CanvasDrawing] {
Expand All @@ -31,9 +36,8 @@ trait Shape extends GenericShape[CanvasDrawing] {
width: Double,
height: Double
): CanvasDrawing[Unit] =
CanvasDrawing { ctx =>
ctx.strokeRect(width, height, width, height)
}
CanvasDrawing.setStroke(stroke) >>
CanvasDrawing(ctx => ctx.strokeRect(width, height, width, height))

def triangle(
tx: Tx,
Expand All @@ -50,7 +54,20 @@ trait Shape extends GenericShape[CanvasDrawing] {
stroke: Option[Stroke],
diameter: Double
): CanvasDrawing[Unit] =
???
CanvasDrawing.setStroke(stroke) >> CanvasDrawing { ctx =>
val path = PathElement.circle(Point.zero, diameter)
ctx.beginPath()
path.foreach(elt =>
elt match {
case MoveTo(to) => ctx.moveTo(to.x, to.y)
case LineTo(to) => ctx.lineTo(to.x, to.y)
case BezierCurveTo(cp1, cp2, to) =>
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, to.x, to.y)
}
)
ctx.closePath()
ctx.stroke()
}

def unit: CanvasDrawing[Unit] =
???
Expand Down
56 changes: 56 additions & 0 deletions canvas/src/main/scala/doodle/canvas/effect/Canvas.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package doodle.canvas.effect

import cats.effect.IO
import doodle.algebra.generic.Finalized
import doodle.canvas.Picture
import doodle.canvas.algebra.Algebra
import doodle.canvas.algebra.CanvasDrawing
import doodle.core.Transform
import org.scalajs.dom

final case class Canvas(target: dom.Node, frame: Frame) {
val canvas: dom.html.Canvas =
dom.document.createElement("canvas").asInstanceOf[dom.html.Canvas]
val ctx = canvas.getContext("2d").asInstanceOf[dom.CanvasRenderingContext2D]
val algebra = Algebra(ctx)

target.appendChild(canvas)

def render[A](picture: Picture[A]): IO[A] = {
IO {
val finalized: Finalized[CanvasDrawing, A] = picture(algebra)
val (bb, rdr) = finalized.run(List.empty).value
val drawing = rdr.runA(Transform.identity).value
val a = drawing(ctx)
a
}
}
}
object Canvas {
def fromFrame(frame: Frame): IO[Canvas] = {
IO {
val target = dom.document.getElementById(frame.id)
if (target == null) {
throw new java.util.NoSuchElementException(
s"Doodle Canvas could not be created, as could not find a DOM element with the requested id ${frame.id}"
)
} else Canvas(target, frame)
}
}
}
31 changes: 31 additions & 0 deletions canvas/src/main/scala/doodle/canvas/effect/CanvasRenderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package doodle.canvas.effect

import cats.effect.IO
import doodle.canvas.Algebra
import doodle.canvas.Picture
import doodle.effect.Renderer

object CanvasRenderer extends Renderer[Algebra, Frame, Canvas] {

def canvas(description: Frame): IO[Canvas] =
Canvas.fromFrame(description)

def render[A](canvas: Canvas)(picture: Picture[A]): IO[A] =
canvas.render(picture)
}
Loading

0 comments on commit 4d768ba

Please sign in to comment.