Low-Level Drawing API
Drawing Primitives
In EvilPlot, we represent everything that can be drawn to the screen as a Drawable
.
A Drawable
is simply a description of the scene, and constructing one does not actually render anything. There are
only a handful of drawing primitives that exist within EvilPlot, and they can be divided into three categories: drawing,
positioning, and style.
The drawing primitives are the “leaves” of a scene. They represent concrete things like shapes and text. They are:
Line
Path
Rect
BorderRect
Disc
Wedge
Polygon
Text
Positioning Primitives
Positioning primitives alter where other Drawable
s are placed. Notably, Drawable
scenes do not expose a notion of
absolute coordinates.
Translate
Rotate
Scale
Affine
Styling Primitives
Styling primitives allow us to modify the appearance of scenes.
Style
colors the inside of aDrawable
StrokeStyle
colors the outline of aDrawable
StrokeWeight
changes the thickness of the outline of aDrawable
LineDash
applies a line style to the outline of aDrawable
Composition
Creating scenes is simply a matter of composing these primitives. For example, to place a blue rectangle next to a red circle, you might write:
import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors._
val rect = Style(Rect(400, 400), HTMLNamedColors.red)
Group(
Seq(
Style(
Translate(Disc(200), x = rect.extent.width),
HTMLNamedColors.blue
),
rect
)
)
The geometry
package also provides convenient syntax for dealing with positioning and styling primitives. So, instead
of nesting constructors like we did up there, we can do the following with the same result.
import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors._
val rect = Rect(40, 40) filled HTMLNamedColors.red
Disc(20) transX rect.extent.width filled HTMLNamedColors.blue behind rect
The drawing API gives us the power to describe all of the scenes involved in the plots that EvilPlot can create; at no point do plot implementations reach below it and onto the rendering target.
Positional Combinators
EvilPlot gives a higher-level vocabulary to refer to common geometric manipulations. In fact, above we just wrote a
positional combinator called beside
. The geometry
package is full of similar combinators, so most of the time you’ll
never have to manually think about shifting objects by their widths like we did above.
beside
behind
above
Alignment
Additionally, you can align an entire sequence of Drawable
s by calling one of the Align
methods, which produce a new
sequence with all elements aligned. Combining alignment with reducing using a binary position operator is especially
helpful:
import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors.HTMLNamedColors._
import com.cibo.evilplot.numeric.Point
val aligned: Seq[Drawable] = Align.right(
Polygon(Seq(Point(0, 30), Point(15, 0), Point(30, 30))) filled red,
Rect(50, 50) filled blue,
Disc(15) filled red,
)
aligned.reduce(_ below _)
The available alignment functions are:1.
Align.bottom
Align.right
Align.middle
Align.center
An example
With EvilPlot’s drawing combinators on our side, we’re well equipped to recreate the box example from above that we made using only primitives–except it will be far easier to reason about. Recall, first we created our elements: two lines and two boxes, then centered them vertically and placed them on top of each other.
import com.cibo.evilplot.geometry._
import com.cibo.evilplot.colors.HTMLNamedColors.{blue, white}
// Some values from the outside:
val width = 100
val topWhisker = 60
val bottomWhisker = 40
val upperToMiddle = 50
val middleToLower = 45
val strokeWidth = 3
Align.center(
Line(topWhisker, strokeWidth).rotated(90),
BorderRect.filled(width, upperToMiddle),
BorderRect.filled(width, middleToLower),
Line(bottomWhisker, strokeWidth).rotated(90)
).reduce(_ above _)
.colored(blue)
.filled(white)
And in fact, if you were to look at the source code for EvilPlot’s default box plot, it would be almost identical.
Drawing to the screen
Of course, at some point we want to draw our scenes to the screen. To do that, we call draw()
trait Drawable {
// ...
def draw(ctx: RenderContext): Unit
}
The RenderContext
is the interface between our drawing API and platform-specific rendering targets. For more
information on how to obtain a RenderContext
, see the docs page.
-
A
.reduce(_ above _)
or.reduce(_ beside _)
was applied to each of these so the examples didn’t stomp all over each other. ↩