commit 48fde028950108713da8f9555078fbad027081e8 Author: = <=> Date: Mon Mar 16 14:26:38 2026 -0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..087fb11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/** +**/__pycache__/** \ No newline at end of file diff --git a/voronoi.py b/voronoi.py new file mode 100644 index 0000000..17875b6 --- /dev/null +++ b/voronoi.py @@ -0,0 +1,261 @@ +import pygame as pg +from math import sqrt, atan2, cos, sin, pi +import random +from dataclasses import dataclass +from collections import Counter +from itertools import pairwise + +class Mover: + + def __init__(self, point: pg.Vector2, bounds: pg.Rect, max_v: float = 500.0): + self.bounds = bounds + self.point = point + self.velocity = pg.Vector2(cos(random.random() * 2 * pi), sin(random.random() * 2 * pi)) * max_v + + def move(self, dt): + self.point += dt * self.velocity + if self.point.x > self.bounds.right: + self.point.x = self.bounds.left + (self.point.x - self.bounds.right) + if self.point.x < self.bounds.left: + self.point.x = (self.bounds.right) - (self.bounds.left - self.point.x) + if self.point.y > self.bounds.bottom: + self.point.y = (self.bounds.top) - (self.bounds.bottom - self.point.y) + if self.point.y < self.bounds.top: + self.point.y = self.bounds.bottom + (self.point.y - self.bounds.top) + +class ColorPoint(pg.Vector2): + + def __init__(self, color: tuple[int,int,int], x: float, y: float): + self.color = color + super().__init__(x,y) + + def __hash__(self): + return hash((self.x, self.y)) + +@dataclass +class Edge: + a: ColorPoint + b: ColorPoint + + def __hash__(self): + return hash(frozenset((self.a, self.b))) + + def __eq__(self, other: 'Edge') -> bool: + return (self.a == other.a and self.b == other.b) or (self.a == other.b and self.b == other.a) + +@dataclass +class Circle: + center: ColorPoint + radius: float + + def point_within(self, point: ColorPoint) -> bool: + return (self.center - point).length() <= self.radius + 1e-6 + + def draw(self, screen): + pg.draw.circle( + screen, + (255,0,0), + self.center, + self.radius, + width=1 + ) + pg.draw.circle( + screen, + (255,0,0), + self.center, + 2 + ) + +@dataclass +class Triangle: + a: ColorPoint + b: ColorPoint + c: ColorPoint + + @property + def edges(self) -> tuple[Edge, Edge, Edge]: + return (Edge(self.a, self.b), Edge(self.b, self.c), Edge(self.c, self.a)) + + @property + def vertices(self) -> tuple[ColorPoint, ColorPoint, ColorPoint]: + return (self.a, self.b, self.c) + + @property + def circumcircle(self) -> Circle | None: + ax, ay = self.a.x, self.a.y + bx, by = self.b.x, self.b.y + cx, cy = self.c.x, self.c.y + + D = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) + + if abs(D) < 1e-10: + return None + + ux = ((ax**2 + ay**2) * (by - cy) + (bx**2 + by**2) * (cy - ay) + (cx**2 + cy**2) * (ay - by)) / D + uy = ((ax**2 + ay**2) * (cx - bx) + (bx**2 + by**2) * (ax - cx) + (cx**2 + cy**2) * (bx - ax)) / D + + radius = sqrt((ax - ux)**2 + (ay - uy)**2) + + return Circle(pg.Vector2(ux, uy), radius) + + def draw(self, screen: pg.Surface) -> None: + pg.draw.lines( + screen, + (0,255,255), + True, + (self.a,self.b,self.c) + ) + + def opposite_point(self, edge: Edge) -> ColorPoint: + if Edge(self.a, self.b) == edge: return self.c + if Edge(self.a, self.c) == edge: return self.b + if Edge(self.b, self.c) == edge: return self.a + + def __hash__(self) -> int: + h = 0 + for edge in self.edges: h += hash(edge) + return h + +class Cell: + def __init__(self, seed: ColorPoint, hull: set[Edge], triangulation: set[Triangle], bounds: pg.Rect): + self.seed: ColorPoint = seed + self.points: list[ColorPoint] = [] + for t in triangulation: + if seed in t.vertices: + self.points.append(t.circumcircle.center) + for edge in t.edges: + if seed in (edge.a, edge.b) and edge in hull: + e = edge.b - edge.a + n = pg.Vector2(e.y, -e.x) + if n.dot((edge.a + edge.b) / 2 - t.opposite_point(edge)) < 0: + n = -n + self.points.append(t.circumcircle.center + n * 1000) + + self.points = sorted(self.points, key=lambda p: atan2(p.y - seed.y, p.x - seed.x)) + self._clip(bounds) + + @property + def edges(self) -> list[Edge]: + edges = [] + for a,b in pairwise(self.points): + edges.append(Edge(a,b)) + edges.append(Edge(self.points[-1], self.points[0])) + return edges + + + + def _clip(self, bounds: pg.Rect): + def _clip_axis(points, boundary, axis, is_max): + output = [] + if not points: + return output + for i in range(len(points)): + a = points[i] + b = points[(i + 1) % len(points)] + a_val = getattr(a, axis) + b_val = getattr(b, axis) + a_inside = a_val <= boundary if is_max else a_val >= boundary + b_inside = b_val <= boundary if is_max else b_val >= boundary + + if a_inside: + output.append(a) + if a_inside != b_inside: + t = (boundary - a_val) / (b_val - a_val) + output.append(a + t * (b - a)) + return output + points = self.points + points = _clip_axis(points, bounds.left, 'x', False) + points = _clip_axis(points, bounds.right, 'x', True) + points = _clip_axis(points, bounds.top, 'y', False) + points = _clip_axis(points, bounds.bottom, 'y', True) + self.points = points + + def draw(self, screen: pg.Surface, draw_point = True): + if len(self.points) < 3: return + pg.draw.polygon( + screen, + color=self.seed.color, + points=self.points + ) + if draw_point: + pg.draw.circle( + screen, + color=(-self.seed.color[0] + 255, -self.seed.color[1] + 255, -self.seed.color[2] + 255), + center=self.seed, + radius=4 + ) + + +def main(): + N = 70 + dimensions = (1920,1080) + pg.init() + clock = pg.time.Clock() + screen = pg.display.set_mode(dimensions) + + points = [] + movers = [] + mover_bounds = screen.get_rect().scale_by(1.5,1.5) + mover_bounds.center = screen.get_rect().center + for _ in range(N): + p = ColorPoint( + (random.randint(0,255),random.randint(0,255),random.randint(0,255)), + random.randint(0, dimensions[0]), + random.randint(0, dimensions[1]) + ) + points.append(p) + movers.append(Mover(p, mover_bounds, max_v=50)) + + + while True: + + super_triangle = Triangle( + ColorPoint((0,0,0),-100000, -100000), + ColorPoint((0,0,0),-100000, 100000), + ColorPoint((0,0,0),100000, 0) + ) + triangles = {super_triangle} + + + for p in points: + bad = set() + counter = Counter() + for t in triangles: + c = t.circumcircle + if c is None or c.point_within(p): + bad.add(t) + for edge in t.edges: counter[edge] += 1 + triangles = triangles.difference(bad) + for edge, count in counter.items(): + if count == 1: triangles.add(Triangle(p, edge.a, edge.b)) + + bad = set() + for t in triangles: + for v in t.vertices: + if v in super_triangle.vertices: bad.add(t) + + triangles = triangles.difference(bad) + + edge_counter = Counter() + + for t in triangles: + for edge in t.edges: edge_counter[edge] += 1 + + hull = {edge for edge, count in edge_counter.items() if count == 1} + + voronoi: list[Cell] = [] + + for p in points: + voronoi.append(Cell(p, hull, triangles, screen.get_rect())) + + dt = clock.tick() / 1000 + screen.fill((0,0,0)) + for mover in movers: mover.move(dt) + for v in voronoi: v.draw(screen, False) + pg.display.flip() + for event in pg.event.get(): + if event.type == pg.QUIT: exit() + + +if __name__ == "__main__": + main() \ No newline at end of file