Edit or run this notebook

Pen Plotting with Pluto

By Paul Butler (@paulgb) for PlutoCon2021

A plotter is a robot that draws based on instructions you provide it.

There are basically three varieties of plotters:

  • Vintage machines (like the HP 7470A) lovingly restored by hobbiests.

  • DIY machines, often of the polargraph variety.

  • Modern pre-built desktop pen plotters, like the AxiDraw.

At a low level, plotters are controlled by a series of commands, the most important being “move to the location (x, y)”, “raise the pen”, and “lower the pen”. In addition to these low-level commands, there are tools out there for converting various vector graphics files into the raw commands.

In this talk, I will use a tiny library I wrote called PenPlots.jl which provides basic data structures for representing points and paths, as well as generating an SVG (scalable vector graphics) representation of one or more paths. Pleasingly, SVG is supported by my plotter driver of choice, Saxi, and is also supported directly in the browser for easy previews in Pluto.

I'll start by generating a few basic shapes to show you how PenPlots.jl works, and then I'll show how Pluto makes it easy to explore the parameter space of more intricate plots.

28.6 μs

Some Basics

Drawing a line

PenPlots.jl uses a Point data structure to represent an (x, y) coordinate on the plotting surface. These are screen coordinates rather than Cartesian coordinates, so the origin is in the upper-left hand corner and the y coordinate increases as we go down the plotting surface.

The Path data structure represents a path through two or more points. Its constructor takes a list of points.

Many types in PenPlots.jl, including Path, implement Base.show for text/html, so that the Pluto notebook will automatically preview them when they are the return value of a cell.

13.5 μs
4.2 μs

Drawing Two Lines

Multiple separate paths can be combined by putting them in a vector. In pen plotter terms, separate paths mean that the pen plotter will draw one path, then lift the pen, move to the beginning of the next path, and then draw it.

I'm avoiding using the terms “first path” and “second path” here. That's because even though we are using an ordered data structure (a vector), the paths will usually be reordered to reduce drawing time. This used to be a manual step (I wrote about it here), but recently it's become a built-in feature of the driver, as is the case with Saxi.

8.7 μs
6.8 μs

Drawing Many Lines

This technique plays really nicely with the map function to draw a bunch of lines.

5.6 μs
214 ms

Drawing A Circle

map can also be used within a Path to create a vector of points to visit. For example, we can create a circle by rotating the unit vector around the origin in tiny increments.

frac_rotation is a helper function to generate a rotation matrix from a fraction, where 1.0 represents a full rotation. PenPlots.jl provides it along with degree_rotation and radian_rotation.

Rotation matrices can be multiplied by Point, Path, and Vector{Path}. Rotations are always about the origin (0, 0).

6.9 μs
13.8 μs

Spiral

3.4 μs
154 μs

Step Size:

34.7 ms
35.0 μs

Step Size1:

Step Size2:

Rotation:

15.4 ms
159 ms

Representing an Image with Amplitude Modulation

In addition to abstract generative art, people often use real images as an input to plotters. Since we can only draw lines, we have to be creative with how to represent the image.

Fortunately, our brains are adept at recognizing images (especially faces) in patterns of light and dark, so we have a huge creative space of potential techniques to explore. As long as we make sure to put more ink on the page for the darker regions of the image, the image will appear.

The technique I'm going to use is an amplitude-modulating spiral. I'll draw a spiral just as before, but with a sine wave added to it. Then I'll overlay the spiral onto the image and vary the amplitude of the wave by the darkness of the pixel that each point in the spiral falls on to. As a result, the pen will cover more distance around the dark regions of the plot, and the source image will emerge.

6.1 μs
14.9 ms
9.2 ms
42.6 μs
4.4 ms

Now, we need a base image to draw. My wife Sarah is more photogenic than I am, so I'm using her image.

3.8 μs
rawimage
1.7 s

For simplicity, I'll stick to a single-pen plot, so I'm only interested in the lightness of each pixel. I'll extract that by converting to grayscale.

3.2 μs
grayimage
117 ms

I'm also applying a blur to the image. This way, every time we probe a pixel in the image we are probing a sample of the pixels in its neighborhood. It mitigates the problem of noise.

4.9 μs
60.1 μs
image
2.4 s

Now all we need is to map from points in the spiral to a pixel value in the image. The probe_val helper function does just that.

4.0 μs
probe_val (generic function with 1 method)
36.6 μs

I'll also use a slider for the scale of the image, which essentially acts as a way to ''crop'' the image inside the circle.

4.4 μs
52.7 μs
144 ms

The following are some topics that I did not cover in my talk, but have provided for your exploration and experimentation.

Recursion

Drawing a Koch Curve

9.5 μs
koch (generic function with 2 methods)
56.1 μs
126 ms

Drawing a Tree

3.3 μs

Angle 1:

Angle 2:

78.2 μs
tree (generic function with 2 methods)
50.5 μs
38.8 ms

Noise Spiral

Seed:

Big Period:

Small Period:

Outer Radius:

22.9 ms
noise_spiral (generic function with 1 method)
97.2 μs
277 ms
45.5 ms
55.7 s
Loading...i