use std::time::{Duration, Instant};
use vello::kurbo::{Affine, Circle};
use vello::peniko::{BlendMode, Color, Fill};
use vello::Scene;
use crate::{ComputedLayout, Mounted};
#[derive(Clone, Copy, Debug)]
pub struct Ripple {
pub key: u64,
pub color: Color,
pub duration: Duration,
}
struct Splash {
key: u64,
lx: f32,
ly: f32,
color: Color,
start: Instant,
duration: Duration,
easing: fn(f32) -> f32,
}
impl Splash {
fn raw(&self, now: Instant) -> f32 {
if self.duration.is_zero() {
return 1.0;
}
let elapsed = now.saturating_duration_since(self.start).as_secs_f32();
(elapsed / self.duration.as_secs_f32()).clamp(0.0, 1.0)
}
fn done(&self, now: Instant) -> bool {
now.saturating_duration_since(self.start) >= self.duration
}
}
#[derive(Default)]
pub struct RippleRegistry {
splashes: Vec<Splash>,
}
impl RippleRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn trigger(
&mut self,
key: u64,
lx: f32,
ly: f32,
color: Color,
duration: Duration,
now: Instant,
) {
self.splashes.push(Splash {
key,
lx,
ly,
color,
start: now,
duration,
easing: crate::ease_out_cubic,
});
}
pub fn animating(&self) -> bool {
!self.splashes.is_empty()
}
pub fn paint<Msg>(
&mut self,
scene: &mut Scene,
mounted: &Mounted<Msg>,
computed: &ComputedLayout,
now: Instant,
) -> bool {
self.splashes.retain(|s| !s.done(now));
if self.splashes.is_empty() {
return false;
}
for s in &self.splashes {
let Some(node) = mounted.nodes.iter().find(|n| {
n.ripple.map(|r| r.key) == Some(s.key)
}) else {
continue;
};
let Some(r) = computed.get(node.id) else {
continue;
};
if r.w <= 0.0 || r.h <= 0.0 {
continue;
}
let cx = r.x as f64 + s.lx as f64;
let cy = r.y as f64 + s.ly as f64;
let corners = [
(r.x as f64, r.y as f64),
((r.x + r.w) as f64, r.y as f64),
(r.x as f64, (r.y + r.h) as f64),
((r.x + r.w) as f64, (r.y + r.h) as f64),
];
let max_radius = corners
.iter()
.map(|(px, py)| ((px - cx).powi(2) + (py - cy).powi(2)).sqrt())
.fold(0.0_f64, f64::max);
let t = s.raw(now);
let radius = (s.easing)(t) as f64 * max_radius;
if radius <= 0.0 {
continue;
}
let fade = 1.0 - t;
let mut col = s.color;
col.components[3] *= fade;
if col.components[3] <= 0.0 {
continue;
}
let rrect = crate::render::node_rrect(
r.x as f64,
r.y as f64,
(r.x + r.w) as f64,
(r.y + r.h) as f64,
node.radius,
node.corner_radii,
0.0,
);
scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, Affine::IDENTITY, &rrect);
let circle = Circle::new((cx, cy), radius);
scene.fill(Fill::NonZero, Affine::IDENTITY, col, None, &circle);
scene.pop_layer();
}
!self.splashes.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{mount, View};
use llimphi_layout::taffy::prelude::*;
use llimphi_layout::{LayoutTree, Style};
fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
Color::from_rgba8(r, g, b, a)
}
fn boton() -> (Mounted<()>, ComputedLayout) {
let v = View::<()>::new(Style {
size: Size { width: length(100.0), height: length(100.0) },
..Default::default()
})
.ripple(5, rgba(255, 255, 255, 80));
let mut layout = LayoutTree::new();
let m = mount(&mut layout, v);
let c = layout.compute(m.root, (200.0, 200.0)).expect("layout");
(m, c)
}
#[test]
fn sin_trigger_no_anima() {
let mut reg = RippleRegistry::new();
let (m, c) = boton();
let mut scene = Scene::new();
assert!(!reg.paint(&mut scene, &m, &c, Instant::now()));
assert!(!reg.animating());
}
#[test]
fn trigger_anima_y_se_autodetiene() {
let mut reg = RippleRegistry::new();
let (m, c) = boton();
let t0 = Instant::now();
reg.trigger(5, 50.0, 50.0, rgba(255, 255, 255, 80), Duration::from_millis(200), t0);
assert!(reg.animating(), "tras el trigger hay onda viva");
let mut scene = Scene::new();
assert!(reg.paint(&mut scene, &m, &c, t0 + Duration::from_millis(100)));
assert!(!reg.paint(&mut scene, &m, &c, t0 + Duration::from_millis(250)));
assert!(!reg.animating());
}
#[test]
fn presses_concurrentes_apilan_ondas() {
let mut reg = RippleRegistry::new();
let t0 = Instant::now();
reg.trigger(5, 10.0, 10.0, rgba(255, 255, 255, 80), Duration::from_millis(200), t0);
reg.trigger(5, 90.0, 90.0, rgba(255, 255, 255, 80), Duration::from_millis(200), t0 + Duration::from_millis(20));
assert_eq!(reg.splashes.len(), 2);
let (m, c) = boton();
let mut scene = Scene::new();
assert!(reg.paint(&mut scene, &m, &c, t0 + Duration::from_millis(100)));
assert_eq!(reg.splashes.len(), 2);
assert!(reg.paint(&mut scene, &m, &c, t0 + Duration::from_millis(210)));
assert_eq!(reg.splashes.len(), 1);
}
#[test]
fn key_inexistente_se_descarta_al_agotarse_sin_panico() {
let mut reg = RippleRegistry::new();
let t0 = Instant::now();
reg.trigger(999, 0.0, 0.0, rgba(255, 255, 255, 80), Duration::from_millis(100), t0);
let (m, c) = boton();
let mut scene = Scene::new();
assert!(reg.paint(&mut scene, &m, &c, t0 + Duration::from_millis(50)));
assert!(!reg.paint(&mut scene, &m, &c, t0 + Duration::from_millis(150)));
}
}