[orx-math] Add orx-math module

This commit is contained in:
Edwin Jakobs
2025-02-02 11:24:39 +01:00
parent 13bc4bc472
commit c88c4454e1
10 changed files with 555 additions and 0 deletions

3
orx-math/README.md Normal file
View File

@@ -0,0 +1,3 @@
# orx-math
Mathematical utilities

46
orx-math/build.gradle.kts Normal file
View File

@@ -0,0 +1,46 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
org.openrndr.extra.convention.`kotlin-multiplatform`
// kotlinx-serialization ends up on the classpath through openrndr-math and Gradle doesn't know which
// version was used. If openrndr were an included build, we probably wouldn't need to do this.
// https://github.com/gradle/gradle/issues/20084
id(libs.plugins.kotlin.serialization.get().pluginId)
alias(libs.plugins.kotest.multiplatform)
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.kotlin.serialization.core)
implementation(libs.openrndr.math)
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.serialization.json)
implementation(libs.kotest.assertions)
implementation(libs.kotest.framework.engine)
}
}
val jvmTest by getting {
dependencies {
implementation(libs.kotlin.serialization.json)
implementation(libs.kotest.assertions)
implementation(libs.kotest.framework.engine)
}
}
val jvmDemo by getting {
dependencies {
implementation(project(":orx-camera"))
implementation(project(":orx-mesh-generators"))
implementation(project(":orx-color"))
implementation(project(":orx-jvm:orx-gui"))
implementation(project(":orx-shade-styles"))
}
}
}
}

View File

@@ -0,0 +1,3 @@
package org.openrndr.extra.math
private fun placeholder() {}

View File

