mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-30 04:57:22 -06:00
LR-MASK-BRUSH: Shift-click draws a straight brush stroke from the last one
A Shift-click with the brush paints a straight line from where the last stroke ended, tapering size, feather and flow when they changed in between, as one undo step. mask.brushStroke takes connect: true for it.
This commit is contained in:
@@ -150,6 +150,49 @@ fn masks_edit(s: &mut Session, c: &str, label: &str, f: impl FnOnce(&mut Vec<Mas
|
||||
Ok(json!({"activeMask": active}))
|
||||
}
|
||||
|
||||
/// Most pieces a tapered ⇧-click segment is cut into.
|
||||
const MAX_TAPER_STEPS: usize = 64;
|
||||
|
||||
/// The strokes a ⇧-click adds: `to` drawn on from point `from`, the last point of stroke `last` (a straight segment
|
||||
/// to `to`'s first point, then the rest of `to`). When the brush's size, feather or flow changed since `last`, the
|
||||
/// segment is cut into pieces whose values step from `last`'s to `to`'s, so the line tapers; strokes combine by their
|
||||
/// maximum, so the pieces join without building up.
|
||||
fn connected_strokes(from: Point, last: &BrushStroke, to: BrushStroke) -> Vec<BrushStroke> {
|
||||
let Some(&first) = to.points.first() else { return vec![to] };
|
||||
let same = |a: f64, b: f64| (a - b).abs() <= 1e-9;
|
||||
if same(last.size, to.size) && same(last.feather, to.feather) && same(last.flow, to.flow) {
|
||||
let mut points = Vec::with_capacity(to.points.len() + 1);
|
||||
points.push(from);
|
||||
points.extend(to.points.iter().copied());
|
||||
return vec![BrushStroke { points, ..to }];
|
||||
}
|
||||
// about 5% of size, or 2 points of feather or flow, per piece
|
||||
let ratio = if last.size > 0.0 && to.size > 0.0 { (to.size / last.size).ln().abs() / 1.05f64.ln() } else { 0.0 };
|
||||
let needed = [ratio, (to.feather - last.feather).abs() / 2.0, (to.flow - last.flow).abs() / 2.0]
|
||||
.into_iter()
|
||||
.filter(|v| v.is_finite())
|
||||
.fold(1.0f64, f64::max);
|
||||
let steps = (needed.ceil() as usize).clamp(1, MAX_TAPER_STEPS);
|
||||
let lerp = |a: f64, b: f64, t: f64| a + (b - a) * t;
|
||||
let at = |t: f64| Point::new(lerp(from.x, first.x, t), lerp(from.y, first.y, t));
|
||||
let mut out: Vec<BrushStroke> = (0..steps)
|
||||
.map(|i| {
|
||||
let (t0, t1) = (i as f64 / steps as f64, (i + 1) as f64 / steps as f64);
|
||||
BrushStroke {
|
||||
points: vec![at(t0), at(t1)],
|
||||
size: lerp(last.size, to.size, t1),
|
||||
feather: lerp(last.feather, to.feather, t1),
|
||||
flow: lerp(last.flow, to.flow, t1),
|
||||
..to.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if to.points.len() > 1 {
|
||||
out.push(to);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn mask_id(p: &Value, active: Option<u32>, c: &str) -> Result<u32> {
|
||||
p.get("id").and_then(Value::as_u64).map(|v| v as u32).or(active).ok_or_else(|| bad(c, "no mask selected (give `id`)"))
|
||||
}
|
||||
@@ -230,7 +273,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Brush Stroke",
|
||||
[],
|
||||
None,
|
||||
"{id?: maskId, points: [[x,y],…] normalized, size?: fraction of long edge (0.03), feather?, flow?, density?, erase?: bool, autoMask?: bool}",
|
||||
"{id?: maskId, points: [[x,y],…] normalized, size?: fraction of long edge (0.03), feather?, flow?, density?, erase?: bool, autoMask?: bool, connect?: bool — start from the last point of the mask's last stroke (⇧-click), size / feather / flow tapering from that stroke's to these} → {activeMask, connected}",
|
||||
has_active,
|
||||
|s, p| {
|
||||
let pts: Vec<Point> = p
|
||||
@@ -250,9 +293,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
erase: bool_or(p, "erase", false),
|
||||
auto_mask: bool_or(p, "autoMask", false),
|
||||
};
|
||||
let connect = bool_or(p, "connect", false);
|
||||
let mut connected = false;
|
||||
let next = s.active().and_then(|id| s.develop_of(id)).map(|d| d.next_mask_id()).unwrap_or(1);
|
||||
let want = p.get("id").and_then(Value::as_u64).map(|v| v as u32).or(s.active_mask);
|
||||
masks_edit(s, "mask.brushStroke", "Brush", |masks, active| {
|
||||
let mut out = masks_edit(s, "mask.brushStroke", "Brush", |masks, active| {
|
||||
let i = match want.and_then(|m| masks.iter().position(|x| x.id == m)) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
@@ -265,7 +310,20 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
if let Some(MaskComponent { shape: MaskShape::Brush { strokes }, .. }) =
|
||||
m.components.iter_mut().rev().find(|c| matches!(c.shape, MaskShape::Brush { .. }))
|
||||
{
|
||||
strokes.push(stroke);
|
||||
// the previous stroke's end and its brush values (not its points)
|
||||
let anchor = strokes.last().filter(|_| connect).and_then(|l| {
|
||||
Some((
|
||||
*l.points.last()?,
|
||||
BrushStroke { points: Vec::new(), size: l.size, feather: l.feather, flow: l.flow, ..BrushStroke::default() },
|
||||
))
|
||||
});
|
||||
match anchor {
|
||||
Some((from, last)) => {
|
||||
strokes.extend(connected_strokes(from, &last, stroke));
|
||||
connected = true;
|
||||
}
|
||||
None => strokes.push(stroke),
|
||||
}
|
||||
} else {
|
||||
m.components.push(MaskComponent {
|
||||
name: None,
|
||||
@@ -275,7 +333,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
if let Some(o) = out.as_object_mut() {
|
||||
o.insert("connected".into(), json!(connected));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
),
|
||||
cmd!("mask.update", "Update Mask Shape", [], None, "{id?, component?: index (0), shape: MaskShape JSON}", has_active, |s, p| {
|
||||
|
||||
Reference in New Issue
Block a user