Add blur/sharpen tool

This commit is contained in:
Skyler Lehmkuhl 2026-03-06 11:47:10 -05:00
parent 922e8f78b6
commit e7641edd0d
6 changed files with 137 additions and 1 deletions

View File

@ -307,6 +307,7 @@ impl BrushEngine {
RasterBlendMode::PatternStamp => 5u32, RasterBlendMode::PatternStamp => 5u32,
RasterBlendMode::DodgeBurn => 6u32, RasterBlendMode::DodgeBurn => 6u32,
RasterBlendMode::Sponge => 7u32, RasterBlendMode::Sponge => 7u32,
RasterBlendMode::BlurSharpen => 8u32,
}; };
let push_dab = |dabs: &mut Vec<GpuDab>, let push_dab = |dabs: &mut Vec<GpuDab>,
@ -371,6 +372,8 @@ impl BrushEngine {
(cr, cg, cb, tp[0], tp[1]), (cr, cg, cb, tp[0], tp[1]),
RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge => RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge =>
(tp[0], 0.0, 0.0, 0.0, 0.0), (tp[0], 0.0, 0.0, 0.0, 0.0),
RasterBlendMode::BlurSharpen =>
(tp[0], 0.0, 0.0, tp[1], 0.0),
_ => (cr, cg, cb, 0.0, 0.0), _ => (cr, cg, cb, 0.0, 0.0),
}; };
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2, push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,
@ -494,6 +497,8 @@ impl BrushEngine {
(cr, cg, cb, tp[0], tp[1]), (cr, cg, cb, tp[0], tp[1]),
RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge => RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge =>
(tp[0], 0.0, 0.0, 0.0, 0.0), (tp[0], 0.0, 0.0, 0.0, 0.0),
RasterBlendMode::BlurSharpen =>
(tp[0], 0.0, 0.0, tp[1], 0.0),
_ => (cr, cg, cb, 0.0, 0.0), _ => (cr, cg, cb, 0.0, 0.0),
}; };
push_dab(&mut dabs, &mut bbox, push_dab(&mut dabs, &mut bbox,
@ -535,6 +540,8 @@ impl BrushEngine {
(cr, cg, cb, tp[0], tp[1]), (cr, cg, cb, tp[0], tp[1]),
RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge => RasterBlendMode::DodgeBurn | RasterBlendMode::Sponge =>
(tp[0], 0.0, 0.0, 0.0, 0.0), (tp[0], 0.0, 0.0, 0.0, 0.0),
RasterBlendMode::BlurSharpen =>
(tp[0], 0.0, 0.0, tp[1], 0.0),
_ => (cr, cg, cb, 0.0, 0.0), _ => (cr, cg, cb, 0.0, 0.0),
}; };
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2, push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,

View File

@ -28,6 +28,8 @@ pub enum RasterBlendMode {
DodgeBurn, DodgeBurn,
/// Sponge: saturate or desaturate existing pixels /// Sponge: saturate or desaturate existing pixels
Sponge, Sponge,
/// Blur / Sharpen: soften or crisp up existing pixels
BlurSharpen,
} }
impl Default for RasterBlendMode { impl Default for RasterBlendMode {
@ -41,7 +43,7 @@ impl RasterBlendMode {
/// use the brush color at all (clone, heal, dodge/burn, sponge). /// use the brush color at all (clone, heal, dodge/burn, sponge).
/// Used by brush_engine.rs to decide whether color_a should be 1.0 or stroke.color[3]. /// Used by brush_engine.rs to decide whether color_a should be 1.0 or stroke.color[3].
pub fn uses_brush_color(self) -> bool { pub fn uses_brush_color(self) -> bool {
!matches!(self, Self::CloneStamp | Self::Healing | Self::DodgeBurn | Self::Sponge) !matches!(self, Self::CloneStamp | Self::Healing | Self::DodgeBurn | Self::Sponge | Self::BlurSharpen)
} }
} }

View File

@ -278,6 +278,52 @@ fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> {
adjusted = mix(current.rgb, luma_vec, s); adjusted = mix(current.rgb, luma_vec, s);
} }
return vec4<f32>(adjusted, current.a); return vec4<f32>(adjusted, current.a);
} else if dab.blend_mode == 8u {
// Blur / Sharpen: 5×5 separable Gaussian kernel.
// color_r: 0.0 = blur, 1.0 = sharpen
// ndx: kernel radius in canvas pixels (> 0)
//
// Samples are placed on a grid at ±step and ±2*step per axis, where step = kr/2.
// Weights are exp(-x²/2σ²) with σ = step, factored as a separable product.
// This gives a true Gaussian falloff rather than a flat ring, so edges blend
// into a smooth gradient rather than a flat averaged zone.
let s = opa_weight * dab.opacity;
if s <= 0.0 { return current; }
let kr = max(dab.ndx, 1.0);
let cx2 = f32(px) + 0.5;
let cy2 = f32(py) + 0.5;
let step = kr * 0.5;
// 1-D Gaussian weights at distances 0, ±step, ±2*step (σ = step):
// exp(0) = 1.0, exp(-0.5) 0.6065, exp(-2.0) 0.1353
var gauss = array<f32, 5>(0.1353, 0.6065, 1.0, 0.6065, 0.1353);
var blur_sum = vec4<f32>(0.0);
var blur_w = 0.0;
for (var iy = 0; iy < 5; iy++) {
for (var ix = 0; ix < 5; ix++) {
let w = gauss[ix] * gauss[iy];
let spx = cx2 + (f32(ix) - 2.0) * step;
let spy = cy2 + (f32(iy) - 2.0) * step;
blur_sum += bilinear_sample(spx, spy) * w;
blur_w += w;
}
}
let blurred = blur_sum / blur_w;
let c = textureLoad(canvas_src, vec2<i32>(px, py), 0);
var result: vec4<f32>;
if dab.color_r < 0.5 {
// Blur: blend current toward the Gaussian-weighted local average.
result = mix(current, blurred, s);
} else {
// Sharpen: unsharp mask push pixel away from the local average.
// sharpened = 2*src - blurred highlights diverge, shadows diverge.
let sharpened = clamp(c * 2.0 - blurred, vec4<f32>(0.0), vec4<f32>(1.0));
result = mix(current, sharpened, s);
}
return result;
} else { } else {
return current; return current;
} }