@@ -0,0 +1,175 @@
package org.openrndr.extra.math.linearrange
import org.openrndr.math.*
import kotlin.jvm.JvmRecord
/**
* Represents a linear range between two values, defined by a start and an end point,
* where the type of the values implements the `LinearType` interface.
* This class allows interpolation and evenly spaced steps within the range.
*
* @param T The type of the range's start and end values, constrained by the `LinearType` interface.
* @property start The starting value of the range.
* @property end The ending value of the range.
*/
@JvmRecord
data class LinearRange1D<T : LinearType<T>>(val start: T, val end: T) :
LinearType<LinearRange1D<T>>,
Parametric1D<T> {
/**
* Computes a value interpolated linearly between the start and end points of the range based on the parameter `t`.
*
* @param t A parameter in the range [0.0, 1.0]. When `t` is 0.0, the result is the start value.
* When `t` is 1.0, the result is the end value. Intermediate values of `t` result in a linear interpolation.
* @return The interpolated value between the start and end points of the range.
*/
override fun value(t: Double) = start * (1.0 - t) + end * t
fun steps(count: Int): Sequence<T> = sequence {
for (i in 0 until count) {
val t = i / (count - 1.0)
yield(value(t))
}
}
override fun plus(right: LinearRange1D<T>): LinearRange1D<T> =
copy(start = start + right.start, end = end + right.end)
override fun minus(right: LinearRange1D<T>): LinearRange1D<T> =
copy(start = start - right.start, end = end - right.end)
override fun times(scale: Double): LinearRange1D<T> =
copy(start = start * scale, end = end * scale)
override fun div(scale: Double): LinearRange1D<T> =
copy(start = start / scale, end = end / scale)
}
/**
* Creates a range from the current linear type instance to the specified end value.
*
* @param end The end value of the range.
* @return A LinearRange that represents the range from the current instance to the specified end value.
*/
operator fun <T : LinearType<T>> LinearType<T>.rangeTo(end: T): LinearRange1D<T> = LinearRange1D(this as T, end)
/**
* Represents a two-dimensional linear range defined by two one-dimensional linear ranges,
* where the `start` and `end` ranges provide endpoints for interpolation.
*
* This class enables bilinear interpolation between the `start` and `end` ranges
* based on parameters `u` and `v`.
*
* @param T The type of the values being interpolated, constrained by the `LinearType` interface.
* @property start The starting one-dimensional linear range.
* @property end The ending one-dimensional linear range.
*/
@JvmRecord
data class LinearRange2D<T : LinearType<T>>(val start: LinearRange1D<T>, val end: LinearRange1D<T>) :
LinearType<LinearRange2D<T>>,
Parametric2D<T> {
override fun value(u: Double, v: Double) = start.value(u) * (1.0 - v) + end.value(u) * v
override fun plus(right: LinearRange2D<T>): LinearRange2D<T> =
copy(start = start + right.start, end = end + right.end)
override fun minus(right: LinearRange2D<T>): LinearRange2D<T> =
copy(start = start - right.start, end = end - right.end)
override fun times(scale: Double): LinearRange2D<T> =
copy(start = start * scale, end = end * scale)
override fun div(scale: Double): LinearRange2D<T> =
copy(start = start / scale, end = end / scale)
}
/**
* Creates a `LinearRange2D` instance using this `LinearRange1D` as the starting range
* and the specified `end` as the ending range.
*
* @param end The ending `LinearRange1D` to create a 2D range.
* @return A `LinearRange2D` instance representing the range from this starting range to the specified ending range.
*/
operator fun <T : LinearType<T>> LinearRange1D<T>.rangeTo(end: LinearRange1D<T>): LinearRange2D<T> {
return LinearRange2D(this, end)
}
/**
* Represents a three-dimensional linear range defined by two two-dimensional linear ranges,
* where the `start` and `end` ranges provide endpoints for trilinear interpolation.
*
* This class allows for interpolation across three dimensions using the parameters `u`, `v`, and `w`.
*
* @param T The type of the values being interpolated, constrained by the `LinearType` interface.
* @property start The starting two-dimensional linear range.
* @property end The ending two-dimensional linear range.
*/
@JvmRecord
data class LinearRange3D<T : LinearType<T>>(val start: LinearRange2D<T>, val end: LinearRange2D<T>) :
LinearType<LinearRange3D<T>>, Parametric3D<T> {
override fun value(u: Double, v: Double, w: Double) = start.value(u, v) * (1.0 - w) + end.value(u, v) * w
override fun plus(right: LinearRange3D<T>): LinearRange3D<T> =
copy(start = start + right.start, end = end + right.end)
override fun minus(right: LinearRange3D<T>): LinearRange3D<T> =
copy(start = start - right.start, end = end - right.end)
override fun times(scale: Double): LinearRange3D<T> =
copy(start = start * scale, end = end * scale)
override fun div(scale: Double): LinearRange3D<T> =
copy(start = start / scale, end = end / scale)
}
/**
* Creates a 3D linear range from the current 2D linear range to the specified 2D linear range.
*
* @param end The ending 2D linear range to define the 3D linear range.
* @return A new instance of LinearRange3D representing the range from the current 2D range to the specified end range.
*/
operator fun <T : LinearType<T>> LinearRange2D<T>.rangeTo(end: LinearRange2D<T>): LinearRange3D<T> =
LinearRange3D(this, end)
/**
* Represents a four-dimensional linear range defined by two three-dimensional linear ranges,
* providing endpoints for quadrilinear interpolation.
*
* This class supports interpolation across four dimensions using the parameters `u`, `v`, `w`, and `t`.
* The interpolation is computed as a combination of the `start` and `end` LinearRange3D objects:
* - The `start` LinearRange3D is weighted by `(1.0 - t)`
* - The `end` LinearRange3D is weighted by `t`
*
* @param T The type of the values being interpolated, constrained by the `LinearType` interface.
* @property start The starting three-dimensional linear range.
* @property end The ending three-dimensional linear range.
*/
@JvmRecord
data class LinearRange4D<T : LinearType<T>>(val start: LinearRange3D<T>, val end: LinearRange3D<T>) :
LinearType<LinearRange4D<T>>,
Parametric4D<T> {
override fun value(u: Double, v: Double, w: Double, t: Double) =
start.value(u, v, w) * (1.0 - t) + end.value(u, v, w) * t
override fun plus(right: LinearRange4D<T>): LinearRange4D<T> =
copy(start = start + right.start, end = end + right.end)
override fun minus(right: LinearRange4D<T>): LinearRange4D<T> =
copy(start = start - right.start, end = end - right.end)
override fun times(scale: Double): LinearRange4D<T> =
copy(start = start * scale, end = end * scale)
override fun div(scale: Double): LinearRange4D<T> =
copy(start = start / scale, end = end / scale)
}
/**
* Creates a LinearRange4D object representing the range between this LinearRange4D instance and the specified end LinearRange3D instance.
*
* @param end The ending LinearRange3D instance that defines the range.
* @return A LinearRange4D object representing the range from this instance to the specified end instance.
*/
operator fun <T : LinearType<T>> LinearRange3D<T>.rangeTo(end: LinearRange3D<T>): LinearRange4D<T> {
return LinearRange4D(this, end)
}

