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