diff --git a/orx-math/README.md b/orx-math/README.md new file mode 100644 index 00000000..310ccf30 --- /dev/null +++ b/orx-math/README.md @@ -0,0 +1,3 @@ +# orx-math + +Mathematical utilities diff --git a/orx-math/build.gradle.kts b/orx-math/build.gradle.kts new file mode 100644 index 00000000..8477db5d --- /dev/null +++ b/orx-math/build.gradle.kts @@ -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")) + } + } + } +} \ No newline at end of file diff --git a/orx-math/src/commonMain/kotlin/Math.kt b/orx-math/src/commonMain/kotlin/Math.kt new file mode 100644 index 00000000..1270a26b --- /dev/null +++ b/orx-math/src/commonMain/kotlin/Math.kt @@ -0,0 +1,3 @@ +package org.openrndr.extra.math + +private fun placeholder() {} diff --git a/orx-math/src/commonMain/kotlin/linearrange/LinearRange.kt b/orx-math/src/commonMain/kotlin/linearrange/LinearRange.kt new file mode 100644 index 00000000..75f80c94 --- /dev/null +++ b/orx-math/src/commonMain/kotlin/linearrange/LinearRange.kt @@ -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>(val start: T, val end: T) : + LinearType>, + Parametric1D { + /** + * 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 = sequence { + for (i in 0 until count) { + val t = i / (count - 1.0) + yield(value(t)) + } + } + + override fun plus(right: LinearRange1D): LinearRange1D = + copy(start = start + right.start, end = end + right.end) + + override fun minus(right: LinearRange1D): LinearRange1D = + copy(start = start - right.start, end = end - right.end) + + override fun times(scale: Double): LinearRange1D = + copy(start = start * scale, end = end * scale) + + override fun div(scale: Double): LinearRange1D = + 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 > LinearType.rangeTo(end: T): LinearRange1D = 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>(val start: LinearRange1D, val end: LinearRange1D) : + LinearType>, + Parametric2D { + override fun value(u: Double, v: Double) = start.value(u) * (1.0 - v) + end.value(u) * v + + override fun plus(right: LinearRange2D): LinearRange2D = + copy(start = start + right.start, end = end + right.end) + + override fun minus(right: LinearRange2D): LinearRange2D = + copy(start = start - right.start, end = end - right.end) + + override fun times(scale: Double): LinearRange2D = + copy(start = start * scale, end = end * scale) + + override fun div(scale: Double): LinearRange2D = + 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 > LinearRange1D.rangeTo(end: LinearRange1D): LinearRange2D { + 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>(val start: LinearRange2D, val end: LinearRange2D) : + LinearType>, Parametric3D { + 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): LinearRange3D = + copy(start = start + right.start, end = end + right.end) + + override fun minus(right: LinearRange3D): LinearRange3D = + copy(start = start - right.start, end = end - right.end) + + override fun times(scale: Double): LinearRange3D = + copy(start = start * scale, end = end * scale) + + override fun div(scale: Double): LinearRange3D = + 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 > LinearRange2D.rangeTo(end: LinearRange2D): LinearRange3D = + 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>(val start: LinearRange3D, val end: LinearRange3D) : + LinearType>, + Parametric4D { + 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): LinearRange4D = + copy(start = start + right.start, end = end + right.end) + + override fun minus(right: LinearRange4D): LinearRange4D = + copy(start = start - right.start, end = end - right.end) + + override fun times(scale: Double): LinearRange4D = + copy(start = start * scale, end = end * scale) + + override fun div(scale: Double): LinearRange4D = + 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 > LinearRange3D.rangeTo(end: LinearRange3D): LinearRange4D { + return LinearRange4D(this, end) +} \ No newline at end of file diff --git a/orx-math/src/commonMain/kotlin/simplexrange/SimplexRange.kt b/orx-math/src/commonMain/kotlin/simplexrange/SimplexRange.kt new file mode 100644 index 00000000..b19d5257 --- /dev/null +++ b/orx-math/src/commonMain/kotlin/simplexrange/SimplexRange.kt @@ -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`, 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>(val x0: T, val x1: T, val x2: T) : Parametric2D { + 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>(val x0: T, val x1: T, val x2: T, val x3: T) : Parametric3D { + 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`, 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>(val x0: T, val x1: T, val x2: T, val x3: T, val x4: T) : Parametric4D { + 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>(val x: List) { + /** + * 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 + } +} \ No newline at end of file diff --git a/orx-math/src/commonTest/kotlin/linearrange/TestLinearRange.kt b/orx-math/src/commonTest/kotlin/linearrange/TestLinearRange.kt new file mode 100644 index 00000000..59b382ce --- /dev/null +++ b/orx-math/src/commonTest/kotlin/linearrange/TestLinearRange.kt @@ -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) + } +} \ No newline at end of file diff --git a/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange02.kt b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange02.kt new file mode 100644 index 00000000..d8c4f850 --- /dev/null +++ b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange02.kt @@ -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)) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange03.kt b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange03.kt new file mode 100644 index 00000000..caef0497 --- /dev/null +++ b/orx-math/src/jvmDemo/kotlin/linearrange/DemoLinearRange03.kt @@ -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)) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/orx-math/src/jvmDemo/kotlin/simplexrange/DemoSimplexRange3D01.kt b/orx-math/src/jvmDemo/kotlin/simplexrange/DemoSimplexRange3D01.kt new file mode 100644 index 00000000..65c10aa3 --- /dev/null +++ b/orx-math/src/jvmDemo/kotlin/simplexrange/DemoSimplexRange3D01.kt @@ -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) + } + } + } + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 08515b0b..f5c444fb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,6 +44,7 @@ include( "orx-image-fit", "orx-kdtree", "orx-jvm:orx-keyframer", + "orx-math", "orx-mesh", "orx-mesh-generators", "orx-mesh-noise",