# Complex shapes

OPENRNDR offers a lot of tools for the creation and drawing of two dimensional shapes.

## Shapes

OPENRNDR uses `Shape` to represent planar shapes of which the contours are described using piece-wise bezier curves.

A `Shape` is composed of one or multiple `ShapeContour` instances. A `ShapeContour` is composed of multiple `Segment` instances, describing a bezier curve each.

## Shape and ShapeContour builders

The `ContourBuilder` class offers a simple way of producing complex two dimensional shapes. `ContourBuilder` employs a vocabulary that is familiar to those who have used SVG.

• `moveTo(position)` move the cursor to the given position
• `lineTo(position)` insert a line contour starting from the cursor, ending at the given position
• `moveOrLineTo(position)` move the cursor if no cursor was previously set or draw a line
• `curveTo(control, position)` insert a quadratic bezier curve starting from the cursor, ending at position
• `curveTo(controlA, controlB, position)` insert a cubic bezier curve starting from the cursor, ending at position
• `continueTo(position)` inside a quadratic bezier curve starting from the cursor and reflecting the tangent of the last control
• `continueTo(controlB, position)` insert a cubic spline
• `arcTo(radiusX, radiusY, largeAngle, sweepFlag, position)`
• `close()` close the contour
• `cursor` a `Vector2` instance representing the current position
• `anchor` a `Vector2` instance representing the current anchor

Let’s create a simple `Contour` and draw it. The following program shows how to use the contour builder to create a triangular contour.

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
extend {
val c = contour {
moveTo(Vector2(width / 2.0 - 150.0, height / 2.0 - 150.00))
// -- here `cursor` points to the end point of the previous command
lineTo(cursor + Vector2(300.0, 0.0))
lineTo(cursor + Vector2(0.0, 300.0))
lineTo(anchor)
close()
}
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
drawer.contour(c)
}
}
}
``````

Now let’s create a `Shape` using the shape builder. The shape is created using two contours, one for outline of the shape, and one for the hole in the shape

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
extend {
val s = shape {
contour {
moveTo(Vector2(width / 2.0 - 150.0, height / 2.0 - 150.00))
lineTo(cursor + Vector2(300.0, 0.0))
lineTo(cursor + Vector2(0.0, 300.0))
lineTo(anchor)
close()
}
contour {
moveTo(Vector2(width / 2.0 - 80.0, height / 2.0 - 100.0))
lineTo(cursor + Vector2(200.0, 0.0))
lineTo(cursor + Vector2(0.0, 200.00))
lineTo(anchor)
close()
}
}
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
drawer.shape(s)
}
}
}
``````

## Shapes and contours from primitives

Not all shapes need to be created using the builders. Some of the OPENRNDR primitives have `.shape` and `.contour` properties that help in creating shapes quickly.

• `LineSegment.contour` and `LineSegment.shape`
• `Rectangle.contour` and `Rectangle.shape`
• `Circle.contour` and `Circle.shape`

## Contours from points

Contours can be created from a list of points or from a list of consecutive segments.

``````fun main() = application {
program {
val points = List(30) {
Vector2(20.0 + it * 20.0, 300.0 + sin(it * 0.8) * 100.0)
}
val wavyContour = ShapeContour.fromPoints(points, closed = true)

val segments = listOf(Segment(Vector2(10.0, 100.0), Vector2(200.0, 80.0)), // Linear Segment
Segment(Vector2(200.0, 80.0), Vector2(250.0, 280.0), Vector2(400.0, 80.0)), // Quadratic Bézier segment
Segment(Vector2(400.0, 80.0), Vector2(450.0, 180.0), Vector2(500.0, 0.0), Vector2(630.0, 80.0))) // Cubic Bézier segment
val horizontalContour = ShapeContour.fromSegments(segments, closed = false)

extend {
drawer.fill = ColorRGBa.PINK
drawer.contour(wavyContour)

drawer.strokeWeight = 5.0
drawer.stroke = ColorRGBa.PINK
drawer.contour(horizontalContour)
}
}
}
``````

## Shape Boolean-operations

