What are complex strokes?

Complex stroking functionality makes it possible to visualize lines constructed from repeating patterns and decorations. An example is a zigzag line with an arrow, or with text decorations. LuciadRIA offers the ComplexStrokedLineStyle API for this purpose. This API is very powerful, and allows you to create a line in virtually any way you want.

This article explains how complex stroking works, and shows in a number of steps how a complicated complex stroke can be built. The main complex stroking concepts are applied along the way. The end result will be the following line:

stroke6

How do complex strokes work?

You can compose a complex stroke of the following elements:

  • Decorations Small non-repeating elements that are added at a specific location on a line, such as arrow heads, text decorations, and icons.

  • Regular stroke pattern A pattern that is repeated along the entire line, zigzag lines, wavy lines or dash patterns, for example.

  • Fallback stroke pattern Similar to the regular stroke pattern, but it is used in places where no decorations or regular stroke pattern can be painted. It is typically a very simple pattern, like a plain line.

In some cases, a complex stroke is interrupted, and partially skipped:

  • Obstacles When a decoration is already painted somewhere along the line, it forms an obstacle for all other other decorations, regular strokes or fallback stroke. Consequently, the decoration, or the obstructed part of the regular/fallback stroke pattern, is dropped and will not be painted at that location. Note that it is possible to tweak this behavior. This is demonstrated below.

  • Corners when a decoration or regular stroke patterns crosses a sharp corner of the line, it is dropped.

The following section shows an example of how to create and compose decorations, regular stroke patterns and fallback stroke patterns.

1. Painting a plain line

We start off our complex stroke with a plain line. We will use this line as our fallback while we proceed with the construction of a more complex stroke. The following code shows how you can use a ComplexStrokedLineStyle to paint a plain line with a width of 2 pixels.

var plainLine = PatternFactory.parallelLine({line: {width: 2, color: "rgb(0,0,200)"}});
return { fallback: plainLine };

In this code, we construct a Pattern that represents a plain line. We want to use the plain line as a fallback stroke, to make sure that a line is painted whenever nothing else is painted The plain line is set on ComplexStrokedLineStyle using the fallback property. In this example, the plain line is visualized as the fallback stroke because nothing else has been painted:

stroke1

2. Adding an arrow

Of course, we are interested in more than just a plain line. Otherwise, we could just have used LineStyle. We want a line with an arrow. To create the arrow, we construct another Pattern. The following code shows how to add the arrow head to the plain line as a decoration.

var plainLine = PatternFactory.parallelLine({line: {width: 2, color: "rgb(0,0,200)"}});
var arrow = PatternFactory.arrow({size: 32, forward: false, fill: {color: "rgb(0,0,200)"}});
return {
    decorations: [{
        location: 0,
        pattern: arrow
    }],
    fallback: plainLine
};

The code adds a default arrow head at the start of the line. The arrow head has a length of 32 pixels, a black color, and points to the start of the line. The PatternFactory.arrow() method allows you to create a large variety of arrow heads of any color or size.

stroke2

The result is an arrow head decoration at the start of the complex stroke, followed by the plain fallback line. The fallback line is there only because we haven’t defined our regular stroke yet.

3. Filling the gap between the arrow and the line

You may have noticed that there is a small gap between the arrow head and the line in the image above. This is not exactly what we want. So how do we fill this gap?

Remember that fallback strokes are only painted whenever nothing else is painted. In this case, the arrow has a length of 32 pixels, but the arrow base has an indentation so that its center lies a few pixels closer to the tip of the arrow. The fallback stroke doesn’t know that, however. It only detects that the arrow is 32 pixels wide, and stops a few pixels short of the arrow base because it thinks something is there already. To connect the line to the arrow, we have to tell the fallback stroke that it can partially overlap with the arrow head through an additional stroke. We can use the combineWithFallback() method to accomplish such an overlap. It tells the fallback stroke that it is okay to add an extra stroke even though there is something there already.

var plainLine = PatternFactory.parallelLine({line: {width: 2, color: "rgb(0,0,200)"}});
var arrowShape = PatternFactory.arrow({size: 32, forward: false, fill: {color: "rgb(0,0,200)"}});
var arrowFallback = PatternFactory.append([PatternFactory.gap(16), PatternFactory.combineWithFallback(PatternFactory.gap(16))]);
var arrow = PatternFactory.compose([arrowShape, arrowFallback]);

return {
    decorations: [{
        location: 0,
        pattern: arrow
    }],
    fallback: plainLine
};

The code creates a transparent stroke of the same length as the arrow head, 32 pixels. This is the arrow fallback line. It consists of one gap stroke of 16 pixels concatenated with another stroke of 16 pixels created with the combineWithFallback() method. The combineWithFallback() method makes it so the fallback pattern is painted, whenever nothing is painted by the pattern that’s passed in. By passing a (transparent) gap of 16 pixels, we ensure that the the fallback pattern is painted in the second half of the arrow. Next, the arrow shape and the transparent stroke, which contains the combineWithFallback() method, are composed to form one Pattern. The resulting decoration is added to the complex stroke style.

