Skip to main content


Doodle automatically manages rendering of Views, and this covers almost all use-cases. Each View draws its content to a Canvas provided during calls to the render method. This either presents the View's contents on the screen for the first time or updates them on subsequent calls.

Doodle calls render whenever a View needs a visual update, which includes changes to its size or it becoming visible. This means you do not need to call render yourself. But there might be cases when you want to request that a View re-renders. You can do this by calling rerender.

This is an example of a simple View that draws a rectangle filled with Blue ColorPaint that covers its bounds.

import io.nacular.doodle.core.View import io.nacular.doodle.drawing.Canvas import io.nacular.doodle.drawing.Color.Companion.Blue import io.nacular.doodle.drawing.paint //sampleStart class RectView: View() { override fun render(canvas: Canvas) { canvas.rect(bounds.atOrigin, Blue.paint) } } //sampleEnd

render is automatically called on size changes and visible changing to true

Efficient rendering

Doodle optimizes rendering to avoid re-applying operations when drawing the same content repeatedly. For example, the Timer app below renders the epoch time every millisecond. However, Doodle only updates the changing regions (i.e. the DOM for Web apps), which is the text in this case. This reduces the amount of thrash in the DOM for Web apps.


Doodle uses Measured for time, angles etc.

The Canvas

All drawing is done via the Canvas API, which offers a rich set of operations for geometric shapes, paths, images, and text. It also supports different Paint types (i.e. ColorPaint, LinearGradientPaint, PatternPaint, etc.) for filling regions.

The Canvas provided to a View's render method has a coordinate system that anchors 0,0 on the Canvas to the View's origin. The Canvas itself extends in all directions beyond the bounds of the View; but the contents drawn to it will be clipped to the view's bounds by default.


Sub-classes can disable clipping by setting clipCanvasToBounds = false.

Filling regions with Paints

Most Views require geometric regions on their Canvas to be filled with some color, gradient, texture, etc.. Therefore, many Canvas APIs take a Paint parameter that determines how the inner region such shapes are filled. There are several built-in Paint types to choose from.


ColorPaint is the simplest and most common way to fill a region. You can create one using the constructor, or via the paint extension on any Color.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color import io.nacular.doodle.drawing.paint fun colorPaint(color: Color) { //sampleStart view { render = { rect(bounds.atOrigin, color.paint) } } //sampleEnd }


You can fill regions with LinearGradientPaints and RadialGradientPaint paints as well. These take a series of Colors and stop locations that are turned into a smoothly transitioning gradient across a shape.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color import io.nacular.doodle.drawing.LinearGradientPaint import io.nacular.doodle.geometry.Point fun linearPaint(color1: Color, color2: Color, point1: Point, point2: Point) { //sampleStart view { render = { rect(bounds.atOrigin, LinearGradientPaint( color1, color2, point1, point2 )) } } //sampleEnd }


Sometimes you need to fill a region with a repeating pattern, like an image or some geometric shapes. This is easy to achieve with ImagePaint (when an image is all you need) or PatternPaint, when more sophisticated patterns are needed.

PatternPaint has a "render" body that provides a powerful and familiar way of creating repeating patterns. You create one by specifying a size and a paint lambda, which has access to the full Canvas APIs.

This app uses stripedPaint to show how a PatternPaint can be transformed using AffineTransform2D, like rotated around its center for example.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.AffineTransform2D import io.nacular.doodle.drawing.PatternPaint import io.nacular.doodle.geometry.Size fun patternPaint(size: Size, transform: AffineTransform2D) { //sampleStart view { render = { rect(bounds.atOrigin, PatternPaint(size, transform) { // render onto canvas // rect(..) }) } } //sampleEnd }

Outlining with Strokes

Regions and curves can be stroked to give them an outline of some sort. The Canvas APIs also take Stroke parameters that determine how to outline the shape being rendered.

A Stroke combines the behavior of a Paint (but for the outline only) and adds several properties that govern how the outline is created.


Strokes are centered along the edge of the shape the outline. This means adding a stroke will expand the area needed to render the shape by stroke.thickness / 2 in the x and y direction. Keep this in mind, and shrink shapes accordingly to ensure outlines are not clipped and they appear where you want them to.

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Paint import io.nacular.doodle.drawing.Stroke import io.nacular.doodle.drawing.Stroke.LineCap.Round import io.nacular.doodle.drawing.Stroke.LineJoint.Miter fun stroke(paint: Paint) { //sampleStart view { render = { rect(bounds.atOrigin, stroke = Stroke( fill = paint, dashes = doubleArrayOf(10.0, 20.0), thickness = 2.5, lineCap = Round, lineJoint = Miter )) } } //sampleEnd }

Basic shapes

The Canvas has APIs for drawing many basic shapes that foundation of most Views. Each shape can be filled with Paint and outlined with Strokes as well.

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, paint, stroke = stroke) } } //sampleEnd }

A View's bounds is relative to its parent, so you need to do bring it into the View's coordinate space before drawing it to the Canvas. This is easy with bounds.atOrigin.


Paths a really powerful primitives that let you create arbitrarily complex shapes that can be filled and stroked like those above. This flexibility comes from the fact that paths are fairly opaque components that behave almost like images.

A major difference with Paths is that they do not track their intrinsic size. This is a tradeoff that allows them to be created in a very light weight way.

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.inscribed import io.nacular.doodle.geometry.path import io.nacular.doodle.geometry.rounded 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")!! // Start with rounded corners val star = Circle( radius = 100.0, center = Point(100, 100) ).inscribed(5)!!.rounded(radius = 10.0) //sampleStart view { render = { path(star, fill = paint, stroke = stroke) } } //sampleEnd }
Module Required

You must include the PathModule in your application in order to use these features.

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 }

Doodle uses opt-in modules like this to improve bundle size.


There are may ways to create Paths.

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 import io.nacular.doodle.geometry.path import io.nacular.doodle.geometry.toPath fun pathCreation(start: Point, end: Point, width: Double, height: Double) { //sampleStart // Just convert most geometric shapes into them: Circle().toPath() Rectangle().toPath() Rectangle().map { it * 2 }.toPath() // Use the path builder function: 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 // Or use a raw String: val path: Path? = path("...") // returns nullable since given string might be invalid val imSure = path("...")!! //sampleEnd }


The Canvas can also be transformed using any AffineTransform. These are linear transformation which encapsulate translation, scaling, and rotation in matrices. You can combine these transforms into more complex ones directly, or apply them to a Canvas in a nested fashion to get similar results

This View flips the Canvas horizontally around its mid-point and draws some text.

package rendering import io.nacular.doodle.core.View import io.nacular.doodle.drawing.Canvas import io.nacular.doodle.drawing.Color.Companion.Black import io.nacular.doodle.drawing.text //sampleStart class MyView: View() { override fun render(canvas: Canvas) { canvas.flipHorizontally(around = width / 2) { text("hello", color = Black) } } } //sampleEnd