Dashing over the line

This is an utterly boring tool. Let’s face it, who cares about drawing straight lines? And yet, there are many times when you need to do a simple shape or two, perhaps some motion graphics, and you have to dust of the After Effects you thought you’d left behind when you found the joys of nuke. No more! (Well, unless you need something other than lines…)

This little node can draw all the lines you want, as well as animate it being drawn, dash the lines, tweak the spacing and softness etc… There’s still a couple of oddities in it that I’ve yet to go back and tidy up, but I’m long overdue a post and it’s tidy enough to be worth putting out there for now.

Installing

This requires a few python callbacks to run nicely, so I’ve tidied up into a quick and easy install package. Simply place the folder in your plugins directory and include the folder in the init.py using nuke.addPluginPath().

Code Breakdown

For those unfamiliar with blink script, I give a quick and basic intro over here. For this script, we forgo having a source image as we are drawing from scratch. The param and local variables are standard with the exception of the points. We can technically include as many points as we want, but for simplicities sake I have included 16. It’s important to note the define statement at the top of the script where we set the max points that can be used. You can increase this as high as you like, but you’ll also need to add the appropriate code to the param and init() functions.

```pt_limit = min( max_pts, upper_limit );
line_limit = close ? pt_limit : pt_limit - 1;

max_length = 0.0f;

// Calculate line equation components for each line
// [ slope, y_intercept, slope_perp, horizontal/vertical, cumulative distance ]
for ( int i = 0; i < line_limit; i++ ) {
int next = ( i + 1 ) % pt_limit;
float2 direction = points[next] - points[i];

if ( direction.y == 0 ) {
// Horizontal
lines[i][3] = 1.0f;
} else if ( direction.x == 0 ) {
// Vertical
lines[i][3] = 2.0f;
} else {
// Slope
lines[i][0] = direction.y / direction.x;
// Y Intercept
lines[i][1] = points[next].y - lines[i][0] * points[next].x;
// Slope Perp
lines[i][2] = -( 1 / lines[i][0] );
// Not Horizontal or Vertical
lines[i][3] = 0.0f;
}

float line_length = length( points[next] - points[i] );
max_length += line_length;
lines[i][4] = max_length;
}

// --- Animation ---
animated_end = clamp( anim_time, 0.0f, 1.0f ) * max_length;```

The rest of the init function calculates the line equation components for each line and stores them in an array. It’s important to note whether they’re horizontal or vertical as well so we can avoid division errors later. We’ll also store a cumulative length for each line so we can quickly tell how far along we are at any point. To animate the line, we can simply cut it off at the fraction we specify.

Now, we come to the actual process. The plan is simple; At each pixel, we find the value it would contribute to each line, check it’s distance along the line to see if it should be dashed, fade the edges as per the softness and keep the maximum value. This allows lines to overlap without getting messy.

```// Find the current pixel value from each line
for ( int i = 0; i < line_limit; i++) {

// --- Intersection from current point ---

if ( lines[i][3] == 2.0f ) {
// Vertical
intersection = float2( points[i].x, pos.y );
} else if ( lines[i][3] == 1.0f ) {
// Horizontal
intersection = float2( pos.x, points[i].y );
} else {
// Intersection @ x using line equations
intersection[0] = ( pos.y - lines[i][1] - lines[i][2] * pos.x ) / ( lines[i][0] - lines[i][2] );
// Intersection @ y using Line 1 equation
intersection[1] = intersection[0] * lines[i][0] + lines[i][1];
}```

First, we find it’s perpendicular intersection with each line. We’ve already calculated the line equation and perpendicular slope, so it’s easy to do the intersection equation. If it’s vertical or horizontal we can simply take the point from existing info.

