Skip to main content

clipping/
clipping.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Applying clip paths using Vello CPU.
5
6use vello_cpu::Pixmap;
7use vello_cpu::RenderContext;
8use vello_cpu::Resources;
9use vello_cpu::color::palette::css::{BLUE, RED, WHITE};
10use vello_cpu::kurbo::{Circle, Rect, Shape};
11
12fn main() {
13    // Clip-paths are a fundamental operation in 2D rendering and allow you
14    // to constrain the visible areas of all subsequently drawn paths to the
15    // shape of another area. Vello CPU has full support for them and actually
16    // provides 2 different ways of using them. Below, we will explore them
17    // and explain their difference and when to use which one.
18
19    let mut ctx = RenderContext::new(200, 200);
20
21    // Two example clip shapes. They have a small overlap in the center that forms
22    // an ellipse.
23    let clip_1 = Circle::new((75.0, 75.0), 50.0).to_path(0.1);
24    let clip_2 = Circle::new((125.0, 75.0), 50.0).to_path(0.1);
25
26    // Method 1: Non-isolated clipping using the `push_clip_path` and
27    // `pop_clip_path` methods:
28    {
29        // Let's first create a white background.
30        ctx.set_paint(WHITE);
31        ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
32
33        // As mentioned, clip paths contain the area that will be affected by
34        // our drawing operations. By default, drawing operations will be
35        // visible on the whole width/height of the render context.
36
37        // After this first `push_clip_path`, all drawing operations will be
38        // constrained to the area of the first circle.
39        ctx.push_clip_path(&clip_1.to_path(0.1));
40        // Clip paths can be nested/stacked to arbitrary depths. By
41        // nesting clip paths, the drawing area will be further reduced to
42        // the _intersection_ of all clip paths that are currently in-place.
43        // Thus, after this second `push_clip_path` call, only the pixels
44        // that lie in the intersection of both circles will be painted.
45        ctx.push_clip_path(&clip_2.to_path(0.1));
46        ctx.set_paint(RED);
47        // Even though the rectangle covers the whole viewport, only the parts
48        // that lie in the intersection of both circles will be painted.
49        ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
50        // By popping a clip path, the top clip-path on the element (in this case
51        // `clip_2`) will be removed. Thus, only `clip_1` remains in-place.
52        ctx.pop_clip_path();
53        ctx.set_paint(BLUE.with_alpha(0.2));
54        // This rectangle will only be constrained by the area of `clip_1`.
55        ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
56
57        // This is optional. You don't strictly have to pop all clip paths
58        // currently in-place before rasterizing to the pixmap.
59        ctx.pop_clip_path();
60
61        ctx.flush();
62
63        save_pixmap(&ctx, "example_clipping1");
64    }
65
66    // Method 2: Isolated clipping using the `push_clip_layer` and `pop_layer`
67    // methods:
68    {
69        // Overall, this method works exactly the same as the previous
70        // one, just that the method calls are different. Instead of
71        // `push_clip_path`, we have `push_clip_layer`, and instead of
72        // `pop_clip_path`, we have `pop_clip_layer`.
73
74        ctx.set_paint(WHITE);
75        ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
76
77        ctx.push_clip_layer(&clip_1.to_path(0.1));
78        ctx.push_clip_layer(&clip_2.to_path(0.1));
79        ctx.set_paint(RED);
80        ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
81        ctx.pop_layer();
82        ctx.set_paint(BLUE.with_alpha(0.2));
83        ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
84
85        // Unlike the first method, THIS PART IS NOT OPTIONAL! Before
86        // rasterizing, you need to make sure that all previously pushed layers
87        // have been popped. Otherwise, the renderer will panic.
88        ctx.pop_layer();
89
90        ctx.flush();
91        save_pixmap(&ctx, "example_clipping2");
92    }
93
94    // If you inspect the above results, you will see that they visually yield
95    // the same result. So what is their difference and when should you use
96    // which one? The answer should become clearer when explaining how they
97    // differ conceptually.
98    // When creating a clip path using `push_clip_path`, every subsequent drawing
99    // operation will conceptually be stencil-masked through the intersection
100    // of all currently active clip paths before being drawn onto the screen.
101    // On the other hand, doing `push_clip_layer` will actually push a whole
102    // new isolated layer, and once you call
103    // `pop_layer`, the layer _as a whole_ will be clipped to the bounds of the
104    // paths and composited back into the previous layer.
105    //
106    // Which one of these two methods you should use depend on the imaging model
107    // you are trying to reflect. For example, in SVG, each group with a clip-path
108    // automatically requires creating a new isolated layer. In this case, the
109    // isolated clipping method fits the imaging model better. On the other hand,
110    // in PDF for example, clip paths and layer isolation are two completely
111    // separate concepts. Therefore, it makes much more sense to use the
112    // `push_clip_path` method, since you don't want to introduce an isolated
113    // layer each time a new clip path is added.
114    //
115    // Finally, it is also worth mentioning that according to your experiments,
116    // non-isolated clipping is usually faster than isolated clipping, especially
117    // on the CPU. Therefore, if you are still in doubt, it is recommended
118    // that you simply use the non-isolated method. If necessary, you can easily
119    // just mix the two different methods as well.
120    //
121    // Another small note: Clip paths can actually be emulated using alpha
122    // masks (see the masking example), so strictly speaking you don't need
123    // to use the specialized clipping methods to create clip paths. However,
124    // the clipping methods are _much faster_ than masking, and you should
125    // therefore always prefer using those over masking.
126}
127
128fn save_pixmap(ctx: &RenderContext, filename: &str) {
129    let mut resources = Resources::new();
130    let mut pixmap = Pixmap::new(ctx.width(), ctx.height());
131    ctx.render(&mut pixmap, &mut resources);
132    let png = pixmap.into_png().unwrap();
133    std::fs::write(format!("{filename}.png"), png).unwrap();
134}