[orx-shapes] Comment the ContourAdjuster and its demos

This commit is contained in:
Abe Pazos
2025-08-29 13:00:27 +02:00
parent c531c45608
commit 2e6c637b49
9 changed files with 248 additions and 33 deletions

View File

@@ -79,41 +79,113 @@ data class ContourAdjusterEdge(val contourAdjuster: ContourAdjuster, val segment
contourAdjuster.updateSelection(newEdge.adjustments)
}
/**
* Convert the edge to a linear edge, truncating control points if those exist
*/
fun toLinear() = wrap { toLinear() }
/**
* Convert the edge to a cubic edge
*/
fun toCubic() = wrap { toCubic() }
/**
* Split the edge at [t]
* @param t an edge t value between 0 and 1. No splitting happens when t == 0 or t == 1.
*/
fun splitAt(t: Double) = wrap { splitAt(t) }
/**
* split edge in [numberOfParts] parts of equal length
* Split the edge in [numberOfParts] parts of equal length
*/
fun splitIn(numberOfParts: Int) = wrap { splitIn(numberOfParts) }
/**
* Creates a new contour edge by applying a translation to the current edge.
*
* @param translation the translation vector to apply to the contour edge.
* @param updateTangents whether the tangents of adjacent segments should be updated after the transformation.
* @return a new instance of the contour edge, transformed by the given translation.
*/
fun moveBy(translation: Vector2, updateTangents: Boolean = true) = wrap { movedBy(translation, updateTangents) }
/**
* Rotates the current edge by a specified angle around an anchor point relative to the edge.
* Optionally updates the tangents of adjacent segments after the rotation.
*
* @param rotationInDegrees the rotation angle in degrees to be applied.
* @param anchorT the relative position along the edge (range 0.0 to 1.0) defining the anchor point of rotation. Defaults to 0.5 (the center of the edge).
* @param updateTangents whether the tangents of adjacent segments should be updated after the rotation. Defaults to true.
*/
fun rotate(rotationInDegrees: Double, anchorT: Double = 0.5, updateTangents: Boolean = true) =
wrap { rotatedBy(rotationInDegrees, anchorT, updateTangents) }
fun scale(scaleFactor: Double, anchorT: Double = 0.5, updateTangents: Boolean = true) =
/**
* Scales the current edge by a specified factor, with an optional anchor point determining the
* scaling center along the edge. The scaling operation updates the tangents of the edge.
*/
fun scale(scaleFactor: Double, anchorT: Double = 0.5) =
wrap { scaledBy(scaleFactor, anchorT, updateTangents = true) }
fun replaceWith(t: Double, updateTangents: Boolean = true) = wrap { replacedWith(t, updateTangents) }
/**
* Replace this edge with a point at [t]
* @param t an edge t value between 0 and 1
*/
fun replaceWith(t: Double) = wrap { replacedWith(t) }
/**
* Replaces the current edge with the segments of an open shape contour.
*
* @param openContour the open shape contour whose segments replace the current edge. The provided
* contour must not be closed.
* @return a new ContourEdge instance with the updated segments from the `openContour`.
*/
fun replaceWith(openContour: ShapeContour) = wrap { replacedWith(openContour) }
fun sub(t0: Double, t1: Double, updateTangents: Boolean = true) {
/**
* Returns part of the edge between [t0] to [t1].
* Preserves topology unless t0 = t1.
* @param t0 the edge's start t-value, between 0 and 1
* @param t1 the edge's end t-value, between 0 and 1
*/
fun sub(t0: Double, t1: Double) {
contourAdjuster.contour =
ContourEdge(contourAdjuster.contour, segmentIndex())
.subbed(t0, t1)
.contour
}
/**
* Moves the starting point of the contour edge by the given translation vector.
*
* @param translation the translation vector to apply to the starting point of the contour edge.
* @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true.
* @return a new instance of the contour edge with the starting point moved by the given translation.
*/
fun moveStartBy(translation: Vector2, updateTangents: Boolean = true) = wrap { startMovedBy(translation, updateTangents) }
/**
* Moves the first control point of a contour edge by a specified translation vector.
*
* @param translation the translation vector to apply to the first control point of the contour edge.
* @return a new instance of the contour edge with the first control point moved by the given translation.
*/
fun moveControl0By(translation: Vector2) = wrap { control0MovedBy(translation) }
/**
* Moves the second control point (Control1) of a contour edge by a specified translation vector.
*
* @param translation the translation vector to apply to the second control point of the contour edge.
* @return a new instance of the contour edge with the second control point moved by the given translation.
*/
fun moveControl1By(translation: Vector2) = wrap { control1MovedBy(translation) }
/**
* Moves the end point of the contour edge by the specified translation vector.
*
* @param translation the translation vector to apply to the end point of the contour edge.
* @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true.
* @return a new instance of the contour edge with the end point moved by the given translation.
*/
fun moveEndBy(translation: Vector2, updateTangents: Boolean = true) = wrap { startMovedBy(translation, updateTangents) }
}

