From 923e64f37ad2388d6a89d18da7c75f73913b51e1 Mon Sep 17 00:00:00 2001 From: Abe Pazos Date: Sun, 5 Oct 2025 15:29:32 +0200 Subject: [PATCH] [orx-math] Add demo and readme texts. --- orx-math/README.md | 4 +- .../kotlin/linearrange/DemoLinearRange02.kt | 23 +++++++++- .../kotlin/linearrange/DemoLinearRange03.kt | 10 ++++ .../kotlin/matrix/DemoLeastSquares01.kt | 26 +++++------ .../kotlin/matrix/DemoLeastSquares02.kt | 26 +++++++---- .../jvmDemo/kotlin/rbf/RbfInterpolation01.kt | 38 +++++++++++---- .../jvmDemo/kotlin/rbf/RbfInterpolation02.kt | 46 ++++++++++++------- .../simplexrange/DemoSimplexRange3D01.kt | 13 ++++++ 8 files changed, 137 insertions(+), 49 deletions(-) diff --git a/orx-math/README.md b/orx-math/README.md index 2f17d07f..b845da7b 100644 --- a/orx-math/README.md +++ b/orx-math/README.md @@ -1,6 +1,8 @@ # orx-math -Mathematical utilities +Mathematical utilities, including complex numbers, +linear ranges, simplex ranges, matrices and radial basis functions (RBF). + ## Demos diff --git a/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange02.kt b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange02.kt index d8c4f850..ff15187a 100644 --- a/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange02.kt +++ b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange02.kt @@ -9,6 +9,27 @@ import org.openrndr.shape.Rectangle import kotlin.math.cos import kotlin.math.sin +/** + * Demonstrate how to create a 1D linear range between two instances of a `LinearType`, in this case, + * a horizontal `Rectangle` and a vertical one. + * + * Notice how the `..` operator is used to construct the `LinearRange1D`. + * + * The resulting `LinearRange1D` provides a `value()` method that takes a normalized + * input and returns an interpolated value between the two input elements. + * + * This example draws a grid of rectangles interpolated between the horizontal and the vertical + * triangles. The x and y coordinates and the `seconds` variable are used to specify the + * interpolation value for each grid cell. + * + * One can use the `LinearRange` class to construct + * - a `LinearRange2D` out of two `LinearRange1D` + * - a `LinearRange3D` out of two `LinearRange2D` + * - a `LinearRange4D` out of two `LinearRange3D` + * + * (not demonstrated here) + * + */ fun main() { application { configure { @@ -24,7 +45,7 @@ fun main() { for (y in 0 until height step 72) { for (x in 0 until width step 72) { val u = cos(seconds + x * 0.007) * 0.5 + 0.5 - val s = sin(seconds*1.03 + y * 0.0075) * 0.5 + 0.5 + val s = sin(seconds * 1.03 + y * 0.0075) * 0.5 + 0.5 drawer.isolated { drawer.translate(x.toDouble(), y.toDouble()) drawer.rectangle(range.value(u * s)) diff --git a/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange03.kt b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange03.kt index caef0497..fd3090a1 100644 --- a/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange03.kt +++ b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange03.kt @@ -9,6 +9,16 @@ import org.openrndr.shape.Rectangle import kotlin.math.cos import kotlin.math.sin +/** + * Demonstrates how to create a `LinearRange2D` out of two `LinearRange1D` instances. + * The first range interpolates a horizontal rectangle into a vertical one. + * The second range interpolates two smaller squares of equal size, one placed + * higher along the y-axis and another one lower. + * + * A grid of such rectangles is displayed, animating the `u` and `v` parameters based on + * `seconds`, `x` and `y` indices. The second range results in a vertical wave effect. + * + */ fun main() { application { configure { diff --git a/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares01.kt b/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares01.kt index f0ed0825..1a469c41 100644 --- a/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares01.kt +++ b/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares01.kt @@ -11,12 +11,12 @@ import kotlin.math.cos import kotlin.random.Random /** - * Demonstrate least squares method to find a regression line through noisy points - * Line drawn in red is the estimated line, in green is the ground-truth line + * Demonstrate least squares method to find a regression line through noisy points. + * The line drawn in red is the estimated line. The green one is the ground-truth. * - * Ax = b => x = A⁻¹b - * because A is likely inconsistent, we look for an approximate x based on AᵀA, which is consistent. - * x̂ = (AᵀA)⁻¹ Aᵀb + * `Ax = b => x = A⁻¹b` + * because `A` is likely inconsistent, we look for an approximate `x` based on `AᵀA`, which is consistent. + * `x̂ = (AᵀA)⁻¹ Aᵀb` */ fun main() { application { @@ -26,23 +26,23 @@ fun main() { } program { extend { - val ls = drawer.bounds.horizontal(0.5).rotateBy(cos(seconds)*45.0) + val groundTruth = drawer.bounds.horizontal(0.5).rotateBy(cos(seconds) * 45.0) - val r = Random((seconds*10).toInt()) + val r = Random((seconds * 10).toInt()) val pointCount = 100 val A = Matrix(pointCount, 2) val b = Matrix(pointCount, 1) for (i in 0 until pointCount) { - val p = ls.position(Double.uniform(0.0, 1.0, r)) - val pr = p + Vector2.uniformRing(0.0, 130.0, r) + val point = groundTruth.position(Double.uniform(0.0, 1.0, r)) + val pointRandomized = point + Vector2.uniformRing(0.0, 130.0, r) A[i, 0] = 1.0 - A[i, 1] = pr.x - b[i, 0] = pr.y + A[i, 1] = pointRandomized.x + b[i, 0] = pointRandomized.y - drawer.circle(pr, 5.0) + drawer.circle(pointRandomized, 5.0) } val At = A.transposed() val AtA = At * A @@ -58,7 +58,7 @@ fun main() { drawer.lineSegment(p0, p1) drawer.stroke = ColorRGBa.GREEN - drawer.lineSegment(ls) + drawer.lineSegment(groundTruth) } } } diff --git a/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares02.kt b/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares02.kt index 97115697..3ae8cb4a 100644 --- a/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares02.kt +++ b/orx-math/src/jvmDemo/kotlin/matrix/DemoLeastSquares02.kt @@ -13,7 +13,17 @@ import kotlin.math.pow import kotlin.random.Random /** - * Demonstrate least squares method to fit a cubic bezier to noisy points + * Demonstrate how to use the `least squares` method to fit a cubic bezier to noisy points. + * + * On every animation frame, 10 concentric circles are created centered on the window and converted to contours. + * In OPENRNDR, circular contours are made ouf of 4 cubic-Bezier curves. Each of those curves is considered + * one by one as the ground truth, then 5 points are sampled near those curves. + * Finally, two matrices are constructed using those points and math operations are applied to + * revert the randomization attempting to reconstruct the original curves. + * + * The result is drawn on every animation frame, revealing concentric circles that are more or less similar + * to the ground truth depending on the random values used. + * */ fun main() { application { @@ -34,8 +44,8 @@ fun main() { } extend { for (z in 0 until 10) { - val c = Circle(drawer.bounds.center, 300.0- z*30.0).contour - for (ls in c.segments) { + val c = Circle(drawer.bounds.center, 300.0 - z * 30.0).contour + for (groundTruth in c.segments) { val pointCount = 5 val A = Matrix(pointCount, 4) @@ -46,15 +56,15 @@ fun main() { pointCount - 1 -> 1.0 else -> Double.uniform(0.0, 1.0, r) } - val p = ls.position(t) - val pr = p + Vector2.uniformRing(0.0, 0.5, r) + val point = groundTruth.position(t) + val pointRandomized = point + Vector2.uniformRing(0.0, 0.5, r) A[i, 0] = bernstein(3, 0, t) A[i, 1] = bernstein(3, 1, t) A[i, 2] = bernstein(3, 2, t) A[i, 3] = bernstein(3, 3, t) - b[i, 0] = pr.x - b[i, 1] = pr.y + b[i, 0] = pointRandomized.x + b[i, 1] = pointRandomized.y } val At = A.transposed() val AtA = At * A @@ -64,11 +74,9 @@ fun main() { val x = AtAI * Atb val segment = Segment2D( - //ls.start, Vector2(x[0, 0], x[0, 1]), Vector2(x[1, 0], x[1, 1]), Vector2(x[2, 0], x[2, 1]), - //ls.end Vector2(x[3, 0], x[3, 1]) ) diff --git a/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt index 6e13d7ae..b4033c9d 100644 --- a/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt +++ b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation01.kt @@ -22,6 +22,29 @@ import kotlin.ranges.until import kotlin.text.trimIndent import kotlin.text.trimMargin +/** + * Demonstrates using a two-dimensional Radial Basis Function (RBF) interpolator + * with the user provided 2D input points, their corresponding values (colors in this demo), + * a smoothing factor, and a radial basis function kernel. + * + * The program chooses 14 random points in the window area leaving a 100 pixels + * margin around the borders and assigns a randomized color to each point. + * + * Next it creates the interpolator using those points and colors, a smoothing factor + * and the RBF function used for interpolation. This function takes a squared distance + * as input and returns a scalar value representing the influence of points at that distance. + * + * A ShadeStyle implementing the RBF interpolation is created next, used to render + * the background gradient interpolating all points and their colors. + * + * After rendering the background, the original points and their colors are + * drawn as circles for reference. + * + * Finally, the current mouse position is used for sampling a color + * from the interpolator and displayed for comparison. Notice that even if + * the fill color is flat, it may look like a gradient due to the changing + * colors in the surrounding pixels. + */ fun main() { application { configure { @@ -32,7 +55,7 @@ fun main() { val r = Random(0) val points = drawer.bounds.offsetEdges(-100.0).uniform(14, r) - val colors = (0 until points.size).map { + val colors = points.map { ColorRGBa.PINK .shiftHue(Double.uniform(-180.0, 180.0, r)) .shadeLuminosity(Double.uniform(0.4, 1.0, r)) @@ -50,12 +73,13 @@ fun main() { /** * Shader style that implements RBF interpolation in the fragment shader. - * Uses Gaussian RBF function to interpolate colors between given points. + * Uses a Gaussian RBF function to interpolate colors between given points. * Includes custom distance calculation and color interpolation functions. */ val ss = shadeStyle { - fragmentPreamble = """${fhash12Phrase} - |${rbfGaussianPhrase} + fragmentPreamble = """ + |$fhash12Phrase + |$rbfGaussianPhrase |float squaredDistance(vec2 p, vec2 q) { | vec2 d = p - q; | return dot(d, d); @@ -64,9 +88,7 @@ fun main() { | vec3 c = p_mean; | for (int i = 0; i < p_weights_SIZE; ++i) { | float r = rbfGaussian(squaredDistance(p_points[i], p), $scale); - | c.r += p_weights[i].r * r; - | c.g += p_weights[i].g * r; - | c.b += p_weights[i].b * r; + | c += p_weights[i].rgb * r; | } | return c; |} @@ -74,8 +96,8 @@ fun main() { fragmentTransform = """ x_fill.rgb = rbfInterpolate(c_boundsPosition.xy * vec2(720.0, 720.0)); - """.trimIndent() + val weights = (0 until points.size).map { Vector3(interpolator.weights[it][0], interpolator.weights[it][1], interpolator.weights[it][2]) }.toTypedArray() diff --git a/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt index 7eddbd9a..4673c4b7 100644 --- a/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt +++ b/orx-math/src/jvmDemo/kotlin/rbf/RbfInterpolation02.kt @@ -9,23 +9,36 @@ import org.openrndr.extra.color.spaces.OKLab import org.openrndr.extra.color.tools.shadeLuminosity import org.openrndr.extra.color.tools.shiftHue import org.openrndr.extra.math.rbf.Rbf2DInterpolator -import org.openrndr.extra.math.rbf.rbfGaussian import org.openrndr.extra.math.rbf.rbfInverseMultiQuadratic -import org.openrndr.extra.math.rbf.rbfInverseQuadratic import org.openrndr.extra.noise.uniform import org.openrndr.extra.shaderphrases.noise.fhash12Phrase -import org.openrndr.extra.shaderphrases.rbf.rbfGaussianPhrase import org.openrndr.extra.shaderphrases.rbf.rbfInverseMultiQuadraticPhrase -import org.openrndr.extra.shaderphrases.rbf.rbfInverseQuadraticPhrase import org.openrndr.math.Vector3 -import kotlin.collections.indices -import kotlin.collections.map -import kotlin.collections.toTypedArray import kotlin.random.Random -import kotlin.ranges.until -import kotlin.text.trimIndent -import kotlin.text.trimMargin +/** + * Demonstrates using a two-dimensional Radial Basis Function (RBF) interpolator + * with the user provided 2D input points, their corresponding values (colors in this demo), + * a smoothing factor, and a radial basis function kernel. + * + * The program chooses 20 random points in the window area leaving a 100 pixels + * margin around the borders and assigns a randomized color to each point. + * + * Next it creates the interpolator using those points and colors, a smoothing factor + * and the RBF function used for interpolation. This function takes a squared distance + * as input and returns a scalar value representing the influence of points at that distance. + * + * A ShadeStyle implementing the same RBF interpolation is created next, used to render + * the background gradient interpolating all points and their colors. + * + * After rendering the background, the original points and their colors are + * drawn as circles for reference. + * + * Finally, the current mouse position is used for sampling a color + * from the interpolator and displayed for comparison. Notice that even if + * the fill color is flat, it may look like a gradient due to the changing + * colors in the surrounding pixels. + */ fun main() { application { configure { @@ -36,7 +49,7 @@ fun main() { val r = Random(0) val points = drawer.bounds.offsetEdges(-100.0).uniform(20, r) - val colors = (0 until points.size).map { + val colors = points.map { ColorRGBa.PINK .shiftHue(Double.uniform(-180.0, 180.0, r)) .shadeLuminosity(Double.uniform(0.4, 1.0, r)) @@ -54,12 +67,13 @@ fun main() { /** * Shader style that implements RBF interpolation in the fragment shader. - * Uses Gaussian RBF function to interpolate colors between given points. + * Uses an Inverse MultiQuadratic RBF function to interpolate colors between given points. * Includes custom distance calculation and color interpolation functions. */ val ss = shadeStyle { - fragmentPreamble = """${fhash12Phrase} - |${rbfInverseMultiQuadraticPhrase} + fragmentPreamble = """ + |$fhash12Phrase + |$rbfInverseMultiQuadraticPhrase |float squaredDistance(vec2 p, vec2 q) { | vec2 d = p - q; | return dot(d, d); @@ -68,9 +82,7 @@ fun main() { | vec3 c = p_mean; | for (int i = 0; i < p_weights_SIZE; ++i) { | float r = rbfInverseMultiQuadratic(squaredDistance(p_points[i], p), $scale); - | c.r += p_weights[i].r * r; - | c.g += p_weights[i].g * r; - | c.b += p_weights[i].b * r; + | c += p_weights[i].rgb * r; | } | return c; |} diff --git a/orx-math/src/jvmDemo/kotlin/simplexrange/DemoSimplexRange3D01.kt b/orx-math/src/jvmDemo/kotlin/simplexrange/DemoSimplexRange3D01.kt index 65c10aa3..c335ebb1 100644 --- a/orx-math/src/jvmDemo/kotlin/simplexrange/DemoSimplexRange3D01.kt +++ b/orx-math/src/jvmDemo/kotlin/simplexrange/DemoSimplexRange3D01.kt @@ -9,6 +9,19 @@ import org.openrndr.extra.meshgenerators.boxMesh import org.openrndr.extra.math.simplexrange.SimplexRange3D import org.openrndr.math.Vector3 +/** + * Demonstrates the use of the `SimplexRange3D` class. Its constructor takes 4 instances of a `LinearType` + * (something that can be interpolated linearly, like `ColorRGBa`). The `SimplexRange3D` instance provides + * a `value()` method that returns a `LinearType` interpolated across the 4 constructor arguments using + * a normalized 3D coordinate. + * + * This demo program creates a 3D grid of 20x20x20 unit 3D cubes. Their color is set by interpolating + * their XYZ index across the 4 input colors. + * + * 2D, 4D and ND varieties are also provided by `SimplexRange`. + * + * *Simplex Range* is not to be confused with *Simplex Noise*. + */ fun main() { application { configure {