stroke3

4. Creating a custom arrow

What if the set of default arrow heads doesn’t contain the right one for the job?

In that case, you can create your own arrow. You can do so by appending or composing other basic Pattern building blocks, like lines. The example shows how to use the polyline() method, which creates a stroke pattern that is composed of multiple line stroke patterns:

var lineStyle = {width: 2, color: "rgb(0,0,200)"};
var plainLine = PatternFactory.parallelLine({line: lineStyle});

var points = [[0, 0], [16, 16], [32, 16], [16, 0], [32, -16], [16, -16], [0, 0]];
var arrowShape = PatternFactory.polyline({points: points, line: lineStyle});
var arrowFallback = PatternFactory.append([PatternFactory.gap(16), PatternFactory.combineWithFallback(PatternFactory.gap(16))]);
var arrow = PatternFactory.compose([arrowShape, arrowFallback]);

return {
    decorations: [{
        location: 0,
        pattern: arrow
    }],
    fallback: plainLine
};

This code defines a polyline, consisting of 6 line segments. The resulting custom arrow is 32 pixels wide as well.

stroke4

5. Adding a repeating pattern along the line

It is often useful to repeat a pattern along the line, to turn it into a symbol with a specific meaning for example. To repeat a pattern, you can create a regular stroke using the regular property. This method repeats a pattern along the entire line. Remember that parts of the regular stroke may be dropped if a decoration is already present, or when the stroke passes through a corner of the line.

var lineStyle = {width: 2, color: "rgb(0,0,200)"};
var plainLine = PatternFactory.parallelLine({line: lineStyle});

var points = [[0, 0], [16, 16], [32, 16], [16, 0], [32, -16], [16, -16], [0, 0]];
var arrowShape = PatternFactory.polyline({points: points, line: lineStyle});
var arrowFallback = PatternFactory.append([PatternFactory.gap(16), PatternFactory.combineWithFallback(PatternFactory.gap(16))]);
var arrow = PatternFactory.compose([arrowShape, arrowFallback]);

var wave1 = PatternFactory.wave({length: 16, amplitude: 4, startAngle: 0, angle: 180, line: lineStyle});
var wave2 = PatternFactory.wave({length: 16, amplitude: 4, startAngle: 180, angle: 180, line: lineStyle});
var wavePattern = PatternFactory.append([wave1, wave2]);

return {
    decorations: [{
        location: 0,
        pattern: arrow
    }],
    regular: wavePattern,
    fallback: plainLine
};

In this example, a wave pattern is applied to the line. A wave consists of an upper arc and a lower arc. Those two wave parts are created first. Next, they are appended to form a complete wave. Such an approach makes it possible to drop just a part of the wave when the stroke encounters an obstacle or a corner. One part of the wave is replaced with the fallback line, while the other part remains visible.

stroke5

6. Adding a text decoration

This step adds a text decoration at a location along the line. The text decoration is surrounded by gaps of 8 pixels.

Note that the combination of the gaps and the text has been made atomic, meaning that the gaps and the text will be treated as one unit. As a result, both the text and the gaps will be dropped when the text cannot be placed, for example because it crosses a corner of the line.

var color = "rgb(0,0,200)";
var lineStyle = {width: 2, color: color};
var plainLine = PatternFactory.parallelLine({line: lineStyle});

var points = [[0, 0], [16, 16], [32, 16], [16, 0], [32, -16], [16, -16], [0, 0]];
var arrowShape = PatternFactory.polyline({points: points, line: lineStyle});
var arrowFallback = PatternFactory.append(
    [PatternFactory.gap(16), PatternFactory.combineWithFallback(PatternFactory.gap(16))]);
var arrow = PatternFactory.compose([arrowShape, arrowFallback]);

var wave1 = PatternFactory.wave({length: 16, amplitude: 4, startAngle: 0, angle: 180, line: lineStyle});
var wave2 = PatternFactory.wave({length: 16, amplitude: 4, startAngle: 180, angle: 180, line: lineStyle});
var wavePattern = PatternFactory.append([wave1, wave2]);

var textStyle = {
  font: "18px Arial, sans-serif",
  fill: color
};
var text = PatternFactory.atomic(PatternFactory.append([
  PatternFactory.gap(8),
  PatternFactory.text({text: ["Text"], textStyle: textStyle}),
  PatternFactory.gap(8)
]));
return {
    decorations: [{
        location: 0,
        pattern: arrow
    }, {
        location: 0.75,
        pattern: text
    }],
    regular: wavePattern,
    fallback: plainLine
};

Of course you can add other patterns and decorations as well. There are many complex stroke building blocks that were not mentioned in this article. For those, have a look at the JSDoc of the ComplexStrokedLineStyle and PatternFactory classes.

stroke6