From b259e9c2de245aca6bb0e7abea3dc6c018a8b8fd Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sun, 6 Jul 2025 23:35:40 +0200 Subject: [PATCH] [orx-shapes] Add rectangle align, distribute and fit functions --- .../kotlin/primitives/RectangleAdjacent.kt | 53 +++++++ .../kotlin/primitives/RectangleAlign.kt | 146 ++++++++++++++++++ .../kotlin/primitives/RectangleTake.kt | 55 +++++++ .../primitives/DemoRectangleDistribute01.kt | 36 +++++ .../DemoRectangleFitHorizontally.kt | 35 +++++ 5 files changed, 325 insertions(+) create mode 100644 orx-shapes/src/commonMain/kotlin/primitives/RectangleAdjacent.kt create mode 100644 orx-shapes/src/commonMain/kotlin/primitives/RectangleAlign.kt create mode 100644 orx-shapes/src/commonMain/kotlin/primitives/RectangleTake.kt create mode 100644 orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleDistribute01.kt create mode 100644 orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleFitHorizontally.kt diff --git a/orx-shapes/src/commonMain/kotlin/primitives/RectangleAdjacent.kt b/orx-shapes/src/commonMain/kotlin/primitives/RectangleAdjacent.kt new file mode 100644 index 00000000..63752507 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/primitives/RectangleAdjacent.kt @@ -0,0 +1,53 @@ +package org.openrndr.extra.shapes.primitives + +import org.openrndr.shape.Rectangle + +/** + * Adjusts the dimensions and position of the rectangle based on the provided parameters. + * The method calculates the new dimensions and coordinates of the rectangle based on specified + * values for left, right, top, bottom, width, or height. If conflicting parameters are provided + * (e.g., both `left` and `right` or `top` and `bottom`), an error is thrown. + * + * @param left Optional offset to adjust the left side of the rectangle. + * If provided along with `right`, an error is thrown. + * @param right Optional offset to adjust the right side of the rectangle. + * If provided along with `left`, an error is thrown. + * @param top Optional offset to adjust the top side of the rectangle. + * If provided along with `bottom`, an error is thrown. + * @param bottom Optional offset to adjust the bottom side of the rectangle. + * If provided along with `top`, an error is thrown. + * @param width Optional value to override the width of the rectangle. Ignored if both + * `left` and `right` are provided. + * @param height Optional value to override the height of the rectangle. Ignored if both + * `top` and `bottom` are provided. + * @return A new [Rectangle] with the adjusted dimensions and position. + */ +fun Rectangle.adjacent(left: Double? = null, right: Double? = null, top: Double? = null, bottom: Double? = null, + width: Double? = null, height: Double? = null + +) : Rectangle { + val newWidth = when { + left != null && right != null -> this.width + left + right + else -> width?: this.width + } + val newHeight = when { + top != null && bottom != null -> this.height + top + bottom + else -> height?: this.height + } + + val newX = when { + left != null && right != null -> error("set either left or right, not both") + left != null -> corner.x - newWidth - left + right != null -> corner.x + this.width + right + else -> corner.x + } + + val newY = when { + bottom != null && top != null -> error("set either top or bottom, not both") + top != null -> corner.y - newHeight - top + bottom != null -> corner.y + this.height + bottom + else -> corner.y + } + + return Rectangle(newX, newY, newWidth, newHeight) +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/primitives/RectangleAlign.kt b/orx-shapes/src/commonMain/kotlin/primitives/RectangleAlign.kt new file mode 100644 index 00000000..18d8b0d5 --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/primitives/RectangleAlign.kt @@ -0,0 +1,146 @@ +package org.openrndr.extra.shapes.primitives + +import org.openrndr.math.Vector2 +import org.openrndr.shape.Rectangle +import org.openrndr.shape.bounds + +/** + * Aligns a list of rectangles horizontally relative to a specified rectangle. + * + * Each rectangle in the list is repositioned so that its horizontal + * alignment matches the specified anchor point of the target rectangle. + * + * @param to The target rectangle to align to. + * @param anchor A value between 0.0 and 1.0 representing the horizontal position + * within the target rectangle. Default is 0.5 (center). + * @return A new list of rectangles aligned horizontally relative to the target rectangle. + */ +fun List.alignToHorizontally(to: Rectangle, anchor: Double = 0.5): List { + val tox = to.position(anchor, 0.0).x + + return this.map { + Rectangle.fromAnchor(Vector2(anchor, 0.0), Vector2(tox, it.y), it.width, it.height) + } +} + +/** + * Aligns the rectangles in the list vertically to a reference rectangle. + * The vertical position of each rectangle is determined based on the reference rectangle + * and the specified vertical anchor point. + * + * @param to The reference rectangle to which the list of rectangles is vertically aligned. + * @param anchor A value between 0.0 and 1.0 representing the vertical anchor point. + * Defaults to 0.5, which aligns based on the center. + * @return A new list of rectangles aligned vertically to the specified rectangle. + */ +fun List.alignToVertically(to: Rectangle, anchor: Double = 0.5): List { + val toy = to.position(0.0, anchor).y + + return this.map { + Rectangle.fromAnchor(Vector2(0.0, anchor), Vector2(it.x, toy), it.width, it.height) + } +} + +/** + * Distributes the rectangles in the list horizontally within a specified bounding rectangle. + * + * Each rectangle is positioned at regular intervals, ensuring equal spacing between them. + * The method maintains the height and y-coordinate of each rectangle, only adjusting their x-coordinates. + * + * @param within The bounding rectangle within which the rectangles are horizontally distributed. + * Defaults to the bounding rectangle covering all rectangles in the list. + * @return A new list of rectangles with updated positions that are evenly distributed horizontally. + */ +fun List.distributeHorizontally(within: Rectangle = bounds): List { + val usedWidth = sumOf { it.width } + val unusedWidth = within.width - usedWidth + val betweenWidth = unusedWidth / (size - 1) + + val distributed = mutableListOf() + + var x = within.x + for (i in indices) { + distributed.add(Rectangle(x, this[i].y, this[i].width, this[i].height)) + x += this[i].width + betweenWidth + } + return distributed +} + +/** + * Distributes the rectangles in the list vertically within the given bounding rectangle. + * The rectangles are spaced evenly, ensuring an equal distance between them, while + * maintaining their original height and width. + * + * @param within The bounding rectangle within which the rectangles will be vertically + * distributed. Defaults to the minimal bounding rectangle containing all rectangles in the list. + * @return A new list of rectangles that are vertically distributed within the specified bounds. + */ +fun List.distributeVertically(within: Rectangle = bounds): List { + val usedHeight = sumOf { it.height } + val unusedHeight = within.height - usedHeight + val betweenHeight = unusedHeight / (size - 1) + + val distributed = mutableListOf() + + var y = within.y + for (i in indices) { + distributed.add(Rectangle(this[i].x, y, this[i].width, this[i].height)) + y += this[i].height + betweenHeight + } + return distributed +} + +/** + * Distributes a list of rectangles horizontally within a given container rectangle, + * maintaining their relative width proportions and adding an optional gutter + * between them. + * + * @param within The container rectangle within which the rectangles will be distributed. + * The default value is the bounding box of the current list of rectangles. + * @param gutter The space (in units) to be added between adjacent rectangles. Default is 0.0. + * @return A new list of rectangles distributed horizontally within the container rectangle. + */ +fun List.fitHorizontally(within: Rectangle = bounds, gutter: Double = 0.0): List { + val gutterlessWidth = within.width - gutter * (size - 1) + + val usedWidth = sumOf { it.width } + val ratios = map { it.width / usedWidth } + + var x = within.x + val distributed = mutableListOf() + for (i in indices) { + val w = ratios[i] * gutterlessWidth + distributed.add(Rectangle(x, this[i].y, w, this[i].height)) + x += w + gutter + } + return distributed +} + +/** + * Fits a list of rectangles within a given vertical rectangular area. + * Each rectangle's height is adjusted proportionally based on its original height + * relative to the total height of all rectangles in the list. The rectangles + * are then distributed vertically, with an optional gutter spacing between them. + * + * @param within The bounding rectangle within which the list of rectangles should + * fit. If not provided, the bounds of the current list of rectangles + * will be used. + * @param gutter The vertical spacing between the rectangles. Default value is 0.0. + * @return A new list of rectangles that are proportionally resized and vertically + * distributed within the specified bounding rectangle. + */ +fun List.fitVertically(within: Rectangle = bounds, gutter: Double = 0.0): List { + val gutterlessHeight = within.height - gutter * (size - 1) + + val usedHeight = sumOf { it.height } + val ratios = map { it.height / usedHeight } + + var y = within.y + val distributed = mutableListOf() + for (i in indices) { + val h = ratios[i] * gutterlessHeight + distributed.add(Rectangle(this[i].x, y, this[i].width, h)) + y += h + gutter + } + return distributed +} \ No newline at end of file diff --git a/orx-shapes/src/commonMain/kotlin/primitives/RectangleTake.kt b/orx-shapes/src/commonMain/kotlin/primitives/RectangleTake.kt new file mode 100644 index 00000000..70ae13de --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/primitives/RectangleTake.kt @@ -0,0 +1,55 @@ +package org.openrndr.extra.shapes.primitives + +import org.openrndr.shape.Rectangle + +/** + * Creates a new [Rectangle] by modifying its dimensions and position based on the provided parameters. + * The method adjusts the position and size of the rectangle depending on which of the optional parameters + * are supplied. Any omitted parameters are calculated to maintain the rectangle's overall layout. + * + * Some quick recipes: + * * Take a 40x50 rectangle from the center: `Rectangle(0.0, 0.0, 100.0, 100.0).take(width=40.0, height=50.0)` + * + *. * Take a 20x30 rectangle from the top left: `Rectangle(0.0, 0.0, 100.0, 100.0).take(left=0.0, top=0.0, width=20, height=30.0)` + * + * * Take a 10x30 rectangle from the bottom right: `Rectangle(0.0, 0.0, 100.0, 100.0).take(bottom=0.0, right=0.0, width=20, height=30.0)` + * + * @param left The amount to shift the rectangle's left edge inward. If null, the left edge remains unchanged. + * @param top The amount to shift the rectangle's top edge inward. If null, the top edge remains unchanged. + * @param right The amount to shift the rectangle's right edge inward. If null, the right edge remains unchanged. + * @param bottom The amount to shift the rectangle's bottom edge inward. If null, the bottom edge remains unchanged. + * @param width The new width for the rectangle. If null, the width is adjusted based on left and right shifts. + * @param height The new height for the rectangle. If null, the height is adjusted based on top and bottom shifts. + * @return A new [Rectangle] instance with the updated dimensions and position. + */ +fun Rectangle.take( + left: Double? = null, + top: Double? = null, + right: Double? = null, + bottom: Double? =null, + width: Double? = null, + height: Double? = null +) :Rectangle { + val newWidth = when { + width != null -> width + else -> this.width - (left ?: 0.0) - (right ?: 0.0) + } + + val newHeight = when { + height != null -> height + else -> this.height - (top ?: 0.0) - (bottom ?: 0.0) + } + + val newX = when { + left != null -> corner.x + left + right != null -> corner.x + this.width - right - newWidth + else -> corner.x + (this.width - newWidth) / 2.0 + } + + val newY = when { + top != null -> corner.y + top + bottom != null -> corner.y + this.height - bottom - newHeight + else -> corner.y + (this.height - newHeight) / 2.0 + } + return Rectangle(newX, newY, newWidth, newHeight) +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleDistribute01.kt b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleDistribute01.kt new file mode 100644 index 00000000..10df00fb --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleDistribute01.kt @@ -0,0 +1,36 @@ +package primitives + +import org.openrndr.application +import org.openrndr.extra.noise.shapes.uniformSub +import org.openrndr.extra.shapes.primitives.alignToVertically +import org.openrndr.extra.shapes.primitives.distributeHorizontally +import org.openrndr.shape.Rectangle +import kotlin.math.cos +import kotlin.random.Random + +/** This function creates an interactive graphical application that displays a dynamic visual composition + * of rectangles, which are generated and manipulated based on time and random parameters. The application + * follows these steps: + * + * 1. Initializes a random generator seeded with the elapsed seconds since the start of the program. + * 2. Creates a sequence of rectangles using the `uniformSub` function to generate random sub-rectangles + * within the bounding rectangle of the canvas. + * 3. Distributes the generated rectangles horizontally within the canvas using the `distributeHorizontally` method. + * 4. Aligns the rectangles vertically according to their position in relation to the bounding rectangle + * and a dynamic anchor point derived from the cosine of elapsed time. + * 5. Renders the rectangles on the canvas in the output window. + */ +fun main() { + application { + program { + extend { + val random = Random(seconds.toInt()) + val rs = (0 until 7).map { drawer.bounds.uniformSub(minWidth = 0.01, maxWidth = 0.1, random = random) } + .distributeHorizontally(drawer.bounds) + .alignToVertically(drawer.bounds, cos(seconds) * 0.5 + 0.5) + + drawer.rectangles(rs) + } + } + } +} \ No newline at end of file diff --git a/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleFitHorizontally.kt b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleFitHorizontally.kt new file mode 100644 index 00000000..8eb3fe1a --- /dev/null +++ b/orx-shapes/src/jvmDemo/kotlin/primitives/DemoRectangleFitHorizontally.kt @@ -0,0 +1,35 @@ +package primitives + +import org.openrndr.application +import org.openrndr.extra.noise.shapes.uniformSub +import org.openrndr.extra.shapes.primitives.alignToHorizontally +import org.openrndr.extra.shapes.primitives.alignToVertically +import org.openrndr.extra.shapes.primitives.distributeHorizontally +import org.openrndr.extra.shapes.primitives.fitHorizontally +import org.openrndr.extra.shapes.primitives.fitVertically +import org.openrndr.shape.Rectangle +import kotlin.math.cos +import kotlin.random.Random + +fun main() = application { + configure { + width = 720 + height = 720 + } + program { + extend { + val rs = (0 until 7).map { drawer.bounds.uniformSub(minWidth = 0.01, maxWidth = 0.1, random = Random(it)) } + .fitHorizontally(drawer.bounds, gutter = 30.0 * mouse.position.y / height) + .alignToVertically(drawer.bounds, cos(seconds) * 0.5 + 0.5) + + drawer.rectangles(rs) + + + val rsh = (0 until 7).map { drawer.bounds.uniformSub(minHeight = 0.01, maxHeight = 0.1, random = Random(it)) } + .fitVertically(drawer.bounds, gutter = 30.0 * mouse.position.y / height) + .alignToHorizontally(drawer.bounds, cos(seconds) * 0.5 + 0.5) + + drawer.rectangles(rsh) + } + } +}