1use bevy::{
5 asset::{
6 saver::{save_using_saver, SavedAsset},
7 RenderAssetUsages,
8 },
9 camera::ScalingMode,
10 color::palettes::tailwind,
11 image::{ImageLoaderSettings, ImageSampler, ImageSaver, ImageSaverSettings},
12 input::common_conditions::input_just_pressed,
13 picking::pointer::Location,
14 prelude::*,
15 render::render_resource::{Extent3d, TextureDimension, TextureFormat},
16 sprite::Anchor,
17 tasks::IoTaskPool,
18};
19
20fn main() {
21 App::new()
22 .add_plugins(DefaultPlugins.set(AssetPlugin {
23 file_path: "examples/asset/saved_assets".to_string(),
26 ..Default::default()
27 }))
28 .add_plugins(image_drawing_plugin)
29 .add_systems(
30 PreUpdate,
31 perform_save.run_if(input_just_pressed(KeyCode::F5)),
32 )
33 .run();
34}
35
36const ASSET_PATH: &str = "art_project.png";
37
38fn perform_save(
39 image_to_save: Res<ImageToSave>,
40 images: Res<Assets<Image>>,
41 asset_server: Res<AssetServer>,
42) {
43 let image = images.get(&image_to_save.0).unwrap();
44
45 let image = image.clone();
46 let asset_server = asset_server.clone();
47 IoTaskPool::get()
48 .spawn(async move {
49 match save_using_saver(
50 asset_server.clone(),
51 &ImageSaver,
52 &ASSET_PATH.into(),
53 SavedAsset::from_asset(&image),
54 &ImageSaverSettings::default(),
55 )
56 .await
57 {
58 Ok(()) => info!("Completed save of {ASSET_PATH}"),
59 Err(err) => error!("Failed to save asset: {err}"),
60 }
61 })
62 .detach();
63}
64
65fn image_drawing_plugin(app: &mut App) {
69 app.add_systems(Startup, setup)
70 .add_observer(on_drag_start)
71 .add_observer(on_drag)
72 .add_observer(try_plot)
73 .init_resource::<DrawColor>()
74 .add_observer(on_enter_selectable)
75 .add_observer(on_leave_selectable)
76 .add_observer(on_press_selectable);
77}
78
79#[derive(Resource)]
80struct ImageToSave(Handle<Image>);
81
82#[derive(Component)]
83struct SpriteToSave;
84
85fn setup(
86 mut commands: Commands,
87 asset_server: Res<AssetServer>,
88 mut images: ResMut<Assets<Image>>,
89) {
90 commands.spawn((
91 Camera2d,
92 Projection::Orthographic(OrthographicProjection {
93 scaling_mode: ScalingMode::FixedVertical {
94 viewport_height: 125.0,
95 },
96 ..OrthographicProjection::default_2d()
97 }),
98 ));
99
100 commands.spawn(Text(
101 r"Select a color from the palette at the bottom
102LMB - Draw with selected color
103F5 - Save image"
104 .into(),
105 ));
106
107 let handle = asset_server
108 .load_builder()
109 .with_settings(|settings: &mut ImageLoaderSettings| {
110 settings.sampler = ImageSampler::nearest();
111 })
112 .load(ASSET_PATH);
113 commands.spawn((
114 Sprite {
115 image: handle.clone(),
116 ..Default::default()
117 },
118 SpriteToSave,
119 Pickable::default(),
120 ));
121
122 images
127 .insert(&handle, {
128 let mut image = Image::new_fill(
129 Extent3d {
130 width: 100,
131 height: 100,
132 depth_or_array_layers: 1,
133 },
134 TextureDimension::D2,
135 &[0, 0, 0, 255],
136 TextureFormat::Rgba8Unorm,
137 RenderAssetUsages::all(),
138 );
139 image.sampler = ImageSampler::nearest();
140 image
141 })
142 .unwrap();
143
144 commands.insert_resource(ImageToSave(handle));
145
146 let container = commands
147 .spawn((
148 Node {
149 width: percent(100),
150 height: percent(100),
151 align_items: AlignItems::End,
152 justify_content: JustifyContent::Center,
153 ..Default::default()
154 },
155 Pickable::IGNORE,
156 ))
157 .id();
158
159 for color in [
160 Color::WHITE,
161 Color::Srgba(tailwind::RED_500),
162 Color::Srgba(tailwind::ORANGE_500),
163 Color::Srgba(tailwind::YELLOW_500),
164 Color::Srgba(tailwind::GREEN_500),
165 Color::Srgba(tailwind::BLUE_500),
166 Color::Srgba(tailwind::INDIGO_500),
167 Color::Srgba(tailwind::VIOLET_500),
168 Color::BLACK,
169 ] {
170 let mut entity = commands.spawn((
171 Node {
172 width: vw(5),
173 height: vh(5),
174 border: px(5).all(),
175 ..Default::default()
176 },
177 SelectableColor,
178 BackgroundColor(color),
179 BorderColor::all(NORMAL_COLOR),
180 ChildOf(container),
181 ));
182 if color == Color::WHITE {
183 entity.insert((Selected, BorderColor::all(SELECTED_COLOR)));
184 }
185 }
186}
187
188#[derive(EntityEvent)]
189struct TryPlot {
190 entity: Entity,
191 location: Location,
192}
193
194fn on_drag_start(event: On<Pointer<DragStart>>, mut commands: Commands) {
195 commands.trigger(TryPlot {
196 entity: event.entity,
197 location: event.pointer_location.clone(),
198 });
199}
200
201fn on_drag(event: On<Pointer<Drag>>, mut commands: Commands) {
202 commands.trigger(TryPlot {
203 entity: event.entity,
204 location: event.pointer_location.clone(),
205 });
206}
207
208fn try_plot(
209 event: On<TryPlot>,
210 sprite: Query<(&Sprite, &Anchor, &GlobalTransform), With<SpriteToSave>>,
211 camera: Single<(&Camera, &GlobalTransform)>,
212 texture_atlases: Res<Assets<TextureAtlasLayout>>,
213 draw_color: Res<DrawColor>,
214 mut images: ResMut<Assets<Image>>,
215) {
216 let Ok((sprite, anchor, sprite_transform)) = sprite.get(event.entity) else {
217 return;
218 };
219 let (camera, camera_transform) = camera.into_inner();
220 let Ok(world_position) = camera.viewport_to_world_2d(camera_transform, event.location.position)
221 else {
222 return;
223 };
224 let relative_to_sprite = sprite_transform
225 .affine()
226 .inverse()
227 .transform_point3(world_position.extend(0.0));
228 let Ok(pixel_space) = sprite.compute_pixel_space_point(
229 relative_to_sprite.xy(),
230 *anchor,
231 &images,
232 &texture_atlases,
233 ) else {
234 return;
235 };
236 let pixel_coordinates = pixel_space.floor().as_uvec2();
237 let mut image = images.get_mut(&sprite.image).unwrap();
238 image
241 .set_color_at(pixel_coordinates.x, pixel_coordinates.y, draw_color.0)
242 .unwrap();
243}
244
245#[derive(Resource, Default)]
246struct DrawColor(Color);
247
248#[derive(Component)]
249struct SelectableColor;
250
251#[derive(Component)]
252struct Selected;
253
254const NORMAL_COLOR: Color = Color::BLACK;
255const HIGHLIGHT_COLOR: Color = Color::Srgba(tailwind::NEUTRAL_500);
256const SELECTED_COLOR: Color = Color::Srgba(tailwind::RED_600);
257
258fn on_enter_selectable(
259 event: On<Pointer<Enter>>,
260 mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
261) {
262 let Ok(mut border) = border.get_mut(event.entity) else {
263 return;
264 };
265
266 *border = BorderColor::all(HIGHLIGHT_COLOR);
267}
268
269fn on_leave_selectable(
270 event: On<Pointer<Leave>>,
271 mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
272) {
273 let Ok(mut border) = border.get_mut(event.entity) else {
274 return;
275 };
276
277 *border = BorderColor::all(NORMAL_COLOR);
278}
279
280fn on_press_selectable(
281 event: On<Pointer<Press>>,
282 mut borders: Query<(Entity, &mut BorderColor, &BackgroundColor), With<SelectableColor>>,
283 mut draw_color: ResMut<DrawColor>,
284 mut commands: Commands,
285) {
286 if !borders.contains(event.entity) {
287 return;
288 }
289 for (entity, mut border, _) in borders.iter_mut() {
290 commands.entity(entity).remove::<Selected>();
291 *border = BorderColor::all(NORMAL_COLOR);
292 }
293 let (_, mut border, background_color) = borders.get_mut(event.entity).unwrap();
294 *border = BorderColor::all(SELECTED_COLOR);
295 commands.entity(event.entity).insert(Selected);
296
297 draw_color.0 = background_color.0;
298}