```// --- Value for current line ---
int next = ( i + 1 ) % pt_limit;
float distance = length( float2( pos.x - intersection.x, pos.y - intersection.y ) );
float line_result = 0.0f;
float distance_along_line = i > 0 ? lines[i-1][4] : 0.0f;
bool edge = false;

// If in bounds
if ( intersection.x >= min( points[i].x, points[next].x ) && max( points[i].x, points[next].x ) >= intersection.x &&
intersection.y >= min( points[i].y, points[next].y ) && max( points[i].y, points[next].y ) >= intersection.y ) {

line_result = distanceToValue( distance );
distance_along_line += length( intersection - points[i] );

} else {
edge = true;

// Calculate closest end point
float dist_to_start = length( float2( points[i].x - pos.x, points[i].y - pos.y ) );
float dist_to_end = length( float2( points[next].x - pos.x, points[next].y - pos.y ) );
float closest = min( dist_to_start, dist_to_end );
distance_along_line = ( closest == dist_to_end ) ? lines[i][4] : distance_along_line;

// Straight edges
if ( !close && !round_ends && ( i == 0 || i == pt_limit - 2 ) ) {
float closest_end;
if ( dist_to_start < dist_to_end )
closest_end = length( intersection - points[i] );
else
closest_end = length( intersection - points[next] );

if ( closest_end <= softness )
line_result = distanceToValue( closest_end + width ) * distanceToValue( distance );
}
// Round ends
else {
line_result = distanceToValue( closest );
}
}```

Bit of a mouthful this piece but quite simple. We store some key info, such as calculating what the next point is, the distance along the line we are so far and set a default 0 value for the line result.

Next, we check if the intersection we calculated is between the two points. If it is, we use our helper function to convert the distance from the point into a value, fading as necessary. We also add the current distance along this line to the total distance so we know exactly how far we have come.

If it’s outside the lines boundaries, we need to check if it’s close to the ends so we can give a smooth transition into the next line. However, if the line isn’t a closed shape, we can have rounded or straight edges.

```// Cutoff Animated Length
if ( distance_along_line > animated_end )
line_result = 0.0f;```

To animate the line, we can remove the value if it’s greater than the fraction of the full length we specified.

```// --- Dashed Line ---
// If dashed
if ( dashed && line_result > 0.0f ) {

// Dash animation
float dash_offset = distance_along_line + spacing * offset;
// Prevent double line at start if reversed
float new_distance = dash_offset < 0 ? fabs( dash_offset - spacing ) : dash_offset;

// On / Off segments
float segment = new_distance / spacing;
int gap = ( int )segment % 2;

// If an odd numbered segment, fade edges into nothing
if ( gap == 1) {
float segment_distance = ( segment - floor( segment ) ) * spacing;
if ( segment_distance < softness && !edge )
line_result = line_result * ( 1 - segment_distance / softness );
else if ( spacing - segment_distance < softness && !edge )
line_result = line_result * ( 1 - ( spacing - segment_distance ) / softness );
else
line_result = 0.0f;
}
}```

If we want the line to be dashed, we simply divide the spacing into the current distance along the line. We can offset the dash by simply adding a fraction of the spacing to the distance before we divide. To avoid some doubling up of dashes at the 0 distance mark, we have to flip the result in the negatives.

Finally, apply a nice falloff to the space between the gaps. There is currently an issue with the way this is done where the softness can bleed out from dashes that are cut off and not visible. What we should be doing is checking if the source dash is visible or not.

You may  be wondering why we calculated the value if only to delete it again with the dash. The reason being is that we don’t know what areas are dashed until we’ve found our intersection and determined if it’s in bounds. At that point, we may as well calculate the value and apply the dash as needed. It also means we can multiply the value so that falloffs combine and the corners smooth out nicely.

Finally, we pick the maximum result, multiply the colour in and we’re done! The process is easily scalable by just adding more points and storing them in the points array, everything will correctly loop from there. You can run the blink script like this if you’d like, but to make it tidier there are a series of python callbacks we can use within nuke.

To do this, we’ll hide all unused points and when we insert a point, simply shuffle them all down and reveal the next row. To delete, we shuffle the points back up and hide the last row. We’ll need to add a delete and insert button per point which would be quite tedious by hand, so I’ve included an initialisation function. Simply pass it the node and it will strip off the point knobs and add them back with it’s delete and insert buttons in the right order. It will also hide everything above the max points in use, as well as the max points and upper limit knobs.

You may have noticed those last two knobs being added to the node, despite not being user adjustable. This is so the python callbacks know when to stop being able to add points.

I think that’s everything! I’ll be updating the scripts to tweak some issues so expect to see an update at some point. Enjoy!