bevy_simple_screenshot 0.1.2

A plug-and-play screenshot library for Bevy 0.17+ with ring-buffered capture and automatic saving
//! Example: Moving object with automatic screenshots
//!
//! Run with: `cargo run --example moving_object`
//!
//! This example creates a moving square and takes screenshots at specific positions.
//! Screenshots are saved to the `.screenshots/` directory with burn-in text overlay.
//!
//! **Font requirement**: Download a TTF font to the project root before running:
//! ```sh
//! curl -L -o FiraMono-Medium.ttf "https://github.com/mozilla/Fira/raw/master/ttf/FiraMono-Medium.ttf"
//! ```
//!
//! ## CLI Options
//!
//! ```sh
//! # Default (full window screenshot, upper-right burn-in)
//! cargo run --example moving_object
//!
//! # Entity-only mode (crops to the moving object)
//! cargo run --example moving_object -- --entity-only
//!
//! # Entity-only with custom padding
//! cargo run --example moving_object -- --entity-only --padding 50
//!
//! # Change burn-in position
//! cargo run --example moving_object -- --position lower-left
//!
//! # Change font size
//! cargo run --example moving_object -- --font-size 24
//!
//! # Show key and description
//! cargo run --example moving_object -- --show-key --show-description
//!
//! # Combine options
//! cargo run --example moving_object -- --entity-only --padding 30 --position upper-left --show-key
//! ```

use bevy::prelude::*;
use bevy_simple_screenshot::prelude::*;
use clap::{Parser, ValueEnum};

#[derive(Parser, Debug)]
#[command(name = "moving_object")]
#[command(about = "Screenshot example with burn-in text overlay")]
struct Args {
    /// Position of burn-in text
    #[arg(long, short, default_value = "upper-right")]
    position: PositionArg,

    /// Font size in pixels
    #[arg(long, short = 's', default_value = "18")]
    font_size: f32,

    /// Show the screenshot key in burn-in
    #[arg(long, short = 'k')]
    show_key: bool,

    /// Show the description in burn-in
    #[arg(long, short = 'd')]
    show_description: bool,

    /// Enable entity-only mode (crop screenshot to the moving object)
    #[arg(long, short = 'e')]
    entity_only: bool,

    /// Padding around entity in pixels (only used with --entity-only)
    #[arg(long, default_value = "20")]
    padding: u32,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
enum PositionArg {
    UpperLeft,
    UpperRight,
    LowerLeft,
    LowerRight,
}

impl From<PositionArg> for BurnInPosition {
    fn from(p: PositionArg) -> Self {
        match p {
            PositionArg::UpperLeft => BurnInPosition::UpperLeft,
            PositionArg::UpperRight => BurnInPosition::UpperRight,
            PositionArg::LowerLeft => BurnInPosition::LowerLeft,
            PositionArg::LowerRight => BurnInPosition::LowerRight,
        }
    }
}

/// Resource to store screenshot mode settings.
#[derive(Resource)]
struct ScreenshotMode {
    entity_only: bool,
    padding: u32,
}

fn main() {
    let args = Args::parse();

    println!("Screenshot config:");
    println!("  Mode: {}", if args.entity_only { "entity-only" } else { "full window" });
    if args.entity_only {
        println!("  Padding: {}px", args.padding);
    }
    println!("Burn-in config:");
    println!("  Position: {:?}", args.position);
    println!("  Font size: {}", args.font_size);
    println!("  Show key: {}", args.show_key);
    println!("  Show description: {}", args.show_description);

    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Screenshot Example - Moving Object".into(),
                resolution: (800u32, 600u32).into(),
                ..default()
            }),
            ..default()
        }))
        .add_plugins(ScreenshotBufferPlugin::with_config(
            ScreenshotConfig::default()
                .with_output_dir(".screenshots")
                .with_buffer_capacity(5)
                .with_burn_in(
                    BurnInConfig::enabled()
                        .with_font("FiraMono-Medium.ttf")
                        .with_position(args.position.into())
                        .with_font_size(args.font_size)
                        .with_show_key(args.show_key)
                        .with_show_description(args.show_description),
                ),
        ))
        .insert_resource(ScreenshotMode {
            entity_only: args.entity_only,
            padding: args.padding,
        })
        .add_systems(Startup, setup)
        .add_systems(Update, (move_object, animate_color, screenshot_on_position))
        .run();
}

