Dashed Lines and Rasterization#

Base Dashed Line#

../_images/zigl_dash.png

Dashed Lines made by Zigl Algorithm#

Note the square patterns

Now that we have the scripts for lines, antialiased lines and thick antialiased lines let's see if we can produce a good set of dashed lines. To start let's choose the plain rasterized line, there is not much to choose between the Bresenham and Zigl algorithms, other than the reason that Zigl's algorithm will be extended for antialiasing and thick line antialiasing.

With direct access to the line drawing algorithm we can simply switch the plotting on and off. Use the dash tuple to create a list (pattern) of 1's and 0's corresponding to the lengths of the dashes and spaces. Within the range part add an if clause to plot only when the list element corresponds to a 1. The list is simply cycled within the for loop.

Show/Hide Dashed Line One Pixel Wide
def zigl_dash(dr, pta, ptb, dash=(), fill='black'):
    # check dash input
    if len(dash)%2 == 0 or len(dash) ==1:
        pass
    else:
        raise Exception('The dash tuple: {} should be one or an equal number '\
                    'of entries'.format(dash))

    dash = dash + dash if len(dash) == 1 else dash # single entry

    sum_dash = sum(dash)

    pattern = []
    while len(dash) > 0:
        dash0, *dash = dash
        pattern.extend([1] * dash0) # dashes
        dash0, *dash = dash
        pattern.extend([0] * dash0) # spaces
    if not pattern:
        pattern.append(1)

    count = 0

    x0, y0 = pta
    x1, y1 = ptb

    dx = x1 - x0
    dy = y1 - y0

    xsign = 1 if dx > 0 else -1
    ysign = 1 if dy > 0 else -1

    dx = abs(dx)
    dy = abs(dy)

    err = dx - dy
    dr = dx + 1 if dx > dy else dy + 1

    for x in range (dr):
        if pattern[count] == 1:
            draw.point([x0, y0], fill= fill)
        e2 = err<<1

        if e2 >= -dx:
            err -= dy
            x0 += sx

        if e2 <= dy:
            err += dx
            y0 += sy

        count = count + 1 if count < sum_dash - 1 else 0

It should be noted how straightforward the creation of dashed lines is made by using rasterization, apart from adding a list that stores the pattern with a count based on this list. The question of changing dash and space lengths has not been encountered.

Adjust for Slope#

As of yet no attempt has been made to regulate the dash and space sizes relative to their orientation, if adjusted then the rectangular pattern in the figure above will become more circular.

../_images/zigl_dash_adjust.png

Adjusted Dashed Lines made by Zigl Algorithm#

The square pattern is less pronounced

Alter the lengths of the dash and space elements according to the slope of the line, keeping the same overall length. This means that when close to 45° the dashes will shorten. Modify the script above, add an adjust option, then when this option is set to True calculate the slope, and allow the dash and space lengths to be modified dependant on the slope. If the slope is shallow base the length on cos, steep slopes base the length on sin. The result has to be an integer rounded up to safeguard single dashes or spaces

Show/Hide Adjusted Dashed Line One Pixel Wide
def zigl_dash(dr, pta, ptb, dash=(5,5), fill='red', adjust=False):
    # check dash input
    if len(dash)%2 == 0 or len(dash) ==1:
        pass
    else:
        raise Exception('The dash tuple: {} should be one or an equal number '\
                    'of entries'.format(dash))
    dash = dash + dash if len(dash) == 1 else dash
    #sum_dash = sum(dash)

    x0, y0 = pta
    x1, y1 = ptb
    dx = (x1 - x0)
    dy = (y1 - y0)

    if adjust is True:
        slope = atan2(dy, dx)
        slope = slope if slope >= 0 else (2*pi + slope)

    dx = abs(dx)
    dy = abs(dy)

    pattern = []
    while len(dash) > 0:
        dash0, *dash = dash
        if adjust is True:
            dash0 = abs(int_up(dash0*cos(slope) if dx >= dy else dash0*sin(slope)))
        pattern.extend([1] * dash0) # dashes
        dash0, *dash = dash
        if adjust is True:
            dash0 = abs(int_up(dash0*cos(slope) if dx >= dy else dash0*sin(slope)))
        pattern.extend([0] * dash0) # spaces

    if not pattern:
        pattern.append(1)

    count = 0

    len_pattern = len(pattern)
    sx = 1 if x0 < x1 else -1
    sy = 1 if y0 < y1 else -1
    err = dx - dy
    dr = dx + 1 if dx > dy else dy + 1

    for x in range (dr):
        if pattern[count] == 1:
            draw.point([x0, y0], fill= fill)
        e2 = err<<1

        if e2 >= -dx:
            err -= dy
            x0 += sx

        if e2 <= dy:
            err += dx
            y0 += sy
    count = count + 1 if count < len_pattern -1 else 0

