What are complex strokes?

The complex stroking capability 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. LuciadCPillar offers the ComplexStrokeLineStyleComplexStrokeLineStyleComplexStrokeLineStyle API for this purpose. Using this powerful API, you can create a line in virtually any way you want.

This article explains how complex stroking works, and shows how you can build a complicated complex stroke in a sequence of steps. It applies the main complex stroking principles along the way. The end result is the following line:

stroke6
Figure 1. End result stroke

How complex strokes work

You can compose a complex stroke of the following elements:

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

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

  • Fallback stroke pattern: like the regular stroke pattern, but you use it in places where you can’t paint decorations or regular stroke patterns. It’s typically a simple pattern, like a plain line.

Sometimes, a complex stroke is interrupted, and partly skipped:

  • Obstacles When a decoration is already present somewhere along the line, it forms an obstacle for all other decorations, regular strokes or fallback stroke. At the location of the obstacle, the decoration, or the obstructed part of the regular/fallback stroke pattern, is dropped. It isn’t painted. Note that you can tweak this behavior. We also show how to do that in the following sections.

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

The next sections show how to create and compose decorations, regular stroke patterns and fallback stroke patterns in an example.

1. Painting a plain line

We start off our complex stroke with a plain line. We 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 ComplexStrokeLineStyleComplexStrokeLineStyleComplexStrokeLineStyle to paint a plain line with a width of 2 pixels.

Program: Plain line stroke
auto plainLine = ComplexStrokePatternFactory::parallelLineBuilder().lineWidth(2).build();
auto style = ComplexStrokeLineStyle::newBuilder().fallback(plainLine).build();
val plainLine = ComplexStrokePatternFactory.parallelLineBuilder().lineWidth(2.0).build()
val style = ComplexStrokeLineStyle.newBuilder().fallback(plainLine).build()
var plainLine = ComplexStrokePatternFactory.ParallelLineBuilder().LineWidth(2.0).Build();
ComplexStrokeLineStyle style = ComplexStrokeLineStyle.NewBuilder().Fallback(plainLine).Build();

In this code, we construct a ComplexStrokePatternComplexStrokePatternComplexStrokePattern 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. We set the plain line on ComplexStrokeLineStyleComplexStrokeLineStyleComplexStrokeLineStyle using the ComplexStrokeLineStyle::Builder::fallbackComplexStrokeLineStyle::Builder::fallbackComplexStrokeLineStyle::Builder::fallback setting. In this example, the plain line is visualized as the fallback stroke because nothing else has been painted:

stroke1
Figure 2. Plain line

2. Adding an arrow

Of course, we want more than a plain line.We want a line with an arrow. To create the arrow, we construct another ComplexStrokePatternComplexStrokePatternComplexStrokePattern. The following code shows how to add the arrow head to the plain line as a decoration.

Program: Arrow
auto plainLine = ComplexStrokePatternFactory::parallelLineBuilder().lineWidth(2).build();
auto arrow = ComplexStrokePatternFactory::arrowBuilder().size(32).forward(false).fillColor(Color::black()).build();
auto style = ComplexStrokeLineStyle::newBuilder().addDecoration(arrow, 0).fallback(plainLine).build();
val plainLine = ComplexStrokePatternFactory.parallelLineBuilder().lineWidth(2.0).build()
val arrow = ComplexStrokePatternFactory.arrowBuilder().size(32.0).forward(false).fillColor(Color.valueOf(Color.BLACK)).build()
val style = ComplexStrokeLineStyle.newBuilder().addDecoration(arrow, 0.0).fallback(plainLine).build()
var plainLine = ComplexStrokePatternFactory.ParallelLineBuilder().LineWidth(2.0).Build();
var arrow = ComplexStrokePatternFactory.ArrowBuilder().Size(32.0).Forward(false).FillColor(Color.Black).Build();
ComplexStrokeLineStyle style = ComplexStrokeLineStyle.NewBuilder()
    .AddDecoration(arrow, 0.0)
    .Fallback(plainLine)
    .Build();

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. Using the ComplexStrokePatternFactory::arrowBuilderComplexStrokePatternFactory::arrowBuilderComplexStrokePatternFactory::arrowBuilder, you can create a large variety of arrow heads of any color or size.

stroke2
Figure 3. Adding an arrow

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 Figure 3, “Adding an arrow”. This isn’t quite 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, though. 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 must tell the fallback stroke that it can partly overlap with the arrow head through an extra stroke. We can use ComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallback to get such an overlap. It tells the fallback stroke that it’s okay to add an extra stroke even though there is something there already.

