use eframe::egui;
use egui::{Align2, Color32, FontId, Rect, Sense, Stroke, Vec2, pos2, vec2};
use facett_core::effects::{self, ParticleBurst, RavenSprite, easing};
use facett_core::{Theme, set_theme};
struct Demo {
theme_idx: usize,
raven: Option<RavenSprite>,
target_row: usize,
row_rects: Vec<Rect>,
burst: Option<ParticleBurst>,
last_time: f64,
}
impl Default for Demo {
fn default() -> Self {
Self { theme_idx: 2, raven: None, target_row: 2, row_rects: Vec::new(), burst: None, last_time: 0.0 }
}
}
const ROWS: &[&str] = &["Hugin", "Munin", "Sleipner", "Yggdrasil", "Bifröst", "Ragnar"];
type NamedEasing = (&'static str, fn(f32) -> f32);
impl eframe::App for Demo {
fn ui(&mut self, root: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = root.ctx().clone();
let theme = Theme::ALL[self.theme_idx]();
set_theme(&ctx, theme);
let now = ctx.input(|i| i.time);
let dt = (now - self.last_time).max(0.0) as f32;
self.last_time = now;
egui::Panel::top("controls").show_inside(root, |ui| {
ui.horizontal_wrapped(|ui| {
ui.label("Theme:");
for (i, ctor) in Theme::ALL.iter().enumerate() {
let t = ctor();
ui.selectable_value(&mut self.theme_idx, i, t.name);
}
});
ui.horizontal(|ui| {
if ui.button("🐦 Send the raven").clicked()
&& let Some(&rect) = self.row_rects.get(self.target_row)
{
let start = pos2(rect.left() - 80.0, rect.top() - 120.0);
self.raven = Some(
RavenSprite::new().from(start).color(Color32::from_rgb(16, 16, 20)).scale(1.3).fly_to(rect),
);
}
ui.label("perch on row:");
for (i, name) in ROWS.iter().enumerate() {
ui.selectable_value(&mut self.target_row, i, *name);
}
});
});
egui::CentralPanel::default().show_inside(root, |ui| {
ui.columns(2, |cols| {
self.gallery(&mut cols[0], theme, now);
self.table(&mut cols[1], theme, now);
});
});
let painter = ctx.layer_painter(egui::LayerId::new(
egui::Order::Foreground,
egui::Id::new("raven_layer"),
));
if let Some(raven) = &mut self.raven {
raven.update(&ctx);
raven.paint(&painter);
if raven.is_perched() && self.burst.is_none() {
self.burst = Some(ParticleBurst::new(raven.pos(), 14, theme.accent, now.to_bits()));
}
}
if let Some(b) = &mut self.burst {
b.update(dt);
b.paint(&painter);
if b.finished() {
self.burst = None;
}
}
}
}
impl Demo {
fn gallery(&self, ui: &mut egui::Ui, theme: Theme, now: f64) {
ui.heading("Effects");
ui.add_space(6.0);
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), 56.0), Sense::hover());
let pulse = easing::ease_in_out_cubic(((now * 1.2).sin() as f32 * 0.5 + 0.5).clamp(0.0, 1.0));
let p = ui.painter();
p.rect_filled(rect.shrink(8.0), 6.0, theme.node_fill);
effects::glow_rect(p, rect.shrink(8.0), theme.glow, pulse, 6);
effects::glow_text(
p,
rect.center(),
Align2::CENTER_CENTER,
"glow_rect + glow_text",
FontId::proportional(18.0),
theme.text,
theme.glow,
pulse,
);
ui.add_space(10.0);
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), 40.0), Sense::hover());
let p = ui.painter();
p.rect_filled(rect, 4.0, theme.panel_bg);
p.rect_stroke(rect, 4.0, Stroke::new(1.0, theme.panel_stroke), egui::StrokeKind::Inside);
effects::shimmer(p, rect, theme.accent, (now as f32 * 0.4).fract());
p.text(rect.center(), Align2::CENTER_CENTER, "shimmer", FontId::proportional(14.0), theme.text);
ui.add_space(10.0);
ui.label("Easing curves (t → f(t)):");
let curves: [NamedEasing; 5] = [
("linear", easing::linear),
("in_out_cubic", easing::ease_in_out_cubic),
("out_back", easing::ease_out_back),
("elastic", easing::elastic),
("bounce", easing::bounce),
];
for (name, f) in curves {
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::hover());
let p = ui.painter();
p.rect_filled(rect, 3.0, theme.node_fill);
let plot = rect.shrink2(vec2(40.0, 6.0));
let n = 48;
let pts: Vec<_> = (0..=n)
.map(|i| {
let t = i as f32 / n as f32;
let y = f(t); pos2(plot.left() + t * plot.width(), plot.bottom() - y.clamp(-0.3, 1.3) * plot.height())
})
.collect();
p.add(egui::Shape::line(pts, Stroke::new(1.5, theme.accent)));
p.text(
rect.left_center() + vec2(2.0, 0.0),
Align2::LEFT_CENTER,
name,
FontId::monospace(10.0),
theme.text_dim,
);
}
}
fn table(&mut self, ui: &mut egui::Ui, theme: Theme, now: f64) {
ui.heading("Perch targets");
ui.add_space(6.0);
self.row_rects.clear();
for (i, name) in ROWS.iter().enumerate() {
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 30.0), Sense::click());
self.row_rects.push(rect);
let selected = i == self.target_row;
let p = ui.painter();
let fill = if selected { theme.accent.linear_multiply(0.18) } else { theme.node_fill };
p.rect_filled(rect, 4.0, fill);
if selected {
let pulse = (now as f32 * 2.0).sin() * 0.5 + 0.5;
effects::glow_rect(p, rect, theme.glow, pulse, 4);
}
p.text(
rect.left_center() + vec2(10.0, 0.0),
Align2::LEFT_CENTER,
*name,
FontId::proportional(14.0),
theme.text,
);
if resp.clicked() {
self.target_row = i;
}
}
ui.add_space(8.0);
ui.label("Pick a row, then “Send the raven”. It flies in on a bezier arc,");
ui.label("eases out, and perches — with a subtle idle bob.");
}
}
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size(Vec2::new(960.0, 620.0)),
..Default::default()
};
eframe::run_native("facett · effects_demo", options, Box::new(|_cc| Ok(Box::<Demo>::default())))
}