diff --git a/lightningbeam-ui/lightningbeam-core/src/renderer.rs b/lightningbeam-ui/lightningbeam-core/src/renderer.rs index d622cc8..b4c5daf 100644 --- a/lightningbeam-ui/lightningbeam-core/src/renderer.rs +++ b/lightningbeam-ui/lightningbeam-core/src/renderer.rs @@ -438,10 +438,38 @@ pub fn render_document_with_transform( /// Draw the document background fn render_background(document: &Document, scene: &mut Scene, base_transform: Affine) { let background_rect = Rect::new(0.0, 0.0, document.width, document.height); + let bg = &document.background_color; - // Convert our ShapeColor to vello's peniko Color - let background_color = document.background_color.to_peniko(); + // Draw checkerboard behind transparent backgrounds + if bg.a < 255 { + use vello::peniko::{Blob, Color, Extend, ImageAlphaType, ImageData, ImageQuality}; + // 2x2 pixel checkerboard pattern: light/dark alternating + let light: [u8; 4] = [204, 204, 204, 255]; + let dark: [u8; 4] = [170, 170, 170, 255]; + let pixels: Vec = [light, dark, dark, light].concat(); + let image_data = ImageData { + data: Blob::from(pixels), + format: ImageFormat::Rgba8, + width: 2, + height: 2, + alpha_type: ImageAlphaType::AlphaPremultiplied, + }; + let brush = ImageBrush::new(image_data) + .with_extend(Extend::Repeat) + .with_quality(ImageQuality::Low); + // Scale each pixel to 16x16 document units + let brush_transform = Affine::scale(16.0); + scene.fill( + Fill::NonZero, + base_transform, + &brush, + Some(brush_transform), + &background_rect, + ); + } + // Draw the background color on top (alpha-blended) + let background_color = bg.to_peniko(); scene.fill( Fill::NonZero, base_transform, diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index f755a0d..10d2abc 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -763,6 +763,7 @@ struct EditorApp { brush_opacity: f32, // brush opacity 0.0–1.0 brush_hardness: f32, // brush hardness 0.0–1.0 brush_spacing: f32, // dabs_per_radius (fraction of radius per dab) + brush_use_fg: bool, // true = paint with FG (stroke) color, false = BG (fill) color // Audio engine integration #[allow(dead_code)] // Must be kept alive to maintain audio output audio_stream: Option, @@ -1043,6 +1044,7 @@ impl EditorApp { brush_opacity: 1.0, brush_hardness: 0.5, brush_spacing: 0.1, + brush_use_fg: true, audio_stream, audio_controller, audio_event_rx, @@ -5420,6 +5422,7 @@ impl eframe::App for EditorApp { brush_opacity: &mut self.brush_opacity, brush_hardness: &mut self.brush_hardness, brush_spacing: &mut self.brush_spacing, + brush_use_fg: &mut self.brush_use_fg, audio_controller: self.audio_controller.as_ref(), video_manager: &self.video_manager, playback_time: &mut self.playback_time, diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs index 638bfdb..ec79d55 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs @@ -302,6 +302,14 @@ impl InfopanelPane { // Raster paint tools Tool::Draw | Tool::Erase | Tool::Smudge if is_raster_paint_tool => { + // Color source toggle (Draw tool only) + if matches!(tool, Tool::Draw) { + ui.horizontal(|ui| { + ui.label("Color:"); + ui.selectable_value(shared.brush_use_fg, true, "FG"); + ui.selectable_value(shared.brush_use_fg, false, "BG"); + }); + } ui.horizontal(|ui| { ui.label("Size:"); ui.add( @@ -374,20 +382,9 @@ impl InfopanelPane { ui.label("Fill:"); match info.fill_color { Some(Some(color)) => { - let mut egui_color = egui::Color32::from_rgba_unmultiplied( - color.r, color.g, color.b, color.a, - ); - if egui::color_picker::color_edit_button_srgba( - ui, - &mut egui_color, - egui::color_picker::Alpha::OnlyBlend, - ).changed() { - let new_color = ShapeColor { - r: egui_color.r(), - g: egui_color.g(), - b: egui_color.b(), - a: egui_color.a(), - }; + let mut rgba = [color.r, color.g, color.b, color.a]; + if ui.color_edit_button_srgba_unmultiplied(&mut rgba).changed() { + let new_color = ShapeColor::rgba(rgba[0], rgba[1], rgba[2], rgba[3]); let action = SetShapePropertiesAction::set_fill_color( layer_id, time, face_ids.clone(), Some(new_color), ); @@ -408,20 +405,9 @@ impl InfopanelPane { ui.label("Stroke:"); match info.stroke_color { Some(Some(color)) => { - let mut egui_color = egui::Color32::from_rgba_unmultiplied( - color.r, color.g, color.b, color.a, - ); - if egui::color_picker::color_edit_button_srgba( - ui, - &mut egui_color, - egui::color_picker::Alpha::OnlyBlend, - ).changed() { - let new_color = ShapeColor { - r: egui_color.r(), - g: egui_color.g(), - b: egui_color.b(), - a: egui_color.a(), - }; + let mut rgba = [color.r, color.g, color.b, color.a]; + if ui.color_edit_button_srgba_unmultiplied(&mut rgba).changed() { + let new_color = ShapeColor::rgba(rgba[0], rgba[1], rgba[2], rgba[3]); let action = SetShapePropertiesAction::set_stroke_color( layer_id, time, edge_ids.clone(), Some(new_color), ); @@ -538,14 +524,14 @@ impl InfopanelPane { } }); - // Background color + // Background color (with alpha) ui.horizontal(|ui| { ui.label("Background:"); let bg = document.background_color; - let mut color = [bg.r, bg.g, bg.b]; - if ui.color_edit_button_srgb(&mut color).changed() { + let mut color = [bg.r, bg.g, bg.b, bg.a]; + if ui.color_edit_button_srgba_unmultiplied(&mut color).changed() { let action = SetDocumentPropertiesAction::set_background_color( - ShapeColor::rgb(color[0], color[1], color[2]), + ShapeColor::rgba(color[0], color[1], color[2], color[3]), ); shared.pending_actions.push(Box::new(action)); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index 4370a4f..f226e72 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -192,6 +192,8 @@ pub struct SharedPaneState<'a> { pub brush_opacity: &'a mut f32, pub brush_hardness: &'a mut f32, pub brush_spacing: &'a mut f32, + /// Whether the brush paints with the foreground (fill) color (true) or background (stroke) color (false) + pub brush_use_fg: &'a mut bool, /// Audio engine controller for playback control (wrapped in Arc> for thread safety) pub audio_controller: Option<&'a std::sync::Arc>>, /// Video manager for video decoding and frame caching diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index b7a25e1..b5d581a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -4482,7 +4482,7 @@ impl StagePane { let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) { [1.0f32, 1.0, 1.0, 1.0] } else { - let c = *shared.stroke_color; + let c = if *shared.brush_use_fg { *shared.stroke_color } else { *shared.fill_color }; [c.r() as f32 / 255.0, c.g() as f32 / 255.0, c.b() as f32 / 255.0, c.a() as f32 / 255.0] }; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs index 254c050..f192158 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/toolbar.rs @@ -211,19 +211,49 @@ impl PaneRenderer for ToolbarPane { let color_row_width = fill_label_width + color_button_size + button_spacing; let color_x = rect.left() + (rect.width() - color_row_width) / 2.0; - // For raster layers show a single "Color" swatch (brush paint color = stroke_color). - // For vector layers show Fill + Stroke. - if !is_raster { - // Fill color label + // Two color swatches: + // Stroke/FG always on top, Fill/BG always on bottom. + // Raster layers label them "FG" / "BG"; vector layers label them "Stroke" / "Fill". + { + let stroke_label = if is_raster { "FG" } else { "Stroke" }; ui.painter().text( egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0), egui::Align2::CENTER_CENTER, - "Fill", + stroke_label, + egui::FontId::proportional(14.0), + egui::Color32::from_gray(200), + ); + + let stroke_button_rect = egui::Rect::from_min_size( + egui::pos2(color_x + fill_label_width + button_spacing, y), + egui::vec2(color_button_size, color_button_size), + ); + let stroke_button_id = ui.id().with(("stroke_color_button", path)); + let stroke_response = ui.interact(stroke_button_rect, stroke_button_id, egui::Sense::click()); + draw_color_button(ui, stroke_button_rect, *shared.stroke_color); + egui::containers::Popup::from_toggle_button_response(&stroke_response) + .show(|ui| { + ui.spacing_mut().slider_width = 275.0; + let changed = egui::color_picker::color_picker_color32(ui, shared.stroke_color, egui::color_picker::Alpha::OnlyBlend); + if changed { + *shared.active_color_mode = super::ColorMode::Stroke; + } + }); + + y += color_button_size + button_spacing; + } + + // Fill/BG color swatch + { + let fill_label = if is_raster { "BG" } else { "Fill" }; + ui.painter().text( + egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0), + egui::Align2::CENTER_CENTER, + fill_label, egui::FontId::proportional(14.0), egui::Color32::from_gray(200), ); - // Fill color button let fill_button_rect = egui::Rect::from_min_size( egui::pos2(color_x + fill_label_width + button_spacing, y), egui::vec2(color_button_size, color_button_size), @@ -233,40 +263,13 @@ impl PaneRenderer for ToolbarPane { draw_color_button(ui, fill_button_rect, *shared.fill_color); egui::containers::Popup::from_toggle_button_response(&fill_response) .show(|ui| { + ui.spacing_mut().slider_width = 275.0; let changed = egui::color_picker::color_picker_color32(ui, shared.fill_color, egui::color_picker::Alpha::OnlyBlend); if changed { *shared.active_color_mode = super::ColorMode::Fill; } }); - - y += color_button_size + button_spacing; } - - // Stroke/brush color label - let stroke_label = if is_raster { "Color" } else { "Stroke" }; - ui.painter().text( - egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0), - egui::Align2::CENTER_CENTER, - stroke_label, - egui::FontId::proportional(14.0), - egui::Color32::from_gray(200), - ); - - // Stroke color button - let stroke_button_rect = egui::Rect::from_min_size( - egui::pos2(color_x + fill_label_width + button_spacing, y), - egui::vec2(color_button_size, color_button_size), - ); - let stroke_button_id = ui.id().with(("stroke_color_button", path)); - let stroke_response = ui.interact(stroke_button_rect, stroke_button_id, egui::Sense::click()); - draw_color_button(ui, stroke_button_rect, *shared.stroke_color); - egui::containers::Popup::from_toggle_button_response(&stroke_response) - .show(|ui| { - let changed = egui::color_picker::color_picker_color32(ui, shared.stroke_color, egui::color_picker::Alpha::OnlyBlend); - if changed { - *shared.active_color_mode = super::ColorMode::Stroke; - } - }); } // end color pickers }