Boolean-operations can be performed on shapes using the `compound {}` builder. There are three kinds of compounds: union, difference and intersection, all three of them are shown in the example below.

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
extend {
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
// -- shape union
val su = compound {
union {
shape(Circle(185.0, height / 2.0 - 80.0, 100.0).shape)
shape(Circle(185.0, height / 2.0 + 80.0, 100.0).shape)
}
}
drawer.shapes(su)

// -- shape difference
val sd = compound {
difference {
shape(Circle(385.0, height / 2.0 - 80.0, 100.0).shape)
shape(Circle(385.0, height / 2.0 + 80.0, 100.0).shape)
}
}
drawer.shapes(sd)

// -- shape intersection
val si = compound {
intersection {
shape(Circle(585.0, height / 2.0 - 80.0, 100.0).shape)
shape(Circle(585.0, height / 2.0 + 80.0, 100.0).shape)
}
}
drawer.shapes(si)
}
}
}
``````

The compound builder is actually a bit more clever than what the previous example demonstrated because it can actually work with an entire tree of compounds. Demonstrated below is the union of two intersections.

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
extend {
drawer.fill = ColorRGBa.PINK
drawer.stroke = null
val cross = compound {
union {
intersection {
shape(Circle(width / 2.0 - 160.0, height / 2.0, 200.0).shape)
shape(Circle(width / 2.0 + 160.0, height / 2.0, 200.0).shape)
}
intersection {
shape(Circle(width / 2.0, height / 2.0 - 160.0, 200.0).shape)
shape(Circle(width / 2.0, height / 2.0 + 160.0, 200.0).shape)
}
}
}
drawer.shapes(cross)
}
}
}
``````

## Cutting contours

A contour be cut into a shorter contour using `ShapeContour.sub()`.

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
extend {
drawer.fill = null
drawer.stroke = ColorRGBa.PINK
drawer.strokeWeight = 4.0

val sub0 = Circle(185.0, height / 2.0, 100.0).contour.sub(0.0, 0.5 + 0.50 * sin(seconds))
drawer.contour(sub0)

val sub1 = Circle(385.0, height / 2.0, 100.0).contour.sub(seconds * 0.1, seconds * 0.1 + 0.1)
drawer.contour(sub1)

val sub2 = Circle(585.0, height / 2.0, 100.0).contour.sub(-seconds * 0.05, seconds * 0.05 + 0.1)
drawer.contour(sub2)
}
}
}
``````

## Placing points on contours

Call `ShapeContour.position()` to sample one specific location or `ShapeContour.equidistantPositions()` to sample multiple equidistant locations on a contour.

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
extend {

drawer.stroke = null
drawer.fill = ColorRGBa.PINK

val point = Circle(185.0, height / 2.0, 90.0).contour.position((seconds * 0.1) % 1.0)
drawer.circle(point, 10.0)

val points0 = Circle(385.0, height / 2.0, 90.0).contour.equidistantPositions(20)
drawer.circles(points0, 10.0)

val points1 = Circle(585.0, height / 2.0, 90.0).contour.equidistantPositions((cos(seconds) * 10.0 + 30.0).toInt())
drawer.circles(points1, 10.0)
}
}
}
``````

## Offsetting contours

The function `ShapeContour.offset` can be used to create an offset version of a contour.

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
// -- create a contour from Rectangle object
val c = Rectangle(100.0, 100.0, width - 200.0, height - 200.0).contour.reversed

extend {
drawer.fill = null
drawer.stroke = ColorRGBa.PINK
drawer.contour(c)
for (i in 1 until 10) {
val o = c.offset(cos(seconds + 0.5) * i * 10.0, SegmentJoin.BEVEL)
drawer.contour(o)
}
}
}
}
``````

`ShapeContour.offset` can also be used to offset curved contours. The following demonstration shows a single cubic bezier offset at multiple distances.

``````fun main() = application {
configure {
width = 770
height = 578
}
program {
val c = contour {
moveTo(width * (1.0 / 2.0), height * (1.0 / 5.0))
curveTo(width * (1.0 / 4.0), height * (2.0 / 5.0), width * (3.0 / 4.0), height * (3.0 / 5.0), width * (2.0 / 4.0), height * (4.0 / 5.0))
}
extend {
drawer.stroke = ColorRGBa.PINK
drawer.strokeWeight = 2.0
drawer.lineJoin = LineJoin.ROUND
drawer.contour(c)
for (i in -8..8) {
val o = c.offset(i * 10.0 * cos(seconds + 0.5))
drawer.contour(o)
}
}
}
}
``````