Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/games/desk_toy.rs
6592 views
1
//! Bevy logo as a desk toy using transparent windows! Now with Googly Eyes!
2
//!
3
//! This example demonstrates:
4
//! - Transparent windows that can be clicked through.
5
//! - Drag-and-drop operations in 2D.
6
//! - Using entity hierarchy, Transform, and Visibility to create simple animations.
7
//! - Creating simple 2D meshes based on shape primitives.
8
9
use bevy::{
10
app::AppExit,
11
input::common_conditions::{input_just_pressed, input_just_released},
12
prelude::*,
13
window::{CursorOptions, PrimaryWindow, WindowLevel},
14
};
15
16
#[cfg(target_os = "macos")]
17
use bevy::window::CompositeAlphaMode;
18
19
fn main() {
20
App::new()
21
.add_plugins(DefaultPlugins.set(WindowPlugin {
22
primary_window: Some(Window {
23
title: "Bevy Desk Toy".into(),
24
transparent: true,
25
#[cfg(target_os = "macos")]
26
composite_alpha_mode: CompositeAlphaMode::PostMultiplied,
27
..default()
28
}),
29
..default()
30
}))
31
.insert_resource(ClearColor(WINDOW_CLEAR_COLOR))
32
.insert_resource(WindowTransparency(false))
33
.insert_resource(CursorWorldPos(None))
34
.add_systems(Startup, setup)
35
.add_systems(
36
Update,
37
(
38
get_cursor_world_pos,
39
update_cursor_hit_test,
40
(
41
start_drag.run_if(input_just_pressed(MouseButton::Left)),
42
end_drag.run_if(input_just_released(MouseButton::Left)),
43
drag.run_if(resource_exists::<DragOperation>),
44
quit.run_if(input_just_pressed(MouseButton::Right)),
45
toggle_transparency.run_if(input_just_pressed(KeyCode::Space)),
46
move_pupils.after(drag),
47
),
48
)
49
.chain(),
50
)
51
.run();
52
}
53
54
/// Whether the window is transparent
55
#[derive(Resource)]
56
struct WindowTransparency(bool);
57
58
/// The projected 2D world coordinates of the cursor (if it's within primary window bounds).
59
#[derive(Resource)]
60
struct CursorWorldPos(Option<Vec2>);
61
62
/// The current drag operation including the offset with which we grabbed the Bevy logo.
63
#[derive(Resource)]
64
struct DragOperation(Vec2);
65
66
/// Marker component for the instructions text entity.
67
#[derive(Component)]
68
struct InstructionsText;
69
70
/// Marker component for the Bevy logo entity.
71
#[derive(Component)]
72
struct BevyLogo;
73
74
/// Component for the moving pupil entity (the moving part of the googly eye).
75
#[derive(Component)]
76
struct Pupil {
77
/// Radius of the eye containing the pupil.
78
eye_radius: f32,
79
/// Radius of the pupil.
80
pupil_radius: f32,
81
/// Current velocity of the pupil.
82
velocity: Vec2,
83
}
84
85
// Dimensions are based on: assets/branding/icon.png
86
// Bevy logo radius
87
const BEVY_LOGO_RADIUS: f32 = 128.0;
88
// Birds' eyes x y (offset from the origin) and radius
89
// These values are manually determined from the logo image
90
const BIRDS_EYES: [(f32, f32, f32); 3] = [
91
(145.0 - 128.0, -(56.0 - 128.0), 12.0),
92
(198.0 - 128.0, -(87.0 - 128.0), 10.0),
93
(222.0 - 128.0, -(140.0 - 128.0), 8.0),
94
];
95
96
const WINDOW_CLEAR_COLOR: Color = Color::srgb(0.2, 0.2, 0.2);
97
98
/// Spawn the scene
99
fn setup(
100
mut commands: Commands,
101
asset_server: Res<AssetServer>,
102
mut meshes: ResMut<Assets<Mesh>>,
103
mut materials: ResMut<Assets<ColorMaterial>>,
104
) {
105
// Spawn a 2D camera
106
commands.spawn(Camera2d);
107
108
// Spawn the text instructions
109
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
110
let text_style = TextFont {
111
font: font.clone(),
112
font_size: 25.0,
113
..default()
114
};
115
commands.spawn((
116
Text2d::new("Press Space to play on your desktop! Press it again to return.\nRight click Bevy logo to exit."),
117
text_style.clone(),
118
Transform::from_xyz(0.0, -300.0, 100.0),
119
InstructionsText,
120
));
121
122
// Create a circle mesh. We will reuse this mesh for all our circles.
123
let circle = meshes.add(Circle { radius: 1.0 });
124
// Create the different materials we will use for each part of the eyes. For this demo they are basic [`ColorMaterial`]s.
125
let outline_material = materials.add(Color::BLACK);
126
let sclera_material = materials.add(Color::WHITE);
127
let pupil_material = materials.add(Color::srgb(0.2, 0.2, 0.2));
128
let pupil_highlight_material = materials.add(Color::srgba(1.0, 1.0, 1.0, 0.2));
129
130
// Spawn the Bevy logo sprite
131
commands
132
.spawn((
133
Sprite::from_image(asset_server.load("branding/icon.png")),
134
BevyLogo,
135
))
136
.with_children(|commands| {
137
// For each bird eye
138
for (x, y, radius) in BIRDS_EYES {
139
let pupil_radius = radius * 0.6;
140
let pupil_highlight_radius = radius * 0.3;
141
let pupil_highlight_offset = radius * 0.3;
142
// eye outline
143
commands.spawn((
144
Mesh2d(circle.clone()),
145
MeshMaterial2d(outline_material.clone()),
146
Transform::from_xyz(x, y - 1.0, 1.0)
147
.with_scale(Vec2::splat(radius + 2.0).extend(1.0)),
148
));
149
150
// sclera
151
commands.spawn((
152
Transform::from_xyz(x, y, 2.0),
153
Visibility::default(),
154
children![
155
// sclera
156
(
157
Mesh2d(circle.clone()),
158
MeshMaterial2d(sclera_material.clone()),
159
Transform::from_scale(Vec3::new(radius, radius, 0.0)),
160
),
161
// pupil
162
(
163
Transform::from_xyz(0.0, 0.0, 1.0),
164
Visibility::default(),
165
Pupil {
166
eye_radius: radius,
167
pupil_radius,
168
velocity: Vec2::ZERO,
169
},
170
children![
171
// pupil main
172
(
173
Mesh2d(circle.clone()),
174
MeshMaterial2d(pupil_material.clone()),
175
Transform::from_xyz(0.0, 0.0, 0.0).with_scale(Vec3::new(
176
pupil_radius,
177
pupil_radius,
178
1.0,
179
)),
180
),
181
// pupil highlight
182
(
183
Mesh2d(circle.clone()),
184
MeshMaterial2d(pupil_highlight_material.clone()),
185
Transform::from_xyz(
186
-pupil_highlight_offset,
187
pupil_highlight_offset,
188
1.0,
189
)
190
.with_scale(Vec3::new(
191
pupil_highlight_radius,
192
pupil_highlight_radius,
193
1.0,
194
)),
195
)
196
],
197
)
198
],
199
));
200
}
201
});
202
}
203
204
/// Project the cursor into the world coordinates and store it in a resource for easy use
205
fn get_cursor_world_pos(
206
mut cursor_world_pos: ResMut<CursorWorldPos>,
207
primary_window: Single<&Window, With<PrimaryWindow>>,
208
q_camera: Single<(&Camera, &GlobalTransform)>,
209
) {
210
let (main_camera, main_camera_transform) = *q_camera;
211
// Get the cursor position in the world
212
cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| {
213
main_camera
214
.viewport_to_world_2d(main_camera_transform, cursor_pos)
215
.ok()
216
});
217
}
218
219
/// Update whether the window is clickable or not
220
fn update_cursor_hit_test(
221
cursor_world_pos: Res<CursorWorldPos>,
222
primary_window: Single<(&Window, &mut CursorOptions), With<PrimaryWindow>>,
223
bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
224
) {
225
let (window, mut cursor_options) = primary_window.into_inner();
226
// If the window has decorations (e.g. a border) then it should be clickable
227
if window.decorations {
228
cursor_options.hit_test = true;
229
return;
230
}
231
232
// If the cursor is not within the window we don't need to update whether the window is clickable or not
233
let Some(cursor_world_pos) = cursor_world_pos.0 else {
234
return;
235
};
236
237
// If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable
238
cursor_options.hit_test = bevy_logo_transform
239
.translation
240
.truncate()
241
.distance(cursor_world_pos)
242
< BEVY_LOGO_RADIUS;
243
}
244
245
/// Start the drag operation and record the offset we started dragging from
246
fn start_drag(
247
mut commands: Commands,
248
cursor_world_pos: Res<CursorWorldPos>,
249
bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
250
) {
251
// If the cursor is not within the primary window skip this system
252
let Some(cursor_world_pos) = cursor_world_pos.0 else {
253
return;
254
};
255
256
// Get the offset from the cursor to the Bevy logo sprite
257
let drag_offset = bevy_logo_transform.translation.truncate() - cursor_world_pos;
258
259
// If the cursor is within the Bevy logo radius start the drag operation and remember the offset of the cursor from the origin
260
if drag_offset.length() < BEVY_LOGO_RADIUS {
261
commands.insert_resource(DragOperation(drag_offset));
262
}
263
}
264
265
/// Stop the current drag operation
266
fn end_drag(mut commands: Commands) {
267
commands.remove_resource::<DragOperation>();
268
}
269
270
/// Drag the Bevy logo
271
fn drag(
272
drag_offset: Res<DragOperation>,
273
cursor_world_pos: Res<CursorWorldPos>,
274
time: Res<Time>,
275
mut bevy_transform: Single<&mut Transform, With<BevyLogo>>,
276
mut q_pupils: Query<&mut Pupil>,
277
) {
278
// If the cursor is not within the primary window skip this system
279
let Some(cursor_world_pos) = cursor_world_pos.0 else {
280
return;
281
};
282
283
// Calculate the new translation of the Bevy logo based on cursor and drag offset
284
let new_translation = cursor_world_pos + drag_offset.0;
285
286
// Calculate how fast we are dragging the Bevy logo (unit/second)
287
let drag_velocity =
288
(new_translation - bevy_transform.translation.truncate()) / time.delta_secs();
289
290
// Update the translation of Bevy logo transform to new translation
291
bevy_transform.translation = new_translation.extend(bevy_transform.translation.z);
292
293
// Add the cursor drag velocity in the opposite direction to each pupil.
294
// Remember pupils are using local coordinates to move. So when the Bevy logo moves right they need to move left to
295
// simulate inertia, otherwise they will move fixed to the parent.
296
for mut pupil in &mut q_pupils {
297
pupil.velocity -= drag_velocity;
298
}
299
}
300
301
/// Quit when the user right clicks the Bevy logo
302
fn quit(
303
cursor_world_pos: Res<CursorWorldPos>,
304
mut app_exit: EventWriter<AppExit>,
305
bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
306
) {
307
// If the cursor is not within the primary window skip this system
308
let Some(cursor_world_pos) = cursor_world_pos.0 else {
309
return;
310
};
311
312
// If the cursor is within the Bevy logo radius send the [`AppExit`] event to quit the app
313
if bevy_logo_transform
314
.translation
315
.truncate()
316
.distance(cursor_world_pos)
317
< BEVY_LOGO_RADIUS
318
{
319
app_exit.write(AppExit::Success);
320
}
321
}
322
323
/// Enable transparency for the window and make it on top
324
fn toggle_transparency(
325
mut commands: Commands,
326
mut window_transparency: ResMut<WindowTransparency>,
327
mut q_instructions_text: Query<&mut Visibility, With<InstructionsText>>,
328
mut primary_window: Single<&mut Window, With<PrimaryWindow>>,
329
) {
330
// Toggle the window transparency resource
331
window_transparency.0 = !window_transparency.0;
332
333
// Show or hide the instructions text
334
for mut visibility in &mut q_instructions_text {
335
*visibility = if window_transparency.0 {
336
Visibility::Hidden
337
} else {
338
Visibility::Visible
339
};
340
}
341
342
// Remove the primary window's decorations (e.g. borders), make it always on top of other desktop windows, and set the clear color to transparent
343
// only if window transparency is enabled
344
let clear_color;
345
(
346
primary_window.decorations,
347
primary_window.window_level,
348
clear_color,
349
) = if window_transparency.0 {
350
(false, WindowLevel::AlwaysOnTop, Color::NONE)
351
} else {
352
(true, WindowLevel::Normal, WINDOW_CLEAR_COLOR)
353
};
354
355
// Set the clear color
356
commands.insert_resource(ClearColor(clear_color));
357
}
358
359
/// Move the pupils and bounce them around
360
fn move_pupils(time: Res<Time>, mut q_pupils: Query<(&mut Pupil, &mut Transform)>) {
361
for (mut pupil, mut transform) in &mut q_pupils {
362
// The wiggle radius is how much the pupil can move within the eye
363
let wiggle_radius = pupil.eye_radius - pupil.pupil_radius;
364
// Store the Z component
365
let z = transform.translation.z;
366
// Truncate the Z component to make the calculations be on [`Vec2`]
367
let mut translation = transform.translation.truncate();
368
// Decay the pupil velocity
369
pupil.velocity *= ops::powf(0.04f32, time.delta_secs());
370
// Move the pupil
371
translation += pupil.velocity * time.delta_secs();
372
// If the pupil hit the outside border of the eye, limit the translation to be within the wiggle radius and invert the velocity.
373
// This is not physically accurate but it's good enough for the googly eyes effect.
374
if translation.length() > wiggle_radius {
375
translation = translation.normalize() * wiggle_radius;
376
// Invert and decrease the velocity of the pupil when it bounces
377
pupil.velocity *= -0.75;
378
}
379
// Update the entity transform with the new translation after reading the Z component
380
transform.translation = translation.extend(z);
381
}
382
}
383
384