Rescaling values is a common operation in Punctual. You’ll often want to relate coordinates to colors, oscillator outputs to color, distance from a point to a rotation transformation, etc.
Common ranges
It may be useful to know the ranges of common variables and arguments in Punctual:
Variable/argument | Minimum | Maximum | Comments |
---|---|---|---|
fx ,fy | -1 | 1 | Can change if using for example fit . |
fr | 0 | √2~=1.414 | |
ft | -π | π | |
red, green, blue, alpha | 0 | 1 | |
hue, saturation, value | 0 | 1 | |
oscillator’s output | -1 | 1 | |
oscillator’s frequency | -∞ | ∞ | But usually low values, from -1 to 1 |
spin ’s argument | -1 | 1 | First lap, any other value is accepted |
lo , mid , hi , ilo , imid , ihi | 0 | 1 | |
fft /ifft argument | 0 | 1 | |
fft /ifft output | 0 | 1 |
Punctual offers a bunch of functions and operators specifically design to avoid doing the maths on the fly when transforming one range into another.
unipolar
, bipolar
unipolar
rescales a [-1, 1] range into [0,1]. This is equivalent to applying the formula (x+1)/2
to the input number x
.
bipolar
rescales a [0,1] range into [-1,1]. This is the same as applying the formula 2*x-1
to the input number x
.
~~
[min] ~~ [max] $ [input]
The ~~
operator rescales a bipolar signal to the specified range, specified as a min
and a max
value.
~~:
[min] ~~: [max] $ [input]
Same as ~~
, but ~~
works in a combinatorial way, while ~~:
works in pair-wise way.
See the difference:
o << [0.1, 0.3] ~~ 0.8 $ osc [0.03, 0.07];
hline o 0.004 >> add;
o << [0.1, 0.3] ~~: 0.8 $ osc [0.03, 0.07];
hline o 0.004 >> add;
In the first version, there are four lines as the result of combining [0.1, 0.3] with [0.03, 0.07] in all possible ways. In the second version, there are only two lines, as 0.1 is matched with 0.03 and 0.3 with 0.07.
+-
[centre] +- [offsetRatio] $ [input]
The +-
operator rescales a bipolar signal to the specified range, just like ~~
, but the range is specified as a centre
and an offsetRatio
.
The offsetRatio
indicates the proportion of variation from the center for the new values.
So, for example:
o << 0.4 +- 0.5 $ osc 0.1;
hline [0.2, 0.6, o] 0.004 >> add;
The blue line will move between the red (at 0.2) and the green (at 0.6) lines. This represents a variation of +-50% from 0.4.
+-:
[center] +-: [offsetRatio] $ [input]
This is the same as +-:
but in a pair-wise way.
linlin
linlin [min1, max1] [min2, max2] [input]
input graph is linearly scaled such that the range (min1,max1)
becomes the range (min2,max2)
. This is useful when you want to rescale a range that is not [0,1] or [-1,1].
In the next example, we use a combination of these functions and operators.
co
defines the color. Its red and blue components depend on the fragment’s distance to the origen, which takes values from 0 to approximately 1.4. We are using linlin
to rescale this value to the desired color component. Note how the red component is stronger as the fragment is closer to the origin, while the blue component is stronger when the fragment is far away from the origin.
ci
defines a set of three circles that are spinning around the center. All three circles come from a single one, whose center is defined as [bipolar imid, 0]
. That means that its x coordinate moves from -1 (when there is no sound), to 1 (when the middle frequencies are at their maximum). Its radius is defined as linlin [0,1] [0.1,0.4] ilo
. Here, we are rescaling the low frequencies intensity to the [0,1,0.4]
range.
The example also uses feedback. Each frame, the feedback is zoomed in or out, depending on the middle frequencies of the incoming sound. Here, we rescale the [0,1]
range from imid
first to [-1,1]
using bipolar
and then to [0.8, 1.2]
using the +-
operator.
co << [linlin [0,1.4] [1,0.2] fr,0,linlin [0,1.4] [0,1] fr,0.8];
ci << mono $ spin (saw [0.1,-0.2,0.3]) $ circle [bipolar imid, 0] (linlin [0,1] [0.1,0.4] ilo);
ci*co >> blend;
zoom (1 +- 0.2 $ bipolar imid) $ fb fxy >> add;
linlinp
linlinp [min1, max1] [min2, max2] [input]
: This function operates as the pairwise version oflinlin
.
To illustrate the difference, consider these two expressions:
dy << linlin [0,1,0.5,0.9] [2/5, 4/5, (-1)/5, 1/5] (ifft $ abs [fx, fx/3]);
hline dy 0.004 >> add;
dy << linlinp [0,1,0.5,0.9] [2/5, 4/5, (-1)/5, 1/5] (ifft $ abs [fx, fx/3]);
hline dy 0.004 >> add;
In the first expression using linlin
, there are two sets each of origin and destination ranges, along with two input signals. This results in 8 output channels. However, in the second expression using linlinp
, despite the same input structure, only two output channels are generated. This highlights the pairwise nature of linlinp
, where each input range pair corresponds to a single output pair, reducing the output channels to match the pairs in the input.
clip
clip [min, max] [input]
: clip input values into the specified range. If a value is less thanmin
it will becomemin
, and if it’s greater thanmax
it will becomemax
.
In the first example, the clip function is used to confine the fx
values within the range [−0.2,0.2]
. This ensures that the y coordinate of each fragment remains within this range, resulting in a line that follows the diagonal in the central part, but is constrained vertically on the sides:
x << clip [-0.2, 0.2] fx;
hline x 0.004 >> add;
In the second example, clip
is applied to mix two images. The resulting image takes the second image as a model, but each component of each fragment cannot exceed the corresponding component of the red channel of the first image. This effectively limits the intensity of each color channel in the second image to match or be lower than the intensity of the red channel in the first image:
i1 << img "https://upload.wikimedia.org/wikipedia/commons/a/a3/Neillia_affinis%2C_trosspirea._23-05-2022_%28actm.%29.jpg";
i2 << img "https://upload.wikimedia.org/wikipedia/commons/5/58/Indian_tightrope_girl_performing_folk_art_Baunsa_Rani_%28Crop_2%29.jpg";
clip [0, rgbr i1] i2 >> add;
Note that using clip
is a shorthand for using max
and min
. For example, the last sentence in the previous example could be rewritten as max 0 (min (rgbr i1) i2) >> add;
. In this specific example, since any pixel in the image already has a value greater than or equal to 0, the max
operation can be skipped. Therefore, the expression can be simplified to: min (rgbr i1) i2 >> add;
.
clipp
clipp [min, max] [input]
: pairwise version ofclip
.
See the difference between the next two expressions. In the first expression, clipp
is used, which applies the specified ranges pairwise to each corresponding input value. As a result, fx
is confined within the range [−0.2,0.2]
and fx+0.5
within [−0.4,0.4]
. This results in two lines.
In the second expression, clip
is used, which applies all the specified ranges to all the inputs. Consequently, each input value is confined within its respective range, resulting in four lines.
y << clipp [-0.2,0.2,-0.4,0.4] [fx, fx+0.5];
hline y px >> add;
y << clip [-0.2,0.2,-0.4,0.4] [fx, fx+0.5];
hline y px >> add;
smoothstep
smoothstep [lowedge, highedge] input
: For input values belowlowedge
, the function yields 0; for values abovehighedge
, it yields 1; and for values in between, it smoothly interpolates. Additionally,smoothstep
accepts the edges in descending order:smoothstep [highedge, lowedge] input
, in which case values belowlowedge
yield 1, and values abovehighedge
yield 0.
In this example, when fx
is less than -0.5, y
is 0, when fx
is more than 0.5, y
is 1, and when fx
is between -0.5 and 0.5, y
goes from 0 to 1:
y << smoothstep [-0.5, 0.5] fx;
hline y px >> add;
Using smoothstep
provides precise control over transitions in patterns. In the following code snippet, multiple vertical white stripes are drawn across the display. These stripes gradually transition from darker on the left side of the screen to whiter on the right side:
f << unipolar $ sin' (fx*40);
s << smoothstep [-0.8,0.5] fx;
s*f >> add;
While a similar effect could be achieved using (unipolar fx)*f >> add;
on the last line, smoothstep
allows us to specify the range of coordinates where the transition occurs with greater precision.
Same idea, applied to the drawing of a mathematical function:
f << sin' (fx*4);
s << smoothstep [-3,2] fx;
fit 1 $ zoom 0.5 $ circle [fx, s*f] 0.02 >> add;
The range [0,1]
returned by smoothstep
can be easily adjusted to fulfill different requirements. In the following pattern, similar to the previous one, s
now varies from 0.1 to 0.7:
f << sin' (fx*4);
s << 0.1+0.6*smoothstep [-3,2] fx;
fit 1 $ zoom 0.5 $ circle [fx, s*f] 0.02 >> add;
So far, we’ve used smoothstep
to gradually introduce a pattern. It’s also well-suited for creating transitions between two different patterns. In the following example, an image is displayed on the left side, and another image on the right side. The section in the middle transitions from one image to the other. This is achieved using the linear interpolation formula in the last line:
i1 << img "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Peaceful_waterfall_%28Unsplash%29.jpg/1024px-Peaceful_waterfall_%28Unsplash%29.jpg";
i2 << img "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Half-timbered_mansion%2C_Zirkel%2C_East_view.jpg/1024px-Half-timbered_mansion%2C_Zirkel%2C_East_view.jpg";
s << smoothstep [-0.25, 0.25] fx;
i1*(1-s)+:i2*s >> add;
This same idea can be used to blend any two different patterns:
a << spin (saw 0.1 + fy) $ tile 4 $ hline (cbrt fx) 0.2;
b << spin [saw [-0.01,0.013]] $ tile [8*(1-abs fx),4*fx] $ spin 0.25 $ rect 0 0.6;
s << smoothstep [0.3, 1.3] (fit 1 $ fr);
s*a+(1-s)*b >> add;
In this example, a
represents a pattern based on white moving lines, while b
is based on colored rectangles. Notice how the two patterns blend: b
dominates the center, a
dominates the sides, and between a radius of 0.3 and 0.6, the two patterns mix together.
The concept of mixing multiple patterns can be expanded. In the following example, three sections are blended together. The entire pattern consists of a wide horizontal line that reacts to input frequencies. On the left side, it responds to low frequencies, in the center to mid-range frequencies, and on the right side to high frequencies.
A sharp version of this idea can be achieved by filtering the analyzed frequency depending on the x coordinate of the fragment:
a << ilo*(fx<(-1)/3);
b << ihi*(fx>1/3);
c << imid*((fx>=(-1)/3)*(fx<=1/3));
hline (-1) (2*(a+b+c)) >> add;
Here, a
is ilo
if fx
is less than -1/3, and 0 otherwise. Similarly, b
is ihi
only for fx
greater than 1/3, and c
is imid
only for the middle section.
Now, we aim to adapt this pattern to smoothly transition from one section to another, ensuring continuity in the line.
To determine the transition coordinates for dividing the display into three equal parts, we’ll add 1/6 on each side of the previous transition points (-1/3 and 1/3). This results in the first transition occurring between -1/2 (-1/3-1/6) and -1/6 (-1/3+1/6), and the second transition occurring between 1/6 (1/3-1/6) and 1/2 (1/3+1/6).
We define ab
and bc
as the transitions from a
to b
and from b
to c
, respectively. In the first line, we use smoothstep
to define ab
, which will be 1 when fx
is less than -1/2 and 1 when fx
is greater than -1/6. Similarly, we define bc
in the second line.
Extending the linear interpolation formula to accommodate three sections is the tricky part. From how we’ve defined ab
and bc
, it’s easy to see that ilo*ab
defines the left part, and ihi*bc
defines the right part. However, defining the middle part requires considering the complement of the other two. Therefore, the result is imid*(1-ab)*(1-bc)
.
Finally, all parts are summed together in s
, and this is applied to the line’s width:
ab << smoothstep [(-1)/6, (-1)/2] fx;
bc << smoothstep [1/6, 1/2] fx;
s << ilo*ab+ihi*bc+imid*(1-ab)*(1-bc);
hline (-1) (s*2) >> add;
smoothstepp
smoothstepp [lowedge, highedge] input
: the pair-wise version ofsmoothstep
.
See the difference between the two functions in these examples:
s << (smoothstep [0.1,0.4,0.8,1.2] [fr, fr/2])/10000;
fit 1 $ zoom 0.5 $ spin (saw s) $ hline 0 0.05 >> add;
s << (smoothstepp [0.1,0.4,0.8,1.2] [fr, fr/2])/10000;
fit 1 $ zoom 0.5 $ spin (saw s) $ hline 0 0.05 >> add;
In the first one, there are a total of four lines:
- Red line, spiraling when
fr
is between 0.1 and 0.4. - Green line, spiraling when
fr/2
is between 0.1 and 0.4, corresponding tofr
being between 0.2 and 0.8. - Blue line, spiraling when
fr
is between 0.8 and 1.2. - White line, spiraling when
fr/2
is between 0.8 and 1.2, corresponding tofr
being between 1.6 and 2.4.
In the second one, there are only two lines:
- Red line, spiraling when
fr
is between 0.1 and 0.4. - Cyan line, spiraling when
fr/2
is between 0.8 and 1.2, corresponding tofr
being between 1.6 and 2.4.