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?

Asked By: Yannis Assael

||

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()
Answered By: Dagrooms

After many trials and errors, the optimal way to do it would be the following:

  1. 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.
    
  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])
    
  3. 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))
    
  4. 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)
    
Answered By: Yannis Assael

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))
Answered By: Andrew

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.

Answered By: TeaCoast

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)
Answered By: Andrew Zmurowski

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.

Answered By: Renvukus
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.