View File

@@ -0,0 +1,150 @@
package org.openrndr.extra.math.simplexrange
import org.openrndr.math.LinearType
import org.openrndr.math.Parametric2D
import org.openrndr.math.Parametric3D
import org.openrndr.math.Parametric4D
import kotlin.jvm.JvmRecord
import kotlin.math.cbrt
import kotlin.math.pow
import kotlin.math.sqrt
/**
* Transforms a given array of coordinates into an array of coefficients
* for use in simplex-based calculations.
*
* @param b An array of doubles representing coordinates.
* The size of this array determines the dimensionality of the input simplex.
* @return A new array of doubles representing the transformed coefficients,
* with one additional element compared to the input array.
*/
fun simplexUpscale(b: DoubleArray): DoubleArray {
val transformed = DoubleArray(b.size) {
b[it].pow(1.0 / (b.size - it))
}
var m = 1.0
val result = DoubleArray(b.size + 1) {
val neg = if (it < transformed.size) 1.0 - transformed[it] else 1.0
val v = m * neg
m *= if (it < transformed.size) transformed[it] else 1.0
v
}
return result
}
/**
* Represents a 2D simplex range interpolated in a parametric space.
* This class defines a triangular range in 2D space, parameterized by three control points `x0`, `x1`, and `x2`.
* It implements the `Parametric2D` interface, allowing evaluation of linear combinations
* of these control points based on two parameters `u` and `v`.
*
* @param T The type parameter constrained to types that implement `LinearType<T>`, enabling
* operations such as addition, multiplication, and scalar interpolation.
* @property x0 The first control point of the simplex.
* @property x1 The second control point of the simplex.
* @property x2 The third control point of the simplex.
*/
@JvmRecord
data class SimplexRange2D<T : LinearType<T>>(val x0: T, val x1: T, val x2: T) : Parametric2D<T> {
override fun value(u: Double, v: Double): T {
val r1 = sqrt(u)
val r2 = v
val a = 1 - r1
val b = r1 * (1 - r2)
val c = r1 * r2
return x0 * a + x1 * b + x2 * c
}
}
/**
* Represents a 3D parametric simplex range defined by four control points.
*
* @param T The type of the coordinate values in the 3D space, which must extend LinearType.
* @property x0 The first control point defining the simplex.
* @property x1 The second control point defining the simplex.
* @property x2 The third control point defining the simplex.
* @property x3 The fourth control point defining the simplex.
*/
@JvmRecord
data class SimplexRange3D<T : LinearType<T>>(val x0: T, val x1: T, val x2: T, val x3: T) : Parametric3D<T> {
override fun value(u: Double, v: Double, w: Double): T {
val r1 = cbrt(u)
val r2 = sqrt(v)
val r3 = w
val a = 1 - r1
val b = r1 * (1 - r2)
val c = r1 * r2 * (1 - r3)
val d = r1 * r2 * r3
return x0 * a + x1 * b + x2 * c + x3 * d
}
}
/**
* Represents a 4D parametric simplex range defined by five control points of type `T`.
*
* This class computes a value within the simplex range based on four parametric inputs (u, v, w, t).
* The control points x0, x1, x2, x3, and x4 determine the shape of the simplex, and the resulting value
* is calculated as a weighted combination of the control points using barycentric-like coordinates derived
* from the parametric inputs.
*
* The generic type `T` must extend `LinearType<T>`, as the calculation requires linear operations.
*
* @param T the type of each control point, constrained to types that implement `LinearType`.
* @property x0 the first control point of the simplex.
* @property x1 the second control point of the simplex.
* @property x2 the third control point of the simplex.
* @property x3 the fourth control point of the simplex.
* @property x4 the fifth control point of the simplex.
*/
@JvmRecord
data class SimplexRange4D<T : LinearType<T>>(val x0: T, val x1: T, val x2: T, val x3: T, val x4: T) : Parametric4D<T> {
override fun value(u: Double, v: Double, w: Double, t: Double): T {
val r1 = u.pow(1.0 / 4.0)
val r2 = cbrt(v)
val r3 = sqrt(w)
val r4 = t
val a = 1 - r1
val b = r1 * (1 - r2)
val c = r1 * r2 * (1 - r3)
val d = r1 * r2 * r3 * (1 - r4)
val e = r1 * r2 * r3 * r4
return x0 * a + x1 * b + x2 * c + x3 * d + x4 * e
}
}
/**
* Represents a value defined over an N-dimensional simplex range.
*
* This class is constructed using a list of elements of a generic type `T` that
* conforms to the `LinearType` interface. The `SimplexRangeND` allows evaluating
* a value within the simplex defined by these elements, which are scaled and
* combined based on a provided set of barycentric coordinates.
*
* @param T The type of elements in the simplex, which must implement the `LinearType` interface.
* @property x The list of elements representing the vertices of the N-dimensional simplex.
*/
class SimplexRangeND<T: LinearType<T>>(val x: List<T>) {
/**
* Computes a value determined by the coordinates given in the input array.
*
* The method uses the `simplexUpscale` function to transform the input array, resulting in a set of
* coefficients. These coefficients are then used to calculate a weighted combination of the elements
* in the simplex, represented by the `x` property of the class.
*
* @param u An array of doubles representing the coordinates for the simplex.
* The size of the array must be one less than the number of elements in the simplex.
* @return A value of type `T` computed as the weighted combination of the simplex elements.
*/
fun value(u : DoubleArray): T {
val b = simplexUpscale(u)
var r = x[0] * b [0]
for (i in 1 until x.size) {
r += x[i] * b[i]
}
return r
}
}

