Add pattern stamp tool
This commit is contained in:
parent
1d9d702a59
commit
7d55443b2a
|
|
@ -303,7 +303,8 @@ impl BrushEngine {
|
|||
RasterBlendMode::Erase => 1u32,
|
||||
RasterBlendMode::Smudge => 2u32,
|
||||
RasterBlendMode::CloneStamp => 3u32,
|
||||
RasterBlendMode::Healing => 4u32,
|
||||
RasterBlendMode::Healing => 4u32,
|
||||
RasterBlendMode::PatternStamp => 5u32,
|
||||
};
|
||||
|
||||
let push_dab = |dabs: &mut Vec<GpuDab>,
|
||||
|
|
@ -360,15 +361,18 @@ impl BrushEngine {
|
|||
state, bs, pt.x, pt.y, base_r, pt.pressure, stroke.color,
|
||||
);
|
||||
if !matches!(base_blend, RasterBlendMode::Smudge) {
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp | RasterBlendMode::Healing) {
|
||||
let (cr2, cg2, cb2, ndx2, ndy2) = if matches!(base_blend, RasterBlendMode::CloneStamp | RasterBlendMode::Healing) {
|
||||
// Store offset in color_r/color_g; shader adds it per-pixel.
|
||||
let (ox, oy) = stroke.clone_src_offset.unwrap_or((0.0, 0.0));
|
||||
(ox, oy, 0.0)
|
||||
(ox, oy, 0.0, 0.0, 0.0)
|
||||
} else if matches!(base_blend, RasterBlendMode::PatternStamp) {
|
||||
// ndx = pattern_type, ndy = pattern_scale
|
||||
(cr, cg, cb, stroke.pattern_type as f32, stroke.pattern_scale)
|
||||
} else {
|
||||
(cr, cg, cb)
|
||||
(cr, cg, cb, 0.0, 0.0)
|
||||
};
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,
|
||||
0.0, 0.0, 0.0);
|
||||
ndx2, ndy2, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -486,6 +490,11 @@ impl BrushEngine {
|
|||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, ox, oy, 0.0,
|
||||
0.0, 0.0, 0.0);
|
||||
} else if matches!(base_blend, RasterBlendMode::PatternStamp) {
|
||||
// ndx = pattern_type, ndy = pattern_scale
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||
stroke.pattern_type as f32, stroke.pattern_scale, 0.0);
|
||||
} else {
|
||||
push_dab(&mut dabs, &mut bbox,
|
||||
ex, ey, radius2, opacity2, cr, cg, cb,
|
||||
|
|
@ -518,15 +527,18 @@ impl BrushEngine {
|
|||
last_smooth_x, last_smooth_y,
|
||||
base_r, last_pressure, stroke.color,
|
||||
);
|
||||
let (cr2, cg2, cb2) = if matches!(base_blend, RasterBlendMode::CloneStamp | RasterBlendMode::Healing) {
|
||||
let (cr2, cg2, cb2, ndx2, ndy2) = if matches!(base_blend, RasterBlendMode::CloneStamp | RasterBlendMode::Healing) {
|
||||
// Store offset in color_r/color_g; shader adds it per-pixel.
|
||||
let (ox, oy) = stroke.clone_src_offset.unwrap_or((0.0, 0.0));
|
||||
(ox, oy, 0.0)
|
||||
(ox, oy, 0.0, 0.0, 0.0)
|
||||
} else if matches!(base_blend, RasterBlendMode::PatternStamp) {
|
||||
// ndx = pattern_type, ndy = pattern_scale
|
||||
(cr, cg, cb, stroke.pattern_type as f32, stroke.pattern_scale)
|
||||
} else {
|
||||
(cr, cg, cb)
|
||||
(cr, cg, cb, 0.0, 0.0)
|
||||
};
|
||||
push_dab(&mut dabs, &mut bbox, ex, ey, r, o, cr2, cg2, cb2,
|
||||
0.0, 0.0, 0.0);
|
||||
ndx2, ndy2, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ pub enum RasterBlendMode {
|
|||
CloneStamp,
|
||||
/// Healing brush: color-corrected clone stamp (preserves source texture, shifts color to match destination)
|
||||
Healing,
|
||||
/// Pattern stamp: paint with a repeating procedural tile pattern
|
||||
PatternStamp,
|
||||
}
|
||||
|
||||
impl Default for RasterBlendMode {
|
||||
|
|
@ -57,9 +59,17 @@ pub struct StrokeRecord {
|
|||
/// None for all non-clone-stamp blend modes.
|
||||
#[serde(default)]
|
||||
pub clone_src_offset: Option<(f32, f32)>,
|
||||
/// Pattern stamp: procedural pattern type (0=Checkerboard, 1=Dots, 2=H-Lines, 3=V-Lines, 4=Diagonal, 5=Crosshatch)
|
||||
#[serde(default)]
|
||||
pub pattern_type: u32,
|
||||
/// Pattern stamp: tile size in pixels
|
||||
#[serde(default = "default_pattern_scale")]
|
||||
pub pattern_scale: f32,
|
||||
pub points: Vec<StrokePoint>,
|
||||
}
|
||||
|
||||
fn default_pattern_scale() -> f32 { 32.0 }
|
||||
|
||||
/// Specifies how the raster content transitions to the next keyframe
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TweenType {
|
||||
|
|
|
|||
|
|
@ -803,6 +803,9 @@ struct EditorApp {
|
|||
smudge_hardness: f32,
|
||||
smudge_spacing: f32,
|
||||
smudge_strength: f32,
|
||||
/// Pattern stamp settings
|
||||
pattern_type: u32,
|
||||
pattern_scale: 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
|
||||
|
|
@ -1102,6 +1105,8 @@ impl EditorApp {
|
|||
smudge_hardness: 0.8,
|
||||
smudge_spacing: 8.0,
|
||||
smudge_strength: 1.0,
|
||||
pattern_type: 0,
|
||||
pattern_scale: 32.0,
|
||||
brush_preview_pixels: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
||||
audio_stream,
|
||||
audio_controller,
|
||||
|
|
@ -5570,6 +5575,8 @@ impl eframe::App for EditorApp {
|
|||
smudge_hardness: &mut self.smudge_hardness,
|
||||
smudge_spacing: &mut self.smudge_spacing,
|
||||
smudge_strength: &mut self.smudge_strength,
|
||||
pattern_type: &mut self.pattern_type,
|
||||
pattern_scale: &mut self.pattern_scale,
|
||||
audio_controller: self.audio_controller.as_ref(),
|
||||
video_manager: &self.video_manager,
|
||||
playback_time: &mut self.playback_time,
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ impl InfopanelPane {
|
|||
let is_raster_paint_tool = active_is_raster && matches!(
|
||||
tool,
|
||||
Tool::Draw | Tool::Pencil | Tool::Pen | Tool::Airbrush
|
||||
| Tool::Erase | Tool::Smudge | Tool::CloneStamp | Tool::HealingBrush
|
||||
| Tool::Erase | Tool::Smudge | Tool::CloneStamp | Tool::HealingBrush | Tool::PatternStamp
|
||||
);
|
||||
|
||||
// Only show tool options for tools that have options
|
||||
|
|
@ -196,6 +196,7 @@ impl InfopanelPane {
|
|||
Tool::Smudge => "Smudge",
|
||||
Tool::CloneStamp => "Clone Stamp",
|
||||
Tool::HealingBrush => "Healing Brush",
|
||||
Tool::PatternStamp => "Pattern Stamp",
|
||||
_ => "Brush",
|
||||
}
|
||||
} else {
|
||||
|
|
@ -330,6 +331,33 @@ impl InfopanelPane {
|
|||
self.render_raster_tool_options(ui, shared, matches!(tool, Tool::Erase));
|
||||
}
|
||||
|
||||
Tool::PatternStamp if is_raster_paint_tool => {
|
||||
const PATTERN_NAMES: &[&str] = &[
|
||||
"Checkerboard", "Dots", "H-Lines", "V-Lines", "Diagonal \\", "Diagonal /", "Crosshatch",
|
||||
];
|
||||
let selected_name = PATTERN_NAMES
|
||||
.get(*shared.pattern_type as usize)
|
||||
.copied()
|
||||
.unwrap_or("Checkerboard");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Pattern:");
|
||||
egui::ComboBox::from_id_salt("pattern_type")
|
||||
.selected_text(selected_name)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, name) in PATTERN_NAMES.iter().enumerate() {
|
||||
ui.selectable_value(shared.pattern_type, i as u32, *name);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scale:");
|
||||
ui.add(egui::Slider::new(shared.pattern_scale, 4.0_f32..=256.0)
|
||||
.logarithmic(true).suffix(" px"));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
self.render_raster_tool_options(ui, shared, false);
|
||||
}
|
||||
|
||||
Tool::Smudge if is_raster_paint_tool => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Size:");
|
||||
|
|
|
|||
|
|
@ -208,6 +208,10 @@ pub struct SharedPaneState<'a> {
|
|||
pub smudge_hardness: &'a mut f32,
|
||||
pub smudge_spacing: &'a mut f32,
|
||||
pub smudge_strength: &'a mut f32,
|
||||
/// Pattern stamp: selected procedural pattern type (0=Checkerboard..5=Crosshatch)
|
||||
pub pattern_type: &'a mut u32,
|
||||
/// Pattern stamp: tile size in pixels
|
||||
pub pattern_scale: &'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
|
||||
|
|
|
|||
|
|
@ -156,6 +156,49 @@ fn apply_dab(current: vec4<f32>, dab: GpuDab, px: i32, py: i32) -> vec4<f32> {
|
|||
alpha * src.b + ba * current.b,
|
||||
alpha * src.a + ba * current.a,
|
||||
);
|
||||
} else if dab.blend_mode == 5u {
|
||||
// Pattern stamp: procedural tiling pattern using brush color.
|
||||
// ndx = pattern_type (0=Checker, 1=Dots, 2=H-Lines, 3=V-Lines, 4=Diagonal, 5=Crosshatch)
|
||||
// ndy = pattern_scale (tile size in pixels, >= 1.0)
|
||||
let scale = max(dab.ndy, 1.0);
|
||||
let pt = u32(dab.ndx);
|
||||
|
||||
// Fractional position within the tile [0.0, 1.0)
|
||||
let tx = fract(f32(px) / scale);
|
||||
let ty = fract(f32(py) / scale);
|
||||
|
||||
var on: bool;
|
||||
if pt == 0u { // Checkerboard
|
||||
let cx = u32(floor(f32(px) / scale));
|
||||
let cy = u32(floor(f32(py) / scale));
|
||||
on = (cx + cy) % 2u == 0u;
|
||||
} else if pt == 1u { // Polka dots (r ≈ 0.35 of cell radius)
|
||||
let ddx = tx - 0.5; let ddy = ty - 0.5;
|
||||
on = ddx * ddx + ddy * ddy < 0.1225;
|
||||
} else if pt == 2u { // Horizontal lines (50% duty)
|
||||
on = ty < 0.5;
|
||||
} else if pt == 3u { // Vertical lines (50% duty)
|
||||
on = tx < 0.5;
|
||||
} else if pt == 4u { // Diagonal \ (top-left → bottom-right)
|
||||
on = fract((f32(px) + f32(py)) / scale) < 0.5;
|
||||
} else if pt == 5u { // Diagonal / (top-right → bottom-left)
|
||||
on = fract((f32(px) - f32(py)) / scale) < 0.5;
|
||||
} else { // Crosshatch (type 6+)
|
||||
on = tx < 0.4 || ty < 0.4;
|
||||
}
|
||||
|
||||
if !on { return current; }
|
||||
|
||||
// Paint with brush color — same compositing as Normal blend
|
||||
let dab_a = opa_weight * dab.opacity * dab.color_a;
|
||||
if dab_a <= 0.0 { return current; }
|
||||
let ba = 1.0 - dab_a;
|
||||
return vec4<f32>(
|
||||
dab_a * dab.color_r + ba * current.r,
|
||||
dab_a * dab.color_g + ba * current.g,
|
||||
dab_a * dab.color_b + ba * current.b,
|
||||
dab_a + ba * current.a,
|
||||
);
|
||||
} else if dab.blend_mode == 4u {
|
||||
// Healing brush: per-pixel color-corrected clone stamp.
|
||||
// color_r/color_g = source offset (ox, oy), same as clone stamp.
|
||||
|
|
|
|||
|
|
@ -615,6 +615,8 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
|||
color: [0.85f32, 0.88, 1.0, 1.0],
|
||||
blend_mode: RasterBlendMode::Normal,
|
||||
clone_src_offset: None,
|
||||
pattern_type: 0,
|
||||
pattern_scale: 32.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 },
|
||||
|
|
@ -4974,6 +4976,8 @@ impl StagePane {
|
|||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
points: vec![first_pt.clone()],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0);
|
||||
|
|
@ -5063,6 +5067,8 @@ impl StagePane {
|
|||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
points: vec![first_pt.clone()],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, &mut stroke_state, 0.0);
|
||||
|
|
@ -5148,6 +5154,8 @@ impl StagePane {
|
|||
color,
|
||||
blend_mode,
|
||||
clone_src_offset,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
points: vec![prev_pt, curr_local],
|
||||
};
|
||||
let current_time = ui.input(|i| i.time);
|
||||
|
|
@ -5210,6 +5218,8 @@ impl StagePane {
|
|||
color,
|
||||
blend_mode,
|
||||
clone_src_offset: self.clone_stroke_offset,
|
||||
pattern_type: *shared.pattern_type,
|
||||
pattern_scale: *shared.pattern_scale,
|
||||
points: vec![pt],
|
||||
};
|
||||
let (dabs, dab_bbox) = BrushEngine::compute_dabs(&single, stroke_state, dt);
|
||||
|
|
@ -7573,6 +7583,9 @@ impl StagePane {
|
|||
// Alt+click (source-setting) is handled before this block.
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::Healing, shared);
|
||||
}
|
||||
Tool::PatternStamp => {
|
||||
self.handle_raster_stroke_tool(ui, &response, world_pos, lightningbeam_core::raster_layer::RasterBlendMode::PatternStamp, shared);
|
||||
}
|
||||
Tool::SelectLasso => {
|
||||
self.handle_raster_lasso_tool(ui, &response, world_pos, shared);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue