Pygame draw anti-aliased thick line
Question:
I used to draw lines (given some start and end points) at pygame like this: pygame.draw.line(window, color_L1, X0, X1, 2)
, where 2 was defining the thickness of the line.
As, anti-aliasing is not supported by .draw
, so I moved to .gfxdraw
and pygame.gfxdraw.line(window, X0[0], X0[1], X1[0], X1[1], color_L1)
.
However, this does not allow me to define the thickness of the line. How could I have thickness and anti-aliasing together?
Answers:
I would suggest a filled rectangle, as shown here: https://www.pygame.org/docs/ref/gfxdraw.html#pygame.gfxdraw.rectangle.
Your code would look something like:
thickLine = pygame.gfxdraw.rectangle(surface, rect, color)
and then remember to fill the surface. This is along the lines of:
thickLine.fill()
After many trials and errors, the optimal way to do it would be the following:
-
First, we define the center point of the shape given the X0_{x,y}
start and X1_{x,y}
end points of the line:
center_L1 = (X0+X1) / 2.
-
Then find the slope (angle) of the line:
length = 10 # Total length of line
thickness = 2
angle = math.atan2(X0[1] - X1[1], X0[0] - X1[0])
-
Using the slope and the shape parameters you can calculate the following coordinates of the box ends:
UL = (center_L1[0] + (length/2.) * cos(angle) - (thickness/2.) * sin(angle),
center_L1[1] + (thickness/2.) * cos(angle) + (length/2.) * sin(angle))
UR = (center_L1[0] - (length/2.) * cos(angle) - (thickness/2.) * sin(angle),
center_L1[1] + (thickness/2.) * cos(angle) - (length/2.) * sin(angle))
BL = (center_L1[0] + (length/2.) * cos(angle) + (thickness/2.) * sin(angle),
center_L1[1] - (thickness/2.) * cos(angle) + (length/2.) * sin(angle))
BR = (center_L1[0] - (length/2.) * cos(angle) + (thickness/2.) * sin(angle),
center_L1[1] - (thickness/2.) * cos(angle) - (length/2.) * sin(angle))
-
Using the computed coordinates, we draw an unfilled anti-aliased polygon (thanks to @martineau) and then fill it as suggested in the documentation of pygame’s gfxdraw
module for drawing shapes.
pygame.gfxdraw.aapolygon(window, (UL, UR, BR, BL), color_L1)
pygame.gfxdraw.filled_polygon(window, (UL, UR, BR, BL), color_L1)
You can also do a bit of a hack with the pygame.draw.aalines()
function by drawing copies of the line +/- 1-N pixels around the original line (yes, this isn’t super efficient, but it works in a pinch). For example, assuming we have a list of line segments (self._segments
) to draw and with a width (self._LINE_WIDTH
):
for segment in self._segments:
if len(segment) > 2:
for i in xrange(self._LINE_WIDTH):
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x,y+i) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x,y-i) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x+i,y) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x-i,y) for x,y in segment))
Your answer gets the job done but I think this would be a better/more readable way to do it. This is piggybacking off of your answer though so credit to you.
from math import atan2, cos, degrees, radians, sin
def Move(rotation, steps, position):
"""Return coordinate position of an amount of steps in a direction."""
xPosition = cos(radians(rotation)) * steps + position[0]
yPosition = sin(radians(rotation)) * steps + position[1]
return (xPosition, yPosition)
def DrawThickLine(surface, point1, point2, thickness, color):
angle = degrees(atan2(point1[1] - point2[1], point1[0] - point2[0]))
vertices = list()
vertices.append(Move(angle-90, thickness, point1))
vertices.append(Move(angle+90, thickness, point1))
vertices.append(Move(angle+90, thickness, point2))
vertices.append(Move(angle-90, thickness, point2))
pygame.gfxdraw.aapolygon(surface, vertices, color)
pygame.gfxdraw.filled_polygon(surface, vertices, color)
Keep in mind that this treats the thickness more as a radius than a diameter. If you want it to act more like a diameter you can divide each instance of the variable by 2.
So anyway, this calculates all the points of the rectangle and fills it in. It does this by going to each point and calculating the two adjacent points by turning 90 degrees and moving forward.
This is a slightly longer code, but maybe will help someone.
It uses vectors and create a stroke on each side of the line connecting two points.
def make_vector(pointA,pointB): #vector between two points
x1,y1,x2,y2 = pointA[0],pointA[1],pointB[0],pointB[1]
x,y = x2-x1,y2-y1
return x,y
def normalize_vector(vector): #sel explanatory
x, y = vector[0], vector[1]
u = math.sqrt(x ** 2 + y ** 2)
try:
return x / u, y / u
except:
return 0,0
def perp_vectorCL(vector): #creates a vector perpendicular to the first clockwise
x, y = vector[0], vector[1]
return y, -x
def perp_vectorCC(vector): #creates a vector perpendicular to the first counterclockwise
x, y = vector[0], vector[1]
return -y, x
def add_thickness(point,vector,thickness): #offsets a point by the vector
return point[0] + vector[0] * thickness, point[1] + vector[1] * thickness
def draw_line(surface,fill,thickness, start,end): #all draw instructions
x,y = make_vector(start,end)
x,y = normalize_vector((x,y))
sx1,sy1 = add_thickness(start,perp_vectorCC((x,y)),thickness//2)
ex1,ey1 = add_thickness(end,perp_vectorCC((x,y)),thickness//2)
pygame.gfxdraw.aapolygon(surface,(start,end,(ex1,ey1),(sx1,sy1)),fill)
pygame.gfxdraw.filled_polygon(surface, (start, end, (ex1, ey1), (sx1, sy1)), fill)
sx2, sy2 = add_thickness(start, perp_vectorCL((x, y)), thickness // 2)
ex2, ey2 = add_thickness(end, perp_vectorCL((x, y)), thickness//2)
pygame.gfxdraw.aapolygon(surface, (start, end, (ex2, ey2), (sx2, sy2)), fill)
pygame.gfxdraw.filled_polygon(surface, (start, end, (ex2, ey2), (sx2, sy2)), fill)
Here is a slightly faster and shorter solution:
def drawLineWidth(surface, color, p1, p2, width):
# delta vector
d = (p2[0] - p1[0], p2[1] - p1[1])
# distance between the points
dis = math.hypot(*d)
# normalized vector
n = (d[0]/dis, d[1]/dis)
# perpendicular vector
p = (-n[1], n[0])
# scaled perpendicular vector (vector from p1 & p2 to the polygon's points)
sp = (p[0]*width/2, p[1]*width/2)
# points
p1_1 = (p1[0] - sp[0], p1[1] - sp[1])
p1_2 = (p1[0] + sp[0], p1[1] + sp[1])
p2_1 = (p2[0] - sp[0], p2[1] - sp[1])
p2_2 = (p2[0] + sp[0], p2[1] + sp[1])
# draw the polygon
pygame.gfxdraw.aapolygon(surface, (p1_1, p1_2, p2_2, p2_1), color)
pygame.gfxdraw.filled_polygon(surface, (p1_1, p1_2, p2_2, p2_1), color)
The polygon’s points here are calculated using vector math rather than trigonometry, which is much less costly.
If efficiency is of the essence, it’s easy to further optimize this code – for instance the first few lines can be condensed to:
d = (p2[0] - p1[0], p2[1] - p1[1])
dis = math.hypot(*d)
sp = (-d[1]*width/(2*dis), d[0]*width/(2*dis))
Hope this helps someone.
I used to draw lines (given some start and end points) at pygame like this: pygame.draw.line(window, color_L1, X0, X1, 2)
, where 2 was defining the thickness of the line.
As, anti-aliasing is not supported by .draw
, so I moved to .gfxdraw
and pygame.gfxdraw.line(window, X0[0], X0[1], X1[0], X1[1], color_L1)
.
However, this does not allow me to define the thickness of the line. How could I have thickness and anti-aliasing together?
I would suggest a filled rectangle, as shown here: https://www.pygame.org/docs/ref/gfxdraw.html#pygame.gfxdraw.rectangle.
Your code would look something like:
thickLine = pygame.gfxdraw.rectangle(surface, rect, color)
and then remember to fill the surface. This is along the lines of:
thickLine.fill()
After many trials and errors, the optimal way to do it would be the following:
-
First, we define the center point of the shape given the
X0_{x,y}
start andX1_{x,y}
end points of the line:center_L1 = (X0+X1) / 2.
-
Then find the slope (angle) of the line:
length = 10 # Total length of line thickness = 2 angle = math.atan2(X0[1] - X1[1], X0[0] - X1[0])
-
Using the slope and the shape parameters you can calculate the following coordinates of the box ends:
UL = (center_L1[0] + (length/2.) * cos(angle) - (thickness/2.) * sin(angle), center_L1[1] + (thickness/2.) * cos(angle) + (length/2.) * sin(angle)) UR = (center_L1[0] - (length/2.) * cos(angle) - (thickness/2.) * sin(angle), center_L1[1] + (thickness/2.) * cos(angle) - (length/2.) * sin(angle)) BL = (center_L1[0] + (length/2.) * cos(angle) + (thickness/2.) * sin(angle), center_L1[1] - (thickness/2.) * cos(angle) + (length/2.) * sin(angle)) BR = (center_L1[0] - (length/2.) * cos(angle) + (thickness/2.) * sin(angle), center_L1[1] - (thickness/2.) * cos(angle) - (length/2.) * sin(angle))
-
Using the computed coordinates, we draw an unfilled anti-aliased polygon (thanks to @martineau) and then fill it as suggested in the documentation of pygame’s
gfxdraw
module for drawing shapes.pygame.gfxdraw.aapolygon(window, (UL, UR, BR, BL), color_L1) pygame.gfxdraw.filled_polygon(window, (UL, UR, BR, BL), color_L1)
You can also do a bit of a hack with the pygame.draw.aalines()
function by drawing copies of the line +/- 1-N pixels around the original line (yes, this isn’t super efficient, but it works in a pinch). For example, assuming we have a list of line segments (self._segments
) to draw and with a width (self._LINE_WIDTH
):
for segment in self._segments:
if len(segment) > 2:
for i in xrange(self._LINE_WIDTH):
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x,y+i) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x,y-i) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x+i,y) for x,y in segment))
pygame.draw.aalines(self._display, self._LINE_COLOR, False,
((x-i,y) for x,y in segment))
Your answer gets the job done but I think this would be a better/more readable way to do it. This is piggybacking off of your answer though so credit to you.
from math import atan2, cos, degrees, radians, sin
def Move(rotation, steps, position):
"""Return coordinate position of an amount of steps in a direction."""
xPosition = cos(radians(rotation)) * steps + position[0]
yPosition = sin(radians(rotation)) * steps + position[1]
return (xPosition, yPosition)
def DrawThickLine(surface, point1, point2, thickness, color):
angle = degrees(atan2(point1[1] - point2[1], point1[0] - point2[0]))
vertices = list()
vertices.append(Move(angle-90, thickness, point1))
vertices.append(Move(angle+90, thickness, point1))
vertices.append(Move(angle+90, thickness, point2))
vertices.append(Move(angle-90, thickness, point2))
pygame.gfxdraw.aapolygon(surface, vertices, color)
pygame.gfxdraw.filled_polygon(surface, vertices, color)
Keep in mind that this treats the thickness more as a radius than a diameter. If you want it to act more like a diameter you can divide each instance of the variable by 2.
So anyway, this calculates all the points of the rectangle and fills it in. It does this by going to each point and calculating the two adjacent points by turning 90 degrees and moving forward.
This is a slightly longer code, but maybe will help someone.
It uses vectors and create a stroke on each side of the line connecting two points.
def make_vector(pointA,pointB): #vector between two points
x1,y1,x2,y2 = pointA[0],pointA[1],pointB[0],pointB[1]
x,y = x2-x1,y2-y1
return x,y
def normalize_vector(vector): #sel explanatory
x, y = vector[0], vector[1]
u = math.sqrt(x ** 2 + y ** 2)
try:
return x / u, y / u
except:
return 0,0
def perp_vectorCL(vector): #creates a vector perpendicular to the first clockwise
x, y = vector[0], vector[1]
return y, -x
def perp_vectorCC(vector): #creates a vector perpendicular to the first counterclockwise
x, y = vector[0], vector[1]
return -y, x
def add_thickness(point,vector,thickness): #offsets a point by the vector
return point[0] + vector[0] * thickness, point[1] + vector[1] * thickness
def draw_line(surface,fill,thickness, start,end): #all draw instructions
x,y = make_vector(start,end)
x,y = normalize_vector((x,y))
sx1,sy1 = add_thickness(start,perp_vectorCC((x,y)),thickness//2)
ex1,ey1 = add_thickness(end,perp_vectorCC((x,y)),thickness//2)
pygame.gfxdraw.aapolygon(surface,(start,end,(ex1,ey1),(sx1,sy1)),fill)
pygame.gfxdraw.filled_polygon(surface, (start, end, (ex1, ey1), (sx1, sy1)), fill)
sx2, sy2 = add_thickness(start, perp_vectorCL((x, y)), thickness // 2)
ex2, ey2 = add_thickness(end, perp_vectorCL((x, y)), thickness//2)
pygame.gfxdraw.aapolygon(surface, (start, end, (ex2, ey2), (sx2, sy2)), fill)
pygame.gfxdraw.filled_polygon(surface, (start, end, (ex2, ey2), (sx2, sy2)), fill)
Here is a slightly faster and shorter solution:
def drawLineWidth(surface, color, p1, p2, width):
# delta vector
d = (p2[0] - p1[0], p2[1] - p1[1])
# distance between the points
dis = math.hypot(*d)
# normalized vector
n = (d[0]/dis, d[1]/dis)
# perpendicular vector
p = (-n[1], n[0])
# scaled perpendicular vector (vector from p1 & p2 to the polygon's points)
sp = (p[0]*width/2, p[1]*width/2)
# points
p1_1 = (p1[0] - sp[0], p1[1] - sp[1])
p1_2 = (p1[0] + sp[0], p1[1] + sp[1])
p2_1 = (p2[0] - sp[0], p2[1] - sp[1])
p2_2 = (p2[0] + sp[0], p2[1] + sp[1])
# draw the polygon
pygame.gfxdraw.aapolygon(surface, (p1_1, p1_2, p2_2, p2_1), color)
pygame.gfxdraw.filled_polygon(surface, (p1_1, p1_2, p2_2, p2_1), color)
The polygon’s points here are calculated using vector math rather than trigonometry, which is much less costly.
If efficiency is of the essence, it’s easy to further optimize this code – for instance the first few lines can be condensed to:
d = (p2[0] - p1[0], p2[1] - p1[1])
dis = math.hypot(*d)
sp = (-d[1]*width/(2*dis), d[0]*width/(2*dis))
Hope this helps someone.