Canvas and CanvasGroup are deprecated in RoboFont 4. They are based on the old drawing backend and they are replaced by Merz. You can still use Canvas and CanvasGroup, but consider that they will get slower and slower over time. If you are here to develop something from scratch, our advice is to head to the Introduction to Merz and the Merz reference without looking back. If you need to update an old tool to Merz, check the comparison table of scripts and extensions before and after RoboFont 4.

A canvas view is useful for creating dynamic previews and interactive interfaces. Imagine DrawBot with a live-updating draw loop, and with the ability to react to events such as mouse position and clicks, keyboard input, etc.

This model may sound familiar to users of interactive graphic environments like Processing; it’s also how the native macOS UI layer Cocoa works. Canvas is just a thin pythonic wrapper around Cocoa’s NSView class.

How canvas works

The Canvas and CanvasGroup objects inherit behavior from the NSView class, which controls the presentation and interaction of visible content in a macOS application.

Both canvas objects have a delegate method which can receive notifications of user actions (events). When an action is sent to the delegate (notification), it triggers a delegate method associated with it (callback). This system makes it possible to update the canvas view in real-time based on user input.

A view is refreshed when an update is requested. An update request can be triggered by an user action (such as a window resize) or programmatically by calling Canvas.update().

An update request doesn’t immediately refresh the view; it turns on a flag which tells the view that it needs to be redrawn before being displayed. The actual redraw is handled by the operating system in an efficient way – for example, it does not happen when the view is not visible on screen.

Events

An event is an object which contains information about an input action such as a mouse click or a key press. Depending on the type of event, we can ask where the mouse was located, which character was typed, etc. Some attributes are common to all events, while others are specific to certain types of events. The Canvas and CanvasGroup documentations include a list of all delegate events which can be used by the canvas objects.

Types of canvas

There are two slightly different types of canvas views in mojo.canvas:

Canvas

A vanillaScrollView with a canvas view inside.

The edges of the drawing have scrollbars. The canvas size is limited.

CanvasGroup

A vanillaGroup with a canvas view inside.

The edges of the drawing are bound to the edges of the window.

Canvas vs. DrawView

With Canvas and CanvasGroup objects, we are drawing directly to the screen while the view is being refreshed. This is very fast, but also fragile: an error in the drawing can crash the whole application.

DrawBot’s DrawView is an alternative for cases in which speed and interactivity play a lesser role. Instead of drawing in the main program loop like Canvas, it uses a two-step process: first the PDF data is generated, and then it is set in the view. This process is not so fast, but a bit more robust.

Examples

Canvas vs. CanvasGroup

This example helps to compare Canvas and CanvasGroup. Resize the windows to see the difference.

from vanilla import Window
from mojo.canvas import Canvas, CanvasGroup
from mojo.drawingTools import rect

class CanvasExample:

    def __init__(self):
        self.w = Window((300, 300), title='Canvas', minSize=(200, 200))
        self.w.canvas = Canvas((0, 0, -0, -0), delegate=self)
        self.w.open()

    def draw(self):
        rect(10, 10, 100, 100)

class CanvasGroupExample:

    def __init__(self):
        self.w = Window((300, 300), title='CanvasGroup', minSize=(200, 200))
        self.w.canvas = CanvasGroup((0, 0, -0, -0), delegate=self)
        self.w.open()

    def draw(self):
        rect(10, 10, 100, 100)

CanvasExample()
CanvasGroupExample()

Canvas events

This example shows a simple canvas-based ‘brush’ tool using mouse and keyboard events.

from vanilla import Window
from mojo.canvas import Canvas
from mojo.drawingTools import *

