Shapes & Paths
Back on Paint & Stroke, you picked up what to fill and outline a shape with. Now let's give that a canvas to work on — the basic set of shapes Doodle ships with, and Path for everything the basic set can't handle.
Basic shapes
Every basic shape follows the same tidy pattern: hand it a fill, a stroke, or both.
- Rect
- Poly
- Circle
- Ellipse
- Arc
- Wedge
package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Paint
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.geometry.Rectangle
fun rect(rectangle: Rectangle, paint: Paint, stroke: Stroke) {
//sampleStart
view {
render = {
rect(rectangle, fill = paint, stroke = stroke)
}
}
//sampleEnd
}A View's bounds are relative to its parent, so bring them into the View's own coordinate space before drawing. bounds.atOrigin does exactly that — it's why you'll see it throughout this section.
package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Paint
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.geometry.Circle
import io.nacular.doodle.geometry.Polygon
import io.nacular.doodle.geometry.inscribed
import io.nacular.measured.units.Angle.Companion.degrees
import io.nacular.measured.units.times
fun poly(polygon: Polygon, circle: Circle, paint: Paint, stroke: Stroke) {
// You can also create equilateral polygons by inscribing them
// within circles.
circle.inscribed(8, rotation = 45 * degrees)
//sampleStart
view {
render = {
poly(polygon, fill = paint, stroke = stroke)
}
}
//sampleEnd
}package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Paint
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.geometry.Circle
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.inscribedCircle
fun circle(circle: Circle, rectangle: Rectangle, paint: Paint, stroke: Stroke) {
// You can also create circles based on Rectangles
rectangle.inscribedCircle()
//sampleStart
view {
render = {
circle(circle, fill = paint, stroke = stroke)
}
}
//sampleEnd
}Circles can be created from Rectangles, using the inscribedCircle method:
package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Paint
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.geometry.Ellipse
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.inscribedEllipse
fun ellipse(ellipse: Ellipse, rectangle: Rectangle, paint: Paint, stroke: Stroke) {
// You can also create ellipses based on Rectangles
rectangle.inscribedEllipse()
//sampleStart
view {
render = {
ellipse(ellipse, fill = paint, stroke = stroke)
}
}
//sampleEnd
}Ellipses can be created from Rectangles, using the inscribedEllipse method:
package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Paint
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.geometry.Point
import io.nacular.measured.units.Angle.Companion.degrees
import io.nacular.measured.units.Angle.Companion.radians
import io.nacular.measured.units.times
import kotlin.math.PI
fun arc(center: Point, paint: Paint, stroke: Stroke) {
//sampleStart
view {
render = {
arc(
center = center,
radius = 100.0,
fill = paint,
stroke = stroke,
sweep = 270 * degrees,
rotation = PI * radians
)
}
}
//sampleEnd
}package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Paint
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.geometry.Point
import io.nacular.measured.units.Angle.Companion.degrees
import io.nacular.measured.units.Angle.Companion.radians
import io.nacular.measured.units.times
import kotlin.math.PI
fun wedge(center: Point, paint: Paint, stroke: Stroke) {
//sampleStart
view {
render = {
wedge(
center = center,
radius = 100.0,
fill = paint,
stroke = stroke,
sweep = 270 * degrees,
rotation = PI / 2 * radians
)
}
}
//sampleEnd
}Paths
Every so often a shape refuses to fit the basic set — that's what Path is for: arbitrary outlines that still fill and stroke exactly the same way. The tradeoff is that paths don't track their own size (get that separately via PathMetrics), which is precisely what keeps them lightweight.
package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Paint
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.geometry.Circle
import io.nacular.doodle.geometry.Point
import io.nacular.doodle.geometry.path
import io.nacular.doodle.geometry.rounded
import io.nacular.doodle.geometry.star
import io.nacular.doodle.utils.isEven
fun path(paint: Paint, stroke: Stroke) {
// Path from SVG data
val heart = path("M50,25 C35,0,-14,25,20,60 L50,90 L80,60 C114,20,65,0,50,25")!!
//sampleStart
// Star with rounded corners
val star = star(
Circle(Point(100, 100), radius = 100.0),
points = 5
)!!.rounded(radius = 30.0) { index,_ -> index.isEven }
view {
render = {
path(star, fill = paint, stroke = stroke)
}
}
//sampleEnd
}
Include the PathModule (Web, Desktop) in your application to use PathMetrics.
package rendering
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.Modules.Companion.PathModule
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import io.nacular.doodle.geometry.PathMetrics
import org.kodein.di.instance
class PathApp(display: Display, pathMetrics: PathMetrics): Application {
override fun shutdown() {}
}
fun pathModule() {
//sampleStart
application(modules = listOf(PathModule)) {
PathApp(display = instance(), pathMetrics = instance())
}
//sampleEnd
}Beyond SVG strings, a Path is also happy to be built with a fluent builder, or conjured directly from most geometric shapes:
package rendering
import io.nacular.doodle.geometry.Circle
import io.nacular.doodle.geometry.Path
import io.nacular.doodle.geometry.Point
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.map
import io.nacular.doodle.geometry.path
import io.nacular.doodle.geometry.toPath
fun pathCreation(start: Point, end: Point, width: Double, height: Double) {
// Just convert most geometric shapes into them:
Circle ().toPath()
Rectangle().toPath()
Rectangle().map { it * 2 }.toPath()
// Use the path builder function:
//sampleStart
path(start)
.cubicTo(
Point(width / 2, height / 2),
Point(width / 3, 0),
Point(width / 2, -10)
)
.cubicTo(
end,
Point(width / 2, height + 10),
Point(width * 2 / 3, height )
).
finish() // or close
//sampleEnd
// Or use a raw String:
val path: Path? = path("...") // returns nullable since given string might be invalid
val imSure = path("...")!!
}
Recap
- Every basic shape (
rect,poly,circle,ellipse,arc,wedge) takes the samefill/strokepair from the Paint & Stroke page. - Some shapes can be derived from others — an equilateral polygon from a
Circle, aCircle/Ellipsefrom aRectangle. - Reach for
Pathwhen a shape doesn't fit the basic set. It needsPathModule, and doesn't track its own size the way basic shapes do.
Next: on to Text to draw, wrap, and measure it.