Skip to main content

The Canvas

Back on Views, you saw that render is where a View's drawing happens, and that Doodle calls it for you automatically. Now it's time to open up that Canvas and see what it's actually capable of — it's the first of five pages that make up the Rendering section:

  • The Canvas (this page) — the coordinate system you're drawing into, and how Doodle redraws efficiently
  • Paint & Stroke — filling and outlining shapes with color, gradients, and patterns
  • Shapes & Paths — rectangles, circles, polygons, and arbitrary outlines
  • Text — drawing, wrapping, styling, and measuring text
  • Images — loading and drawing images

Start here, then follow the "Next" link at the bottom of each page — or skip straight to whichever one answers your immediate question.

Let's ease in with the simplest possible example: a View that fills itself with blue.

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

The Canvas you're handed in render plays by one important rule: 0,0 is always the View's own top-left corner, no matter where that View actually sits on screen. You never have to think about your View's position while drawing — only its own little pocket of coordinate space.

The canvas technically stretches beyond your View's bounds in every direction, but anything you draw out there gets quietly clipped away by default.

tip

Need to draw outside your own bounds on purpose? Set clipCanvasToBounds = false in your subclass.

Transformations​

You can transform the canvas itself using an AffineTransform — translation, scaling, and rotation, all folded into one matrix. This View flips the canvas horizontally around its midpoint before drawing 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

Going further: 3D​

Canvas also stretches into basic 3D, pairing AffineTransforms with a Camera so several Views can share a common 3D space (a folding cube is the classic demo). Most apps never need to go there, so we won't linger on it here — see Transforms in Advanced Topics when you're ready for the full picture, perspective and shared cameras included.

Efficient rendering​

Doodle doesn't blindly redraw everything on every render call — it only touches the parts that actually changed (for Web apps, that means updating only the DOM nodes that changed, not the whole tree). The Timer app below calls rerender every millisecond to keep a clock running, yet only the text itself ever gets touched in the DOM — the surrounding rectangles are left alone:

package timerdsl import io.nacular.doodle.application.Application import io.nacular.doodle.core.Display import io.nacular.doodle.core.view import io.nacular.doodle.drawing.Color.Companion.Black import io.nacular.doodle.drawing.Color.Companion.Green import io.nacular.doodle.drawing.Color.Companion.Red import io.nacular.doodle.drawing.Color.Companion.White import io.nacular.doodle.drawing.paint import io.nacular.doodle.drawing.rect import io.nacular.doodle.drawing.stroke import io.nacular.doodle.drawing.text import io.nacular.doodle.geometry.Size import io.nacular.doodle.geometry.at import io.nacular.doodle.geometry.inset import io.nacular.doodle.layout.constraints.center import io.nacular.doodle.layout.constraints.constrain import io.nacular.doodle.scheduler.Scheduler import io.nacular.doodle.time.Clock import io.nacular.measured.units.Time.Companion.milliseconds import io.nacular.measured.units.times //sampleStart class Timer(display: Display, clock: Clock, scheduler: Scheduler): Application { init { display += view { suggestSize(Size(200)) scheduler.every(1 * milliseconds) { rerender() } render = { rect(bounds.at(x = 0.0, y = 20.0), color = Green) rect(bounds.atOrigin.inset(0.5), Red.stroke) text("${clock.epoch}", color = Black) } } display.layout = constrain(display.first(), center) display.fill(White.paint) } override fun shutdown() {} } //sampleEnd
info

Doodle uses Measured for typed units like time and angles — that's what 1 * milliseconds is.

Recap​

  • render is where a View draws itself, with 0, 0 always at the View's own top-left corner.
  • The canvas clips to the View's bounds by default; opt out with clipCanvasToBounds.
  • Doodle only touches what actually changed on a redraw, so calling rerender often — once a millisecond, even — is perfectly fine.

Next: on to Paint & Stroke to see what you can actually fill and outline shapes with.