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 positionlineTo(position)
insert a line contour starting from the cursor, ending at the given positionmoveOrLineTo(position)
move the cursor if no cursor was previously set or draw a linecurveTo(control, position)
insert a quadratic bezier curve starting from the cursor, ending at positioncurveTo(controlA, controlB, position)
insert a cubic bezier curve starting from the cursor, ending at positioncontinueTo(position)
inside a quadratic bezier curve starting from the cursor and reflecting the tangent of the last controlcontinueTo(controlB, position)
insert a cubic splinearcTo(radiusX, radiusY, largeAngle, sweepFlag, position)
close()
close the contourcursor
aVector2
instance representing the current positionanchor
aVector2
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
andLineSegment.shape
Rectangle.contour
andRectangle.shape
Circle.contour
andCircle.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)
}
}
}
}