Skip to main content

2d_shapes/
2d_shapes.rs

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
15use bevy::prelude::*;
16#[cfg(not(target_arch = "wasm32"))]
17use bevy::sprite_render::{Wireframe2dConfig, Wireframe2dPlugin};
18use bevy::{
19    feathers::{controls::FeathersCheckbox, theme::UiTheme, FeathersPlugins},
20    ui_widgets::{checkbox_self_update, ValueChange},
21};
22use checkbox::feathers_option_checkbox;
23use scene::{bottom_left_scene, top_left_scene};
24
25#[path = "../helpers/checkbox.rs"]
26mod checkbox;
27
28#[path = "../helpers/theme.rs"]
29mod theme;
30
31#[path = "../helpers/scene.rs"]
32mod scene;
33
34/// Various settings for the demo.
35#[derive(Resource, Default)]
36struct AppStatus {
37    rotation: bool,
38}
39
40#[derive(Clone, Copy, Component, Debug, Default, PartialEq)]
41enum CheckboxInput {
42    #[default]
43    Rotation,
44    Wireframe,
45}
46
47fn 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
64const X_EXTENT: f32 = 1000.;
65const Y_EXTENT: f32 = 150.;
66const THICKNESS: f32 = 5.0;
67
68fn 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.
170fn 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
190fn 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
211fn rotate_checkbox_checked(app_status: Res<AppStatus>) -> bool {
212    app_status.rotation
213}
214
215fn 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}