Added files, initial commit
This commit is contained in:
BIN
__pycache__/lpgf.cpython-311.pyc
Normal file
BIN
__pycache__/lpgf.cpython-311.pyc
Normal file
Binary file not shown.
244
boid.py
Normal file
244
boid.py
Normal 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()
|
||||||
269
lpgf.py
Normal file
269
lpgf.py
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
from typing import Any
|
||||||
|
import pygame as pg
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
def isoceles_triangle(dimensions: tuple[int, int], color = (0,0,0,255)) -> pg.Surface:
|
||||||
|
surf = pg.Surface(dimensions, pg.SRCALPHA)
|
||||||
|
surf.fill((255,255,255,0))
|
||||||
|
pg.draw.polygon(
|
||||||
|
surf,
|
||||||
|
color,
|
||||||
|
(
|
||||||
|
(dimensions[0], dimensions[1] / 2),
|
||||||
|
(0,0),
|
||||||
|
(0, dimensions[1])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return surf
|
||||||
|
|
||||||
|
def circle(radius, color: tuple[int,int,int,int] = (0,0,0,255)) -> pg.Surface:
|
||||||
|
surf = pg.Surface((radius, radius), pg.SRCALPHA)
|
||||||
|
surf.fill((255,255,255,0))
|
||||||
|
pg.draw.circle(surf, color, (radius / 2, radius / 2), radius)
|
||||||
|
return surf
|
||||||
|
|
||||||
|
class Pane(pg.Surface):
|
||||||
|
|
||||||
|
on_click = lambda self, m_pos: None
|
||||||
|
on_click_release = lambda self, m_pos: None
|
||||||
|
on_click_held = lambda self, m_pos: None
|
||||||
|
on_middle_click = lambda self, m_pos: None
|
||||||
|
on_middle_click_release = lambda self, m_pos: None
|
||||||
|
on_middle_click_held = lambda self, m_pos: None
|
||||||
|
on_right_click = lambda self, m_pos: None
|
||||||
|
on_right_click_release = lambda self, m_pos: None
|
||||||
|
on_right_click_held = lambda self, m_pos: None
|
||||||
|
on_mouse_move = lambda self, l_m_pos, m_pos: None
|
||||||
|
on_mouse_enter = lambda self, m_pos: None
|
||||||
|
on_mouse_exit = lambda self, m_pos: None
|
||||||
|
|
||||||
|
def get_screen_position(self):
|
||||||
|
if isinstance(self._parent, Pane):
|
||||||
|
screen_position = self._parent.get_screen_position()
|
||||||
|
return (
|
||||||
|
self.position[0] + screen_position[0],
|
||||||
|
self.position[1] + screen_position[1]
|
||||||
|
)
|
||||||
|
return self.position
|
||||||
|
|
||||||
|
|
||||||
|
def get_screen_rect(self):
|
||||||
|
return self.get_rect().move(self.get_screen_position())
|
||||||
|
|
||||||
|
def get_rect(self) -> pg.Rect:
|
||||||
|
return super().get_rect().move(*self.position)
|
||||||
|
|
||||||
|
def get_rect_position_zero(self) -> pg.Rect:
|
||||||
|
return super().get_rect()
|
||||||
|
|
||||||
|
def master_pane(dimensions: tuple[int,int], blanking_color: tuple[int,int,int] = (0,0,0), **kwargs) -> 'Pane':
|
||||||
|
return Pane(
|
||||||
|
dimensions,
|
||||||
|
(0,0),
|
||||||
|
pg.display.set_mode(dimensions, **kwargs),
|
||||||
|
blanking_color=blanking_color,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dimensions: tuple[int,int],
|
||||||
|
position: tuple[int, int],
|
||||||
|
parent: pg.Surface,
|
||||||
|
blanking_color: tuple[int,int,int] = (0,0,0),
|
||||||
|
background_image: pg.Surface | None = None,
|
||||||
|
flags=0
|
||||||
|
):
|
||||||
|
self._m_last_state = [False,False,False]
|
||||||
|
self._m_last_inframe = False
|
||||||
|
self._m_last_pos = pg.mouse.get_pos()
|
||||||
|
self.position = position
|
||||||
|
self.blanking_color = blanking_color
|
||||||
|
self._background_image = background_image
|
||||||
|
self._subpanes: list['Pane'] = []
|
||||||
|
if isinstance(parent, Pane):
|
||||||
|
parent.add_subpane(self)
|
||||||
|
else:
|
||||||
|
self._parent = parent
|
||||||
|
super().__init__(dimensions, flags)
|
||||||
|
|
||||||
|
def add_subpane(self, subpane: 'Pane') -> None:
|
||||||
|
subpane._parent = self
|
||||||
|
self._subpanes.append(subpane)
|
||||||
|
|
||||||
|
def blank(self) -> None:
|
||||||
|
self.fill(self.blanking_color)
|
||||||
|
if self._background_image:
|
||||||
|
self.blit(self._background_image, (0,0))
|
||||||
|
for subpane in self._subpanes:
|
||||||
|
subpane.blank()
|
||||||
|
|
||||||
|
def poll_mouse(self, _m_upper_pane_position: tuple[int,int] | None = None, _occluded: bool = False) -> None:
|
||||||
|
if not _m_upper_pane_position:
|
||||||
|
_m_upper_pane_position = pg.mouse.get_pos()
|
||||||
|
m_pos = (
|
||||||
|
_m_upper_pane_position[0] - self.position[0],
|
||||||
|
_m_upper_pane_position[1] - self.position[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
for subpane in self._subpanes[::-1]:
|
||||||
|
subpane.poll_mouse(m_pos, _occluded)
|
||||||
|
if not _occluded and subpane.get_rect().collidepoint(m_pos): _occluded = True
|
||||||
|
|
||||||
|
m_state = pg.mouse.get_pressed()
|
||||||
|
m_inframe = not _occluded and pg.mouse.get_focused() and self.get_rect_position_zero().collidepoint(m_pos)
|
||||||
|
|
||||||
|
if not m_state[0] and self._m_last_state[0]:
|
||||||
|
self.on_click_release(m_pos)
|
||||||
|
self._m_last_state[0] = False
|
||||||
|
if not m_state[1] and self._m_last_state[1]:
|
||||||
|
self.on_middle_click_release(m_pos)
|
||||||
|
self._m_last_state[1] = False
|
||||||
|
if not m_state[2] and self._m_last_state[2]:
|
||||||
|
self.on_right_click_release(m_pos)
|
||||||
|
self._m_last_state[2] = False
|
||||||
|
|
||||||
|
if m_inframe:
|
||||||
|
if m_state[0]:
|
||||||
|
self.on_click_held(m_pos)
|
||||||
|
if not self._m_last_state[0]: self.on_click(m_pos)
|
||||||
|
if m_state[1]:
|
||||||
|
self.on_middle_click_held(m_pos)
|
||||||
|
if not self._m_last_state[1]: self.on_middle_click(m_pos)
|
||||||
|
if m_state[2]:
|
||||||
|
self.on_right_click_held(m_pos)
|
||||||
|
if not self._m_last_state[2]: self.on_right_click(m_pos)
|
||||||
|
if m_pos != self._m_last_pos:
|
||||||
|
self.on_mouse_move(self._m_last_pos, m_pos)
|
||||||
|
|
||||||
|
for index, state in enumerate(m_state):
|
||||||
|
if state: self._m_last_state[index] = True
|
||||||
|
|
||||||
|
#this is done so the on_click_release callback is still called if mouse is out of frame
|
||||||
|
|
||||||
|
if m_inframe and not self._m_last_inframe:
|
||||||
|
self.on_mouse_enter(m_pos)
|
||||||
|
elif not m_inframe and self._m_last_inframe:
|
||||||
|
self.on_mouse_exit(m_pos)
|
||||||
|
|
||||||
|
self._m_last_inframe = m_inframe
|
||||||
|
self._m_last_pos = m_pos
|
||||||
|
|
||||||
|
def update(self, **draw_options):
|
||||||
|
for subpane in self._subpanes:
|
||||||
|
subpane.update()
|
||||||
|
|
||||||
|
def move_to_front(self, item):
|
||||||
|
if item not in self._subpanes:
|
||||||
|
return
|
||||||
|
self._subpanes.remove(item)
|
||||||
|
self._subpanes.append(item)
|
||||||
|
|
||||||
|
def draw(self, special_flags: int = 0):
|
||||||
|
for subpane in self._subpanes:
|
||||||
|
subpane.draw()
|
||||||
|
self._parent.blit(self, self.position, special_flags=special_flags)
|
||||||
|
|
||||||
|
class DynamicPane(Pane):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_pane(self):
|
||||||
|
return self._content_pane
|
||||||
|
|
||||||
|
def _stick(self, _):
|
||||||
|
self._stuck = True
|
||||||
|
self._parent.move_to_front(self)
|
||||||
|
|
||||||
|
def _unstick(self, _):
|
||||||
|
self._stuck = False
|
||||||
|
parent_rect = self._parent.get_rect()
|
||||||
|
self_rect = self.get_rect()
|
||||||
|
if not parent_rect.collidepoint(self_rect.center):
|
||||||
|
self.position = (
|
||||||
|
parent_rect.center[0] - ceil(self_rect.width / 2),
|
||||||
|
parent_rect.center[1] - ceil(self_rect.height / 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dimensions: tuple[int,int],
|
||||||
|
position: tuple[int,int,int],
|
||||||
|
parent: Pane,
|
||||||
|
ribbon_height: int,
|
||||||
|
ribbon_color: tuple[int,int,int] = (255,255,255),
|
||||||
|
blanking_color: tuple[int,int,int] = (0,0,0),
|
||||||
|
background_image: pg.Surface | None = None,
|
||||||
|
content_flags=0,
|
||||||
|
ribbon_flags=0
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
dimensions,
|
||||||
|
position,
|
||||||
|
parent,
|
||||||
|
(255,255,255,0),
|
||||||
|
flags=pg.SRCALPHA
|
||||||
|
)
|
||||||
|
self._ribbon: Pane = Pane(
|
||||||
|
(dimensions[0], ribbon_height),
|
||||||
|
(0,0),
|
||||||
|
self,
|
||||||
|
ribbon_color,
|
||||||
|
flags=ribbon_flags
|
||||||
|
)
|
||||||
|
self._content_pane: Pane = Pane(
|
||||||
|
(dimensions[0], dimensions[1] - ribbon_height),
|
||||||
|
(0, ribbon_height),
|
||||||
|
self,
|
||||||
|
blanking_color=blanking_color,
|
||||||
|
background_image=background_image,
|
||||||
|
flags=content_flags
|
||||||
|
)
|
||||||
|
self._stuck = False
|
||||||
|
self._ribbon.on_click = self._stick
|
||||||
|
self._ribbon.on_click_release = self._unstick
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
m_pos = pg.mouse.get_pos()
|
||||||
|
s_pos = self.get_screen_position()
|
||||||
|
m_pane_pos = (
|
||||||
|
m_pos[0] - s_pos[0],
|
||||||
|
m_pos[1] - s_pos[1]
|
||||||
|
)
|
||||||
|
if self._stuck:
|
||||||
|
delta = (
|
||||||
|
m_pane_pos[0] - self._m_last_pos[0],
|
||||||
|
m_pane_pos[1] - self._m_last_pos[1]
|
||||||
|
)
|
||||||
|
self.position = (
|
||||||
|
self.position[0] + delta[0],
|
||||||
|
self.position[1] + delta[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
super().update()
|
||||||
|
|
||||||
|
|
||||||
|
class Slider(Pane):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dimensions: tuple[int,int],
|
||||||
|
parent: Pane,
|
||||||
|
knob_size: int,
|
||||||
|
bar_thickness: int,
|
||||||
|
knob_color: tuple[int,int,int] = (255,255,255),
|
||||||
|
bar_color: tuple[int,int,int] = (150,150,150)
|
||||||
|
):
|
||||||
|
self._bar = Pane(
|
||||||
|
(dimensions[0], bar_thickness),
|
||||||
|
(0, ceil(dimensions[1] / 2) + ceil(bar_thickness / 2)),
|
||||||
|
self,
|
||||||
|
bar_color
|
||||||
|
)
|
||||||
|
self._knob = Pane(
|
||||||
|
(knob_size, knob_size),
|
||||||
|
(0, ceil(dimensions[1] / 2) + ceil(knob_size / 2)),
|
||||||
|
self,
|
||||||
|
(0,0,0,0),
|
||||||
|
background_image=circle(knob_size, knob_color)
|
||||||
|
)
|
||||||
|
|
||||||
Reference in New Issue
Block a user