Initial
This commit is contained in:
commit
91a723e40d
8 changed files with 283 additions and 0 deletions
BIN
animation-day.mp4
Normal file
BIN
animation-day.mp4
Normal file
Binary file not shown.
BIN
animation-night.mp4
Normal file
BIN
animation-night.mp4
Normal file
Binary file not shown.
BIN
animation.mp4
Normal file
BIN
animation.mp4
Normal file
Binary file not shown.
1
config.py
Normal file
1
config.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
WIDTH, HEIGHT = 1280, 720
|
||||
82
ember.py
Normal file
82
ember.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import pygame
|
||||
from typing import Tuple
|
||||
import numpy as np
|
||||
import pygame.gfxdraw
|
||||
from math import ceil
|
||||
from config import HEIGHT, WIDTH
|
||||
from utils import clamp, mix
|
||||
|
||||
BLUR_RADIUS = 3
|
||||
TTL_LOSS_NEAR_BORDER_IN_PIXELS = 200
|
||||
UP_DRAFT = 300
|
||||
|
||||
|
||||
class Ember:
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.position = (np.random.randint(0, WIDTH), HEIGHT)
|
||||
self.up_draft = np.random.uniform(0.3, 1) * UP_DRAFT
|
||||
self.time_to_live = np.random.uniform(1, 6)
|
||||
self.color_offset = np.random.uniform(0, 1)
|
||||
self.size = np.random.uniform(2.5, 6.5)
|
||||
|
||||
def update(self, velocity: Tuple[float, float], delta_time: float):
|
||||
self.position = (
|
||||
self.position[0] + velocity[0] * delta_time,
|
||||
self.position[1] + (velocity[1] - self.up_draft) * delta_time,
|
||||
)
|
||||
self.time_to_live -= delta_time
|
||||
if self.distance_from_screen_border() < TTL_LOSS_NEAR_BORDER_IN_PIXELS:
|
||||
self.time_to_live = min(
|
||||
self.distance_from_screen_border() / TTL_LOSS_NEAR_BORDER_IN_PIXELS,
|
||||
self.time_to_live,
|
||||
)
|
||||
|
||||
if self.time_to_live < 0:
|
||||
self.reset()
|
||||
|
||||
def draw(self, screen: pygame.Surface):
|
||||
color = (
|
||||
int(mix(255, 189, self.color_offset)),
|
||||
int(mix(165, 40, self.color_offset)),
|
||||
int(mix(0, 40, self.color_offset)),
|
||||
255 * (1 - clamp((0.5 - self.time_to_live) * 2, 0, 1) ** 2),
|
||||
)
|
||||
draw_antialiased_circle(
|
||||
screen, self.position[0], self.position[1], int(self.size), color
|
||||
)
|
||||
|
||||
def distance_from_screen_border(self):
|
||||
return min(
|
||||
self.position[0],
|
||||
WIDTH - self.position[0],
|
||||
self.position[1],
|
||||
# HEIGHT - self.position[1],
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Ember(position={self.position}, velocity={self.velocity}, time_to_live={self.time_to_live})"
|
||||
|
||||
|
||||
def draw_antialiased_circle(surface: pygame.Surface, x, y, radius, color):
|
||||
for dx in range(-ceil(radius), ceil(radius) + 1):
|
||||
for dy in range(-ceil(radius), ceil(radius) + 1):
|
||||
distance = ((dx - x % 1) ** 2 + (dy - y % 1) ** 2) ** 0.5 - radius
|
||||
if (
|
||||
distance < 0
|
||||
and 0 <= x + dx < surface.get_width()
|
||||
and 0 <= y + dy < surface.get_height()
|
||||
):
|
||||
alpha = min(color[3] / 255, -distance / BLUR_RADIUS)
|
||||
value = surface.get_at((int(x + dx), int(y + dy)))
|
||||
surface.set_at(
|
||||
(int(x + dx), int(y + dy)),
|
||||
(
|
||||
mix(value[0], color[0], alpha),
|
||||
mix(value[1], color[1], alpha),
|
||||
mix(value[2], color[2], alpha),
|
||||
mix(value[3], color[3], alpha),
|
||||
),
|
||||
)
|
||||
73
main.py
Normal file
73
main.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from typing import List
|
||||
import pygame
|
||||
import sys
|
||||
import numpy as np
|
||||
import imageio
|
||||
from config import HEIGHT, WIDTH
|
||||
from tqdm import tqdm
|
||||
from ember import Ember
|
||||
from wind import WindField
|
||||
|
||||
|
||||
IS_DEVELOPMENT = False
|
||||
EMBER_COUNT = 600
|
||||
FRAME_RATE = 60
|
||||
RUNNING_TIME_IN_SECONDS = 300
|
||||
WIND_STRENTH = 300
|
||||
|
||||
delta_time = 1 / 150
|
||||
|
||||
|
||||
embers: List[Ember] = [Ember() for _ in range(EMBER_COUNT)]
|
||||
wind_field = WindField(WIDTH, HEIGHT)
|
||||
|
||||
|
||||
pygame.init()
|
||||
|
||||
flags = pygame.HWSURFACE | pygame.HWACCEL
|
||||
screen = pygame.display.set_mode((WIDTH, HEIGHT), flags)
|
||||
pygame.display.set_caption("Animated Video")
|
||||
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
video_file = "animation.mp4"
|
||||
video_writer = imageio.get_writer(video_file, fps=FRAME_RATE)
|
||||
|
||||
for frame in tqdm(range(RUNNING_TIME_IN_SECONDS * FRAME_RATE)):
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
|
||||
# screen.fill((0, 0, 0))
|
||||
# Add a semi-transparent rectangle to create the trail effect
|
||||
fade_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
|
||||
fade_surface.fill(
|
||||
(0, 0, 0, 10)
|
||||
) # Adjust the last parameter for the opacity (0-255)
|
||||
screen.blit(fade_surface, (0, 0))
|
||||
|
||||
wind_field.update(time=frame / 150)
|
||||
# wind_field.draw(screen)
|
||||
|
||||
for ember in embers:
|
||||
wind = wind_field.get_wind(ember.position)
|
||||
velocity_x = wind[0] * WIND_STRENTH
|
||||
velocity_y = wind[1] * WIND_STRENTH
|
||||
|
||||
ember.update((velocity_x, velocity_y), delta_time=delta_time)
|
||||
ember.draw(screen)
|
||||
|
||||
pygame.display.flip()
|
||||
|
||||
if not IS_DEVELOPMENT:
|
||||
current_frame = pygame.surfarray.array3d(screen)
|
||||
current_frame = np.rot90(current_frame)
|
||||
# current_frame = np.flip(current_frame, axis=0)
|
||||
video_writer.append_data(current_frame.copy())
|
||||
|
||||
if IS_DEVELOPMENT:
|
||||
clock.tick(FRAME_RATE)
|
||||
|
||||
if not IS_DEVELOPMENT:
|
||||
video_writer.close()
|
||||
6
utils.py
Normal file
6
utils.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
def clamp(x, min_value, max_value):
|
||||
return max(min(x, max_value), min_value)
|
||||
|
||||
|
||||
def mix(a, b, mix):
|
||||
return a * (1 - mix) + b * mix
|
||||
121
wind.py
Normal file
121
wind.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
from typing import Tuple
|
||||
import numpy as np
|
||||
import noise
|
||||
import pygame
|
||||
|
||||
from utils import clamp
|
||||
|
||||
SEED = 42
|
||||
np.random.seed(SEED)
|
||||
|
||||
|
||||
class WindField:
|
||||
def __init__(self, width, height, downscale=10):
|
||||
self.width = int(width / downscale)
|
||||
self.height = int(height / downscale)
|
||||
self.downscale = downscale
|
||||
self.field_x = np.zeros((self.width, self.height))
|
||||
self.field_y = np.zeros((self.width, self.height))
|
||||
|
||||
def update(
|
||||
self,
|
||||
time=0,
|
||||
scale=15.0,
|
||||
time_scale=2.0,
|
||||
octaves=5,
|
||||
persistence=0.3,
|
||||
lacunarity=4.0,
|
||||
):
|
||||
for i in range(self.width):
|
||||
for j in range(self.height):
|
||||
self.field_x[i][j] = noise.pnoise3(
|
||||
i / scale,
|
||||
j / scale,
|
||||
time / time_scale,
|
||||
octaves=octaves,
|
||||
persistence=persistence,
|
||||
lacunarity=lacunarity,
|
||||
base=SEED,
|
||||
)
|
||||
self.field_y[i][j] = noise.pnoise3(
|
||||
i / scale,
|
||||
j / scale,
|
||||
time / time_scale,
|
||||
octaves=octaves,
|
||||
persistence=persistence,
|
||||
lacunarity=lacunarity,
|
||||
base=SEED + 1,
|
||||
)
|
||||
|
||||
def draw(self, screen):
|
||||
for i in range(self.width):
|
||||
for j in range(self.height):
|
||||
color = (
|
||||
abs(self.field_x[i][j] * 255),
|
||||
abs(self.field_y[i][j] * 255),
|
||||
0,
|
||||
)
|
||||
pygame.draw.rect(
|
||||
screen,
|
||||
color,
|
||||
(
|
||||
i * self.downscale,
|
||||
j * self.downscale,
|
||||
self.downscale,
|
||||
self.downscale,
|
||||
),
|
||||
)
|
||||
|
||||
# draw with get_wind
|
||||
# def draw(self, screen):
|
||||
# for i in range(self.width * self.downscale):
|
||||
# for j in range(self.height * self.downscale):
|
||||
# wind = self.get_wind((i, j))
|
||||
# color = (
|
||||
# abs(wind[0] * 255),
|
||||
# abs(wind[1] * 255),
|
||||
# 0,
|
||||
# )
|
||||
# pygame.draw.rect(
|
||||
# screen,
|
||||
# color,
|
||||
# (
|
||||
# i,
|
||||
# j,
|
||||
# 1,
|
||||
# 1,
|
||||
# ),
|
||||
# )
|
||||
|
||||
def get_wind(self, position: Tuple[int, int]) -> Tuple[float, float]:
|
||||
x, y = position
|
||||
x /= self.downscale
|
||||
y /= self.downscale
|
||||
|
||||
x0 = int(x)
|
||||
y0 = int(y)
|
||||
x1 = x0 + 1
|
||||
y1 = y0 + 1
|
||||
|
||||
x0 = clamp(x0, 0, self.width - 1)
|
||||
y0 = clamp(y0, 0, self.height - 1)
|
||||
x1 = clamp(x1, 0, self.width - 1)
|
||||
y1 = clamp(y1, 0, self.height - 1)
|
||||
|
||||
dx = x - x0
|
||||
dy = y - y0
|
||||
|
||||
wind_x = (
|
||||
(1 - dx) * (1 - dy) * self.field_x[x0][y0]
|
||||
+ dx * (1 - dy) * self.field_x[x1][y0]
|
||||
+ (1 - dx) * dy * self.field_x[x0][y1]
|
||||
+ dx * dy * self.field_x[x1][y1]
|
||||
)
|
||||
wind_y = (
|
||||
(1 - dx) * (1 - dy) * self.field_y[x0][y0]
|
||||
+ dx * (1 - dy) * self.field_y[x1][y0]
|
||||
+ (1 - dx) * dy * self.field_y[x0][y1]
|
||||
+ dx * dy * self.field_y[x1][y1]
|
||||
)
|
||||
|
||||
return wind_x, wind_y
|
||||
Loading…
Add table
Add a link
Reference in a new issue