class CanvasBrushExample:

    radius = 7
    color  = 0, 1, 0
    points = []
    shape  = oval

    def __init__(self):
        self.w = Window((300, 300), title='CanvasBrush', minSize=(200, 200))
        self.w.canvas = Canvas((10, 10, -10, -10), delegate=self)
        self.w.open()

    def acceptsMouseMoved(self):
        return True

    def draw(self):
        if not len(self.points):
            return
        view = self.w.canvas.getNSView()
        fill(*self.color)
        for pt in self.points:
            pos = view.convertPoint_fromView_(pt, None)
            save()
            translate(pos.x, pos.y)
            self.shape(-self.radius, -self.radius, self.radius * 2, self.radius * 2)
            restore()

    def mouseMoved(self, event):
        self.points = [event.locationInWindow()]
        self.w.canvas.update()

    def mouseDragged(self, event):
        self.points.append(event.locationInWindow())
        self.w.canvas.update()

    def mouseDown(self, event):
        self.radius *= 2
        self.color = 1, 0, 0, 0.2
        self.w.canvas.update()

    def mouseUp(self, event):
        self.radius *= 0.5
        self.color = 0, 1, 0
        self.points = []
        self.w.canvas.update()

    def keyDown(self, event):
        self.shape = rect

    def keyUp(self, event):
        self.shape = oval

CanvasBrushExample()

Using canvas with vanilla

In this example, the canvas reacts to an event triggered by another UI component (slider) in the same window. The slider callback calls the Canvas.update() method, which tells the canvas view to refresh itself.

from vanilla import Window, Slider
from mojo.canvas import Canvas
from mojo.drawingTools import rect

class CanvasUIExample:

    size = 50

    def __init__(self):
        self.w = Window((400, 400), minSize=(200, 200))
        self.w.slider = Slider((10, 5, -10, 22), value=self.size, callback=self.sliderCallback)
        self.w.canvas = Canvas((0, 30, -0, -0), delegate=self)
        self.w.open()

    def sliderCallback(self, sender):
        self.size = sender.get()
        self.w.canvas.update()

    def draw(self):
        rect(10, 10, self.size, self.size)

CanvasUIExample()

Canvas animation

This example shows an animation using Canvas.

The canvas view is continuously updated using a timer (NSTimer). The timer triggers a custom redraw callback, which updates the canvas view and schedules the next timer event to happen after a given time interval.

from AppKit import NSTimer
from vanilla import Window
from mojo.canvas import Canvas
from mojo.drawingTools import fill, rect
from mojo.tools import CallbackWrapper

class CanvasAnimationExample:

    width = 500
    height = 400
    pos = 0, 0
    size = 100
    steps = 5
    framesPerSecond = 30
    interval = framesPerSecond / 1000.
    addHorizontal = True
    directionX = 1
    directionY = 1

    def __init__(self):
        self.w = Window((self.width, self.height))
        self.w.canvas = Canvas((0, 0, self.width, self.height), delegate=self, canvasSize=(self.width, self.height))
        self.w.open()

        self._callback = CallbackWrapper(self.redraw)
        self.scheduleTimer()

    def scheduleTimer(self):
        if self.w.getNSWindow() is not None:
            self.trigger = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(self.interval, self._callback, "action:", None, False)

    def redraw(self, timer):
        self.w.canvas.update()
        self.scheduleTimer()

    def draw(self):
        x, y = self.pos

        if self.addHorizontal:
            x += self.steps * self.directionX
        else:
            y += self.steps * self.directionY

        if x > (self.width - self.size):
            self.addHorizontal = False
            x = self.width - self.size
            self.directionX *= -1
        elif x < 0:
            self.addHorizontal = False
            x = 0
            self.directionX *= -1

        if y > (self.height - self.size):
            self.addHorizontal = True
            y = self.height - self.size
            self.directionY *= -1
        elif y < 0:
            self.addHorizontal = True
            y = 0
            self.directionY *= -1

        fill(x / float(self.width), y / float(self.height), 1)
        rect(x, y, self.size, self.size)

        self.pos = x, y

CanvasAnimationExample()
Last edited on 01/09/2021