//! Bevy logo as a desk toy using transparent windows! Now with Googly Eyes! //! //! This example demonstrates: //! - Transparent windows that can be clicked through. //! - Drag-and-drop operations in 2D. //! - Using entity hierarchy and [`SpatialBundle`]s to create simple animations. //! - Creating simple 2D meshes based on shape primitives. use bevy::{ app::AppExit, input::common_conditions::{input_just_pressed, input_just_released}, prelude::*, window::{PrimaryWindow, WindowLevel}, }; #[cfg(target_os = "macos")] use bevy::window::CompositeAlphaMode; fn main() { App::new() .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Bevy Desk Toy".into(), transparent: true, #[cfg(target_os = "macos")] composite_alpha_mode: CompositeAlphaMode::PostMultiplied, ..default() }), ..default() })) .insert_resource(ClearColor(WINDOW_CLEAR_COLOR)) .insert_resource(WindowTransparency(false)) .insert_resource(CursorWorldPos(None)) .add_systems(Startup, setup) .add_systems( Update, ( get_cursor_world_pos, update_cursor_hit_test, ( start_drag.run_if(input_just_pressed(MouseButton::Left)), end_drag.run_if(input_just_released(MouseButton::Left)), drag.run_if(resource_exists::), quit.run_if(input_just_pressed(MouseButton::Right)), toggle_transparency.run_if(input_just_pressed(KeyCode::Space)), move_pupils.after(drag), ), ) .chain(), ) .run(); } /// Whether the window is transparent #[derive(Resource)] struct WindowTransparency(bool); /// The projected 2D world coordinates of the cursor (if it's within primary window bounds). #[derive(Resource)] struct CursorWorldPos(Option); /// The current drag operation including the offset with which we grabbed the Bevy logo. #[derive(Resource)] struct DragOperation(Vec2); /// Marker component for the instructions text entity. #[derive(Component)] struct InstructionsText; /// Marker component for the Bevy logo entity. #[derive(Component)] struct BevyLogo; /// Component for the moving pupil entity (the moving part of the googly eye). #[derive(Component)] struct Pupil { /// Radius of the eye containing the pupil. eye_radius: f32, /// Radius of the pupil. pupil_radius: f32, /// Current velocity of the pupil. velocity: Vec2, } // Dimensions are based on: assets/branding/icon.png // Bevy logo radius const BEVY_LOGO_RADIUS: f32 = 128.0; // Birds' eyes x y (offset from the origin) and radius // These values are manually determined from the logo image const BIRDS_EYES: [(f32, f32, f32); 3] = [ (145.0 - 128.0, -(56.0 - 128.0), 12.0), (198.0 - 128.0, -(87.0 - 128.0), 10.0), (222.0 - 128.0, -(140.0 - 128.0), 8.0), ]; const WINDOW_CLEAR_COLOR: Color = Color::srgb(0.2, 0.2, 0.2); /// Spawn the scene fn setup( mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, ) { // Spawn a 2D camera commands.spawn(Camera2d); // Spawn the text instructions let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let text_style = TextFont { font: font.clone(), font_size: 25.0, ..default() }; commands.spawn(( Text2d::new("Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit."), text_style.clone(), Transform::from_xyz(0.0, -300.0, 100.0), InstructionsText, )); // Create a circle mesh. We will reuse this mesh for all our circles. let circle = meshes.add(Circle { radius: 1.0 }); // Create the different materials we will use for each part of the eyes. For this demo they are basic [`ColorMaterial`]s. let outline_material = materials.add(Color::BLACK); let sclera_material = materials.add(Color::WHITE); let pupil_material = materials.add(Color::srgb(0.2, 0.2, 0.2)); let pupil_highlight_material = materials.add(Color::srgba(1.0, 1.0, 1.0, 0.2)); // Spawn the Bevy logo sprite commands .spawn(( Sprite::from_image(asset_server.load("branding/icon.png")), BevyLogo, )) .with_children(|commands| { // For each bird eye for (x, y, radius) in BIRDS_EYES { // eye outline commands.spawn(( Mesh2d(circle.clone()), MeshMaterial2d(outline_material.clone()), Transform::from_xyz(x, y - 1.0, 1.0) .with_scale(Vec2::splat(radius + 2.0).extend(1.0)), )); // sclera commands .spawn((Transform::from_xyz(x, y, 2.0), Visibility::default())) .with_children(|commands| { // sclera commands.spawn(( Mesh2d(circle.clone()), MeshMaterial2d(sclera_material.clone()), Transform::from_scale(Vec3::new(radius, radius, 0.0)), )); let pupil_radius = radius * 0.6; let pupil_highlight_radius = radius * 0.3; let pupil_highlight_offset = radius * 0.3; // pupil commands .spawn(( Transform::from_xyz(0.0, 0.0, 1.0), Visibility::default(), Pupil { eye_radius: radius, pupil_radius, velocity: Vec2::ZERO, }, )) .with_children(|commands| { // pupil main commands.spawn(( Mesh2d(circle.clone()), MeshMaterial2d(pupil_material.clone()), Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::new( pupil_radius, pupil_radius, 1.0, )), )); // pupil highlight commands.spawn(( Mesh2d(circle.clone()), MeshMaterial2d(pupil_highlight_material.clone()), Transform::from_xyz( -pupil_highlight_offset, pupil_highlight_offset, 1.0, ) .with_scale(Vec3::new( pupil_highlight_radius, pupil_highlight_radius, 1.0, )), )); }); }); } }); } /// Project the cursor into the world coordinates and store it in a resource for easy use fn get_cursor_world_pos( mut cursor_world_pos: ResMut, primary_window: Single<&Window, With>, q_camera: Single<(&Camera, &GlobalTransform)>, ) { let (main_camera, main_camera_transform) = *q_camera; // Get the cursor position in the world cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| { main_camera .viewport_to_world_2d(main_camera_transform, cursor_pos) .ok() }); } /// Update whether the window is clickable or not fn update_cursor_hit_test( cursor_world_pos: Res, mut primary_window: Single<&mut Window, With>, bevy_logo_transform: Single<&Transform, With>, ) { // If the window has decorations (e.g. a border) then it should be clickable if primary_window.decorations { primary_window.cursor_options.hit_test = true; return; } // If the cursor is not within the window we don't need to update whether the window is clickable or not let Some(cursor_world_pos) = cursor_world_pos.0 else { return; }; // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable primary_window.cursor_options.hit_test = bevy_logo_transform .translation .truncate() .distance(cursor_world_pos) < BEVY_LOGO_RADIUS; } /// Start the drag operation and record the offset we started dragging from fn start_drag( mut commands: Commands, cursor_world_pos: Res, bevy_logo_transform: Single<&Transform, With>, ) { // If the cursor is not within the primary window skip this system let Some(cursor_world_pos) = cursor_world_pos.0 else { return; }; // Get the offset from the cursor to the Bevy logo sprite let drag_offset = bevy_logo_transform.translation.truncate() - cursor_world_pos; // If the cursor is within the Bevy logo radius start the drag operation and remember the offset of the cursor from the origin if drag_offset.length() < BEVY_LOGO_RADIUS { commands.insert_resource(DragOperation(drag_offset)); } } /// Stop the current drag operation fn end_drag(mut commands: Commands) { commands.remove_resource::(); } /// Drag the Bevy logo fn drag( drag_offset: Res, cursor_world_pos: Res, time: Res