Added files, initial commit

This commit is contained in:
=
2024-11-17 12:35:37 -06:00
parent 7122402eb3
commit 91116ec69f
5 changed files with 513 additions and 0 deletions

244
boid.py Normal file
View File

@@ -0,0 +1,244 @@
import pygame as pg
from math import sqrt, ceil, floor
from random import random, randint
from lpgf import Pane, DynamicPane, isoceles_triangle
SCREEN_DIMENSIONS = (1500, 1000)
PADDING_CONSTANT = 0.75
FRAME_RATE = 144
N_BOIDS = 250
def clamp(x, min, max) -> int:
if x < min:
return min
if x > max:
return max
return x
def surface_color(position: pg.Vector2, surf: pg.Surface) -> tuple[int,int,int]:
rect = surf.get_rect()
x = clamp(position.x, 0, rect.width)
y = clamp(position.y, 0, rect.height)
return (
ceil(255 * (x / rect.width)),
ceil(255 * (y / rect.height)),
100
)
class Boid:
def __init__(
self,
search_radius: float,
separation_radius: float,
max_speed: float,
cohesion: float,
alignment: float,
separation: float,
bounds: pg.Rect,
image: pg.Surface = isoceles_triangle((15,10)) #this is the same triangle instance on purpose
):
self.position = pg.Vector2(randint(bounds.left, bounds.right), randint(bounds.top, bounds.bottom))
self.velocity = pg.Vector2(2*(random()-0.5)*max_speed, 2*(random()-0.5)*max_speed) #Know this can technically produce velicities higher than max, dont care
self._acceleration = pg.Vector2(0,0)
self._max_speed = max_speed
self.cohesion = cohesion
self.alignment = alignment
self.separation = separation
self.bounds = bounds
self._radius_squared = search_radius**2
self._separation_radius_squared = separation_radius**2
self._image = pg.transform.scale_by(image, random() * 0.5 + 0.5)
@property
def search_radius(self):
return sqrt(self._radius_squared)
@search_radius.setter
def search_radius(self, rad: float):
self._radius_squared = rad**2
@property
def separation_radius(self):
return sqrt(self._separation_radius_squared)
@separation_radius.setter
def separation_radius(self, speed: float):
self._max_speed_squared = speed**2
def calculate_next_timestep(self, boids: list['Boid']):
centroid_target = pg.Vector2(0,0)
velocity_target = pg.Vector2(0,0)
nearby = 0
for b in boids:
if (self.position-b.position).length_squared() > self._radius_squared or b is self:
continue
centroid_target += b.position
velocity_target += b.velocity
nearby += 1
if (self.position - b.position).length_squared() < self._separation_radius_squared:
self._acceleration += self.separation * (self.position - b.position).normalize()
if nearby > 0:
centroid_target /= nearby
velocity_target /= nearby
self._acceleration += self.cohesion * (centroid_target - self.position).normalize()
self._acceleration += self.alignment * velocity_target.normalize()
if self.position.y < self.bounds.top:
self._acceleration += pg.Vector2(0,150)
elif self.position.y > self.bounds.bottom:
self._acceleration -= pg.Vector2(0,150)
if self.position.x < self.bounds.left:
self._acceleration += pg.Vector2(150,0)
elif self.position.x > self.bounds.right:
self._acceleration -= pg.Vector2(150,0)
def attract(self, p: pg.Vector2, radius: float, strength: float) -> None:
diff = p - self.position
if diff.length_squared() < radius**2:
self._acceleration += strength * diff.normalize()
def update(self, dt: float) -> None:
self.velocity += dt * self._acceleration
if self.velocity.length_squared() > self._max_speed**2:
self.velocity.scale_to_length(self._max_speed)
self.position += self.velocity * dt
self._acceleration = pg.Vector2(0,0)
def draw(self, surface: pg.Surface):
image = self._image.copy() #as per pygame docs, do not continuously rotate an image. Make a copy of a prefab image
image.fill(surface_color(self.position, surface), special_flags=pg.BLEND_ADD)
image = pg.transform.rotate(image, self.velocity.angle_to(pg.Vector2(1,0)))
surface.blit(image, self.position)
class BoidSim:
def set_attractor(self, m_pos):
self._attractor = m_pos
def clear_attractor(self, _):
self._attractor = None
def set_repulsor(self, m_pos):
self._repulsor = m_pos
def clear_repulsor(self, _):
self._repulsor = None
def clear_all(self, m_pos):
self.clear_repulsor(m_pos)
self.clear_attractor(m_pos)
def spawn(self, m_pos):
boid = Boid(
150,
20,
100,
80,
80,
100,
self._pane.get_rect().scale_by(0.75),
)
boid.position = pg.Vector2(m_pos)
self._boids.append(boid)
def __init__(self, n_boids: int, pane: Pane):
self._pane: Pane = pane
self._pane.on_click_held = self.set_attractor
self._pane.on_click_release = self.clear_attractor
self._pane.on_right_click = self.set_repulsor
self._pane.on_right_click_release = self.clear_repulsor
self._pane.on_middle_click = self.spawn
self._pane.on_mouse_exit = self.clear_all
self._attractor: tuple[int,int] | None = None
self._repulsor: tuple[int,int] | None = None
self._boids: list[Boid] = []
bounds = pane.get_rect_position_zero().scale_by(0.75)
for _ in range(n_boids):
self._boids.append(
Boid(
150,
20,
100,
80,
80,
100,
bounds,
)
)
def update(self, dt: float):
for boid in self._boids:
boid.calculate_next_timestep(self._boids)
if self._attractor:
boid.attract(self._attractor, 200, 1000)
if self._repulsor:
boid.attract(self._repulsor, 200, -1000)
boid.update(dt)
boid.draw(self._pane)
def main():
running = True
pg.init()
clock = pg.time.Clock()
cat = pg.image.load("cat.png")
screen: Pane = Pane.master_pane(SCREEN_DIMENSIONS, (0,0,255))
boid_pane1 = DynamicPane(
(cat.get_rect().width, cat.get_rect().height),
(10,10),
screen,
10,
blanking_color=(150,0,150),
background_image=cat
)
boid_pane2 = DynamicPane(
(200,200),
(250,250),
boid_pane1.content_pane,
10,
blanking_color=(0,0,0,100),
ribbon_color=(0,50,100,180),
content_flags=pg.SRCALPHA,
ribbon_flags=pg.SRCALPHA
)
DynamicPane(
(100,100),
(0,0),
screen,
10,
blanking_color=(255,255,255,0),
background_image=isoceles_triangle((100,90)),
content_flags=pg.SRCALPHA
)
sim1 = BoidSim(20, screen)
sim2 = BoidSim(20, boid_pane2.content_pane)
while running:
screen.blank()
screen.update()
screen.poll_mouse()
dt = clock.tick(FRAME_RATE) / 1000
print(screen._subpanes)
events = pg.event.get()
for event in events:
if event.type == pg.QUIT:
running = False
sim1.update(dt)
sim2.update(dt)
screen.draw()
pg.display.flip()
if __name__ == "__main__":
main()