use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::tokens::Duration;
pub struct RipplePlugin;
impl Plugin for RipplePlugin {
fn build(&self, app: &mut App) {
app.add_message::<SpawnRipple>()
.add_systems(Update, (spawn_ripple_system, animate_ripple_system));
}
}
#[derive(Component, Default)]
pub struct RippleHost {
pub color: Option<Color>,
pub unbounded: bool,
}
impl RippleHost {
pub fn new() -> Self {
Self::default()
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn unbounded(mut self) -> Self {
self.unbounded = true;
self
}
}
#[derive(Event, bevy::prelude::Message)]
pub struct SpawnRipple {
pub host: Entity,
pub position: Vec2,
}
#[derive(Component)]
pub struct Ripple {
pub scale: f32,
pub opacity: f32,
pub timer: Timer,
pub fading_out: bool,
pub max_radius: f32,
pub center: Vec2,
pub color: Color,
}
impl Ripple {
pub fn new(center: Vec2, max_radius: f32, color: Color) -> Self {
Self {
scale: 0.0,
opacity: 0.12,
timer: Timer::from_seconds(Duration::MEDIUM4, TimerMode::Once),
fading_out: false,
max_radius,
center,
color,
}
}
pub fn start_fade_out(&mut self) {
self.fading_out = true;
self.timer = Timer::from_seconds(Duration::SHORT4, TimerMode::Once);
}
pub fn is_complete(&self) -> bool {
self.fading_out && self.timer.is_finished()
}
}
fn spawn_ripple_system(
mut commands: Commands,
mut events: MessageReader<SpawnRipple>,
hosts: Query<(&RippleHost, &ComputedNode, &GlobalTransform)>,
windows: Query<&Window, With<PrimaryWindow>>,
) {
let scale = windows
.single()
.map(|window| window.scale_factor())
.unwrap_or(1.0);
for event in events.read() {
if let Ok((host, computed_node, _transform)) = hosts.get(event.host) {
let size = computed_node.size() / scale;
let max_radius = (size.x.powi(2) + size.y.powi(2)).sqrt();
let color = host.color.unwrap_or(Color::srgba(1.0, 1.0, 1.0, 0.12));
commands.entity(event.host).with_children(|parent| {
parent.spawn((
Ripple::new(event.position, max_radius, color),
Node {
position_type: PositionType::Absolute,
left: Val::Px(event.position.x),
top: Val::Px(event.position.y),
width: Val::Px(0.0),
height: Val::Px(0.0),
border_radius: BorderRadius::all(Val::Percent(50.0)),
..default()
},
BackgroundColor(color),
));
});
}
}
}
fn animate_ripple_system(
mut commands: Commands,
time: Res<Time>,
mut ripples: Query<(Entity, &mut Ripple, &mut Node, &mut BackgroundColor)>,
) {
for (entity, mut ripple, mut node, mut bg_color) in ripples.iter_mut() {
ripple.timer.tick(time.delta());
let progress = ripple.timer.fraction();
if ripple.fading_out {
ripple.opacity = 0.12 * (1.0 - ease_out(progress));
} else {
ripple.scale = ease_out(progress);
}
let current_radius = ripple.max_radius * ripple.scale;
let diameter = current_radius * 2.0;
node.width = Val::Px(diameter);
node.height = Val::Px(diameter);
node.left = Val::Px(ripple.center.x - current_radius);
node.top = Val::Px(ripple.center.y - current_radius);
*bg_color = BackgroundColor(ripple.color.with_alpha(ripple.opacity));
if !ripple.fading_out && ripple.timer.is_finished() {
ripple.start_fade_out();
}
if ripple.is_complete() {
commands.entity(entity).despawn();
}
}
}
fn ease_out(t: f32) -> f32 {
1.0 - (1.0 - t).powi(3)
}