diff --git a/.scalafix.conf b/.scalafix.conf index 7c3dd80a..4eedd04e 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -1,4 +1,5 @@ rules = [ OrganizeImports ] +OrganizeImports.removeUnused = false OrganizeImports.coalesceToWildcardImportThreshold = 5 diff --git a/canvas/src/main/scala/doodle/canvas/algebra/CanvasAlgebra.scala b/canvas/src/main/scala/doodle/canvas/algebra/CanvasAlgebra.scala index 6d29d825..b994f625 100644 --- a/canvas/src/main/scala/doodle/canvas/algebra/CanvasAlgebra.scala +++ b/canvas/src/main/scala/doodle/canvas/algebra/CanvasAlgebra.scala @@ -29,9 +29,12 @@ final case class CanvasAlgebra( applyDrawing: Apply[CanvasDrawing] = Apply.apply[CanvasDrawing], functorDrawing: Functor[CanvasDrawing] = Apply.apply[CanvasDrawing] ) extends Path, + Raster, Shape, GenericDebug[CanvasDrawing], GenericLayout[CanvasDrawing], + GenericRaster[CanvasDrawing, Immediate], + GenericShape[CanvasDrawing], GenericSize[CanvasDrawing], GenericStyle[CanvasDrawing], GenericTransform[CanvasDrawing], @@ -39,6 +42,12 @@ final case class CanvasAlgebra( GivenFunctor[CanvasDrawing], doodle.algebra.Algebra { type Drawing[A] = doodle.canvas.Drawing[A] + + override def empty: Finalized[CanvasDrawing, Unit] = + Finalized.leaf(_ => + (BoundingBox.empty, Renderable.unit(CanvasDrawing.unit)) + ) + implicit val drawingInstance: Monad[Drawing] = new Monad[Drawing] { def pure[A](x: A): Drawing[A] = @@ -67,5 +76,4 @@ final case class CanvasAlgebra( ) } } - } diff --git a/canvas/src/main/scala/doodle/canvas/algebra/CanvasDrawing.scala b/canvas/src/main/scala/doodle/canvas/algebra/CanvasDrawing.scala index ef94387e..5a756253 100644 --- a/canvas/src/main/scala/doodle/canvas/algebra/CanvasDrawing.scala +++ b/canvas/src/main/scala/doodle/canvas/algebra/CanvasDrawing.scala @@ -16,6 +16,8 @@ package doodle.canvas.algebra +import doodle.algebra.generic.* + import cats.Apply import doodle.algebra.generic.Fill import doodle.algebra.generic.Fill.ColorFill @@ -36,6 +38,7 @@ import doodle.core.font.FontSize import doodle.core.font.FontStyle import doodle.core.font.FontWeight import org.scalajs.dom.CanvasRenderingContext2D +import org.scalajs.dom.Path2D import scala.scalajs.js import scala.scalajs.js.JSConverters.* @@ -44,6 +47,7 @@ import scala.scalajs.js.JSConverters.* * 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])( @@ -121,6 +125,21 @@ object CanvasDrawing { } } + def raster(width: Int, height: Int)( + f: Immediate => Unit + ): CanvasDrawing[Unit] = { + CanvasDrawing { ctx => + val path = new Path2D() + val immediate = new ImmediateImpl(width, height, ctx, path) + f(immediate) + } + } + + def text(text: String, x: Double, y: Double): CanvasDrawing[Unit] = + CanvasDrawing { ctx => + ctx.fillText(text, x, y) + } + def setFill(fill: Option[Fill]): CanvasDrawing[Unit] = fill.map(setFill).getOrElse(unit) diff --git a/canvas/src/main/scala/doodle/canvas/algebra/Immediate.scala b/canvas/src/main/scala/doodle/canvas/algebra/Immediate.scala new file mode 100644 index 00000000..73c4413e --- /dev/null +++ b/canvas/src/main/scala/doodle/canvas/algebra/Immediate.scala @@ -0,0 +1,524 @@ +/* + * 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 doodle.core.Color +import doodle.core.* +import org.scalajs.dom.CanvasRenderingContext2D +import org.scalajs.dom.Path2D +import scala.annotation.tailrec + +import scala.scalajs.js.JSConverters.* + +trait Immediate { + def arc( + x: Int, + y: Int, + diameter: Int, + startAngle: Double, + endAngle: Double, + counterclockwise: Boolean = false, + closedPath: Boolean = false, + segments: Array[Double] = Array.empty[Double] + ): Unit + def arcTo( + x1: Int, + y1: Int, + x2: Int, + y2: Int, + diameter: Int, + counterclockwise: Boolean = false, + closedPath: Boolean = false, + segments: Array[Double] = Array.empty[Double] + ): Unit + def bezierCurveTo( + cp1x: Int, + cp1y: Int, + cp2x: Int, + cp2y: Int, + x: Int, + y: Int, + segments: Array[Double] = Array.empty[Double] + ): Unit + def clearRect(x: Int, y: Int, width: Int, height: Int): Unit + def clip(): Unit + def clipArc( + x: Int, + y: Int, + diameter: Int, + startAngle: Double = 0, + endAngle: Double = 2 * Math.PI, + counterclockwise: Boolean = false + ): Unit + def clipRect(x: Int, y: Int, width: Int, height: Int): Unit + def circle( + x: Double, + y: Double, + diameter: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit + def dashLine( + x1: Double, + y1: Double, + x2: Double, + y2: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit + def ellipse( + x: Double, + y: Double, + width: Double, + height: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit + def ellipseWithRotation( + x: Double, + y: Double, + diameterX: Double, + diameterY: Double, + rotation: Double, + startAngle: Double, + endAngle: Double, + counterclockwise: Boolean = false, + segments: Array[Double] = Array.empty[Double] + ): Unit + def endClip(): Unit + def fill(color: Color): Unit + def line(x: Double, y: Double, closedPath: Boolean = false): Unit + def lineTo(x1: Double, y1: Double, x2: Double, y2: Double): Unit + def pentagon( + x: Double, + y: Double, + diameter: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit + def quadraticCurveTo(cpx: Double, cpy: Double, x: Double, y: Double): Unit + def rectangle( + x: Double, + y: Double, + width: Double, + height: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit + def rotate(angle: Double): Unit + def roundedRectangle( + x: Int, + y: Int, + width: Int, + height: Int, + diameter: Int, + segments: Array[Double] = Array.empty[Double] + ): Unit + def star( + x: Double, + y: Double, + diameter: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit + def stroke(color: Color): Unit + def strokeText(text: String, x: Double, y: Double): Unit + def square( + x: Double, + y: Double, + size: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit + def text( + text: String, + x: Double, + y: Double, + color: Color = Color.black, + font: String = "25px serif" + ): Unit + def transform( + a: Double, + b: Double, + c: Double, + d: Double, + e: Double, + f: Double + ): Unit + def translate(x: Double, y: Double): Unit + def triangle( + x1: Double, + y1: Double, + x2: Double, + y2: Double, + x3: Double, + y3: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit +} + +class ImmediateImpl( + rasterWidth: Int, + rasterHeight: Int, + ctx: CanvasRenderingContext2D, + region: Path2D +) extends Immediate { + def arc( + x: Int, + y: Int, + diameter: Int, + startAngle: Double, + endAngle: Double, + counterclockwise: Boolean = false, + closedPath: Boolean = false, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + ctx.beginPath() + ctx.setLineDash(segments.toJSArray) + ctx.arc(x0, y0, diameter / 2, startAngle, endAngle, counterclockwise) + if closedPath then ctx.closePath(); + ctx.stroke() + } + + def arcTo( + x1: Int, + y1: Int, + x2: Int, + y2: Int, + diameter: Int, + counterclockwise: Boolean = false, + closedPath: Boolean = false, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + ctx.beginPath() + ctx.arcTo(x1, y1, x2, y2, diameter / 2) + ctx.stroke() + } + + def bezierCurveTo( + cp1x: Int, + cp1y: Int, + cp2x: Int, + cp2y: Int, + x: Int, + y: Int, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + ctx.beginPath() + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) + ctx.stroke() + } + + def clearRect(x: Int, y: Int, width: Int, height: Int): Unit = { + val x0 = x / 2 + val y0 = y / 2 + ctx.clearRect(x0, y0, width, height) + } + + def clip(): Unit = { + ctx.save() + ctx.clip(region) + } + + def clipArc( + x: Int, + y: Int, + diameter: Int, + startAngle: Double = 0, + endAngle: Double = 2 * Math.PI, + counterclockwise: Boolean = false + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + region.arc(x0, y0, diameter / 2, startAngle, endAngle, counterclockwise) + } + + def clipRect(x: Int, y: Int, width: Int, height: Int): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + region.rect(x0, y0, width, height) + } + + def circle( + x: Double, + y: Double, + diameter: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2.0 + val y0 = y - rasterHeight / 2.0 + ctx.beginPath() + ctx.setLineDash(segments.toJSArray) + ctx.arc(x0, y0, diameter / 2, 0, 2 * Math.PI) + ctx.stroke() + } + + def dashLine( + x1: Double, + y1: Double, + x2: Double, + y2: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x1 - rasterWidth / 2 + val y0 = y1 - rasterHeight / 2 + val x = x2 - rasterWidth / 2 + val y = y2 - rasterHeight / 2 + ctx.beginPath() + ctx.setLineDash(segments.toJSArray) + ctx.moveTo(x0, y0) + ctx.lineTo(x, y) + ctx.stroke() + } + + def ellipse( + x: Double, + y: Double, + width: Double, + height: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + ctx.beginPath() + ctx.setLineDash(segments.toJSArray) + ctx.ellipse(x0, y0, width / 2, height / 2, 0, 0, 2 * Math.PI) + ctx.fill() + } + + def ellipseWithRotation( + x: Double, + y: Double, + diameterX: Double, + diameterY: Double, + rotation: Double, + startAngle: Double, + endAngle: Double, + counterclockwise: Boolean = false, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + ctx.beginPath() + ctx.setLineDash(segments.toJSArray) + ctx.ellipse( + x0, + y0, + diameterX, + diameterY, + rotation, + startAngle, + endAngle, + counterclockwise + ) + ctx.fill() + } + + def endClip(): Unit = { + ctx.restore() + } + + def fill(color: Color): Unit = { + ctx.fillStyle = CanvasDrawing.colorToCSS(color) + ctx.fill() + } + + def text( + text: String, + x: Double, + y: Double, + color: Color = Color.black, + font: String = "25px serif" + ): Unit = { + ctx.save() + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + ctx.fillStyle = CanvasDrawing.colorToCSS(color) + ctx.font = font; + ctx.beginPath() + ctx.translate(x0, y0) + ctx.rotate(Math.PI) + ctx.scale(-1, 1) + ctx.fillText(text, -ctx.measureText(text).width / 2, 0) + ctx.fill() + ctx.restore() + } + + def line(x: Double, y: Double, closedPath: Boolean = false): Unit = { + ctx.lineTo(x - rasterWidth / 2, y - rasterHeight / 2) + if closedPath then ctx.closePath() + ctx.stroke() + } + + def lineTo(x1: Double, y1: Double, x2: Double, y2: Double): Unit = { + val x0 = x1 - rasterWidth / 2 + val y0 = y1 - rasterHeight / 2 + val x = x2 - rasterWidth / 2 + val y = y2 - rasterHeight / 2 + ctx.beginPath(); + ctx.moveTo(x0, y0) + ctx.lineTo(x, y) + ctx.stroke() + } + + def pentagon( + x: Double, + y: Double, + diameter: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + + @tailrec + def drawSide(i: Int): Unit = { + if i <= 5 then { + val angle = 2 * Math.PI * i / 5 + val x1 = x0 + (diameter / 2) * Math.cos(angle) + val y1 = y0 + (diameter / 2) * Math.sin(angle) + ctx.lineTo(x1, y1) + drawSide(i + 1) + } + } + ctx.beginPath() + ctx.moveTo(x0 + (diameter / 2), y0) + drawSide(1) + } + + def quadraticCurveTo(cpx: Double, cpy: Double, x: Double, y: Double): Unit = { + ctx.beginPath() + ctx.quadraticCurveTo(cpx, cpy, x, y) + ctx.stroke() + } + + def rectangle( + x: Double, + y: Double, + width: Double, + height: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + ctx.beginPath() + ctx.rect(x0, y0, width, height) + } + + def rotate(angle: Double): Unit = { + ctx.rotate(angle) + } + + def roundedRectangle( + x: Int, + y: Int, + width: Int, + height: Int, + diameter: Int, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + ctx.beginPath() + ctx.moveTo(x0 + (diameter / 2), y0) + ctx.arcTo(x0 + width, y0, x0 + width, y0 + height, diameter / 2) + ctx.arcTo(x0 + width, y0 + height, x0, y0 + height, diameter / 2) + ctx.arcTo(x0, y0 + height, x0, y0, diameter / 2) + ctx.arcTo(x0, y0, x0 + width, y0, diameter / 2) + ctx.fill() + } + + def star( + x1: Double, + y1: Double, + diameter: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val outerRadius = diameter / 2 + val innerRadius = diameter / 4 + val angle = Math.PI / 5 + val x = x1 - rasterWidth / 2 + val y = y1 - rasterHeight / 2 + + ctx.beginPath() + ctx.moveTo( + x + Math.cos(0) * outerRadius, + y + Math.sin(0) * outerRadius + ) + + for i <- 1 to 10 do { + val isOuter = i % 2 == 0 + val r = if isOuter then outerRadius else innerRadius + val a = i * angle + ctx.lineTo( + x + Math.cos(a) * r, + y + Math.sin(a) * r + ) + } + ctx.closePath() + ctx.fill() + } + + def stroke(color: Color): Unit = { + ctx.strokeStyle = CanvasDrawing.colorToCSS(color) + ctx.stroke() + } + + def strokeText(text: String, x: Double, y: Double): Unit = { + val x0 = x - rasterWidth * 2 + ctx.strokeText(text, x0, y) + } + + def square( + x: Double, + y: Double, + size: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + val x0 = x - rasterWidth / 2 + val y0 = y - rasterHeight / 2 + ctx.beginPath() + ctx.setLineDash(segments.toJSArray) + ctx.rect(x0, y0, size, size) + } + + def transform( + a: Double, + b: Double, + c: Double, + d: Double, + e: Double, + f: Double + ): Unit = { + ctx.transform(a, b, c, d, e, f) + } + + def translate(x: Double, y: Double): Unit = { + ctx.translate(x, y) + } + + def triangle( + x1: Double, + y1: Double, + x2: Double, + y2: Double, + x3: Double, + y3: Double, + segments: Array[Double] = Array.empty[Double] + ): Unit = { + ctx.beginPath() + ctx.moveTo(x1, y1) + ctx.lineTo(x2, y2) + ctx.lineTo(x3, y3) + ctx.lineTo(x1, y1) + ctx.fill() + } +} diff --git a/canvas/src/main/scala/doodle/canvas/algebra/Raster.scala b/canvas/src/main/scala/doodle/canvas/algebra/Raster.scala new file mode 100644 index 00000000..da5e8013 --- /dev/null +++ b/canvas/src/main/scala/doodle/canvas/algebra/Raster.scala @@ -0,0 +1,40 @@ +/* + * 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 doodle.algebra.Algebra +import doodle.algebra.generic.* +import doodle.core.Transform as Tx + +trait Raster extends GenericRaster[CanvasDrawing, Immediate] { + self: Algebra { type Drawing[U] = Finalized[CanvasDrawing, U] } => + + object RasterApi extends RasterApi { + def raster( + tx: Tx, + width: Int, + height: Int + )(f: Immediate => Unit): CanvasDrawing[Unit] = { + CanvasDrawing.setTransform(tx) >> + CanvasDrawing.raster(width, height)(f) + } + + def unit: CanvasDrawing[Unit] = { + CanvasDrawing.unit + } + } +} diff --git a/canvas/src/main/scala/doodle/canvas/algebra/Text.scala b/canvas/src/main/scala/doodle/canvas/algebra/Text.scala index 1338345f..dd3bcf56 100644 --- a/canvas/src/main/scala/doodle/canvas/algebra/Text.scala +++ b/canvas/src/main/scala/doodle/canvas/algebra/Text.scala @@ -18,6 +18,7 @@ package doodle.canvas.algebra import doodle.algebra.Algebra import doodle.algebra.generic.* +import doodle.core.* import doodle.core.BoundingBox import doodle.core.font.Font import doodle.core.{Transform as Tx} @@ -36,8 +37,7 @@ trait Text extends GenericText[CanvasDrawing] { font: Font, text: String, bounds: Bounds - ): CanvasDrawing[Unit] = - ??? + ): CanvasDrawing[Unit] = ??? def textBoundingBox(text: String, font: Font): (BoundingBox, Bounds) = { ??? diff --git a/core/js/src/main/scala/doodle/syntax/package.scala b/core/js/src/main/scala/doodle/syntax/package.scala index 607578ca..103a8e12 100644 --- a/core/js/src/main/scala/doodle/syntax/package.scala +++ b/core/js/src/main/scala/doodle/syntax/package.scala @@ -25,6 +25,7 @@ package object syntax { with LayoutSyntax with NormalizedSyntax with PathSyntax + with RasterSyntax with RendererSyntax with ShapeSyntax with SizeSyntax @@ -41,6 +42,7 @@ package object syntax { object layout extends LayoutSyntax object normalized extends NormalizedSyntax object path extends PathSyntax + object raster extends RasterSyntax object renderer extends RendererSyntax object shape extends ShapeSyntax object size extends SizeSyntax diff --git a/core/jvm/src/main/scala/doodle/syntax/package.scala b/core/jvm/src/main/scala/doodle/syntax/package.scala index bdb53859..ee8b89ee 100644 --- a/core/jvm/src/main/scala/doodle/syntax/package.scala +++ b/core/jvm/src/main/scala/doodle/syntax/package.scala @@ -27,6 +27,7 @@ package object syntax { with LayoutSyntax with NormalizedSyntax with PathSyntax + with RasterSyntax with RendererSyntax with ShapeSyntax with SizeSyntax @@ -46,6 +47,7 @@ package object syntax { object layout extends LayoutSyntax object normalized extends NormalizedSyntax object path extends PathSyntax + object raster extends RasterSyntax object renderer extends RendererSyntax object shape extends ShapeSyntax object size extends SizeSyntax diff --git a/core/shared/src/main/scala/doodle/algebra/Raster.scala b/core/shared/src/main/scala/doodle/algebra/Raster.scala new file mode 100644 index 00000000..8d41a707 --- /dev/null +++ b/core/shared/src/main/scala/doodle/algebra/Raster.scala @@ -0,0 +1,32 @@ +/* + * 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 +package algebra + +trait Raster[A] extends Algebra { + def raster(width: Int, height: Int)(f: A => Unit): Drawing[Unit] +} + +trait RasterConstructor[A] { + self: BaseConstructor { type Algebra <: Raster[A] } => + + def raster(width: Int, height: Int)(f: A => Unit): Picture[Unit] = + new Picture[Unit] { + def apply(implicit algebra: Algebra): algebra.Drawing[Unit] = + algebra.raster(width, height)(f) + } +} diff --git a/core/shared/src/main/scala/doodle/algebra/generic/GenericRaster.scala b/core/shared/src/main/scala/doodle/algebra/generic/GenericRaster.scala new file mode 100644 index 00000000..8e217ad0 --- /dev/null +++ b/core/shared/src/main/scala/doodle/algebra/generic/GenericRaster.scala @@ -0,0 +1,53 @@ +/* + * 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 +package algebra +package generic + +import cats.data.State +import doodle.core.BoundingBox +import doodle.core.{Transform as Tx} + +trait GenericRaster[G[_], A] extends Raster[A] { + self: Algebra { type Drawing[U] = Finalized[G, U] } => + + trait RasterApi { + def raster( + tx: Tx, + width: Int, + height: Int + )(f: A => Unit): G[Unit] + def unit: G[Unit] + } + + def RasterApi: RasterApi + + def raster(width: Int, height: Int)(f: A => Unit): Finalized[G, Unit] = { + Finalized.leaf { dc => + val bb = BoundingBox.centered(width, height) + ( + bb, + State.inspect(tx => RasterApi.raster(tx, width, height)(f)) + ) + } + } + + def empty: Finalized[G, Unit] = + Finalized.leaf { _ => + (BoundingBox.empty, Renderable.unit(RasterApi.unit)) + } +} diff --git a/core/shared/src/main/scala/doodle/syntax/RasterSyntax.scala b/core/shared/src/main/scala/doodle/syntax/RasterSyntax.scala new file mode 100644 index 00000000..ba84b210 --- /dev/null +++ b/core/shared/src/main/scala/doodle/syntax/RasterSyntax.scala @@ -0,0 +1,35 @@ +/* + * 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 +package syntax + +import doodle.algebra.Algebra +import doodle.algebra.Picture +import doodle.algebra.Raster + +trait RasterSyntax { + def raster[Alg <: Algebra, R]( + width: Int, + height: Int + )( + f: R => Unit + ): Picture[Alg & Raster[R], Unit] = + new Picture[Alg & Raster[R], Unit] { + def apply(implicit algebra: Alg & Raster[R]): algebra.Drawing[Unit] = + algebra.raster(width, height)(f) + } +} diff --git a/docs/src/pages/canvas/examples.md b/docs/src/pages/canvas/examples.md index 55b3df82..f1a38b9d 100644 --- a/docs/src/pages/canvas/examples.md +++ b/docs/src/pages/canvas/examples.md @@ -12,6 +12,10 @@ The source for these examples is [in the repository](https://github.com/creative @:doodle("parametric-spiral", "CanvasParametricSpiral.draw") +## Canvas Immediate Mode + +@:doodle("CanvasImmediateMode", "CanvasImmediateMode.draw") + ## Frame Background diff --git a/docs/src/pages/svg/README.md b/docs/src/pages/svg/README.md index f7272af8..019fb841 100644 --- a/docs/src/pages/svg/README.md +++ b/docs/src/pages/svg/README.md @@ -61,3 +61,6 @@ The source for these examples is [in the repository](https://github.com/creative ### Parametric Spiral @:doodle("parametric-spiral", "SvgParametricSpiral.draw") + +### Experimenting +@:doodle("Experiment", "Experiment.draw") diff --git a/examples/js/src/main/scala/doodle/examples/canvas/CanvasFrameBackground.scala b/examples/js/src/main/scala/doodle/examples/canvas/CanvasFrameBackground.scala index 21c171cf..9d1c5930 100644 --- a/examples/js/src/main/scala/doodle/examples/canvas/CanvasFrameBackground.scala +++ b/examples/js/src/main/scala/doodle/examples/canvas/CanvasFrameBackground.scala @@ -17,8 +17,8 @@ package doodle.examples.canvas import cats.effect.unsafe.implicits.global -import doodle.core.Color import doodle.canvas.{*, given} +import doodle.core.Color import doodle.syntax.all.* import scala.scalajs.js.annotation.* diff --git a/examples/js/src/main/scala/doodle/examples/canvas/CanvasImmediateMode.scala b/examples/js/src/main/scala/doodle/examples/canvas/CanvasImmediateMode.scala new file mode 100644 index 00000000..bd3311a8 --- /dev/null +++ b/examples/js/src/main/scala/doodle/examples/canvas/CanvasImmediateMode.scala @@ -0,0 +1,233 @@ +/* + * 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.examples.canvas + +import cats.effect.unsafe.implicits.global +import doodle.canvas.{*, given} +import doodle.canvas.algebra.Immediate +import doodle.core.* +import doodle.syntax.all.* +import scala.annotation.tailrec + +import scala.scalajs.js.annotation.* + +@JSExportTopLevel("CanvasImmediateMode") +object Experiment { + + def drawHotel = + (ctx: Immediate) => { + + ctx.rectangle(25, 0, 150, 200) + ctx.fill(Color.rgb(142, 113, 88)) + ctx.stroke(Color.black) + + ctx.rectangle(45, 0, 20, 40) + ctx.fill(Color.rgb(72, 36, 20)) + @tailrec + def drawWindows(index: Int): Unit = { + if index < 12 then { + val row = index / 4 + val col = index % 4 + val x = 30 + col * 40 + val y = 160 - row * 40 + + val color = (index, col) match { + case (0 | 1 | 4 | 5 | 8, _) => Color.rgb(255, 174, 0) + case (2 | 3 | 6 | 9, _) => Color.black + case _ => Color.rgb(255, 174, 0) + } + + ctx.square(x, y, 20) + ctx.fill(color) + + drawWindows(index + 1) + } + } + + drawWindows(0) + + ctx.stroke(Color.black) + ctx.rectangle(175, 0, 50, 200) + ctx.fill(Color.rgb(105, 66, 43)) + @tailrec + def drawLines(y: Int): Unit = { + if y >= 0 then { + ctx.lineTo(175, y, 225, y) + drawLines(y - 25) + } + } + + drawLines(165) + + ctx.ellipse(130, 35, 75, 50, Array(5, 5)) + ctx.fill(Color.rgb(234, 215, 163)) + + ctx.text("HOTEL", 130, 32, font = "20px Fraktur") + } + + val hotel = raster(240, 200)(drawHotel) + + def drawApartment = + (ctx: Immediate) => { + @tailrec + def drawBuildings(index: Int): Unit = { + if index < 5 then { + val x = index * 50 + val height = index match { + case 0 => 200 + case 1 | 3 => 150 + case 2 => 175 + case 4 => 200 + } + + ctx.rectangle(x, 0, 50, height) + ctx.fill(Color.rgb(178, 165, 148)) + ctx.stroke(Color.black) + + @tailrec + def drawWindows(row: Int, col: Int): Unit = { + if row < 4 && col < 2 then { + val windowX = x + col * 20 + 5 + val windowY = height - (row + 1) * 40 + 10 + if windowY > 60 then { + ctx.rectangle(windowX, windowY, 15, 25) + ctx.fill(Color.rgb(220, 220, 220)) + ctx.stroke(Color.black) + } + if col < 1 then drawWindows(row, col + 1) + else drawWindows(row + 1, 0) + } + } + + drawWindows(0, 0) + drawBuildings(index + 1) + } + } + + @tailrec + def drawShops(index: Int): Unit = { + if index < 5 then { + val x = index * 50 + val color = index match { + case 0 => Color.red + case 1 => Color.rgb(39, 89, 70) + case 2 => Color.rgb(59, 31, 92) + case 3 => Color.rgb(133, 36, 81) + case 4 => Color.rgb(59, 31, 92) + } + + ctx.rectangle(x, 40, 50, 10) + ctx.fill(color) + ctx.stroke(Color.black) + + ctx.rectangle(x, 0, 50, 40) + ctx.fill(Color.rgb(204, 239, 234)) + ctx.stroke(Color.black) + + drawShops(index + 1) + } + } + + drawBuildings(0) + drawShops(0) + + ctx.clipRect(150, 150, 50, 50) + ctx.clipRect(50, 150, 50, 50) + ctx.clip() + + ctx.circle(100, 150, 50) + ctx.fill(Color.rgb(178, 165, 148)) + ctx.stroke(Color.black) + ctx.circle(150, 150, 50) + ctx.fill(Color.rgb(178, 165, 148)) + ctx.stroke(Color.black) + + ctx.endClip() + + ctx.lineTo(100, 150, 100, 200) + ctx.lineTo(100, 150, 50, 150) + ctx.lineTo(150, 150, 150, 200) + ctx.lineTo(150, 150, 200, 150) + } + + def drawCitySpace = + (ctx: Immediate) => { + ctx.rectangle(0, 0, 130, 200) + ctx.fill(Color.rgb(185, 185, 185)) + ctx.stroke(Color.black) + ctx.rectangle(10, 130, 50, 60) + ctx.fill(Color.rgb(162, 0, 0)) + ctx.stroke(Color.black) + ctx.rectangle(70, 130, 50, 60) + ctx.fill(Color.black); + ctx.text("CITY SPACE", 65, 100, font = "20px Fraktur") + + ctx.rectangle(10, 60, 110, 30) + ctx.fill(Color.rgb(204, 239, 234)) + ctx.stroke(Color.black) + + ctx.rectangle(10, 20, 30, 30) + ctx.fill(Color.rgb(204, 239, 234)) + ctx.stroke(Color.black) + ctx.rectangle(50, 0, 30, 50) + ctx.fill(Color.rgb(204, 239, 234)) + ctx.stroke(Color.black) + ctx.rectangle(90, 20, 30, 30) + ctx.fill(Color.rgb(204, 239, 234)) + ctx.stroke(Color.black) + } + + def drawRoad = + (ctx: Immediate) => { + ctx.rectangle(0, 0, 800, 50) + ctx.fill(Color.rgb(135, 135, 144)) + ctx.stroke(Color.black) + ctx.dashLine(0, 25, 800, 25, Array(5, 5)) + } + + val citySpace = raster(150, 200)(drawCitySpace) + val apartments = raster(250, 200)(drawApartment) + val road = raster(800, 50)(drawRoad) + + val tree = + Picture + .triangle(100, 100) + .fillColor(Color.green) + .strokeColor(Color.black) + .above( + Picture + .rectangle(20, 50) + .fillColor(Color.brown) + .strokeColor(Color.black) + .beside( + Picture + .rectangle(20, 50) + .fillColor(Color.brown) + .strokeColor(Color.black) + ) + ) + .at(0, -120) + .scale(0.5, 0.5) + + val joint = (citySpace + .beside(apartments.beside(hotel.beside(tree).beside(tree)))) + .above(road) + + @JSExport + def draw(mount: String) = + joint.drawWithFrame(Frame(mount)) +}