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
}
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:
- Linear
- Radial
- Sweep
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
}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.RadialGradientPaint
import io.nacular.doodle.geometry.Circle
/**
* Example showing how to use [RadialGradientPaint]s.
*/
fun radialPaint(color1: Color, color2: Color, circle1: Circle, circle2: Circle) {
//sampleStart
view {
render = {
// Simple version with 2 colors
rect(bounds.atOrigin, RadialGradientPaint(color1, color2, circle1, circle2))
}
}
view {
render = {
// Or use a full list of color stops
rect(bounds.atOrigin, RadialGradientPaint(
listOf(Stop(color1, 0f), Stop(color1, 1f/3), /*...*/),
circle1,
circle2
))
}
}
//sampleEnd
}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.SweepGradientPaint
import io.nacular.doodle.geometry.Point
import io.nacular.measured.units.Angle
import io.nacular.measured.units.Measure
/**
* Example showing how to use [SweepGradientPaint]s.
*/
fun sweepGradientPaint(color1: Color, color2: Color, center: Point, rotation: Measure<Angle>) {
//sampleStart
view {
render = {
// Simple version with 2 colors
rect(bounds.atOrigin, SweepGradientPaint(color1, color2, center, rotation))
}
}
view {
render = {
// Or use a full list of color stops
rect(bounds.atOrigin, SweepGradientPaint(
listOf(Stop(color1, 0f), Stop(color1, 1f/3), /*...*/),
center,
rotation
))
}
}
//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:
- Demo
- Code
package rendering
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.ImagePaint
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.image.Image
fun imagePaint(image: Image, size: Size, opacity: Float) {
//sampleStart
view {
render = {
rect(bounds.atOrigin, ImagePaint(image, size, opacity))
}
}
//sampleEnd
}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.
- Demo
- Code
package rendering
import io.nacular.doodle.core.View
import io.nacular.doodle.core.renderProperty
import io.nacular.doodle.drawing.AffineTransform.Companion.Identity
import io.nacular.doodle.drawing.AffineTransform2D
import io.nacular.doodle.drawing.Canvas
import io.nacular.doodle.drawing.Color
import io.nacular.doodle.drawing.Color.Companion.Red
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.PatternPaint
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.drawing.stripedPaint
import io.nacular.doodle.drawing.visible
import io.nacular.doodle.drawing.width
import io.nacular.doodle.geometry.Point
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.Size
import io.nacular.measured.units.Angle.Companion.degrees
import io.nacular.measured.units.times
// Doodle's implementation of striped-paint looks something like this
fun stripedPaintApprox(
stripeWidth : Double,
evenRowColor: Color? = null,
oddRowColor : Color? = null,
transform : AffineTransform2D = Identity
) = PatternPaint(
Size(if (evenRowColor.visible || oddRowColor.visible) stripeWidth else 0.0, 2 * stripeWidth),
transform
) {
evenRowColor?.let { rect(Rectangle( width, width), it.paint) }
oddRowColor?.let { rect(Rectangle(0.0, width, width, width), it.paint) }
}
//sampleStart
class SomeView: View() {
private val stripeWidth = 20.0
private var paintAngle by renderProperty(0 * degrees)
override fun render(canvas: Canvas) {
val paintCenter = Point(canvas.size.width / 2, canvas.size.height / 2)
canvas.rect(bounds.atOrigin, stripedPaint(
stripeWidth = stripeWidth,
evenRowColor = Red,
oddRowColor = White,
transform = Identity.rotate(around = paintCenter, by = paintAngle)
))
}
}
//sampleEndImagePaint 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
}
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
Paintfills a shape's interior —Color/ColorPaintfor solid fills, gradients for blends,ImagePaint/PatternPaintfor repeating textures,FrostedGlassPaintfor a blur effect. - A
Strokeoutlines a shape the same way aPaintfills one, plus thickness, dashing, and cap/joint style. - Strokes straddle a shape's edge, so account for half the
thicknessfalling outside its bounds.
Next: let's put all this to work — on to Shapes & Paths.