I am working on a graph based project. I am drawing lines to represent the edges. My goal is to change color of line when I click on the specific line. the problem is pygame treats line as diagonal of virtual rectangle. so when even if I don’t click on the line but the mouse position is in the projected area of the virtual rectangle, the event is detected as collision while using collidepoint(). I want to detect this only when the mouse is clicked on actual line only.
I am new to pygame, so let me know if there is other function or library that I can use. This is the example code of my project.
import pygame pygame.init() screen = pygame.display.set_mode((1200,700)) running = True red = 0 green = 255 blue = 210 while running: screen.fill((red,green,blue)) line = pygame.draw.line(screen, green, (50,50), (400,400),10) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.MOUSEBUTTONDOWN: if line.collidepoint(event.pos): red = 255 green = 0 blue = 0 if event.type == pygame.MOUSEBUTTONUP: red = 0 green = 255 blue = 210 pygame.display.update()
Advertisement
Answer
pygame.draw.line
returns a pygame.Rect
object that defines the axis aligned bounding rectangle surrounding the line. collidepoint
test if a point is in a rectangle.
You have to use a different approach. Write a function that computes the shortest distance of a point to a line:
dist = abs(dot(normalized(NV), P - LP))
, where NV
is the normal vector to the line, LP
is a point on the line and P
is the point whose distance needs to be calculated.
import math
def distance_point_line(pt, l1, l2): nx, ny = l1[1] - l2[1], l2[0] - l1[0] nlen = math.hypot(nx, ny) nx /= nlen ny /= nlen vx, vy = pt[0] - l1[0], pt[1] - l1[1] dist = abs(nx*vx + ny*vy) return dist
The same function with the use of pygame.math.Vector2
:
def distance_point_line(pt, l1, l2): NV = pygame.math.Vector2(l1[1] - l2[1], l2[0] - l1[0]) LP = pygame.math.Vector2(l1) P = pygame.math.Vector2(pt) return abs(NV.normalize().dot(P -LP))
Test whether the mouse pointer is in the rectangle defined by the line and whether the distance is less than half the line width:
if (line_rect.collidepoint(event.pos) and distance_point_line(event.pos, (50,50), (400,400)) < 5): # [...]
Explanation:
I’ve used the Dot product distance from the point to the line.. In general The Dot product of 2 vectors is equal the cosine of the angle between the 2 vectors multiplied by the magnitude (length) of both vectors.
dot( A, B ) == | A | * | B | * cos( angle_A_B )
This follows, that the Dot product of 2 Unit vectors is equal the cosine of the angle between the 2 vectors, because the length of a unit vector is 1.
uA = normalize( A ) uB = normalize( B ) cos( angle_A_B ) == dot( uA, uB )
Therefore the Dot product of the normalized normal vector to the line (NV) and a vector from a point on the line (LP) to the point whose distance must be calculated (P) is the shortest distance of the point to the line.
Minimal example:
import pygame import math pygame.init() screen = pygame.display.set_mode((1200,700)) def distance_point_line(pt, l1, l2): NV = pygame.math.Vector2(l1[1] - l2[1], l2[0] - l1[0]) LP = pygame.math.Vector2(l1) P = pygame.math.Vector2(pt) return abs(NV.normalize().dot(P -LP)) color = (255, 255, 255) running = True while running: screen.fill((0, 0, 0)) line_rect = pygame.draw.line(screen, color, (50,50), (400,400), 10) pygame.display.update() for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.MOUSEBUTTONDOWN: if (line_rect.collidepoint(event.pos) and distance_point_line(event.pos, (50,50), (400,400)) < 5): color = (255, 0, 0) if event.type == pygame.MOUSEBUTTONUP: color = (255, 255, 255)