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?

Yannis Assael
  • 1,099
  • 2
  • 20
  • 43
  • 2
    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. – martineau Jun 01 '15 at 17:56
  • 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. – Yannis Assael Jun 02 '15 at 01:04
  • 1
    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_](http://stackoverflow.com/questions/1109536/an-algorithm-for-inflating-deflating-offsetting-buffering-polygons). – martineau Jun 02 '15 at 07:40

6 Answers6

7

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)
    
martineau
  • 119,623
  • 25
  • 170
  • 301
Yannis Assael
  • 1,099
  • 2
  • 20
  • 43
  • 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?](https://stackoverflow.com/questions/68522726/how-to-set-alpha-transparency-property-using-pygame-draw-line) – martineau Jul 26 '21 at 12:43
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()
Dagrooms
  • 1,507
  • 2
  • 16
  • 42
  • 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. – Yannis Assael Jun 02 '15 at 00:52
  • 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. – Yannis Assael Jun 02 '15 at 14:00
2

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))
martineau
  • 119,623
  • 25
  • 170
  • 301
Andrew
  • 21
  • 1
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.

martineau
  • 119,623
  • 25
  • 170
  • 301
TeaCoast
  • 352
  • 3
  • 12
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.

Renvukus
  • 11
  • 1
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)