mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 11:26:50 -06:00
A stroke can carry a width profile: widths at positions along the path
(each subpath by length), interpolated smoothly (monotone cubic).
- doc: WidthProfile / WidthPoint / WidthPreset (uniform, wedge, taper both
ends, eye, rounded, pinch) on StrokeProps, serde-defaulted so older files
load; paint reach and hit testing follow the widest point.
- pathops: path measuring by length and the variable outline (offset along
the normals, round/miter/bevel joins, caps, dashes keep their width,
Inside/Outside clip as before; bounded samples and dashes, uniform
fallback). Canvas, PNG, SVG (filled outline), PDF and outline stroke use it.
- engine: stroke.widthProfile {preset | points}, node.set widthProfile,
inspect output; the Variable width tool (Shift+W) in vector edit mode:
drag the path to add a width point, slide points, drag their handles,
Delete removes the selected one; vector.edit.widthPoint add/set/remove.
Every gesture is one undo step.
- UI: width profile dropdown with previews drawn in code and a flip button
in the stroke settings; the tool in the edit toolbar (new icon drawn in
code); width handles and a hover preview on the canvas; Position/Width
fields for the selected width point.
- Tests: profile maths, outlines, render regressions, tool gestures,
commands, exports and junk sweeps. ROADMAP, README, docs updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
49 lines
1.9 KiB
Rust
49 lines
1.9 KiB
Rust
//! Variable width strokes render along the path, past the weight where the profile widens them.
|
|
|
|
use super::*;
|
|
use figcraft_color::{Color, Paint};
|
|
use figcraft_doc::{NodeKind, WidthPreset, WidthProfile};
|
|
|
|
fn dark(p: &Pixmap, x: u32, y: u32) -> bool {
|
|
let i = ((y * p.width() + x) * 4) as usize;
|
|
p.data().get(i).is_some_and(|r| *r < 40)
|
|
}
|
|
|
|
/// A horizontal line from (10, 50) to (90, 50) with a 20 wide black stroke.
|
|
fn line_doc(profile: Option<WidthProfile>) -> (figcraft_doc::Document, figcraft_doc::NodeId) {
|
|
let mut d = figcraft_doc::Document::new("t");
|
|
let p = d.first_page().unwrap();
|
|
let l = d.create(p, NodeKind::Line, "L", 10.0, 50.0, 80.0, 0.0).unwrap();
|
|
let n = d.get_mut(l).unwrap();
|
|
n.strokes = vec![Paint::solid(Color::BLACK)];
|
|
n.stroke.weight = 20.0;
|
|
n.stroke.width_profile = profile;
|
|
(d, p)
|
|
}
|
|
|
|
fn render(d: &figcraft_doc::Document, p: figcraft_doc::NodeId) -> Pixmap {
|
|
render_view(d, p, &View { origin: (0.0, 0.0), scale: 1.0, width: 100, height: 100 }, &Options::default()).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn wedge_profile_tapers_the_stroke() {
|
|
let (d, p) = line_doc(WidthPreset::Wedge.profile());
|
|
let pix = render(&d, p);
|
|
assert!(dark(&pix, 15, 58) && dark(&pix, 15, 42), "wide at the start");
|
|
assert!(dark(&pix, 70, 50), "a quarter of the width three quarters along");
|
|
assert!(!dark(&pix, 85, 54) && !dark(&pix, 85, 46), "thin at the end");
|
|
// Uniform for comparison: full width at both ends.
|
|
let (d, p) = line_doc(None);
|
|
let pix = render(&d, p);
|
|
assert!(dark(&pix, 85, 58));
|
|
}
|
|
|
|
#[test]
|
|
fn widened_profile_paints_past_the_weight() {
|
|
// Three times the weight in the middle: 30 either side of the line.
|
|
let (d, p) = line_doc(Some(WidthProfile::new([(0.0, 1.0), (0.5, 3.0), (1.0, 1.0)])));
|
|
let pix = render(&d, p);
|
|
assert!(dark(&pix, 50, 77) && dark(&pix, 50, 23), "not culled at the weight's reach");
|
|
assert!(!dark(&pix, 15, 70));
|
|
}
|