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