Fireworks Celebration

Five simulated fireworks burst in mid-air, each composed of 80–150 particles flung outward at random angles and exponential speeds. A linear gravity term curves the trajectories, and short trails fade behind every particle for a long-exposure feel.

Each firework owns its own dm.cspace ramp pulled from random opening and closing colour names — no two bursts repeat the same gradient, even on subsequent runs (when the seed changes).

plot particles fireworks
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle

import dartwork_mpl as dm

np.random.seed(42)
dm.style.use("scientific")

fig, ax = plt.subplots(figsize=dm.figsize("14cm", 0.8))

n_fireworks = 5
explosion_data = []

for _fw in range(n_fireworks):
    center_x = np.random.uniform(-8, 8)
    center_y = np.random.uniform(2, 8)
    n_particles_fw = np.random.randint(80, 150)
    explosion_time = np.random.uniform(0, 0.5)

    color_start = (
        f"oc.{np.random.choice(['red', 'blue', 'green', 'yellow', 'purple'])}9"
    )
    color_end = (
        f"oc.{np.random.choice(['orange', 'cyan', 'pink', 'teal', 'violet'])}3"
    )
    colors = dm.cspace(color_start, color_end, n=n_particles_fw)

    angles = np.random.uniform(0, 2 * np.pi, n_particles_fw)
    speeds = np.random.exponential(2, n_particles_fw) + np.random.uniform(1, 3)

    explosion_data.append(
        {
            "center": (center_x, center_y),
            "particles": n_particles_fw,
            "angles": angles,
            "speeds": speeds,
            "colors": colors,
            "time": explosion_time,
        }
    )

for fw_data in explosion_data:
    center_x, center_y = fw_data["center"]
    t = 2 - fw_data["time"]
    gravity = 0.5

    for i in range(fw_data["particles"]):
        angle = fw_data["angles"][i]
        speed = fw_data["speeds"][i]
        x = center_x + speed * np.cos(angle) * t
        y = center_y + speed * np.sin(angle) * t - 0.5 * gravity * t**2

        trail_steps = 10
        for j in range(trail_steps):
            t_trail = t - j * 0.05
            if t_trail > 0:
                x_trail = center_x + speed * np.cos(angle) * t_trail
                y_trail = (
                    center_y
                    + speed * np.sin(angle) * t_trail
                    - 0.5 * gravity * t_trail**2
                )
                alpha = (1 - j / trail_steps) * (1 - t / 3) * 0.5
                size = 3 * (1 - j / trail_steps)
                ax.scatter(
                    x_trail,
                    y_trail,
                    s=size,
                    c=[fw_data["colors"][i].to_hex()],
                    alpha=alpha,
                )

        if y > -2:
            alpha = max(0, 1 - t / 3)
            ax.scatter(
                x,
                y,
                s=10,
                c=[fw_data["colors"][i].to_hex()],
                edgecolors="white",
                linewidths=0.3,
                alpha=alpha,
                zorder=10,
            )

    if fw_data["time"] < 0.1:
        ax.add_patch(
            Circle(
                (center_x, center_y),
                1,
                color="white",
                alpha=0.5 - fw_data["time"] * 5,
            )
        )

# Sparkling background stars
n_stars = 100
ax.scatter(
    np.random.uniform(-10, 10, n_stars),
    np.random.uniform(-2, 10, n_stars),
    s=np.random.exponential(1, n_stars),
    c="white",
    alpha=np.random.uniform(0.2, 0.8, n_stars),
    marker="*",
)

ax.fill_between([-10, 10], -2, -2, color="dc.ocean5", alpha=0.3)

ax.set_xlim(-10, 10)
ax.set_ylim(-2, 10)
ax.set_aspect("equal")
for s in ax.spines.values():
    s.set_visible(False)
ax.set_facecolor("dc.nordic5")

ax.text(
    0,
    -3,
    "Fireworks Celebration",
    ha="center",
    fontsize=dm.fs(3),
    color="white",
    weight="bold",
)

dm.simple_layout(fig)
plt.show()

Total running time of the script: (1 minutes 23.452 seconds)