docs/bevy/examples/2d/2d_shapes.rs 8.4 K · 219 lines · raw · history

1 //! Here we use shape primitives to build meshes in a 2D rendering context, making each mesh a certain color by giving that mesh's entity a material based off a [`Color`].
2 //! Meshes are better known for their use in 3D rendering, but we can use them in a 2D context too. Without a third dimension, the meshes we're building are flat – like paper on a table. These are still very useful for "vector-style" graphics, picking behavior, or as a foundation to build off of for where to apply a shader.
3 //!
4 //! A "shape definition" is not a mesh on its own. A circle can be defined with a radius, i.e. [`Circle::new(50.0)`][Circle::new], but rendering tends to happen with meshes built out of triangles. So we need to turn shape descriptions into meshes.
5 //!
6 //! Thankfully, we can add shape primitives directly to [`Assets<Mesh>`] because [`Mesh`] implements [`From`] for shape primitives and [`Assets<T>::add`] can be given any value that can be "turned into" `T`!
7 //!
8 //! We apply a material to the shape by first making a [`Color`] then calling [`Assets<ColorMaterial>::add`] with that color as its argument, which will create a material from that color through the same process [`Assets<Mesh>::add`] can take a shape primitive.
9 //!
10 //! Both the mesh and material need to be wrapped in their own "newtypes". The mesh and material are currently [`Handle<Mesh>`] and [`Handle<ColorMaterial>`] at the moment, which are not components. Handles are put behind "newtypes" to prevent ambiguity, as some entities might want to have handles to meshes (or images, or materials etc.) for different purposes! All we need to do to make them rendering-relevant components is wrap the mesh handle and the material handle in [`Mesh2d`] and [`MeshMaterial2d`] respectively.
11 //!
12 //! You can toggle wireframes with a Feathers checkbox except on wasm. Wasm does not support
13 //! `POLYGON_MODE_LINE` on the gpu.
14
15 use bevy::prelude::*;
16 #[cfg(not(target_arch = "wasm32"))]
17 use bevy::sprite_render::{Wireframe2dConfig, Wireframe2dPlugin};
18 use bevy::{
19 feathers::{controls::FeathersCheckbox, theme::UiTheme, FeathersPlugins},
20 ui_widgets::{checkbox_self_update, ValueChange},
21 };
22 use checkbox::feathers_option_checkbox;
23 use scene::{bottom_left_scene, top_left_scene};
24
25 #[path = "../helpers/checkbox.rs"]
26 mod checkbox;
27
28 #[path = "../helpers/theme.rs"]
29 mod theme;
30
31 #[path = "../helpers/scene.rs"]
32 mod scene;
33
34 /// Various settings for the demo.
35 #[derive(Resource, Default)]
36 struct AppStatus {
37 rotation: bool,
38 }
39
40 #[derive(Clone, Copy, Component, Debug, Default, PartialEq)]
41 enum CheckboxInput {
42 #[default]
43 Rotation,
44 Wireframe,
45 }
46
47 fn main() {
48 let mut app = App::new();
49 app.add_plugins((
50 DefaultPlugins,
51 FeathersPlugins,
52 #[cfg(not(target_arch = "wasm32"))]
53 Wireframe2dPlugin::default(),
54 ))
55 .init_resource::<AppStatus>()
56 .add_systems(Startup, setup);
57 app.insert_resource(UiTheme(theme::basic_example_theme(Color::WHITE)));
58 app.add_observer(handle_value_change_checkbox);
59 app.add_systems(Update, rotate.run_if(rotate_checkbox_checked));
60 app.add_observer(checkbox_self_update);
61 app.run();
62 }
63
64 const X_EXTENT: f32 = 1000.;
65 const Y_EXTENT: f32 = 150.;
66 const THICKNESS: f32 = 5.0;
67
68 fn setup(
69 mut commands: Commands,
70 mut meshes: ResMut<Assets<Mesh>>,
71 mut materials: ResMut<Assets<ColorMaterial>>,
72 ) {
73 commands.spawn(Camera2d);
74
75 let shapes = [
76 meshes.add(Circle::new(50.0)),
77 meshes.add(CircularSector::new(50.0, 1.0)),
78 meshes.add(CircularSegment::new(50.0, 1.25)),
79 meshes.add(Ellipse::new(25.0, 50.0)),
80 meshes.add(Annulus::new(25.0, 50.0)),
81 meshes.add(Capsule2d::new(25.0, 50.0)),
82 meshes.add(Rhombus::new(75.0, 100.0)),
83 meshes.add(Rectangle::new(50.0, 100.0)),
84 meshes.add(RegularPolygon::new(50.0, 6)),
85 meshes.add(Triangle2d::new(
86 Vec2::Y * 50.0,
87 Vec2::new(-50.0, -50.0),
88 Vec2::new(50.0, -50.0),
89 )),
90 meshes.add(Segment2d::new(
91 Vec2::new(-50.0, 50.0),
92 Vec2::new(50.0, -50.0),
93 )),
94 meshes.add(Polyline2d::new(vec![
95 Vec2::new(-50.0, 50.0),
96 Vec2::new(0.0, -50.0),
97 Vec2::new(50.0, 50.0),
98 ])),
99 ];
100 let num_shapes = shapes.len();
101
102 for (i, shape) in shapes.into_iter().enumerate() {
103 // Distribute colors evenly across the rainbow.
104 let color = Color::hsl(360. * i as f32 / num_shapes as f32, 0.95, 0.7);
105
106 commands.spawn((
107 Mesh2d(shape),
108 MeshMaterial2d(materials.add(color)),
109 Transform::from_xyz(
110 // Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
111 -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT,
112 Y_EXTENT / 2.,
113 0.0,
114 ),
115 ));
116 }
117
118 let rings = [
119 meshes.add(Circle::new(50.0).to_ring(THICKNESS)),
120 // this visually produces an arc segment but this is not technically accurate
121 meshes.add(Ring::new(
122 CircularSector::new(50.0, 1.0),
123 CircularSector::new(45.0, 1.0),
124 )),
125 meshes.add(CircularSegment::new(50.0, 1.25).to_ring(THICKNESS)),
126 meshes.add({
127 // This is an approximation; Ellipse does not implement Inset as concentric ellipses do not have parallel curves
128 let outer = Ellipse::new(25.0, 50.0);
129 let mut inner = outer;
130 inner.half_size -= Vec2::splat(THICKNESS);
131 Ring::new(outer, inner)
132 }),
133 // this is equivalent to the Annulus::new(25.0, 50.0) above
134 meshes.add(Ring::new(Circle::new(50.0), Circle::new(25.0))),
135 meshes.add(Capsule2d::new(25.0, 50.0).to_ring(THICKNESS)),
136 meshes.add(Rhombus::new(75.0, 100.0).to_ring(THICKNESS)),
137 meshes.add(Rectangle::new(50.0, 100.0).to_ring(THICKNESS)),
138 meshes.add(RegularPolygon::new(50.0, 6).to_ring(THICKNESS)),
139 meshes.add(
140 Triangle2d::new(
141 Vec2::Y * 50.0,
142 Vec2::new(-50.0, -50.0),
143 Vec2::new(50.0, -50.0),
144 )
145 .to_ring(THICKNESS),
146 ),
147 ];
148 // Allow for 2 empty spaces
149 let num_rings = rings.len() + 2;
150
151 for (i, shape) in rings.into_iter().enumerate() {
152 // Distribute colors evenly across the rainbow.
153 let color = Color::hsl(360. * i as f32 / num_rings as f32, 0.95, 0.7);
154
155 commands.spawn((
156 Mesh2d(shape),
157 MeshMaterial2d(materials.add(color)),
158 Transform::from_xyz(
159 // Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2.
160 -X_EXTENT / 2. + i as f32 / (num_rings - 1) as f32 * X_EXTENT,
161 -Y_EXTENT / 2.,
162 0.0,
163 ),
164 ));
165 }
166 spawn_buttons(&mut commands);
167 }
168
169 /// Spawns the checkboxes in the bottom left corner of the screen.
170 fn spawn_buttons(commands: &mut Commands) {
171 if !cfg!(target_arch = "wasm32") {
172 commands.spawn_scene(bsn! {
173 @bottom_left_scene()
174 Children [
175 @feathers_option_checkbox("ROTATE", Some(CheckboxInput::Rotation))
176 --
177 @feathers_option_checkbox("WIREFRAME", Some(CheckboxInput::Wireframe))
178 ]
179 });
180 } else {
181 commands.spawn_scene(bsn! {
182 @top_left_scene() // so the user can immediately see the control in browser w/o scrolling
183 Children [
184 @feathers_option_checkbox("ROTATE", Some(CheckboxInput::Rotation))
185 ]
186 });
187 }
188 }
189
190 fn handle_value_change_checkbox(
191 event: On<ValueChange<bool>>,
192 #[cfg(not(target_arch = "wasm32"))] mut wireframe_config: ResMut<Wireframe2dConfig>,
193 mut app_status: ResMut<AppStatus>,
194 checkbox_input_q: Query<&CheckboxInput, With<FeathersCheckbox>>,
195 ) {
196 if let Ok(checkbox_input) = checkbox_input_q.get(event.source) {
197 match checkbox_input {
198 CheckboxInput::Rotation => {
199 app_status.rotation = event.value;
200 }
201 #[cfg(target_arch = "wasm32")]
202 CheckboxInput::Wireframe => {}
203 #[cfg(not(target_arch = "wasm32"))]
204 CheckboxInput::Wireframe => {
205 wireframe_config.global = event.value;
206 }
207 }
208 }
209 }
210
211 fn rotate_checkbox_checked(app_status: Res<AppStatus>) -> bool {
212 app_status.rotation
213 }
214
215 fn rotate(mut query: Query<&mut Transform, With<Mesh2d>>, time: Res<Time>) {
216 for mut transform in &mut query {
217 transform.rotate_z(time.delta_secs() / 2.0);
218 }
219 }