From 901aa042467856a458f14fe8e3d98f5f69754455 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 6 Mar 2026 09:17:35 -0500 Subject: [PATCH] Add sponge tool --- .../lightningbeam-core/src/brush_engine.rs | 15 +++++++- .../lightningbeam-core/src/raster_layer.rs | 5 +++ .../lightningbeam-editor/src/main.rs | 16 +++++++++ .../src/panes/infopanel.rs | 36 +++++++++++++++++-- .../lightningbeam-editor/src/panes/mod.rs | 7 ++++ .../src/panes/shaders/brush_dab.wgsl | 18 ++++++++++ .../lightningbeam-editor/src/panes/stage.rs | 19 ++++++++-- 7 files changed, 111 insertions(+), 5 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs b/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs index 6d647c2..9712657 100644 --- a/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs +++ b/lightningbeam-ui/lightningbeam-core/src/brush_engine.rs @@ -306,6 +306,7 @@ impl BrushEngine { RasterBlendMode::Healing => 4u32, RasterBlendMode::PatternStamp => 5u32, RasterBlendMode::DodgeBurn => 6u32, + RasterBlendMode::Sponge => 7u32, }; let push_dab = |dabs: &mut Vec, @@ -328,7 +329,7 @@ impl BrushEngine { color_b: cb, // Clone stamp: color_r/color_g hold source canvas X/Y, so color_a = 1.0 // (blend strength is opa_weight × opacity × 1.0 in the shader). - color_a: if blend_mode_u == 3 || blend_mode_u == 4 || blend_mode_u == 6 { 1.0 } else { stroke.color[3] }, + color_a: if blend_mode_u == 3 || blend_mode_u == 4 || blend_mode_u == 6 || blend_mode_u == 7 { 1.0 } else { stroke.color[3] }, ndx, ndy, smudge_dist, blend_mode: blend_mode_u, elliptical_dab_ratio: bs.elliptical_dab_ratio.max(1.0), @@ -372,6 +373,9 @@ impl BrushEngine { } else if matches!(base_blend, RasterBlendMode::DodgeBurn) { // color_r = mode (0=dodge, 1=burn); strength comes from opacity (stroke.dodge_burn_mode as f32, 0.0, 0.0, 0.0, 0.0) + } else if matches!(base_blend, RasterBlendMode::Sponge) { + // color_r = mode (0=saturate, 1=desaturate); strength comes from opacity + (stroke.sponge_mode as f32, 0.0, 0.0, 0.0, 0.0) } else { (cr, cg, cb, 0.0, 0.0) }; @@ -505,6 +509,12 @@ impl BrushEngine { ex, ey, radius2, opacity2, stroke.dodge_burn_mode as f32, 0.0, 0.0, 0.0, 0.0, 0.0); + } else if matches!(base_blend, RasterBlendMode::Sponge) { + // color_r = mode (0=saturate, 1=desaturate) + push_dab(&mut dabs, &mut bbox, + ex, ey, radius2, opacity2, + stroke.sponge_mode as f32, 0.0, 0.0, + 0.0, 0.0, 0.0); } else { push_dab(&mut dabs, &mut bbox, ex, ey, radius2, opacity2, cr, cg, cb, @@ -547,6 +557,9 @@ impl BrushEngine { } else if matches!(base_blend, RasterBlendMode::DodgeBurn) { // color_r = mode (0=dodge, 1=burn) (stroke.dodge_burn_mode as f32, 0.0, 0.0, 0.0, 0.0) + } else if matches!(base_blend, RasterBlendMode::Sponge) { + // color_r = mode (0=saturate, 1=desaturate) + (stroke.sponge_mode as f32, 0.0, 0.0, 0.0, 0.0) } else { (cr, cg, cb, 0.0, 0.0) }; diff --git a/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs b/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs index 5fa7006..86546e8 100644 --- a/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs +++ b/lightningbeam-ui/lightningbeam-core/src/raster_layer.rs @@ -26,6 +26,8 @@ pub enum RasterBlendMode { PatternStamp, /// Dodge / Burn: lighten (dodge) or darken (burn) existing pixels DodgeBurn, + /// Sponge: saturate or desaturate existing pixels + Sponge, } impl Default for RasterBlendMode { @@ -70,6 +72,9 @@ pub struct StrokeRecord { /// Dodge/Burn mode: 0 = dodge (lighten), 1 = burn (darken) #[serde(default)] pub dodge_burn_mode: u32, + /// Sponge mode: 0 = saturate, 1 = desaturate + #[serde(default)] + pub sponge_mode: u32, pub points: Vec, } diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 8e948aa..446b244 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -812,6 +812,12 @@ struct EditorApp { dodge_burn_spacing: f32, dodge_burn_exposure: f32, dodge_burn_mode: u32, + /// Sponge tool settings + sponge_radius: f32, + sponge_hardness: f32, + sponge_spacing: f32, + sponge_flow: f32, + sponge_mode: u32, /// GPU-rendered brush preview pixel buffers, shared with VelloCallback::prepare(). brush_preview_pixels: std::sync::Arc)>>>, // Audio engine integration @@ -1118,6 +1124,11 @@ impl EditorApp { dodge_burn_spacing: 3.0, dodge_burn_exposure: 0.5, dodge_burn_mode: 0, + sponge_radius: 30.0, + sponge_hardness: 0.5, + sponge_spacing: 3.0, + sponge_flow: 0.5, + sponge_mode: 0, brush_preview_pixels: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), audio_stream, audio_controller, @@ -5593,6 +5604,11 @@ impl eframe::App for EditorApp { dodge_burn_spacing: &mut self.dodge_burn_spacing, dodge_burn_exposure: &mut self.dodge_burn_exposure, dodge_burn_mode: &mut self.dodge_burn_mode, + sponge_radius: &mut self.sponge_radius, + sponge_hardness: &mut self.sponge_hardness, + sponge_spacing: &mut self.sponge_spacing, + sponge_flow: &mut self.sponge_flow, + sponge_mode: &mut self.sponge_mode, 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 c726e84..6d63616 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/infopanel.rs @@ -173,7 +173,7 @@ impl InfopanelPane { tool, Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush | Tool::Erase | Tool::Smudge | Tool::CloneStamp | Tool::HealingBrush | Tool::PatternStamp - | Tool::DodgeBurn + | Tool::DodgeBurn | Tool::Sponge ); // Only show tool options for tools that have options @@ -198,7 +198,8 @@ impl InfopanelPane { Tool::CloneStamp => "Clone Stamp", Tool::HealingBrush => "Healing Brush", Tool::PatternStamp => "Pattern Stamp", - Tool::DodgeBurn => "Dodge / Burn", + Tool::DodgeBurn => "Dodge / Burn", + Tool::Sponge => "Sponge", _ => "Brush", } } else { @@ -391,6 +392,37 @@ impl InfopanelPane { }); } + Tool::Sponge if is_raster_paint_tool => { + ui.horizontal(|ui| { + if ui.selectable_label(*shared.sponge_mode == 0, "Saturate").clicked() { + *shared.sponge_mode = 0; + } + if ui.selectable_label(*shared.sponge_mode == 1, "Desaturate").clicked() { + *shared.sponge_mode = 1; + } + }); + ui.horizontal(|ui| { + ui.label("Size:"); + ui.add(egui::Slider::new(shared.sponge_radius, 1.0_f32..=500.0).logarithmic(true).suffix(" px")); + }); + ui.horizontal(|ui| { + ui.label("Flow:"); + ui.add(egui::Slider::new(shared.sponge_flow, 0.0_f32..=1.0) + .custom_formatter(|v, _| format!("{:.0}%", v * 100.0))); + }); + ui.horizontal(|ui| { + ui.label("Hardness:"); + ui.add(egui::Slider::new(shared.sponge_hardness, 0.0_f32..=1.0) + .custom_formatter(|v, _| format!("{:.0}%", v * 100.0))); + }); + ui.horizontal(|ui| { + ui.label("Spacing:"); + ui.add(egui::Slider::new(shared.sponge_spacing, 0.5_f32..=20.0) + .logarithmic(true) + .custom_formatter(|v, _| format!("{:.1}", v))); + }); + } + Tool::Smudge if is_raster_paint_tool => { ui.horizontal(|ui| { ui.label("Size:"); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index 1951793..10b77ca 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -219,6 +219,13 @@ pub struct SharedPaneState<'a> { pub dodge_burn_exposure: &'a mut f32, /// 0 = dodge (lighten), 1 = burn (darken) pub dodge_burn_mode: &'a mut u32, + /// Sponge tool settings + pub sponge_radius: &'a mut f32, + pub sponge_hardness: &'a mut f32, + pub sponge_spacing: &'a mut f32, + pub sponge_flow: &'a mut f32, + /// 0 = saturate, 1 = desaturate + pub sponge_mode: &'a mut u32, /// 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/shaders/brush_dab.wgsl b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/brush_dab.wgsl index 8b6e6c6..d324b3a 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/brush_dab.wgsl +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/brush_dab.wgsl @@ -260,6 +260,24 @@ fn apply_dab(current: vec4, dab: GpuDab, px: i32, py: i32) -> vec4 { adjusted = pow(rgb, vec3(1.0 + s)); } return vec4(clamp(adjusted, vec3(0.0), vec3(1.0)), current.a); + } else if dab.blend_mode == 7u { + // Sponge: saturate or desaturate existing pixels. + // color_r: 0.0 = saturate, 1.0 = desaturate + // Computes luminance, then moves RGB toward (desaturate) or away from (saturate) it. + let s = opa_weight * dab.opacity; + if s <= 0.0 { return current; } + + let luma = dot(current.rgb, vec3(0.2126, 0.7152, 0.0722)); + let luma_vec = vec3(luma); + var adjusted: vec3; + if dab.color_r < 0.5 { + // Saturate: push RGB away from luma (increase chroma) + adjusted = clamp(current.rgb + s * (current.rgb - luma_vec), vec3(0.0), vec3(1.0)); + } else { + // Desaturate: blend RGB toward luma + adjusted = mix(current.rgb, luma_vec, s); + } + return vec4(adjusted, current.a); } else { return current; } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 50128c3..ee733c4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -618,6 +618,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback { pattern_type: 0, pattern_scale: 32.0, dodge_burn_mode: 0, + sponge_mode: 0, points: vec![ StrokePoint { x: x0, y: y_lo, pressure: 1.0, tilt_x: 0.0, tilt_y: 0.0, timestamp: 0.0 }, StrokePoint { x: mid_x, y: mid_y, pressure: 1.0, tilt_x: 0.0, tilt_y: 0.0, timestamp: 0.0 }, @@ -4893,6 +4894,13 @@ impl StagePane { *shared.dodge_burn_hardness, *shared.dodge_burn_spacing, ), + RasterBlendMode::Sponge => ( + lightningbeam_core::brush_settings::BrushSettings::default(), + *shared.sponge_radius, + *shared.sponge_flow, + *shared.sponge_hardness, + *shared.sponge_spacing, + ), _ => ( shared.active_brush_settings.clone(), *shared.brush_radius, @@ -4987,6 +4995,7 @@ impl StagePane { pattern_type: *shared.pattern_type, pattern_scale: *shared.pattern_scale, dodge_burn_mode: *shared.dodge_burn_mode, + sponge_mode: *shared.sponge_mode, points: vec![first_pt.clone()], }; let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0); @@ -5079,6 +5088,7 @@ impl StagePane { pattern_type: *shared.pattern_type, pattern_scale: *shared.pattern_scale, dodge_burn_mode: *shared.dodge_burn_mode, + sponge_mode: *shared.sponge_mode, points: vec![first_pt.clone()], }; let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0); @@ -5167,6 +5177,7 @@ impl StagePane { pattern_type: *shared.pattern_type, pattern_scale: *shared.pattern_scale, dodge_burn_mode: *shared.dodge_burn_mode, + sponge_mode: *shared.sponge_mode, points: vec![prev_pt, curr_local], }; let current_time = ui.input(|i| i.time); @@ -5232,6 +5243,7 @@ impl StagePane { pattern_type: *shared.pattern_type, pattern_scale: *shared.pattern_scale, dodge_burn_mode: *shared.dodge_burn_mode, + sponge_mode: *shared.sponge_mode, points: vec![pt], }; let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, stroke_state, dt); @@ -7601,6 +7613,9 @@ impl StagePane { Tool::DodgeBurn => { self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::DodgeBurn, shared); } + Tool::Sponge => { + self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Sponge, shared); + } Tool::SelectLasso => { self.handle_raster_lasso_tool(ui, &response, world_pos, shared); } @@ -8080,9 +8095,9 @@ impl StagePane { let (a_world, b_world, dab_angle_rad) = match *shared.selected_tool { Tool::Erase => (*shared.eraser_radius, *shared.eraser_radius, 0.0_f32), Tool::Smudge - | Tool::BlurSharpen - | Tool::Sponge => (*shared.smudge_radius, *shared.smudge_radius, 0.0_f32), + | Tool::BlurSharpen => (*shared.smudge_radius, *shared.smudge_radius, 0.0_f32), Tool::DodgeBurn => (*shared.dodge_burn_radius, *shared.dodge_burn_radius, 0.0_f32), + Tool::Sponge => (*shared.sponge_radius, *shared.sponge_radius, 0.0_f32), _ => { let bs = &shared.active_brush_settings; let r = *shared.brush_radius;