Scaling values

Different ways to rescale a range of values

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:

fx,fy-11Can change if using for example fit.
red, green, blue, alpha01
hue, saturation, value01
oscillator’s output-11
oscillator’s frequency-∞But usually low values, from -1 to 1
spin’s argument-11First lap, any other value is accepted
lo, mid, hi, ilo, imid, ihi01
fft/ifft argument01
fft/ifft output01

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;

Scaling values example 1

o << [0.1, 0.3] ~~: 0.8 $ osc [0.03, 0.07];
hline o 0.004 >> add;

Scaling values example 2

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;

Scaling values example 3

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 [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;

Scaling values example 4


  • linlinp [min1, max1] [min2, max2] [input]: This function operates as the pairwise version of linlin.

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;

Scaling values example 5

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;

Scaling values example 6

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 [min, max] [input]: clip input values into the specified range. If a value is less than min it will become min, and if it’s greater than max it will become max.

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;

Scaling values example 7

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 "";
i2 << img "";
clip [0, rgbr i1] i2 >> add;

Scaling values example 8

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 [min, max] [input]: pairwise version of clip.

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;

Scaling values example 9

y << clip [-0.2,0.2,-0.4,0.4] [fx, fx+0.5];
hline y px >> add;

Scaling values example 10


  • smoothstep [lowedge, highedge] input: For input values below lowedge, the function yields 0; for values above highedge, 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 below lowedge yield 1, and values above highedge 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;

Scaling values example 11

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;

Scaling values example 12

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;

Scaling values example 13

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;

Scaling values example 14

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 "";
i2 << img "";
s << smoothstep [-0.25, 0.25] fx;
i1*(1-s)+:i2*s >> add;

Scaling values example 15

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;

Scaling values example 16

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;

Scaling values example 17

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;

Scaling values example 18


  • smoothstepp [lowedge, highedge] input: the pair-wise version of smoothstep.

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;

Scaling values example 19

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;

Scaling values example 20

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 to fr 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 to fr 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 to fr being between 1.6 and 2.4.