Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,4 @@ project/metals.sbt

## NPM
node_modules/
targets.tar
63 changes: 63 additions & 0 deletions algebra/shared/src/main/scala/doodle/algebra/Layout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,53 @@ trait Layout extends Algebra {
left: Double
): Drawing[A]

/** Expand the bounding box of img by the given amounts. Each Landmark
* parameter can specify either absolute (point) or relative (percent)
* dimensions. Percentages are relative to the current bounding box size:
* left/right margins are percentages of the current width, top/bottom
* margins are percentages of the current height.
*
* The default implementation evaluates Landmarks against a zero-sized
* bounding box (so percentage values resolve to 0 and point values give
* their absolute amounts). Override for full bbox-aware behaviour (as
* GenericLayout does).
*/
def margin[A](
img: Drawing[A],
top: Landmark,
right: Landmark,
bottom: Landmark,
left: Landmark
): Drawing[A] =
margin(
img,
top.y.eval(0, 0),
right.x.eval(0, 0),
bottom.y.eval(0, 0),
left.x.eval(0, 0)
)

/** 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]

/** Set the width and height of the given `Drawing's` bounding box. Each
* Landmark parameter can specify either absolute (point) or relative
* (percent) dimensions. Percentages are relative to the current bounding box
* size. For example, Landmark.percent(200, 200) will double the size, while
* Landmark.percent(50, 50) will halve it.
*
* The default implementation evaluates Landmarks against a zero-sized
* bounding box (so percentage values resolve to 0 and point values give
* their absolute amounts). Override for full bbox-aware behaviour (as
* GenericLayout does).
*/
def size[A](img: Drawing[A], width: Landmark, height: Landmark): Drawing[A] =
size(img, width.x.eval(0, 0), height.y.eval(0, 0))

// Derived methods

def under[A](bottom: Drawing[A], top: Drawing[A])(implicit
Expand Down Expand Up @@ -100,7 +140,30 @@ trait Layout extends Algebra {
def margin[A](img: Drawing[A], width: Double): Drawing[A] =
margin(img, width, width, width, width)

/** Expand the bounding box by horizontal and vertical margins specified as
* Landmarks. Supports both absolute and percentage-based margins.
*/
def margin[A](
img: Drawing[A],
horizontal: Landmark,
vertical: Landmark
): Drawing[A] =
margin(img, vertical, horizontal, vertical, horizontal)

/** Expand the bounding box by the same margin on all sides, specified as a
* Landmark. Supports both absolute and percentage-based margins.
*/
def margin[A](img: Drawing[A], all: Landmark): Drawing[A] =
margin(img, all, all, all, all)

/** 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)

/** Set the width and height to the same value, specified as a Landmark.
* Supports both absolute and percentage-based sizing. For example, size(img,
* Landmark.percent(200, 200)) will double the size.
*/
def size[A](img: Drawing[A], extent: Landmark): Drawing[A] =
size(img, extent, extent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,72 @@ trait GenericLayout[G[_]] extends Layout {
(newBb, rdr)
}
}
override def margin[A](
img: Finalized[G, A],
top: Landmark,
right: Landmark,
bottom: Landmark,
left: Landmark
): Finalized[G, A] =
img.map { case (bb, rdr) =>
// Evaluate landmarks relative to current bounding box dimensions
// For left/right: use width as the reference dimension
// For top/bottom: use height as the reference dimension
val width = bb.width
val height = bb.height

// Evaluate each landmark coordinate
// Using the x-coordinate of the landmark for horizontal margins
// Using the y-coordinate of the landmark for vertical margins
val topMargin = top.y.eval(0, height)
val rightMargin = right.x.eval(0, width)
val bottomMargin = bottom.y.eval(0, height)
val leftMargin = left.x.eval(0, width)

val newBb = BoundingBox(
left = bb.left - leftMargin,
top = bb.top + topMargin,
right = bb.right + rightMargin,
bottom = bb.bottom - bottomMargin
)
(newBb, rdr)
}

override def size[A](
img: Finalized[G, A],
width: Landmark,
height: Landmark
): Finalized[G, A] =
img.map { case (bb, rdr) =>
// Evaluate landmarks relative to current bounding box dimensions
// Using the x-coordinate for width and y-coordinate for height
val currentWidth = bb.width
val currentHeight = bb.height

// Evaluate the new dimensions
val newWidth = width.x.eval(0, currentWidth)
val newHeight = height.y.eval(0, currentHeight)

// Validate the new dimensions
assert(
newWidth >= 0,
s"Evaluated size resulted in a width of ${newWidth}. The bounding box's width must be non-negative."
)
assert(
newHeight >= 0,
s"Evaluated size resulted in a height of ${newHeight}. The bounding box's height must be non-negative."
)

val w = newWidth / 2.0
val h = newHeight / 2.0

val newBb = BoundingBox(
left = -w,
top = h,
right = w,
bottom = -h
)

(newBb, rdr)
}
}
39 changes: 39 additions & 0 deletions algebra/shared/src/main/scala/doodle/syntax/LayoutSyntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -174,5 +174,44 @@ trait LayoutSyntax {
def apply(implicit algebra: Alg with Layout): algebra.Drawing[A] =
algebra.size(picture(algebra), extent)
}
// Landmark-based margin methods
def margin(
top: Landmark,
right: Landmark,
bottom: Landmark,
left: Landmark
): Picture[Alg with Layout, A] =
new Picture[Alg with Layout, A] {
def apply(implicit algebra: Alg with Layout): algebra.Drawing[A] =
algebra.margin(picture(algebra), top, right, bottom, left)
}

def margin(
horizontal: Landmark,
vertical: Landmark
): Picture[Alg with Layout, A] =
new Picture[Alg with Layout, A] {
def apply(implicit algebra: Alg with Layout): algebra.Drawing[A] =
algebra.margin(picture(algebra), horizontal, vertical)
}

def margin(all: Landmark): Picture[Alg with Layout, A] =
new Picture[Alg with Layout, A] {
def apply(implicit algebra: Alg with Layout): algebra.Drawing[A] =
algebra.margin(picture(algebra), all)
}

// Landmark-based size methods
def size(width: Landmark, height: Landmark): 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: Landmark): 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)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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