Adding Antialiasing#

../_images/line_aa_dashed.png

Antialiased Dashed Lines#

The square pattern shows

../_images/line_aa_dashed_adj.png

Antialiased Dashed Adjusted Lines#

The square pattern has been broken up

Antialiasing can be simply added to the above scripts.

Show/Hide Dashed Line Antialiased
def plotDashAA(draw, pta, ptb, dash=(5,5), fill='black', adjust=False):
    # draw a black antialiased line on white (255) background
    # check dash input
    if len(dash)%2 == 0 or len(dash) ==1:
        pass
    else:
    raise Exception('The dash tuple: {} should be one or an equal number '\
                    'of entries'.format(dash))
    dash = dash + dash if len(dash) == 1 else dash

    x0, y0 = pta
    x1, y1 = ptb

    dx = (x1 - x0)
    dy = (y1 - y0)

    if adjust is True:
        slope = atan2(dy, dx)
        slope = slope if slope >= 0 else (2*pi + slope)

    dx = abs(dx)
    dy = abs(dy)

    pattern = []
    while len(dash) > 0:
        dash0, *dash = dash
        if adjust is True:
            dash0 = abs(int_up(dash0*cos(slope) if dx >= dy else dash0*sin(slope)))
        pattern.extend([1] * dash0) # dashes
        dash0, *dash = dash
        if adjust is True:
            dash0 = abs(int_up(dash0*cos(slope) if dx >= dy else dash0*sin(slope)))
        pattern.extend([0] * dash0) # spaces

    if not pattern:
        pattern.append(1)
    len_pattern = len(pattern)

    count = 0
    sx = 1 if x0 < x1 else -1
    sy = 1 if y0 < y1 else -1
    err = dx - dy                            # error value e_xy

    ed = dx + dy

    ed = 1 if ed == 0 else sqrt(dx*dx+dy*dy) # max(dx, dy) #
    dr = dx + 1 if dx > dy else dy + 1      # better plotting when steep

    for x in range (dr):                    # pixel loop
        if pattern[count] == 1:
            hue = int(255*abs(err-dx+dy)/ed)
            draw.point([x0, y0], fill=(hue, hue, hue))

        e2 = err
        x2 = x0

        if e2<<1 >= -dx:                    # y-step
            if e2+dy < ed and x < dr - 1 and pattern[count] == 1:
                hue = int(255*(e2+dy)/ed)
                draw.point([x0,y0+sy], fill=(hue, hue, hue))

            err -= dy
            x0 += sx

        if e2<<1 <= dy and x < dr - 1:      # x-step
            if dx-e2 < ed and pattern[count] == 1:
                hue = int(255*(dx-e2)/ed)
                draw.point([x2+sx,y0], fill=(hue, hue, hue))

            err += dx
            y0 += sy

        count = count + 1 if count < len_pattern -1 else 0

Since dashed lines can have small dash sizes it was felt that thick lines would not be appropriate, however we can add the default dictionary and enable colour choices, as seen in thick lines:

....
def errs(comp, size,j):
    return 255 if comp == 255 else int((255-comp) * j / size) + comp

diffs = defaultdict(list)
diffs = defaultdict(lambda:back, diffs)
for i in range(int(ed)+1):
    if fill == (0,0,0):
        diffs[i] = tuple(int(255*i/ed) for j in range(3))
    else:
        diffs[i] = tuple(errs(fill[j],ed,i) for j in range(3))
....
            draw.point((x0, y0), fill=diffs[abs(err-dx+dy)]) # main line
....
            draw.point([x0, y0+sy], fill=diffs[abs(e2+dy)])  # y-step
....
            draw.point([x2+sx, y0], fill=diffs[abs(dx-e2)])  # x-step
....