Skip to content

Commit

Permalink
Rendering to Java2D canvas does not deadlock
Browse files Browse the repository at this point in the history
- Rendering to the Java2D canvas no longer waits for the canvas to be
closed. This in turns means that rendering does not deadlock animations,
and animations should now work as expected.

- Add some documentation for the Java2D backend

- Fix up some the animation examples to work with the more recent
changes

- Other miscellaneous fixes

Closes #176
  • Loading branch information
noelwelsh committed Dec 13, 2024
1 parent 6fdcd74 commit b1f4488
Show file tree
Hide file tree
Showing 24 changed files with 171 additions and 73 deletions.
12 changes: 4 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Install sbt
if: contains(runner.os, 'macos')
run: brew install sbt
uses: sbt/setup-sbt@v1

- name: Checkout current branch (full)
uses: actions/checkout@v4
Expand Down Expand Up @@ -99,8 +98,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Install sbt
if: contains(runner.os, 'macos')
run: brew install sbt
uses: sbt/setup-sbt@v1

- name: Checkout current branch (full)
uses: actions/checkout@v4
Expand Down Expand Up @@ -164,8 +162,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Install sbt
if: contains(runner.os, 'macos')
run: brew install sbt
uses: sbt/setup-sbt@v1

- name: Checkout current branch (full)
uses: actions/checkout@v4
Expand Down Expand Up @@ -200,8 +197,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Install sbt
if: contains(runner.os, 'macos')
run: brew install sbt
uses: sbt/setup-sbt@v1