Program: Arrow without gap
auto plainLine = ComplexStrokePatternFactory::parallelLineBuilder().lineWidth(2).build();

auto arrowShape = ComplexStrokePatternFactory::arrowBuilder().size(32).forward(false).fillColor(Color::black()).build();
auto arrowFallback = ComplexStrokePatternFactory::appendPatterns({
  ComplexStrokePatternFactory::gapFixed(16),
  ComplexStrokePatternFactory::combineWithFallback(ComplexStrokePatternFactory::gapFixed(16))});
auto arrow = ComplexStrokePatternFactory::composePatterns({arrowShape, arrowFallback});

auto style = ComplexStrokeLineStyle::newBuilder().addDecoration(arrow, 0).fallback(plainLine).build();
val plainLine = ComplexStrokePatternFactory.parallelLineBuilder().lineWidth(2.0).build()

val arrowShape = ComplexStrokePatternFactory.arrowBuilder().size(32.0).forward(false).fillColor(Color.valueOf(Color.BLACK)).build()
val arrowFallback = ComplexStrokePatternFactory.appendPatterns(
    listOf(
        ComplexStrokePatternFactory.gapFixed(16.0),
        ComplexStrokePatternFactory.combineWithFallback(ComplexStrokePatternFactory.gapFixed(16.0))
    )
)
val arrow = ComplexStrokePatternFactory.composePatterns(listOf(arrowShape, arrowFallback))

val style = ComplexStrokeLineStyle.newBuilder().addDecoration(arrow, 0.0).fallback(plainLine).build()
var plainLine = ComplexStrokePatternFactory.ParallelLineBuilder().LineWidth(2.0).Build();

var arrowShape = ComplexStrokePatternFactory.ArrowBuilder().Size(32.0).Forward(false).FillColor(Color.Black).Build();
var arrowFallback = ComplexStrokePatternFactory.AppendPatterns(new List<ComplexStrokePattern>
{
    ComplexStrokePatternFactory.GapFixed(16.0),
    ComplexStrokePatternFactory.CombineWithFallback(ComplexStrokePatternFactory.GapFixed(16.0))
});
var arrow = ComplexStrokePatternFactory.ComposePatterns(
    new List<ComplexStrokePattern> { arrowShape, arrowFallback });

ComplexStrokeLineStyle style = ComplexStrokeLineStyle.NewBuilder()
    .AddDecoration(arrow, 0.0)
    .Fallback(plainLine)
    .Build();

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 ComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallback. The ComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallback method makes it so that 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 fallback pattern is painted in the second half of the arrow. Next, the arrow shape and the transparent stroke, which has the ComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallbackComplexStrokePatternFactory::combineWithFallback method, are composed to form one ComplexStrokePatternComplexStrokePatternComplexStrokePattern. The resulting decoration is added to the complex stroke style.

stroke3
Figure 4. Filling the gap

4. Creating a custom arrow

What if you can’t find the right arrow head for the job within the set of default arrow heads?

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 ComplexStrokePatternFactory::polylineBuilderComplexStrokePatternFactory::polylineBuilderComplexStrokePatternFactory::polylineBuilder, which creates a stroke pattern that consists of several line stroke patterns:

Program: Custom arrow
auto plainLine = ComplexStrokePatternFactory::parallelLineBuilder().lineWidth(2).build();

std::vector<Coordinate> points = {{0, 0}, {16, 16}, {32, 16}, {16, 0}, {32, -16}, {16, -16}, {0, 0}};
auto arrowShape = ComplexStrokePatternFactory::polylineBuilder().points(points).lineWidth(2).build();
auto arrowFallback = ComplexStrokePatternFactory::appendPatterns({
  ComplexStrokePatternFactory::gapFixed(16),
  ComplexStrokePatternFactory::combineWithFallback(ComplexStrokePatternFactory::gapFixed(16))});
auto arrow = ComplexStrokePatternFactory::composePatterns({arrowShape, arrowFallback});

auto style = ComplexStrokeLineStyle::newBuilder().addDecoration(arrow, 0).fallback(plainLine).build();
val plainLine = ComplexStrokePatternFactory.parallelLineBuilder().lineWidth(2.0).build()

