[orx-turtle] Add orx-turtle module

This commit is contained in:
Edwin Jakobs
2023-02-02 09:33:06 +01:00
parent 443b7f4155
commit c850ed9c84
9 changed files with 312 additions and 2 deletions

33
orx-turtle/README.md Normal file
View File

@@ -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

View File

@@ -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"))
}
}
}
}

View File

@@ -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
}

View File

@@ -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<Matrix44>()
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<Vector2>()
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<ShapeContour> {
return Turtle(initalPosition).apply(program).cb.result
}

View File

@@ -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<ShapeContour>
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)
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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<ShapeContour>
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)
}
}
}
}