separate brush and eraser in infopanel
This commit is contained in:
parent
f2c15d7f0d
commit
553cc383d5
|
|
@ -766,8 +766,19 @@ struct EditorApp {
|
|||
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
|
||||
/// Full brush settings for the currently active preset (carries elliptical, jitter, etc.)
|
||||
/// Full brush settings for the currently active paint preset (carries elliptical, jitter, etc.)
|
||||
active_brush_settings: lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Eraser tool brush settings (separate from paint brush, defaults to "Brush" preset)
|
||||
eraser_radius: f32,
|
||||
eraser_opacity: f32,
|
||||
eraser_hardness: f32,
|
||||
eraser_spacing: f32,
|
||||
active_eraser_settings: lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Smudge tool settings (no preset picker)
|
||||
smudge_radius: f32,
|
||||
smudge_hardness: f32,
|
||||
smudge_spacing: f32,
|
||||
smudge_strength: f32,
|
||||
/// GPU-rendered brush preview pixel buffers, shared with VelloCallback::prepare().
|
||||
brush_preview_pixels: std::sync::Arc<std::sync::Mutex<Vec<(u32, u32, Vec<u8>)>>>,
|
||||
// Audio engine integration
|
||||
|
|
@ -1052,6 +1063,19 @@ impl EditorApp {
|
|||
brush_spacing: 0.1,
|
||||
brush_use_fg: true,
|
||||
active_brush_settings: lightningbeam_core::brush_settings::BrushSettings::default(),
|
||||
eraser_radius: 10.0,
|
||||
eraser_opacity: 1.0,
|
||||
eraser_hardness: 0.5,
|
||||
eraser_spacing: 0.1,
|
||||
active_eraser_settings: lightningbeam_core::brush_settings::bundled_brushes()
|
||||
.iter()
|
||||
.find(|p| p.name == "Brush")
|
||||
.map(|p| p.settings.clone())
|
||||
.unwrap_or_default(),
|
||||
smudge_radius: 15.0,
|
||||
smudge_hardness: 0.8,
|
||||
smudge_spacing: 8.0,
|
||||
smudge_strength: 1.0,
|
||||
brush_preview_pixels: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
||||
audio_stream,
|
||||
audio_controller,
|
||||
|
|
@ -5510,6 +5534,15 @@ impl eframe::App for EditorApp {
|
|||
brush_spacing: &mut self.brush_spacing,
|
||||
brush_use_fg: &mut self.brush_use_fg,
|
||||
active_brush_settings: &mut self.active_brush_settings,
|
||||
eraser_radius: &mut self.eraser_radius,
|
||||
eraser_opacity: &mut self.eraser_opacity,
|
||||
eraser_hardness: &mut self.eraser_hardness,
|
||||
eraser_spacing: &mut self.eraser_spacing,
|
||||
active_eraser_settings: &mut self.active_eraser_settings,
|
||||
smudge_radius: &mut self.smudge_radius,
|
||||
smudge_hardness: &mut self.smudge_hardness,
|
||||
smudge_spacing: &mut self.smudge_spacing,
|
||||
smudge_strength: &mut self.smudge_strength,
|
||||
audio_controller: self.audio_controller.as_ref(),
|
||||
video_manager: &self.video_manager,
|
||||
playback_time: &mut self.playback_time,
|
||||
|
|
|
|||
|
|
@ -26,18 +26,29 @@ pub struct InfopanelPane {
|
|||
tool_section_open: bool,
|
||||
/// Whether the shape properties section is expanded
|
||||
shape_section_open: bool,
|
||||
/// Index of the selected brush preset (None = custom / unset)
|
||||
/// Index of the selected paint brush preset (None = custom / unset)
|
||||
selected_brush_preset: Option<usize>,
|
||||
/// Whether the paint brush picker is expanded
|
||||
brush_picker_expanded: bool,
|
||||
/// Index of the selected eraser brush preset
|
||||
selected_eraser_preset: Option<usize>,
|
||||
/// Whether the eraser brush picker is expanded
|
||||
eraser_picker_expanded: bool,
|
||||
/// Cached preview textures, one per preset (populated lazily).
|
||||
brush_preview_textures: Vec<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
impl InfopanelPane {
|
||||
pub fn new() -> Self {
|
||||
let presets = bundled_brushes();
|
||||
let default_eraser_idx = presets.iter().position(|p| p.name == "Brush");
|
||||
Self {
|
||||
tool_section_open: true,
|
||||
shape_section_open: true,
|
||||
selected_brush_preset: None,
|
||||
brush_picker_expanded: false,
|
||||
selected_eraser_preset: default_eraser_idx,
|
||||
eraser_picker_expanded: false,
|
||||
brush_preview_textures: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -308,52 +319,31 @@ impl InfopanelPane {
|
|||
}
|
||||
|
||||
// Raster paint tools
|
||||
Tool::Draw | Tool::Erase | Tool::Smudge if is_raster_paint_tool => {
|
||||
// Brush preset picker (Draw tool only)
|
||||
if matches!(tool, Tool::Draw) {
|
||||
self.render_brush_preset_grid(ui, shared);
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
Tool::Draw | Tool::Erase if is_raster_paint_tool => {
|
||||
self.render_raster_tool_options(ui, shared, matches!(tool, Tool::Erase));
|
||||
}
|
||||
|
||||
// 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");
|
||||
});
|
||||
}
|
||||
Tool::Smudge if is_raster_paint_tool => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(
|
||||
egui::Slider::new(shared.brush_radius, 1.0_f32..=200.0)
|
||||
.logarithmic(true)
|
||||
.suffix(" px"),
|
||||
);
|
||||
ui.add(egui::Slider::new(shared.smudge_radius, 1.0_f32..=200.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Reach:");
|
||||
ui.add(egui::Slider::new(shared.smudge_strength, 0.1_f32..=5.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.2}x", v)));
|
||||
});
|
||||
if !matches!(tool, Tool::Smudge) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Opacity:");
|
||||
ui.add(
|
||||
egui::Slider::new(shared.brush_opacity, 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.brush_hardness, 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)),
|
||||
);
|
||||
ui.add(egui::Slider::new(shared.smudge_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.brush_spacing, 0.01_f32..=1.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)),
|
||||
);
|
||||
ui.add(egui::Slider::new(shared.smudge_spacing, 0.5_f32..=20.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.1}", v)));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -364,8 +354,56 @@ impl InfopanelPane {
|
|||
});
|
||||
}
|
||||
|
||||
/// Render the brush preset thumbnail grid for the Draw raster tool.
|
||||
fn render_brush_preset_grid(&mut self, ui: &mut Ui, shared: &mut SharedPaneState) {
|
||||
/// Render all options for a raster paint tool (brush picker + sliders).
|
||||
/// `is_eraser` drives which shared state is read/written.
|
||||
fn render_raster_tool_options(
|
||||
&mut self,
|
||||
ui: &mut Ui,
|
||||
shared: &mut SharedPaneState,
|
||||
is_eraser: bool,
|
||||
) {
|
||||
self.render_brush_preset_grid(ui, shared, is_eraser);
|
||||
ui.add_space(2.0);
|
||||
|
||||
if !is_eraser {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Color:");
|
||||
ui.selectable_value(shared.brush_use_fg, true, "FG");
|
||||
ui.selectable_value(shared.brush_use_fg, false, "BG");
|
||||
});
|
||||
}
|
||||
|
||||
macro_rules! field {
|
||||
($eraser:ident, $brush:ident) => {
|
||||
if is_eraser { &mut *shared.$eraser } else { &mut *shared.$brush }
|
||||
}
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
ui.add(egui::Slider::new(field!(eraser_radius, brush_radius), 1.0_f32..=200.0).logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Opacity:");
|
||||
ui.add(egui::Slider::new(field!(eraser_opacity, brush_opacity), 0.0_f32..=1.0)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Hardness:");
|
||||
ui.add(egui::Slider::new(field!(eraser_hardness, brush_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(field!(eraser_spacing, brush_spacing), 0.01_f32..=1.0)
|
||||
.logarithmic(true)
|
||||
.custom_formatter(|v, _| format!("{:.0}%", v * 100.0)));
|
||||
});
|
||||
}
|
||||
|
||||
/// Render the brush preset thumbnail grid (collapsible).
|
||||
/// `is_eraser` drives which picker state and which shared settings are updated.
|
||||
fn render_brush_preset_grid(&mut self, ui: &mut Ui, shared: &mut SharedPaneState, is_eraser: bool) {
|
||||
let presets = bundled_brushes();
|
||||
if presets.is_empty() { return; }
|
||||
|
||||
|
|
@ -390,80 +428,107 @@ impl InfopanelPane {
|
|||
}
|
||||
}
|
||||
|
||||
// Read picker state into locals to avoid multiple &mut self borrows.
|
||||
let mut expanded = if is_eraser { self.eraser_picker_expanded } else { self.brush_picker_expanded };
|
||||
let mut selected = if is_eraser { self.selected_eraser_preset } else { self.selected_brush_preset };
|
||||
|
||||
let gap = 3.0;
|
||||
let cols = 2usize;
|
||||
let cell_w = ((ui.available_width() - gap * (cols as f32 - 1.0)) / cols as f32).max(50.0);
|
||||
let avail_w = ui.available_width();
|
||||
let cell_w = ((avail_w - gap * (cols as f32 - 1.0)) / cols as f32).max(50.0);
|
||||
let cell_h = 80.0;
|
||||
|
||||
for (row_idx, chunk) in presets.chunks(cols).enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = gap;
|
||||
for (col_idx, preset) in chunk.iter().enumerate() {
|
||||
let idx = row_idx * cols + col_idx;
|
||||
let is_selected = self.selected_brush_preset == Some(idx);
|
||||
|
||||
let (rect, resp) = ui.allocate_exact_size(
|
||||
egui::vec2(cell_w, cell_h),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
let bg = if is_selected {
|
||||
egui::Color32::from_rgb(45, 65, 95)
|
||||
} else if resp.hovered() {
|
||||
egui::Color32::from_rgb(45, 50, 62)
|
||||
} else {
|
||||
egui::Color32::from_rgb(32, 36, 44)
|
||||
};
|
||||
painter.rect_filled(rect, 4.0, bg);
|
||||
if is_selected {
|
||||
painter.rect_stroke(
|
||||
rect, 4.0,
|
||||
egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 140, 220)),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
|
||||
// Dab preview (upper portion, leaving 18 px for the name)
|
||||
let preview_rect = egui::Rect::from_min_size(
|
||||
rect.min + egui::vec2(4.0, 4.0),
|
||||
egui::vec2(cell_w - 8.0, cell_h - 22.0),
|
||||
);
|
||||
if let Some(tex) = self.brush_preview_textures.get(idx) {
|
||||
painter.image(
|
||||
tex.id(), preview_rect,
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
egui::Color32::WHITE,
|
||||
);
|
||||
}
|
||||
|
||||
// Name
|
||||
painter.text(
|
||||
egui::pos2(rect.center().x, rect.max.y - 9.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
preset.name,
|
||||
egui::FontId::proportional(9.5),
|
||||
if is_selected {
|
||||
egui::Color32::from_rgb(140, 190, 255)
|
||||
} else {
|
||||
egui::Color32::from_gray(160)
|
||||
},
|
||||
);
|
||||
|
||||
if resp.clicked() {
|
||||
self.selected_brush_preset = Some(idx);
|
||||
let s = &preset.settings;
|
||||
// Size is intentionally NOT reset — it is global and persists
|
||||
// across preset switches. All other parameters load from preset.
|
||||
*shared.brush_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
*shared.brush_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
*shared.brush_spacing = s.dabs_per_radius;
|
||||
*shared.active_brush_settings = s.clone();
|
||||
}
|
||||
if !expanded {
|
||||
// Collapsed: show just the currently selected preset as a single wide cell.
|
||||
let show_idx = selected.unwrap_or(0);
|
||||
if let Some(preset) = presets.get(show_idx) {
|
||||
let full_w = avail_w.max(50.0);
|
||||
let (rect, resp) = ui.allocate_exact_size(egui::vec2(full_w, cell_h), egui::Sense::click());
|
||||
let painter = ui.painter();
|
||||
let bg = if resp.hovered() {
|
||||
egui::Color32::from_rgb(50, 56, 70)
|
||||
} else {
|
||||
egui::Color32::from_rgb(45, 65, 95)
|
||||
};
|
||||
painter.rect_filled(rect, 4.0, bg);
|
||||
painter.rect_stroke(rect, 4.0, egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 140, 220)), egui::StrokeKind::Middle);
|
||||
let preview_rect = egui::Rect::from_min_size(
|
||||
rect.min + egui::vec2(4.0, 4.0),
|
||||
egui::vec2(rect.width() - 8.0, cell_h - 22.0),
|
||||
);
|
||||
if let Some(tex) = self.brush_preview_textures.get(show_idx) {
|
||||
painter.image(tex.id(), preview_rect,
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
egui::Color32::WHITE);
|
||||
}
|
||||
});
|
||||
ui.add_space(gap);
|
||||
painter.text(egui::pos2(rect.center().x, rect.max.y - 9.0),
|
||||
egui::Align2::CENTER_CENTER, preset.name,
|
||||
egui::FontId::proportional(9.5), egui::Color32::from_rgb(140, 190, 255));
|
||||
if resp.clicked() { expanded = true; }
|
||||
}
|
||||
} else {
|
||||
// Expanded: full grid; clicking a preset selects it and collapses.
|
||||
for (row_idx, chunk) in presets.chunks(cols).enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = gap;
|
||||
for (col_idx, preset) in chunk.iter().enumerate() {
|
||||
let idx = row_idx * cols + col_idx;
|
||||
let is_sel = selected == Some(idx);
|
||||
let (rect, resp) = ui.allocate_exact_size(egui::vec2(cell_w, cell_h), egui::Sense::click());
|
||||
let painter = ui.painter();
|
||||
let bg = if is_sel {
|
||||
egui::Color32::from_rgb(45, 65, 95)
|
||||
} else if resp.hovered() {
|
||||
egui::Color32::from_rgb(45, 50, 62)
|
||||
} else {
|
||||
egui::Color32::from_rgb(32, 36, 44)
|
||||
};
|
||||
painter.rect_filled(rect, 4.0, bg);
|
||||
if is_sel {
|
||||
painter.rect_stroke(rect, 4.0, egui::Stroke::new(1.5, egui::Color32::from_rgb(80, 140, 220)), egui::StrokeKind::Middle);
|
||||
}
|
||||
let preview_rect = egui::Rect::from_min_size(
|
||||
rect.min + egui::vec2(4.0, 4.0),
|
||||
egui::vec2(cell_w - 8.0, cell_h - 22.0),
|
||||
);
|
||||
if let Some(tex) = self.brush_preview_textures.get(idx) {
|
||||
painter.image(tex.id(), preview_rect,
|
||||
egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
|
||||
egui::Color32::WHITE);
|
||||
}
|
||||
painter.text(egui::pos2(rect.center().x, rect.max.y - 9.0),
|
||||
egui::Align2::CENTER_CENTER, preset.name,
|
||||
egui::FontId::proportional(9.5),
|
||||
if is_sel { egui::Color32::from_rgb(140, 190, 255) } else { egui::Color32::from_gray(160) });
|
||||
if resp.clicked() {
|
||||
selected = Some(idx);
|
||||
expanded = false;
|
||||
let s = &preset.settings;
|
||||
if is_eraser {
|
||||
*shared.eraser_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
*shared.eraser_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
*shared.eraser_spacing = s.dabs_per_radius;
|
||||
*shared.active_eraser_settings = s.clone();
|
||||
} else {
|
||||
*shared.brush_opacity = s.opaque.clamp(0.0, 1.0);
|
||||
*shared.brush_hardness = s.hardness.clamp(0.0, 1.0);
|
||||
*shared.brush_spacing = s.dabs_per_radius;
|
||||
*shared.active_brush_settings = s.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(gap);
|
||||
}
|
||||
}
|
||||
|
||||
// Write back picker state.
|
||||
if is_eraser {
|
||||
self.eraser_picker_expanded = expanded;
|
||||
self.selected_eraser_preset = selected;
|
||||
} else {
|
||||
self.brush_picker_expanded = expanded;
|
||||
self.selected_brush_preset = selected;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -187,15 +187,27 @@ pub struct SharedPaneState<'a> {
|
|||
pub draw_simplify_mode: &'a mut lightningbeam_core::tool::SimplifyMode,
|
||||
pub rdp_tolerance: &'a mut f64,
|
||||
pub schneider_max_error: &'a mut f64,
|
||||
/// Raster brush settings
|
||||
/// Raster paint brush settings
|
||||
pub brush_radius: &'a mut f32,
|
||||
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,
|
||||
/// Full brush settings for the active preset (carries elliptical, jitter, slow_tracking, etc.)
|
||||
/// Full brush settings for the active paint preset (carries elliptical, jitter, slow_tracking, etc.)
|
||||
pub active_brush_settings: &'a mut lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Raster eraser brush settings (separate from paint brush)
|
||||
pub eraser_radius: &'a mut f32,
|
||||
pub eraser_opacity: &'a mut f32,
|
||||
pub eraser_hardness: &'a mut f32,
|
||||
pub eraser_spacing: &'a mut f32,
|
||||
/// Full brush settings for the active eraser preset
|
||||
pub active_eraser_settings: &'a mut lightningbeam_core::brush_settings::BrushSettings,
|
||||
/// Raster smudge tool settings (no preset picker)
|
||||
pub smudge_radius: &'a mut f32,
|
||||
pub smudge_hardness: &'a mut f32,
|
||||
pub smudge_spacing: &'a mut f32,
|
||||
pub smudge_strength: &'a mut f32,
|
||||
/// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety)
|
||||
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
|
||||
/// Video manager for video decoding and frame caching
|
||||
|
|
|
|||
|
|
@ -4772,17 +4772,48 @@ impl StagePane {
|
|||
if !is_raster { return; }
|
||||
|
||||
let brush = {
|
||||
// Start from the active preset (carries elliptical ratio/angle, jitter, etc.)
|
||||
// then override the four parameters the user controls via UI sliders.
|
||||
let mut b = shared.active_brush_settings.clone();
|
||||
// Start from the active preset for this tool, then override the
|
||||
// user-controlled slider values.
|
||||
use lightningbeam_core::raster_layer::RasterBlendMode;
|
||||
let (base_settings, radius, opacity, hardness, spacing) = match blend_mode {
|
||||
RasterBlendMode::Erase => (
|
||||
shared.active_eraser_settings.clone(),
|
||||
*shared.eraser_radius,
|
||||
*shared.eraser_opacity,
|
||||
*shared.eraser_hardness,
|
||||
*shared.eraser_spacing,
|
||||
),
|
||||
RasterBlendMode::Smudge => (
|
||||
lightningbeam_core::brush_settings::BrushSettings::default(),
|
||||
*shared.smudge_radius,
|
||||
1.0, // opacity fixed at 1.0; strength is a separate smudge_dist multiplier
|
||||
*shared.smudge_hardness,
|
||||
*shared.smudge_spacing,
|
||||
),
|
||||
_ => (
|
||||
shared.active_brush_settings.clone(),
|
||||
*shared.brush_radius,
|
||||
*shared.brush_opacity,
|
||||
*shared.brush_hardness,
|
||||
*shared.brush_spacing,
|
||||
),
|
||||
};
|
||||
let mut b = base_settings;
|
||||
// Compensate for pressure_radius_gain so that the UI-chosen radius is the
|
||||
// actual rendered radius at our fixed mouse pressure of 1.0.
|
||||
// radius_at_pressure(1.0) = exp(radius_log + gain × 0.5)
|
||||
// → radius_log = ln(brush_radius) - gain × 0.5
|
||||
b.radius_log = shared.brush_radius.ln() - b.pressure_radius_gain * 0.5;
|
||||
b.hardness = *shared.brush_hardness;
|
||||
b.opaque = *shared.brush_opacity;
|
||||
b.dabs_per_radius = *shared.brush_spacing;
|
||||
// → radius_log = ln(radius) - gain × 0.5
|
||||
b.radius_log = radius.ln() - b.pressure_radius_gain * 0.5;
|
||||
b.hardness = hardness;
|
||||
b.opaque = opacity;
|
||||
b.dabs_per_radius = spacing;
|
||||
if matches!(blend_mode, RasterBlendMode::Smudge) {
|
||||
// Zero dabs_per_actual_radius so the spacing slider is the sole density control.
|
||||
b.dabs_per_actual_radius = 0.0;
|
||||
// strength controls how far behind the stroke to sample (smudge_dist multiplier).
|
||||
// smudge_dist = radius * exp(smudge_radius_log), so log(strength) gives the ratio.
|
||||
b.smudge_radius_log = shared.smudge_strength.ln();
|
||||
}
|
||||
b
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue