1mod helpers;
6
7use argh::FromArgs;
8use bevy::prelude::*;
9
10use helpers::Next;
11
12#[derive(FromArgs)]
13pub struct Args {
15 #[argh(positional)]
16 scene: Option<Scene>,
17}
18
19fn main() {
20 #[cfg(not(target_arch = "wasm32"))]
21 let args: Args = argh::from_env();
22 #[cfg(target_arch = "wasm32")]
23 let args: Args = Args::from_args(&[], &[]).unwrap();
24
25 let mut app = App::new();
26 app.add_plugins((DefaultPlugins,))
27 .add_systems(OnEnter(Scene::Shapes), shapes::setup)
28 .add_systems(OnEnter(Scene::Bloom), bloom::setup)
29 .add_systems(OnEnter(Scene::Text), text::setup)
30 .add_systems(OnEnter(Scene::Sprite), sprite::setup)
31 .add_systems(OnEnter(Scene::SpriteSlicing), sprite_slicing::setup)
32 .add_systems(OnEnter(Scene::Gizmos), gizmos::setup)
33 .add_systems(
34 OnEnter(Scene::TextureAtlasBuilder),
35 texture_atlas_builder::setup,
36 )
37 .add_systems(OnEnter(Scene::ColorConsistency), color_consistency::setup)
38 .add_systems(OnExit(Scene::ColorConsistency), color_consistency::teardown)
39 .add_systems(Update, switch_scene)
40 .add_systems(Update, gizmos::draw_gizmos.run_if(in_state(Scene::Gizmos)));
41
42 match args.scene {
43 None => app.init_state::<Scene>(),
44 Some(scene) => app.insert_state(scene),
45 };
46
47 #[cfg(feature = "bevy_ci_testing")]
48 app.add_systems(Update, helpers::switch_scene_in_ci::<Scene>);
49
50 app.run();
51}
52
53#[derive(Debug, Clone, Eq, PartialEq, Hash, States, Default)]
54enum Scene {
55 #[default]
56 Shapes,
57 Bloom,
58 Text,
59 Sprite,
60 SpriteSlicing,
61 Gizmos,
62 TextureAtlasBuilder,
63 ColorConsistency,
64}
65
66impl std::str::FromStr for Scene {
67 type Err = String;
68
69 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
70 let mut isit = Self::default();
71 while s.to_lowercase() != format!("{isit:?}").to_lowercase() {
72 isit = isit.next();
73 if isit == Self::default() {
74 return Err(format!("Invalid Scene name: {s}"));
75 }
76 }
77 Ok(isit)
78 }
79}
80
81impl Next for Scene {
82 fn next(&self) -> Self {
83 match self {
84 Scene::Shapes => Scene::Bloom,
85 Scene::Bloom => Scene::Text,
86 Scene::Text => Scene::Sprite,
87 Scene::Sprite => Scene::SpriteSlicing,
88 Scene::SpriteSlicing => Scene::Gizmos,
89 Scene::Gizmos => Scene::TextureAtlasBuilder,
90 Scene::TextureAtlasBuilder => Scene::ColorConsistency,
91 Scene::ColorConsistency => Scene::Shapes,
92 }
93 }
94}
95
96fn switch_scene(
97 keyboard: Res<ButtonInput<KeyCode>>,
98 scene: Res<State<Scene>>,
99 mut next_scene: ResMut<NextState<Scene>>,
100) {
101 if keyboard.just_pressed(KeyCode::Space) {
102 info!("Switching scene");
103 next_scene.set(scene.get().next());
104 }
105}
106
107mod shapes {
108 use bevy::prelude::*;
109
110 const X_EXTENT: f32 = 900.;
111
112 pub fn setup(
113 mut commands: Commands,
114 mut meshes: ResMut<Assets<Mesh>>,
115 mut materials: ResMut<Assets<ColorMaterial>>,
116 ) {
117 commands.spawn((Camera2d, DespawnOnExit(super::Scene::Shapes)));
118
119 let shapes = [
120 meshes.add(Circle::new(50.0)),
121 meshes.add(CircularSector::new(50.0, 1.0)),
122 meshes.add(CircularSegment::new(50.0, 1.25)),
123 meshes.add(Ellipse::new(25.0, 50.0)),
124 meshes.add(Annulus::new(25.0, 50.0)),
125 meshes.add(Capsule2d::new(25.0, 50.0)),
126 meshes.add(Rhombus::new(75.0, 100.0)),
127 meshes.add(Rectangle::new(50.0, 100.0)),
128 meshes.add(RegularPolygon::new(50.0, 6)),
129 meshes.add(Triangle2d::new(
130 Vec2::Y * 50.0,
131 Vec2::new(-50.0, -50.0),
132 Vec2::new(50.0, -50.0),
133 )),
134 ];
135 let num_shapes = shapes.len();
136
137 for (i, shape) in shapes.into_iter().enumerate() {
138 let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);
140
141 commands.spawn((
142 Mesh2d(shape),
143 MeshMaterial2d(materials.add(color)),
144 Transform::from_xyz(
145 -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
147 0.0,
148 0.0,
149 ),
150 DespawnOnExit(super::Scene::Shapes),
151 ));
152 }
153 }
154}
155
156mod bloom {
157 use bevy::{core_pipeline::tonemapping::Tonemapping, post_process::bloom::Bloom, prelude::*};
158
159 pub fn setup(
160 mut commands: Commands,
161 mut meshes: ResMut<Assets<Mesh>>,
162 mut materials: ResMut<Assets<ColorMaterial>>,
163 ) {
164 commands.spawn((
165 Camera2d,
166 Tonemapping::TonyMcMapface,
167 Bloom::default(),
168 DespawnOnExit(super::Scene::Bloom),
169 ));
170
171 commands.spawn((
172 Mesh2d(meshes.add(Circle::new(100.))),
173 MeshMaterial2d(materials.add(Color::srgb(7.5, 0.0, 7.5))),
174 Transform::from_translation(Vec3::new(-200., 0., 0.)),
175 DespawnOnExit(super::Scene::Bloom),
176 ));
177
178 commands.spawn((
179 Mesh2d(meshes.add(RegularPolygon::new(100., 6))),
180 MeshMaterial2d(materials.add(Color::srgb(6.25, 9.4, 9.1))),
181 Transform::from_translation(Vec3::new(200., 0., 0.)),
182 DespawnOnExit(super::Scene::Bloom),
183 ));
184 }
185}
186
187mod text {
188 use bevy::color::palettes;
189 use bevy::prelude::*;
190 use bevy::sprite::Anchor;
191 use bevy::text::TextBounds;
192
193 pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
194 commands.spawn((Camera2d, DespawnOnExit(super::Scene::Text)));
195
196 for (i, justify) in [
197 Justify::Left,
198 Justify::Right,
199 Justify::Center,
200 Justify::Justified,
201 ]
202 .into_iter()
203 .enumerate()
204 {
205 let y = 230. - 150. * i as f32;
206 spawn_anchored_text(&mut commands, -300. * Vec3::X + y * Vec3::Y, justify, None);
207 spawn_anchored_text(
208 &mut commands,
209 300. * Vec3::X + y * Vec3::Y,
210 justify,
211 Some(TextBounds::new(150., 60.)),
212 );
213 }
214
215 let sans_serif = TextFont::from(asset_server.load("fonts/FiraSans-Bold.ttf"));
216
217 const NUM_ITERATIONS: usize = 10;
218 for i in 0..NUM_ITERATIONS {
219 let fraction = i as f32 / (NUM_ITERATIONS - 1) as f32;
220
221 commands.spawn((
222 Text2d::new("Bevy"),
223 sans_serif.clone(),
224 Transform::from_xyz(0.0, fraction * 200.0, i as f32)
225 .with_scale(1.0 + Vec2::splat(fraction).extend(1.))
226 .with_rotation(Quat::from_rotation_z(fraction * core::f32::consts::PI)),
227 TextColor(Color::hsla(fraction * 360.0, 0.8, 0.8, 0.8)),
228 DespawnOnExit(super::Scene::Text),
229 ));
230 }
231
232 commands.spawn((
233 Text2d::new("This text is invisible."),
234 Visibility::Hidden,
235 DespawnOnExit(super::Scene::Text),
236 ));
237 }
238
239 fn spawn_anchored_text(
240 commands: &mut Commands,
241 dest: Vec3,
242 justify: Justify,
243 bounds: Option<TextBounds>,
244 ) {
245 commands.spawn((
246 Sprite {
247 color: palettes::css::YELLOW.into(),
248 custom_size: Some(5. * Vec2::ONE),
249 ..Default::default()
250 },
251 Transform::from_translation(dest),
252 DespawnOnExit(super::Scene::Text),
253 ));
254
255 for anchor in [
256 Anchor::TOP_LEFT,
257 Anchor::TOP_RIGHT,
258 Anchor::BOTTOM_RIGHT,
259 Anchor::BOTTOM_LEFT,
260 ] {
261 let mut text = commands.spawn((
262 Text2d::new("L R\n"),
263 TextLayout::justify(justify),
264 Transform::from_translation(dest + Vec3::Z),
265 anchor,
266 DespawnOnExit(super::Scene::Text),
267 ShowAabbGizmo {
268 color: Some(palettes::tailwind::AMBER_400.into()),
269 },
270 children![
271 (
272 TextSpan::new(format!("{}, {}\n", anchor.x, anchor.y)),
273 TextFont::from_font_size(14.0),
274 TextColor(palettes::tailwind::BLUE_400.into()),
275 ),
276 (
277 TextSpan::new(format!("{justify:?}")),
278 TextFont::from_font_size(14.0),
279 TextColor(palettes::tailwind::GREEN_400.into()),
280 ),
281 ],
282 ));
283 if let Some(bounds) = bounds {
284 text.insert(bounds);
285
286 commands.spawn((
287 Sprite {
288 color: palettes::tailwind::GRAY_900.into(),
289 custom_size: Some(Vec2::new(bounds.width.unwrap(), bounds.height.unwrap())),
290 ..Default::default()
291 },
292 Transform::from_translation(dest - Vec3::Z),
293 anchor,
294 DespawnOnExit(super::Scene::Text),
295 ));
296 }
297 }
298 }
299}
300
301mod sprite {
302 use bevy::color::palettes::css::{BLUE, LIME, RED};
303 use bevy::prelude::*;
304 use bevy::sprite::Anchor;
305
306 pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
307 commands.spawn((Camera2d, DespawnOnExit(super::Scene::Sprite)));
308 for (anchor, flip_x, flip_y, color) in [
309 (Anchor::BOTTOM_LEFT, false, false, Color::WHITE),
310 (Anchor::BOTTOM_RIGHT, true, false, RED.into()),
311 (Anchor::TOP_LEFT, false, true, LIME.into()),
312 (Anchor::TOP_RIGHT, true, true, BLUE.into()),
313 ] {
314 commands.spawn((
315 Sprite {
316 image: asset_server.load("branding/bevy_logo_dark.png"),
317 flip_x,
318 flip_y,
319 color,
320 ..default()
321 },
322 anchor,
323 DespawnOnExit(super::Scene::Sprite),
324 ));
325 }
326 }
327}
328
329mod sprite_slicing {
330 use bevy::prelude::*;
331 use bevy::sprite::{BorderRect, SliceScaleMode, SpriteImageMode, TextureSlicer};
332
333 pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
334 commands.spawn((Camera2d, DespawnOnExit(super::Scene::SpriteSlicing)));
335
336 let texture = asset_server.load("textures/slice_square_2.png");
337 let font = asset_server.load("fonts/FiraSans-Bold.ttf");
338
339 commands.spawn((
340 Sprite {
341 image: texture.clone(),
342 ..default()
343 },
344 Transform::from_translation(Vec3::new(-150.0, 50.0, 0.0)).with_scale(Vec3::splat(2.0)),
345 DespawnOnExit(super::Scene::SpriteSlicing),
346 ));
347
348 commands.spawn((
349 Sprite {
350 image: texture,
351 image_mode: SpriteImageMode::Sliced(TextureSlicer {
352 border: BorderRect::all(20.0),
353 center_scale_mode: SliceScaleMode::Stretch,
354 ..default()
355 }),
356 custom_size: Some(Vec2::new(200.0, 200.0)),
357 ..default()
358 },
359 Transform::from_translation(Vec3::new(150.0, 50.0, 0.0)),
360 DespawnOnExit(super::Scene::SpriteSlicing),
361 ));
362
363 commands.spawn((
364 Text2d::new("Original"),
365 TextFont {
366 font: FontSource::from(font.clone()),
367 font_size: FontSize::Px(20.0),
368 ..default()
369 },
370 Transform::from_translation(Vec3::new(-150.0, -80.0, 0.0)),
371 DespawnOnExit(super::Scene::SpriteSlicing),
372 ));
373
374 commands.spawn((
375 Text2d::new("Sliced"),
376 TextFont {
377 font: FontSource::from(font.clone()),
378 font_size: FontSize::Px(20.0),
379 ..default()
380 },
381 Transform::from_translation(Vec3::new(150.0, -80.0, 0.0)),
382 DespawnOnExit(super::Scene::SpriteSlicing),
383 ));
384 }
385}
386
387mod gizmos {
388 use bevy::{color::palettes::css::*, prelude::*};
389
390 pub fn setup(mut commands: Commands) {
391 commands.spawn((Camera2d, DespawnOnExit(super::Scene::Gizmos)));
392 }
393
394 pub fn draw_gizmos(mut gizmos: Gizmos) {
395 gizmos.rect_2d(
396 Isometry2d::from_translation(Vec2::new(-200.0, 0.0)),
397 Vec2::new(200.0, 200.0),
398 RED,
399 );
400 gizmos
401 .circle_2d(
402 Isometry2d::from_translation(Vec2::new(-200.0, 0.0)),
403 200.0,
404 GREEN,
405 )
406 .resolution(64);
407
408 gizmos.text_2d(
409 Isometry2d::from_translation(Vec2::new(-200.0, 0.0)),
410 "text_2d gizmo",
411 15.,
412 Vec2 { x: 0., y: 0. },
413 Color::WHITE,
414 );
415
416 for i in 0..4 {
418 let x = 200.0 * (1.0 + (i % 2) as f32);
419 let y = 150.0 * (0.5 - (i / 2) as f32);
420 let mut grid = gizmos.grid(
421 Vec3::new(x, y, 0.0),
422 UVec2::new(5, 4),
423 Vec2::splat(30.),
424 Color::WHITE,
425 );
426 if i & 1 > 0 {
427 grid = grid.outer_edges_x();
428 }
429 if i & 2 > 0 {
430 grid.outer_edges_y();
431 }
432 }
433 }
434}
435
436mod texture_atlas_builder {
437 use bevy::{
438 asset::RenderAssetUsages,
439 image::ImageSampler,
440 prelude::*,
441 render::render_resource::{Extent3d, TextureDimension, TextureFormat},
442 sprite::Anchor,
443 };
444
445 const ATLAS_SIZE: UVec2 = UVec2::splat(64);
446 const IMAGE_SIZE: UVec2 = UVec2::splat(28);
447 const PADDING_SIZE: UVec2 = UVec2::splat(2);
448 const ATLAS_SCALE: f32 = 4.;
449 const IMAGE_SCALE: f32 = 4.;
450
451 pub fn setup(
452 mut commands: Commands,
453 mut textures: ResMut<Assets<Image>>,
454 mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
455 ) {
456 commands.spawn((Camera2d, DespawnOnExit(super::Scene::TextureAtlasBuilder)));
457
458 for (i, padding) in [UVec2::ZERO, PADDING_SIZE].into_iter().enumerate() {
459 let images = [
461 [255, 0, 0, 255],
462 [0, 255, 0, 255],
463 [0, 0, 255, 255],
464 [255, 255, 0, 255],
465 ]
466 .map(|pixel| {
467 Image::new_fill(
468 Extent3d {
469 width: 28,
470 height: 28,
471 depth_or_array_layers: 1,
472 },
473 TextureDimension::D2,
474 &pixel,
475 TextureFormat::Rgba8UnormSrgb,
476 RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
477 )
478 });
479
480 let mut texture_atlas_builder = TextureAtlasBuilder::default();
481 texture_atlas_builder
482 .initial_size(ATLAS_SIZE)
483 .max_size(ATLAS_SIZE)
484 .padding(padding);
485 for image in &images {
486 texture_atlas_builder.add_texture(None, image);
487 }
488
489 let (atlas_layout, _, atlas_texture) = texture_atlas_builder.build().expect(
490 "The images are 28 pixels square, so they should fit with 4 pixels left over",
491 );
492 let atlas_layout = texture_atlases.add(atlas_layout);
493
494 let mut nearest_atlas_image = atlas_texture.clone();
495 nearest_atlas_image.sampler = ImageSampler::nearest();
496
497 let atlas_handle = textures.add(atlas_texture);
498 let nearest_atlas_handle = textures.add(nearest_atlas_image);
499
500 let position = ((2. * i as f32 - 1.) * (0.625 * ATLAS_SIZE.x as f32 * ATLAS_SCALE))
501 .round()
502 * Vec3::X;
503
504 commands.spawn((
505 Sprite {
506 image: nearest_atlas_handle,
507 custom_size: Some(ATLAS_SIZE.as_vec2() * ATLAS_SCALE),
508 ..default()
509 },
510 Anchor::BOTTOM_CENTER,
511 ShowAabbGizmo {
512 color: Some(Color::WHITE),
513 },
514 DespawnOnExit(super::Scene::TextureAtlasBuilder),
515 Transform::from_translation(position),
516 ));
517
518 for (index, anchor) in [
519 Anchor::BOTTOM_RIGHT,
520 Anchor::BOTTOM_LEFT,
521 Anchor::TOP_LEFT,
522 Anchor::TOP_RIGHT,
523 ]
524 .into_iter()
525 .enumerate()
526 {
527 commands.spawn((
528 Sprite {
529 image: atlas_handle.clone(),
530 texture_atlas: Some(TextureAtlas {
531 layout: atlas_layout.clone(),
532 index,
533 }),
534 custom_size: Some(IMAGE_SIZE.as_vec2() * IMAGE_SCALE),
535 ..default()
536 },
537 Transform::from_translation(
538 position
539 + -2.
540 * IMAGE_SCALE
541 * (Vec3::Y * IMAGE_SIZE.y as f32 + anchor.as_vec().extend(0.)),
542 ),
543 anchor,
544 DespawnOnExit(super::Scene::TextureAtlasBuilder),
545 ));
546 }
547 }
548 }
549}
550
551mod color_consistency {
552 use bevy::{core_pipeline::tonemapping::Tonemapping, prelude::*};
562
563 const TEST_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
565 const DEFAULT_WIDTH: f32 = 1280.0;
566 const DEFAULT_HEIGHT: f32 = 720.0;
567 const STRIP_WIDTH: f32 = DEFAULT_WIDTH / 3.0;
568 const STRIP_HEIGHT: f32 = DEFAULT_HEIGHT / 3.0;
569
570 pub fn setup(
571 mut commands: Commands,
572 mut meshes: ResMut<Assets<Mesh>>,
573 mut materials: ResMut<Assets<ColorMaterial>>,
574 ) {
575 commands.insert_resource(ClearColor(TEST_COLOR));
577
578 commands.spawn((
580 Camera2d,
581 Tonemapping::None,
582 DespawnOnExit(super::Scene::ColorConsistency),
583 ));
584
585 commands.spawn((
587 Sprite {
588 color: TEST_COLOR,
589 custom_size: Some(Vec2::new(STRIP_WIDTH, STRIP_HEIGHT)),
590 ..default()
591 },
592 Transform::from_xyz(0.0, STRIP_HEIGHT, 0.0),
593 DespawnOnExit(super::Scene::ColorConsistency),
594 ));
595
596 commands.spawn((
598 Mesh2d(meshes.add(Rectangle::new(STRIP_WIDTH, STRIP_HEIGHT))),
599 MeshMaterial2d(materials.add(ColorMaterial::from_color(TEST_COLOR))),
600 Transform::from_xyz(0.0, 0.0, 0.0),
601 DespawnOnExit(super::Scene::ColorConsistency),
602 ));
603
604 commands.spawn((
606 Node {
607 position_type: PositionType::Absolute,
608 bottom: Val::Px(0.0),
609 left: Val::Px(0.0),
610 width: Val::Percent(33.3),
611 height: Val::Px(STRIP_HEIGHT),
612 ..default()
613 },
614 BackgroundColor(TEST_COLOR),
615 DespawnOnExit(super::Scene::ColorConsistency),
616 ));
617 }
618
619 pub fn teardown(mut commands: Commands) {
622 commands.insert_resource(ClearColor::default());
623 }
624}