Genuary 19 - Flocking

2024-01-19

"""2024-01-19
Genuary 19 - Flocking
Simulação de 3 tipos de pássaros se agrupando e voando em conjunto.
gif
Sketch,py5,CreativeCoding,genuary,genuary19
"""
from collections import deque
from typing import List

import numpy as np
import py5
import py5_tools
import pymunk

from utils import helpers

sketch = helpers.info_for_sketch(__file__, __doc__)


space = pymunk.Space()

# Configuracao
DISTANCIA_VIZINHOS = 80
PESO_ALINHAMENTO = 0.2
PESO_COESAO = 0.1
DISTANCIA_SEPARACAO = 10
PESO_SEPARACAO = 8
FORCA_MAX_SEPARACAO = 1
AMORTECIMENTO_SEPARACAO = 0.5
VELOCIDADE_MAX = 6.0


GRUPOS = [
    [[], 40, 20, (255, 0, 0)],
    [[], 100, 10, (100, 255, 0)],
    [[], 80, 9, (0, 0, 220)],
]


class Passaro:
    def __init__(self, posicao, cor, tamanho):
        self.historico = deque(maxlen=10)
        self.historico.append(posicao)
        self.posicao = posicao
        self.velocidade = pymunk.Vec2d(
            np.random.uniform(-1, 1), np.random.uniform(-1, 1)
        )
        self.velocidade_max = VELOCIDADE_MAX
        self.cor = cor
        self.tamanho = tamanho
        body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
        body.posicao = posicao
        self.body = body
        space.add(body)

    def alinhamento(self, vizinhos: List["Passaro"]):
        if not vizinhos:
            return

        velocidade_med = pymunk.Vec2d(0, 0)
        for vizinho in vizinhos:
            velocidade_med += vizinho.velocidade

        velocidade_med /= len(vizinhos)
        self.velocidade += (velocidade_med - self.velocidade) * PESO_ALINHAMENTO

    def coesao(self, vizinhos: List["Passaro"]):
        if not vizinhos:
            return

        posicao_med = pymunk.Vec2d(0, 0)
        for vizinho in vizinhos:
            posicao_med += vizinho.posicao

        posicao_med /= len(vizinhos)
        cohesion_force = posicao_med - self.posicao
        self.velocidade += cohesion_force * PESO_COESAO

    def separacao(self, vizinhos: List["Passaro"]):
        if not vizinhos:
            return

        direcao = pymunk.Vec2d(0, 0)
        for vizinho in vizinhos:
            distancia = self.posicao.get_distance(vizinho.posicao)
            if 0 < distancia < DISTANCIA_SEPARACAO:
                diff = (self.posicao - vizinho.posicao) / distancia
                direcao += diff

        direcao = direcao.normalized() * FORCA_MAX_SEPARACAO
        direcao *= AMORTECIMENTO_SEPARACAO
        self.velocidade += direcao * PESO_SEPARACAO
        self.velocidade = self.velocidade.normalized() * self.velocidade_max

    def calcula_posicao(self, vizinhos: List["Passaro"]):
        self.alinhamento(vizinhos)
        self.coesao(vizinhos)
        self.separacao(vizinhos)
        self.posicao += self.velocidade
        self.ajusta_limites()
        self.historico.append(self.posicao)

    def desenha(self):
        r, g, b = self.cor
        cor = py5.color(r, g, b, 100)
        py5.fill(cor)
        py5.circle(self.posicao.x, self.posicao.y, self.tamanho)
        total = len(self.historico)
        cor = py5.color(r, g, b, 30)
        for idx, posicao in enumerate(self.historico):
            tamanho = (idx / total) * self.tamanho
            py5.stroke(cor)
            py5.stroke_weight(tamanho)
            py5.point(posicao.x, posicao.y)

    def ajusta_limites(self):
        x = self.posicao.x % py5.width
        y = self.posicao.y % py5.height
        self.posicao = pymunk.Vec2d(x, y)


def encontra_vizinhos(passaro, todos_passaros, range):
    vizinhos = []
    for outro in todos_passaros:
        if outro != passaro and passaro.posicao.get_distance(outro.posicao) < range:
            vizinhos.append(outro)
    return vizinhos


def setup():
    global GRUPOS
    py5.size(helpers.LARGURA, helpers.ALTURA, py5.P3D)
    for grupo, numero, tamanho, cor in GRUPOS:
        for _ in range(numero):
            posicao = pymunk.Vec2d(
                np.random.uniform(0, py5.width), np.random.uniform(0, py5.height)
            )
            passaro = Passaro(
                posicao,
                cor=cor,
                tamanho=tamanho,
            )
            grupo.append(passaro)


def draw():
    py5.background(0)
    for grupo, *_ in GRUPOS:
        for passaro in grupo:
            vizinhos = encontra_vizinhos(passaro, grupo, DISTANCIA_VIZINHOS)
            passaro.calcula_posicao(vizinhos)
            passaro.desenha()
    space.step(1 / py5.get_frame_rate())
    helpers.write_legend(sketch=sketch)


def key_pressed():
    key = py5.key
    if key == " ":
        save_and_close()


def save_and_close():
    py5.no_loop()
    py5.exit_sketch()


if __name__ == "__main__":
    py5_tools.animated_gif(
        f"{sketch.path}/{sketch.day}.gif",
        count=60,
        period=0.25,
        duration=0.00,
        block=False,
    )
    py5.run_sketch()