Files
voronoi/voronoi.py
2026-03-16 14:26:38 -04:00

261 lines
8.1 KiB
Python

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()