Positioning pictures relative to other pictures is important for many compositions, and the Layout algebra provides a flexible system for handling this.
Above, Beside, and On
The most basic layout methods are
on. They do what their names suggest, putting a picture above, beside, or on top of another picture. Below is an example.
import doodle.core._ import doodle.java2d._ import doodle.syntax.all._ val basicLayout = Picture .circle(100) .strokeColor(Color.blue) .beside(Picture.square(100).strokeColor(Color.darkBlue)) .above(Picture.triangle(100, 100).strokeColor(Color.crimson)) .strokeWidth(5.0)
Here's the output this creates.
As a convenience, there are also methods
under, which are the opposite of
on respectively. That is,
a.above(b) == b.below(a) and
a.on(b) == b.under(a)
Bounding Box and Origin
To really understand how layout works we have to understand how layout works with the bounding box and origin. Every picture has a bounding box and origin. The bounding box defines the outer extent of the picture, and the origin is an arbitrary point within the bounding box. By convention, the built-in shapes and paths have their origin in the center of the bounding box. You can position the origin anywhere you want, either by creating your own paths or using the
originAt methods described below. If necessary, the bounding box will expand to include the origin.
We can see the bounding box and origin using the
debug method. In the example below I'm displaying the bounding box and origin of the circle and pentagon separately, above the bounding box and origin of the circle beside the pentagon.
val debugLayout = Picture .circle(100) .debug .beside(Picture.regularPolygon(5, 30).debug) .above( Picture.circle(100).beside(Picture.regularPolygon(5, 30)).debug )
This gives us some insight into how the basic layout works. Using
beside horizontally aligns the origins of the two pictures, the creates a new bounding box enclosing the two existing boxes with the new origin in the middle of the line joining the two origins.
Above works similarly, except the alignment is vertical, while
on simply places the origins at the same location.
Repositioning the Origin
The origin defines a local coordinate system for each picture, and the origin is always the point (0, 0). Changing the location of the origin is the key to creative layouts. There are two methods that do this:
at, which changes the location of the picture relative to the origin; and
originAt, which changes the location of the origin relative to the picture.
As you can see from the description, the two methods are opposites of one another. Let's see an example of use.
val atAndOriginAt = Picture .circle(100) .at(25, 25) .debug .beside(Picture.circle(100).originAt(25, 25).debug)
When you want to position pictures at arbitrary locations, a common pattern is to use
on. For example, here we position five shapes at the points of a pentagon. This also demonstrates we can use polar coordinates with
val pentagon = Picture .circle(10) .at(50, 0.degrees) .on(Picture.circle(10).at(50, 72.degrees)) .on(Picture.circle(10).at(50, 144.degrees)) .on(Picture.circle(10).at(50, 216.degrees)) .on(Picture.circle(10).at(50, 288.degrees))
Positioning using Landmarks
Landmark provides more flexible layout, by allowing you to specify points relative to the bounding box or origin instead of in absolute terms relative to the origin. For example, we can specify the top left of the bounding box by simply using
Landmark.topLeft instead of working out the coordinates of this location. Both
originAt support landmarks.
Ultimately, all landmarks are specified relative to the origin, but you can use a percentage Coordinate instead of an absolute value. Zero percent is the origin, 100% is the top or right edge of the bounding box, and -100% is the bottom or left edge of the bounding box.
In the example below we use landmarks to specify points that are halfway between the origin and the edge of the bounding box. In this simple example we could easily work out the absolute coordinate directly, but landmarks come into their own in more complex examples.
val overlappingCircles = Picture .circle(100) .originAt(Landmark(Coordinate.percent(50), Coordinate.percent(-50))) .on( Picture .circle(100) .originAt(Landmark(Coordinate.percent(-50), Coordinate.percent(-50))) ) .on( Picture .circle(100) .originAt(Landmark(Coordinate.percent(-50), Coordinate.percent(50))) ) .on( Picture .circle(100) .originAt(Landmark(Coordinate.percent(50), Coordinate.percent(50))) )
Adjusting the Bounding Box
To adjust the size of the bounding box, instead of the position of the origin, we can use
margin. This allows us to add extra space around a picture or, with a negative margin, to have a picture that overflows its bounding box. Here's an example that uses the form of
margin that adjusts both the width and height of the bounding box. There are other variants that allow us to adjust the width and the height separately, or adjust all four edges independently.
val circle = Picture.circle(50) val rollingCircles = circle .margin(25) .debug .beside(circle.margin(15).debug) .beside(circle.debug) .beside(circle.margin(-15).debug) .beside(circle.margin(-25).debug)
Layout algebra supports all the features described above.
Image doesn't support landmarks.