View File

@ -4904,6 +4904,10 @@ impl StagePane {
// smudge_dist = radius * exp(smudge_radius_log), so log(strength) gives the ratio. // smudge_dist = radius * exp(smudge_radius_log), so log(strength) gives the ratio.
b.smudge_radius_log = shared.raster_settings.smudge_strength; // linear [0,1] strength b.smudge_radius_log = shared.raster_settings.smudge_strength; // linear [0,1] strength
} }
if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::BlurSharpen) {
// Zero dabs_per_actual_radius so the spacing slider is the sole density control.
b.dabs_per_actual_radius = 0.0;
}
b b
}; };

View File

@ -0,0 +1,60 @@
use super::{BrushParams, RasterToolDef, RasterToolSettings};
use eframe::egui;
use lightningbeam_core::{brush_settings::BrushSettings, raster_layer::RasterBlendMode};
pub struct BlurSharpenTool;
pub static BLUR_SHARPEN: BlurSharpenTool = BlurSharpenTool;
impl RasterToolDef for BlurSharpenTool {
fn blend_mode(&self) -> RasterBlendMode { RasterBlendMode::BlurSharpen }
fn header_label(&self) -> &'static str { "Blur / Sharpen" }
fn brush_params(&self, s: &RasterToolSettings) -> BrushParams {
BrushParams {
base_settings: BrushSettings::default(),
radius: s.blur_sharpen_radius,
opacity: s.blur_sharpen_strength,
hardness: s.blur_sharpen_hardness,
spacing: s.blur_sharpen_spacing,
}
}
fn tool_params(&self, s: &RasterToolSettings) -> [f32; 4] {
[s.blur_sharpen_mode as f32, s.blur_sharpen_kernel, 0.0, 0.0]
}
fn show_brush_preset_picker(&self) -> bool { false }
fn render_ui(&self, ui: &mut egui::Ui, s: &mut RasterToolSettings) {
ui.horizontal(|ui| {
if ui.selectable_label(s.blur_sharpen_mode == 0, "Blur").clicked() {
s.blur_sharpen_mode = 0;
}
if ui.selectable_label(s.blur_sharpen_mode == 1, "Sharpen").clicked() {
s.blur_sharpen_mode = 1;
}
});
ui.horizontal(|ui| {
ui.label("Size:");
ui.add(egui::Slider::new(&mut s.blur_sharpen_radius, 1.0_f32..=500.0).logarithmic(true).suffix(" px"));
});
ui.horizontal(|ui| {
ui.label("Strength:");
ui.add(egui::Slider::new(&mut s.blur_sharpen_strength, 0.0_f32..=1.0)
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
});
ui.horizontal(|ui| {
ui.label("Hardness:");
ui.add(egui::Slider::new(&mut s.blur_sharpen_hardness, 0.0_f32..=1.0)
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
});
ui.horizontal(|ui| {
ui.label("Kernel:");
ui.add(egui::Slider::new(&mut s.blur_sharpen_kernel, 1.0_f32..=20.0)
.logarithmic(true)
.custom_formatter(|v, _| format!("{:.1} px", v)));
});
ui.horizontal(|ui| {
ui.label("Spacing:");
ui.add(egui::Slider::new(&mut s.blur_sharpen_spacing, 0.5_f32..=20.0)
.logarithmic(true)
.custom_formatter(|v, _| format!("{:.1}", v)));
});
}
}

View File

@ -20,6 +20,7 @@ pub mod healing_brush;
pub mod pattern_stamp; pub mod pattern_stamp;
pub mod dodge_burn; pub mod dodge_burn;
pub mod sponge; pub mod sponge;
pub mod blur_sharpen;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Shared settings struct (replaces 20+ individual SharedPaneState / EditorApp fields) // Shared settings struct (replaces 20+ individual SharedPaneState / EditorApp fields)
@ -67,6 +68,15 @@ pub struct RasterToolSettings {
pub sponge_flow: f32, pub sponge_flow: f32,
/// 0 = saturate, 1 = desaturate /// 0 = saturate, 1 = desaturate
pub sponge_mode: u32, pub sponge_mode: u32,
// --- Blur / Sharpen ---
pub blur_sharpen_radius: f32,
pub blur_sharpen_hardness: f32,
pub blur_sharpen_spacing: f32,
pub blur_sharpen_strength: f32,
/// Neighborhood kernel radius in canvas pixels (120)
pub blur_sharpen_kernel: f32,
/// 0 = blur, 1 = sharpen
pub blur_sharpen_mode: u32,
} }
impl Default for RasterToolSettings { impl Default for RasterToolSettings {
@ -104,6 +114,12 @@ impl Default for RasterToolSettings {
sponge_spacing: 3.0, sponge_spacing: 3.0,
sponge_flow: 0.5, sponge_flow: 0.5,
sponge_mode: 0, sponge_mode: 0,
blur_sharpen_radius: 30.0,
blur_sharpen_hardness: 0.5,
blur_sharpen_spacing: 3.0,
blur_sharpen_strength: 0.5,
blur_sharpen_kernel: 5.0,
blur_sharpen_mode: 0,
} }
} }
} }
@ -158,6 +174,7 @@ pub fn raster_tool_def(tool: &Tool) -> Option<&'static dyn RasterToolDef> {
Tool::PatternStamp => Some(&pattern_stamp::PATTERN_STAMP), Tool::PatternStamp => Some(&pattern_stamp::PATTERN_STAMP),
Tool::DodgeBurn => Some(&dodge_burn::DODGE_BURN), Tool::DodgeBurn => Some(&dodge_burn::DODGE_BURN),
Tool::Sponge => Some(&sponge::SPONGE), Tool::Sponge => Some(&sponge::SPONGE),
Tool::BlurSharpen => Some(&blur_sharpen::BLUR_SHARPEN),
_ => None, _ => None,
} }
} }