Canvas and CanvasGroup ↩
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()