val points = listOf(
    Coordinate(0.0, 0.0),
    Coordinate(16.0, 16.0),
    Coordinate(32.0, 16.0),
    Coordinate(16.0, 0.0),
    Coordinate(32.0, -16.0),
    Coordinate(16.0, -16.0),
    Coordinate(0.0, 0.0)
)
val arrowShape = ComplexStrokePatternFactory.polylineBuilder().points(points).lineWidth(2.0).build()
val arrowFallback = ComplexStrokePatternFactory.appendPatterns(
    listOf(
        ComplexStrokePatternFactory.gapFixed(16.0),
        ComplexStrokePatternFactory.combineWithFallback(ComplexStrokePatternFactory.gapFixed(16.0))
    )
)
val arrow = ComplexStrokePatternFactory.composePatterns(listOf(arrowShape, arrowFallback))

val style = ComplexStrokeLineStyle.newBuilder().addDecoration(arrow, 0.0).fallback(plainLine).build()
var plainLine = ComplexStrokePatternFactory.ParallelLineBuilder().LineWidth(2.0).Build();

List<Coordinate> points = new List<Coordinate>
{
    new Coordinate(0.0, 0.0),
    new Coordinate(16.0, 16.0),
    new Coordinate(32.0, 16.0),
    new Coordinate(16.0, 0.0),
    new Coordinate(32.0, -16.0),
    new Coordinate(16.0, -16.0),
    new Coordinate(0.0, 0.0)
};
var arrowShape = ComplexStrokePatternFactory.PolylineBuilder().Points(points).LineWidth(2.0).Build();
var arrowFallback = ComplexStrokePatternFactory.AppendPatterns(new List<ComplexStrokePattern>
{
    ComplexStrokePatternFactory.GapFixed(16.0),
    ComplexStrokePatternFactory.CombineWithFallback(ComplexStrokePatternFactory.GapFixed(16.0))
});
var arrow = ComplexStrokePatternFactory.ComposePatterns(
    new List<ComplexStrokePattern> { arrowShape, arrowFallback });

ComplexStrokeLineStyle style = ComplexStrokeLineStyle.NewBuilder()
    .AddDecoration(arrow, 0.0)
    .Fallback(plainLine)
    .Build();

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

stroke4
Figure 5. Custom arrow

5. Adding a repeating pattern along the line

It’s 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 ComplexStrokeLineStyle::Builder::regularComplexStrokeLineStyle::Builder::regularComplexStrokeLineStyle::Builder::regular. 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.

Program: Repeating pattern
auto plainLine = ComplexStrokePatternFactory::parallelLineBuilder().lineWidth(2).build();

std::vector<Coordinate> points = {{0, 0}, {16, 16}, {32, 16}, {16, 0}, {32, -16}, {16, -16}, {0, 0}};
auto arrowShape = ComplexStrokePatternFactory::polylineBuilder().points(points).lineWidth(2).build();
auto arrowFallback = ComplexStrokePatternFactory::appendPatterns({
  ComplexStrokePatternFactory::gapFixed(16),
  ComplexStrokePatternFactory::combineWithFallback(ComplexStrokePatternFactory::gapFixed(16))});
auto arrow = ComplexStrokePatternFactory::composePatterns({arrowShape, arrowFallback});

auto zigUp = ComplexStrokePatternFactory::lineBuilder().offset1(5).fixedLength(8).lineWidth(2).build();
auto zigDown = ComplexStrokePatternFactory::lineBuilder().offset0(5).fixedLength(8).lineWidth(2).build();
auto zig = ComplexStrokePatternFactory::atomic(ComplexStrokePatternFactory::appendPatterns({zigUp, zigDown}));
auto zagDown = ComplexStrokePatternFactory::lineBuilder().offset1(-5).fixedLength(8).lineWidth(2).build();
auto zagUp = ComplexStrokePatternFactory::lineBuilder().offset0(-5).fixedLength(8).lineWidth(2).build();
auto zag = ComplexStrokePatternFactory::atomic(ComplexStrokePatternFactory::appendPatterns({zagDown, zagUp}));
auto zigzag = ComplexStrokePatternFactory::appendPatterns({zig, zag});

auto style = ComplexStrokeLineStyle::newBuilder()
  .addDecoration(arrow, 0)
  .regular(zigzag)
  .fallback(plainLine)
  .build();
val plainLine = ComplexStrokePatternFactory.parallelLineBuilder().lineWidth(2.0).build()