- name: Checkout current branch (full)
uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ object LayoutSpec extends Properties("Layout properties") {
property("hand generated at bounding boxes are correct") = {
import doodle.syntax.all.*
import doodle.syntax.approximatelyEqual.*
import doodle.algebra.generic.*
import Instances.*
import TestAlgebra.*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package generic
package reified

import cats.data.WriterT
import doodle.algebra.generic.*
import doodle.core.*
import doodle.core.Transform as Tx

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package generic
package reified

import cats.data.WriterT
import doodle.algebra.generic.*
import doodle.core.Point
import doodle.core.Transform as Tx

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package generic
package reified

import cats.data.WriterT
import doodle.algebra.generic.*
import doodle.core.BoundingBox
import doodle.core.Transform as Tx
import doodle.core.font.Font
Expand Down
2 changes: 1 addition & 1 deletion docs/src/pages/canvas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The rendered picture will appear where the element is positioned on your web pag
## Frame

The `Frame` must specify the `id` of the element where the drawing should be placed.
Additional options include the background color and the frame size. The frame size can either be determined by the size of the picture or a fixed size. The first case is the default, and includes a 20 pixel border. You can change this by calling `withSizedToPicture`. The picture will be drawn centered on the drawing area. A fixed size frame, specified with `withSize`, has the given size and the origin is set to the center of the drawing area. This means that pictures that are not centered a the origin will not be centered on the screen.
Additional options include the background color and the frame size. The frame size can either be determined by the size of the picture or a fixed size. The first case is the default, and includes a 20 pixel border. You can change this by calling `withSizedToPicture`. The picture will be drawn centered on the drawing area. A fixed size frame, specified with `withSize`, has the given size and the origin is set to the center of the drawing area. This means that pictures that are not centered at the origin will not be centered on the screen.



Expand Down
1 change: 1 addition & 0 deletions docs/src/pages/directory.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ laika.navigationOrder = [
interact
algebra
effect
java2d
svg
canvas
development
Expand Down
80 changes: 80 additions & 0 deletions docs/src/pages/java2d/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Doodle Java2D

Doodle Java2D draws Doodle pictures using [Java 2D](https://www.oracle.com/java/technologies/java-2d-api.html), the graphic library that ships with the JVM.


## Usage

Firstly, import the Doodle core and syntax, the Java2D definitions, and the default Cats Effect runtime.

```scala mdoc:silent
import cats.effect.unsafe.implicits.global
import doodle.core.*
import doodle.java2d.*
import doodle.syntax.all.*
```

Now you can draw `Pictures` like so:

```scala mdoc:compile-only
Picture.circle(100).draw()
```

The drawing will appear in a window on the screen.

You can define a @:api(doodle.java2d.effect.Frame) to have more control over the appearance of the window in which a `Picture` is drawn.
The `Frame` allows you to specify, for example, the title for the window, the background color, or the size of the window.
Here's an example:

```scala mdoc:silent
val frame =
Frame.default
.withSize(600, 600)
.withCenterAtOrigin
.withBackground(Color.midnightBlue)
.withTitle("Oodles of Doodles!")
```

This sets:

- the size of the window to 600 pixels wide and high;
- the center of the window to the origin (0, 0), instead of the center of the bounding box of the `Picture` that is rendered;
- the background color to midnight blue; and
- the window's title to "Oodles of Doodles!"

We can drawn to a frame using the `drawWithFrame` syntax.

```scala mdoc:compile-only
Picture.circle(100).drawWithFrame(frame)
```


## Canvas

The Java2D @:api(doodle.java2d.effect.Canvas) offers several features that are useful for interactive programs.
It provides the following streams of events:

* `mouseClick`, which is a `Stream[IO, Point]` producing the position of mouse clicks;
* `mouseMove`, which is a `Stream[IO, Point]` producing the position of the mouse cursor and updates when the mouse moves;
* `redraw`, which is a `Stream[IO, Int]` producing a value approximately every 16.67ms (i.e. at a rate of 60fps). The value is the duration, in milliseconds, since the previous event.

Also available are

* `closed`, an `IO[Unit]` that produces a value when the window has been closed; and
* `close()`, a method to programmatically close the window instead of waiting for the user to do so.

To create a `Canvas` you call the `canvas()` method on a `Frame`.
The returns a `Resource` that will produce the canvas when `use`d.
Here's a small example:

```scala mdoc:compile-only
frame
.canvas()
.use { canvas =>
// Do stuff here
// This example just closes the canvas
canvas.close()
}
```

`
3 changes: 3 additions & 0 deletions docs/src/pages/java2d/directory.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
laika.navigationOrder = [
README.md
]
Original file line number Diff line number Diff line change
@@ -1,3 +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.examples.svg

import cats.effect.IO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package examples
import cats.instances.all.*
import cats.syntax.all.*
import doodle.core.*
import doodle.image.*
import doodle.random.*
import doodle.syntax.all.*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package image
package examples

import doodle.core.*
import doodle.image.*
import doodle.syntax.all.*

object TimeSeries {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package image
package examples

import doodle.core.*
import doodle.image.*
import doodle.syntax.all.*

object Tree {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package examples

import cats.syntax.all.*
import doodle.core.*
import doodle.image.*
import doodle.random.*
import doodle.syntax.all.*

Expand Down
14 changes: 10 additions & 4 deletions java2d/src/main/scala/doodle/java2d/effect/Canvas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ final class Canvas private (
redraw.merge(mouseClick).merge(mouseMove).merge(closeStream)
}

private val interruptWhen = windowClosed.void.attempt
private val interruptWhen = closed.attempt
val redraw: Stream[IO, Int] =
redrawTopic.subscribe(4).interruptWhen(interruptWhen)
val mouseClick: Stream[IO, Point] = mouseClickTopic
Expand All @@ -120,16 +120,22 @@ final class Canvas private (
}
}
object Canvas {

/** Creates a Canvas Resource from the given Frame.
*
* The Resource, when used, will wait for the user to close the window before
* the use block returns. Call the close() method to programmatically close
* the window if that is desired.
*/
def apply(frame: Frame): Resource[IO, Canvas] = {
(Topic[IO, Int], Topic[IO, Point], Topic[IO, Point])
.mapN { (redrawTopic, mouseClickTopic, mouseMoveTopic) =>
new Canvas(frame, redrawTopic, mouseClickTopic, mouseMoveTopic)
}
.toResource
.flatMap(canvas => canvas.stream.compile.drain.background.as(canvas))
.flatMap(canvas =>
canvas.stream.compile.drain.background
.as(canvas)
.onFinalize(canvas.close().void)
Resource.make(IO.pure(canvas))(canvas => canvas.closed)
)
}
}
6 changes: 3 additions & 3 deletions java2d/src/main/scala/doodle/java2d/effect/Java2DPanel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ final class Java2DPanel(
* Default size is 1 as the most common case is being asked to render only
* one picture.
*
* As an optimization with check the [[Redraw]] property of the [[Frame]],
* and if we use an opaque color to redraw we only keep the last element
* around. See [[opaqueRedraw]].
* As an optimization we check the [[Redraw]] property of the [[Frame]], and
* if we use an opaque color to redraw we only keep the last element around.
* See [[opaqueRedraw]].
*/
private val pictures: ArrayBuffer[(BoundingBox, List[Reified])] =
new ArrayBuffer(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ object Java2dRenderer extends Renderer[Algebra, Frame, Canvas] {
def canvas(description: Frame): Resource[IO, Canvas] =
Canvas(description)

def render[A](canvas: Canvas)(picture: Picture[A]): IO[A] = {
val result = canvas.render(picture)
canvas.closed >> result
}
def render[A](canvas: Canvas)(picture: Picture[A]): IO[A] =
canvas.render(picture)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ final class Java2dWindow(
}

/** Event listener for redraw events. Translates events into the time since
* the last frame (TODO: what are the units)?
* the last frame, in milliseconds.
*/
private val frameEvent: ActionListener = {

Expand Down
Loading

0 comments on commit b1f4488

Please sign in to comment.