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.
- How canvas works
- Types of canvas
- Canvas vs. DrawView
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
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
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.
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
A vanillaScrollView with a canvas view inside.
The edges of the drawing have scrollbars. The canvas size is limited.
A vanillaGroup with a canvas view inside.
The edges of the drawing are bound to the edges of the window.
Canvas vs. DrawView
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.
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.
Canvas vs. CanvasGroup
This example helps to compare
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()
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()
This example shows an animation using
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()