Pygame draw anti-aliased thick line
Asked Answered
I

7

9

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?

Immediacy answered 1/6, 2015 at 16:1 Comment(3)
Define the proper offset lines based your line's end-points and width and use them to define a polygon representing the fat line. Then use pygame.gfxdraw.aapolygon() to draw it.Lilybel
My line is rotating at each step so I am not sure to define its ends as a polygon, +-1 wouldn't work in this case.Immediacy
Drawing fat lines the way I suggested is a case of offsetting a degenerate polygon. A fair amount of math is usually involved. See An algorithm for inflating/deflating (offsetting, buffering) polygons.Lilybel
I
9

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)
    
Immediacy answered 2/6, 2015 at 14:47 Comment(1)
BTW, I used an optimized version your answer in one of my own to a related question titled How to set alpha transparency property using pygame.draw.line?Lilybel
Z
2

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()
Zombie answered 1/6, 2015 at 21:4 Comment(3)
Thank you very much! I tried that as well as the box, but I cannot find how I can define the start and the end given that I have the coordinates of only the beginning and the end of the line.Immediacy
The problem lies at the fact that my line is rotating so a simple +-1 is not enough, as you can end up with a constant side that doesn't follow the rotation of the other end.Immediacy
Won't this only work for horizontal/vertical lines?An
W
1

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))
Wu answered 4/2, 2019 at 4:44 Comment(0)
W
1

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.

Whiffletree answered 12/5, 2021 at 18:39 Comment(0)
S
1

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.

Shinleaf answered 11/8, 2022 at 18:53 Comment(0)
F
0

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)
Fleet answered 6/10, 2021 at 10:22 Comment(0)
S
0
def Boxline(x1, y1, x2, y2, width, direction, addloraddr):
    global xyadd
    xyadd = 0 
    if addloraddr == 1:
        for i in range(width):
            pygame.draw.aaline(screen, 'red', ((x1/2)-xyadd, (y1*.25)-xyadd), ((x2*direction)-xyadd, y2))
            xyadd += 0.2
    if addloraddr == 2:
        for i in range(width):
            pygame.draw.aaline(screen, 'red', ((x1/2)-xyadd, (y1*.25)+xyadd), ((x2*direction)-xyadd, y2))
            xyadd += 0.2

This solution draws multiple anti-aliased lines next to each other within a very close increment. Boxline(WIDTH, HEIGHT, WIDTH, HEIGHT, 20, .8, 1) Boxline(WIDTH, HEIGHT, WIDTH, HEIGHT, 20, .2, 2) I added this code into the game loop and it works.

Result:

here's a picture of the lines it makes

Spireme answered 5/4, 2024 at 3:54 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.