val points = listOf(
    Coordinate(0.0, 0.0),
    Coordinate(16.0, 16.0),
    Coordinate(32.0, 16.0),
    Coordinate(16.0, 0.0),
    Coordinate(32.0, -16.0),
    Coordinate(16.0, -16.0),
    Coordinate(0.0, 0.0)
)
val arrowShape = ComplexStrokePatternFactory.polylineBuilder().points(points).lineWidth(2.0).build()
val arrowFallback = ComplexStrokePatternFactory.appendPatterns(
    listOf(
        ComplexStrokePatternFactory.gapFixed(16.0),
        ComplexStrokePatternFactory.combineWithFallback(ComplexStrokePatternFactory.gapFixed(16.0))
    )
)
val arrow = ComplexStrokePatternFactory.composePatterns(listOf(arrowShape, arrowFallback))

val zigUp = ComplexStrokePatternFactory.lineBuilder().offset1(5.0).fixedLength(8.0).lineWidth(2.0).build()
val zigDown = ComplexStrokePatternFactory.lineBuilder().offset0(5.0).fixedLength(8.0).lineWidth(2.0).build()
val zig = ComplexStrokePatternFactory.atomic(ComplexStrokePatternFactory.appendPatterns(listOf(zigUp, zigDown)))
val zagDown = ComplexStrokePatternFactory.lineBuilder().offset1(-5.0).fixedLength(8.0).lineWidth(2.0).build()
val zagUp = ComplexStrokePatternFactory.lineBuilder().offset0(-5.0).fixedLength(8.0).lineWidth(2.0).build()
val zag = ComplexStrokePatternFactory.atomic(ComplexStrokePatternFactory.appendPatterns(listOf(zagDown, zagUp)))
val zigzag = ComplexStrokePatternFactory.appendPatterns(listOf(zig, zag))

val style =ComplexStrokeLineStyle.newBuilder()
    .addDecoration(arrow, 0.0)
    .regular(zigzag)
    .fallback(plainLine).build()
var plainLine = ComplexStrokePatternFactory.ParallelLineBuilder().LineWidth(2.0).Build();

List<Coordinate> points = new List<Coordinate>
{
    new Coordinate(0.0, 0.0),
    new Coordinate(16.0, 16.0),
    new Coordinate(32.0, 16.0),
    new Coordinate(16.0, 0.0),
    new Coordinate(32.0, -16.0),
    new Coordinate(16.0, -16.0),
    new Coordinate(0.0, 0.0)
};
var arrowShape = ComplexStrokePatternFactory.PolylineBuilder().Points(points).LineWidth(2.0).Build();
var arrowFallback = ComplexStrokePatternFactory.AppendPatterns(new List<ComplexStrokePattern>
{
    ComplexStrokePatternFactory.GapFixed(16.0),
    ComplexStrokePatternFactory.CombineWithFallback(ComplexStrokePatternFactory.GapFixed(16.0))
});
var arrow = ComplexStrokePatternFactory.ComposePatterns(
    new List<ComplexStrokePattern> { arrowShape, arrowFallback });

var zigUp = ComplexStrokePatternFactory.LineBuilder().Offset1(5).FixedLength(8).LineWidth(2).Build();
var zigDown = ComplexStrokePatternFactory.LineBuilder().Offset0(5).FixedLength(8).LineWidth(2).Build();
var zig = ComplexStrokePatternFactory.Atomic(ComplexStrokePatternFactory.AppendPatterns(
    new List<ComplexStrokePattern> { zigUp, zigDown }));
var zagDown = ComplexStrokePatternFactory.LineBuilder().Offset1(-5).FixedLength(8).LineWidth(2).Build();
var zagUp = ComplexStrokePatternFactory.LineBuilder().Offset0(-5).FixedLength(8).LineWidth(2).Build();
var zag = ComplexStrokePatternFactory.Atomic(ComplexStrokePatternFactory.AppendPatterns(
    new List<ComplexStrokePattern> { zagDown, zagUp }));
var zigzag = ComplexStrokePatternFactory.AppendPatterns(new List<ComplexStrokePattern> { zig, zag });

ComplexStrokeLineStyle style = ComplexStrokeLineStyle.NewBuilder()
    .AddDecoration(arrow, 0.0)
    .Regular(zigzag)
    .Fallback(plainLine).Build();

