Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/demo/particles.ex
defmodule Raxol.Demo.Particles do
@moduledoc """
Particle system for animated demo effects.
Supports sparkles, explosions, trails, and floating elements.
"""
@sparkle_chars ~w(* . + ' `)
@star_chars ["*", ".", "+", "o"]
@trail_chars ["â–ˆ", "â–“", "â–’", "â–‘"]
# Size-based character sets
@small_chars [".", "'", "`", ","]
@medium_chars ["*", "+", "o", "x"]
@large_chars ["@", "#", "O", "*"]
# Afterimage trail characters (bright to dim)
@afterimage_chars [".", "'", "`"]
@cyan_palette [51, 50, 49, 44, 45]
@magenta_palette [201, 200, 199, 164, 165]
@gold_palette [220, 221, 222, 228, 229]
@white_palette [255, 254, 253, 252, 251]
@red_palette [196, 197, 198, 199, 200]
@type particle :: %{
:x => float(),
:y => float(),
:vx => float(),
:vy => float(),
:char => String.t(),
:color => non_neg_integer(),
:life => integer(),
:history => list({float(), float()}),
:generation => non_neg_integer(),
:spawn_at_life => non_neg_integer(),
optional(:phase) => float()
}
@doc """
Creates a new particle at the given position.
"""
@spec create(float(), float(), keyword()) :: particle()
def create(x, y, opts \\ []) do
%{
x: x,
y: y,
vx: Keyword.get(opts, :vx, (:rand.uniform() - 0.5) * 2),
vy: Keyword.get(opts, :vy, -:rand.uniform() * 2),
char: Keyword.get(opts, :char, Enum.random(@sparkle_chars)),
color: Keyword.get(opts, :color, random_color()),
life: Keyword.get(opts, :life, 20 + :rand.uniform(30)),
history: Keyword.get(opts, :history, []),
generation: Keyword.get(opts, :generation, 0),
spawn_at_life: Keyword.get(opts, :spawn_at_life, 0)
}
end
@doc """
Creates a particle with size-based character selection.
Size can be :small, :medium, or :large.
"""
@spec create_sized(float(), float(), atom(), keyword()) :: particle()
def create_sized(x, y, size, opts \\ []) do
chars =
case size do
:small -> @small_chars
:medium -> @medium_chars
:large -> @large_chars
_ -> @medium_chars
end
create(x, y, Keyword.put(opts, :char, Enum.random(chars)))
end
@doc """
Creates a particle with trail tracking enabled.
"""
@spec create_with_trail(float(), float(), keyword()) :: particle()
def create_with_trail(x, y, opts \\ []) do
create(x, y, Keyword.put(opts, :history, [{x, y}]))
end
@doc """
Creates a sparkle particle (rising, fading).
"""
@spec create_sparkle(float(), float()) :: particle()
def create_sparkle(x, y) do
create(x, y,
vx: (:rand.uniform() - 0.5) * 0.5,
vy: -0.3 - :rand.uniform() * 0.5,
char: Enum.random(@sparkle_chars),
color: Enum.random(@white_palette ++ @cyan_palette),
life: 30 + :rand.uniform(40)
)
end
@doc """
Creates an explosion particle (radiates outward).
"""
@spec create_explosion(float(), float()) :: particle()
def create_explosion(x, y) do
angle = :rand.uniform() * 2 * :math.pi()
speed = 0.5 + :rand.uniform() * 1.5
create(x, y,
vx: :math.cos(angle) * speed,
vy: :math.sin(angle) * speed * 0.5,
char: Enum.random(@star_chars),
color: Enum.random(@gold_palette ++ @magenta_palette),
life: 15 + :rand.uniform(25)
)
end
@doc """
Creates an explosion particle with size distribution.
70% small, 25% medium, 5% large.
"""
@spec create_sized_explosion(float(), float()) :: particle()
def create_sized_explosion(x, y) do
size =
case :rand.uniform(100) do
n when n <= 70 -> :small
n when n <= 95 -> :medium
_ -> :large
end
angle = :rand.uniform() * 2 * :math.pi()
speed = 0.5 + :rand.uniform() * 1.5
create_sized(x, y, size,
vx: :math.cos(angle) * speed,
vy: :math.sin(angle) * speed * 0.5,
color: Enum.random(@gold_palette ++ @magenta_palette),
life: 15 + :rand.uniform(25)
)
end
@doc """
Creates a cascading explosion particle that spawns children.
"""
@spec create_cascade_explosion(float(), float(), integer()) :: particle()
def create_cascade_explosion(x, y, generation) do
angle = :rand.uniform() * 2 * :math.pi()
speed = 0.8 + :rand.uniform() * 1.2
life = 20 + :rand.uniform(15)
spawn_at = div(life, 2)
create(x, y,
vx: :math.cos(angle) * speed,
vy: :math.sin(angle) * speed * 0.5,
char: Enum.random(@star_chars),
color: Enum.random(@gold_palette ++ @magenta_palette ++ @red_palette),
life: life,
generation: generation,
spawn_at_life: spawn_at,
history: [{x, y}]
)
end
@doc """
Creates a trail particle (slow fade, minimal movement).
"""
@spec create_trail(float(), float(), non_neg_integer()) :: particle()
def create_trail(x, y, color) do
create(x, y,
vx: 0,
vy: 0,
char: Enum.at(@trail_chars, :rand.uniform(4) - 1),
color: color,
life: 5 + :rand.uniform(10)
)
end
@doc """
Creates a rain particle (falls down).
"""
@spec create_rain(float(), float(), non_neg_integer()) :: particle()
def create_rain(x, y, color) do
create(x, y,
vx: (:rand.uniform() - 0.5) * 0.2,
vy: 0.3 + :rand.uniform() * 0.3,
char: Enum.random([".", "|", ":", "'"]),
color: color,
life: 40 + :rand.uniform(20)
)
end
@doc """
Creates a rain particle with phase for sine wave motion.
"""
@spec create_rain_with_phase(float(), float(), non_neg_integer(), float()) ::
particle()
def create_rain_with_phase(x, y, color, phase) do
p = create_rain(x, y, color)
Map.put(p, :phase, phase)
end
@doc """
Creates a ring of particles expanding outward.
Y is compressed for terminal aspect ratio.
"""
@spec create_ring_burst(float(), float(), integer(), keyword()) ::
list(particle())
def create_ring_burst(center_x, center_y, count \\ 24, opts \\ []) do
speed = Keyword.get(opts, :speed, 1.5)
color = Keyword.get(opts, :color, random_color())
life = Keyword.get(opts, :life, 25)
char = Keyword.get(opts, :char, "*")
for i <- 0..(count - 1) do
angle = i / count * 2 * :math.pi()
create(center_x, center_y,
vx: :math.cos(angle) * speed,
vy: :math.sin(angle) * speed * 0.5,
char: char,
color: color,
life: life + :rand.uniform(10),
history: [{center_x, center_y}]
)
end
end
@doc """
Creates a rising rocket particle with trail.
"""
@spec create_rocket(float(), float(), keyword()) :: particle()
def create_rocket(x, y, opts \\ []) do
target_y = Keyword.get(opts, :target_y, 5)
color = Keyword.get(opts, :color, random_color())
create(x, y,
vx: (:rand.uniform() - 0.5) * 0.3,
vy: -1.2 - :rand.uniform() * 0.5,
char: "|",
color: color,
life: round((y - target_y) * 1.2),
history: [{x, y}]
)
end
@doc """
Updates a particle's position and life.
"""
@spec update(particle(), keyword()) :: particle()
def update(particle, opts \\ []) do
gravity = Keyword.get(opts, :gravity, 0.05)
friction = Keyword.get(opts, :friction, 0.98)
%{
particle
| x: particle.x + particle.vx,
y: particle.y + particle.vy,
vx: particle.vx * friction,
vy: particle.vy + gravity,
life: particle.life - 1
}
end
@doc """
Updates particle without gravity (for floating effects).
"""
@spec update_float(particle()) :: particle()
def update_float(particle) do
update(particle, gravity: 0, friction: 1.0)
end
@doc """
Updates particle and maintains position history for trail effect.
Keeps last 4 positions.
"""
@spec update_with_trail(particle(), keyword()) :: particle()
def update_with_trail(particle, opts \\ []) do
history = Map.get(particle, :history, [])
new_history = [{particle.x, particle.y} | Enum.take(history, 3)]
particle
|> update(opts)
|> Map.put(:history, new_history)
end
@doc """
Updates cascading particles and returns {updated_particle, spawned_children}.
Call this when you need cascade behavior.
"""
@spec update_cascade(particle(), keyword()) :: {particle(), list(particle())}
def update_cascade(particle, opts \\ []) do
updated = update_with_trail(particle, opts)
generation = Map.get(particle, :generation, 0)
spawn_at = Map.get(particle, :spawn_at_life, 0)
children =
if generation > 0 and updated.life == spawn_at do
for _ <- 1..(3 + :rand.uniform(2)) do
create_cascade_explosion(updated.x, updated.y, generation - 1)
end
else
[]
end
{updated, children}
end
@doc """
Renders particles to a frame buffer, returns ANSI escape sequences.
"""
@spec render(list(particle()), non_neg_integer(), non_neg_integer()) ::
String.t()
def render(particles, width, height) do
particles
|> Enum.filter(fn p ->
p.life > 0 and p.x >= 0 and p.x < width and p.y >= 0 and p.y < height
end)
|> Enum.map_join(fn p ->
x = trunc(p.x) + 1
y = trunc(p.y) + 1
alpha = min(1.0, p.life / 20)
color_code = if alpha > 0.5, do: p.color, else: dim_color(p.color)
"\e[#{y};#{x}H\e[38;5;#{color_code}m#{p.char}\e[0m"
end)
end
@doc """
Renders particles with their trail history.
Trail chars fade from bright to dim.
"""
@spec render_with_trails(
list(particle()),
non_neg_integer(),
non_neg_integer()
) :: String.t()
def render_with_trails(particles, width, height) do
trail_output =
particles
|> Enum.flat_map(&render_particle_trail(&1, width, height))
|> Enum.join("")
trail_output <> render(particles, width, height)
end
defp render_particle_trail(particle, width, height) do
color = dim_color(dim_color(particle.color))
particle
|> Map.get(:history, [])
|> Enum.with_index()
|> Enum.map(fn {{hx, hy}, idx} ->
render_trail_point(hx, hy, idx, color, width, height)
end)
end
defp render_trail_point(hx, hy, idx, color, width, height)
when hx >= 0 and hx < width and hy >= 0 and hy < height do
x = trunc(hx) + 1
y = trunc(hy) + 1
char = Enum.at(@afterimage_chars, min(idx, length(@afterimage_chars) - 1))
"\e[#{y};#{x}H\e[38;5;#{color}m#{char}\e[0m"
end
defp render_trail_point(_hx, _hy, _idx, _color, _width, _height), do: ""
@doc """
Filters out dead particles.
"""
@spec prune(list(particle())) :: list(particle())
def prune(particles) do
Enum.filter(particles, fn p -> p.life > 0 end)
end
@doc """
Returns a random color from the available palettes.
"""
@spec random_color() :: non_neg_integer()
def random_color do
Enum.random(
@cyan_palette ++ @magenta_palette ++ @gold_palette ++ @white_palette
)
end
@doc """
Returns a random color from a specific palette.
"""
@spec palette_color(atom()) :: non_neg_integer()
def palette_color(:cyan), do: Enum.random(@cyan_palette)
def palette_color(:magenta), do: Enum.random(@magenta_palette)
def palette_color(:gold), do: Enum.random(@gold_palette)
def palette_color(:white), do: Enum.random(@white_palette)
def palette_color(:red), do: Enum.random(@red_palette)
defp dim_color(color) when color > 240, do: color - 3
defp dim_color(color), do: max(232, color - 2)
end