Skip to content

Commit

Permalink
Add size layout method
Browse files Browse the repository at this point in the history
This allows directly setting the width and height of the bounding box.

Closes #158
  • Loading branch information
noelwelsh committed Apr 16, 2024
1 parent b68cbee commit 71c5399
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 10 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## NEXT

- Add `size` method to set bounding box width and height. Distinguished by the
existing `size` method on `Size` by it's arguments. (#158)


## 0.22.0 10-Apr-2024

- Arcs are available as paths on `OpenPath`, `ClosedPath`, and `PathElement`,
Expand Down
11 changes: 11 additions & 0 deletions core/shared/src/main/scala/doodle/algebra/Layout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ trait Layout extends Algebra {
left: Double
): Drawing[A]

/** Set the width and height of the given `Drawing's` bounding box to the
* given values. The new bounding box has the same origin as the original
* bounding box, and extends symmetrically above and below, and left and
* right of the origin.
*/
def size[A](img: Drawing[A], width: Double, height: Double): Drawing[A]

// Derived methods

def under[A](bottom: Drawing[A], top: Drawing[A])(implicit
Expand Down Expand Up @@ -92,4 +99,8 @@ trait Layout extends Algebra {
margin(img, height, width, height, width)
def margin[A](img: Drawing[A], width: Double): Drawing[A] =
margin(img, width, width, width, width)

/** Utility to set the width and height to the same value. */
def size[A](img: Drawing[A], extent: Double): Drawing[A] =
size(img, extent, extent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package generic

import cats._
import cats.implicits._
import doodle.core.BoundingBox
import doodle.core.Landmark
import doodle.core.Transform

Expand Down Expand Up @@ -108,12 +109,40 @@ trait GenericLayout[G[_]] extends Layout {
left: Double
): Finalized[G, A] =
img.map { case (bb, rdr) =>
val newBb = bb.copy(
val newBb = BoundingBox(
left = bb.left - left,
top = bb.top + top,
right = bb.right + right,
bottom = bb.bottom - bottom
)
(newBb, rdr)
}

def size[A](
img: Finalized[G, A],
width: Double,
height: Double
): Finalized[G, A] = {
assert(
width >= 0,
s"Called `size` with a width of ${width}. The bounding box's width must be non-negative."
)
assert(
height >= 0,
s"Called `size` with a height of ${height}. The bounding box's height must be non-negative."
)
val w = width / 2.0
val h = height / 2.0

img.map { case (bb, rdr) =>
val newBb = BoundingBox(
left = -w,
top = h,
right = w,
bottom = -h
)

(newBb, rdr)
}
}
}
12 changes: 12 additions & 0 deletions core/shared/src/main/scala/doodle/syntax/LayoutSyntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,17 @@ trait LayoutSyntax {
def apply(implicit algebra: Alg with Layout): algebra.Drawing[A] =
algebra.margin(picture(algebra), width)
}

def size(width: Double, height: Double): Picture[Alg with Layout, A] =
new Picture[Alg with Layout, A] {
def apply(implicit algebra: Alg with Layout): algebra.Drawing[A] =
algebra.size(picture(algebra), width, height)
}

def size(extent: Double): Picture[Alg with Layout, A] =
new Picture[Alg with Layout, A] {
def apply(implicit algebra: Alg with Layout): algebra.Drawing[A] =
algebra.size(picture(algebra), extent)
}
}
}
25 changes: 21 additions & 4 deletions core/shared/src/test/scala/doodle/algebra/generic/LayoutSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,27 @@ object LayoutSpec extends Properties("Layout properties") {
.margin(shape, top, right, bottom, left)
.boundingBox

newBb.left ?= bb.left - left
newBb.top ?= bb.top + top
newBb.right ?= bb.right + right
newBb.bottom ?= bb.bottom - bottom
(newBb.left ?= bb.left - left) &&
(newBb.top ?= bb.top + top) &&
(newBb.right ?= bb.right + right) &&
(newBb.bottom ?= bb.bottom - bottom)
}
}