In this example, a zigzag pattern is applied to the line. The zigzag pattern is a composition of four line patterns, grouped in pairs. The first pair goes above the line, the second pair goes under it. Because each pair is atomic, the line always returns to the baseline, even if there is an obstacle.

Splitting the pattern into two atomic groups makes it possible to drop just a part of the zigzag when the stroke encounters an obstacle or a corner. One part of the zigzag is replaced with the fallback line, while the other part remains visible.

stroke5
Figure 6. Repeating pattern

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 is atomic, meaning that the gaps and the text are treated as one unit. As a result, both the text and the gaps are dropped when the text can’t be placed, for example because it crosses a corner of the line.

Program: Text decoration
auto plainLine = ComplexStrokePatternFactory::parallelLineBuilder().lineWidth(2).build();

std::vector<Coordinate> points = {{0, 0}, {16, 16}, {32, 16}, {16, 0}, {32, -16}, {16, -16}, {0, 0}};
auto arrowShape = ComplexStrokePatternFactory::polylineBuilder().points(points).lineWidth(2).build();
auto arrowFallback = ComplexStrokePatternFactory::appendPatterns({
  ComplexStrokePatternFactory::gapFixed(16),
  ComplexStrokePatternFactory::combineWithFallback(ComplexStrokePatternFactory::gapFixed(16))});
auto arrow = ComplexStrokePatternFactory::composePatterns({arrowShape, arrowFallback});

auto zigUp = ComplexStrokePatternFactory::lineBuilder().offset1(5).fixedLength(8).lineWidth(2).build();
auto zigDown = ComplexStrokePatternFactory::lineBuilder().offset0(5).fixedLength(8).lineWidth(2).build();
auto zig = ComplexStrokePatternFactory::atomic(ComplexStrokePatternFactory::appendPatterns({zigUp, zigDown}));
auto zagDown = ComplexStrokePatternFactory::lineBuilder().offset1(-5).fixedLength(8).lineWidth(2).build();
auto zagUp = ComplexStrokePatternFactory::lineBuilder().offset0(-5).fixedLength(8).lineWidth(2).build();
auto zag = ComplexStrokePatternFactory::atomic(ComplexStrokePatternFactory::appendPatterns({zagDown, zagUp}));
auto zigzag = ComplexStrokePatternFactory::appendPatterns({zig, zag});

auto textStyle = TextStyle::newBuilder().fontSize(18).fontName("Arial").haloWidth(0).build();
auto text = ComplexStrokePatternFactory::atomic(ComplexStrokePatternFactory::appendPatterns({
  ComplexStrokePatternFactory::gapFixed(8),
  ComplexStrokePatternFactory::textBuilder().text("label").textStyle(textStyle).build(),
  ComplexStrokePatternFactory::gapFixed(8)}));

auto style = ComplexStrokeLineStyle::newBuilder()
  .addDecoration(arrow, 0)
  .addDecoration(text, 0.75)
  .regular(zigzag)
  .fallback(plainLine)
  .build();
val plainLine = ComplexStrokePatternFactory.parallelLineBuilder().lineWidth(2.0).build()

val points = listOf(
    Coordinate(0.0, 0.0),
    Coordinate(16.0, 16.0),
    Coordinate(32.0, 16.0),
    Coordinate(16.0, 0.0),
    Coordinate(32.0, -16.0),
    Coordinate(16.0, -16.0),
    Coordinate(0.0, 0.0)
)
val arrowShape = ComplexStrokePatternFactory.polylineBuilder().points(points).lineWidth(2.0).build()
val arrowFallback = ComplexStrokePatternFactory.appendPatterns(
    listOf(
        ComplexStrokePatternFactory.gapFixed(16.0),
        ComplexStrokePatternFactory.combineWithFallback(ComplexStrokePatternFactory.gapFixed(16.0))
    )
)
val arrow = ComplexStrokePatternFactory.composePatterns(listOf(arrowShape, arrowFallback))

val zigUp = ComplexStrokePatternFactory.lineBuilder().offset1(5.0).fixedLength(8.0).lineWidth(2.0).build()
val zigDown = ComplexStrokePatternFactory.lineBuilder().offset0(5.0).fixedLength(8.0).lineWidth(2.0).build()
val zig = ComplexStrokePatternFactory.atomic(ComplexStrokePatternFactory.appendPatterns(listOf(zigUp, zigDown)))
val zagDown = ComplexStrokePatternFactory.lineBuilder().offset1(-5.0).fixedLength(8.0).lineWidth(2.0).build()
val zagUp = ComplexStrokePatternFactory.lineBuilder().offset0(-5.0).fixedLength(8.0).lineWidth(2.0).build()
val zag = ComplexStrokePatternFactory.atomic(ComplexStrokePatternFactory.appendPatterns(listOf(zagDown, zagUp)))
val zigzag = ComplexStrokePatternFactory.appendPatterns(listOf(zig, zag))

