Making an Explorer for your Art Piece
Using Explore, creating a GUI for your Doodle art means writing
two functions: explorer
, and render
. explorer
describes the
GUI of the doodle, and render
generates a Picture
from the GUI's
output values.
Explorer
Your explorer
function describes the GUI for your doodle, implicitly using
a few explore
interpreters targeting your preferred backend. You'll need
an implicit argument for each type of component, e.g. ExploreInt
or ExploreColor
.
You'll probably also want to use a Layout
interpreter. Interpreters are generic
over their Component
type, which corresponds to the targeted backend. The two
backends built in to Doodle Explore are doodle.explore.java2d.Component
, targeting
Java Swing, and doodle.explore.laminar.Component
, targeting HTML/JS via laminar
.
Let's make a simple GUI for the Sierpinski triangle using explore
. It should allow
users to adjust the base triangle size, the number of iterations, and the stroke color
of the triangles. To accomplish this, we'll need the interpreters ExploreInt
and ExploreColor
for our components, and then a Layout
interpreter to put them together. We'll start with the
java2d
backend to avoid dealing with HTML. Our function skeleton looks like this:
import doodle.explore.java2d.Component
import doodle.explore.{ExploreInt, ExploreColor, Layout}
def explorer(using
intGui: ExploreInt[Component],
colorGui: ExploreColor[Component],
layoutGui: Layout[Component],
) = {
import intGui._
import colorGui._
???
}
Now let's add some components. To add a GUI component that outputs an
integer, we can use any function found in ExploreInt
that returns a F[Int]
.
To start out with, use int(label: String)
. This element will adjust the
base size of the triangles. We probably want to restrict this to a range,
so we can use within
. IntComponentOps
contains a few extension methods
so we can call it in infix.
int("Base Size").within(1 to 30)
To compose multiple components and choose how they are layed out, we use
Layout
. Let's add another integer to adjust the number of iterations:
int("Base Size").within(1 to 30)
.beside(int("Iterations").within(1 to 6).withDefault(2))
Finally, we'll use ExploreColor
to add a color picker. Our final function
looks like this:
def explorer(using
intGui: ExploreInt[Component],
colorGui: ExploreColor[Component],
layoutGui: Layout[Component],
) = {
int("Base Size").within(1 to 30)
.beside(int("Iterations").within(1 to 6).withDefault(2))
.above(color("Stroke Color"))
}
Render
The render
function describes how to create a Picture
from the values
produced by your explorer
. Because the output of the explorer will be
a nested tuple, it is best to use an anonymous function to destructure
the values and then pass them to another function that handles the rest.
For this to work, we have to pass the function directly to our explorer.explore
call so that type inference takes effect.
For this example, we will use the sierpinski function from doodle.image.examples.Sierpinski
.
We'll also use the following frame:
val frame = Frame(
FixedSize(1200.0, 1200.0),
"Explore",
AtOrigin,
Some(Color.white),
ClearToBackground,
)
Using this, we can finally run our explorer with:
explorer.explore(frame, { case ((size, iterations), color) =>
Image.compile {
doodle.image.examples.Sierpinski.sierpinski(iterations, size).strokeColor(color)
}
})
Our final code is this:
def explorer(using
intGui: ExploreInt[Component],
colorGui: ExploreColor[Component],
layoutGui: Layout[Component],
) = {
import intGui._
import colorGui._
int("Base Size").within(1 to 30)
.beside(int("Iterations").within(1 to 6).withDefault(2))
.above(color("Stroke Color"))
}
val frame = Frame(
FixedSize(1200.0, 1200.0),
"Explore",
AtOrigin,
Some(Color.white),
ClearToBackground,
)
explorer.explore(frame, { case ((size, iterations), color) =>
Image.compile {
doodle.image.examples.sierpinski(iterations, size).strokeColor(color)
}
})