import cats.effect.unsafe.implicits.global
import doodle.core.*
import doodle.java2d.*
import doodle.syntax.all.*

object LandmarkLayoutExamples {

/** Example 1: Percentage-based sizing
*
* Demonstrates using Landmarks to create scaled versions of shapes
*/
def percentageSizing = {
val baseCircle = circle(50).fillColor(Color.royalBlue)

// Double the size (200%)
val doubleSize = baseCircle.size(Landmark.percent(200, 200))

// Half the size (50%)
val halfSize = baseCircle.size(Landmark.percent(50, 50))

// Different scaling in each dimension
val stretched = baseCircle.size(Landmark.percent(100, 150))

halfSize.beside(baseCircle).beside(doubleSize).beside(stretched)
}

/** Example 2: Percentage-based margins
*
* Shows how to use relative margins for responsive spacing
*/
def percentageMargins = {
val box = square(100).fillColor(Color.crimson)

// Add margin of 50% of current width/height on all sides
val withMargin = box.margin(Landmark.percent(50, 50))

// Add different margins on each side (top/bottom: 25%, left/right: 75%)
val asymmetricMargin = box.margin(
Landmark.percent(0, 75), // horizontal (left/right)
Landmark.percent(25, 0) // vertical (top/bottom)
)

withMargin.beside(asymmetricMargin)
}

/** Example 3: Mixing absolute and percentage values
*
* Demonstrates combining Point (absolute) and Percent coordinates
*/
def mixedValues = {
val rect = rectangle(80, 40).fillColor(Color.seaGreen)

// Size: 100% width, 200 pixels height
val mixedSize = rect.size(
Landmark(Coordinate.percent(100), Coordinate.point(200)),
Landmark(Coordinate.percent(100), Coordinate.point(200))
)

rect.beside(mixedSize)
}

/** Example 4: Responsive grid layout
*
* Creates a grid where elements scale proportionally
*/
def responsiveGrid = {
val cell = square(60).fillColor(Color.hotPink)

// Each cell scales to 80% of its size
val scaled = cell.size(Landmark.percent(80, 80))

// Add uniform margin of 20% around each cell
val withSpacing = scaled.margin(Landmark.percent(20, 20))

val row = withSpacing.beside(withSpacing).beside(withSpacing)
row.above(row).above(row)
}

/** Example 5: Progressive scaling
*
* Chain percentage operations to create compound effects
*/
def progressiveScaling = {
val start = circle(40).fillColor(Color.orange)

// Each subsequent circle is 125% of the previous one
val s1 = start.size(Landmark.percent(125, 125))
val s2 = s1.size(Landmark.percent(125, 125))
val s3 = s2.size(Landmark.percent(125, 125))

start.beside(s1).beside(s2).beside(s3)
}

def main(args: Array[String]): Unit = {
// Render all examples
percentageSizing.draw()
percentageMargins.draw()
mixedValues.draw()
responsiveGrid.draw()
progressiveScaling.draw()
}
}