mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 01:52:46 +00:00
Type: Crisp and Strong reshape the inner edge of a character stroke
A run's solid fill and its stroke no longer share one edge group, so the stroke's edge inside the characters takes the method. docs/mcp.md says the native document an SVG, PDF or EPS file carries keeps each frame's method.
This commit is contained in:
@@ -553,9 +553,10 @@ const MAX_TYPE_PAINTS: usize = 8;
|
||||
/// own fills and strokes below the characters, the runs' fills and strokes, then the object's own
|
||||
/// fills and strokes above the characters. Each of the object's own fills and strokes is a group,
|
||||
/// and so is each fill and stroke of a run, except that fills and strokes of runs one after
|
||||
/// another in solid colors are one group when colors are drawn opaque (`opaque`). Paints that
|
||||
/// draw nothing are left out. `None` for type with more than [`MAX_TYPE_PAINTS`] groups, which is
|
||||
/// drawn as one.
|
||||
/// another in solid colors are one group when colors are drawn opaque (`opaque`). A run's stroke
|
||||
/// starts a new group rather than join its own fill's, so the stroke's edges inside the characters
|
||||
/// take the method too. Paints that draw nothing are left out. `None` for type with more than
|
||||
/// [`MAX_TYPE_PAINTS`] groups, which is drawn as one.
|
||||
fn type_paints(n: &Node, t: &TextObject, g: &TextGeom, opaque: bool) -> Option<Vec<Vec<TypePaint>>> {
|
||||
let shown = |i: &AppearanceItem| i.visible() && !i.paint().is_none() && !matches!(i, AppearanceItem::Stroke(st) if st.width <= 0.0);
|
||||
let items = &n.appearance.items;
|
||||
@@ -571,8 +572,10 @@ fn type_paints(n: &Node, t: &TextObject, g: &TextGeom, opaque: bool) -> Option<V
|
||||
let stroke = run.style.has_stroke().then_some((TypePaint::Stroke(i), &run.style.stroke));
|
||||
for (p, paint) in [fill, stroke].into_iter().flatten() {
|
||||
let solid = opaque && matches!(paint, vectorcraft_color::Paint::Solid { .. });
|
||||
// A run's fill and stroke overlap along every outline, so they never share a group.
|
||||
let own_fill = matches!(p, TypePaint::Stroke(_)) && groups.last().is_some_and(|last| last.contains(&TypePaint::Fill(i)));
|
||||
match groups.last_mut() {
|
||||
Some(last) if joins && solid => last.push(p),
|
||||
Some(last) if joins && solid && !own_fill => last.push(p),
|
||||
_ => groups.push(vec![p]),
|
||||
}
|
||||
joins = solid;
|
||||
|
||||
@@ -190,6 +190,47 @@ fn the_methods_reshape_each_paint_on_its_own() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Crisp and Strong reshape the inner edge of a character stroke, where the stroke lies on the
|
||||
/// characters' fill in another color: at a pixel the fill covers wholly, the stroke shows the
|
||||
/// method's share of its own coverage.
|
||||
#[test]
|
||||
fn the_methods_reshape_a_character_strokes_inner_edge() {
|
||||
let opts = RenderOptions::default();
|
||||
// "Hamburg" with the characters' fill `fill` and a 1.5 pt stroke `stroke`, drawn with `m`.
|
||||
let draw = |m: TypeAntiAlias, fill: Paint, stroke: Paint| {
|
||||
let mut d = page(m, None);
|
||||
let id = d.layers[0].children().and_then(|c| c.first()).map(|n| n.id).unwrap();
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
let s = &mut t.runs[0].style;
|
||||
(s.fill, s.stroke, s.stroke_width) = (fill, stroke, 1.5);
|
||||
}
|
||||
Renderer::new().render(&d, W, H, Affine::IDENTITY, &opts).pixels
|
||||
};
|
||||
let black = || Paint::solid(Color::BLACK);
|
||||
let alpha = |p: Vec<u8>| p.chunks(4).map(|p| p[3]).collect::<Vec<u8>>();
|
||||
let fill = alpha(draw(TypeAntiAlias::Sharp, black(), Paint::None));
|
||||
let stroke = alpha(draw(TypeAntiAlias::Sharp, Paint::None, black()));
|
||||
// Pixels inside the fill that the stroke covers in part: the stroke's inner edge.
|
||||
let inner: Vec<usize> = (0..fill.len()).filter(|i| fill[*i] == 255 && stroke[*i] > 8 && stroke[*i] < 247).collect();
|
||||
assert!(inner.len() > 50, "{} pixels on the stroke's inner edge", inner.len());
|
||||
for m in [TypeAntiAlias::Crisp, TypeAntiAlias::Strong] {
|
||||
let drawn = draw(m, Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
|
||||
let wrong: Vec<_> = inner
|
||||
.iter()
|
||||
.filter(|i| {
|
||||
let c = stroke[**i];
|
||||
let near = [c.saturating_sub(1), c, c.saturating_add(1)].map(|c| edge_coverage(m, c));
|
||||
let (lo, hi) = (near.iter().min().copied().unwrap_or(0), near.iter().max().copied().unwrap_or(255));
|
||||
let red = drawn[**i * 4];
|
||||
red < lo.saturating_sub(2) || red > hi.saturating_add(2)
|
||||
})
|
||||
.take(5)
|
||||
.map(|i| (*i, stroke[*i], drawn[*i * 4]))
|
||||
.collect();
|
||||
assert!(wrong.is_empty(), "{m:?}: (pixel, stroke coverage, red) {wrong:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Crisp and Strong type with several faint fills of the object draws the pixels inside the
|
||||
/// characters as Sharp draws them: the fills are composited there before their colors are rounded.
|
||||
#[test]
|
||||
|
||||
+3
-1
@@ -5065,7 +5065,9 @@ inline graphics or color glyphs (bitmap or COLR), when its own fills or strokes
|
||||
stroke or a blend mode other than Normal, when it overprints while Overprint Preview is on, and on the canvas while it
|
||||
shows a substitution highlight.
|
||||
With Preferences › General › Anti-aliased Artwork off, the canvas draws all its art with hard edges, type included, and
|
||||
so does an export whose own Anti-aliasing option is None. SVG, PDF and EPS export are unchanged and carry no setting.
|
||||
so does an export whose own Anti-aliasing option is None. SVG, PDF and EPS export draw the type unchanged. The native
|
||||
document such a file carries (an EPS VectorCraft wrote, or an SVG or PDF saved with Preserve Editing) keeps each frame's
|
||||
method, and opening the file restores it.
|
||||
Reset Panel sets Sharp. Saved as `antiAlias` only when not `sharp`. One undo step.
|
||||
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user