use egui::{Color32, Rect, Rounding, Sense, Stroke, Ui};
const FRAME_INTERVAL_MS: u64 = 33;
#[inline]
pub fn schedule_frame(ctx: &egui::Context) {
ctx.request_repaint_after(std::time::Duration::from_millis(FRAME_INTERVAL_MS));
}
pub fn render_rising_particles(ui: &mut Ui, width: f32, height: f32) {
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, height), Sense::hover());
let painter = ui.painter();
let time = ui.ctx().input(|i| i.time) as f32;
const PARTICLES: [(f32, f32, f32, Color32, f32, f32); 8] = [
(0.35, 0.4, 4.5, Color32::from_rgb(255, 150, 170), 0.75, 0.0),
(
0.45,
0.35,
5.0,
Color32::from_rgb(200, 140, 220),
1.1,
0.125,
),
(0.55, 0.3, 4.0, Color32::from_rgb(180, 160, 255), 0.9, 0.25),
(
0.50,
0.45,
5.5,
Color32::from_rgb(240, 170, 210),
1.25,
0.375,
),
(0.40, 0.5, 4.8, Color32::from_rgb(170, 200, 230), 0.8, 0.5),
(
0.60,
0.4,
4.2,
Color32::from_rgb(220, 180, 200),
1.15,
0.625,
),
(
0.48,
0.35,
5.0,
Color32::from_rgb(190, 150, 240),
0.85,
0.75,
),
(0.52, 0.5, 4.5, Color32::from_rgb(255, 170, 190), 1.0, 0.875),
];
for (x_center, spread, dot_size, color, speed, phase) in PARTICLES.iter() {
let cycle = ((time * 0.1 * speed) + phase) % 1.0;
let y_progress = cycle;
let spread_direction = if *x_center < 0.5 { -1.0 } else { 1.0 };
let x_offset = spread_direction * spread * y_progress;
let x_pct = x_center + x_offset;
let x = rect.left() + width * x_pct;
let y = rect.bottom() - height * y_progress;
let alpha = if y_progress < 0.2 {
y_progress / 0.2
} else if y_progress > 0.7 {
1.0 - (y_progress - 0.7) / 0.3
} else {
1.0
};
let drift = (time * 0.5 + phase * 10.0).sin() * 6.0;
let final_x = x + drift;
painter.circle_filled(
egui::pos2(final_x, y),
*dot_size,
color.linear_multiply(alpha.max(0.0) * 0.7),
);
}
}
const NUM_POINTS: usize = 32;
#[inline]
fn lemniscate_point(t: f32) -> (f32, f32) {
let sin_t = t.sin();
let cos_t = t.cos();
let denom = 1.0 + sin_t * sin_t;
(cos_t / denom, sin_t * cos_t / denom)
}
pub fn render_infinity(painter: &egui::Painter, time: f32, rect: Rect, color: Color32, speed: f32) {
let center = rect.center();
let half_width = rect.width() / 2.0 - 2.0;
let half_height = rect.height() / 2.0 - 1.0;
let cycle = (time * speed * 0.3) % 1.0;
const TRAIL_LENGTH: f32 = 0.35;
let head_pos = cycle;
let tail_pos = (cycle - TRAIL_LENGTH).rem_euclid(1.0);
for i in 0..NUM_POINTS {
let seg_mid = (i as f32 + 0.5) / NUM_POINTS as f32;
let in_trail = if head_pos > tail_pos {
seg_mid >= tail_pos && seg_mid <= head_pos
} else {
seg_mid >= tail_pos || seg_mid <= head_pos
};
if !in_trail {
continue;
}
let dist_from_head = {
let direct = (head_pos - seg_mid).abs();
let wrapped = 1.0 - direct;
direct.min(wrapped)
};
let alpha = if dist_from_head < TRAIL_LENGTH {
let progress = 1.0 - (dist_from_head / TRAIL_LENGTH);
progress * progress
} else {
0.0
};
if alpha > 0.02 {
let t1 = (i as f32 / NUM_POINTS as f32) * std::f32::consts::TAU;
let t2 = ((i + 1) as f32 / NUM_POINTS as f32) * std::f32::consts::TAU;
let (x1, y1) = lemniscate_point(t1);
let (x2, y2) = lemniscate_point(t2);
let p1 = egui::pos2(center.x + half_width * x1, center.y + half_height * y1);
let p2 = egui::pos2(center.x + half_width * x2, center.y + half_height * y2);
painter.line_segment([p1, p2], Stroke::new(1.5, color.linear_multiply(alpha)));
}
}
let head_t = cycle * std::f32::consts::TAU;
let (hx, hy) = lemniscate_point(head_t);
painter.circle_filled(
egui::pos2(center.x + half_width * hx, center.y + half_height * hy),
2.0,
color,
);
}
pub fn render_progress_bar(
painter: &egui::Painter,
time: f32,
rect: Rect,
progress: f32,
bg_color: Color32,
fill_color: Color32,
) {
let progress = progress.clamp(0.0, 1.0);
let rounding = Rounding::same(rect.height() / 2.0);
painter.rect_filled(rect, rounding, bg_color);
if progress > 0.0 {
let fill_width = rect.width() * progress;
let fill_rect =
Rect::from_min_max(rect.min, egui::pos2(rect.min.x + fill_width, rect.max.y));
painter.rect_filled(fill_rect, rounding, fill_color);
let shimmer_cycle = (time * 0.5) % 2.0; if shimmer_cycle < 1.0 {
let shimmer_pos = shimmer_cycle;
let shimmer_x = fill_rect.min.x + (fill_width * shimmer_pos);
if shimmer_x >= fill_rect.min.x && shimmer_x <= fill_rect.max.x {
let shimmer_width = 12.0_f32.min(fill_width * 0.3);
let shimmer_rect = Rect::from_min_max(
egui::pos2(
(shimmer_x - shimmer_width / 2.0).max(fill_rect.min.x),
rect.min.y,
),
egui::pos2(
(shimmer_x + shimmer_width / 2.0).min(fill_rect.max.x),
rect.max.y,
),
);
let shimmer_color = Color32::from_rgba_unmultiplied(255, 255, 255, 76);
painter.rect_filled(shimmer_rect, rounding, shimmer_color);
}
}
}
}