import colors from drawable import * from ray import * class Polygon(drawable): """ Polygon class as 'drawable' objects. Implements functionality of abstract drawable class. """ def __init__(self, *points, color=colors.BLACK): """Initilise 'self' with standard color 'BLACK'.""" drawable.__init__(self, *points, color=color) def draw(self, screen, offset): """ Draw 'self' as a polygon on 'screen'. """ if not self: # empty return polygon = self.copy() # calculate real position on screen rect = Vector(*screen.get_rect()[2:]) for i, _ in enumerate(polygon): polygon[i] = round(polygon[i] - offset + rect / 2) if polygon[i].length2() > 30000: # hotfix return # point if len(polygon) == 1: pygame.gfxdraw.pixel(screen, *polygon[0], self.color) # line elif len(polygon) == 2: pygame.draw.aaline(screen, self.color, polygon[0], polygon[1]) # polygon else: pygame.gfxdraw.aapolygon(screen, polygon, self.color) pygame.gfxdraw.filled_polygon(screen, polygon, self.color) def draw_shadow(self, screen, offset, source, color=colors.DARK_GRAY): """ Draw draw a shadow of 'self' on 'screen' respecting the light-'source'. """ if not self: # empty return # point elif len(self) == 1: Ray(self[0], self[0] - source, color=color).draw(screen, offset) # line elif len(self) == 2: # edgecase: would lead to DivisionByZeroError if self[0] == source or self[1] == source: for point in self: Ray(point, point - source, color=color).draw(screen, offset) return # corners of screen area rect = screen.get_rect() corner = Vector(*rect[2:]) / 2 corners = [ corner * Vector( 1, 1), corner * Vector(-1, 1), corner * Vector(-1,-1), corner * Vector( 1,-1), corner * Vector( 1, 1), corner * Vector( 1,-1), ] # rays from source to both ends ray1 = Ray(self[0], self[0] - source) ray2 = Ray(self[1], self[1] - source) # garantees anticlockwise order (important for calculations) if ray1.dir.angle_to(ray2.dir) > 180: ray1, ray2 = ray2, ray1 # creates minimal polygon shadow = Polygon(color=color) shadow.append(ray2.offscreen(screen, offset)) shadow.append(ray2.start) shadow.append(ray1.start) shadow.append(ray1.offscreen(screen, offset)) # adds corners of screen to polygon if needed for i, _ in enumerate(corners[1:-1]): if corners[i].is_between(ray1.dir, ray2.dir): if corners[i-1].is_between(ray1.dir, ray2.dir): shadow.append(offset + corners[i-1]) shadow.append(offset + corners[i]) if corners[i+1].is_between(ray1.dir, ray2.dir): shadow.append(offset + corners[i+1]) break # finally draws calculated shadow shadow.draw(screen, offset) # polygon else: # draws shadow for every line in polygon for i, _ in enumerate(self[:-1]): Polygon(self[i], self[i+1]).draw_shadow(screen, offset, source) Polygon(self[-1], self[0]).draw_shadow(screen, offset, source) def dist_to(self, source): """ Return euclidean distance to the player ('source'). """ def sdf_line(a, b, p): """ Return distance from point p to line a-b. (source: https://youtu.be/PMltMdi1Wzg) """ if b == a: h = 0 else: h = min(1, max(0, (p-a).dot(b-a) / (b-a).dot(b-a))) return ((p-a) - h * (b-a)).length() if not self: # empty return float('inf') # minimum distance to a side m = float('inf') for i, _ in enumerate(self): m = min(m, sdf_line(self[i-1], self[i], source)) return m def cut(self, source, portal): """ Return 'self' but cut to fit in the view from 'source' though 'portal'. This algorithm isn't perfect, but the best I got at the moment. """ def position(point): """ Return the area 'point' is in: -1 -- in the triangle between source and portal line 0 -- to the right of visible area 1 -- in the visible portal area 2 -- to the left of visible area """ vec = point - source if vec.is_between(ray1.dir, ray2.dir): if (point - ray1.start).is_between(ray1.dir, border.dir): return 1 else: return -1 if vec.is_between(null, ray1.dir): return 0 if vec.is_between(ray2.dir, null): return 2 # rays that are borders of different areas ray1 = Ray(portal.start, portal.start - source) # area 0 <--> 1 ray2 = Ray(portal.end, portal.end - source) # area 1 <--> 2 border = Ray(ray1.start, ray2.start - ray1.start) # area -1 <--> 1 null = -(ray1.dir + ray2.dir) # area 0 <--> 2 # garantees anticlockwise order (important for calculations) if ray1.dir.angle_to(ray2.dir) > 180: ray1, ray2 = ray2, ray1 rest = Polygon() rest.__dict__ = self.__dict__ if not self: # empty return rest # edgecase for when a portal is in its own portal world in the same place if len(self) == 2: if self[0] in portal and self[1] in portal: return rest # algorithm for cutting polygon nxt_pos = position(self[-1]) for i, _ in enumerate(self): cur_pos = nxt_pos nxt_pos = position(self[i]) if cur_pos == 1: # visible rest.append(self[i-1]) # same position, nothing changes if nxt_pos == cur_pos: continue # different position, intersections with visible area needed inter = [] # through the left or right boundary of visible area -> intersection if 1 in (cur_pos, nxt_pos): if 0 in (cur_pos, nxt_pos): inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) elif 2 in (cur_pos, nxt_pos): inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) if not inter: # empty inter += border.intersect(Ray(self[i-1], self[i] - self[i-1])) # line goes outside the visible area around the portal -> edges of portal elif -1 in (cur_pos, nxt_pos): if 0 in (cur_pos, nxt_pos): inter += [ray1.start] else: # 2 inter += [ray2.start] # line goes from the left to the right -> may intersect visible area else: # 0 and 2 if cur_pos == 0: inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) else: # 2 inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) # add intersections for pt in inter: if pt.length2() > 10000: # hotfix print(f"CalculationError while cutting {self} at {ray1, ray2}:\n {pt} is very big") else: rest.append(pt) return rest