Lazy Nezumi Pro has its own scripting engine, which can allow users to write and run scripts that will be evaluated as you draw.
This page will show you how to use this feature to add patterns to your lines, and will teach you how to write your own scripts.
Reference tables of all available input/output variables and predefined functions can be found on the Scripting Reference page.
If you've never programmed before, you may be wondering what a script actually is. Basically, it's a text-based computer program that defines how to process some input data in order to produce output data.
In the context of LNP, the input data includes the current state of your pen, such as its position and pressure values. The script will then define how to change this data, and output it back to the pen state that your art program will then use to draw to the canvas.
This means that you can write scripts that will, for example, automatically draw patterns while you draw straight lines. Or tweak the pressure response curve, or even add noise to your lines by adding randomness to the pen pressure. If you like to tinker, this adds a lot of possibilities.
You don't need to be a programmer to understand how to use scripts in LNP. The app comes pre-loaded with lots of scripts ready for you to use. But learning how to write your own will give you more power, and should (hopefully) not be too difficult with this guide.
Here's an example of a script applied with LNP's Ellipse Ruler. Since the script processing happens after LNP's ruler stage, this can help you draw perfect circular patterns!
To access LNP's scripting functionalities, open the main window, and unfold the Scripting section at the bottom by clicking on the arrow icon. This part of the guide will explain what the controls in this section do.
1. The Script list contains all the scripts that come pre-loaded with LNP, organized by subfolders. (These are physically located in the "distortion" subfolder of your LNP installation.) You'll notice that the first entry in the list is "<custom>". This is the element you should select when writing your own script.
2. Under the Script list, you'll find a set of Script Parameters (if the script has any), with sliders and input boxes for setting their values. These values are assigned to the associated variables in the script, and will change how the script affects your lines. For example, a wave script could have parameters to change the period and amplitude of the wave. Double-clicking a parameter's slider will reset that parameter to its default value. Hovering over a parameter name, slider, or value input box will show a tooltip, if one is defined for that parameter.
3. The Show Code button will open the script's code text box. The text is read-only for the pre-loaded scripts.
4. The Graph button will open a window that will show you a preview of the effects of the current script and its parameters.
5. The Reference button will open a window showing all available input/ouput variables, and predefined functions with their definitions. (This was added in LNP version 21.06.04. If you are using an older version, you can use the online version of the reference.)
6. The Edit button will copy the current script to your current preset's Custom script, which will allow you to modify it. You can use this if you don't want to start from scratch when writing a new script. (This was also added in LNP version 21.06.04. If you are using an older version, you can simply copy the script code, and paste it into the custom script's code text box after switching to the "<custom>" script.)
7. When you are in custom script mode, a Compile button will appear underneath the code text box. This button will scan your code for errors, and display any if found. If there are no errors, the new code will become active, and the graph preview will be updated.
Clicking the Graph button will open a window that will show you a preview of the effects of the current script and its parameters. The "..." button at the top-left corner will expand the window to show the graph options.
To generate the graph, a straight line is used as the simulated input to the script (a circle is used instead if you have the Ellipse ruler enabled). The options section will let you set the start and end values of the predefined input variables (such as position and pressure), so that you can see what effect they will have if your script uses them.
At the bottom of the options section, you'll find the animation controls. Here you can select a script parameter variable to be animated, along with its start and end values. The Play button will then animate the graph by changing the selected variable over the desired animation duration, with an optional loop option.
In LNP, a script program is defined by a series of statements separated by semi-colons. The statements are read and evaluated from top to bottom. These statements can be composed of variable assignments, function calls, and mathematical expressions. All variables contain real numbers (stored as 32bit floating point). Here's an example of a typical statement:
wave = triangle(d/period);
In this example, the value of variable "d" is divided by the value of variable "period, and the result is passed as an argument to the "triangle" function, which assigns its result to the "wave" variable.
Variable names can contain the following characters: _abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789. They should not start with a number character. Unlike some other scripting languages, you do not need to declare a variable before using it in a statement.
A variable is considered an input variable if it used in an expression to produce a result, and an output variable if it is assigned a value. A variable can be both an input and an output variable.
If your script contains input variables that are not also output variables (they are used in statements but no statements assign values to them), these are considered Script Parameters, and will show up in the UI under the script list after successful compilation. This will allow users to set their values via the sliders or input boxes.
Conditional assignments can be performed via the following syntax:
a = condition ? b : c;
Here, the variable "a" will receive the value of "b" if the condition tests true, "c" otherwise.
All predefined input/output variables, operators used for mathematical expressions, and functions are described in the tables of the Scripting Reference page.
Starting with version 21.06.04 of LNP, you can write comments in your script to document what it does. The compiler will ignore any text after // until the end of the line, as well as any text between /* and */.
When you start to draw with a script program enabled, all output variables (predefined, as well are your own) are initialized to zero. When new tablet data comes in as you're drawing, the predefined input variable values are updated, the program is evaluated, and the predefined output variables are read from and used to modify the data that is passed to your art application to draw the line.
Note that after initialization, the values of user defined output variables are not changed in between script invocations. This lets your script use state logic, where new values are computed based on previous values.
Note that you don't have to modify all the predefined output variables if you don't need to. It is perfectly acceptable to have a program only affect the output pressure (variable "op"), or just the pen position (variables "ox" and "oy"). However, you will need to modify at least one of these variables in order for the program to compile successfully.
Like with the rulers, when using a script, be sure to turn off your art app's brush smoothing/stabilization. Since the app's smoothing is applied after all LNP processing, leaving it on could interfere with your script output and change the expected shape of your patterns. If you need stabilization, you can use LNP's smoothing, as that is applied before the script.
For our first example, let's write a simple script that will increase the pen pressure, in order to make it easier to draw thicker lines (this assumes your pen pressure is mapped to line thickness in your art program). This could be accomplished by using the Gain parameter in LNP's Pressure Curve section, but it makes a great starting example.
It's a good idea to keep the Scripting Reference window open (or the online version) while trying these examples, so you can quickly see the description of the predefined input/output variables and functions used.
Using a new or existing preset, activate the Scripting section, then select the "<custom>" element from the script list. Click the Show Code button, and then type or copy/paste this code into the box:
temp = p * 2; op = temp;
Now click the Compile button, and try drawing a couple of lines in your art program. They should come out thicker than when the script is turned off. Let's explain what this script does:
This is nice, but what if we don't always want to double the pressure? Let's make this script more interactive by adding a Script Parameter. This is done by adding an input variable and not having any statement assign a value to it:
temp = p * pressureGain; op = temp;
As you can see, we've replaced the constant 2 with a new variable named "pressureGain". When you compile this program, you will see a new Script Parameter appear under the script list:
When you add a new Script Parameter, by default it will be assigned a value of 0.5, and it will have a value range of [0..1]. In this case we would like to be able to multiply the pressure by more than 1, so we need to change the range. To do so, click on the "..." button next to the parameter's value input box. This will open the Parameter Info window, where you can rename the variable, set its range and default value, precision, and tooltip. Go ahead and change the maximum value to 5 and click OK.
Congratulations, you have now written your very first interactive LNP custom script! If you now save the preset, the script and its parameter details will be saved with it.
A user once explained that they needed to draw lots of hair strands. Controlling the pen pressure for every stroke to produce the same fade-in thickness was making their hand tired, and they were wondering if there was a way a script could help. This is the script that was sent to them:
op = p ? d/maxPDist : 0;
This simple script introduces another very useful predefined input variable: "d". This variable contains the total distance traveled (in screen pixels) since the start of the current stroke. By dividing this variable by a constant value, and assigning the result to the output pressure variable "op", we can effectively ramp up line thickness automatically. This means that the user wouldn't have to worry about controlling the pressure of the pen themselves, which would greatly reduce effort and strain!
Here, the "maxPDist" variable represents the distance at which the pressure should reach its maximum value of 1 ("p" and "op" are normalized to always be in range [0..1]). A good range for this parameter is [50..300] pixels. Here's what it looks like at a value of around 200, with a 15 radius brush:
This script also introduces the question mark operator for conditional assignments. The condition in this case is the value of the input pressure variable "p". If it is anything other than zero, then "op" will receive "d/maxPDist" (the left side of the : operator), otherwise it will receive zero (the right side of the : operator). This is an important thing to remember when writing to the output pressure variable. When the user lifts the pen, input pressure becomes zero. In this case, we want the line to end, so we must also assign zero to the output pressure variable. Forgetting to do this will cause your art program to wait for more input from the tablet, as it will think the pen is still down.
If you wanted the pressure to fade out instead of fading in, you could use this script instead:
op = p ? 1-d/maxPDist : 0;
Another user once asked if it was possible to draw dashed lines with LNP. Here's one way to script this:
wave = square(d/period, pattern); dash = (wave > 0) ? p : 0; op = p ? dash : 0;
Compiling this creates two Script Parameters: "period" and "pattern". Try a range of [10..100] for "period", and [-0.99..0.99] for "pattern".
The first statement of this script uses the predefined "square" wave function to generate a pattern based on the current line distance "d", and assigns it to the "wave" variable. You can see a graphical preview of this function here. The "pattern" parameter controls how wide the gaps should be, and the "period" parameter sets the distance in pixels before the pattern repeats. You will see "d/period" used very often with wave functions, as by design they have a period of 1.
In the second statement, we check if the current value of the "wave" variable is above zero. If it is, we assign the input pressure variable's value to the "dash" variable. Otherwise, "dash" receives zero, which is what will create the gaps.
The third statement assigns the value of "dash" to the output pressure variable, unless the input pressure is zero, like in the previous example.
Here's what this looks like with "period" set to 30, and "pattern" set to -0.40:
In this example, we'll write a script that helps us quickly draw zippers!
The previous example introduced the "square" wave function. Here we will use a similar function called "sine", which takes 2 arguments: the distance at which to sample the function, and the thickness of the wave crests. You can preview this function here.
This time, we want to use the result of this function to displace the pen position instead of modifying its pressure. To do this, we'll use the predefined input variables "nx" and "ny". These are the (x,y) coordinates of the normal vector at your current position along your stroke. If you're unfamiliar with vectors, each coordinate represents a displacement along one axis. We can use this to displace the current position directly away from the curve.
Here is the code for this script:
offset = amplitude * sine(d/period, thickness); ox = x + nx * offset; oy = y + ny * offset;
This will create three Script Parameters. A good range for the "thickness" parameter here is [0.25..5]. The "amplitude" parameter will control how high the wave will go from the center, so try a range of [5..200]. And for the "period" parameter, you can use a range of [10..100] like in the previous example.
All the wave functions return a value in range [-1..1], so the first line of the script multiplies the result by our "amplitude" parameter and stores the result in the "offset" variable.
The next two lines assign values to the predefined output position variables "ox" and "oy", by taking the current (x,y) position, and displacing it along the (unit length) normal vector by "offset" amount. This will cause the drawn line to go back and forth around the direction of travel of your pen.
Be sure to play around with the "thickness" parameter to see how you can change the shape of the wave pattern. Drawing with this script enabled and "thickness" set to 3 will give us the following result:
Here's another version of this script that modulates the amplitude by the input pressure amount. This lets us change the shape as we draw by applying more or less pressure to the pen.
offset = p * amplitude * sine(d/period, thickness); ox = x + nx * offset; oy = y + ny * offset;
When using scripts that displace the pen position this way, it helps to have a little bit of smoothing enabled in LNP, as this will also smoothen the changes of your normal vector as you draw the line. And if you want the shape to be centered around a perfectly straight line, you can also enable one of LNP's rulers!
You can find other scripts that use this normal displacement concept in the Basic Patterns section of the Script list.
Did that last example start turning your gears? Well then let's draw some gears!
If we keep the script from the previous example, and enable the Ellipse ruler in our preset, we quickly see there's a problem:
With this script, we can't easily get the teeth of the gear (or "cogs") to align as we close the circle. This is because the "period" parameter doesn't necessarily divide the perimeter of the circle evenly. Fortunately, there's an easy way around this problem!
There is another predefined distance input variable available we can use: "dn", which stands for "normalized distance". If you look at the description in the Scripting Reference, you'll see: Ellipse: 0.0 at start, plus or minus 1.0 for each time around. This is great, because it means we don't have to worry about calculating the perimeter of the ellipse ourselves! Since the period of the wave functions is 1 by design, we can simply multiply "dn" by the number of teeth we want per loop, and pass that to the wave function. Here's the modified script:
offset = amplitude * sine(dn * teeth, thickness); ox = x + nx * offset; oy = y + ny * offset;
When you compile this, the "period" parameter will be replaced by a new "teeth" parameter. Set its range to [3..20], and 0 decimals, as we want integral numbers for this parameter. Here's a perfectly aligned 6-teeth gear:
You can find other scripts that work well with the Ellipse ruler in the Circular Patterns section of the Script list.
When working on calligraphy or lettering, one must exercise very controlled strokes to give style and character to the lines. Here is a script that can help by modulating your line thickness based on the direction of your pen. It lets the user to set a pressure scale factor for when the pen is mostly moving up, and one for when it's mostly moving down. This lets the user focus on pen movement, while the script helps with the pressure/thickness aspect.
ydir = nx * 0.5 + 0.5; op = p * mix(minScale, maxScale, ydir);
The idea behind this script is to scale pressure based on line direction. To do this, we look at the line's current normal vector (nx, ny). Since the normal vector is orthogonal to the line direction vector, "ny" tells us how much of the current line direction is horizontal, and "nx" tells us how much is vertical. We want to modulate pressure based on vertical movement in this case, so we'll use "nx".
The normal vector is a unit vector (of length 1), so each of its coordinates is in range [-1..1]. The first thing the script does is to remap this range to [0..1], and stores the result in the "ydir" variable. This variable is then used to compute our final pressure scale factor, by mixing between "minScale" and "maxScale".
Set the "minScale" parameter range to [0..1], and "maxScale" to [1..2]. Here are some lines drawn with "minScale" set to 0.75 and "maxScale" set to 1.3. Smoothing was also used in this preset, to give the lines some nice flow. You can see that this script can even give a sort of 3d effect to the lines!
Now instead of always using the vertical axis, let's add two parameters that will let the user set the axis and direction for which the pressure modulation effect should be applied:
dir = axis ? nx : ny; dir = direction ? dir : -dir; dir = dir * 0.5 + 0.5; op = p * mix(minScale, maxScale, dir);
Set the parameter range for both "axis" and "direction" to [0..1], with 0 decimals. Now if you set "axis" to 0, the effect will apply to the horizontal parts of your line instead. The "direction" parameter can be used to flip the direction in which "minScale" and "maxScale" are applied along this axis.
"Smooth, beautiful lines" is one of LNP's tag lines, and many users discovered it while looking for a stabilizer. But if used properly, messy, jittery lines can also look wonderful in the right context. Let's write a script that gives our lines some tasteful imperfections!
The first thing we're going to do is add some line thickness grit, to make it look like we're drawing with a runny ink pen. If you look through the function reference, you'll see there's a "rand" function that returns a random value in range [0..1]. Let's try multiplying our input pressure by a random amount and see what we get.
op = p ? p * rand() : 0;
Interesting, but not quite what we're looking for. A little too blotchy. Instead of scaling our input pressure, let's try adding a small random amount instead. We'll use the "mix" function to remap the random value to a value between "minNoise" and "maxNoise".
op = p ? p + mix(minNoise, maxNoise, rand()) : 0;
Here's what this looks like with "minNoise" at -0.2, and "maxNoise" at 0:
This looks better, but it would be nice if we could control the length of the imperfections. The "rand" function returns a different value every time, so we'll have to try something else. If you look at the function reference, you'll spot a wave function called "noise". Here's a graph preview. This is a non-repeating continuous function, which is great because it will look random, but we can still control its scale like with the other wave functions. Try this script:
n = 0.5 + noise(d/period) / 2; op = p ? p + mix(minNoise, maxNoise, n) : 0;
The "noise" function returns a value in range [-1..1], so we remap it to [0..1] in the first line so we can use it to mix between "minNoise" and "maxNoise", just like in the previous script.
Here's what this looks like with "period" at 30, "minNoise" at -0.3, and "maxNoise" at 0:
Now we're getting somewhere! The noise looks a lot more subtle, unlike the previous scripts that made it look like we bashed the hell out of our pen. Being able to control the width of the noise pattern via the "period" parameter is very nice indeed.
But now if you look closely at the three straight bottom lines, you may notice something. Even though the pressure applied for each line was different, you can still tell that the noise pattern is exactly the same every time. This can be useful in certain situations, but it would be nice if we could change the pattern. We can do this quite easily by adding a fixed offset to the distance value we pass to the noise function. Let's call it "seed" and set its range to [0..10000]:
n = 0.5 + noise((seed+d)/period) / 2; op = p ? p + mix(minNoise, maxNoise, n) : 0;
We can now change the "seed" parameter every time we want a different pattern. This is great if we want to be able to reproduce a particular pattern we liked, but what if we want a different pattern for every line, without having to change the parameter every time? In this case we can use the "rand" function to initialize our seed automatically at the start of the line:
seed = seed ? seed : rand() * 10000; n = 0.5 + noise((seed+d)/period) / 2; op = p ? p + mix(minNoise, maxNoise, n) : 0;
Since script variables are initialized to zero before you start drawing, we can re-initialize our seed value by only giving it a new value if it's zero. This is what the first line does using conditional assignment. The noise pattern now looks different for each line!
Let's finish this script by also adding jitter to the pen position. We'll use the same normal vector displacement technique shown in the previous example. But this time, the position will be displaced by the result of another call to the noise function, multiplied by a new "posNoiseAmount" parameter:
seed = seed ? seed : rand() * 10000; n = 0.5 + noise((seed+d)/period) / 2; op = p ? p + mix(minNoise, maxNoise, n) : 0; pn = posNoiseAmount * noise((seed+d)/posNoisePeriod); ox = x + nx * pn; oy = y + ny * pn;
Here's what this looks like with "posNoisePeriod" at 30 and "posNoiseAmount" at 3. This actually makes a great water reflected line effect!
In this example, we'll write a script that draws the outline of a city's buildings. This can be very useful for backgrounds, or for generating design ideas.
Since most buildings are rectangular, we'll want to use a function such as the "square" wave used previously. But this function is too repetitive, so it won't do. Fortunately, there is another predefined function that is perfect for this: "cellNoise". Here's a graph preview. It behaves like the "noise" function seen in the previous examples, but is non-continuous. Its output produces straight lines that jump from one level to the next randomly. This is perfect for lots of applications, including building outlines! Try this script:
p1 = period; p2 = period/3; p3 = period/7; p4 = period/11; offsetDist = d + seed; w1 = w1Amp * cellNoise(offsetDist/p1); w2 = w2Amp * cellNoise(offsetDist/p2); w3 = w3Amp * cellNoise(offsetDist/p3); w4 = w4Amp * cellNoise(offsetDist/p4); offset = d < 20 ? 0 : (w1+w2+w3+w4); ox = x + nx * offset; oy = y + ny * offset;
Set the following parameter ranges: "period" [100..300], "seed" [0..50000], "w1Amp" to "w4Amp" [0..100].
A common technique when generating random patterns is to combine several noise waves with decreasing periods. The wave with the biggest period sets the general outline of the curve, and details are then added by adding high-frequency waves.
This is what we do here: the main "period" parameter is divided by 3, 7, and 11 to produce three smaller periods, which are then used in successive calls to the "cellNoise" function. A global seed is shared for all waves. Each wave gets scaled by its own amplitude parameter, and the resulting offset amount is produced by adding the four waves together. You'll also notice that if the current distance is smaller than 20, we set "offset" to zero. This is so the line doesn't jump straight up or down when we start drawing.
Now for the fun part! Open the script graph window, play with the amplitude parameters, and find a seed you like!
Very cool! But now if we wanted to draw cityscapes at different depths, we'd have to adjust the period and the amplitudes every time. To make this easier, let's add a global "scale" parameter that does this for us, and set its range to [0..10].
p1 = period*scale; p2 = p1/3; p3 = p1/7; p4 = p1/11; offsetDist = d + seed; w1 = w1Amp * cellNoise(offsetDist/p1); w2 = w2Amp * cellNoise(offsetDist/p2); w3 = w3Amp * cellNoise(offsetDist/p3); w4 = w4Amp * cellNoise(offsetDist/p4); offset = d < 20 ? 0 : scale*(w1+w2+w3+w4); ox = x + nx * offset; oy = y + ny * offset;
We can now quickly create cityscape scenes that have depth, by decreasing the scale parameter for each line. Here's an example that was created using the Parallel Lines ruler enabled for a perfectly straight guide for this script:
In this example, we'll write a script that helps us draw lightning bolts.
If you look at reference photos of lightning, you'll see that bolts have a general shape with a few main directions. Within these main directions, there are smaller imperfections as the lightning looks for a path in the air to the ground. Like in the previous example, we'll use the method of combing several wave functions of different periods and amplitudes. This time though, we'll use the "noise" function instead of "cellNoise".
seed = seed ? seed : rand()*10000; offsetDist = d + seed; w1 = w1Amplitude * noise(offsetDist/w1Period); w2 = w2Amplitude * noise(offsetDist/w2Period); w3 = w3Amplitude * noise(offsetDist/w3Period); offset = smoothStep(0, 20, abs(d)) * (w1+w2+w3); ox = x + nx * offset; oy = y + ny * offset;
In this script, the seed is generated randomly, since we'll usually want a different bolt every time we draw. We use the smoothStep function to gradually fade the position offset in over 20 pixels, to avoid having an offset at the start of the line (we want the bolt to start at our actual pen down position).
You'll want to set a range of [0..200] for the amplitude parameters, and [1..150] for the period parameters. Here's what this produces with the following parameter values: w1Amplitude = 46, w2Amplitude = 25, w3Amplitude = 4, w1Period = 130, w2Period = 57, w3Period = 8.
To remain snappy and responsive, your tablet or mouse driver is sampling its position at a fixed rate. This means that if you draw very fast, you will have fewer samples for your line than if you were to draw more slowly.
LNP processes tablet and mouse data before passing it on to the art application. It does not draw the lines itself. This means that if you're using a script that samples functions based on current distance, such as the wave functions used in the examples above, you will run into an issue called "aliasing" if you draw too fast. This happens when the function is under-sampled, and will lead to a final output that doesn't look like a more fully-sampled pattern. The only way around this is to slow down when using such scripts.
The following image illustrates this, using the dashed line script from Ex. 3:
If your script is not doing what you want it to do, you can look at the graph window to try and determine what's wrong graphically. But sometimes you'll need to look at the actual values of your variables to really understand what's going on.
To do this, you can use the special debugging functions "debugOut1" and debugOut2". As arguments, these functions accept a string of text and either one or two variables. The text should contain format specifiers using the printf syntax, which will be replaced by the variable values in the final string. Here's an example:
debugOut2("p: %f, d: %e", p, d);
This will print the value of the input pressure variable in decimal format, and the value of the distance variable in scientific notation format.
To actually see this text, you will need to use the DebugView app, which you can download here. As this app can be used to monitor lots of events in Windows, we recommend using the "*DebugOut" input filter to only see LNP script related text.
Congratulations on finishing this tutorial! Hopefully, you had fun, and the examples helped you understand the capabilities of LNP's scripting engine.
Your feedback about this guide would be much appreciated! Was anything confusing or unclear? Are you having any problems writing your own scripts? Is there anything you felt was missing and would like explained? Did it spark your creativity, and were you able to come up with some cool new scripts? We'd love to seem them! :) Also, please let us know if you had ever programmed before reading this guide.