Skip to main content

Paint & Stroke

Back on Canvas, you got acquainted with the coordinate system every View draws into. Now let's fill some of that space in — this page covers the other half of drawing a shape: what you actually fill and outline it with.

Filling shapes with Paint​

Most shapes need something to fill their interior with — a color, a gradient, a texture. That's what a Paint is, and it's what nearly every Canvas drawing call is waiting for.

Colors​

Color is Doodle's RGBA color type, and it's happy to be built a few different ways — from hex, from a built-in constant, or derived from another color:

package rendering import io.nacular.doodle.drawing.Color import io.nacular.doodle.drawing.darker import io.nacular.doodle.drawing.opacity import io.nacular.doodle.drawing.paint fun colors() { //sampleStart Color(0xff0000u ) // create red Color(0xffffffu, opacity = 0.5f) // white with 0.5 opacity Color.Red // build-in red Color.Blue opacity 0.5f // blue with 0.5 opacity Color.Blue.inverted // inverse of blue Color.Lightgray.darker(0.5f) // darker gray Color.Blue.paint // paint from a color //sampleEnd }

ColorPaint — the simplest, most common Paint of them all — is what that last line hands you. The .paint extension turns any Color into one:

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

Doodle also has utilities for HSL and HSV color spaces (used internally by ColorPicker, for instance) — you can't render with them directly, but there are conversions to and from RGBA.

Gradients​

LinearGradientPaint, RadialGradientPaint, and SweepGradientPaint blend a series of colors smoothly across a shape, and they all share the same two calling conventions:

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color import io.nacular.doodle.drawing.GradientPaint.Stop import io.nacular.doodle.drawing.LinearGradientPaint import io.nacular.doodle.geometry.Point /** * Example showing how to use [LinearGradientPaint]s. */ fun linearPaint(color1: Color, color2: Color, point1: Point, point2: Point) { //sampleStart view { render = { // Simple version with 2 colors rect(bounds.atOrigin, LinearGradientPaint(color1, color2, point1, point2)) } } view { render = { // Or use a full list of color stops rect(bounds.atOrigin, LinearGradientPaint( listOf(Stop(color1, 0f), Stop(color1, 1f/3), /*...*/), point1, point2 )) } } //sampleEnd }

RadialGradientPaint and SweepGradientPaint take the same two forms as LinearGradientPaint; just swap the geometry (a Circle pair for radial, a center point and rotation for sweep). Their API pages have the exact parameters waiting for you.

Patterns and repeating fills​

Want something to repeat across a shape rather than blend? ImagePaint tiles an Image, and PatternPaint hands you the full Canvas API to draw the repeating unit yourself:

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 }

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

info

ImagePaint can't crop the image it's given — use PatternPaint if you need that.

Frosted glass​

FrostedGlassPaint blurs whatever's sitting underneath it — genuine frosted-glass texture, no filters required:

package rendering import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color.Companion.White import io.nacular.doodle.drawing.FrostedGlassPaint import io.nacular.doodle.drawing.opacity /** * Example showing how to use [FrostedGlassPaint]s. */ fun frostedGlassPaint() { //sampleStart view { render = { rect(bounds.atOrigin, FrostedGlassPaint( color = White opacity 0.5f, blurRadius = 10.0 )) } } //sampleEnd }

Outlining with Stroke​

Sometimes you want the edge, not the interior. A Stroke outlines a shape instead of filling it — think of it as a Paint for the border, plus a handful of properties governing exactly how that border is drawn:

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 import io.nacular.doodle.geometry.inset fun stroke(paint: Paint) { //sampleStart view { render = { val thickness = 2.5 rect(bounds.atOrigin.inset(thickness / 2), stroke = Stroke( fill = paint, dashes = doubleArrayOf(10.0, 20.0), thickness = thickness, lineCap = Round, lineJoint = Miter )) } } //sampleEnd }
caution

Strokes are centered on the shape's edge — half the thickness falls outside the shape's bounds. Shrink your shape slightly if you need the outline to stay fully within its bounds and avoid being clipped.

Recap​

  • A Paint fills a shape's interior — Color/ColorPaint for solid fills, gradients for blends, ImagePaint/PatternPaint for repeating textures, FrostedGlassPaint for a blur effect.
  • A Stroke outlines a shape the same way a Paint fills one, plus thickness, dashing, and cap/joint style.
  • Strokes straddle a shape's edge, so account for half the thickness falling outside its bounds.

Next: let's put all this to work — on to Shapes & Paths.