- 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 a 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.
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()
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()
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 for after a given time interval.
- explain what
CallbackWrapperdoes, and why it is needed here
- add a comment about
NSObjectsuper 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()