View File

@@ -57,7 +57,7 @@ data class ContourEdge(
}
/**
* convert the edge to a linear edge, truncating control points if those exist
* Convert the edge to a linear edge, truncating control points if those exist
*/
fun toLinear(): ContourEdge {
return if (contour.segments[segmentIndex].type != SegmentType.LINEAR) {
@@ -75,7 +75,7 @@ data class ContourEdge(
}
/**
* convert the edge to a cubic edge
* Convert the edge to a cubic edge
*/
fun toCubic(): ContourEdge {
return if (contour.segments[segmentIndex].type != SegmentType.CUBIC) {
@@ -99,10 +99,10 @@ data class ContourEdge(
/**
* replace this edge with a point at [t]
* Replace this edge with a point at [t]
* @param t an edge t value between 0 and 1
*/
fun replacedWith(t: Double, updateTangents: Boolean): ContourEdge {
fun replacedWith(t: Double): ContourEdge {
if (contour.empty) {
return withoutAdjustments()
}
@@ -125,6 +125,9 @@ data class ContourEdge(
return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex, adjustments)
}
/**
* Split the edge in [numberOfParts] parts of equal length
*/
fun splitIn(parts: Int): ContourEdge {
if (contour.empty || parts < 2) {
return withoutAdjustments()
@@ -140,13 +143,20 @@ data class ContourEdge(
return replacedWith(ShapeContour.fromContours(newSegments, false, 1.0))
}
/**
* Replaces the current edge with the segments of an open shape contour.
*
* @param openContour the open shape contour whose segments replace the current edge. The provided
* contour must not be closed.
* @return a new ContourEdge instance with the updated segments from the `openContour`.
*/
fun replacedWith(openContour: ShapeContour): ContourEdge {
if (contour.empty) {
return withoutAdjustments()
}
require(!openContour.closed) { "openContour should be open" }
val segment = contour.segments[segmentIndex]
var newSegments = contour.segments.toMutableList()
val newSegments = contour.segments.toMutableList()
var insertIndex = segmentIndex
val adjustments = newSegments.adjust {
@@ -169,11 +179,12 @@ data class ContourEdge(
/**
* subs the edge from [t0] to [t1], preserves topology unless t0 = t1
* @param t0 the start edge t-value, between 0 and 1
* @param t1 the end edge t-value, between 0 and 1
* Returns part of the edge between [t0] to [t1].
* Preserves topology unless t0 = t1.
* @param t0 the edge's start t-value, between 0 and 1
* @param t1 the edge's end t-value, between 0 and 1
*/
fun subbed(t0: Double, t1: Double, updateTangents: Boolean = true): ContourEdge {
fun subbed(t0: Double, t1: Double): ContourEdge {
if (contour.empty) {
return withoutAdjustments()
}
@@ -195,23 +206,23 @@ data class ContourEdge(
newSegments[segmentIndex] = sub
return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex)
} else {
return replacedWith(t0, updateTangents)
return replacedWith(t0)
}
}
/**
* split the edge at [t]
* @param t an edge t value between 0 and 1, will not split when t == 0 or t == 1
* Split the edge at [t]
* @param t An edge t value between 0 and 1. No splitting happens when t == 0 or t == 1.
*/
fun splitAt(t: Double): ContourEdge {
if (contour.empty) {
return withoutAdjustments()
}
val newContour = contour.insertPointAt(segmentIndex, t)
if (newContour.segments.size == contour.segments.size + 1) {
return ContourEdge(newContour, segmentIndex, listOf(SegmentOperation.Insert(segmentIndex + 1, 1)))
return if (newContour.segments.size == contour.segments.size + 1) {
ContourEdge(newContour, segmentIndex, listOf(SegmentOperation.Insert(segmentIndex + 1, 1)))
} else {
return this.copy(adjustments = emptyList())
this.copy(adjustments = emptyList())
}
}
@@ -282,25 +293,58 @@ data class ContourEdge(
return ContourEdge(ShapeContour.fromSegments(newSegments, contour.closed), segmentIndex)
}
/**
* Moves the starting point of the contour edge by the given translation vector.
*
* @param translation the translation vector to apply to the starting point of the contour edge.
* @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true.
* @return a new instance of the contour edge with the starting point moved by the given translation.
*/
fun startMovedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge =
transformedBy(buildTransform {
translate(translation)
}, updateTangents = updateTangents, mask = maskOf(ControlMask.START))
/**
* Moves the first control point of a contour edge by a specified translation vector.
*
* @param translation the translation vector to apply to the first control point of the contour edge.
* @return a new instance of the contour edge with the first control point moved by the given translation.
*/
fun control0MovedBy(translation: Vector2): ContourEdge = transformedBy(buildTransform {
translate(translation)
}, updateTangents = false, mask = maskOf(ControlMask.CONTROL0), promoteToCubic = true)
/**
* Moves the second control point (Control1) of a contour edge by a specified translation vector.
*
* @param translation the translation vector to apply to the second control point of the contour edge.
* @return a new instance of the contour edge with the second control point moved by the given translation.
*/
fun control1MovedBy(translation: Vector2): ContourEdge = transformedBy(buildTransform {
translate(translation)
}, updateTangents = false, mask = maskOf(ControlMask.CONTROL1), promoteToCubic = true)
/**
* Moves the end point of the contour edge by the specified translation vector.
*
* @param translation the translation vector to apply to the end point of the contour edge.
* @param updateTangents whether the tangents of adjacent segments should be updated after the transformation. Defaults to true.
* @return a new instance of the contour edge with the end point moved by the given translation.
*/
fun endMovedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge {
return transformedBy(buildTransform {
translate(translation)
}, updateTangents = updateTangents, mask = maskOf(ControlMask.END))
}
/**
* Creates a new contour edge by applying a translation to the current edge.
*
* @param translation the translation vector to apply to the contour edge.
* @param updateTangents whether the tangents of adjacent segments should be updated after the transformation.
* @return a new instance of the contour edge, transformed by the given translation.
*/
fun movedBy(translation: Vector2, updateTangents: Boolean = true): ContourEdge {
return transformedBy(buildTransform {
translate(translation)

View File

@@ -9,7 +9,7 @@ import kotlin.math.cos
/**
* Demonstrates an `adjustContour` animated effect where edge 0 of a contour
* is replaced by a point sampled on that edge. The specific edge point oscillates between
* 0.0 (at the start) and 1.0 (at the end) using a cosine and the `seconds` variable.
* 0.0 (at the start of the segment) and 1.0 (at the end) using a cosine and the `seconds` variable.
*
* The base contour used for the effect alternates every second
* between a rectangular and a circular contour.

View File

@@ -6,6 +6,19 @@ import org.openrndr.extra.shapes.adjust.adjustContour
import org.openrndr.shape.Circle
import kotlin.math.cos
/**
* Demonstrates animated modifications to a circular contour using `adjustContour`.
*
* The application creates a circular contour and dynamically alters its edges
* based on the current time in seconds. Each edge of the contour is selected
* and transformed through a series of operations:
*
* - The currently active edge (based on time modulo 4) is replaced with a point at 0.5.
* - All other edges are reshaped by reducing their length dynamically, with the reduction
* calculated using a cosine function involving the current time in seconds.
*
* The resulting contour is then drawn with a red stroke color.
*/
fun main() = application {
configure {
width = 800

View File

@@ -6,6 +6,22 @@ import org.openrndr.extra.shapes.adjust.adjustContour
import org.openrndr.shape.Circle
import kotlin.math.cos
/**
* Demonstrates the use of `adjustContour`
* to create an animated effect where edges are split, vertices are selected,
* and transformations such as scaling are applied.
*
* The program creates a circular contour which is modified on each animation frame.
*
* - Edges of the circular contour are split dynamically based on a time-based cosine function.
* - Newly created vertices are selected and scaled around the center of the contour
* using time-dependent transformations.
*
* The selection of vertices happens automatically thanks to
* `parameters.clearSelectedVertices` and `parameters.selectInsertedVertices`
*
* The modified animated contour is finally drawn.
*/
fun main() = application {
configure {
width = 800
@@ -27,7 +43,7 @@ fun main() = application {
for (e in edges) {
e.splitAt(splitT)
}
// as a resut of the clearSelectedVertices and selectInsertedVertices settings
// as a result of the clearSelectedVertices and selectInsertedVertices settings,
// the vertex selection is set to the newly inserted vertices
for ((index, v) in vertices.withIndex()) {
v.scale(cos(seconds + i + index) * 0.5 * (1.0 / (1.0 + i)) + 1.0, drawer.bounds.center)

View File

@@ -7,6 +7,15 @@ import org.openrndr.math.Vector2
import org.openrndr.shape.contour
import kotlin.math.cos
/**
* Demonstrates how to create and manipulate a contour dynamically using the `adjustContour` function.
*
* The program initializes a simple linear contour and applies transformations to it on each animation frame:
* - The only edge of the contour is split into many equal parts.
* - A value between 0 and 1 is calculated based on the cosine of the current time in seconds.
* - That value is used to calculate an anchor point and to select all vertices to its right
* - The selected vertices are rotated around an anchor, as if rolling a straight line into a spiral.
*/
fun main() = application {
configure {
width = 800
@@ -22,7 +31,7 @@ fun main() = application {
contour = adjustContour(contour) {
selectEdge(0)
edge.splitIn(128)
val tr = cos(seconds) * 0.5 + 0.5
val tr = cos(seconds + 2.0) * 0.5 + 0.5
selectVertices { i, v -> v.t >= tr }
val anchor = contour.position(tr)

View File

@@ -6,6 +6,16 @@ import org.openrndr.extra.shapes.adjust.adjustContour
import org.openrndr.math.Vector2
import org.openrndr.shape.contour
/**
* Demonstrates how to adjust and manipulate the vertices and edges of a contour.
*
* This method shows two approaches for transforming contours:
*
* 1. Adjusting vertices directly by selecting specific vertices in a contour and modifying their control points.
* 2. Adjusting edges of a contour by transforming their control points.
*
* For each approach, a red line is drawn representing the transformed contour.
*/
fun main() = application {
configure {
width = 800
@@ -13,6 +23,7 @@ fun main() = application {
}
program {
extend {
// Adjust a contour by transforming its vertices
var contour = contour {
moveTo(drawer.bounds.position(0.5, 0.1) - Vector2(300.0, 0.0))
lineTo(drawer.bounds.position(0.5, 0.1) + Vector2(300.0, 0.0))
@@ -29,7 +40,7 @@ fun main() = application {
drawer.stroke = ColorRGBa.RED
drawer.contour(contour)
// Achieve the same effect by transforming the control points of its edge
contour = contour {
moveTo(drawer.bounds.position(0.5, 0.2) - Vector2(300.0, 0.0))
lineTo(drawer.bounds.position(0.5, 0.2) + Vector2(300.0, 0.0))

View File

@@ -2,6 +2,8 @@ package adjust
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.loadFont
import org.openrndr.extra.color.presets.DARK_CYAN
import org.openrndr.extra.shapes.adjust.adjustContour
import org.openrndr.extra.shapes.adjust.extensions.averageTangents
import org.openrndr.extra.shapes.adjust.extensions.switchTangents
@@ -9,14 +11,36 @@ import org.openrndr.extra.shapes.tunni.tunniLine
import org.openrndr.extra.shapes.tunni.tunniPoint
import kotlin.math.sqrt
/**
* Demonstrates how to manipulate a contour by adjusting and transforming its vertices
* and edges, and subsequently visualizing the result using different drawing styles.
*
* The program creates a rectangular contour derived by shrinking the bounds of the drawing area.
* It then applies multiple transformations to selected vertices. These transformations include:
*
* - Averaging tangents for selected vertices
* - Scaling and rotating vertex positions based on the horizontal mouse position
* - Switching tangents for specific vertices
*
* The resulting contour is drawn in black. Additionally:
*
* - Control line segments are visualized in red, connecting segment endpoints to control points.
* - Vertices are numbered and highlighted with black-filled circles.
* - Tunni lines, which represent optimized control line placements, are visualized in cyan.
* - Tunni points, marking the Tunni line's control, are emphasized with yellow-filled circles.
*
*/
fun main() = application {
configure {
width = 800
height = 800
}
program {
val font = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
extend {
drawer.clear(ColorRGBa.WHITE)
drawer.fontMap = font
var contour = drawer.bounds.offsetEdges(-200.0).contour
drawer.fill = null
@@ -26,7 +50,7 @@ fun main() = application {
for (v in vertices) {
v.averageTangents()
v.scale(sqrt(2.0))
v.rotate(45.0)
v.rotate(mouse.position.x - 45.0)
}
selectVertices(2)
@@ -38,19 +62,25 @@ fun main() = application {
drawer.contour(contour)
drawer.stroke = ColorRGBa.RED
for (s in contour.segments) {
drawer.lineSegment(s.start, s.cubic.control[0])
drawer.lineSegment(s.end, s.cubic.control[1])
}
// Draw points and numbers
drawer.fill = ColorRGBa.BLACK
drawer.stroke = null
drawer.circles(contour.segments.map { it.start }, 5.0)
contour.segments.forEachIndexed { i, it ->
drawer.text(i.toString(), it.start + 10.0)
drawer.circle(it.start, 5.0)
}
drawer.stroke = ColorRGBa.GRAY
drawer.fill = ColorRGBa.YELLOW
drawer.stroke = ColorRGBa.CYAN
for (s in contour.segments) {
drawer.strokeWeight = 3.0
drawer.lineSegment(s.tunniLine)
drawer.fill = ColorRGBa.CYAN
drawer.strokeWeight = 1.0
drawer.circle(s.tunniPoint, 5.0)
}
}

View File

@@ -12,6 +12,24 @@ import org.openrndr.math.Vector2
import org.openrndr.shape.Segment2D
import kotlin.math.cos
/**
* Demonstrates how to adjust and animate contour segments and vertices.
*
* The method initially creates a contour by offsetting the edges of the window's bounds. A process is
* defined to sequence through various transformations on the contour, such as selecting edges, selecting
* vertices, rotating points, or modifying segment attributes based on mathematical transformations.
*
* The adjusted contour and its modified segments and vertices are iterated through a sequence
* and updated in real time. Rendering involves visualizing the contour, its control points, the
* Tunni lines, Tunni points, as well as the selected segments and points with distinct styles
* for better visualization.
*
* The complex animation sequence is implemented using coroutines. Two loops in the code alternate
* between rotating vertices and adjusting Tunni lines while the `extend` function takes care of
* rendering the composition in its current state.
*
* The core elements to study to in this demo are `adjustContourSequence` and `launch`.
*/
fun main() = application {
configure {
width = 800
@@ -39,7 +57,6 @@ fun main() = application {
selectVertices((i * 3).mod(4))
for (v in vertices) {
yield(status)
}
@@ -47,7 +64,10 @@ fun main() = application {
selectEdges(i.mod(4))
for (j in 0 until 30) {
for (e in edges) {
e.withTunniLine(e.tunniLine.position(0.5) + e.tunniLine.normal * cos(i.toDouble() + e.segmentIndex()) * 50.0 / 30.0)
e.withTunniLine(
e.tunniLine.position(0.5) +
e.tunniLine.normal * cos(i.toDouble() + e.segmentIndex()) * 50.0 / 30.0
)
yield(status)
}
}