The py5 library is designed to work well with Jupyter tools, including Jupyter notebook. There are included IPython magics that are useful for supporting your development and documentation efforts as well as your creative endeavors. Used well, they can greatly enhance your programming workflow.
To use py5 in a notebook, first import the library:
import py5_tools import py5
Before continuing, it is worth pointing out that you can access the docstrings for any py5 functions by appending a
? to the end of the function, or with the builtin
? which something like
py5.rect? temporarily displays the documentation at the bottom of the notebook window. The builtin
help function puts the result in the notebook cell.
Help on function rect in module py5: rect(*args) Draws a rectangle to the screen. Underlying Java method: PApplet.rect Methods ------- You can use any of the following signatures: * rect(a: float, b: float, c: float, d: float, /) -> None * rect(a: float, b: float, c: float, d: float, r: float, /) -> None * rect(a: float, b: float, c: float, d: float, tl: float, tr: float, br: float, bl: float, /) -> None Parameters ---------- a: float x-coordinate of the rectangle by default b: float y-coordinate of the rectangle by default bl: float radius for bottom-left corner br: float radius for bottom-right corner c: float width of the rectangle by default d: float height of the rectangle by default r: float radii for all four corners tl: float radius for top-left corner tr: float radius for top-right corner Notes ----- Draws a rectangle to the screen. A rectangle is a four-sided shape with every angle at ninety degrees. By default, the first two parameters set the location of the upper-left corner, the third sets the width, and the fourth sets the height. The way these parameters are interpreted, however, may be changed with the ``rect_mode()`` function. To draw a rounded rectangle, add a fifth parameter, which is used as the radius value for all four corners. To use a different radius value for each corner, include eight parameters. When using eight parameters, the latter four set the radius of the arc at each corner separately, starting with the top-left corner and moving clockwise around the rectangle.
Pause to take a moment to appreciate that documentation, with its proper type signatures and explicit variable types. You'll also notice the content is analogous what is in py5's rect() reference documentation. Producing thorough and coordinated py5 docstrings and reference documentation like this took an enormous amount of work.
You'll notice that all the usual Jupyter niceties such as tab completion work for py5. There are also Python typehints for all py5 objects.
These "magic" commands are like extra functionality added to what Python and Jupyter notebooks can already do.
The py5 magics all start with "py5". The cell magics are:
As before, documentation for each is available by appending a
?, such as when you type
%%py5draw? in an empty cell. The builtin
help function does not work with IPython magics.
See below for demonstrations of what each does.
The below example creates a static image with some simple shapes.
The first line in the cell,
%%py5draw 300 200, is not Python code. Instead, it is a command to Jupyter itself, instructing it to send the rest of the cell's contents to a special py5 draw function.
Observe that there are no defined
%%py5draw 300 200 # make the background light gray py5.background(240) # draw a red square py5.fill(255, 0, 0) py5.rect_mode(py5.CENTER) py5.rect(170, 80, 100, 100) # add a thick green line py5.stroke(0, 255, 0) py5.stroke_weight(15) py5.line(40, 30, 220, 180)
The code can access variables and functions defined in other non-magic notebook cells. This is especially useful when the code leverages py5 functionality.
For example, the below function sets the fill color to a random color.
def pick_random_fill(): py5.fill(py5.random(255), py5.random(255), py5.random(255))
The below example uses
pick_random_fill to draw randomly colored rectangles. This example is a bit contrived, but you do see how the code below can call a function defined elsewhere.
%%py5draw 300 200 py5.background(240) py5.rect_mode(py5.CENTER) for i in range(100): pick_random_fill() py5.rect(py5.random(py5.width), py5.random(py5.height), 10, 10)
pick_random_fill function can be reused again elsewhere in this notebook.
By default, any new functions and variables defined in that
%%py5draw cell are not available outside of the cell. However, you can explicitly change this. See below for further discussion.
Saving to a File¶
If you like you can save the generated image to a file with the
-f parameter, like so:
%%py5draw 300 200 -f images/jupyter_notebooks/simple_example.png py5.background(240) py5.rect_mode(py5.CENTER) for i in range(100): pick_random_fill() py5.rect(py5.random(py5.width), py5.random(py5.height), 10, 10)
PNG file written to images/jupyter_notebooks/simple_example.png
Now there's an image on my computer located at
images/jupyter_notebooks/simple_example.png. I can embed that in this notebook using markdown.
%%py5draw 300 200 -r P2D py5.background(240) py5.rect_mode(py5.CENTER) for i in range(100): pick_random_fill() py5.rect(py5.random(py5.width), py5.random(py5.height), 10, 10)
When that cell runs, a py5 window is quickly opened and closed. For whatever reason, the Processing's OpenGL renderers cannot draw to an invisible window (but I would be delighted to be proven wrong about that).
%%py5draw examples in this notebook used a special
HIDDEN renderer based on the default
JAVA2D renderer that does not need to open a window. That
HIDDEN renderer was created just for this purpose. Despite my best efforts, I couldn't create similar renderers based on the OpenGL renderers
The 3D renderer also works:
%%py5draw 300 300 -r P3D py5.background(240) N = 10 for i in range(N): py5.push_matrix() pick_random_fill() py5.translate(i * py5.width / N, i * py5.width / N, i * 20 - 200) py5.box(40) py5.pop_matrix()
To create SVG images, use the
As before, the result can be saved to a file with the
%%py5drawsvg 300 200 -f /tmp/test.svg py5.background(240) py5.rect_mode(py5.CENTER) for i in range(100): pick_random_fill() py5.rect(py5.random(py5.width), py5.random(py5.height), 10, 10)
SVG drawing written to /tmp/test.svg
Write to PDF files using
%%py5drawpdf. Since Jupyter notebook does not support embedded PDF files, writing the output to a file is not optional.
%%py5drawpdf 300 200 /tmp/simple_example.pdf py5.background(240) py5.rect_mode(py5.CENTER) for i in range(100): pick_random_fill() py5.rect(py5.random(py5.width), py5.random(py5.height), 10, 10)
PDF written to /tmp/simple_example.pdf
Write 3D objects to DXF files with
%%py5drawdxf. This probably won't be a popular choice, but maybe somebody will appreciate it.
%%py5drawdxf 200 200 /tmp/test.dxf py5.translate(py5.width / 2, py5.height / 2) py5.rotate_x(0.4) py5.rotate_y(0.8) py5.box(80)
DXF written to /tmp/test.dxf
0 SECTION 2 ENTITIES 0 3DFACE 8 0 10 -56.562515
By default, new variables defined inside cell magics such as
%%py5draw cannot be accessed elsewhere in the notebook.
Consider the below example. It creates new variables
random_y to store the location of the square.
%%py5draw 300 200 py5.background(240) py5.rect_mode(py5.CENTER) random_x = py5.random(py5.width) random_y = py5.random(py5.height) py5.rect(random_x, random_y, 50, 50)
random_y are not accessible outside of that cell:
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-15-495f30123407> in <module> ----> 1 random_x, random_y NameError: name 'random_x' is not defined
This behavior is by design.
Consider that the py5 library is using the Processing library to create these graphics. Builtin Processing objects such as PImage or PGraphics are designed to be associated with one and only one Processing sketch. Py5 can let users write code to that subverts these assumptions, and as a result use Processing in a way that is completely different from how it was designed to be used. Sometimes this can be beneficial, but other times it will cause unexpected errors.
If you understand the risks, or if you are working with non-Processing objects (as is the case for
random_y in the above example), you have two options available to you.
First, you can add the
global keyword to your code:
%%py5draw 300 200 py5.background(240) py5.rect_mode(py5.CENTER) global random_x global random_y random_x = py5.random(py5.width) random_y = py5.random(py5.height) py5.rect(random_x, random_y, 50, 50)
Now we can get the position of the square:
Alternatively, you can use the
--unsafe parameter. This is a blanket statement that mixes the cell magic's namespace with the notebook namespace. As the name implies, you can cause problems for yourself by using this.
%%py5draw 300 200 --unsafe py5.background(240) py5.rect_mode(py5.CENTER) random_x = py5.random(py5.width) random_y = py5.random(py5.height) py5.rect(random_x, random_y, 50, 50)
Of course, there's more to py5 than static sketches. The py5 magics will support animated sketches as well.
Note that starting your development process with static sketches is a great way to write py5 code. For example, you can quickly develop functions for parts of your creation and test them out with static sketches, and later use those same functions in your final animation.
Let's create a simple example. The below
setup function will tell py5 to create a 500 by 400 sketch window. It will set the background color and set the rect mode.
def setup(): py5.size(500, 400, py5.P2D) py5.background(240) py5.rect_mode(py5.CENTER)
And finally, the
draw function to draw squares at the current mouse position.
def draw(): pick_random_fill() py5.rect(py5.mouse_x, py5.mouse_y, 10, 10)
To run the sketch, use the run_sketch() method. It will pull out the
draw functions from the notebook's namespace and put them together in a sketch.
py5.run_sketch() print('the sketch is running!')
the sketch is running!
A new window opens for the running sketch.
By default, the run_sketch() method returns right away, as illustrated by the
sketch_snapshot = py5_tools.screenshot() sketch_snapshot
py5_tools.save_frames('/tmp/testframes/', start=0, limit=10)
saving frame 10/10
['/tmp/testframes/frame_0000.png', '/tmp/testframes/frame_0001.png', '/tmp/testframes/frame_0002.png', '/tmp/testframes/frame_0003.png', '/tmp/testframes/frame_0004.png', '/tmp/testframes/frame_0005.png', '/tmp/testframes/frame_0006.png', '/tmp/testframes/frame_0007.png', '/tmp/testframes/frame_0008.png', '/tmp/testframes/frame_0009.png']
frames = py5_tools.capture_frames(10, period=1)
collecting frame 10/10
The last magic creates animated GIFs from your sketch. Everybody loves animated GIFs.
py5_tools.animated_gif('images/jupyter_notebooks/simple_example.gif', 10, 1, 0.5)
collecting frame 10/10
The animated GIF can then be embedded in a notebook markdown cell.
Question: if the user's
draw method contains
This would be a simple question if the run_sketch() method did not return right away (which you could achieve by passing the parameter
block=False). The print statements would always go into the output of the cell with the call to that method. When the sketch exits, the output for that cell would be complete.
By default, the run_sketch() method does return right away when called from a Jupyter notebook. The print statements will still go to the output of the cell with the call to that method, but when you move on to the next cell, the print statements will start to appear there instead. Why? This has to do with the inner workings of Jupyter notebooks. It gets weirder if you delete the cell that is currently receiving the output. If that happens, the print output will disappear completely.
This is less than perfect, and might frustrate users who like to debug their code with print statements. Instead, use the println() method. It will route all print statements to the output of the cell that made the call to run_sketch(). The functionality of println() can be customized with set_println_stream().
If instead you want to send messages to the Jupyter notebook log, you can always do so like this:
shell = get_ipython() shell.log.critical('test message')
In the terminal I used to run
jupyter notebook, I see this message:
[IPKernelApp] CRITICAL | test message
This is a more reliable debugging technique.