[orx-shapes] Add rectangle align, distribute and fit functions

This commit is contained in:
Edwin Jakobs
2025-07-06 23:35:40 +02:00
parent 1139183708
commit b259e9c2de
5 changed files with 325 additions and 0 deletions

View File

@@ -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)
}

View File

@@ -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<Rectangle>.alignToHorizontally(to: Rectangle, anchor: Double = 0.5): List<Rectangle> {
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<Rectangle>.alignToVertically(to: Rectangle, anchor: Double = 0.5): List<Rectangle> {
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<Rectangle>.distributeHorizontally(within: Rectangle = bounds): List<Rectangle> {
val usedWidth = sumOf { it.width }
val unusedWidth = within.width - usedWidth
val betweenWidth = unusedWidth / (size - 1)
val distributed = mutableListOf<Rectangle>()
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<Rectangle>.distributeVertically(within: Rectangle = bounds): List<Rectangle> {
val usedHeight = sumOf { it.height }
val unusedHeight = within.height - usedHeight
val betweenHeight = unusedHeight / (size - 1)
val distributed = mutableListOf<Rectangle>()
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<Rectangle>.fitHorizontally(within: Rectangle = bounds, gutter: Double = 0.0): List<Rectangle> {
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<Rectangle>()
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<Rectangle>.fitVertically(within: Rectangle = bounds, gutter: Double = 0.0): List<Rectangle> {
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<Rectangle>()
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
}

View File

@@ -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)
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}