============================== Dashed Lines and Rasterization ============================== Base Dashed Line ================ .. figure:: ../figures/bres/zigl_dash.png :width: 400 :height: 400 :align: center **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. .. _dash-bres: 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. .. raw:: html
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 .. raw:: html
| 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 :ref:`circular`. .. figure:: ../figures/bres/zigl_dash_adjust.png :width: 400 :height: 400 :align: center **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 .. raw:: html
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 .. raw:: html
| Adding Antialiasing =================== .. figure:: ../figures/bres/line_aa_dashed.png :width: 300 :height: 300 :align: left **Antialiased Dashed Lines** The square pattern shows .. figure:: ../figures/bres/line_aa_dashed_adj.png :width: 300 :height: 300 :align: right **Antialiased Dashed Adjusted Lines** The square pattern has been broken up Antialiasing can be simply added to the above scripts. .. raw:: html
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 .. raw:: html
| 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 ....