This tutorial is designed as a smoother introduction to Punctual for beginners. Each section links to the corresponding sections in the guide.
Punctual is a language for live coding audio and visuals. It enables users to construct and modify networks of signal processors, such as oscillators and filters, on the fly.
Punctual is compatible with all major web browsers. Throughout this tutorial, we will use Punctual within Estuary, a collaborative live coding platform that supports various live-coding languages.
Setup
To follow along with the examples in this tutorial:
Open a new web browser tab and navigate to https://estuary.mcmaster.ca/. Select
Solo Mode
.In the
Terminal/Chat:
, type!presetview twocolumns
and press enter.Use the dropdown in each cell to select the language for that cell. Choose
Punctual
for the left cell andMiniTidal
for the right cell (which we will use in later examples).If needed, click the
?
marker and selectSettings
. Here, you can adjust settings likeResolution
(I personally prefer QHD),Frames per Second (FPS)
, orBrightness
to match your preferences. Click again on the?
to hide theSettings
.You can also change the
Theme
toDark
on the upper right dropdown list to increase the contrast between the code and the visuals.Other elements in the Estuary interface can be hidden as well. Click on the
Estuary
title to hide the upper text, on the bottom left to hide theTerminal
, and on the bottom right to hide the footer. Clicking these areas again will make these elements visible again.
Shapes
Let’s start by drawing some shapes:
circle [0,0] 0.25 >> add;
This code snippet draws a circle with its center at (0, 0) and a radius of 0.25. Notice how (0, 0) represents the center of the screen. You can change either coordinate to move the circle around:
circle [0.5, 0] 0.25 >> add;
circle [-0.5, 0] 0.25 >> add;
circle [0, 0.5] 0.25 >> add;
circle [0, -0.5] 0.25 >> add;
Note that moving more than 1 unit in any direction will position the center of the circle outside the screen.
For more information on, see the coordinate system and coordinates sections.
You can also change the radius to make the circle bigger or smaller:
circle [0.3, -0.5] 0.5 >> add;
circle [0.3, -0.5] 0.1 >> add;
Adding more coordinates in a single circle
instruction allows you to draw multiple circles at different positions:
circle [0.5,-0.8,0.2,0.3] 0.25 >> add;
In this code, we have a circle centered at (0.5, -0.8) and another one at (0.2, 0.3). We’ll understand why they are of different colors shortly.
Feel free to experiment by adding as many coordinates to the set as you like. Each additional pair of coordinates will create another circle at the specified position.
More shapes
There are some more shapes that can be directly drawn with Punctual.
Drawing vertical and horizontal lines is quite straightforward:
vline 0.2 0.01 >> add;
This code draws a vertical line at position x=0.2
with a width of 0.01.
hline (-0.3) 0.01 >> add;
This code draws a horizontal line at position y=−0.3
with a width of 0.01.
The first number in each instruction represents the position along the respective axis, and the second number represents the width of the line.
As with circles, you can add more numbers to a list to create more lines with a single instruction:
vline [-0.3,0.2,0.6] 0.1 >> add;
Now, Let’s draw some rectangles:
rect [-0.1,0.3] [0.4,0.1] >> add;
Here, we draw a rectangle with its center at (-0.1, 0.3), a width of 0.4 and a height of 0.1.
rect [-0.5,0,0.5,0] 0.4 >> add;
For more information, see the Shapes and textures section.
The add
output
We’ve been using add
to draw our shapes. add
allows to keep adding different shapes to the screen:
circle [0.3, -0.5] 0.1 >> add;
hline [0, 0.5, 0.8] 0.01 >> add;
rect [0.1,0.3,-0.5,-0.9] [0.3,0.2] >> add;
In this code, we draw a circle, three horizontal lines, and two rectangles, and they all stay visible on the screen simultaneously because of the add
output. This feature allows us to create complex compositions by combining different shapes and patterns.
When drawing multiple shapes in a single instruction (like the horizontal lines or the rectangles above), each one gets a different color.
In Punctual, a signal can have many channels. In our last example, the circle
has one channel, the hline
has 3 channels, corresponding to the 3 lines, and the rect
has 2 channels, corresponding to the 2 rectangles.
The add
output also has channels. Specifically, it has 3 channels representing red, green, and blue components.
When sending a signal to the add
output, each channel of the signal is matched with the corresponding channel of the output. So, for the hline
instruction, the first line goes to the first channel (red), the second line to the second channel (green), and the third line to the third channel (blue).
When the number of channels of the signal and the output is not the same, the last channel of the signal is repeated to make them match. So, the first rectangle goes to red, and the second rectangle goes to green and blue, which results in cyan.
The circle has only one channel, so it is repeated for the red, green, and blue output channels, resulting in a white circle.
For more information, see the Output notations section.
Comments
Using comments in code is a great way to temporarily disable certain instructions without deleting them. In Punctual, lines starting with --
are treated as comments and are ignored during execution.
For example, consider the following code snippet:
circle 0 0.1 >> add;
-- hline 0.5 0.1 >> add;
In this code, only the circle
instruction will be executed because the hline
instruction is commented out.
For more information, see Getting Started section.
Variables
Using variables can help simplify code and make it more readable. The <<
operator is used to assign the result of an expression to a variable. Here’s an example:
c << circle 0 0.1;
c >> add;
This code is equivalent to:
circle 0 0.1 >> add;
Storing the circle expression in the c
variable allows you to reuse it multiple times if needed. This not only reduces repetition but also makes the code more concise and easier to understand. Additionally, it can reduce the need for parentheses in complex expressions.
For more information, see the Bindings section.
Colors
As seen before, color is determined by the signal received by each channel in the add
output, with the first channel being the red component, the second the green, and the third the blue.
We can create any color this way, just by multiplying an expression by the desired color:
c << circle 0 0.1;
c * [0.5, 0.2, 0.8] >> add;
Each color component can take a value from 0 to 1.
The mono
function in Punctual is quite handy for converting a multi-channel signal into a single-channel one. This is particularly useful when you want to avoid coloring a set of shapes when sending them to add
.
For instance, consider these examples:
l << vline [-0.3,0.2,0.6] 0.1;
l >> add;
In this case, each line in the vline
command will have a different color assigned. However, by using mono
, you can ensure they all are white:
l << vline [-0.3,0.2,0.6] 0.1;
mono l >> add;
Additionally, you can send a color directly to the output without using any shape:
[0.8, 0.4, 0] >> add;
0.5 >> add;
Since add
adds colors, you can leverage this behavior to create dark shapes over a colored background:
c << circle 0 0.8;
[0.5,0,0] >> add;
c*(-1) >> add;
In the last example, the first add
command tints the background with a red color, and the second one creates a dark circle by multiplying the circle shape c
by -1.
For more information, see the Colors and Combining channels sections.
Parentheses and dollars
Let’s revisit the previous mono
example:
l << vline [-0.3,0.2,0.6] 0.1;
mono l >> add;
Here, mono
is applied to the whole content of l
, the three lines. If we weren’t using a variable, we could write an equivalent expression like so:
mono (vline [-0.3,0.2,0.6] 0.1) >> add;
The parentheses are necessary, as otherwise Punctual wouldn’t know that [-0.3,0.2,0.6]
and 0.1
are arguments for vline
and not mono
.
Another equivalent way to write this is by using a dollar sign instead of parentheses:
mono $ vline [-0.3,0.2,0.6] 0.1 >> add;
A dollar sign means that everything following it should be treated as a unit, in this case, as a single parameter for mono
. Using $
is more concise, as it reduces the need for additional parentheses, especially in longer or more complex expressions.
For more information, see the Notes on Haskell section.
Correcting distortion with fit
When drawing shapes, you might have observed that our circles are not perfectly circular and our squares are not perfectly square. Punctual uses a coordinate system where the visible screen ranges from -1 to 1 on each axis. However, since our browser windows are usually not square, the fragments or points in the image are often wider than they are tall.
The fit
function allows us to change the aspect ratio of our fragments. Specifically, fit 1
will force fragments to be completely square, making our shapes visually correct:
fit 1 $ circle 0 0.2 >> add;
fit 1 $ rect 0.5 0.2 >> add;
Note that when applying fit 1
, we can’t guarantee anymore that our visible coordinates are from -1 to 1 on each axis:
fit 1 $ vline 1.5 0.01 >> add;
See the Coordinates section for more information on fit
.
Fragment coordinates
So far, we have used fixed numbers for our expressions. There are some expressions that are fragment-dependent, meaning they have a different value for each point on the screen.
fx
is the horizontal coordinate of the fragment.fy
is the vertical coordinate.fr
is the radius, the distance from[0,0]
.
These functions have many useful applications, including the creation of color variations:
fx >> add;
[fr, 0, 0] >> add;
[fy, 1-fx, fr] >> add;
c << circle 0 1;
c * [0.3, 0, fr] >> add;
See the Fragments and Coordinates sections for more information on fragment coordinates.
Scaling values
Let’s revisit this gradient example:
fx >> add;
You’ll notice that the left half of the screen is completely black, and the white gradient affects only the right half. This is because fx
takes values from -1 to 1, while color values range from 0 to 1.
To address this common situation, Punctual provides two functions to convert between the -1 to 1 range and the 0 to 1 range:
unipolar
rescales a number from [-1,1] to [0,1]bipolar
rescales a number from [0,1] to [-1,1]:
unipolar fx >> add;
See the Scaling values section for more ways to rescale values.
Oscillators
Our patterns have been static so far. One straightforward way to animate them is by using oscillators.
Oscillators are functions that generate varying values over time. In Punctual, oscillators operate within a range from -1 to 1. When using an oscillator, you specify its frequency, which determines how many cycles it completes per second.
For instance, osc 1
creates an oscillator that oscillates from -1 to 1 and back to -1 once every second. Typically, we use low frequencies for our oscillators. For example, osc 0.1
takes 10 seconds to complete one full cycle.
We can use an oscillator at any place where a number is expected:
vline (osc 0.2) 0.02 >> add;
This code creates a vertical line that oscillates in position with a frequency of 0.2.
vline (osc [0.11,0.15,0.19]) 0.02 >> add;
Three vertical lines oscillate at frequencies of 0.11, 0.15, and 0.19, respectively.
circle 0 (unipolar $ osc 0.13) >> add;
The radius of the circle oscillates from 0 to 1 at a frequency of 0.13.
circle [0,0.3,0.3,-0.3,-0.3,-0.3] (unipolar $ osc 0.13) >> add;
Three circles with different positions have their radius oscillate from 0 to 1 at a frequency of 0.13.
[unipolar $ osc 0.13, 0, unipolar $ osc 0.15] >> add;
This code creates a varying color. As the frequencies of the two oscillators are different, this will produce a lot of different colors over time.
osc fr >> add;
The amount of white color applied to each fragment oscillates at a different frequency. This creates an interesting and evolving pattern.
hline (osc $ fx/10) 0.01 >> add;
Here, each point of a horizontal line moves up and down at a frequency that depends of its horizontal position. This creates a vertically symmetric pattern of moving little lines.
[0,0.3,0.1]*hline 0 (0.01*osc fr) >> add;
This code follows a similar idea, but now the width of the line at each point oscillates at a frequency that depends on its distance from the origin.
For more information and other types of oscillators, see the Oscillators section.
Transformations
There are four main transformations you can apply to any pattern in Punctual: move
, spin
, tile
and zoom
.
move
move
needs the displacement on the horizontal and vertical axis:
move [0.1, -0.3] $ rect 0 0.1 >> add;
Moves a rectangle 0.1 units to the right and 0.3 units upwards.
move 0.1 $ rect 0 0.1 >> add;
Moves a rectangle 0.1 units both to the right and upwards.
r << rect 0 0.1;
move [0, 0, 0.1, -0.3, -0.8, 0.7] r >> add;
Applies multiple displacements to a rectangle, creating three copies of the original at different positions.
r << rect 0 0.1;
move (osc [0.19,0.17]) r >> add;
Oscillates the position of a rectangle horizontally and vertically.
r << rect 0 0.1;
o << osc [0.19, 0.17]*osc [0.14, 0.15];
move o r >> add;
Uses combined oscillators to create complex oscillating movements for a rectangle.
r << rect 0 0.4;
move [fy,0] r >> add;
Moves a rectangle horizontally based on the fragment’s vertical coordinate. This effectively skews the rectangle.
r << rect (0.5*osc [0.08, 0.04]) 0.4;
move [fy/fr,0] r >> add;
The original moving rectangle is deformed by a move
operation that displaces each fragment by a different amount, based on its vertical coordinate and radius.
spin
spin
receives the rotation amount, with 2 representing a full turn.
h << hline 0.5 0.01;
spin 0.5 h >> add;
Rotates a horizontal line by 180 degrees, clockwise.
h << hline 0 0.01;
spin [0, 1/3, 2/3] h >> add;
Rotates three horizontal lines by 0, 120, and 240 degrees respectively.
h << hline 0 0.01;
o << osc $ [3,7,9]/100;
spin o h >> add;
Rotates three horizontal lines at different frequencies, creating a spinning effect.
r << spin (osc 0.019) $ unipolar fx;
b << unipolar $ osc 0.013;
[r, 0, b] >> add;
Creates a color pattern combining a rotating red gradient and a pulsating blue.
c << circle (osc [0.18,0.16]) 0.1;
spin [0, 1/2, 1, 3/2] c >> add;
Creates four copies of a moving circle, each one rotated by a different amount.
l << hline 0 0.1;
spin fr l >> add;
Spins a horizontal line based on the radius of each segment, curving the line.
tile
tile
repeats the pattern the specified number of times on the x and y axes.
r << rect 0 0.3;
tile [5,3] r >> add;
Tiles a rectangle 5 times along the horizontal axis and 3 times along the vertical axis.
r << rect 0 0.3;
tile 4 r >> add;
Tile a rectangle 4 times on each axis, creating a 4x4 grid.
c << circle 0 0.5;
fit 1 $ move [2*osc 0.1,0] $ tile 1 c >> add;
The circle moves horizontally following an oscillator that oscillates between -2 and 2. tile 1
tiles the pattern, causing the circle to appear on the other side of the screen when it reaches the -1 or 1 coordinate.
r << rect 0 0.3;
tile (4+osc [0.13, 0.19]) r >> add;
The rectangle is now repeated a variable number of times over time, between 3 and 5 in each axis.
r << rect 0 0.3;
tile (8*unipolar fy) r >> add;
By making tile
depend on the vertical coordinate of each fragment, a kind of 3D effect is created.
tile [8,4] $ [unipolar fx, unipolar fy, 0] >> add;
A color gradient is repeated in a grid.
r << rect 0 [0.3, 0.3, 0.6, 0.6];
tile [4,2,8,4,2,1] r >> add;
Creates two squares, one inside the other, and then generates three copies of the pair of rectangles, each one tiled by a different amount, creating a pattern.
r << rect 0 [0.3, 0.3, 0.6, 0.6];
s << spin (osc [0.1,-0.1]) r ;
tile [4,2,8,4,2,1] s >> add;
Modifies the last pattern by spinning the original rectangle. As two oscillators are used for the spin
, the rectangles are duplicated.
zoom
zoom
zooms in or out of the pattern. It needs the zooming amount on the x and y axes, with 1 being the original size.
c << circle 0 0.4;
zoom [0.6, 2] c >> add;
Zooms the circle, making it narrower along the x-axis and taller along the y-axis.
c << circle 0 0.4;
zoom 2 c >> add;
Zooms the circle, making it twice as large in both dimensions.
c << circle 0 1;
zoom (osc 0.03) c >> add;
Creates a pulsating effect on the circle’s size using an oscillator.
c << circle 0 1;
zoom [1,1,1/2,1/2] c >> add;
Zooms the circle by distinct amounts, creating two copies of it.
c << circle 0 1;
o << osc [0.13, 0.13, 0.18, 0.18];
zoom o c >> add;
A variant of the last example, now each circle’s copy is zoomed in and out periodically.
See Geometric Transformations for more examples and transformations.
Feedback
Feedback involves using the last image frame to build the current one. There are many ways to use feedback, but a simple and very effective one is to add a slightly attenuated version of the last frame to a changing pattern.
fb fxy
is the last frame without any other changes.
o << osc [0.17, 0.19];
circle o 0.2 >> add;
0.97*fb fxy >> add;
Using feedback this way can create interesting trails and persistence-of-vision effects in animations. Feel free to explore this concept with other patterns or combinations!
See Playing with feedback for other ways to use feedback in your patterns.
Audio reactive visuals
The easiest way to create audio-reactive visuals in Punctual is by using the lo
, mid
, and hi
functions. They represent the power of the low, middle, and high frequencies that are playing at every moment inside Estuary.
Their counterparts, ilo
, imid
, and ihi
, work in the same way but use external audio captured by the computer microphone as their input.
To experiment with the first group, you’ll need to create an audio pattern inside Estuary. In the second cell in Estuary, select MiniTidal
as the language, and then write and execute the following code:
s "bass arpy linnhats:3"
This code will repeatedly play three sounds: one bass sound composed mainly of low frequencies, one note composed of middle frequencies, and a hi-hat sound composed of high frequencies.
In the Punctual cell, let’s explore how we can use lo
, mid
, and hi
:
circle [-0.5, lo] 0.1 >> add;
circle [0, mid] 0.1 >> add;
circle [0.5, hi] 0.1 >> add;
Moves circles vertically based on the power of low, middle, and high frequencies in the audio.
c << circle [0.5, 0] 0.2;
spin [lo,mid,hi] c >> add;
Spins circles based on the power of low, middle, and high frequencies in the audio.
vline (bipolar lo) 0.01 >> add;
Moves a vertical line left and right based on the power of low frequencies in the audio.
l << hline 0 0.1;
tile 4 $ spin (fr*mid) $ tile 4 l >> add;
0.9 * fb fxy >> add;
Creates an animated grid of horizontal lines that spin based on the power of middle frequencies in the audio, with feedback applied for visual continuity.
See the Audio reactive visuals section to learn about other ways to synchronize audio and visuals.
Conclusion
“How do you create cute patterns with Punctual?” a friend of mine asked me once.
In this guide, you’ll find a lot of advanced techniques and creative ideas I’ve been exploring for more than a year.
However, there is no need to learn all of it at once. In fact, you can already create awesome patterns with just the information presented in this tutorial.
This is a list of ingredients I find useful when creating patterns with Punctual:
- Start simple. It’s usually enough to begin with a circle or a line. Complexity arises through the composition of multiple simple steps. You can always go back and add complexity to an existing shape later.
- Build a pattern step by step. Especially when you are starting to use Punctual, it’s better to organize your pattern in small steps and ensure they are well written. This way, it’s easier to detect a syntax error in the code.
- Use variables. You can build a pattern by creating long lines of code, but variables make it easier to modify, understand, and fix. I often spend a few seconds during my performances rearranging my code and simplifying things before continuing.
- Add movement. This is one of the first things you want to do in any pattern. Even a simple circle moving across the screen is much more interesting than a static scene.
- Use symmetry and repetition. That’s why they are called patterns. Punctual makes this simple, as adding numbers to any list will create copies of the existing shapes.
- Create irregularity. Symmetry and repetition are cool, but adding irregularity on top of those is what makes a pattern really unique. Use any transformation that depends on one of the fragment’s coordinates (
fx
,fy
,fr
) to break the homogeneity of a pattern. - Use color. Using
mono
and multiplying by a color is very easy to tint your patterns. Changing a color can completely modify a pattern’s vibe. - Feedback is your friend. Most patterns benefit from adding a good amount of feedback. Just keep in mind that feedback can result in very bright visuals, so add it smoothly and be ready to darken your pattern when necessary.
Next is a beautiful pattern built with these ingredients in mind. It only uses the functions and ideas explained in this tutorial.
Step 1: Start simple
hline 0.3 0.002 >> add;
Step 2: Add movement
l << hline 0.3 0.002;
spin (saw 0.2) l >> add;
Step 3: Add symmetry
l << hline 0.3 0.002;
sp << spin (saw 0.2) l;
mono $ spin [0,0.5,1,1.5] sp >> add;
Step 4: Feedback is your friend
l << hline 0.3 0.002;
sp << spin (saw 0.2) l;
mono $ spin [0,0.5,1,1.5] sp >> add;
0.97 * fb fxy >> add;
Step 5: Use color
l << hline 0.3 0.002;
sp << spin (saw 0.2) l;
pat << mono $ spin [0,0.5,1,1.5] sp;
color << [1, 0.1, 0];
color * pat >> add;
0.97 * fb fxy >> add;
Step 6: Create irregularity
l << hline 0.3 0.002;
sp << spin (saw 0.2) l;
mv << move fr $ tile 1 sp;
pat << mono $ spin [0,0.5,1,1.5] mv;
color << [1, 0.1, 0];
color * pat >> add;
0.97 * fb fxy >> add;
Note the addition of tile 1
before applying move fr
in this step. Without it, the lines get out of the screen from time to time. As explained in a prior example, tile 1
tiles a pattern, so when the line leaves the screen from one side, it appears on the opposite site.
Step 7: Add repetition
l << hline 0.3 0.002;
sp << spin (saw 0.2) l;
mv << move fr $ tile 1 sp;
t << tile [8, 4] mv;
pat << mono $ spin [0,0.5,1,1.5] t;
color << [1, 0.1, 0];
color * pat >> add;
0.97 * fb fxy >> add;
Step 8: Small adjustments
l << hline 0.3 0.02;
sp << spin (saw 0.2) l;
mv << move fr $ tile 1 sp;
t << tile [8, 4] mv;
pat << mono $ spin [0,0.5,1,1.5] t;
color << [0.2, 0.02, 0];
color * pat >> add;
0.97 * fb fxy >> add;
Here, we increase the line width to make it more visible, but divide the color by 5 to compensate for the high feedback, while maintaining the same red-orange color palette from before.