From c850ed9c849b55976a788ec65db8c7f26e8730c5 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Thu, 2 Feb 2023 09:33:06 +0100 Subject: [PATCH] [orx-turtle] Add orx-turtle module --- build.gradle | 3 +- orx-turtle/README.md | 33 +++++++ orx-turtle/build.gradle.kts | 23 +++++ .../src/commonMain/kotlin/NinjaTurtle.kt | 51 ++++++++++ orx-turtle/src/commonMain/kotlin/Turtle.kt | 96 +++++++++++++++++++ orx-turtle/src/jvmDemo/kotlin/DemoTurtle01.kt | 40 ++++++++ orx-turtle/src/jvmDemo/kotlin/DemoTurtle02.kt | 28 ++++++ orx-turtle/src/jvmDemo/kotlin/DemoTurtle03.kt | 37 +++++++ settings.gradle.kts | 3 +- 9 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 orx-turtle/README.md create mode 100644 orx-turtle/build.gradle.kts create mode 100644 orx-turtle/src/commonMain/kotlin/NinjaTurtle.kt create mode 100644 orx-turtle/src/commonMain/kotlin/Turtle.kt create mode 100644 orx-turtle/src/jvmDemo/kotlin/DemoTurtle01.kt create mode 100644 orx-turtle/src/jvmDemo/kotlin/DemoTurtle02.kt create mode 100644 orx-turtle/src/jvmDemo/kotlin/DemoTurtle03.kt diff --git a/build.gradle b/build.gradle index 3973c126..b5584c90 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,8 @@ def multiplatformModules = [ "orx-hash-grid", "orx-depth-camera", "orx-triangulation", - "orx-view-box" + "orx-view-box", + "orx-turtle" ] def doNotPublish = ["openrndr-demos"] diff --git a/orx-turtle/README.md b/orx-turtle/README.md new file mode 100644 index 00000000..8d8e4688 --- /dev/null +++ b/orx-turtle/README.md @@ -0,0 +1,33 @@ +# orx-turtle + +Bezier (`ShapeContour`) backed turtle graphics. + +## The turtle language + +The basic turtle language consists of: + + * `rotate(degrees: Double)` to rotate + * `forward(distance: Double` to walk forward + * `penUp()` to raise the pen + * `penDown()` to lower the pen, this will start a new contour + +Orientation/direction and position can be set directly + * `position: Vector2` get/set position of the turtle, teleporting the turtle will start a new contour + * `direction: Vector2` get/set direction of the turtle, setting direction will compute `orientation` + * `orientation: Matrix44` the orientation matrix from which `direction` is evaluated + * `isPenDown: Boolean` + +The language also holds some tools to manage the position and orientation of the turtle. + + * `resetOrientation()` will reset the orientation to the default orientation + * `push()` push the position and orientation on the stack + * `pop()` pop the position and orientation from the stack + * `pushOrientation()` push the orientation on the stack + * `popOrientation()` pop the orientation on the stack + * `pushPosition()` push the position on the stack + * `popPosition()` pop the position from the stack + +## The extended turtle language + + * `segment(s: Segment)` to draw a segment with its entrance tangent aligned to the turtle's orientation + * `contour(c: ShapeContour)` to draw a contour with its entrance tangent aligned to the turtle's orientation diff --git a/orx-turtle/build.gradle.kts b/orx-turtle/build.gradle.kts new file mode 100644 index 00000000..327da5ab --- /dev/null +++ b/orx-turtle/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + org.openrndr.extra.convention.`kotlin-multiplatform` +} + +kotlin { + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + api(libs.openrndr.math) + api(libs.openrndr.shape) + } + } + + @Suppress("UNUSED_VARIABLE") + val jvmDemo by getting { + dependencies { + implementation(project(":orx-shapes")) + implementation(project(":orx-noise")) + } + } + } +} \ No newline at end of file diff --git a/orx-turtle/src/commonMain/kotlin/NinjaTurtle.kt b/orx-turtle/src/commonMain/kotlin/NinjaTurtle.kt new file mode 100644 index 00000000..9b78b7f4 --- /dev/null +++ b/orx-turtle/src/commonMain/kotlin/NinjaTurtle.kt @@ -0,0 +1,51 @@ +package org.openrndr.extra.turtle + +import org.openrndr.math.Matrix44 +import org.openrndr.math.Vector4 +import org.openrndr.math.transforms.buildTransform +import org.openrndr.shape.Segment +import org.openrndr.shape.ShapeContour + +fun Turtle.contour(contour: ShapeContour, alignTangent: Boolean = true) { + if (!contour.empty) { + val align = segment(contour.segments.first(), alignTangent) + for (segment in contour.segments.drop(1)) { + segment(segment, alignTangent = false, externalAlignTransform = align) + } + } +} + +fun Turtle.segment( + segment: Segment, + alignTangent: Boolean = true, + externalAlignTransform: Matrix44 = Matrix44.IDENTITY +): Matrix44 { + var segment0 = segment.transform(buildTransform { + translate(-segment.start) + }) + + var alignTransform = externalAlignTransform + + if (alignTangent) { + val n = -segment0.normal(0.0) + val t = n.perpendicular() + val m = Matrix44.fromColumnVectors(t.xy00, n.xy00, Vector4.UNIT_Z, Vector4.UNIT_W) + alignTransform = orientation * m.inversed + } + + segment0 = segment0.transform(buildTransform { + translate(position) + multiply(alignTransform) + }) + + require(position.distanceTo(segment0.start) < 1E-5) { + """${position}, ${segment0.start}""" + } + cb.segment(segment0) + orientation = cb.segments.last().pose(1.0).matrix33.matrix44 + + if (!isPenDown) { + cb.segments.removeLastOrNull() + } + return alignTransform +} \ No newline at end of file diff --git a/orx-turtle/src/commonMain/kotlin/Turtle.kt b/orx-turtle/src/commonMain/kotlin/Turtle.kt new file mode 100644 index 00000000..e6c63bf2 --- /dev/null +++ b/orx-turtle/src/commonMain/kotlin/Turtle.kt @@ -0,0 +1,96 @@ +package org.openrndr.extra.turtle + +import org.openrndr.math.* +import org.openrndr.math.transforms.rotateZ +import org.openrndr.shape.* + +class Turtle(initialPosition: Vector2) { + val cb = ContourBuilder(multipleContours = true).apply { + moveTo(initialPosition) + } + + var orientation = Matrix44.fromColumnVectors(Vector4.UNIT_X, -Vector4.UNIT_Y, Vector4.UNIT_Z, Vector4.UNIT_W) + private val orientationStack = ArrayDeque() + fun pushOrientation() { + orientationStack.addLast(orientation) + } + + fun popOrientation() { + orientation = orientationStack.removeLastOrNull() ?: error("orientation stack underflow") + } + + var position: Vector2 + get() { + return cb.cursor + } + set(value) { + cb.moveTo(value) + } + + private val positionStack = ArrayDeque() + fun pushPosition() { + positionStack.addLast(position) + } + + fun popPosition() { + position = positionStack.removeLastOrNull() ?: error("position stack underflow") + } + + fun close() { + cb.close() + } + + fun resetOrientation() { + orientation = Matrix44.fromColumnVectors(Vector4.UNIT_X, -Vector4.UNIT_Y, Vector4.UNIT_Z, Vector4.UNIT_W) + } + + var direction: Vector2 + get() = (orientation * Vector4.UNIT_X).xy + set(value) { + val directionNormalized = value.normalized + orientation = Matrix44.fromColumnVectors( + directionNormalized.xy00, + directionNormalized.perpendicular().xy00, + Vector4.UNIT_Z, + Vector4.UNIT_W + ) + } + + var isPenDown = true + + fun penUp() { + isPenDown = false + } + + fun penDown() { + isPenDown = true + } + + fun push() { + pushOrientation() + pushPosition() + } + + fun pop() { + popPosition() + popOrientation() + } + + fun rotate(degrees: Double) { + orientation *= Matrix44.rotateZ(degrees) + } + + fun forward(distance: Double) { + if (distance >= 1E-6) { + if (isPenDown) { + cb.lineTo(position + direction * distance) + } else { + cb.moveTo(position + direction * distance) + } + } + } +} + +fun turtle(initalPosition: Vector2, program: Turtle.() -> Unit): List { + return Turtle(initalPosition).apply(program).cb.result +} diff --git a/orx-turtle/src/jvmDemo/kotlin/DemoTurtle01.kt b/orx-turtle/src/jvmDemo/kotlin/DemoTurtle01.kt new file mode 100644 index 00000000..34082139 --- /dev/null +++ b/orx-turtle/src/jvmDemo/kotlin/DemoTurtle01.kt @@ -0,0 +1,40 @@ +/* +Drawing a square using the turtle interface. +*/ + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.turtle.turtle + +fun main() { + application { + program { + // turtle returns List + val contours = turtle(drawer.bounds.center) { + penUp() + forward(50.0) + rotate(90.0) + forward(50.0) + penDown() + + rotate(90.0) + forward(100.0) + + rotate(90.0) + forward(100.0) + + rotate(90.0) + forward(100.0) + + rotate(90.0) + forward(100.0) + } + + extend { + // draw the contours + drawer.stroke = ColorRGBa.PINK + drawer.contours(contours) + } + } + } +} \ No newline at end of file diff --git a/orx-turtle/src/jvmDemo/kotlin/DemoTurtle02.kt b/orx-turtle/src/jvmDemo/kotlin/DemoTurtle02.kt new file mode 100644 index 00000000..1f3d8fac --- /dev/null +++ b/orx-turtle/src/jvmDemo/kotlin/DemoTurtle02.kt @@ -0,0 +1,28 @@ +/* +A simple random walk made using the turtle interface. +*/ + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.noise.uniform +import org.openrndr.extra.turtle.turtle +import org.openrndr.math.Vector2 +import kotlin.random.Random + +fun main() { + application { + program { + val r = Random(40) + val contours = turtle(drawer.bounds.center + Vector2(-50.0, 50.0)) { + for (i in 0 until 500) { + rotate(Double.uniform(-90.0, 90.0, r)) + forward(Double.uniform(10.0, 40.0, r)) + } + } + extend { + drawer.stroke = ColorRGBa.PINK + drawer.contours(contours) + } + } + } +} \ No newline at end of file diff --git a/orx-turtle/src/jvmDemo/kotlin/DemoTurtle03.kt b/orx-turtle/src/jvmDemo/kotlin/DemoTurtle03.kt new file mode 100644 index 00000000..8484dc95 --- /dev/null +++ b/orx-turtle/src/jvmDemo/kotlin/DemoTurtle03.kt @@ -0,0 +1,37 @@ +/* +Drawing shape contours aligned to the turtle's orientation. +*/ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.turtle.contour +import org.openrndr.extra.turtle.turtle +import org.openrndr.math.Vector2 +import org.openrndr.shape.Circle + +fun main() { + application { + program { + // turtle returns List + val contours = turtle(drawer.bounds.center + Vector2(-100.0, 100.0)) { + forward(100.0) + + // let the turtle draw a full a circle + val circle0 = Circle(Vector2.ZERO, 100.0) + contour(circle0.contour) + + // let the turtle draw a half circle + val circle1 = Circle(Vector2.ZERO, 50.0) + contour(circle1.contour.sub(0.0, 0.5)) + + // let the turtle draw another half circle + val circle2 = Circle(Vector2.ZERO, 25.0) + contour(circle2.contour.sub(0.0, 0.5)) + } + extend { + // draw the contours + drawer.stroke = ColorRGBa.PINK + drawer.contours(contours) + } + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index cae2febf..bc20bbdc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -83,6 +83,7 @@ include( "orx-jvm:orx-video-profiles", "orx-depth-camera", "orx-jvm:orx-depth-camera-calibrator", - "orx-view-box" + "orx-view-box", + "orx-turtle" ) ) \ No newline at end of file