Merge pull request #6820 from dotCooCoo/feat/brushes-ink-wash-library

Brushes: built-in Ink and Wash library with tapered ink, brush pen and wash brushes
This commit is contained in:
BFlat
2026-11-13 02:46:00 +07:00
committed by GitHub
34 changed files with 401 additions and 12 deletions
+73
View File
@@ -170,6 +170,7 @@ pub struct BuiltinBrushLibrary {
/// The built-in brush libraries, in menu order: original brushes generated in code.
pub const BUILTIN: &[BuiltinBrushLibrary] = &[
BuiltinBrushLibrary { id: "art-ribbons", name: "Art Ribbons", make: art_ribbons },
BuiltinBrushLibrary { id: "ink-and-wash", name: "Ink and Wash", make: ink_and_wash },
BuiltinBrushLibrary { id: "calligraphic-nibs", name: "Calligraphic Nibs", make: calligraphic_nibs },
BuiltinBrushLibrary { id: "bristle-brushes", name: "Bristle Brushes", make: bristle_brushes },
BuiltinBrushLibrary { id: "pattern-borders", name: "Pattern Borders", make: pattern_borders },
@@ -268,6 +269,78 @@ fn art_ribbons() -> BrushLibrary {
built("Art Ribbons", doc, brushes)
}
/// A band between the edges y = `top(x)` and y = `bottom(x)` for x in `from`..`to`, in `n` steps.
fn between(from: f64, to: f64, n: usize, top: impl Fn(f64) -> f64, bottom: impl Fn(f64) -> f64) -> PathData {
let xs: Vec<f64> = (0..=n).map(|i| from + (to - from) * i as f64 / n as f64).collect();
let mut pts: Vec<Point> = xs.iter().map(|&x| Point::new(x, top(x))).collect();
pts.extend(xs.iter().rev().map(|&x| Point::new(x, bottom(x))));
PathData::single(SubPath::polyline(&pts, true))
}
/// Path `p` filled with gray `hex` at `opacity`: one layer of a wash.
fn wash(p: PathData, hex: &str, opacity: f32) -> Node {
let mut n = filled(p, Paint::solid(Color::from_hex(hex).unwrap_or(Color::BLACK)));
n.opacity = opacity;
n
}
/// How much of a wash's width is left at x: rounded ends, full width from 8 pt in.
fn wash_ends(x: f64) -> f64 {
let ends = ((x / 8.0).min(1.0) * ((100.0 - x) / 8.0).min(1.0)).max(0.0);
0.4 + 0.6 * ends.sqrt()
}
/// Ink and Wash: Tapered Ink, broad near its start and thinning to a fine tip; Brush Pen, a
/// rounded nib whose size follows the pen pressure; and two washes of translucent grays, Soft Wash
/// with faint edges and Pooling Wash with a darker rim and two light breaks. Black and grays take
/// the stroke color as tints of it.
fn ink_and_wash() -> BrushLibrary {
let tapered = {
let half = |x: f64| 3.0 * (x / 22.0).min(1.0).powf(0.6) * ((100.0 - x) / 78.0).min(1.0).powf(1.4);
black(between(0.0, 100.0, 48, |x| -0.85 * half(x), |x| 1.15 * half(x)))
};
let pen = Brush {
name: "Brush Pen".into(),
kind: BrushKind::Calligraphic(Calligraphic {
angle: 40.0,
roundness: 75.0,
size: 5.0,
variation: [0.0, 0.0, 2.5],
modes: Some([Variation::Fixed, Variation::Fixed, Variation::Pressure]),
}),
};
let soft = {
let top = |x: f64| -(4.5 + 0.5 * (x * 0.21).sin() + 0.3 * (x * 0.57 + 1.3).sin()) * wash_ends(x);
let bottom = |x: f64| (4.5 + 0.45 * (x * 0.26 + 2.1).sin() + 0.3 * (x * 0.69 + 0.4).sin()) * wash_ends(x);
group(vec![
wash(between(0.0, 100.0, 40, top, bottom), "#404040", 0.4),
wash(between(2.0, 98.0, 32, |x| top(x) - 0.7 - 0.3 * (x * 0.43).sin().abs(), |x| top(x) + 0.5), "#404040", 0.2),
wash(between(3.0, 97.0, 32, |x| bottom(x) - 0.5, |x| bottom(x) + 0.6 + 0.35 * (x * 0.37 + 1.0).sin().abs()), "#404040", 0.18),
wash(shapes::ellipse(Rect::new(21.0, -2.4, 39.0, 0.8)), "#e6e6e6", 0.22),
wash(shapes::ellipse(Rect::new(61.0, -0.3, 75.0, 2.3)), "#e6e6e6", 0.18),
])
};
let pooling = {
let top = |x: f64| -(4.0 + 0.4 * (x * 0.18 + 0.7).sin() + 0.2 * (x * 0.53).sin()) * wash_ends(x);
let bottom = |x: f64| (4.0 + 0.35 * (x * 0.23 + 1.9).sin() + 0.25 * (x * 0.61 + 0.2).sin()) * wash_ends(x);
let rim = |x: f64, k: f64, phase: f64| 0.35 + k * (0.5 + 0.5 * (x * 0.29 + phase).sin());
group(vec![
wash(between(0.0, 100.0, 40, top, bottom), "#4d4d4d", 0.35),
wash(between(1.0, 99.0, 40, top, |x| top(x) + rim(x, 0.45, 0.0)), "#262626", 0.55),
wash(between(1.0, 99.0, 40, |x| bottom(x) - rim(x, 0.5, 1.1), bottom), "#262626", 0.55),
wash(polygon(&[(34.0, -2.6), (39.0, -2.8), (36.0, 2.7), (31.0, 2.5)]), "#e6e6e6", 0.2),
wash(polygon(&[(70.0, -2.4), (74.0, -2.5), (72.0, 2.6), (68.0, 2.4)]), "#e6e6e6", 0.18),
])
};
let brushes = vec![
art("Tapered Ink", tapered, Colorization::Tints),
pen,
art("Soft Wash", soft, Colorization::Tints),
art("Pooling Wash", pooling, Colorization::Tints),
];
built("Ink and Wash", Document::new(100.0, 100.0), brushes)
}
fn nib(name: &str, angle: f64, roundness: f64, size: f64) -> Brush {
Brush { name: name.into(), kind: BrushKind::Calligraphic(Calligraphic { angle, roundness, size, ..Default::default() }) }
}