Tutorial 06: Stroking

Using OpenVG API it is possible to draw vector paths by filling and/or stroking them. The concept of path filling is really simple:

Path filling coincides with the filling of the inside region. Path stroking, instead, is a more complex topic, so lets introduce it.


Path stroking in OpenVG

Stroking a path consists of “widening” the edges of the path using a straight-line pen held perpendicularly to the path. In addition:

Conceptually, stroking of a path is performed in two steps. First, the stroke parameters are applied in the user coordinate system to form a new shape representing the end result of dashing, widening the path, and applying the end cap and line join styles. Second, a path is created that defines the outline of this stroked shape. This path is transformed using the path-user-to-surface transformation (VG_MATRIX_PATH_USER_TO_SURFACE matrix). Finally, the resulting path is filled with paint in exactly the same manner as when filling a user-defined path using the non-zero fill rule.


Stroke parameters

Stroking a path involves the following parameters, set on the current OpenVG context:

These parameters are set using the variants of the vgSet function: the values most recently set prior to calling vgDrawPath are applied to generate the stroke. Here’s an example:

// a simple dash pattern "on", "off", "on", "off"
float dashPattern[4] = { 5.0f, 55.0f, 35.0f, 35.0f };
// stroke parameters
vgSetf(VG_STROKE_LINE_WIDTH, 12.0f);
vgSeti(VG_STROKE_CAP_STYLE, VG_CAP_ROUND);
vgSeti(VG_STROKE_JOIN_STYLE, VG_JOIN_MITER);
vgSetf(VG_STROKE_MITER_LIMIT, 4.0f);
vgSetfv(VG_STROKE_DASH_PATTERN, 4, dashPattern);
vgSetf(VG_STROKE_DASH_PHASE, 0.0f);
vgSeti(VG_STROKE_DASH_PHASE_RESET, VG_TRUE);
vgDrawPath(clover, VG_STROKE_PATH);
Setting stroke parameters
Setting stroke parameters

End cap styles

End cap styles
End cap styles

Line join styles

Join styles
Join styles

The ratio of miter length to line width may be computed directly from the angle θ between the two line segments being joined as 1 / sin(θ/2). A number of angles with their corresponding miter limits for a line width of 1 are shown in the following table:

Angle (degrees)Miter limit
10.0011.47
11.4710.00
23.005.00
28.954.00
30.003.86
38.943.00
45.002.61
60.002.00
90.001.41
120.001.15
150.001.03
180.001.00

Dashing

The dash pattern consists of a sequence of lengths of alternating “on” and “off” dash segments. The first value of the dash array defines the length, in user coordinates, of the first “on” dash segment. The second value defines the length of the following “off” segment. Each subsequent pair of values defines one “on” and one “off” segment. The dash phase defines the starting point in the dash pattern that is associated with the start of the first segment of the path (a negative dash phase is equivalent to the positive phase obtained by adding a suitable multiple of the dash pattern length).

For example, if the dash pattern is [ 10 20 30 40 ] and the dash phase is 35, the path will be stroked with an “on” segment of length 25 (skipping the first “on” segment of length 10, the following “off” segment of length 20, and the first 5 units of the next “on” segment), followed by an “off” segment of length 40. The pattern will then repeat from the beginning, with an “on” segment of length 10, an “off” segment of length 20, an “on” segment of length 30, etc.

Dashing
Dashing

Conceptually, dashing is performed by breaking the path into a set of subpaths according to the dash pattern. Each subpath is then drawn independently using the end cap, line join style, and miter limit that were set for the path as a whole.


The tutorial code

The tutorial draws a clover-like path at the center of drawing surface, stroked with a plain color. The path, consisting in four cubic Bézier segments, is defined in user-space within a square region having a side of 512 units: center (0, 0), lower-left corner (-256, -256), upper-right corner (256, 256).

By having defined the path in this way, it’s really easy to scale and center it in order to fit the drawing surface:

// find the minimum dimension between surface width and height, then halve it
int halfDim = (surfaceWidth < surfaceHeight) ? (surfaceWidth / 2) : (surfaceHeight / 2);
// calculate scale factor in order to cover 90% of it
userToSurfaceScale = (halfDim / 256.0f) * 0.9f;
// translate to the surface center
userToSurfaceTranslation[X] = surfaceWidth / 2;
userToSurfaceTranslation[Y] = surfaceHeight / 2;

// "user to surface" transformation (a uniform scale plus a translation),
// upload the matrix to the OpenVG backend
vgSeti(VG_MATRIX_MODE, VG_MATRIX_PATH_USER_TO_SURFACE);
vgLoadIdentity();
vgTranslate(userToSurfaceTranslation[X], userToSurfaceTranslation[Y]);
vgScale(userToSurfaceScale, userToSurfaceScale);

So we can implement a simple map from the path coordinates system to the drawing surface system as follow:

// Map a point from the path coordinates system to the drawing surface system.
PathToSurface(pathPoint) = (pathPoint * userToSurfaceScale) + userToSurfaceTranslation

The inverse transformation maps a point from the drawing surface coordinates system to the path system:

// Map a point from the drawing surface system to the path reference system.
SurfaceToPath(surfacePoint) = (surfacePoint - userToSurfaceTranslation) / userToSurfaceScale

The path is drawn using the current stroke parameters; their values are stored to global variables and uploaded to the OpenVG backend when needed:

// available dash patterns
const float dashPatterns[4][4] = {
    { 30.0f, 30.0f,  5.0f, 45.0f },
    { 15.0f, 40.0f, 15.0f, 40.0f },
    { 20.0f, 25.0f, 20.0f, 45.0f },
    {  5.0f, 45.0f, 35.0f, 25.0f }
};

// an index pointing to one of the four available 
// dash patterns (i.e. valid values: 0, 1, 2, 3)
int dashPattern;
// dash phase
float dashPhase;
// join style
VGJoinStyle joinStyle;
// cap style
VGCapStyle capStyle;

The user can play with such stroke parameters and, at the same time, move path control points. About the control points:

Solid stroke in the tutorial app
Solid stroke in the tutorial app

Stroke extensions

AmanithVG implements an extension to the OpenVG API relative to stroke parameters: VG_MZT_separable_cap_style (see extensions chapter for additional details). OpenVG specifications provide a way to set a single cap style, that will be used for both start-cap and end-cap in a dashed stroke. Through this extension, instead, it is possible to independently specify a different style for start-cap and end-cap.

The tutorial application will check (at runtime) the support of VG_MZT_separable_cap_style extension and, if found, will enable the user to modify both start-cap and end-cap styles.

// global variables used to store cap styles
VGCapStyle startCapStyle;
VGCapStyle endCapStyle;

void extensionsCheck(void) {
    // get the list of supported OpenVG extensions
    const char* extensions = (const char*)vgGetString(VG_EXTENSIONS);
    // check for the support of VG_MZT_separable_cap_style extension
    separableCapsSupported = extensionFind("VG_MZT_separable_cap_style", extensions);
}

// set cap style(s)
if (separableCapsSupported) {
    vgSeti(VG_STROKE_START_CAP_STYLE_MZT, startCapStyle);
    vgSeti(VG_STROKE_END_CAP_STYLE_MZT, endCapStyle);
}
else {
    vgSeti(VG_STROKE_CAP_STYLE, startCapStyle);
}
start cap butt, end cap round
start cap butt, end cap round