#[derive(Component)]
struct MovingObject {
    speed: f32,
    screenshot_positions: Vec<f32>,
    screenshots_taken: Vec<f32>,
}

/// Component for color animation (lerp between RED and GREEN).
#[derive(Component)]
struct ColorAnimation {
    /// Animation speed (cycles per second)
    speed: f32,
    /// Current phase (0.0 to 1.0)
    phase: f32,
}

fn setup(mut commands: Commands) {
    // Camera
    commands.spawn(Camera2d::default());

    // Moving sprite with color animation (RED <-> GREEN)
    commands.spawn((
        Sprite {
            color: Color::srgb(1.0, 0.0, 0.0), // Start with RED
            custom_size: Some(Vec2::new(50.0, 50.0)),
            ..default()
        },
        Transform::from_translation(Vec3::new(-300.0, 0.0, 0.0)),
        MovingObject {
            speed: 150.0,
            screenshot_positions: vec![-200.0, -100.0, 0.0, 100.0, 200.0],
            screenshots_taken: vec![],
        },
        ColorAnimation {
            speed: 0.381,
            phase: 0.0,
        },
    ));
}

fn move_object(time: Res<Time>, mut query: Query<(&mut Transform, &mut MovingObject)>) {
    for (mut transform, mut obj) in &mut query {
        transform.translation.x += obj.speed * time.delta_secs();

        // Reset when going off screen
        if transform.translation.x > 350.0 {
            transform.translation.x = -350.0;
            obj.screenshots_taken.clear();
        }
    }
}

/// Animate sprite color: smoothly transition between RED and GREEN.
fn animate_color(time: Res<Time>, mut query: Query<(&mut Sprite, &mut ColorAnimation)>) {
    for (mut sprite, mut anim) in &mut query {
        // Update phase (ping-pong between 0 and 1)
        anim.phase += time.delta_secs() * anim.speed;

        // Use sine wave for smooth ping-pong effect
        let t = (anim.phase * std::f32::consts::PI).sin().abs();

        // Lerp between RED (1,0,0) and GREEN (0,1,0)
        let r = 1.0 - t;
        let g = t;
        let b = 0.0;

        sprite.color = Color::srgb(r, g, b);
    }
}

fn screenshot_on_position(
    trigger: ScreenshotTrigger,
    entity_trigger: EntityScreenshotTrigger,
    mode: Res<ScreenshotMode>,
    mut query: Query<(Entity, &Transform, &mut MovingObject)>,
) {
    for (entity, transform, mut obj) in &mut query {
        let x = transform.translation.x;

        for &pos in &obj.screenshot_positions.clone() {
            // Check if we crossed this position and haven't taken screenshot yet
            if x >= pos && !obj.screenshots_taken.contains(&pos) {
                let desc = format!("position_{}", pos as i32);

                if mode.entity_only {
                    // Entity-focused screenshot (cropped to entity bounds + padding)
                    let settings = EntityScreenshotSettings::default().with_padding(mode.padding);
                    screenshot_entity!(&entity_trigger, entity, "movement", &desc, settings);
                    info!("Entity screenshot taken at x={} (padding={}px)", pos, mode.padding);
                } else {
                    // Full window screenshot
                    screenshot!(&trigger, "movement", &desc);
                    info!("Screenshot taken at x={}", pos);
                }

                obj.screenshots_taken.push(pos);
            }
        }
    }
}