facett-core 0.1.1

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **effects_demo** — the facett look, alive. A live gallery of:
//!
//! * every [`Theme`] in [`Theme::ALL`] (click to switch the whole window),
//! * the [`effects`] bloom helpers (glow rect/text, shimmer sweep), and
//! * the signature [`RavenSprite`]: press **Send the raven** and it flies in
//!   along a bezier arc, eases out, and **perches** on the selected table row.
//!
//! Pure egui on the **glow** backend (no wgpu, no custom shader) — see
//! `effects.rs` for where a real GPU bloom pass would later slot in.
//!
//! Run: `cargo run -p facett-core --example effects_demo`

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>,
    /// The row the raven is aimed at.
    target_row: usize,
    /// Live row rects (updated each frame so the raven can re-aim).
    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"];

/// A named easing curve (for the little gallery plots).
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);
            });
        });

        // Drive + paint the raven on the layer above everything.
        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);

            // Kick a little feather-burst the moment it perches.
            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 {
    /// Left column: the bloom helpers showing off `theme.glow`.
    fn gallery(&self, ui: &mut egui::Ui, theme: Theme, now: f64) {
        ui.heading("Effects");
        ui.add_space(6.0);

        // Pulsing glow rect (intensity tweened with ease_in_out_cubic).
        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);

        // Shimmer sweep.
        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)):");
        // Plot each easing curve in its own little box.
        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); // may overshoot [0,1] for back/elastic
                    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,
            );
        }
    }

    /// Right column: a little "table" whose rows are perch targets. Records each
    /// row's rect so the raven can land on the selected one.
    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 {
                // a soft glow on the chosen row, pulsing gently
                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())))
}