val textStyle = TextStyle.newBuilder().fontSize(18).fontName("Arial").haloWidth(0.0).build()
val text = ComplexStrokePatternFactory.atomic(
    ComplexStrokePatternFactory.appendPatterns(
        listOf(
            ComplexStrokePatternFactory.gapFixed(8.0),
            ComplexStrokePatternFactory.textBuilder().text("label").textStyle(textStyle).build(),
            ComplexStrokePatternFactory.gapFixed(8.0)
        )
    )
)
val style = ComplexStrokeLineStyle.newBuilder()
    .addDecoration(arrow, 0.0)
    .addDecoration(text, 0.75)
    .regular(zigzag)
    .fallback(plainLine)
    .build()
var plainLine = ComplexStrokePatternFactory.ParallelLineBuilder().LineWidth(2.0).Build();

List<Coordinate> points = new List<Coordinate>
{
    new Coordinate(0.0, 0.0),
    new Coordinate(16.0, 16.0),
    new Coordinate(32.0, 16.0),
    new Coordinate(16.0, 0.0),
    new Coordinate(32.0, -16.0),
    new Coordinate(16.0, -16.0),
    new Coordinate(0.0, 0.0)
};
var arrowShape = ComplexStrokePatternFactory.PolylineBuilder().Points(points).LineWidth(2.0).Build();
var arrowFallback = ComplexStrokePatternFactory.AppendPatterns(new List<ComplexStrokePattern>
{
    ComplexStrokePatternFactory.GapFixed(16.0),
    ComplexStrokePatternFactory.CombineWithFallback(ComplexStrokePatternFactory.GapFixed(16.0))
});
var arrow = ComplexStrokePatternFactory.ComposePatterns(
    new List<ComplexStrokePattern> { arrowShape, arrowFallback });

var zigUp = ComplexStrokePatternFactory.LineBuilder().Offset1(5).FixedLength(8).LineWidth(2).Build();
var zigDown = ComplexStrokePatternFactory.LineBuilder().Offset0(5).FixedLength(8).LineWidth(2).Build();
var zig = ComplexStrokePatternFactory.Atomic(ComplexStrokePatternFactory.AppendPatterns(
    new List<ComplexStrokePattern> { zigUp, zigDown }));
var zagDown = ComplexStrokePatternFactory.LineBuilder().Offset1(-5).FixedLength(8).LineWidth(2).Build();
var zagUp = ComplexStrokePatternFactory.LineBuilder().Offset0(-5).FixedLength(8).LineWidth(2).Build();
var zag = ComplexStrokePatternFactory.Atomic(ComplexStrokePatternFactory.AppendPatterns(
    new List<ComplexStrokePattern> { zagDown, zagUp }));
var zigzag = ComplexStrokePatternFactory.AppendPatterns(new List<ComplexStrokePattern> { zig, zag });

TextStyle textStyle = TextStyle.NewBuilder().FontSize(18).FontName("Arial").HaloWidth(0.0).Build();
var text = ComplexStrokePatternFactory.Atomic(ComplexStrokePatternFactory.AppendPatterns(
    new List<ComplexStrokePattern>
    {
        ComplexStrokePatternFactory.GapFixed(8.0),
        ComplexStrokePatternFactory.TextBuilder().Text("label").TextStyle(textStyle).Build(),
        ComplexStrokePatternFactory.GapFixed(8.0)
    }));

ComplexStrokeLineStyle style = ComplexStrokeLineStyle.NewBuilder()
    .AddDecoration(arrow, 0.0)
    .AddDecoration(text, 0.75)
    .Regular(zigzag)
    .Fallback(plainLine)
    .Build();

Of course you can add other patterns and decorations as well. This article doesn’t mention all available complex stroke building blocks. For those, have a look at the reference documentation of the ComplexStrokeLineStyleComplexStrokeLineStyleComplexStrokeLineStyle and ComplexStrokePatternFactoryComplexStrokePatternFactoryComplexStrokePatternFactory.

stroke6
Figure 7. Text decoration