View File

@@ -0,0 +1,54 @@
package linearrangeimport
import org.openrndr.math.Vector2
import org.openrndr.math.Vector4
import org.openrndr.extra.math.linearrange.rangeTo
import kotlin.test.Test
import kotlin.test.assertTrue
class TestLinearRange {
val e = 1E-6
/**
* Verifies the interpolation behavior of a one-dimensional linear range.
*
* This test checks whether the interpolated value computed for a specific parameter (`t`)
* within a linear range of two `Vector2` instances falls within a specified range of error tolerance.
*/
@Test
fun testLinearRange1D() {
val v0 = Vector2.UNIT_X
val v1 = Vector2.UNIT_Y
val lr = v0..v1
val c = lr.value(0.5)
assertTrue(c.x in 0.5 - e..0.5 + e)
assertTrue(c.y in 0.5 - e..0.5 + e)
}
/**
* Verifies the interpolation behavior of a two-dimensional linear range.
*
* This test validates that the computed interpolated value within a 2D linear range of `Vector4` instances
* falls within an acceptable range of error tolerance. The interpolation is performed using a `LinearRange2D`
* created from two 1D linear ranges, which are themselves defined by start and end `Vector4` points.
* The test checks the accuracy of the interpolation for the midpoints along both dimensions.
*/
@Test
fun testLinearRange2D() {
val v00 = Vector4.UNIT_X
val v01 = Vector4.UNIT_Y
val lr0 = v00..v01
val v10 = Vector4.UNIT_Z
val v11 = Vector4.UNIT_W
val lr1 = v10..v11
val lr = lr0..lr1
val c = lr.value(0.5, 0.5)
assertTrue(c.x in 0.25 - e..0.25 + e)
assertTrue(c.y in 0.25 - e..0.25 + e)
assertTrue(c.z in 0.25 - e..0.25 + e)
assertTrue(c.w in 0.25 - e..0.25 + e)
}
}

