Alternate line styles in PostScript

PostScript gives you the ability to step through the current path and execute arbitrary code, processing the steps of the path. (In the PostScript Language Tutorial and Cookbook, this is used to set text along a path.) I have written code to do two kinds of varying-width lines: a tapering line and a pseudo-calligraphic line.

var_line and bolt

The var_line function consumes the current path and replaces it with a path that outlines a variable-width line along that path. It also does a small amount of filtering on the points - specifically, it ignores any line segment whose length is less than 1/10 the current line width. (This removes some artifacts that appear when a short line is used to join very close points.) It takes a function that determines the "line radius" (half the desired line thickness) based on a number of internal variables.

As an example, the bolt function simply calls var_line with a particular function:

/bolt {
    { 
	dist linelen div 1 exch sub 
	currentlinewidth mul 2 div 
    } var_line
} def
This sets the line radius of the line to half the current line width times the proportion of the line left to be drawn, moving evenly from the full thickness at the beginning of the line to zero at the end. This creates a "bolt of lightning", "horn" or "flame" effect. The boltstroke function just fills that bolt outline, making it a drop-in replacement for the built-in stroke operator.

For sharp points, a miter is created, keeping the actual line thickness approximately correct. The linecap, linejoin, and miterlimit settings are respected as in a normal PostScript stroke operation. (The image on the left depicts a boltstroke junction with linejoin set to 0, 1, and 2.) The flattenpath operator is called on the path before analysis, turning arcs and curves into series of small straight lines.

Using other functions produce different kinds of lines. The bipoint function calls var_line with a different function:

/bipoint {
    {
	i 0 eq i pathcount 1 sub eq or 
	{0} {currentlinewidth 2 div} ifelse
    } var_line
} def
This sets the line radius to 0 at the first and last point and the natural line thickness everywhere else. This draws a very thin line if there are only two points in the path. (A bipointstroke function is also provided.)

Currently, var_line has two limitation to the paths it handles: it will quit if there is more than one moveto in the path, and it will behave unpredictably if the path goes directly back on itself (two segments meeting at a 0-degree angle).

calligraphic

The calligraphic function works very similarly to the var_line function: it consumes the path and replaces it with an outline. In this case, the outline is of a path that is full-width at junction points and becomes thinner in between. Like var_line, calligraphic supports the linecap, linejoin, and miterlimit settings. The function takes one argument, spline_offset, which controls the thickness of the intermediate line. The actual thickness is complicated to predict but for straight lines, the minimum thickness is 1/4 plus 3/4 of the spline_offset, as in the table below.

offset:thickness
0:1/4
1/3:1/2
2/3:3/4

I also provide a callistroke function, which takes the same spline_offset argument, calls calligraphic, and fills the resulting path. As a convenience, I also define spindly and spindlystroke, which call calligraphic and callistroke with an argument of 0, and brushy and brushystroke which call them with an argument of 1/3. The spindlystroke and brushystroke functions are drop-in replacements for stroke.


The edges of the line are actually drawn as a spline. The start and end points are calculated as in var_line, including miters, bevels, and curved joins as appropriate. The control points are at 1/3 of the length of the line segment, and are spline_offset times the line radius away from the line.

Unlike var_line, calligraphic does not flatten the path before analysis. It handles curves and splines (and in fact converts straight lines to splines during analysis). Control point positions are offset by spline_offset and mitered as if the spline was a set of straight lines.


A brief diversion regarding arcs

PostScript internally converts all arcs into splines. Specifically, it creates a spline that goes from the starting point of the arc to the first angle that is divisible from 90, then appends 90-degree splines until it is less than 90 degrees from the ending point, then adds a final spline to the ending point.

When using calligraphic, I want more control over the starting and ending points, so I added the sparc and sparcn functions, which are drop-in replacements for arc and arcn. These calculate the single spline that most closely resembles the arc and adds that spline to the path. In the first image on the right, the gray semicircle is a PostScript arc and the black semicircle is a 180-degree sparc.

Splines for arcs less than 45 degrees are almost indistinguishable from actual arcs. As they get larger, they become slightly less round. A 180-degree arc's spline is noticeably but not tremendously squared off. Splines for arcs larger than that look less and less like arcs. (In my own work I've avoided any arcs larger than 180, breaking them at intermediate angles.)

The sparc and sparcn functions also include the feature from arc and arcn that automatically jumps to the starting point of the curve - if there is a current point, a lineto will precede the spline, and if not a moveto will precede it.

calligraphic pitfalls

There are a number of pitfalls in using calligraphic. For one, since the start and end points of each side of a curved line are offset based on the direction of the previous and next lines, the thickness of the center of a curved line may be thinner or thicker than expected. In some cases, the two sides of the line may even cross. The best way I have found of mitigating this is to split long arcs into shorter arcs, when that is artistically acceptable. This applies to other curves as well.

Since calligraphic lines are thinner, sometimes shapes that look good with a fixed-width stroke will look wrong with calligraphic. I have noticed this especially with arrows - often, the points of the main line will poke through the edges of the arrow. Cases like this have to be fixed manually, by moving the endpoint of the main line back out of the arrow slightly.

The calligraphic function has the same two limitations as var_line: it will quit if there is more than one moveto in the path, and it will behave unpredictably if the path goes directly back on itself (two segments meeting at a 0-degree angle).


Other functions provided

For each of the -stroke functions mentioned, I've also provided a -outline function (boltoutline, callioutline, etc.) which draws the outline of the new path. These functions take an additional argument before any base arguments, which specifies the line width of the outline. These outlines may contain some interior lines in the case of bevels or curved joins.

Examples

In addition to the illustrations on this page, I've used these functions in some other work.

Share and Enjoy

Please feel free to use this code however you like.


More PostScript Hacks
This page, code, and images by Denis Moskowitz. First posted on August 24, 2008.