use std::f32::consts::TAU;
use tiny_skia::PathBuilder;
use super::{Canvas, Push, Random};
const MAX_PARTICLES: usize = 640;
const WARMUP_STEPS: u16 = 24;
const WARMUP_STEP: f32 = 1.0 / 60.0;
const ARRIVAL_GLOW: f32 = 0.5;
#[allow(clippy::approx_constant)]
const SEED_RANGE: f32 = 6.28;
struct Particle {
x: f32,
y: f32,
vx: f32,
vy: f32,
age: f32,
life: f32,
size: f32,
seed: f32,
}
#[derive(Default)]
pub(super) struct State {
particles: Vec<Particle>,
acc: f32,
warmed: bool,
}
impl State {
pub fn reset(&mut self) {
self.particles.clear();
self.acc = 0.0;
self.warmed = false;
}
}
pub(super) fn render(
canvas: &mut Canvas<'_>,
push: &Push,
state: &mut State,
random: &mut Random,
dt: f32,
) {
if push.proximity > 0.01 {
canvas.spot(
push,
0.65,
&[(0.0, 0.15, 0.55), (0.5, 0.6, 0.2), (1.0, 0.0, 0.0)],
);
}
if !push.animate {
return;
}
advance(push, state, random, dt, Emission::Edge);
draw(canvas, push, state, dt);
}
pub(super) fn arrival(
canvas: &mut Canvas<'_>,
push: &Push,
state: &mut State,
random: &mut Random,
dt: f32,
) {
canvas.spot(
push,
0.65,
&[
(0.0, 0.15, 0.55 * ARRIVAL_GLOW),
(0.5, 0.6, 0.2 * ARRIVAL_GLOW),
(1.0, 0.0, 0.0),
],
);
if !push.animate {
return;
}
let emission = Emission::Arrival {
radius: (push.look.landing.radius() * push.look.size.multiplier()) as f32,
};
if !state.warmed {
*random = Random::default();
for _ in 0..WARMUP_STEPS {
advance(push, state, random, WARMUP_STEP, emission);
}
state.warmed = true;
}
advance(push, state, random, dt, emission);
draw(canvas, push, state, dt);
}
#[derive(Clone, Copy)]
enum Emission {
Edge,
Arrival { radius: f32 },
}
impl Emission {
fn spawn(self, push: &Push, random: &mut Random) -> (f32, f32, f32, f32) {
match self {
Self::Edge => {
let spread = (random.unit() - 0.5) * push.look.radius * 0.85;
let inward = (45.0 + random.unit() * 110.0) * push.look.depth / 55.0;
let tangent = (random.unit() - 0.5) * 80.0;
let x = (push.pointer + spread).clamp(
2.0 - push.start,
(push.length - push.start - 2.0).max(2.0 - push.start),
);
(x, 2.0, tangent, inward)
}
Self::Arrival { radius } => {
let angle = random.unit() * TAU;
let distance = random.unit().sqrt() * radius;
let speed = radius * (0.2 + random.unit() * 0.6);
let (sin, cos) = angle.sin_cos();
(cos * distance, sin * distance, cos * speed, sin * speed)
}
}
}
}
fn advance(push: &Push, state: &mut State, random: &mut Random, dt: f32, emission: Emission) {
if state.particles.capacity() < MAX_PARTICLES {
state
.particles
.reserve(MAX_PARTICLES - state.particles.len());
}
if push.proximity > 0.01 {
state.acc += 180.0 * push.look.frequency * push.proximity.powi(2) * dt;
while state.acc >= 1.0 {
state.acc -= 1.0;
if state.particles.len() >= MAX_PARTICLES {
continue;
}
let (x, y, vx, vy) = emission.spawn(push, random);
state.particles.push(Particle {
x,
y,
vx,
vy,
age: 0.0,
life: 0.45 + random.unit() * 0.75,
size: 1.3 + random.unit() * 2.3,
seed: random.unit() * SEED_RANGE,
});
}
}
state.particles.retain_mut(|s| {
s.age += dt;
if s.age >= s.life {
return false;
}
let swirl = (push.phase * 9.0 + s.seed + s.age * 6.0).sin() * 55.0 * dt * 0.4;
let (ax, ay) = match emission {
Emission::Arrival { .. } => (0.0, 0.0),
Emission::Edge => match push.edge {
super::Edge::Top => (swirl, -16.0 * dt),
super::Edge::Bottom => (swirl, 16.0 * dt),
super::Edge::Right => (-16.0 * dt, -swirl),
_ => (-16.0 * dt, swirl),
},
};
s.vx = s.vx * 0.96 + ax;
s.vy = s.vy * 0.96 + ay;
s.x += s.vx * dt;
s.y += s.vy * dt;
true
});
}
fn draw(canvas: &mut Canvas<'_>, push: &Push, state: &State, dt: f32) {
for s in &state.particles {
let life = s.age / s.life;
let flicker = 0.75 + 0.25 * (push.phase * 35.0 + s.seed).sin();
let alpha = ((1.0 - life).powf(1.3) * flicker * push.look.intensity).min(1.0);
let mut path = PathBuilder::new();
path.move_to(s.x - s.vx * dt, s.y - s.vy * dt);
path.line_to(s.x, s.y);
canvas.solid(life, alpha * 0.85);
if let Some(path) = path.finish() {
canvas.stroke(&path, s.size * (1.0 - life * 0.5));
}
canvas.solid(life, alpha);
canvas.circle(s.x, s.y, s.size * 0.65 * (1.0 - life * 0.4));
}
}