Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This release has an [MSRV][] of 1.88.
### Changed

- Breaking change: wgpu has been updated to wgpu 27. ([#1280][] by [@theoparis][])
- Breaking change: Make `Scene` clip / layers honor fill rule (even-odd clips). ([#1332][] by [@waywardmonkeys][])
When pushing a layer, you should use `Fill::NonZero` as the clip fill rule to achieve the same behavior as previous versions.
Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should now be updated, something like the following.

Suggested change
- Breaking change: Make `Scene` clip / layers honor fill rule (even-odd clips). ([#1332][] by [@waywardmonkeys][])
When pushing a layer, you should use `Fill::NonZero` as the clip fill rule to achieve the same behavior as previous versions.
- Breaking change: Allow setting `Scene` layer clipping style, adding even-odd filled path clipping and stroked path clipping. ([#1332][] by [@waywardmonkeys][])
When pushing a layer, you should use `StyleRef::Fill(Fill::NonZero)` as the clip fill rule to achieve the same behavior as previous versions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It takes Into<StyleRef> so Fill::NonZero works and is more concise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, didn't realize we had the From, but that makes sense.


### Fixed

Expand Down Expand Up @@ -383,6 +385,7 @@ This release has an [MSRV][] of 1.75.
[#1273]: https://github.com/linebender/vello/pull/1273
[#1280]: https://github.com/linebender/vello/pull/1280
[#1323]: https://github.com/linebender/vello/pull/1323
[#1332]: https://github.com/linebender/vello/pull/1332

<!-- Note that this still comparing against 0.5.0, because 0.5.1 is a cherry-picked patch -->
[Unreleased]: https://github.com/linebender/vello/compare/v0.5.0...HEAD
Expand Down
98 changes: 91 additions & 7 deletions examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ mod impls {
&rect,
);
let alpha = params.time.sin() as f32 * 0.5 + 0.5;
scene.push_layer(Mix::Normal, alpha, Affine::IDENTITY, &rect);
scene.push_layer(Fill::NonZero, Mix::Normal, alpha, Affine::IDENTITY, &rect);
scene.fill(
Fill::NonZero,
Affine::translate((100.0, 100.0)) * Affine::scale(0.2),
Expand Down Expand Up @@ -1125,6 +1125,7 @@ mod impls {
let mut depth = 0;
for (width, color) in &options[..params.complexity.min(options.len() - 1)] {
scene.push_layer(
Fill::NonZero,
Mix::Normal,
0.9,
Affine::IDENTITY,
Expand Down Expand Up @@ -1152,7 +1153,7 @@ mod impls {
const CLIPS_PER_FILL: usize = 3;
for _ in 0..CLIPS_PER_FILL {
let rot = Affine::rotate(rng.random_range(0.0..PI));
scene.push_clip_layer(translate * rot, &base_tri);
scene.push_clip_layer(Fill::NonZero, translate * rot, &base_tri);
}
let rot = Affine::rotate(rng.random_range(0.0..PI));
let color = Color::new([rng.random(), rng.random(), rng.random(), 1.]);
Expand Down Expand Up @@ -1212,7 +1213,7 @@ mod impls {
PathEl::LineTo((X0, Y1).into()),
PathEl::ClosePath,
];
scene.push_clip_layer(Affine::IDENTITY, &path);
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &path);
}
let rect = Rect::new(X0, Y0, X1, Y1);
scene.fill(
Expand Down Expand Up @@ -1243,7 +1244,11 @@ mod impls {
None,
&make_diamond(1024.0, 125.0),
);
scene.push_clip_layer(Affine::IDENTITY, &make_diamond(1024.0, 150.0));
scene.push_clip_layer(
Fill::NonZero,
Affine::IDENTITY,
&make_diamond(1024.0, 150.0),
);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
Expand Down Expand Up @@ -1271,11 +1276,11 @@ mod impls {
scene.fill(Fill::NonZero, transform, &radial, None, &rect);
}
const COLORS: &[Color] = &[palette::css::RED, palette::css::LIME, palette::css::BLUE];
scene.push_layer(Mix::Normal, 1.0, transform, &rect);
scene.push_layer(Fill::NonZero, Mix::Normal, 1.0, transform, &rect);
for (i, c) in COLORS.iter().enumerate() {
let linear = Gradient::new_linear((0.0, 0.0), (0.0, 200.0))
.with_stops([palette::css::WHITE, *c]);
scene.push_layer(blend, 1.0, transform, &rect);
scene.push_layer(Fill::NonZero, blend, 1.0, transform, &rect);
// squash the ellipse
let a = transform
* Affine::translate((100., 100.))
Expand Down Expand Up @@ -1579,7 +1584,7 @@ mod impls {
PathEl::ClosePath,
]
};
scene.push_clip_layer(Affine::IDENTITY, &clip);
scene.push_clip_layer(Fill::NonZero, Affine::IDENTITY, &clip);
{
let text_size = 60.0 + 40.0 * (params.time as f32).sin();
let s = "Some clipped text!";
Expand All @@ -1594,6 +1599,80 @@ mod impls {
}
scene.pop_layer();

// Even-odd clip-layer demo: a self-intersecting star ("pentagram") has different results
// under non-zero vs even-odd fill rules (even-odd produces a hole).
let demo_rect = Rect::new(250.0, 20.0, 450.0, 220.0);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
palette::css::BLUE,
None,
&demo_rect,
);
let mut star = BezPath::new();
let center = Point::new(350.0, 120.0);
let outer_r = 90.0;
let start_angle = -std::f64::consts::FRAC_PI_2;
let pts: [Point; 5] = core::array::from_fn(|i| {
let a = start_angle + (i as f64) * (2.0 * std::f64::consts::PI / 5.0);
center + Vec2::new(a.cos() * outer_r, a.sin() * outer_r)
});
let order = [0_usize, 2, 4, 1, 3];
star.move_to(pts[order[0]]);
for &idx in &order[1..] {
star.line_to(pts[idx]);
}
star.close_path();

scene.push_clip_layer(Fill::EvenOdd, Affine::IDENTITY, &star);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
palette::css::RED,
None,
&demo_rect,
);
scene.pop_layer();

// Stroke clip demo: clip to the stroked outline of a path.
let stroke_demo_rect = Rect::new(250.0, 240.0, 450.0, 440.0);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
palette::css::SLATE_GRAY,
None,
&stroke_demo_rect,
);
let mut stroke_star = BezPath::new();
let center = Point::new(350.0, 340.0);
let outer_r = 85.0;
let start_angle = -std::f64::consts::FRAC_PI_2;
let pts: [Point; 5] = core::array::from_fn(|i| {
let a = start_angle + (i as f64) * (2.0 * std::f64::consts::PI / 5.0);
center + Vec2::new(a.cos() * outer_r, a.sin() * outer_r)
});
let order = [0_usize, 2, 4, 1, 3];
stroke_star.move_to(pts[order[0]]);
for &idx in &order[1..] {
stroke_star.line_to(pts[idx]);
}
stroke_star.close_path();
let mut stroke = Stroke::new(18.0);
stroke.join = Join::Round;
stroke.start_cap = Cap::Round;
stroke.end_cap = Cap::Round;
scene.push_clip_layer(&stroke, Affine::IDENTITY, &stroke_star);
let grad = Gradient::new_linear((250.0, 240.0), (450.0, 440.0))
.with_stops([palette::css::MAGENTA, palette::css::CYAN]);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
&grad,
None,
&stroke_demo_rect,
);
scene.pop_layer();

let large_background_rect = Rect::new(-1000.0, -1000.0, 2000.0, 2000.0);
let inside_clip_rect = Rect::new(11.0, 13.399999999999999, 59.0, 56.6);
let outside_clip_rect = Rect::new(
Expand All @@ -1606,6 +1685,7 @@ mod impls {
let scale = 2.0;

scene.push_layer(
Fill::NonZero,
BlendMode {
mix: Mix::Normal,
compose: Compose::SrcOver,
Expand Down Expand Up @@ -1926,6 +2006,7 @@ mod impls {
},
);
scene.push_layer(
Fill::NonZero,
BlendMode::new(Mix::Normal, Compose::SrcOver),
1.0,
Affine::IDENTITY,
Expand All @@ -1949,6 +2030,7 @@ mod impls {
},
);
scene.push_luminance_mask_layer(
Fill::NonZero,
1.0,
Affine::IDENTITY,
&Rect {
Expand Down Expand Up @@ -1993,6 +2075,7 @@ mod impls {
.unwrap();
// HACK: Porter-Duff "over" the base color, restoring full alpha
scene.push_layer(
Fill::NonZero,
BlendMode::new(Mix::Normal, Compose::SrcOver),
1.0,
Affine::IDENTITY,
Expand Down Expand Up @@ -2028,6 +2111,7 @@ mod impls {
},
);
scene.push_luminance_mask_layer(
Fill::NonZero,
1.0,
Affine::IDENTITY,
&Rect {
Expand Down
96 changes: 84 additions & 12 deletions vello/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ impl Scene {
/// Pushes a new layer clipped by the specified shape and composed with
/// previous layers using the specified blend mode.
///
/// The `clip_style` controls how the `clip` shape is interpreted.
///
/// - Use [`Fill`] to clip to the interior of the shape, with the chosen fill rule.
/// - Use [`Stroke`] (via `&Stroke`) to clip to the stroked outline of the shape.
///
/// Every drawing command after this call will be clipped by the shape
/// until the layer is [popped](Self::pop_layer).
/// For layers which are only added for clipping, you should
Expand All @@ -90,9 +95,14 @@ impl Scene {
/// **However, the transforms are *not* saved or modified by the layer stack.**
/// That is, the `transform` argument to this function only applies a transform to the `clip` shape.
#[expect(deprecated, reason = "Provided by the user, need to handle correctly.")]
#[expect(
single_use_lifetimes,
reason = "False positive: https://github.com/rust-lang/rust/issues/129255"
)]
#[track_caller]
pub fn push_layer(
pub fn push_layer<'a>(
&mut self,
clip_style: impl Into<StyleRef<'a>>,
blend: impl Into<BlendMode>,
alpha: f32,
transform: Affine,
Expand All @@ -107,6 +117,7 @@ impl Scene {
}
self.push_layer_inner(
DrawBeginClip::new(blend, alpha.clamp(0.0, 1.0)),
clip_style.into(),
transform,
clip,
);
Expand All @@ -118,6 +129,11 @@ impl Scene {
/// That is, content drawn between this and the matching `pop_layer` call will serve
/// as a luminance mask for the prior content in this layer.
///
/// The `clip_style` controls how the `clip` shape is interpreted.
///
/// - Use [`Fill`] to clip to the interior of the shape, with the chosen fill rule.
/// - Use [`Stroke`] (via `&Stroke`) to clip to the stroked outline of the shape.
///
/// Every drawing command after this call will be clipped by the shape
/// until the layer is [popped](Self::pop_layer).
///
Expand All @@ -135,16 +151,32 @@ impl Scene {
/// This issue only occurs if there are no intermediate opaque layers, so can be worked around
/// by drawing something opaque (or having an opaque `base_color`), then putting a layer around your entire scene
/// with a [`Compose::SrcOver`].
pub fn push_luminance_mask_layer(&mut self, alpha: f32, transform: Affine, clip: &impl Shape) {
#[expect(
single_use_lifetimes,
reason = "False positive: https://github.com/rust-lang/rust/issues/129255"
)]
pub fn push_luminance_mask_layer<'a>(
&mut self,
clip_style: impl Into<StyleRef<'a>>,
alpha: f32,
transform: Affine,
clip: &impl Shape,
) {
self.push_layer_inner(
DrawBeginClip::luminance_mask(alpha.clamp(0.0, 1.0)),
clip_style.into(),
transform,
clip,
);
}

/// Pushes a new layer clipped by the specified `clip` shape.
///
/// The `clip_style` controls how the `clip` shape is interpreted.
///
/// - Use [`Fill`] to clip to the interior of the shape, with the chosen fill rule.
/// - Use [`Stroke`] (via `&Stroke`) to clip to the stroked outline of the shape.
///
/// The pushed layer is intended to not impact the "source" for blending; that is, any blends
/// within this layer will still include content from before this method was called in the "source"
/// of that blend operation.
Expand All @@ -157,21 +189,55 @@ impl Scene {
///
/// **However, the transforms are *not* saved or modified by the layer stack.**
/// That is, the `transform` argument to this function only applies a transform to the `clip` shape.
pub fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) {
self.push_layer_inner(DrawBeginClip::clip(), transform, clip);
#[expect(
single_use_lifetimes,
reason = "False positive: https://github.com/rust-lang/rust/issues/129255"
)]
pub fn push_clip_layer<'a>(
&mut self,
clip_style: impl Into<StyleRef<'a>>,
transform: Affine,
clip: &impl Shape,
) {
self.push_layer_inner(DrawBeginClip::clip(), clip_style.into(), transform, clip);
}

/// Helper for logic shared between [`Self::push_layer`] and [`Self::push_luminance_mask_layer`]
fn push_layer_inner(
fn push_layer_inner<'a>(
&mut self,
parameters: DrawBeginClip,
clip_style: StyleRef<'a>,
transform: Affine,
clip: &impl Shape,
) {
let t = Transform::from_kurbo(&transform);
self.encoding.encode_transform(t);
self.encoding.encode_fill_style(Fill::NonZero);
if !self.encoding.encode_shape(clip, true) {
let (is_fill, stroke_for_estimate) = match clip_style {
StyleRef::Fill(fill) => {
self.encoding.encode_fill_style(fill);
(true, None)
}
StyleRef::Stroke(stroke) => {
let encoded_stroke = self.encoding.encode_stroke_style(stroke);
(false, encoded_stroke.then_some(stroke))
}
};
if stroke_for_estimate.is_none() && matches!(clip_style, StyleRef::Stroke(_)) {
// If the stroke has zero width, encode a valid empty path. This suppresses
// all drawing until the layer is popped.
self.encoding.encode_fill_style(Fill::NonZero);
self.encoding.encode_empty_shape();
#[cfg(feature = "bump_estimate")]
{
use peniko::kurbo::PathEl;
let path = [PathEl::MoveTo(Point::ZERO), PathEl::LineTo(Point::ZERO)];
self.estimator.count_path(path.into_iter(), &t, None);
}
self.encoding.encode_begin_clip(parameters);
return;
}

if !self.encoding.encode_shape(clip, is_fill) {
// If the layer shape is invalid, encode a valid empty path. This suppresses
// all drawing until the layer is popped.
self.encoding.encode_empty_shape();
Expand All @@ -183,7 +249,8 @@ impl Scene {
}
} else {
#[cfg(feature = "bump_estimate")]
self.estimator.count_path(clip.path_elements(0.1), &t, None);
self.estimator
.count_path(clip.path_elements(0.1), &t, stroke_for_estimate);
}
self.encoding.encode_begin_clip(parameters);
}
Expand Down Expand Up @@ -894,7 +961,7 @@ impl ColorPainter for DrawColorGlyphs<'_> {
};
self.clip_depth += 1;
self.scene
.push_clip_layer(self.last_transform().to_kurbo(), &path.0);
.push_clip_layer(Fill::NonZero, self.last_transform().to_kurbo(), &path.0);
}

fn push_clip_box(&mut self, clip_box: skrifa::raw::types::BoundingBox<f32>) {
Expand All @@ -909,7 +976,7 @@ impl ColorPainter for DrawColorGlyphs<'_> {
}
self.clip_depth += 1;
self.scene
.push_clip_layer(self.last_transform().to_kurbo(), &clip_box);
.push_clip_layer(Fill::NonZero, self.last_transform().to_kurbo(), &clip_box);
}

fn pop_clip(&mut self) {
Expand Down Expand Up @@ -949,8 +1016,13 @@ impl ColorPainter for DrawColorGlyphs<'_> {
// TODO:
_ => Compose::SrcOver,
};
self.scene
.push_layer(blend, 1.0, self.last_transform().to_kurbo(), &self.clip_box);
self.scene.push_layer(
Fill::NonZero,
blend,
1.0,
self.last_transform().to_kurbo(),
&self.clip_box,
);
}

fn pop_layer(&mut self) {
Expand Down
Loading