From d5ffa8ff8f2f5345f79f30b89f82d6e5ecd7c6a1 Mon Sep 17 00:00:00 2001 From: Vansh Date: Mon, 16 Feb 2026 14:56:17 +0530 Subject: [PATCH 1/4] Add Landmark support for margin and size in Layout --- .../main/scala/doodle/algebra/Layout.scala | 45 +++++++ .../algebra/generic/GenericLayout.scala | 68 ++++++++++ .../scala/doodle/syntax/LayoutSyntax.scala | 39 ++++++ .../examples/LandmarkLayoutExamples.scala | 120 ++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala diff --git a/algebra/shared/src/main/scala/doodle/algebra/Layout.scala b/algebra/shared/src/main/scala/doodle/algebra/Layout.scala index ee2be138..665c2480 100644 --- a/algebra/shared/src/main/scala/doodle/algebra/Layout.scala +++ b/algebra/shared/src/main/scala/doodle/algebra/Layout.scala @@ -55,6 +55,20 @@ 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. + */ + def margin[A]( + img: Drawing[A], + top: Landmark, + right: Landmark, + bottom: Landmark, + left: Landmark + ): 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 @@ -62,6 +76,14 @@ trait Layout extends Algebra { */ 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. + */ + def size[A](img: Drawing[A], width: Landmark, height: Landmark): Drawing[A] + // Derived methods def under[A](bottom: Drawing[A], top: Drawing[A])(implicit @@ -100,7 +122,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) } diff --git a/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala b/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala index d2a11f91..fcf838a8 100644 --- a/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala +++ b/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala @@ -145,4 +145,72 @@ trait GenericLayout[G[_]] extends Layout { (newBb, rdr) } } + 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) + } + + 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) +} } diff --git a/algebra/shared/src/main/scala/doodle/syntax/LayoutSyntax.scala b/algebra/shared/src/main/scala/doodle/syntax/LayoutSyntax.scala index 642e84d1..0dcbd40e 100644 --- a/algebra/shared/src/main/scala/doodle/syntax/LayoutSyntax.scala +++ b/algebra/shared/src/main/scala/doodle/syntax/LayoutSyntax.scala @@ -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) + } } } diff --git a/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala b/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala new file mode 100644 index 00000000..f26fb1e7 --- /dev/null +++ b/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala @@ -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() + } +} From d86d40c4486fa687df2cb5468ad873ea6103426a Mon Sep 17 00:00:00 2001 From: Vansh Date: Tue, 17 Feb 2026 20:13:53 +0530 Subject: [PATCH 2/4] few checks --- .../main/scala/doodle/algebra/Layout.scala | 10 ++--- .../algebra/generic/GenericLayout.scala | 12 +++--- .../examples/LandmarkLayoutExamples.scala | 42 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/algebra/shared/src/main/scala/doodle/algebra/Layout.scala b/algebra/shared/src/main/scala/doodle/algebra/Layout.scala index 665c2480..e4ced7e3 100644 --- a/algebra/shared/src/main/scala/doodle/algebra/Layout.scala +++ b/algebra/shared/src/main/scala/doodle/algebra/Layout.scala @@ -78,9 +78,9 @@ trait Layout extends Algebra { /** 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. + * (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. */ def size[A](img: Drawing[A], width: Landmark, height: Landmark): Drawing[A] @@ -143,8 +143,8 @@ trait Layout extends Algebra { 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. + * 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) diff --git a/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala b/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala index fcf838a8..4bbcfae1 100644 --- a/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala +++ b/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala @@ -158,7 +158,7 @@ trait GenericLayout[G[_]] extends Layout { // 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 @@ -166,7 +166,7 @@ trait GenericLayout[G[_]] extends Layout { 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, @@ -186,11 +186,11 @@ trait GenericLayout[G[_]] extends Layout { // 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, @@ -200,7 +200,7 @@ trait GenericLayout[G[_]] extends Layout { 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 @@ -212,5 +212,5 @@ trait GenericLayout[G[_]] extends Layout { ) (newBb, rdr) -} + } } diff --git a/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala b/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala index f26fb1e7..f537b6b2 100644 --- a/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala +++ b/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala @@ -24,88 +24,88 @@ 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) + 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 + * + * 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) } From 3e2605a735113b856849d8de752cdf960fbc067e Mon Sep 17 00:00:00 2001 From: Vansh Date: Sun, 22 Feb 2026 15:43:51 +0530 Subject: [PATCH 3/4] fix some typo and did some checks --- .../main/scala/doodle/algebra/Layout.scala | 22 +++++++++++++++++-- .../algebra/generic/GenericLayout.scala | 4 ++-- .../examples/LandmarkLayoutExamples.scala | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/algebra/shared/src/main/scala/doodle/algebra/Layout.scala b/algebra/shared/src/main/scala/doodle/algebra/Layout.scala index e4ced7e3..825aa12b 100644 --- a/algebra/shared/src/main/scala/doodle/algebra/Layout.scala +++ b/algebra/shared/src/main/scala/doodle/algebra/Layout.scala @@ -60,6 +60,11 @@ trait Layout extends Algebra { * 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], @@ -67,7 +72,14 @@ trait Layout extends Algebra { right: Landmark, bottom: Landmark, left: Landmark - ): Drawing[A] + ): 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 @@ -81,8 +93,14 @@ trait Layout extends Algebra { * (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] + 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 diff --git a/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala b/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala index 4bbcfae1..4a5f6e74 100644 --- a/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala +++ b/algebra/shared/src/main/scala/doodle/algebra/generic/GenericLayout.scala @@ -145,7 +145,7 @@ trait GenericLayout[G[_]] extends Layout { (newBb, rdr) } } - def margin[A]( + override def margin[A]( img: Finalized[G, A], top: Landmark, right: Landmark, @@ -176,7 +176,7 @@ trait GenericLayout[G[_]] extends Layout { (newBb, rdr) } - def size[A]( + override def size[A]( img: Finalized[G, A], width: Landmark, height: Landmark diff --git a/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala b/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala index f537b6b2..92782b23 100644 --- a/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala +++ b/examples/jvm/src/main/scala/doodle/examples/LandmarkLayoutExamples.scala @@ -82,7 +82,7 @@ object LandmarkLayoutExamples { * Creates a grid where elements scale proportionally */ def responsiveGrid = { - val cell = square(60).fillColor(Color.hotpink) + val cell = square(60).fillColor(Color.hotPink) // Each cell scales to 80% of its size val scaled = cell.size(Landmark.percent(80, 80)) From 315ea9da00a88bd8a3d39f7f336cd45b2c3639f2 Mon Sep 17 00:00:00 2001 From: Vansh Date: Sun, 22 Feb 2026 15:50:30 +0530 Subject: [PATCH 4/4] Ignore targets.tar --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f4b7c770..741c4b3f 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,4 @@ project/metals.sbt ## NPM node_modules/ +targets.tar