View File

@@ -0,0 +1,37 @@
package linearrange
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.isolated
import org.openrndr.extra.math.linearrange.rangeTo
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import kotlin.math.cos
import kotlin.math.sin
fun main() {
application {
configure {
width = 720
height = 720
}
program {
val range = Rectangle.fromCenter(Vector2(36.0, 36.0), 72.0, 18.0)..
Rectangle.fromCenter(Vector2(36.0, 36.0), 18.0, 72.0)
extend {
drawer.fill = ColorRGBa.PINK.opacify(0.9)
drawer.stroke = null
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
drawer.isolated {
drawer.translate(x.toDouble(), y.toDouble())
drawer.rectangle(range.value(u * s))
}
}
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
package linearrange
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.isolated
import org.openrndr.extra.math.linearrange.rangeTo
import org.openrndr.math.Vector2
import org.openrndr.shape.Rectangle
import kotlin.math.cos
import kotlin.math.sin
fun main() {
application {
configure {
width = 720
height = 720
}
program {
val range0 = Rectangle.fromCenter(Vector2(36.0, 36.0), 72.0, 18.0)..
Rectangle.fromCenter(Vector2(36.0, 36.0), 18.0, 72.0)
val range1 = Rectangle.fromCenter(Vector2(36.0, 0.0), 9.0, 9.0)..
Rectangle.fromCenter(Vector2(36.0, 72.0), 9.0, 9.0)
val range = range0..range1
extend {
drawer.fill = ColorRGBa.PINK.opacify(0.9)
drawer.stroke = null
for (y in 0 until height step 72) {
for (x in 0 until width step 72) {
val u = cos(seconds* 2.0 + x * 0.01) * 0.5 + 0.5
val v = sin(seconds * 1.03 + y * 0.01) * 0.5 + 0.5
drawer.isolated {
drawer.translate(x.toDouble(), y.toDouble())
drawer.rectangle(range.value(u, v))
}
}
}
}
}
}
}

View File

@@ -0,0 +1,45 @@
package simplexrange
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.isolated
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.boxMesh
import org.openrndr.extra.math.simplexrange.SimplexRange3D
import org.openrndr.math.Vector3
fun main() {
application {
configure {
width = 720
height = 720
}
program {
val box = boxMesh()
extend(Orbital()) {
eye = Vector3(1.0, -1.0, 1.0).normalized * 140.0
fov = 15.0
}
extend {
val sr = SimplexRange3D(
ColorRGBa.PINK.toLinear(),
ColorRGBa.RED.toLinear(),
ColorRGBa.MAGENTA.toLinear(),
ColorRGBa.BLUE.toLinear()
)
for (z in 0 until 20)
for (y in 0 until 20)
for (x in 0 until 20) {
drawer.isolated {
drawer.translate(x - 10.0, y - 10.0, z - 10.0)
drawer.fill = sr.value(x / 20.0, y / 20.0, z / 20.0)
drawer.vertexBuffer(box, DrawPrimitive.TRIANGLES)
}
}
}
}
}
}

View File

@@ -44,6 +44,7 @@ include(
"orx-image-fit",
"orx-kdtree",
"orx-jvm:orx-keyframer",
"orx-math",
"orx-mesh",
"orx-mesh-generators",
"orx-mesh-noise",