Draft

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 a 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

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 + UI interaction

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 to refresh itself.

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

class CanvasUIExample:

    def __init__(self):
        self.size = 50
        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 for after a given time interval.

  • explain what CallbackWrapper does, and why it is needed here
  • add a comment about NSObject super long method names
from AppKit import NSTimer
from vanilla import Window
from mojo.canvas import Canvas
from mojo.drawingTools import fill, rect
from lib.baseObjects import CallbackWrapper

class CanvasAnimationExample(object):

    def __init__(self):
        self.pos = 0, 0
        self.width = 500
        self.height = 400
        self.size = 100
        self.steps = 5
        self.addHorizontal = True
        self.directionX = 1
        self.directionY = 1
        framesPerSecond = 30
        self.interval = framesPerSecond / 1000.

        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 30/03/2018