Skip to main content

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.

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

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.

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 }
Module Required

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 same fill/stroke pair from the Paint & Stroke page.
  • Some shapes can be derived from others — an equilateral polygon from a Circle, a Circle/Ellipse from a Rectangle.
  • Reach for Path when a shape doesn't fit the basic set. It needs PathModule, and doesn't track its own size the way basic shapes do.

Next: on to Text to draw, wrap, and measure it.