Revisit Line Lengths#
As the line patterns become longer some dashes will be drawn after the end coordinates, we want the pattern to end at or just short of the end position with part of the pattern of dashes and gaps at the finish.
The dashes and gaps should be constant in pixel number even when the line length is not an exact multiple of the dash pattern. Limit the arrays so that coordinates that are beyond the end position are deleted, then it is feasible to fit the pattern as optimally as possible.
Within the main while loop each linspace is followed by a short while loop that deletes the last row of the array if that row is too far from the start position:
while(dist(start_pos,start_arr[-1,:]) > size):
start_arr = delete (start_arr, (-1), axis=0)
and the end array:
while(dist(start_pos,end_arr[-1,:]) > size):
end_arr = delete (end_arr, (-1), axis=0)
The previous script shortened the line, we can afford to stretch the line to accommodate inclined situations. First determine the size of the line and then the longest x or y ordinates:
size = dist(start_pos, end_pos)
....
length = abs(x1 - x0) if abs(x1 - x0) >= abs(y1 - y0) else abs(y1 - y0)
if length % dash_gap_length != 0:
factor = length//dash_gap_length
length = factor * dash_gap_length
if abs(x1 - x0) >= abs(y1 - y0):
x1 = x0 + fx * length
y1 = int_up(y0 + fy * length * abs(tan(theta)))
else:
y1 = y0 + fy * length
x1 = int_up(x0 + fx * length * abs(tan(phi)))
....
# make second part of the line array
if abs(x1 - x0) >= abs(y1 - y0):
x0 = x0 + fx * dash_minus
y0 = y0 + fy * int_up(dash_minus * abs(tan(theta)))
x1 = x1 + fx * dash_minus
y1 = y1 + fy * int_up(dash_minus * abs(tan(theta)))
else:
x0 = x0 + fx * int_up(dash_minus * abs(tan(phi)))
y0 = y0 + fy * dash_minus
x1 = x1 + fx * int_up(dash_minus * abs(tan(phi)))
y1 = y1 + fy * dash_minus
This also shortens the line, but keeps the longest of the x or y lengths,
determine whether the change in length is added or subtracted.
Likewise the changes in lengths are made using abs(tan(theta)) or
abs(tan(phi)) where phi is the complementary angle to theta. It was
found that as the final array was sorted the start position switched from
first to last if the angle of the line was greater than 180°. If there was
an equal number of rows in the array this was of no consequence, but if there
was an odd number of rows in the array the first position is overlooked
and a wrong pattern drawn. If the final array is inverted when the line
angle is more than 180° then this problem is solved:
fact = -1 if abs(theta) >= pi else 1
....
# sort along the column, where the maximum change occurs
if abs(x1 -x0) > abs(y1 -y0):
fin_arr = int_(all_arr[all_arr[:, 0].argsort()])
fin_arr = fin_arr[::-1] if fact == -1 else fin_arr
else:
fin_arr = int_(all_arr[all_arr[:, 1].argsort()])
fin_arr = fin_arr[::-1] if fact == -1 else fin_arr
Using tan to adjust lengths#
Although the 45° lines are shorter, they are actually true
All the lines were drawn radially, hence the different colours. The pattern was (7,1,1,1) and looks good at the main axes.
Show/Hide Code 10angled_dash_gap.py
import sys
sys.path.append('../dims')
from PIL import Image, ImageDraw, ImageFont
from DimLinesPIL import polar2cart, int_up
from numpy import linspace, concatenate, int_, delete
from math import dist, atan2, tan, pi, radians
def line_dashed(dr, start_pos, end_pos=None, dash=(5,5), angle=None,
size=None, width = 1, fill='black'):
# create dashed lines in PIL
x0, y0 = start_pos
if end_pos is None:
theta = radians(angle)
end_pos = x1, y1 = polar2cart(start_pos, theta, size)
elif angle is None:
x1, y1 = end_pos
theta = atan2(y1 - y0, x1 - x0) # line slope
size = dist(start_pos, end_pos)
else:
raise Exception('line_dashed: Either supply end_pos {}, or ' \
'size {} and angle {}'.format(end_pos, size, angle))
# 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))
if len(dash) == 1 :
dash = dash + dash
dash_gap_length = sum(dash)
# is the line increasing or decreasing
#fact = -1 if abs(theta) >= pi else 1
theta = theta if theta > 0 else 2*pi + theta # change minus values
phi = pi/2 - theta
fx = -1 if pi/2 < theta < 3*pi/2 else 1
fy = 1 if 0 < theta < pi else -1
# lengthof longest ordinate
length = abs(x1 - x0) if abs(x1 - x0) >= abs(y1 - y0) else abs(y1 - y0)
if length % dash_gap_length != 0:
factor = length//dash_gap_length # + 1
length = factor * dash_gap_length
if abs(x1 - x0) >= abs(y1 - y0):
x1 = x0 + fx * length
y1 = int_up(y0 + fy * length * abs(tan(theta)))
else:
y1 = y0 + fy * length
x1 = int_up(x0 + fx * length * abs(tan(phi)))
# sort out lengths
dash_amount = int_up(length / dash_gap_length) + 1
all_arr = None
while len(dash) > 0:
start_arr = linspace((x0, y0), (x1, y1), dash_amount)
while(dist(start_pos,start_arr[-1,:]) > size):
start_arr = delete (start_arr, (-1), axis=0)
dash0, *dash = dash
dash_minus = dash0 - 1
# make second part of the line array
if abs(x1 - x0) >= abs(y1 - y0):
x0 = x0 + fx * dash_minus
y0 = y0 + fy * int_up(dash_minus * abs(tan(theta)))
x1 = x1 + fx * dash_minus
y1 = y1 + fy * int_up(dash_minus * abs(tan(theta)))
else:
x0 = x0 + fx * int_up(dash_minus * abs(tan(phi)))
y0 = y0 + fy * dash_minus
x1 = x1 + fx * int_up(dash_minus * abs(tan(phi)))
y1 = y1 + fy * dash_minus
end_arr = linspace((x0, y0), (x1, y1), dash_amount)
while(dist(start_pos,end_arr[-1,:]) > size):
end_arr = delete (end_arr, (-1), axis=0)
if all_arr is None:
all_arr = concatenate([start_arr, end_arr], axis=0)
else:
all_arr = concatenate([start_arr, end_arr, all_arr], axis=0)
dash0, *dash = dash
dash_plus = dash0 + 1
if abs(x1 - x0) >= abs(y1 - y0):
x0 = x0 + fx * dash_plus
y0 = y0 + fy * int_up(dash_plus * abs(tan(theta)))
x1 = x1 + fx * dash_plus
y1 = y1 + fy * int_up(dash_plus * abs(tan(theta)))
else:
x0 = x0 + fx * int_up(dash_plus * abs(tan(phi)))
y0 = y0 + fy * dash_plus
x1 = x1 + fx * int_up(dash_plus * abs(tan(phi)))
y1 = y1 + fy * dash_plus
# sort along the column, where the maximum change occurs
if abs(x1 -x0) > abs(y1 -y0):
fin_arr = int_(all_arr[all_arr[:, 0].argsort()])
else:
fin_arr = int_(all_arr[all_arr[:, 1].argsort()])
fin_arr = fin_arr[::-1] if (fin_arr[-1,:] == start_pos).all() else fin_arr
nr_lines = len(fin_arr) //2 * 2
[dr.line([tuple(fin_arr[n]), tuple(fin_arr[n+1])], width=width, fill=fill)
for n in range(0, nr_lines, 2)]
if __name__ == "__main__":
Font = ImageFont.truetype('consola.ttf', 12)
# wide, height = Font.getsize('(30, 84)')
unused1, unused2, wide, height = Font.getbbox('(30, 84)')
Dash = (21,3,3,3)
Dash1 = (7,1,1,1)
Dash2 = (5, 5)
Start_pos = (50, 50) # (30, 10) (10, 10)
End_pos = (26, 26) # (75, 75) (30, 90)
x0, y0 = (80, 90)
x1, y1 = (15, 25)
w, h = 200, 200
image = Image.new('RGB', (w,h), '#FFFFDD')
draw = ImageDraw.Draw(image)
for i in range(0, 360, 5):
line_dashed(draw, (100, 100), size=65, angle=i, dash=Dash1,
width = 1, fill=(255-int_up(i*0.7),int_up(i*0.7),0)) #
'''
a = [0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330]
#a = [0, 45, 90, 135, 180, 225, 270, 315]
#a = [30, 45]
for i in range(len(a)):
line_dashed(image, (100, 100), angle=a[i], size = 65, dash=Dash1,
width = 1, fill=(255-15*i,15*i,0))
x = [74, 74, 50, 26, 26, 26, 50, 74]
y = [50, 74, 74, 74, 50, 26, 26, 26]
for i in range(len(x)):
line_dashed(image, Start_pos, (x[i], y[i]), dash=Dash2, width = 1, fill='black')
'''
#drawa = ImageDraw.Draw(image)
'''
draw.line([(80, 80), (74, 74)], fill='red')
draw.line([(72, 72), (72, 72)], fill='red')
draw.line([(70, 70), (64, 64)], fill='red')
'''
image.show()
#image.save('../figures/10angled_dashes.png') #
An alternative function was made, based on sine and cosine. The advantage here was that no adjustment for sign was necessary, so this simplified the task:
# make second part of the line array
if angle not in (45, 135, 225, 315):
x0 = x0 + int_up(dash_minus * cos(theta))
y0 = y0 + int_up(dash_minus * sin(theta))
x1 = x1 + int_up(dash_minus * cos(theta))
y1 = y1 + int_up(dash_minus * sin(theta))
else:
x0 = x0 + dash_minus * int_up(cos(theta))
y0 = y0 + dash_minus * int_up(sin(theta))
x1 = x1 + dash_minus * int_up(cos(theta))
y1 = y1 + dash_minus * int_up(sin(theta))
But all was not plain sailing. Later on rather than invert the array the drawing was started one line later if the line angle was greater than 180° and there was an odd number of rows in the array:
if fact == -1:
start_draw = 0 if len(fin_arr)%2 == 0 else 1
else:
start_draw = 0
[draw.line([tuple(fin_arr[n]), tuple(fin_arr[n+1])], width=1, fill=fill)
for n in range(start_draw, nr_lines, 2)]
Using sin and cos to adjust lengths#
Apart from the 45° lines, the other lines look to be the same size
This script was saved in the dimension tools due to its simplicity.
Show/Hide Code 11angled_dash_gap_sin_cos.py
import sys
sys.path.append('../dims')
from PIL import Image, ImageDraw, ImageFont
from DimLinesPIL import polar2cart, int_up
from numpy import linspace, concatenate, int_, delete
from math import dist, atan2, sin, cos, radians
def line_dashed(dr, start_pos, end_pos=None, dash=(5,5), angle=None,
size=None, width = 1, fill='black'):
# create dashed lines in PIL
x0, y0 = start_pos
if end_pos is None:
theta = radians(angle)
end_pos = x1, y1 = polar2cart(start_pos, theta, size)
elif angle is None:
x1, y1 = end_pos
theta = atan2(y1 - y0, x1 - x0) # line slope
else:
raise Exception('line_dashed: Either supply end_pos {}, or ' \
'size {} and angle {}'.format(end_pos, size, angle))
# 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))
if len(dash) == 1 :
dash = dash + dash
size = dist(start_pos, end_pos)
dash_gap_length = sum(dash)
# length of longest ordinate
length = abs(x1 - x0) if abs(x1 - x0) >= abs(y1 - y0) else abs(y1 - y0)
if length % dash_gap_length != 0:
factor = length//dash_gap_length
length = factor * dash_gap_length
if angle not in (45, 135, 225, 315):
x1 = int_up(x0 + length * cos(theta))
y1 = int_up(y0 + length * sin(theta))
else:
x1 = x0 + length * int_up(cos(theta))
y1 = y0 + length * int_up(sin(theta))
# sort out number of generated samples
dash_amount = int_up(length / dash_gap_length) + 1
all_arr = None
while len(dash) > 0:
start_arr = linspace((x0, y0), (x1, y1), dash_amount)
while(dist(start_pos,start_arr[-1,:]) > size):
start_arr = delete (start_arr, (-1), axis=0)
dash0, *dash = dash
dash_minus = dash0 - 1
# make second part of the line array
if angle not in (45, 135, 225, 315):
x0 = x0 + int_up(dash_minus * cos(theta))
y0 = y0 + int_up(dash_minus * sin(theta))
x1 = x1 + int_up(dash_minus * cos(theta))
y1 = y1 + int_up(dash_minus * sin(theta))
else:
x0 = x0 + dash_minus * int_up(cos(theta))
y0 = y0 + dash_minus * int_up(sin(theta))
x1 = x1 + dash_minus * int_up(cos(theta))
y1 = y1 + dash_minus * int_up(sin(theta))
end_arr = linspace((x0, y0), (x1, y1), dash_amount)
while(dist(start_pos,end_arr[-1,:]) > size):
end_arr = delete (end_arr, (-1), axis=0)
if all_arr is None:
all_arr = concatenate([start_arr, end_arr], axis=0)
else:
all_arr = concatenate([start_arr, end_arr, all_arr], axis=0)
dash0, *dash = dash
dash_plus = dash0 + 1
if angle not in (45, 135, 225, 315):
x0 = x0 + int_up(dash_plus * cos(theta))
y0 = y0 + int_up(dash_plus * sin(theta))
x1 = x1 + int_up(dash_plus * cos(theta))
y1 = y1 + int_up(dash_plus * sin(theta))
else:
x0 = x0 + dash_plus * int_up(cos(theta))
y0 = y0 + dash_plus * int_up(sin(theta))
x1 = x1 + dash_plus * int_up(cos(theta))
y1 = y1 + dash_plus * int_up(sin(theta))
# sort along the column, where the maximum change occurs
if abs(x1 -x0) > abs(y1 -y0):
fin_arr = int_(all_arr[all_arr[:, 0].argsort()])
else:
fin_arr = int_(all_arr[all_arr[:, 1].argsort()])
fin_arr = fin_arr[::-1] if (fin_arr[-1,:] == start_pos).all() else fin_arr
nr_lines = len(fin_arr) //2 * 2
[dr.line([tuple(fin_arr[n]), tuple(fin_arr[n+1])], width=width, fill=fill)
for n in range(0, nr_lines, 2)]
if __name__ == "__main__":
Font = ImageFont.truetype('consola.ttf', 12)
#wide, height = Font.getsize('(30, 84)')
unused1, unused2, wide, height = Font.getbbox('(30, 84)')
Dash = (21,3,3,3)
Dash1 = (7,1,1,1)
w, h = 200, 200
image = Image.new('RGB', (w,h), '#FFFFDD')
draw = ImageDraw.Draw(image)
for i in range(0, 360, 5):
line_dashed(draw, (100, 100), size=65, angle=i, dash=Dash1,
width = 1, fill=(0,int_up(i*0.7),255-int_up(i*0.7)))
'''
x = [50, 150, 150, 50, 50]
y = [50, 50, 150, 150, 50]
#x = [50, 50]
#y = [150, 50]
for i in range(len(x)-1):
#print(i, 'i')
line_dashed(image, (x[i],y[i]), end_pos= (x[i+1],y[i+1]), dash=dash1,
width = 1, fill=(0,i*63,255-(i*63)))
a = [0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330]
#a = [0,30,45,60]
for i in range(len(a)):
line_dashed(image, (100, 100), size=65, angle=a[i], dash=dash1,
width = 1, fill=(0,i*15,255-(i*15)))
'''
image.show()
#image.save('../figures/11angled_dashes.png') #