property("size sets bounding box to the correct size") = {
val algebra = TestAlgebra()
val genShape = Generators.finalizedOfDepth(algebra, 5)
val genDim = Gen.choose[Double](0.0, 50.0)

forAllNoShrink(genShape, genDim, genDim) { (shape, width, height) =>
val newBb = algebra
.size(shape, width, height)
.boundingBox

(newBb.left ?= -(width / 2)) &&
(newBb.right ?= (width / 2)) &&
(newBb.top ?= (height / 2)) &&
(newBb.bottom ?= -(height / 2))
}
}
}
30 changes: 28 additions & 2 deletions docs/src/main/scala/pictures/Layout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ object Layout {

val circle = Picture.circle(50)
val rollingCircles =
circle
.margin(25)
.beside(circle.margin(15))
.beside(circle)
.beside(circle.margin(-15))
.beside(circle.margin(-25))
// Increase the bounding box so it covers the whole image.
// Otherwise the image is cropped when it is rendered.
.size(300, 100)

rollingCircles.save("pictures/rolling-circles.png")

val rollingCirclesMargin =
circle
.margin(25)
.debug
Expand All @@ -97,7 +110,20 @@ object Layout {
.beside(circle.margin(-25).debug)
// Increase the bounding box so it covers the whole image.
// Otherwise the image is cropped when it is rendered.
.margin(0, 50, 0, 0)
.size(300, 100)

rollingCircles.save("pictures/rolling-circles.png")
rollingCirclesMargin.save("pictures/rolling-circles-margin.png")

// Rolling circles using the size method
val rollingCirclesSize =
circle
.size(100, 25)
.debug
.beside(circle.size(80, 20).debug)
.beside(circle.size(50, 15).debug)
.beside(circle.size(20, 10).debug)
.beside(circle.size(0, 0).debug)
.size(300, 100)

rollingCirclesSize.save("pictures/rolling-circles-size.png")
}
31 changes: 28 additions & 3 deletions docs/src/pages/pictures/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,36 @@ val overlappingCircles =

### 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.
The `size` and `margin` methods allow direct manipulation of the bounding box. We will show examples below to generate this image:

@:image(rolling-circles.png) {
alt = Five circles with different bounding boxes
title = Five circles with different bounding boxes
}

We can directly adjust the size of the bounding box using `size`, which sets the width and height of the bounding box to the given values. These values must be non-negative, and the resulting bounding box distributes the width and height equally between the left and right, and top and bottom, respectively. Here's an example where we set the width and height to different values, and use `debug` to draw the resulting bounding boxes.

```scala mdoc:silent
val rollingCirclesSize =
circle
.size(100, 25)
.debug
.beside(circle.size(80, 20).debug)
.beside(circle.size(50, 15).debug)
.beside(circle.size(20, 10).debug)
.beside(circle.size(0, 0).debug)
```

@:image(rolling-circles-size.png) {
alt = Five circles with different bounding boxes
title = Five circles with different bounding boxes
}

To adjust the existing bounding box 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.

```scala mdoc:silent
val circle = Picture.circle(50)
val rollingCircles =
val rollingCirclesMargin =
circle
.margin(25)
.debug
Expand All @@ -149,7 +174,7 @@ val rollingCircles =
.beside(circle.margin(-25).debug)
```

@:image(rolling-circles.png) {
@:image(rolling-circles-margin.png) {
alt = Five circles with different margins
title = Five circles with different margins
}
Expand Down
Binary file added golden/src/test/golden/image-size.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added golden/src/test/golden/layout-size.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions golden/src/test/scala/doodle/golden/ImageLayout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ class ImageLayout extends FunSuite with GoldenImage {
.originAt(Landmark(Coordinate.percent(50), Coordinate.percent(50)))
)
}

testImage("image-size") {
Image.circle(100).size(50, 50).beside(Image.square(50)).size(150, 150)
}
}
34 changes: 34 additions & 0 deletions golden/src/test/scala/doodle/golden/Layout.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 golden

import cats.implicits._
import doodle.java2d._
import doodle.syntax.all._
import munit._

class Layout extends FunSuite with GoldenPicture {

testPicture("layout-size") {
Picture
.regularPolygon(4, 100)
.size(50, 50)
.beside(regularPolygon[Algebra](5, 100))
.size(300, 300)
}
}
7 changes: 7 additions & 0 deletions image/shared/src/main/scala/doodle/image/Image.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ sealed abstract class Image extends Product with Serializable {
def margin(width: Double): Image =
Margin(this, width, width, width, width)

def size(width: Double, height: Double): Image =
Size(this, width, height)

// Context Transform ------------------------------------------------

def strokeColor(color: Color): Image =
Expand Down Expand Up @@ -190,6 +193,8 @@ object Image {
bottom: Double,
left: Double
) extends Image
final case class Size(image: Image, width: Double, height: Double)
extends Image
final case class OriginAt(image: Image, landmark: Landmark) extends Image
// Style
final case class StrokeWidth(image: Image, width: Double) extends Image
Expand Down Expand Up @@ -321,6 +326,8 @@ object Image {
algebra.at(compile(image)(algebra), landmark)
case Margin(image, top, right, bottom, left) =>
algebra.margin(compile(image)(algebra), top, right, bottom, left)
case Size(image, width, height) =>
algebra.size(compile(image)(algebra), width, height)
case OriginAt(image, landmark) =>
algebra.originAt(compile(image)(algebra), landmark)

Expand Down

0 comments on commit 71c5399

Please sign in to comment.