zeno/
lib.rs

1/*!
2This crate provides a high performance, low level 2D rasterization library
3with support for rendering paths of various styles into alpha or subpixel
4masks.
5
6Broadly speaking, support is provided for the following:
7- 256x anti-aliased rasterization (8-bit alpha or 32-bit RGBA subpixel alpha)
8- Pixel perfect hit testing with customizable coverage threshold
9- Non-zero and even-odd fills
10- Stroking with the standard set of joins and caps
11  (separate start and end caps are possible)
12- Numerically stable dashing for smooth dash offset animation
13- Vertex traversal for marker placement
14- Stepped distance traversal for animation or text-on-path support
15- Abstract representation of path data that imposes no policy on storage
16
17While this crate is general purpose, in the interest of interoperability and
18familiarity, the feature set was chosen specifically to accommodate the
19requirements of the
20[SVG path specification](https://www.w3.org/TR/SVG/paths.html).
21
22Furthermore, the rasterized masks are nearly identical to those generated by
23Skia (sans slight AA differences) and as such, should yield images that are
24equivalent to those produced by modern web browsers.
25
26# Rendering
27
28Due to the large configuration space for styling and rendering paths, the
29builder pattern is used pervasively. The [`Mask`] struct is the builder
30used for rasterization. For example, to render a simple triangle into a
3164x64 8-bit alpha mask:
32
33```rust
34use zeno::{Mask, PathData};
35
36// The target buffer that will contain the mask
37let mut mask = [0u8; 64 * 64];
38
39// Create a new mask with some path data
40Mask::new("M 8,56 32,8 56,56 Z")
41    // Choose an explicit size for the target
42    .size(64, 64)
43    // Finally, render the path into the target
44    .render_into(&mut mask, None);
45```
46
47Note that, in this case, the path itself is supplied as a string in SVG path
48data format. This crate provides several different kinds of path data by
49default along with support for custom implementations. See the
50[`PathData`] trait for more detail.
51
52The previous example did not provide a style, so a non-zero
53[`Fill`] was chosen by default. Let's render the same path with
54a 4 pixel wide stroke and a round line join:
55
56```rust
57use zeno::{Join, Mask, PathData, Stroke};
58
59let mut mask = [0u8; 64 * 64];
60
61Mask::new("M 8,56 32,8 56,56 Z")
62    .size(64, 64)
63    .style(Stroke::new(4.0).join(Join::Round))
64    .render_into(&mut mask, None);
65```
66
67Or to make it a bit more dashing:
68
69```rust
70use zeno::{Cap, Join, Mask, PathData, Stroke};
71
72let mut mask = [0u8; 64 * 64];
73
74Mask::new("M 8,56 32,8 56,56 Z")
75    .style(
76        Stroke::new(4.0)
77            .join(Join::Round)
78            .cap(Cap::Round)
79            // dash accepts a slice of dash lengths and an initial dash offset
80            .dash(&[10.0, 12.0, 0.0], 0.0),
81    )
82    .size(64, 64)
83    .render_into(&mut mask, None);
84```
85
86See the [`Stroke`] builder struct for all available options.
87
88So far, we've generated our masks into fixed buffers with explicit sizes. It is
89often the case that it is preferred to ignore all empty space and render a path
90into a tightly bound mask of dynamic size. This can be done by eliding the call
91for the size method:
92
93```rust
94use zeno::{Mask, PathData};
95
96// Dynamic buffer that will contain the mask
97let mut mask = Vec::new();
98
99let placement = Mask::new("M 8,56 32,8 56,56 Z")
100    // Insert an inspect call here to access the computed dimensions
101    .inspect(|format, width, height| {
102        // Make sure our buffer is the correct size
103        mask.resize(format.buffer_size(width, height), 0);
104    })
105    .render_into(&mut mask, None);
106```
107
108The call to size has been replaced with a call to inspect which injects a
109closure into the call chain giving us the opportunity to extend our buffer to
110the appropriate size. Note also that the render method has a return value that
111has been captured here. This [`Placement`] struct describes the dimensions of
112the resulting mask along with an offset that should be applied during
113composition to compensate for the removal of any empty space.
114
115Finally, it is possible to render without a target buffer, in which case the
116rasterizer will allocate and return a new `Vec<u8>` containing the mask:
117
118```rust
119use zeno::{Mask, PathData};
120
121// mask is a Vec<u8>
122let (mask, placement) = Mask::new("M 8,56 32,8 56,56 Z")
123    // Calling render() instead of render_into() will allocate a buffer
124    // for you that is returned along with the placement
125    .render();
126```
127
128Both [`Mask`] and [`Stroke`] offer large sets of options for fine-grained
129control of styling and rasterization including offsets, scaling,
130transformations, formats, coordinate spaces and more. See
131their respective documentation for more detail.
132
133# Hit testing
134
135Hit testing is the process of determining if a point is within the region that
136would be painted by the path. A typical use case is to determine if a user's
137cursor is hovering over a particular path. The process generally follows the
138same form as rendering:
139
140```rust
141use zeno::{HitTest, PathData};
142
143// A 20x10 region with the right half covered by the path
144let hit_test = HitTest::new("M10,0 10,10 20,10 20,0 Z");
145
146assert_eq!(hit_test.test([15, 5]), true);
147assert_eq!(hit_test.test([5, 5]), false);
148```
149
150Due to the fact that paths are anti-aliased, the hit test builder offers a
151threshold option that determines how much "coverage" is required for a hit test
152to pass at a particular point.
153
154```rust
155use zeno::{HitTest, PathData};
156
157let mut hit_test = HitTest::new("M2.5,0 2.5,2 5,2 5,0 Z");
158
159// Require full coverage for a successful hit test
160hit_test.threshold(255);
161assert_eq!(hit_test.test([2, 0]), false);
162
163// Succeed for any non-zero coverage
164hit_test.threshold(0);
165assert_eq!(hit_test.test([2, 0]), true);
166```
167
168See the [`HitTest`] type for more detail.
169
170# Path building
171
172While SVG paths are a reasonable choice for static storage, there sometimes
173arise cases where paths must be built dynamically at runtime:
174
175```rust
176use zeno::{Command, Mask, PathBuilder, PathData};
177
178// Create a vector to store the path commands
179let mut path: Vec<Command> = Vec::new();
180
181// Construct the path with chained method calls
182path.move_to([8, 56]).line_to([32, 8]).line_to([56, 56]).close();
183
184// Ensure it is equal to the equivalent SVG path
185assert!((&path).commands().eq("M 8,56 32,8 56,56 Z".commands()));
186
187// &Vec<Command> is also valid path data
188Mask::new(&path).render(); // ...
189```
190
191Here, a vector of [`Command`]s is used to store the path data and the
192[`PathBuilder`] trait provides the extension methods necessary for
193building a path.
194
195Beyond the four basic path commands, the path builder trait also provides
196arcs (and position relative versions of all previous commands) along with
197rectangles, round rectangles, ellipses and circles:
198
199```rust
200use zeno::{Angle, ArcSize, ArcSweep, Command, PathBuilder, PathData};
201
202let mut path: Vec<Command> = Vec::new();
203
204path.move_to([1, 2]).rel_arc_to(
205    8.0,
206    4.0,
207    Angle::from_degrees(30.0),
208    ArcSize::Small,
209    ArcSweep::Positive,
210    [10, 4],
211);
212
213assert!((&path).commands().eq("M1,2 a8,4,30,0,1,10,4".commands()));
214```
215
216Along with incremental building of paths, path builder can also be used as a
217"sink" for capturing the result of the application of a style and transform
218to some path data. For example, it is possible to store the output of a stroke
219style to avoid the cost of stroke evaluation for future rendering or hit test
220operations with the use of the [`apply`] function:
221
222```rust
223use zeno::{apply, Cap, Command, PathBuilder, PathData, Stroke};
224
225let mut stroke: Vec<Command> = Vec::new();
226
227apply("L10,0", Stroke::new(4.0).cap(Cap::Round), None, &mut stroke);
228```
229
230[`PathBuilder`] is only implemented for `Vec<Command>` by default, but
231custom implementations are possible to support capturing and building
232paths into other data structures.
233
234# Traversal
235
236Path traversal involves incremental evaluation of a path by some metric. This
237crate currently provides two methods of traversal.
238
239The [`Vertices`] iterator yields a variant of the [`Vertex`] enum at the
240beginning and end of each subpath and between each path command. Each variant
241provides all the geometric information necessary to place SVG style markers.
242
243The [`Walk`] type is an iterator-like type that allows for
244stepping along the path by arbitrary distances. Each step yields the position
245on the path at the next distance along with a vector describing the
246left-ward direction from the path at that point. This is useful for animating
247objects along a path, or for rendering text attached to a path.
248
249# Transient memory allocations
250
251The algorithms in this crate make a concerted effort to avoid dynamic
252allocations where possible, but paths of significant size or complexity
253may cause spills into temporary heap memory. Specifically, stroke evaluation
254and rasterization may cause heap allocations.
255
256To amortize the cost of these, the appropriately named
257[`Scratch`] struct is available. This type contains internal
258heap allocated storage and provides replacement methods for functions that may
259allocate. In addition, the [`Mask::with_scratch`] and [`HitTest::with_scratch`]
260constructors are provided which take a scratch instance as an argument and
261redirect all transient allocations to the reusable storage.
262 */
263
264#![cfg_attr(not(feature = "std"), no_std)]
265
266#[cfg(not(any(feature = "std", feature = "libm")))]
267compile_error! { "Either the std or libm feature must be enabled"  }
268
269extern crate alloc;
270
271mod command;
272mod geometry;
273#[cfg(feature = "eval")]
274mod hit_test;
275#[cfg(feature = "eval")]
276mod mask;
277mod path_builder;
278mod path_data;
279#[cfg(feature = "eval")]
280mod raster;
281#[cfg(feature = "eval")]
282mod scratch;
283mod segment;
284#[cfg(feature = "eval")]
285mod stroke;
286mod style;
287mod svg_parser;
288#[cfg(feature = "eval")]
289mod traversal;
290
291pub use command::{Command, Verb};
292pub use geometry::{Angle, Bounds, Origin, Placement, Point, Transform, Vector};
293#[cfg(feature = "eval")]
294pub use hit_test::HitTest;
295#[cfg(feature = "eval")]
296pub use mask::{Format, Mask};
297pub use path_builder::{ArcSize, ArcSweep, PathBuilder};
298#[cfg(feature = "eval")]
299pub use path_data::{apply, bounds};
300pub use path_data::{length, PathData};
301#[cfg(feature = "eval")]
302pub use scratch::Scratch;
303pub use style::*;
304pub use svg_parser::validate_svg;
305#[cfg(feature = "eval")]
306pub use traversal::{Vertex, Vertices, Walk};
307
308macro_rules! define_f32_ext {
309    ($($fpname:ident($($argname:ident: $argty:ty),*) -> $ret:ty => $libmname:ident;)*) => {
310        /// An extension trait defining floating point operations.
311        #[allow(dead_code)]
312        trait F32Ext {
313            $(
314            fn $fpname(self, $($argname:$argty),*) -> $ret;
315            )*
316        }
317
318        #[cfg(feature = "std")]
319        impl F32Ext for f32 {
320            $(
321            fn $fpname(self, $($argname:$argty),*) -> $ret {
322                // This intrinsic is natively defined in libstd.
323                f32::$fpname(self, $($argname),*)
324            }
325            )*
326        }
327
328        #[cfg(all(not(feature = "std"), feature = "libm"))]
329        impl F32Ext for f32 {
330            $(
331            fn $fpname(self, $($argname:$argty),*) -> $ret {
332                // Use the libm version of this intrinsic.
333                <$ret>::libm_cvt(libm::$libmname(
334                    self.into(),
335                    $(($argname).into()),*
336                ) as _)
337            }
338            )*
339        }
340    }
341}
342
343define_f32_ext! {
344    abs() -> f32 => fabs;
345    acos() -> f32 => acos;
346    atan2(x:f32) -> f32 => atan2;
347    ceil() -> f32 => ceil;
348    cos() -> f32 => cos;
349    floor() -> f32 => floor;
350    sin_cos() -> (f32, f32) => sincos;
351    sqrt() -> f32 => sqrt;
352    powf(x:f32) -> f32 => powf;
353    powi(x:i32) -> f32 => pow;
354    tan() -> f32 => tan;
355}
356
357#[cfg(all(not(feature = "std"), feature = "libm"))]
358trait LibmCvt {
359    type Input;
360    fn libm_cvt(input: Self::Input) -> Self;
361}
362
363#[cfg(all(not(feature = "std"), feature = "libm"))]
364impl LibmCvt for f32 {
365    type Input = f64;
366    fn libm_cvt(input: f64) -> f32 {
367        input as f32
368    }
369}
370
371#[cfg(all(not(feature = "std"), feature = "libm"))]
372impl LibmCvt for (f32, f32) {
373    type Input = (f64, f64);
374    fn libm_cvt((a, b): (f64, f64)) -> (f32, f32) {
375        (a as f32, b as f32)
376    }
377}
378
379// Prep for no_std support when core supports FP intrinsics.
380mod lib {
381    pub use alloc::vec::Vec;
382}