Brush: paint with a blend mode (options-bar Mode), wired in the UI
paint.stroke/paint.pencil now honour a "mode" param (normal|multiply|screen|…), and the options-bar Mode dropdown sets the session brush's blend mode (it was decorative). The Eraser is forced to Normal (no blend mode in Photoshop). New brush_blend_mode_multiply test; clippy + wasm green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9dc394dfc8
commit
c89c33c704
|
|
@ -179,17 +179,28 @@ fn stroke_with(s: &mut Session, p: &Value, label: &str, brush: BrushSettings, pt
|
|||
Ok(damage_json(s, dmg))
|
||||
}
|
||||
|
||||
/// Applies the options-bar blend `mode` to a brush. `"mode"` accepts any blend-mode name
|
||||
/// (normal|multiply|screen|…). The Eraser has no blend mode in Photoshop, so it is forced to Normal.
|
||||
fn with_blend_mode(mut b: BrushSettings, p: &Value) -> BrushSettings {
|
||||
if b.erase {
|
||||
b.mode = photocraft_color::BlendMode::Normal;
|
||||
} else if let Some(m) = p.get("mode").and_then(Value::as_str).and_then(crate::commands::blend_from_str) {
|
||||
b.mode = m;
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// `paint.stroke`: the Brush (and Eraser) tool.
|
||||
pub fn paint_stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pts = parse_points(p, "paint.stroke")?;
|
||||
let brush = resolve_brush(s, p, "paint.stroke")?;
|
||||
let brush = with_blend_mode(resolve_brush(s, p, "paint.stroke")?, p);
|
||||
let label = if brush.erase { "Eraser" } else { "Brush Tool" };
|
||||
stroke_with(s, p, label, brush, pts, false)
|
||||
}
|
||||
|
||||
fn pencil(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let pts = parse_points(p, "paint.pencil")?;
|
||||
let mut brush = resolve_brush(s, p, "paint.pencil")?;
|
||||
let mut brush = with_blend_mode(resolve_brush(s, p, "paint.pencil")?, p);
|
||||
brush.aliased = true;
|
||||
if num(p, "hardness").is_none() {
|
||||
brush.hardness = 1.0;
|
||||
|
|
@ -423,7 +434,7 @@ macro_rules! spec {
|
|||
/// Brush command specs.
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
spec!("paint.pencil", "Pencil", r##"{"points":[[x,y,pressure?,tiltX?,tiltY?,rotation?,timeMs?,wheel?],…],"brush":{…}?,"preset":name?,"size":px?,"opacity":0..1?,"color":"#rrggbb"?=foreground,"erase":bool?,"autoErase":bool=false,"seed":u64?,"target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target}"##, has_paintable, pencil, true),
|
||||
spec!("paint.pencil", "Pencil", r##"{"points":[[x,y,pressure?,tiltX?,tiltY?,rotation?,timeMs?,wheel?],…],"brush":{…}?,"preset":name?,"size":px?,"opacity":0..1?,"color":"#rrggbb"?=foreground,"mode":"normal|multiply|screen|…"="normal","erase":bool?,"autoErase":bool=false,"seed":u64?,"target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target}"##, has_paintable, pencil, true),
|
||||
spec!("paint.mixerBrush", "Mixer Brush", r##"{"points":[…],"brush":{…}?,"preset":name?,"size":px?,"wet":0..100=50,"load":0..100=50,"mix":0..100=50,"flow":0..100=100,"color":"#rrggbb"?=foreground,"sampleAllLayers":bool=false,"cleanAfterStroke":bool=true,"loadAfterStroke":bool=true,"seed":u64?}"##, has_paintable, mixer_brush, true),
|
||||
spec!("paint.colorReplacement", "Color Replacement", r##"{"points":[…],"brush":{…}?,"size":px?,"mode":"hue|saturation|color|luminosity"="color","sampling":"continuous|once|backgroundSwatch"="continuous","limits":"contiguous|discontiguous|findEdges"="contiguous","tolerance":0..100=30,"antiAlias":bool=true,"color":"#rrggbb"?=foreground,"seed":u64?}"##, crate::commands::has_paintable, color_replacement, true),
|
||||
spec!("brush.presets.list", "List Brush Presets", r##"{"full":bool=false}"##, always, presets_list, false),
|
||||
|
|
|
|||
|
|
@ -203,3 +203,26 @@ fn works_on_cmyk_and_16_bit() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn brush_blend_mode_multiply() {
|
||||
// Painting with a blend mode (options-bar "Mode"): blue × yellow = black (Multiply).
|
||||
let fill = |s: &mut Session| {
|
||||
s.edit("bg", |doc, a| {
|
||||
doc.layer_mut(a.unwrap()).unwrap().surface_mut().unwrap().fill_rect(Rect::new(0, 0, 40, 40), &[1.0, 1.0, 0.0, 1.0]);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
};
|
||||
let mut s = session(40, 40);
|
||||
fill(&mut s);
|
||||
s.execute("paint.stroke", json!({"points": [[5, 20], [35, 20]], "size": 12, "color": "#0000ff", "mode": "multiply", "brush": {"hardness": 1.0}})).unwrap();
|
||||
let c = rgba(&s, 20, 20);
|
||||
assert!(c[0] < 0.1 && c[1] < 0.1 && c[2] < 0.1, "multiply blue×yellow ≈ black: {c:?}");
|
||||
// Normal mode paints opaque blue at the same spot.
|
||||
let mut s2 = session(40, 40);
|
||||
fill(&mut s2);
|
||||
s2.execute("paint.stroke", json!({"points": [[5, 20], [35, 20]], "size": 12, "color": "#0000ff", "brush": {"hardness": 1.0}})).unwrap();
|
||||
let n = rgba(&s2, 20, 20);
|
||||
assert!(n[2] > 0.9 && n[0] < 0.1, "normal paints blue: {n:?}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -687,7 +687,7 @@ fn build() -> Vec<CommandSpec> {
|
|||
destructive_adjust(s, "Desaturate", Adjustment::HueSaturation { hue: 0.0, saturation: -100.0, lightness: 0.0, colorize: false }, &Value::Null)
|
||||
}),
|
||||
// Paint
|
||||
cmd!("paint.stroke", "Brush Stroke", [], None, r##"{"points":[[x,y,pressure?,tiltX?,tiltY?,rotation?,timeMs?,wheel?],…],"brush":{…BrushSettings}?,"preset":name?,"size":px?,"hardness":0..1?,"opacity":0..1?,"flow":0..1?,"spacing":0..10?,"color":"#rrggbb"?=foreground,"erase":bool?,"smoothing":0..1?,"zoom":number=1,"seed":u64?,"target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target}"##, has_paintable, crate::brush_cmds::paint_stroke),
|
||||
cmd!("paint.stroke", "Brush Stroke", [], None, r##"{"points":[[x,y,pressure?,tiltX?,tiltY?,rotation?,timeMs?,wheel?],…],"brush":{…BrushSettings}?,"preset":name?,"size":px?,"hardness":0..1?,"opacity":0..1?,"flow":0..1?,"spacing":0..10?,"color":"#rrggbb"?=foreground,"mode":"normal|multiply|screen|…"="normal","erase":bool?,"smoothing":0..1?,"zoom":number=1,"seed":u64?,"target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target}"##, has_paintable, crate::brush_cmds::paint_stroke),
|
||||
cmd!("tools.setColors", "Set Colors", [], None, r##"{"foreground":"#rrggbb"?,"background":"#rrggbb"?}"##, always, |s, p| {
|
||||
s.tools.foreground = color_param(p, "foreground", s.tools.foreground);
|
||||
s.tools.background = color_param(p, "background", s.tools.background);
|
||||
|
|
|
|||
|
|
@ -307,9 +307,11 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
brush_preset_chip(ui, b);
|
||||
widgets::vline(ui, 22.0);
|
||||
opt_label(ui, "Mode");
|
||||
let mut mode = BlendMode::Normal;
|
||||
let mut mode = b.mode;
|
||||
let opts: Vec<(BlendMode, &str)> = BlendMode::LAYER_MODES.iter().map(|m| (*m, m.label())).collect();
|
||||
widgets::dropdown(ui, "brush-mode", &mut mode, &opts, 96.0);
|
||||
if widgets::dropdown(ui, "brush-mode", &mut mode, &opts, 96.0) {
|
||||
b.mode = mode;
|
||||
}
|
||||
opt_label(ui, "Opacity");
|
||||
let mut o = b.opacity * 100.0;
|
||||
if widgets::value_field(ui, &mut o, 0.0..=100.0, "%", 62.0).changed() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue