From 8b328c2a023fe3cd2b67e481ebdbf92e809c2687 Mon Sep 17 00:00:00 2001 From: Chad Date: Tue, 6 Oct 2020 04:27:46 -0400 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cargo.toml | 14 + LICENSE-APACHE | 201 +++++++++ LICENSE-MIT | 25 ++ README.md | 73 ++++ src/command.rs | 177 ++++++++ src/fill.rs | 10 + src/geometry.rs | 525 ++++++++++++++++++++++++ src/hit_test.rs | 90 ++++ src/lib.rs | 293 ++++++++++++++ src/mask.rs | 512 +++++++++++++++++++++++ src/path_builder.rs | 596 +++++++++++++++++++++++++++ src/path_data.rs | 214 ++++++++++ src/raster.rs | 823 +++++++++++++++++++++++++++++++++++++ src/scratch.rs | 101 +++++ src/segment.rs | 634 +++++++++++++++++++++++++++++ src/stroke.rs | 969 ++++++++++++++++++++++++++++++++++++++++++++ src/style.rs | 51 +++ src/svg_parser.rs | 657 ++++++++++++++++++++++++++++++ src/traversal.rs | 234 +++++++++++ 20 files changed, 6201 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 src/command.rs create mode 100644 src/fill.rs create mode 100644 src/geometry.rs create mode 100644 src/hit_test.rs create mode 100644 src/lib.rs create mode 100644 src/mask.rs create mode 100644 src/path_builder.rs create mode 100644 src/path_data.rs create mode 100644 src/raster.rs create mode 100644 src/scratch.rs create mode 100644 src/segment.rs create mode 100644 src/stroke.rs create mode 100644 src/style.rs create mode 100644 src/svg_parser.rs create mode 100644 src/traversal.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..76ed0c2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "zeno" +version = "0.1.0" +authors = ["Chad Brokaw "] +edition = "2018" +description = "High performance, low level 2D path rasterization." +license = "MIT" +keywords = ["path", "rasterizer", "svg"] +categories = ["graphics"] +repository = "https://github.com/dfrg/zeno" +homepage = "https://github.com/dfrg/zeno" +readme = "README.md" + +[dependencies] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..1724c94 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2020 Chad Brokaw + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..109d5f7 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# zeno + +Zeno is a pure Rust crate that provides high performance, low level 2D +rasterization library with support for rendering paths of various styles +into alpha or subpixel masks. + +[![Crates.io][crates-badge]][crates-url] +[![Docs.rs][docs-badge]][docs-url] +[![MIT licensed][mit-badge]][mit-url] +[![Apache licensed][apache-badge]][apache-url] + +[crates-badge]: https://img.shields.io/crates/v/zeno.svg +[crates-url]: https://crates.io/crates/zeno +[docs-badge]: https://docs.rs/zeno/badge.svg +[docs-url]: https://docs.rs/zeno +[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[mit-url]: LICENSE-MIT +[apache-badge]: https://img.shields.io/badge/license-Apache--2.0-blue.svg +[apache-url]: LICENSE-APACHE + +## Features + +- 256x anti-aliased rasterization (8-bit alpha or 32-bit RGBA subpixel alpha) +- Pixel perfect hit testing with customizable coverage threshold +- Non-zero and even-odd fills +- Stroking with the standard set of joins and caps + (separate start and end caps are possible) +- Numerically stable dashing for smooth dash offset animation +- Vertex traversal for marker placement +- Stepped distance traversal for animation or text-on-path support +- Abstract representation of path data that imposes no policy on storage + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +zeno = "0.1.0" +``` + +Rendering a dashed stroke of a triangle: + +```rust +use zeno::{Cap, Join, Mask, PathData, Stroke}; + +// Buffer to store the mask +let mut mask = [0u8; 64 * 64]; + +/// Create a mask builder with some path data +Mask::new("M 8,56 32,8 56,56 Z") + .style( + // Stroke style with a width of 4 + Stroke::new(4.0) + // Round line joins + .join(Join::Round) + // And round line caps + .cap(Cap::Round) + // Dash pattern followed by a dash offset + .dash(&[10.0, 12.0, 0.0], 0.0), + ) + // Set the target dimensions + .size(64, 64) + // Render into the target buffer + .render_into(&mut mask, None); +``` + +Resulting in the following mask: + +![Dashed Triangle](https://muddl.com/zeno/tri_dash.png) + +For detail on additional features and more advanced usage, +see the full API [documentation](https://docs.rs/zeno). \ No newline at end of file diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..75de11a --- /dev/null +++ b/src/command.rs @@ -0,0 +1,177 @@ +//! Path commands. + +use super::geometry::{Point, Transform}; +use super::path_builder::PathBuilder; + +use std::borrow::Borrow; + +/// Path command. +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Command { + /// Begins a new subpath at the specified point. + MoveTo(Point), + /// A straight line from the previous point to the specified point. + LineTo(Point), + /// A cubic bezier curve from the previous point to the final point with + /// two intermediate control points. + CurveTo(Point, Point, Point), + /// A quadratic curve from the previous point to the final point with one + /// intermediate control point. + QuadTo(Point, Point), + /// Closes a subpath, connecting the final point to the initial point. + Close, +} + +impl Command { + /// Returns the associated verb for the command. + pub fn verb(&self) -> Verb { + use Command::*; + match self { + MoveTo(..) => Verb::MoveTo, + LineTo(..) => Verb::LineTo, + QuadTo(..) => Verb::QuadTo, + CurveTo(..) => Verb::CurveTo, + Close => Verb::CurveTo, + } + } + + /// Returns the result of a transformation matrix applied to the command. + #[inline] + pub fn transform(&self, transform: &Transform) -> Self { + use Command::*; + let t = transform; + match self { + MoveTo(p) => MoveTo(t.transform_point(*p)), + LineTo(p) => LineTo(t.transform_point(*p)), + QuadTo(c, p) => QuadTo(t.transform_point(*c), t.transform_point(*p)), + CurveTo(c1, c2, p) => CurveTo( + t.transform_point(*c1), + t.transform_point(*c2), + t.transform_point(*p), + ), + Close => Close, + } + } +} + +/// Action of a path command. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Verb { + MoveTo, + LineTo, + CurveTo, + QuadTo, + Close, +} + +#[derive(Clone)] +pub struct PointsCommands<'a> { + points: &'a [Point], + verbs: &'a [Verb], + point: usize, + verb: usize, +} + +impl<'a> PointsCommands<'a> { + pub(super) fn new(points: &'a [Point], verbs: &'a [Verb]) -> Self { + Self { + points, + verbs, + point: 0, + verb: 0, + } + } + + #[inline(always)] + pub(super) fn copy_to(&self, sink: &mut impl PathBuilder) { + self.copy_to_inner(sink); + } + + #[inline(always)] + fn copy_to_inner(&self, sink: &mut impl PathBuilder) -> Option<()> { + let mut i = 0; + for verb in self.verbs { + match verb { + Verb::MoveTo => { + let p = self.points.get(i)?; + i += 1; + sink.move_to(*p); + } + Verb::LineTo => { + let p = self.points.get(i)?; + i += 1; + sink.line_to(*p); + } + Verb::QuadTo => { + let p = self.points.get(i + 1)?; + let c = self.points.get(i)?; + i += 2; + sink.quad_to(*c, *p); + } + Verb::CurveTo => { + let p = self.points.get(i + 2)?; + let c2 = self.points.get(i + 1)?; + let c1 = self.points.get(i)?; + i += 3; + sink.curve_to(*c1, *c2, *p); + } + Verb::Close => { + sink.close(); + } + } + } + Some(()) + } +} + +impl<'a> Iterator for PointsCommands<'a> { + type Item = Command; + + #[inline(always)] + fn next(&mut self) -> Option { + use Command::*; + let verb = self.verbs.get(self.verb)?; + self.verb += 1; + Some(match verb { + Verb::MoveTo => { + let p = self.points.get(self.point)?; + self.point += 1; + MoveTo(*p) + } + Verb::LineTo => { + let p = self.points.get(self.point)?; + self.point += 1; + LineTo(*p) + } + Verb::QuadTo => { + let p = self.points.get(self.point..self.point + 2)?; + self.point += 2; + QuadTo(p[0], p[1]) + } + Verb::CurveTo => { + let p = self.points.get(self.point..self.point + 3)?; + self.point += 3; + CurveTo(p[0], p[1], p[2]) + } + Verb::Close => Close, + }) + } +} + +#[derive(Clone)] +pub struct TransformCommands { + pub data: D, + pub transform: Transform, +} + +impl Iterator for TransformCommands +where + D: Iterator + Clone, + D::Item: Borrow, +{ + type Item = Command; + + fn next(&mut self) -> Option { + Some(self.data.next()?.borrow().transform(&self.transform)) + } +} diff --git a/src/fill.rs b/src/fill.rs new file mode 100644 index 0000000..04cc10b --- /dev/null +++ b/src/fill.rs @@ -0,0 +1,10 @@ +//! Fill rule. + +/// Describes the visual style of a fill. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Fill { + /// The non-zero fill rule. + NonZero, + /// The even-odd fill rule. + EvenOdd, +} diff --git a/src/geometry.rs b/src/geometry.rs new file mode 100644 index 0000000..4ca56ac --- /dev/null +++ b/src/geometry.rs @@ -0,0 +1,525 @@ +//! Geometric primitives. + +use std::borrow::Borrow; +use std::ops::{Add, Div, Mul, Sub}; + +/// Represents an angle in degrees or radians. +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Angle(f32); + +impl Angle { + /// Angle of zero degrees. + pub const ZERO: Self = Self(0.); + + /// Creates a new angle in degrees. + pub fn degrees(degrees: f32) -> Self { + Self(degrees * std::f32::consts::PI / 180.) + } + + /// Creates a new angle in radians. + pub fn radians(radians: f32) -> Self { + Self(radians) + } + + /// Returns the angle in radians. + pub fn to_radians(self) -> f32 { + self.0 + } + + /// Returns the angle in degrees. + pub fn to_degrees(self) -> f32 { + self.0 * 180. / std::f32::consts::PI + } +} + +/// Two dimensional vector. +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Vector { + pub x: f32, + pub y: f32, +} + +impl Vector { + /// Vector with both components set to zero. + pub const ZERO: Self = Self { x: 0., y: 0. }; + + /// Creates a new vector with the specified coordinates. + #[inline] + pub const fn new(x: f32, y: f32) -> Self { + Self { x, y } + } + + /// Returns the length of the vector. + #[inline] + pub fn length(self) -> f32 { + (self.x * self.x + self.y * self.y).sqrt() + } + + /// Returns the squared length of the vector. + #[inline] + pub fn length_squared(self) -> f32 { + self.x * self.x + self.y * self.y + } + + /// Returns the distance between two points. + #[inline] + pub fn distance_to(self, other: Self) -> f32 { + (self - other).length() + } + + /// Computes the dot product of two vectors. + #[inline] + pub fn dot(self, other: Self) -> f32 { + self.x * other.x + self.y * other.y + } + + /// Computes the cross product of two vectors. + #[inline] + pub fn cross(self, other: Self) -> f32 { + self.x * other.y - self.y * other.x + } + + /// Returns a normalized copy of the vector. + #[inline] + pub fn normalize(self) -> Self { + let length = self.length(); + if length == 0. { + return Self::new(0., 0.); + } + let inverse = 1. / length; + Self::new(self.x * inverse, self.y * inverse) + } + + /// Returns a new vector containing the smallest integer values greater than + /// or equal to each component. + pub fn ceil(self) -> Self { + Self::new(self.x.ceil(), self.y.ceil()) + } + + /// Returns a new vector containing the largest integer values less than + /// or equal to each component. + pub fn floor(self) -> Self { + Self::new(self.x.floor(), self.y.floor()) + } + + /// Returns the angle to the specified vector. + pub fn angle_to(self, other: Self) -> Angle { + Angle::radians(self.cross(other).atan2(self.dot(other))) + } + + /// Returns true if this vector is approximately equal to other using a + /// standard single precision epsilon value. + #[inline] + pub fn nearly_eq(self, other: Vector) -> bool { + self.nearly_eq_by(other, f32::EPSILON) + } + + /// Returns true if this vector is approximately equal to other using + /// the specified epsilon value. + #[inline] + pub fn nearly_eq_by(self, other: Vector, epsilon: f32) -> bool { + (self.x - other.x).abs() < epsilon && (self.y - other.y).abs() < epsilon + } +} + +impl Add for Vector { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self::new(self.x + rhs.x, self.y + rhs.y) + } +} + +impl Sub for Vector { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self::new(self.x - rhs.x, self.y - rhs.y) + } +} + +impl Mul for Vector { + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + Self::new(self.x * rhs.x, self.y * rhs.y) + } +} + +impl Mul for Vector { + type Output = Self; + #[inline] + fn mul(self, rhs: f32) -> Self { + Self::new(self.x * rhs, self.y * rhs) + } +} + +impl Div for Vector { + type Output = Self; + #[inline] + fn div(self, rhs: Self) -> Self { + Self::new(self.x / rhs.x, self.y / rhs.y) + } +} + +impl Div for Vector { + type Output = Self; + #[inline] + fn div(self, rhs: f32) -> Self { + let s = 1. / rhs; + Self::new(self.x * s, self.y * s) + } +} + +impl From<[f32; 2]> for Vector { + fn from(v: [f32; 2]) -> Self { + Self::new(v[0], v[1]) + } +} + +impl From<[i32; 2]> for Vector { + fn from(v: [i32; 2]) -> Self { + Self::new(v[0] as f32, v[1] as f32) + } +} + +impl From<(f32, f32)> for Vector { + fn from(v: (f32, f32)) -> Self { + Self::new(v.0, v.1) + } +} + +impl From<(i32, i32)> for Vector { + fn from(v: (i32, i32)) -> Self { + Self::new(v.0 as f32, v.1 as f32) + } +} + +impl From<(f32, i32)> for Vector { + fn from(v: (f32, i32)) -> Self { + Self::new(v.0, v.1 as f32) + } +} + +impl From<(i32, f32)> for Vector { + fn from(v: (i32, f32)) -> Self { + Self::new(v.0 as f32, v.1) + } +} + +impl From for Vector { + fn from(x: f32) -> Self { + Self::new(x, x) + } +} + +impl From for Vector { + fn from(x: i32) -> Self { + let x = x as f32; + Self::new(x, x) + } +} + +impl From for [f32; 2] { + fn from(v: Vector) -> Self { + [v.x, v.y] + } +} + +impl From for (f32, f32) { + fn from(v: Vector) -> Self { + (v.x, v.y) + } +} + +/// Alias for vector to distinguish intended use. +pub type Point = Vector; + +#[inline(always)] +pub(super) fn normal(start: Vector, end: Vector) -> Vector { + Vector::new(end.y - start.y, -(end.x - start.x)).normalize() +} + +/// Two dimensional transformation matrix. +#[derive(Copy, Clone, Default, Debug)] +pub struct Transform { + pub xx: f32, + pub xy: f32, + pub yx: f32, + pub yy: f32, + pub x: f32, + pub y: f32, +} + +impl Transform { + /// Identity matrix. + pub const IDENTITY: Self = Self { + xx: 1., + xy: 0., + yy: 1., + yx: 0., + x: 0., + y: 0., + }; + + /// Creates a new transform. + pub fn new(xx: f32, xy: f32, yx: f32, yy: f32, x: f32, y: f32) -> Self { + Self { + xx, + xy, + yx, + yy, + x, + y, + } + } + + /// Creates a translation transform. + pub fn translation(x: f32, y: f32) -> Self { + Self::new(1., 0., 0., 1., x, y) + } + + /// Creates a rotation transform. + pub fn rotation(angle: Angle) -> Self { + let (sin, cos) = angle.0.sin_cos(); + Self { + xx: cos, + xy: sin, + yx: -sin, + yy: cos, + x: 0., + y: 0., + } + } + + /// Creates a rotation transform around a point. + pub fn rotation_about(point: impl Into, angle: Angle) -> Self { + let p = point.into(); + Self::translation(p.x, p.y) + .then_rotate(angle) + .then_translate(-p.x, -p.y) + } + + /// Creates a scale transform. + pub fn scale(x: f32, y: f32) -> Self { + Self::new(x, 0., 0., y, 0., 0.) + } + + /// Creates a skew transform. + pub fn skew(x: Angle, y: Angle) -> Self { + Self { + xx: 1., + xy: y.0.tan(), + yx: x.0.tan(), + yy: 1., + x: 0., + y: 0., + } + } + + fn combine(a: &Transform, b: &Transform) -> Self { + let xx = a.xx * b.xx + a.yx * b.xy; + let yx = a.xx * b.yx + a.yx * b.yy; + let xy = a.xy * b.xx + a.yy * b.xy; + let yy = a.xy * b.yx + a.yy * b.yy; + let x = a.x * b.xx + a.y * b.xy + b.x; + let y = a.x * b.yx + a.y * b.yy + b.y; + Self { + xx, + yx, + xy, + yy, + x, + y, + } + } + + /// Returns a new transform that represents the application of this transform + /// followed by other. + pub fn then(&self, other: &Transform) -> Self { + Self::combine(self, other) + } + + /// Returns a new transform that represents a translation followed by this + /// transform. + pub fn pre_translate(&self, x: f32, y: f32) -> Self { + Self::combine(&Self::translation(x, y), self) + } + + /// Returns a new transform that represents this transform followed by a + /// translation. + pub fn then_translate(&self, x: f32, y: f32) -> Self { + let mut t = *self; + t.x += x; + t.y += y; + t + } + + /// Returns a new transform that represents a rotation followed by this + /// transform. + pub fn pre_rotate(&self, angle: Angle) -> Self { + Self::combine(&Self::rotation(angle), self) + } + + /// Returns a new transform that represents this transform followed by a + /// rotation. + pub fn then_rotate(&self, angle: Angle) -> Self { + Self::combine(self, &Self::rotation(angle)) + } + + /// Returns a new transform that represents a scale followed by this + /// transform. + pub fn pre_scale(&self, x: f32, y: f32) -> Self { + Self::combine(&Self::scale(x, y), self) + } + + /// Returns a new transform that represents this transform followed by a + /// scale. + pub fn then_scale(&self, x: f32, y: f32) -> Self { + Self::combine(self, &Self::scale(x, y)) + } + + /// Returns the determinant of the transform. + pub fn determinant(&self) -> f32 { + self.xx * self.yy - self.yx * self.xy + } + + /// Returns the inverse of the transform, if any. + pub fn invert(&self) -> Option { + let det = self.determinant(); + if !det.is_finite() || det == 0. { + return None; + } + let s = 1. / det; + let a = self.xx; + let b = self.xy; + let c = self.yx; + let d = self.yy; + let x = self.x; + let y = self.y; + Some(Transform { + xx: d * s, + xy: -b * s, + yx: -c * s, + yy: a * s, + x: (b * y - d * x) * s, + y: (c * x - a * y) * s, + }) + } + + /// Returns the result of applying this transform to a point. + #[inline(always)] + pub fn transform_point(&self, point: Point) -> Point { + Vector { + x: (point.x * self.xx + point.y * self.yx) + self.x, + y: (point.x * self.xy + point.y * self.yy) + self.y, + } + } + + /// Returns the result of applying this transform to a vector. + #[inline(always)] + pub fn transform_vector(&self, vector: Vector) -> Vector { + Vector { + x: (vector.x * self.xx + vector.y * self.yx), + y: (vector.x * self.xy + vector.y * self.yy), + } + } +} + +/// Axis-aligned bounding box. +#[derive(Copy, Clone, Default, Debug)] +pub struct Bounds { + pub min: Point, + pub max: Point, +} + +impl Bounds { + /// Creates a new bounding box from minimum and maximum points. + pub fn new(min: Point, max: Point) -> Self { + Self { min, max } + } + + /// Creates a new bounding box from a sequence of points. + pub fn from_points(points: I) -> Self + where + I: IntoIterator, + I::Item: Borrow, + { + let mut b = BoundsBuilder::new(); + for p in points { + b.add(*p.borrow()); + } + b.build() + } + + /// Returns true if the bounding box is empty. + pub fn is_empty(&self) -> bool { + self.min.x >= self.max.x || self.min.y >= self.max.y + } + + /// Returns the width of the bounding box. + pub fn width(&self) -> f32 { + self.max.x - self.min.x + } + + /// Returns the height of the bounding box. + pub fn height(&self) -> f32 { + self.max.y - self.min.y + } + + /// Returns true if the box contains the specified point. + pub fn contains(&self, point: impl Into) -> bool { + let p = point.into(); + p.x > self.min.x && p.x < self.max.x && p.y > self.min.y && p.y < self.max.y + } +} + +pub(super) struct BoundsBuilder { + pub count: usize, + pub start: Point, + pub current: Point, + pub min: Point, + pub max: Point, +} + +impl BoundsBuilder { + pub fn new() -> Self { + Self { + count: 0, + start: Point::ZERO, + current: Point::ZERO, + min: Point::new(f32::MAX, f32::MAX), + max: Point::new(f32::MIN, f32::MIN), + } + } + + pub fn add(&mut self, p: Point) -> &mut Self { + let x = p.x; + let y = p.y; + if x < self.min.x { + self.min.x = x; + } + if x > self.max.x { + self.max.x = x; + } + if y < self.min.y { + self.min.y = y; + } + if y > self.max.y { + self.max.y = y; + } + self.count += 1; + self + } + + pub fn build(&self) -> Bounds { + if self.count != 0 { + Bounds { + min: self.min, + max: self.max, + } + } else { + Bounds::default() + } + } +} diff --git a/src/hit_test.rs b/src/hit_test.rs new file mode 100644 index 0000000..88cd63e --- /dev/null +++ b/src/hit_test.rs @@ -0,0 +1,90 @@ +//! Hit testing. + +use super::geometry::{Point, Transform}; +use super::mask::Mask; +use super::path_data::PathData; +use super::scratch::Scratch; +use super::style::{Fill, Style}; + +use std::cell::RefCell; + +/// Builder for configuring and executing a hit test. +pub struct HitTest<'a, 's, D> { + data: D, + style: Style<'a>, + transform: Option, + threshold: u8, + scratch: RefCell>, +} + +impl<'a, 's, D> HitTest<'a, 's, D> +where + D: PathData, +{ + /// Creates a new hit test builder for the specified path data. + pub fn new(data: D) -> Self { + Self { + data, + style: Style::Fill(Fill::NonZero), + transform: None, + threshold: 0, + scratch: RefCell::new(None), + } + } + + /// Creates a new hit test builder for the specified path data and scratch memory. + pub fn with_scratch(data: D, scratch: &'s mut Scratch) -> Self { + Self { + data, + style: Style::Fill(Fill::NonZero), + transform: None, + threshold: 0, + scratch: RefCell::new(Some(scratch)), + } + } + + /// Sets the style of the path. + pub fn style(&mut self, style: impl Into>) -> &mut Self { + self.style = style.into(); + self + } + + /// Sets the transformation matrix of the path. + pub fn transform(&mut self, transform: Option) -> &mut Self { + self.transform = transform; + self + } + + /// Sets the threshold value for determining whether a hit test registers. + pub fn threshold(&mut self, threshold: u8) -> &mut Self { + self.threshold = threshold; + self + } + + /// Returns true if the specified point is painted by the path. + pub fn test(&self, point: impl Into) -> bool { + let mut scratch = self.scratch.borrow_mut(); + let mut buf = [0u8; 1]; + let p = point.into() * -1.; + if let Some(scratch) = scratch.as_mut() { + Mask::with_scratch(&self.data, scratch) + .style(self.style) + .offset(p) + .transform(self.transform) + .size(1, 1) + .render_into(&mut buf, None); + } else { + Mask::new(&self.data) + .style(self.style) + .offset(p) + .transform(self.transform) + .size(1, 1) + .render_into(&mut buf, None); + } + if self.threshold == 0xFF { + buf[0] >= self.threshold + } else { + buf[0] > self.threshold + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4a60e32 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,293 @@ +/*! +This crate provides a high performance, low level 2D rasterization library +with support for rendering paths of various styles into alpha or subpixel +masks. + +Broadly speaking, support is provided for the following: +- 256x anti-aliased rasterization (8-bit alpha or 32-bit RGBA subpixel alpha) +- Pixel perfect hit testing with customizable coverage threshold +- Non-zero and even-odd fills +- Stroking with the standard set of joins and caps + (separate start and end caps are possible) +- Numerically stable dashing for smooth dash offset animation +- Vertex traversal for marker placement +- Stepped distance traversal for animation or text-on-path support +- Abstract representation of path data that imposes no policy on storage + +While this crate is general purpose, in the interest of interoperability and +familiarity, the feature set was chosen specifically to accommodate the +requirements of the +[SVG path specification](https://www.w3.org/TR/SVG/paths.html). + +Furthermore, the rasterized masks are nearly identical to those generated by +Skia (sans slight AA differences) and as such, should yield images that are +equivalent to those produced by modern web browsers. + +# Rendering + +Due to the large configuration space for styling and rendering paths, the +builder pattern is used pervasively. The [Mask](struct.Mask.html) struct is the +builder used for rasterization. For example, to render a simple triangle +into a 64x64 8-bit alpha mask: + +```rust +use zeno::{Mask, PathData}; + +// The target buffer that will contain the mask +let mut mask = [0u8; 64 * 64]; + +// Create a new mask with some path data +Mask::new("M 8,56 32,8 56,56 Z") + // Choose an explicit size for the target + .size(64, 64) + // Finally, render the path into the target + .render_into(&mut mask, None); +``` + +Note that, in this case, the path itself is supplied as a string in SVG path +data format. This crate provides several different kinds of path data by +default along with support for custom implementations. See the +[PathData](trait.PathData.html) trait for more detail. + +The previous example did not provide a style, so a non-zero +[Fill](enum.Fill.html) was chosen by default. Let's render the same path with +a 4 pixel wide stroke and a round line join: + +```rust +use zeno::{Join, Mask, PathData, Stroke}; + +let mut mask = [0u8; 64 * 64]; + +Mask::new("M 8,56 32,8 56,56 Z") + .size(64, 64) + .style(Stroke::new(4.0).join(Join::Round)) + .render_into(&mut mask, None); +``` + +Or to make it a bit more dashing: + +```rust +use zeno::{Cap, Join, Mask, PathData, Stroke}; + +let mut mask = [0u8; 64 * 64]; + +Mask::new("M 8,56 32,8 56,56 Z") + .style( + Stroke::new(4.0) + .join(Join::Round) + .cap(Cap::Round) + // dash accepts a slice of dash lengths and an initial dash offset + .dash(&[10.0, 12.0, 0.0], 0.0), + ) + .size(64, 64) + .render_into(&mut mask, None); +``` + +See the [Stroke](struct.Stroke.html) builder struct for all available options. + +So far, we've generated our masks into fixed buffers with explicit sizes. It is +often the case that it is preferred to ignore all empty space and render a path +into a tightly bound mask of dynamic size. This can be done by eliding the call +for the size method: + +```rust +use zeno::{Mask, PathData}; + +// Dynamic buffer that will contain the mask +let mut mask = Vec::new(); + +let placement = Mask::new("M 8,56 32,8 56,56 Z") + // Insert an inspect call here to access the computed dimensions + .inspect(|format, width, height| { + // Make sure our buffer is the correct size + mask.resize(format.buffer_size(width, height), 0); + }) + .render_into(&mut mask, None); +``` + +The call to size has been replaced with a call to inspect which injects a +closure into the call chain giving us the opportunity to extend our buffer to +the appropriate size. Note also that the render method has a return value that +has been captured here. This [Placement](struct.Placement.html) struct +describes the dimensions of the resulting mask along with an offset that should +be applied during composition to compensate for the removal of any empty space. + +Finally, it is possible to render without a target buffer, in which case the +rasterizer will allocate and return a new `Vec` containing the mask: + +```rust +use zeno::{Mask, PathData}; + +// mask is a Vec +let (mask, placement) = Mask::new("M 8,56 32,8 56,56 Z") + // Calling render() instead of render_into() will allocate a buffer + // for you that is returned along with the placement + .render(); +``` + +Both [Mask](struct.Mask.html) and [Stroke](struct.Stroke.html) offer large +sets of options for fine-grained control of styling and rasterization including +offsets, scaling, transformations, formats, coordinate spaces and more. See +their respective documentation for more detail. + +# Hit testing + +Hit testing is the process of determining if a point is within the region that +would be painted by the path. A typical use case is to determine if a user's +cursor is hovering over a particular path. The process generally follows the +same form as rendering: + +```rust +use zeno::{HitTest, PathData}; + +// A 20x10 region with the right half covered by the path +let hit_test = HitTest::new("M10,0 10,10 20,10 20,0 Z"); + +assert_eq!(hit_test.test([15, 5]), true); +assert_eq!(hit_test.test([5, 5]), false); +``` + +Due to the fact that paths are anti-aliased, the hit test builder offers a +threshold option that determines how much "coverage" is required for a hit test +to pass at a particular point. + +```rust +use zeno::{HitTest, PathData}; + +let mut hit_test = HitTest::new("M2.5,0 2.5,2 5,2 5,0 Z"); + +// Require full coverage for a successful hit test +hit_test.threshold(255); +assert_eq!(hit_test.test([2, 0]), false); + +// Succeed for any non-zero coverage +hit_test.threshold(0); +assert_eq!(hit_test.test([2, 0]), true); +``` + +See the [HitTest](struct.HitTest.html) type for more detail. + +# Path building + +While SVG paths are a reasonable choice for static storage, there sometimes +arise cases where paths must be built dynamically at runtime: + +```rust +use zeno::{Command, Mask, PathBuilder, PathData}; + +// Create a vector to store the path commands +let mut path: Vec = Vec::new(); + +// Construct the path with chained method calls +path.move_to([8, 56]).line_to([32, 8]).line_to([56, 56]).close(); + +// Ensure it is equal to the equivalent SVG path +assert!((&path).commands().eq("M 8,56 32,8 56,56 Z".commands())); + +// &Vec is also valid path data +Mask::new(&path).render(); // ... +``` + +Here, a vector of [Command](enum.Command.html)s is used to store the path data +and the [PathBuilder](trait.PathBuilder.html) trait provides the extension +methods necessary for building a path. + +Beyond the four basic path commands, the path builder trait also provides +arcs (and position relative versions of all previous commands) along with +rectangles, round rectangles, ellipses and circles: + +```rust +use zeno::{Angle, ArcSize, ArcSweep, Command, PathBuilder, PathData}; + +let mut path: Vec = Vec::new(); + +path.move_to([1, 2]).rel_arc_to( + 8.0, + 4.0, + Angle::degrees(30.0), + ArcSize::Small, + ArcSweep::Positive, + [10, 4], +); + +assert!((&path).commands().eq("M1,2 a8,4,30,0,1,10,4".commands())); +``` + +Along with incremental building of paths, path builder can also be used as a +"sink" for capturing the result of the application of a style and transform +to some path data. For example, it is possible to store the output of a stroke +style to avoid the cost of stroke evaluation for future rendering or hit test +operations with the use of the [apply](fn.apply.html) function: + +```rust +use zeno::{apply, Cap, Command, PathBuilder, PathData, Stroke}; + +let mut stroke: Vec = Vec::new(); + +apply("L10,0", Stroke::new(4.0).cap(Cap::Round), None, &mut stroke); +``` + +[PathBuilder](struct.PathBuilder.html) is only implemented for `Vec` +by default, but custom implementations are possible to support capturing +and building paths into other data structures. + +# Traversal + +Path traversal involves incremental evaluation of a path by some metric. This +crate currently provides two methods of traversal. + +The [Vertices](struct.Vertices.html) iterator yields a variant of the +[Vertex](enum.Vertex.html) enum at the beginning and end of each subpath and +between each path command. Each variant provides all the geometric +information necessary to place SVG style markers. + +The [Walk](struct.Walk.html) type is an iterator-like type that allows for +stepping along the path by arbitrary distances. Each step yields the position +on the path at the next distance along with a vector describing the +left-ward direction from the path at that point. This is useful for animating +objects along a path, or for rendering text attached to a path. + +# Transient memory allocations + +The algorithms in this crate make a concerted effort to avoid dynamic +allocations where possible, but paths of significant size or complexity +may cause spills into temporary heap memory. Specifically, stoke evaluation +and rasterization may cause heap allocations. + +To amortize the cost of these, the appropriately named +[Scratch](struct.Scratch.html) struct is available. This type contains internal +heap allocated storage and provides replacement methods for functions that may +allocate. In addition, the +[Mask::with_scratch](struct.Mask.html#method.with_scratch) and +[HitTest::with_scratch](struct.HitTest.html#method.with_scratch) +constructors are provided which take a scratch instance as an argument and +redirect all transient allocations to the reusable storage. + */ + +mod command; +mod fill; +mod geometry; +mod hit_test; +mod mask; +mod path_builder; +mod path_data; +mod raster; +mod scratch; +mod segment; +mod stroke; +mod style; +mod svg_parser; +mod traversal; + +pub use command::{Command, Verb}; +pub use fill::Fill; +pub use geometry::{Angle, Bounds, Point, Transform, Vector}; +pub use hit_test::HitTest; +pub use mask::{Format, Mask, Origin, Placement}; +pub use path_builder::{ArcSize, ArcSweep, PathBuilder}; +pub use path_data::{apply, bounds, length, PathData}; +pub use scratch::Scratch; +pub use stroke::{Cap, Join, Stroke}; +pub use style::Style; +pub use svg_parser::validate_svg; +pub use traversal::{Vertex, Vertices, Walk}; diff --git a/src/mask.rs b/src/mask.rs new file mode 100644 index 0000000..ea7ed13 --- /dev/null +++ b/src/mask.rs @@ -0,0 +1,512 @@ +//! Mask generator. + +use super::geometry::{Bounds, Transform, Vector}; +use super::path_data::{apply, PathData}; +use super::scratch::Scratch; +use super::style::{Fill, Style}; + +use std::cell::RefCell; + +/// The desired output image format for rendering. +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Format { + /// 8-bit alpha mask. + Alpha, + /// 32-bit RGBA subpixel mask with 1/3 pixel offsets for the red and + /// blue channels. + Subpixel, + /// 32-bit RGBA subpixel mask with custom offsets. + CustomSubpixel([f32; 3]), +} + +impl Format { + /// Creates a format for BGRA subpixel rendering. + pub fn subpixel_bgra() -> Self { + Self::CustomSubpixel([0.3, 0., -0.3]) + } + + /// Returns the necessary buffer size to hold an image of the specified + /// width and height with this format. + pub fn buffer_size(self, width: u32, height: u32) -> usize { + (width + * height + * match self { + Self::Alpha => 1, + _ => 4, + }) as usize + } +} + +impl Default for Format { + fn default() -> Self { + Self::Alpha + } +} + +/// The origin of the coordinate system for rendering. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Origin { + /// Origin (0, 0) at the top left of the image. + TopLeft, + /// Origin (0, 0) at the bottom left of the image. + BottomLeft, +} + +impl Default for Origin { + fn default() -> Self { + Self::TopLeft + } +} + +/// Describes the offset and dimensions of a rendered mask. +#[derive(Copy, Clone, Debug, Default)] +pub struct Placement { + /// Horizontal offset with respect to the origin specified when computing + /// the placement. + pub left: i32, + /// Vertical offset with respect to the origin specified when computing + /// the placement. + pub top: i32, + /// Width in pixels. + pub width: u32, + /// Height in pixels. + pub height: u32, +} + +impl Placement { + /// Given an origin, offset and bounding box, computes the resulting offset + /// and placement for a tightly bounded mask. + pub fn compute( + origin: Origin, + offset: impl Into, + bounds: &Bounds, + ) -> (Vector, Placement) { + let offset = offset.into(); + let mut bounds = *bounds; + bounds.min = (bounds.min + offset).floor(); + bounds.max = (bounds.max + offset).ceil(); + let offset = Vector::new(-bounds.min.x + 1., -bounds.min.y); + let width = bounds.width() as u32 + 2; + let height = bounds.height() as u32; + let left = -offset.x as i32; + let top = if origin == Origin::BottomLeft { + (-offset.y).floor() + height as f32 + } else { + -offset.y + } as i32; + ( + offset, + Placement { + left, + top, + width, + height, + }, + ) + } +} + +/// Builder for configuring and rendering a mask. +pub struct Mask<'a, 's, D> { + data: D, + style: Style<'a>, + transform: Option, + format: Format, + origin: Origin, + offset: Vector, + render_offset: Vector, + width: u32, + height: u32, + explicit_size: bool, + has_size: bool, + bounds_offset: Vector, + scratch: RefCell>, +} + +impl<'a, 's, D> Mask<'a, 's, D> +where + D: PathData, +{ + /// Creates a new mask builder for the specified path data. + pub fn new(data: D) -> Self { + Self { + data, + style: Style::Fill(Fill::NonZero), + transform: None, + format: Format::Alpha, + origin: Origin::TopLeft, + offset: Vector::ZERO, + render_offset: Vector::ZERO, + width: 0, + height: 0, + explicit_size: false, + has_size: false, + bounds_offset: Vector::ZERO, + scratch: RefCell::new(None), + } + } + + /// Creates a new mask builder for the specified path data and scratch memory. + pub fn with_scratch(data: D, scratch: &'s mut Scratch) -> Self { + Self { + data, + style: Style::Fill(Fill::NonZero), + transform: None, + format: Format::Alpha, + origin: Origin::TopLeft, + offset: Vector::ZERO, + render_offset: Vector::ZERO, + width: 0, + height: 0, + explicit_size: false, + has_size: false, + bounds_offset: Vector::ZERO, + scratch: RefCell::new(Some(scratch)), + } + } + + /// Sets the style of the path. The default is a non-zero fill. + pub fn style(&mut self, style: impl Into>) -> &mut Self { + self.style = style.into(); + self + } + + /// Sets the transformation matrix of the path. + pub fn transform(&mut self, transform: Option) -> &mut Self { + self.transform = transform; + self + } + + /// Sets the desired format of the mask. The default value is an 8-bit + /// alpha format. + pub fn format(&mut self, format: Format) -> &mut Self { + self.format = format; + self + } + + /// Sets the origin that defines the coordinate system for the mask. + pub fn origin(&mut self, origin: Origin) -> &mut Self { + self.origin = origin; + self + } + + /// Sets the offset for the path. + pub fn offset(&mut self, offset: impl Into) -> &mut Self { + self.offset = offset.into(); + self + } + + /// Sets an explicit size for the mask. If left unspecified, the size will + /// be computed from the bounding box of the path after applying any + /// relevant style, offset and transform. + pub fn size(&mut self, width: u32, height: u32) -> &mut Self { + self.width = width; + self.height = height; + self.explicit_size = true; + self.has_size = true; + self + } + + /// Sets an additional rendering offset for the mask. This offset does not + /// affect bounds or size computations and is only applied during + /// rendering. + pub fn render_offset(&mut self, offset: impl Into) -> &mut Self { + self.render_offset = offset.into(); + self + } + + /// Invokes a closure with the format, width and height of the mask provided + /// as arguments. This is primarily useful for preparing a target buffer without + /// interrupting the call chain. + pub fn inspect(&mut self, mut f: impl FnMut(Format, u32, u32)) -> &mut Self { + self.ensure_size(); + f(self.format, self.width, self.height); + self + } + + /// Renders the mask into a byte buffer. If specified, the pitch describes + /// the number of bytes between subsequent rows of the target buffer. This + /// is primarily useful for rendering into tiled images such as texture + /// atlases. If left unspecified, the buffer is assumed to be linear and + /// tightly packed. + pub fn render_into(&self, buffer: &mut [u8], pitch: Option) -> Placement { + let (offset, placement) = self.placement(); + let pitch = match pitch { + Some(pitch) => pitch, + _ => { + placement.width as usize + * match self.format { + Format::Alpha => 1, + _ => 4, + } + } + }; + render(self, offset, &placement, buffer, pitch); + placement + } + + /// Renders the mask to a newly allocated buffer. + pub fn render(&self) -> (Vec, Placement) { + let mut buf = Vec::new(); + let (offset, placement) = self.placement(); + buf.resize( + self.format.buffer_size(placement.width, placement.height), + 0, + ); + let pitch = placement.width as usize + * match self.format { + Format::Alpha => 1, + _ => 4, + }; + render(self, offset, &placement, &mut buf, pitch); + (buf, placement) + } + + fn ensure_size(&mut self) { + if self.has_size { + return; + } + let (offset, placement) = self.placement(); + self.bounds_offset = offset; + self.width = placement.width; + self.height = placement.height; + self.explicit_size = false; + self.has_size = true; + } + + fn placement(&self) -> (Vector, Placement) { + let mut placement = Placement { + left: 0, + top: 0, + width: self.width, + height: self.height, + }; + let mut offset = self.offset; + if self.explicit_size { + return (offset, placement); + } else if !self.has_size { + let mut scratch = self.scratch.borrow_mut(); + let mut bounds = if let Some(scratch) = scratch.as_mut() { + scratch.bounds(&self.data, self.style, self.transform) + } else { + super::bounds(&self.data, self.style, self.transform) + }; + bounds.min = (bounds.min + self.offset).floor(); + bounds.max = (bounds.max + self.offset).ceil(); + offset = Vector::new(-bounds.min.x + 1., -bounds.min.y); + placement.width = bounds.width() as u32 + 2; + placement.height = bounds.height() as u32; + } else { + offset = self.bounds_offset; + } + placement.left = -offset.x as i32; + placement.top = if self.origin == Origin::BottomLeft { + (-offset.y).floor() + self.height as f32 + } else { + -offset.y + } as i32; + (offset, placement) + } +} + +pub fn render<'a, 'c, D>( + mask: &'a Mask<'a, 'c, D>, + offset: Vector, + placement: &Placement, + buf: &mut [u8], + pitch: usize, +) where + D: PathData, +{ + let y_up = mask.origin == Origin::BottomLeft; + let (is_subpx, subpx) = match mask.format { + Format::Alpha => (false, [Vector::ZERO; 3]), + Format::Subpixel => ( + true, + [Vector::new(-0.3, 0.), Vector::ZERO, Vector::new(0.3, 0.)], + ), + Format::CustomSubpixel(subpx) => ( + true, + [ + Vector::new(subpx[0], 0.), + Vector::new(subpx[1], 0.), + Vector::new(subpx[2], 0.), + ], + ), + }; + let fill = match mask.style { + Style::Fill(fill) => fill, + _ => Fill::NonZero, + }; + let w = placement.width; + let h = placement.height; + let shift = offset + mask.render_offset; + let data = &mask.data; + let style = mask.style; + let transform = mask.transform; + let mut scratch = mask.scratch.borrow_mut(); + use super::raster::{AdaptiveStorage, Rasterizer}; + if let Some(scratch) = scratch.as_mut() { + let mut ras = Rasterizer::new(&mut scratch.render); + let inner = &mut scratch.inner; + if is_subpx { + ras.rasterize_write( + shift + subpx[0], + w, + h, + &mut |r| { + inner.apply(data.clone(), &style, transform, r); + }, + fill, + pitch, + y_up, + &mut |row_offset, x, count, coverage| { + let buf = &mut buf[row_offset..]; + let mut i = 0; + let mut j = x * 4; + while i < count { + buf[j] = coverage; + i += 1; + j += 4; + } + }, + ); + ras.rasterize_write( + shift + subpx[1], + w, + h, + &mut |r| { + inner.apply(data, &style, transform, r); + }, + fill, + pitch, + y_up, + &mut |row_offset, x, count, coverage| { + let buf = &mut buf[row_offset..]; + let mut i = 0; + let mut j = x * 4 + 1; + while i < count { + buf[j] = coverage; + i += 1; + j += 4; + } + }, + ); + ras.rasterize_write( + shift + subpx[2], + w, + h, + &mut |r| { + inner.apply(data, &style, transform, r); + }, + fill, + pitch, + y_up, + &mut |row_offset, x, count, coverage| { + let buf = &mut buf[row_offset..]; + let mut i = 0; + let mut j = x * 4 + 2; + while i < count { + buf[j] = coverage; + i += 1; + j += 4; + } + }, + ); + } else { + ras.rasterize( + shift, + w, + h, + &mut |r| { + inner.apply(data, &style, transform, r); + }, + fill, + buf, + pitch, + y_up, + ); + } + } else { + let mut storage = AdaptiveStorage::new(); + let mut ras = Rasterizer::new(&mut storage); + if is_subpx { + ras.rasterize_write( + shift + subpx[0], + w, + h, + &mut |r| { + apply(data, style, transform, r); + }, + fill, + pitch, + y_up, + &mut |row_offset, x, count, coverage| { + let buf = &mut buf[row_offset..]; + let mut i = 0; + let mut j = x * 4; + while i < count { + buf[j] = coverage; + i += 1; + j += 4; + } + }, + ); + ras.rasterize_write( + shift + subpx[1], + w, + h, + &mut |r| { + apply(data, style, transform, r); + }, + fill, + pitch, + y_up, + &mut |row_offset, x, count, coverage| { + let buf = &mut buf[row_offset..]; + let mut i = 0; + let mut j = x * 4 + 1; + while i < count { + buf[j] = coverage; + i += 1; + j += 4; + } + }, + ); + ras.rasterize_write( + shift + subpx[2], + w, + h, + &mut |r| { + apply(data, style, transform, r); + }, + fill, + pitch, + y_up, + &mut |row_offset, x, count, coverage| { + let buf = &mut buf[row_offset..]; + let mut i = 0; + let mut j = x * 4 + 2; + while i < count { + buf[j] = coverage; + i += 1; + j += 4; + } + }, + ); + } else { + ras.rasterize( + shift, + w, + h, + &mut |r| { + apply(data, style, transform, r); + }, + fill, + buf, + pitch, + y_up, + ); + } + } +} diff --git a/src/path_builder.rs b/src/path_builder.rs new file mode 100644 index 0000000..7027b24 --- /dev/null +++ b/src/path_builder.rs @@ -0,0 +1,596 @@ +//! Path builder. + +use super::command::Command; +use super::geometry::{Angle, BoundsBuilder, Point, Transform}; + +/// Describes the size of an arc. +#[derive(Copy, Clone, PartialEq)] +pub enum ArcSize { + /// An arc of <= 180 degrees will be drawn. + Small, + /// An arc of >= 180 degrees will be drawn. + Large, +} + +/// Describes the sweep direction for an arc. +#[derive(Copy, Clone, PartialEq)] +pub enum ArcSweep { + /// The arc is drawn in a positive angle direction. + Positive, + /// The arc is drawn in a negative angle direction. + Negative, +} + +/// Trait for types that accept path commands. +pub trait PathBuilder: Sized { + /// Returns the current point of the path. + fn current_point(&self) -> Point; + + /// Moves to the specified point, beginning a new subpath. + fn move_to(&mut self, to: impl Into) -> &mut Self; + + /// Moves to the specified point, relative to the current point, + /// beginning a new subpath. + fn rel_move_to(&mut self, to: impl Into) -> &mut Self { + self.move_to(to.into() + self.current_point()) + } + + /// Adds a line to the specified point. This will begin a new subpath + /// if the path is empty or the previous subpath was closed. + fn line_to(&mut self, to: impl Into) -> &mut Self; + + /// Adds a line to the specified point, relative to the current point. This + /// will begin a new subpath if the path is empty or the previous subpath + /// was closed. + fn rel_line_to(&mut self, to: impl Into) -> &mut Self { + self.line_to(to.into() + self.current_point()) + } + + /// Adds a cubic bezier curve from the current point through the specified + /// control points to the final point. This will begin a new subpath if the + /// path is empty or the previous subpath was closed. + fn curve_to( + &mut self, + control1: impl Into, + control2: impl Into, + to: impl Into, + ) -> &mut Self; + + /// Adds a cubic bezier curve from the current point through the specified + /// control points to the final point. All points are considered relative to the + /// current point. This will begin a new subpath if the path is empty or the + /// previous subpath was closed. + fn rel_curve_to( + &mut self, + control1: impl Into, + control2: impl Into, + to: impl Into, + ) -> &mut Self { + let r = self.current_point(); + self.curve_to(control1.into() + r, control2.into() + r, to.into() + r) + } + + /// Adds a quadratic bezier curve from the current point through the specified + /// control point to the final point. This will begin a new subpath if the + /// path is empty or the previous subpath was closed. + fn quad_to(&mut self, control1: impl Into, to: impl Into) -> &mut Self; + + /// Adds a quadratic bezier curve from the current point through the specified + /// control point to the final point. All points are considered relative to the + /// current point. This will begin a new subpath if the path is empty or the + /// previous subpath was closed. + fn rel_quad_to(&mut self, control: impl Into, to: impl Into) -> &mut Self { + let r = self.current_point(); + self.quad_to(control.into() + r, to.into() + r) + } + + /// Adds an arc with the specified x- and y-radius, rotation angle, arc size, + /// and arc sweep from the current point to the specified end point. The center + /// point of the arc will be computed from the parameters. This will begin a + /// new subpath if the path is empty or the previous subpath was closed. + fn arc_to( + &mut self, + rx: f32, + ry: f32, + angle: Angle, + size: ArcSize, + sweep: ArcSweep, + to: impl Into, + ) -> &mut Self { + let from = self.current_point(); + arc( + self, + from, + rx, + ry, + angle.to_radians(), + size, + sweep, + to.into(), + ); + self + } + + /// Adds an arc with the specified x- and y-radius, rotation angle, arc size, + /// and arc sweep from the current point to the specified end point. The end + /// point is considered relative to the current point. The center point of the + /// arc will be computed from the parameters. This will begin a new subpath if + /// the path is empty or the previous subpath was closed. + fn rel_arc_to( + &mut self, + rx: f32, + ry: f32, + angle: Angle, + size: ArcSize, + sweep: ArcSweep, + to: impl Into, + ) -> &mut Self { + self.arc_to(rx, ry, angle, size, sweep, to.into() + self.current_point()) + } + + /// Closes the current subpath. + fn close(&mut self) -> &mut Self; + + /// Adds a rectangle with the specified position and size to the path. This + /// will create a new closed subpath. + fn add_rect(&mut self, xy: impl Into, w: f32, h: f32) -> &mut Self { + let p = xy.into(); + let (l, t, r, b) = (p.x, p.y, p.x + w, p.y + h); + self.move_to(p); + self.line_to((r, t)); + self.line_to((r, b)); + self.line_to((l, b)); + self.close() + } + + /// Adds a rounded rectangle with the specified position, size and radii to + /// the path. This will create a new closed subpath. + fn add_round_rect( + &mut self, + xy: impl Into, + w: f32, + h: f32, + rx: f32, + ry: f32, + ) -> &mut Self { + let p = xy.into(); + let size = ArcSize::Small; + let sweep = ArcSweep::Positive; + let a = Angle::radians(0.); + let hw = w * 0.5; + let rx = rx.max(0.).min(hw); + let hh = h * 0.5; + let ry = ry.max(0.).min(hh); + self.move_to((p.x + rx, p.y)); + self.line_to((p.x + w - rx, p.y)); + self.arc_to(rx, ry, a, size, sweep, (p.x + w, p.y + ry)); + self.line_to((p.x + w, p.y + h - ry)); + self.arc_to(rx, ry, a, size, sweep, (p.x + w - rx, p.y + h)); + self.line_to((p.x + rx, p.y + h)); + self.arc_to(rx, ry, a, size, sweep, (p.x, p.y + h - ry)); + self.line_to((p.x, p.y + ry)); + self.arc_to(rx, ry, a, size, sweep, (p.x + rx, p.y)); + self.close() + } + + /// Adds an ellipse with the specified center and radii to the path. This + /// will create a new closed subpath. + fn add_ellipse(&mut self, center: impl Into, rx: f32, ry: f32) -> &mut Self { + let center = center.into(); + let cx = center.x; + let cy = center.y; + let a = 0.551915024494; + let arx = a * rx; + let ary = a * ry; + self.move_to((cx + rx, cy)); + self.curve_to((cx + rx, cy + ary), (cx + arx, cy + ry), (cx, cy + ry)); + self.curve_to((cx - arx, cy + ry), (cx - rx, cy + ary), (cx - rx, cy)); + self.curve_to((cx - rx, cy - ary), (cx - arx, cy - ry), (cx, cy - ry)); + self.curve_to((cx + arx, cy - ry), (cx + rx, cy - ary), (cx + rx, cy)); + self.close() + } + + /// Adds a circle with the specified center and radius to the path. This + /// will create a new closed subpath. + fn add_circle(&mut self, center: impl Into, r: f32) -> &mut Self { + self.add_ellipse(center, r, r) + } +} + +impl PathBuilder for Vec { + fn current_point(&self) -> Point { + match self.last() { + None => Point::ZERO, + Some(cmd) => match cmd { + Command::MoveTo(p) + | Command::LineTo(p) + | Command::QuadTo(_, p) + | Command::CurveTo(_, _, p) => *p, + Command::Close => { + for cmd in self.iter().rev().skip(1) { + match cmd { + Command::MoveTo(p) => return *p, + _ => {} + } + } + return Point::ZERO; + } + }, + } + } + + fn move_to(&mut self, to: impl Into) -> &mut Self { + self.push(Command::MoveTo(to.into())); + self + } + + fn line_to(&mut self, to: impl Into) -> &mut Self { + self.push(Command::LineTo(to.into())); + self + } + + fn quad_to(&mut self, control: impl Into, to: impl Into) -> &mut Self { + self.push(Command::QuadTo(control.into(), to.into())); + self + } + + fn curve_to( + &mut self, + control1: impl Into, + control2: impl Into, + to: impl Into, + ) -> &mut Self { + self.push(Command::CurveTo( + control1.into(), + control2.into(), + to.into(), + )); + self + } + + fn close(&mut self) -> &mut Self { + self.push(Command::Close); + self + } +} + +pub struct TransformSink<'a, S> { + pub sink: &'a mut S, + pub transform: Transform, +} + +impl<'a, S: PathBuilder> PathBuilder for TransformSink<'a, S> { + fn current_point(&self) -> Point { + self.sink.current_point() + } + + fn move_to(&mut self, to: impl Into) -> &mut Self { + self.sink.move_to(self.transform.transform_point(to.into())); + self + } + + fn line_to(&mut self, to: impl Into) -> &mut Self { + self.sink.line_to(self.transform.transform_point(to.into())); + self + } + + fn quad_to(&mut self, control: impl Into, to: impl Into) -> &mut Self { + self.sink.quad_to( + self.transform.transform_point(control.into()), + self.transform.transform_point(to.into()), + ); + self + } + + fn curve_to( + &mut self, + control1: impl Into, + control2: impl Into, + to: impl Into, + ) -> &mut Self { + self.sink.curve_to( + self.transform.transform_point(control1.into()), + self.transform.transform_point(control2.into()), + self.transform.transform_point(to.into()), + ); + self + } + + fn close(&mut self) -> &mut Self { + self.sink.close(); + self + } +} + +impl PathBuilder for BoundsBuilder { + fn current_point(&self) -> Point { + self.current + } + + fn move_to(&mut self, to: impl Into) -> &mut Self { + let p = to.into(); + self.add(p); + self.current = p; + self + } + + fn line_to(&mut self, to: impl Into) -> &mut Self { + let p = to.into(); + self.add(p); + self.current = p; + self + } + + fn quad_to(&mut self, control: impl Into, to: impl Into) -> &mut Self { + self.add(control.into()); + let p = to.into(); + self.add(p); + self.current = p; + self + } + + fn curve_to( + &mut self, + control1: impl Into, + control2: impl Into, + to: impl Into, + ) -> &mut Self { + self.add(control1.into()); + self.add(control2.into()); + let p = to.into(); + self.add(p); + self.current = p; + self + } + + fn close(&mut self) -> &mut Self { + self + } +} + +/// An iterator that generates cubic bezier curves for an arc. +#[derive(Copy, Clone, Default)] +pub struct Arc { + count: usize, + center: (f32, f32), + radii: (f32, f32), + cosphi: f32, + sinphi: f32, + ang1: f32, + ang2: f32, + a: f32, +} + +impl Arc { + pub fn new( + from: impl Into<[f32; 2]>, + rx: f32, + ry: f32, + angle: f32, + size: ArcSize, + sweep: ArcSweep, + to: impl Into<[f32; 2]>, + ) -> Self { + let from = from.into(); + let to = to.into(); + let (px, py) = (from[0], from[1]); + const TAU: f32 = 3.141579 * 2.; + let (sinphi, cosphi) = angle.sin_cos(); + let pxp = cosphi * (px - to[0]) / 2. + sinphi * (py - to[1]) / 2.; + let pyp = -sinphi * (px - to[0]) / 2. + cosphi * (py - to[1]) / 2.; + if pxp == 0. && pyp == 0. { + return Self::default(); + } + let mut rx = rx.abs(); + let mut ry = ry.abs(); + let lambda = pxp.powi(2) / rx.powi(2) + pyp.powi(2) / ry.powi(2); + if lambda > 1. { + let s = lambda.sqrt(); + rx *= s; + ry *= s; + } + let large_arc = size == ArcSize::Large; + let sweep = sweep == ArcSweep::Positive; + let (cx, cy, ang1, mut ang2) = { + fn vec_angle(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 { + let sign = if (ux * vy - uy * vx) < 0. { -1. } else { 1. }; + let mut dot = ux * vx + uy * vy; + if dot > 1. { + dot = 1. + } else if dot < -1. { + dot = -1. + } + sign * dot.acos() + } + let rxsq = rx * rx; + let rysq = ry * ry; + let pxpsq = pxp * pxp; + let pypsq = pyp * pyp; + let mut radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq); + if radicant < 0. { + radicant = 0.; + } + radicant /= (rxsq * pypsq) + (rysq * pxpsq); + radicant = radicant.sqrt() * if large_arc == sweep { -1. } else { 1. }; + let cxp = radicant * rx / ry * pyp; + let cyp = radicant * -ry / rx * pxp; + let cx = cosphi * cxp - sinphi * cyp + (px + to[0]) / 2.; + let cy = sinphi * cxp + cosphi * cyp + (py + to[1]) / 2.; + let vx1 = (pxp - cxp) / rx; + let vy1 = (pyp - cyp) / ry; + let vx2 = (-pxp - cxp) / rx; + let vy2 = (-pyp - cyp) / ry; + let ang1 = vec_angle(1., 0., vx1, vy1); + let mut ang2 = vec_angle(vx1, vy1, vx2, vy2); + if !sweep && ang2 > 0. { + ang2 -= TAU; + } + if sweep && ang2 < 0. { + ang2 += TAU; + } + (cx, cy, ang1, ang2) + }; + let mut ratio = ang2.abs() / (TAU / 4.); + if (1. - ratio).abs() < 0.0000001 { + ratio = 1. + } + let segments = ratio.ceil().max(1.); + ang2 /= segments; + let a = if ang2 == 1.5707963267948966 { + 0.551915024494 + } else if ang2 == -1.5707963267948966 { + -0.551915024494 + } else { + 4. / 3. * (ang2 / 4.).tan() + }; + Self { + count: segments as usize, + center: (cx, cy), + radii: (rx, ry), + sinphi, + cosphi, + ang1, + ang2, + a, + } + } +} + +impl Iterator for Arc { + type Item = Command; + + fn next(&mut self) -> Option { + if self.count == 0 { + return None; + } + self.count -= 1; + let (y1, x1) = self.ang1.sin_cos(); + let (y2, x2) = (self.ang1 + self.ang2).sin_cos(); + let a = self.a; + let (cx, cy) = self.center; + let (rx, ry) = self.radii; + let sinphi = self.sinphi; + let cosphi = self.cosphi; + let c1 = Point::new((x1 - y1 * a) * rx, (y1 + x1 * a) * ry); + let c1 = Point::new( + cx + (cosphi * c1.x - sinphi * c1.y), + cy + (sinphi * c1.x + cosphi * c1.y), + ); + let c2 = Point::new((x2 + y2 * a) * rx, (y2 - x2 * a) * ry); + let c2 = Point::new( + cx + (cosphi * c2.x - sinphi * c2.y), + cy + (sinphi * c2.x + cosphi * c2.y), + ); + let p = Point::new(x2 * rx, y2 * ry); + let p = Point::new( + cx + (cosphi * p.x - sinphi * p.y), + cy + (sinphi * p.x + cosphi * p.y), + ); + self.ang1 += self.ang2; + Some(Command::CurveTo(c1, c2, p)) + } +} + +pub fn arc( + sink: &mut impl PathBuilder, + from: Point, + rx: f32, + ry: f32, + angle: f32, + size: ArcSize, + sweep: ArcSweep, + to: Point, +) { + let p = from; + let (px, py) = (p.x, p.y); + const TAU: f32 = std::f32::consts::PI * 2.; + let (sinphi, cosphi) = angle.sin_cos(); + let pxp = cosphi * (px - to.x) / 2. + sinphi * (py - to.y) / 2.; + let pyp = -sinphi * (px - to.x) / 2. + cosphi * (py - to.y) / 2.; + if pxp == 0. && pyp == 0. { + return; + } + let mut rx = rx.abs(); + let mut ry = ry.abs(); + let lambda = pxp.powi(2) / rx.powi(2) + pyp.powi(2) / ry.powi(2); + if lambda > 1. { + let s = lambda.sqrt(); + rx *= s; + ry *= s; + } + let large_arc = size == ArcSize::Large; + let sweep = sweep == ArcSweep::Positive; + let (cx, cy, mut ang1, mut ang2) = { + fn vec_angle(ux: f32, uy: f32, vx: f32, vy: f32) -> f32 { + let sign = if (ux * vy - uy * vx) < 0. { -1. } else { 1. }; + let mut dot = ux * vx + uy * vy; + if dot > 1. { + dot = 1. + } else if dot < -1. { + dot = -1. + } + sign * dot.acos() + } + let rxsq = rx * rx; + let rysq = ry * ry; + let pxpsq = pxp * pxp; + let pypsq = pyp * pyp; + let mut radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq); + if radicant < 0. { + radicant = 0.; + } + radicant /= (rxsq * pypsq) + (rysq * pxpsq); + radicant = radicant.sqrt() * if large_arc == sweep { -1. } else { 1. }; + let cxp = radicant * rx / ry * pyp; + let cyp = radicant * -ry / rx * pxp; + let cx = cosphi * cxp - sinphi * cyp + (px + to.x) / 2.; + let cy = sinphi * cxp + cosphi * cyp + (py + to.y) / 2.; + let vx1 = (pxp - cxp) / rx; + let vy1 = (pyp - cyp) / ry; + let vx2 = (-pxp - cxp) / rx; + let vy2 = (-pyp - cyp) / ry; + let ang1 = vec_angle(1., 0., vx1, vy1); + let mut ang2 = vec_angle(vx1, vy1, vx2, vy2); + if !sweep && ang2 > 0. { + ang2 -= TAU; + } + if sweep && ang2 < 0. { + ang2 += TAU; + } + (cx, cy, ang1, ang2) + }; + let mut ratio = ang2.abs() / (TAU / 4.); + if (1. - ratio).abs() < 0.0000001 { + ratio = 1. + } + let segments = ratio.ceil().max(1.); + ang2 /= segments; + let a = if ang2 == 1.5707963267948966 { + 0.551915024494 + } else if ang2 == -1.5707963267948966 { + -0.551915024494 + } else { + 4. / 3. * (ang2 / 4.).tan() + }; + for _ in 0..segments as usize { + let (y1, x1) = ang1.sin_cos(); + let (y2, x2) = (ang1 + ang2).sin_cos(); + let c1 = Point::new((x1 - y1 * a) * rx, (y1 + x1 * a) * ry); + let c1 = Point::new( + cx + (cosphi * c1.x - sinphi * c1.y), + cy + (sinphi * c1.x + cosphi * c1.y), + ); + let c2 = Point::new((x2 + y2 * a) * rx, (y2 - x2 * a) * ry); + let c2 = Point::new( + cx + (cosphi * c2.x - sinphi * c2.y), + cy + (sinphi * c2.x + cosphi * c2.y), + ); + let p = Point::new(x2 * rx, y2 * ry); + let p = Point::new( + cx + (cosphi * p.x - sinphi * p.y), + cy + (sinphi * p.x + cosphi * p.y), + ); + sink.curve_to(c1, c2, p); + ang1 += ang2; + } +} diff --git a/src/path_data.rs b/src/path_data.rs new file mode 100644 index 0000000..0700a38 --- /dev/null +++ b/src/path_data.rs @@ -0,0 +1,214 @@ +//! Path data. + +use super::command::{Command, PointsCommands, Verb}; +use super::geometry::{Bounds, BoundsBuilder, Point, Transform}; +use super::path_builder::{PathBuilder, TransformSink}; +use super::segment::segments; +use super::stroke::stroke_into; +use super::style::*; +use super::svg_parser::SvgCommands; + +/// Trait for types that represent path data. +/// +/// A primary design goal for this crate is to be agnostic with regard to +/// storage of path data. This trait provides the abstration to make that +/// possible. +/// +/// All path data is consumed internally as an iterator over path +/// [commands](enum.Command.html) and as such, this trait is similar to +/// the IntoIterator trait, but restricted to iterators of commands and +/// without consuming itself. +/// +/// Implementations of this trait are provided for SVG path data (in the form +/// of strings), slices/vectors of commands, and the common point and +/// verb list structure (as the tuple `(&[Point], &[Verb])`). +/// +/// As such, these paths are all equivalent: +/// +/// ```rust +/// use zeno::{Command, PathData, Point, Verb}; +/// +/// // SVG path data +/// let path1 = "M1,2 L3,4"; +/// +/// // Slice of commands +/// let path2 = &[ +/// Command::MoveTo(Point::new(1.0, 2.0)), +/// Command::LineTo(Point::new(3.0, 4.0)), +/// ][..]; +/// +/// // Tuple of slices to points and verbs +/// let path3 = ( +/// &[Point::new(1.0, 2.0), Point::new(3.0, 4.0)][..], +/// &[Verb::MoveTo, Verb::LineTo][..], +/// ); +/// +/// assert!(path1.commands().eq(path2.commands())); +/// assert!(path2.commands().eq(path3.commands())); +/// ``` +/// +/// Implementing PathData is similar to implementing IntoIterator: +/// +/// ```rust +/// use zeno::{Command, PathData}; +/// +/// pub struct MyPath { +/// data: Vec +/// } +/// +/// impl<'a> PathData for &'a MyPath { +/// // Copied here because PathData expects Commands by value +/// type Commands = std::iter::Copied>; +/// +/// fn commands(&self) -> Self::Commands { +/// self.data.iter().copied() +/// } +/// } +/// ``` +/// +/// The provided copy_into() method evaluates the command iterator and +/// submits the commands to a sink. You should also implement this if you +/// have a more direct method of dispatching to a sink as rasterizer +/// performance can be sensitive to latencies here. +pub trait PathData { + /// Command iterator. + type Commands: Iterator + Clone; + + /// Returns an iterator over the commands described by the path data. + fn commands(&self) -> Self::Commands; + + /// Copies the path data into the specified sink. + fn copy_to(&self, sink: &mut impl PathBuilder) { + for cmd in self.commands() { + use Command::*; + match cmd { + MoveTo(p) => sink.move_to(p), + LineTo(p) => sink.line_to(p), + QuadTo(c, p) => sink.quad_to(c, p), + CurveTo(c1, c2, p) => sink.curve_to(c1, c2, p), + Close => sink.close(), + }; + } + } +} + +/// Computes the total length of the path. +pub fn length(data: impl PathData, transform: Option) -> f32 { + let data = data.commands(); + let mut length = 0.; + if let Some(transform) = transform { + for s in segments(data.map(|cmd| cmd.transform(&transform)), false) { + length += s.length(); + } + } else { + for s in segments(data, false) { + length += s.length(); + } + } + length +} + +/// Computes the bounding box of the path. +pub fn bounds<'a>( + data: impl PathData, + style: impl Into>, + transform: Option, +) -> Bounds { + let style = style.into(); + let mut bounds = BoundsBuilder::new(); + apply(data, style, transform, &mut bounds); + bounds.build() +} + +/// Applies the style and transform to the path and emits the result to the +/// specified sink. +pub fn apply<'a>( + data: impl PathData, + style: impl Into>, + transform: Option, + sink: &mut impl PathBuilder, +) -> Fill { + let style = style.into(); + match style { + Style::Fill(fill) => { + if let Some(transform) = transform { + let mut transform_sink = TransformSink { sink, transform }; + data.copy_to(&mut transform_sink); + fill + } else { + data.copy_to(sink); + fill + } + } + Style::Stroke(stroke) => { + if let Some(transform) = transform { + if stroke.scale { + let mut transform_sink = TransformSink { sink, transform }; + stroke_into(data.commands(), &stroke, &mut transform_sink); + } else { + stroke_into( + data.commands().map(|cmd| cmd.transform(&transform)), + &stroke, + sink, + ); + } + } else { + stroke_into(data.commands(), &stroke, sink); + } + Fill::NonZero + } + } +} + +impl PathData for &'_ T +where + T: PathData, +{ + type Commands = T::Commands; + + fn commands(&self) -> Self::Commands { + T::commands(*self) + } + + #[inline(always)] + fn copy_to(&self, sink: &mut impl PathBuilder) { + T::copy_to(*self, sink) + } +} + +impl<'a> PathData for &'a str { + type Commands = SvgCommands<'a>; + + fn commands(&self) -> Self::Commands { + SvgCommands::new(self) + } +} + +impl<'a> PathData for (&'a [Point], &'a [Verb]) { + type Commands = PointsCommands<'a>; + + fn commands(&self) -> Self::Commands { + PointsCommands::new(self.0, self.1) + } + + #[inline(always)] + fn copy_to(&self, sink: &mut impl PathBuilder) { + self.commands().copy_to(sink); + } +} + +impl<'a> PathData for &'a [Command] { + type Commands = std::iter::Copied>; + + fn commands(&self) -> Self::Commands { + self.iter().copied() + } +} + +impl<'a> PathData for &'a Vec { + type Commands = std::iter::Copied>; + + fn commands(&self) -> Self::Commands { + self.iter().copied() + } +} diff --git a/src/raster.rs b/src/raster.rs new file mode 100644 index 0000000..c281650 --- /dev/null +++ b/src/raster.rs @@ -0,0 +1,823 @@ +//! Path rasterizer. + +use super::geometry::{Point, Vector}; +use super::path_builder::PathBuilder; +use super::style::Fill; + +#[inline(always)] +fn coverage(fill: Fill, mut coverage: i32) -> u8 { + coverage >>= PIXEL_BITS * 2 + 1 - 8; + if fill == Fill::EvenOdd { + coverage &= 511; + if coverage >= 256 { + coverage = 511i32.wrapping_sub(coverage); + } + } else { + if coverage < 0 { + coverage = !coverage; + } + if coverage >= 256 { + coverage = 255; + } + } + coverage as u8 +} + +pub struct Rasterizer<'a, S: RasterStorage> { + storage: &'a mut S, + xmin: i32, + xmax: i32, + ymin: i32, + ymax: i32, + height: i32, + shift: Vector, + start: FixedPoint, + closed: bool, + current: Point, + x: i32, + y: i32, + px: i32, + py: i32, + cover: i32, + area: i32, + invalid: bool, +} + +impl<'a, S: RasterStorage> Rasterizer<'a, S> { + pub fn new(storage: &'a mut S) -> Self { + Self { + storage, + xmin: 0, + xmax: 0, + ymin: 0, + ymax: 0, + height: 0, + shift: Vector::ZERO, + start: FixedPoint::default(), + closed: false, + current: Point::ZERO, + x: 0, + y: 0, + px: 0, + py: 0, + cover: 0, + area: 0, + invalid: false, + } + } + + pub fn rasterize( + &mut self, + shift: Vector, + width: u32, + height: u32, + apply: &mut impl FnMut(&mut Self), + fill: Fill, + buffer: &mut [u8], + pitch: usize, + y_up: bool, + ) { + let w = width as i32; + let h = height as i32; + self.storage + .reset(FixedPoint { x: 0, y: 0 }, FixedPoint { x: w, y: h }); + self.shift = shift; + self.start = FixedPoint::default(); + self.closed = true; + self.current = Point::ZERO; + self.xmin = 0; + self.ymin = 0; + self.xmax = w; + self.ymax = h; + self.height = h; + self.x = 0; + self.y = 0; + self.px = 0; + self.py = 0; + self.invalid = true; + apply(self); + if !self.closed { + self.line_to(self.start); + } + if !self.invalid { + self.storage.set(self.x, self.y, self.area, self.cover); + } + let indices = self.storage.indices(); + let cells = self.storage.cells(); + let min = FixedPoint::new(self.xmin, self.ymin); + let max = FixedPoint::new(self.xmax, self.ymax); + let height = height as usize; + for (i, &index) in indices.iter().enumerate() { + if index != -1 { + let y = ((i as i32) - min.y) as usize; + let row_offset = if y_up { + (pitch * (height - 1 - y)) as usize + } else { + (pitch * y) as usize + }; + let row = &mut buffer[row_offset..]; + let mut x = min.x; + let mut cover = 0; + let mut area; + let mut index = index; + loop { + let cell = &cells[index as usize]; + if cover != 0 && cell.x > x { + let count = (cell.x - x) as usize; + let c = coverage(fill, cover); + let xi = x as usize; + for b in &mut row[xi..xi + count] { + *b = c; + } + } + cover = cover.wrapping_add(cell.cover.wrapping_mul(ONE_PIXEL * 2)); + area = cover.wrapping_sub(cell.area); + if area != 0 && cell.x >= min.x { + let count = 1; + let c = coverage(fill, area); + let xi = cell.x as usize; + for b in &mut row[xi..xi + count] { + *b = c; + } + } + x = cell.x + 1; + index = cell.next; + if index == -1 { + break; + } + } + if cover != 0 { + let count = (max.x - x) as usize; + let c = coverage(fill, cover); + let xi = x as usize; + for b in &mut row[xi..xi + count] { + *b = c; + } + } + } + } + } + + pub fn rasterize_write( + &mut self, + shift: Vector, + width: u32, + height: u32, + apply: &mut impl FnMut(&mut Self), + fill: Fill, + pitch: usize, + y_up: bool, + write: &mut impl FnMut(usize, usize, usize, u8), + ) { + let w = width as i32; + let h = height as i32; + self.storage + .reset(FixedPoint { x: 0, y: 0 }, FixedPoint { x: w, y: h }); + self.shift = shift; + self.start = FixedPoint::default(); + self.closed = true; + self.current = Point::ZERO; + self.xmin = 0; + self.ymin = 0; + self.xmax = w; + self.ymax = h; + self.height = h; + self.x = 0; + self.y = 0; + self.px = 0; + self.py = 0; + self.invalid = true; + apply(self); + if !self.closed { + self.line_to(self.start); + } + if !self.invalid { + self.storage.set(self.x, self.y, self.area, self.cover); + } + let indices = self.storage.indices(); + let cells = self.storage.cells(); + let min = FixedPoint::new(self.xmin, self.ymin); + let max = FixedPoint::new(self.xmax, self.ymax); + let height = height as usize; + for (i, &index) in indices.iter().enumerate() { + if index != -1 { + let y = ((i as i32) - min.y) as usize; + let row_offset = if y_up { + (pitch * (height - 1 - y)) as usize + } else { + (pitch * y) as usize + }; + let mut x = min.x; + let mut cover = 0; + let mut area; + let mut index = index; + loop { + let cell = &cells[index as usize]; + if cover != 0 && cell.x > x { + let count = (cell.x - x) as usize; + let c = coverage(fill, cover); + let xi = x as usize; + write(row_offset, xi, count, c); + } + cover = cover.wrapping_add(cell.cover.wrapping_mul(ONE_PIXEL * 2)); + area = cover.wrapping_sub(cell.area); + if area != 0 && cell.x >= min.x { + let count = 1; + let c = coverage(fill, area); + let xi = cell.x as usize; + write(row_offset, xi, count, c); + } + x = cell.x + 1; + index = cell.next; + if index == -1 { + break; + } + } + if cover != 0 { + let count = (max.x - x) as usize; + let c = coverage(fill, cover); + let xi = x as usize; + write(row_offset, xi, count, c); + } + } + } + } + + #[inline(always)] + fn set_cell(&mut self, x: i32, y: i32) { + if !self.invalid && (self.area != 0 || self.cover != 0) { + self.storage.set(self.x, self.y, self.area, self.cover); + } + self.area = 0; + self.cover = 0; + self.x = if x > (self.xmin - 1) { + x + } else { + self.xmin - 1 + }; + self.y = y; + self.invalid = y >= self.ymax || y < self.ymin || x >= self.xmax; + } + + fn move_to(&mut self, to: FixedPoint) { + self.set_cell(trunc(to.x), trunc(to.y)); + self.px = to.x; + self.py = to.y; + } + + fn line_to(&mut self, to: FixedPoint) { + let to_x = to.x; + let to_y = to.y; + let mut ey1 = trunc(self.py); + let ey2 = trunc(to_y); + if (ey1 >= self.ymax && ey2 >= self.ymax) || (ey1 < self.ymin && ey2 < self.ymin) { + self.px = to_x; + self.py = to_y; + return; + } + let mut ex1 = trunc(self.px); + let ex2 = trunc(to_x); + let mut fx1 = fract(self.px); + let mut fy1 = fract(self.py); + let dx = to_x - self.px; + let dy = to_y - self.py; + if ex1 == ex2 && ey1 == ey2 { + // empty + } else if dy == 0 { + self.set_cell(ex2, ey2); + self.px = to_x; + self.py = to_y; + return; + } else if dx == 0 { + if dy > 0 { + loop { + let fy2 = ONE_PIXEL; + self.cover += fy2 - fy1; + self.area += (fy2 - fy1) * fx1 * 2; + fy1 = 0; + ey1 += 1; + self.set_cell(ex1, ey1); + if ey1 == ey2 { + break; + } + } + } else { + loop { + let fy2 = 0; + self.cover += fy2 - fy1; + self.area += (fy2 - fy1) * fx1 * 2; + fy1 = ONE_PIXEL; + ey1 -= 1; + self.set_cell(ex1, ey1); + if ey1 == ey2 { + break; + } + } + } + } else { + let mut prod = dx * fy1 - dy * fx1; + let dx_r = if ex1 != ex2 { (0x00FFFFFF) / dx } else { 0 }; + let dy_r = if ey1 != ey2 { (0x00FFFFFF) / dy } else { 0 }; + fn udiv(a: i32, b: i32) -> i32 { + ((a as u64 * b as u64) >> (4 * 8 - PIXEL_BITS)) as i32 + } + loop { + if prod <= 0 && prod - dx * ONE_PIXEL > 0 { + let fx2 = 0; + let fy2 = udiv(-prod, -dx_r); + prod -= dy * ONE_PIXEL; + self.cover += fy2 - fy1; + self.area += (fy2 - fy1) * (fx1 + fx2); + fx1 = ONE_PIXEL; + fy1 = fy2; + ex1 -= 1; + } else if prod - dx * ONE_PIXEL <= 0 && prod - dx * ONE_PIXEL + dy * ONE_PIXEL > 0 { + prod -= dx * ONE_PIXEL; + let fx2 = udiv(-prod, dy_r); + let fy2 = ONE_PIXEL; + self.cover += fy2 - fy1; + self.area += (fy2 - fy1) * (fx1 + fx2); + fx1 = fx2; + fy1 = 0; + ey1 += 1; + } else if prod - dx * ONE_PIXEL + dy * ONE_PIXEL <= 0 && prod + dy * ONE_PIXEL >= 0 + { + prod += dy * ONE_PIXEL; + let fx2 = ONE_PIXEL; + let fy2 = udiv(prod, dx_r); + self.cover += fy2 - fy1; + self.area += (fy2 - fy1) * (fx1 + fx2); + fx1 = 0; + fy1 = fy2; + ex1 += 1; + } else { + let fx2 = udiv(prod, -dy_r); + let fy2 = 0; + prod += dx * ONE_PIXEL; + self.cover += fy2 - fy1; + self.area += (fy2 - fy1) * (fx1 + fx2); + fx1 = fx2; + fy1 = ONE_PIXEL; + ey1 -= 1; + } + self.set_cell(ex1, ey1); + if ex1 == ex2 && ey1 == ey2 { + break; + } + } + } + let fx2 = fract(to_x); + let fy2 = fract(to_y); + self.cover += fy2 - fy1; + self.area += (fy2 - fy1) * (fx1 + fx2); + self.px = to_x; + self.py = to_y; + } + + fn quad_to(&mut self, control: FixedPoint, to: FixedPoint) { + let mut arc: [FixedPoint; 16 * 2 + 1] = + unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + arc[0].x = to.x; + arc[0].y = to.y; + arc[1].x = control.x; + arc[1].y = control.y; + arc[2].x = self.px; + arc[2].y = self.py; + if (trunc(arc[0].y) >= self.ymax + && trunc(arc[1].y) >= self.ymax + && trunc(arc[2].y) >= self.ymax) + || (trunc(arc[0].y) < self.ymin + && trunc(arc[1].y) < self.ymin + && trunc(arc[2].y) < self.ymin) + { + self.px = arc[0].x; + self.py = arc[0].y; + return; + } + let mut dx = (arc[2].x + arc[0].x - 2 * arc[1].x).abs(); + let dy = (arc[2].y + arc[0].y - 2 * arc[1].y).abs(); + if dx < dy { + dx = dy; + } + let mut draw = 1; + while dx > ONE_PIXEL / 4 { + dx >>= 2; + draw <<= 1; + } + let mut a = 0; + loop { + let mut split = draw & (-draw); + loop { + split >>= 1; + if split == 0 { + break; + } + split_quad(&mut arc[a..]); + a += 2; + } + let p = arc[a]; + self.line_to(p); + draw -= 1; + if draw == 0 { + break; + } + a -= 2; + } + } + + fn curve_to(&mut self, control1: FixedPoint, control2: FixedPoint, to: FixedPoint) { + let mut arc: [FixedPoint; 16 * 8 + 1] = + unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + arc[0].x = to.x; + arc[0].y = to.y; + arc[1].x = control2.x; + arc[1].y = control2.y; + arc[2].x = control1.x; + arc[2].y = control1.y; + arc[3].x = self.px; + arc[3].y = self.py; + if (trunc(arc[0].y) >= self.ymax + && trunc(arc[1].y) >= self.ymax + && trunc(arc[2].y) >= self.ymax + && trunc(arc[3].y) >= self.ymax) + || (trunc(arc[0].y) < self.ymin + && trunc(arc[1].y) < self.ymin + && trunc(arc[2].y) < self.ymin + && trunc(arc[3].y) < self.ymin) + { + self.px = arc[0].x; + self.py = arc[0].y; + return; + } + let mut a = 0; + loop { + if (2 * arc[a].x - 3 * arc[a + 1].x + arc[a + 3].x).abs() > ONE_PIXEL / 2 + || (2 * arc[a].y - 3 * arc[a + 1].y + arc[a + 3].y).abs() > ONE_PIXEL / 2 + || (arc[a].x - 3 * arc[a + 2].x + 2 * arc[a + 3].x).abs() > ONE_PIXEL / 2 + || (arc[a].y - 3 * arc[a + 2].y + 2 * arc[a + 3].y).abs() > ONE_PIXEL / 2 + { + let buf = &mut arc[a..]; + // if buf.len() < 7 { + // return; + // } + if buf.len() >= 7 { + split_cubic(buf); + a += 3; + continue; + } else { + self.line_to(to); + return; + } + } + let p = arc[a]; + self.line_to(p); + if a == 0 { + return; + } + a -= 3; + } + } +} + +impl<'a, S: RasterStorage> PathBuilder for Rasterizer<'a, S> { + fn current_point(&self) -> Point { + self.current + self.shift + } + + #[inline(always)] + fn move_to(&mut self, to: impl Into) -> &mut Self { + if !self.closed { + self.line_to(self.start); + } + let to = to.into(); + let p = FixedPoint::from_point(to + self.shift); + self.move_to(p); + self.closed = false; + self.start = p; + self.current = to; + self + } + + #[inline(always)] + fn line_to(&mut self, to: impl Into) -> &mut Self { + let to = to.into(); + self.current = to; + self.closed = false; + self.line_to(FixedPoint::from_point(to + self.shift)); + self + } + + #[inline(always)] + fn quad_to(&mut self, control: impl Into, to: impl Into) -> &mut Self { + let to = to.into(); + self.current = to; + self.closed = false; + self.quad_to( + FixedPoint::from_point(control.into() + self.shift), + FixedPoint::from_point(to + self.shift), + ); + self + } + + #[inline(always)] + fn curve_to( + &mut self, + control1: impl Into, + control2: impl Into, + to: impl Into, + ) -> &mut Self { + let to = to.into(); + self.current = to; + self.closed = false; + self.curve_to( + FixedPoint::from_point(control1.into() + self.shift), + FixedPoint::from_point(control2.into() + self.shift), + FixedPoint::from_point(to + self.shift), + ); + self + } + + #[inline(always)] + fn close(&mut self) -> &mut Self { + self.line_to(self.start); + self.closed = true; + self + } +} + +#[derive(Copy, Clone)] +pub struct Cell { + x: i32, + cover: i32, + area: i32, + next: i32, +} + +pub trait RasterStorage { + fn reset(&mut self, min: FixedPoint, max: FixedPoint); + fn cells(&self) -> &[Cell]; + fn indices(&self) -> &[i32]; + fn set(&mut self, x: i32, y: i32, area: i32, cover: i32); +} + +#[derive(Default)] +pub struct HeapStorage { + min: FixedPoint, + max: FixedPoint, + cells: Vec, + indices: Vec, +} + +impl RasterStorage for HeapStorage { + fn reset(&mut self, min: FixedPoint, max: FixedPoint) { + self.min = min; + self.max = max; + self.cells.clear(); + self.indices.clear(); + self.indices.resize((max.y - min.y) as usize, -1); + } + + fn cells(&self) -> &[Cell] { + &self.cells + } + + fn indices(&self) -> &[i32] { + &self.indices + } + + #[inline(always)] + fn set(&mut self, x: i32, y: i32, area: i32, cover: i32) { + let yindex = (y - self.min.y) as usize; + let mut cell_index = self.indices[yindex]; + let mut last_index = -1; + while cell_index != -1 { + let cell = &mut self.cells[cell_index as usize]; + if cell.x > x { + break; + } else if cell.x == x { + cell.area = cell.area.wrapping_add(area); + cell.cover = cell.cover.wrapping_add(cover); + return; + } + last_index = cell_index; + cell_index = cell.next; + } + let new_index = self.cells.len(); + let cell = Cell { + x: x, + area, + cover, + next: cell_index, + }; + if last_index != -1 { + self.cells[last_index as usize].next = new_index as i32; + } else { + self.indices[yindex] = new_index as i32; + } + self.cells.push(cell); + } +} + +const MAX_CELLS: usize = 1024; +const MAX_BAND: usize = 512; + +pub struct AdaptiveStorage { + min: FixedPoint, + max: FixedPoint, + height: usize, + cell_count: usize, + cells: [Cell; MAX_CELLS], + heap_cells: Vec, + indices: [i32; MAX_BAND], + heap_indices: Vec, +} + +impl AdaptiveStorage { + pub fn new() -> Self { + Self { + min: FixedPoint::default(), + max: FixedPoint::default(), + height: 0, + cell_count: 0, + cells: unsafe { std::mem::MaybeUninit::uninit().assume_init() }, + heap_cells: Vec::new(), + indices: unsafe { std::mem::MaybeUninit::uninit().assume_init() }, + heap_indices: Vec::new(), + } + } +} + +impl RasterStorage for AdaptiveStorage { + fn reset(&mut self, min: FixedPoint, max: FixedPoint) { + self.min = min; + self.max = max; + self.height = (max.y - min.y) as usize; + self.cell_count = 0; + self.heap_cells.clear(); + self.heap_indices.clear(); + if self.height > MAX_BAND { + self.heap_indices.resize((max.y - min.y) as usize, -1); + } else { + for i in 0..self.height as usize { + self.indices[i] = -1; + } + } + } + + fn cells(&self) -> &[Cell] { + if self.cell_count > MAX_CELLS { + &self.heap_cells + } else { + &self.cells + } + } + + fn indices(&self) -> &[i32] { + if self.height > MAX_BAND { + &self.heap_indices + } else { + &self.indices[..self.height] + } + } + + #[inline(always)] + fn set(&mut self, x: i32, y: i32, area: i32, cover: i32) { + let yindex = (y - self.min.y) as usize; + let indices = if self.height > MAX_BAND { + &mut self.heap_indices[..] + } else { + &mut self.indices[..] + }; + let cells = if self.heap_cells.len() != 0 { + &mut self.heap_cells[..] + } else { + &mut self.cells[..] + }; + let mut cell_index = indices[yindex]; + let mut last_index = -1; + while cell_index != -1 { + let cell = &mut cells[cell_index as usize]; + if cell.x > x { + break; + } else if cell.x == x { + cell.area = cell.area.wrapping_add(area); + cell.cover = cell.cover.wrapping_add(cover); + return; + } + last_index = cell_index; + cell_index = cell.next; + } + let new_index = self.cell_count; + self.cell_count += 1; + let cell = Cell { + x: x, + area, + cover, + next: cell_index, + }; + if last_index != -1 { + cells[last_index as usize].next = new_index as i32; + } else { + indices[yindex] = new_index as i32; + } + if new_index < MAX_CELLS { + cells[new_index] = cell; + } else { + if self.heap_cells.len() == 0 { + self.heap_cells.extend_from_slice(&self.cells); + } + self.heap_cells.push(cell); + } + } +} + +const _MAX_DIM: u32 = std::i16::MAX as u32; + +fn split_quad(base: &mut [FixedPoint]) { + let mut a; + let mut b; + base[4].x = base[2].x; + a = base[0].x + base[1].x; + b = base[1].x + base[2].x; + base[3].x = b >> 1; + base[2].x = (a + b) >> 2; + base[1].x = a >> 1; + base[4].y = base[2].y; + a = base[0].y + base[1].y; + b = base[1].y + base[2].y; + base[3].y = b >> 1; + base[2].y = (a + b) >> 2; + base[1].y = a >> 1; +} + +fn split_cubic(base: &mut [FixedPoint]) { + let mut a; + let mut b; + let mut c; + base[6].x = base[3].x; + a = base[0].x + base[1].x; + b = base[1].x + base[2].x; + c = base[2].x + base[3].x; + base[5].x = c >> 1; + c += b; + base[4].x = c >> 2; + base[1].x = a >> 1; + a += b; + base[2].x = a >> 2; + base[3].x = (a + c) >> 3; + base[6].y = base[3].y; + a = base[0].y + base[1].y; + b = base[1].y + base[2].y; + c = base[2].y + base[3].y; + base[5].y = c >> 1; + c += b; + base[4].y = c >> 2; + base[1].y = a >> 1; + a += b; + base[2].y = a >> 2; + base[3].y = (a + c) >> 3; +} + +#[derive(Copy, Clone, Default)] +pub struct FixedPoint { + pub x: i32, + pub y: i32, +} + +impl std::fmt::Debug for FixedPoint { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} + +impl FixedPoint { + pub fn new(x: i32, y: i32) -> Self { + Self { x, y } + } + + #[inline(always)] + pub fn from_point(p: Point) -> Self { + Self { + x: to_fixed(p.x), + y: to_fixed(p.y), + } + } +} + +#[inline(always)] +fn to_fixed(v: f32) -> i32 { + unsafe { (v * 256.).to_int_unchecked() } +} + +const PIXEL_BITS: i32 = 8; +const ONE_PIXEL: i32 = 1 << PIXEL_BITS; + +#[inline(always)] +fn trunc(x: i32) -> i32 { + x >> PIXEL_BITS +} + +#[inline(always)] +fn fract(x: i32) -> i32 { + x & (ONE_PIXEL - 1) +} diff --git a/src/scratch.rs b/src/scratch.rs new file mode 100644 index 0000000..f2734e6 --- /dev/null +++ b/src/scratch.rs @@ -0,0 +1,101 @@ +//! Context for reusing dynamic memory allocations. + +use super::geometry::{Bounds, BoundsBuilder, Transform}; +use super::path_builder::{PathBuilder, TransformSink}; +use super::path_data::PathData; +use super::raster::HeapStorage; +use super::segment::Segment; +use super::stroke::stroke_with_storage; +use super::style::{Fill, Style}; + +use std::borrow::Borrow; + +/// Scratch memory for reusable heap allocations. +#[derive(Default)] +pub struct Scratch { + pub(super) inner: Inner, + pub(super) render: HeapStorage, +} + +impl Scratch { + /// Creates a new scratch memory context. + pub fn new() -> Self { + Self::default() + } + + /// Applies the style and transform to the path and emits the result to the specified sink. + pub fn apply<'a>( + &mut self, + data: impl PathData, + style: impl Into>, + transform: Option, + sink: &mut impl PathBuilder, + ) -> Fill { + self.inner.apply(data, &style.into(), transform, sink) + } + + /// Computes the bounding box of the path. + pub fn bounds<'a>( + &mut self, + data: impl PathData, + style: impl Into>, + transform: Option, + ) -> Bounds { + let style = style.into(); + let mut bounds = BoundsBuilder::new(); + self.apply(data, style, transform, &mut bounds); + bounds.build() + } +} + +#[derive(Default)] +pub(super) struct Inner { + pub segments: Vec, +} + +impl Inner { + pub fn apply( + &mut self, + data: impl PathData, + style: &Style, + transform: Option, + sink: &mut impl PathBuilder, + ) -> Fill { + match style { + Style::Fill(fill) => { + if let Some(transform) = transform { + let mut transform_sink = TransformSink { sink, transform }; + data.copy_to(&mut transform_sink); + *fill + } else { + data.copy_to(sink); + *fill + } + } + Style::Stroke(stroke) => { + if let Some(transform) = transform { + if stroke.scale { + let mut transform_sink = TransformSink { sink, transform }; + stroke_with_storage( + data.commands(), + &stroke, + &mut transform_sink, + &mut self.segments, + ); + } else { + stroke_with_storage( + data.commands() + .map(|cmd| cmd.borrow().transform(&transform)), + &stroke, + sink, + &mut self.segments, + ); + } + } else { + stroke_with_storage(data.commands(), &stroke, sink, &mut self.segments); + } + Fill::NonZero + } + } + } +} diff --git a/src/segment.rs b/src/segment.rs new file mode 100644 index 0000000..5b051d4 --- /dev/null +++ b/src/segment.rs @@ -0,0 +1,634 @@ +//! Path segmentation. + +use super::command::Command; +use super::geometry::*; + +use std::borrow::Borrow; + +/// Represents the time parameter for a specific distance along +/// a segment. +#[derive(Copy, Clone, Debug)] +pub struct SegmentTime { + pub distance: f32, + pub time: f32, +} + +/// Line segment. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Line { + pub a: Point, + pub b: Point, +} + +impl Line { + /// Creates a new line segment. + pub fn new(a: impl Into, b: impl Into) -> Self { + Self { + a: a.into(), + b: b.into(), + } + } + + /// Returns the length of the line segment. + pub fn length(&self) -> f32 { + (self.b - self.a).length() + } + + /// Returns a slice of the line segment described by the specified start and end times. + pub fn slice(&self, start: f32, end: f32) -> Self { + let dir = self.b - self.a; + Self::new(self.a + dir * start, self.a + dir * end) + } + + pub fn time(&self, distance: f32) -> SegmentTime { + let len = (self.b - self.a).length(); + if distance > len { + return SegmentTime { + distance: len, + time: 1., + }; + } + SegmentTime { + distance, + time: distance / len, + } + } + + pub fn reverse(&self) -> Self { + Self::new(self.b, self.a) + } +} + +/// Cubic bezier curve. +#[derive(Copy, Clone, PartialEq, Default, Debug)] +pub struct Curve { + pub a: Point, + pub b: Point, + pub c: Point, + pub d: Point, +} + +impl Curve { + /// Creates a new curve. + pub fn new( + a: impl Into, + b: impl Into, + c: impl Into, + d: impl Into, + ) -> Self { + Curve { + a: a.into(), + b: b.into(), + c: c.into(), + d: d.into(), + } + } + + /// Creates a new curve from a quadratic bezier curve. + pub fn from_quadratic(a: impl Into, b: impl Into, c: impl Into) -> Self { + let a = a.into(); + let b = b.into(); + let c = c.into(); + Self { + a, + b: Point::new(a.x + 2. / 3. * (b.x - a.x), a.y + 2. / 3. * (b.y - a.y)), + c: Point::new(c.x + 2. / 3. * (b.x - c.x), c.y + 2. / 3. * (b.y - c.y)), + d: c, + } + } + + /// Returns the length of the curve. + pub fn length(&self) -> f32 { + let mut len = 0.; + let mut prev = self.a; + let steps = 64; + let step = 1. / steps as f32; + let mut t = 0.; + for _ in 0..=steps { + t += step; + let next = self.evaluate(t); + len += (next - prev).length(); + prev = next; + } + len + } + + /// Returns a slice of the curve described by the specified start and end times. + pub fn slice(&self, start: f32, end: f32) -> Self { + let t0 = start; + let t1 = end; + let u0 = 1. - t0; + let u1 = 1. - t1; + let v0 = self.a; + let v1 = self.b; + let v2 = self.c; + let v3 = self.d; + Self::new( + (v0 * (u0 * u0 * u0)) + + (v1 * (t0 * u0 * u0 + u0 * t0 * u0 + u0 * u0 * t0)) + + (v2 * (t0 * t0 * u0 + u0 * t0 * t0 + t0 * u0 * t0)) + + (v3 * (t0 * t0 * t0)), + (v0 * (u0 * u0 * u1)) + + (v1 * (t0 * u0 * u1 + u0 * t0 * u1 + u0 * u0 * t1)) + + (v2 * (t0 * t0 * u1 + u0 * t0 * t1 + t0 * u0 * t1)) + + (v3 * (t0 * t0 * t1)), + (v0 * (u0 * u1 * u1)) + + (v1 * (t0 * u1 * u1 + u0 * t1 * u1 + u0 * u1 * t1)) + + (v2 * (t0 * t1 * u1 + u0 * t1 * t1 + t0 * u1 * t1)) + + (v3 * (t0 * t1 * t1)), + (v0 * (u1 * u1 * u1)) + + (v1 * (t1 * u1 * u1 + u1 * t1 * u1 + u1 * u1 * t1)) + + (v2 * (t1 * t1 * u1 + u1 * t1 * t1 + t1 * u1 * t1)) + + (v3 * (t1 * t1 * t1)), + ) + } + + /// Returns a curve with the direction reversed. + pub fn reverse(&self) -> Self { + Self::new(self.d, self.c, self.b, self.a) + } + + /// Returns the time parameter for the specified linear distance along + /// the curve. + pub fn time(&self, distance: f32, tolerance: f32) -> SegmentTime { + let (distance, time) = self.time_impl(distance, tolerance, 1., 0); + SegmentTime { distance, time } + } + + /// Returns true if the curve can be represented as a line within some + /// tolerance. + pub fn is_line(&self, tolerance: f32) -> bool { + let degen_ab = self.a.nearly_eq_by(self.b, tolerance); + let degen_bc = self.b.nearly_eq_by(self.c, tolerance); + let degen_cd = self.c.nearly_eq_by(self.d, tolerance); + degen_ab as u8 + degen_bc as u8 + degen_cd as u8 >= 2 + } + + /// Evaluates the curve at the specified time. + pub fn evaluate(&self, time: f32) -> Point { + let t = time; + let t0 = 1. - t; + (self.a * (t0 * t0 * t0)) + + (self.b * (3. * t0 * t0 * t)) + + (self.c * (3. * t0 * t * t)) + + (self.d * (t * t * t)) + } + + fn to_segment(&self, id: SegmentId) -> Option { + if self.is_line(MERGE_EPSILON) { + if self.a.nearly_eq_by(self.d, MERGE_EPSILON) { + None + } else { + Some(Segment::Line(id, Line::new(self.a, self.d))) + } + } else { + Some(Segment::Curve(id, *self)) + } + } + + fn split_at_max_curvature(&self, splits: &mut [Curve; 4]) -> usize { + let mut tmp = [0f32; 3]; + let count1 = self.max_curvature(&mut tmp); + let mut count = 0; + let mut ts = [0f32; 4]; + for &t in &tmp[..count1] { + if t > 0. && t < 1. { + ts[count] = t; + count += 1; + } + } + if count == 0 { + splits[0] = *self; + } else { + let mut i = 0; + let mut last_t = 0.; + for &t in &ts[..count] { + splits[i] = self.slice(last_t, t); + i += 1; + last_t = t; + } + splits[i] = self.slice(last_t, 1.); + } + count + 1 + } + + fn split(&self, t: f32) -> (Self, Self) { + (self.slice(0., t), self.slice(t, 1.)) + } + + fn time_impl(&self, distance: f32, tolerance: f32, t: f32, level: u8) -> (f32, f32) { + if level < 5 && self.too_curvy(tolerance) { + let c0 = self.slice(0., 0.5); + let (dist0, t0) = c0.time_impl(distance, tolerance, t * 0.5, level + 1); + if dist0 < distance { + let c1 = self.slice(0.5, 1.); + let (dist1, t1) = c1.time_impl(distance - dist0, tolerance, t * 0.5, level + 1); + (dist0 + dist1, t0 + t1) + } else { + (dist0, t0) + } + } else { + let dist = (self.d - self.a).length(); + if dist >= distance { + let s = distance / dist; + (distance, t * s) + } else { + (dist, t) + } + } + } + + fn max_curvature(&self, ts: &mut [f32; 3]) -> usize { + let comps_x = [self.a.x, self.b.x, self.c.x, self.d.x]; + let comps_y = [self.a.y, self.b.y, self.c.y, self.d.y]; + fn get_coeffs(src: [f32; 4]) -> [f32; 4] { + let a = src[1] - src[0]; + let b = src[2] - 2. * src[1] + src[0]; + let c = src[3] + 3. * (src[1] - src[2]) - src[0]; + [c * c, 3. * b * c, 2. * b * b + c * a, a * b] + } + let mut coeffs = get_coeffs(comps_x); + let coeffs_y = get_coeffs(comps_y); + for i in 0..4 { + coeffs[i] += coeffs_y[i]; + } + Self::solve(coeffs, ts) + } + + fn solve(coeff: [f32; 4], ts: &mut [f32; 3]) -> usize { + const PI: f32 = std::f32::consts::PI; + let i = 1. / coeff[0]; + let a = coeff[1] * i; + let b = coeff[2] * i; + let c = coeff[3] * i; + let q = (a * a - b * 3.) / 9.; + let r = (2. * a * a * a - 9. * a * b + 27. * c) / 54.; + let q3 = q * q * q; + let r2_sub_q3 = r * r - q3; + let adiv3 = a / 3.; + if r2_sub_q3 < 0. { + let theta = satf32(r / q3.sqrt()).acos(); + let neg2_root_q = -2. * q.sqrt(); + ts[0] = satf32(neg2_root_q * (theta / 3.).cos() - adiv3); + ts[1] = satf32(neg2_root_q * ((theta + 2. * PI) / 3.).cos() - adiv3); + ts[2] = satf32(neg2_root_q * ((theta - 2. * PI) / 3.).cos() - adiv3); + ts.sort_unstable_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Less)); + let mut count = 3; + if ts[0] == ts[1] { + ts[1] = ts[2]; + count -= 1; + } + if ts[1] == ts[2] { + count -= 1; + } + count + } else { + let mut a = r.abs() + r2_sub_q3.sqrt(); + a = a.powf(0.3333333); + if r > 0. { + a = -a; + } + if a != 0. { + a += q / a; + } + ts[0] = satf32(a - adiv3); + 1 + } + } + + fn too_curvy(&self, tolerance: f32) -> bool { + (2. * self.d.x - 3. * self.c.x + self.a.x).abs() > tolerance + || (2. * self.d.y - 3. * self.c.y + self.a.y).abs() > tolerance + || (self.d.x - 3. * self.b.x + 2. * self.a.x).abs() > tolerance + || (self.d.y - 3. * self.b.y + 2. * self.a.y).abs() > tolerance + } + + fn needs_split(&self) -> bool { + if self.b.nearly_eq_by(self.c, MERGE_EPSILON) { + return true; + } + let normal_ab = normal(self.a, self.b); + let normal_bc = normal(self.b, self.c); + fn too_curvy(n0: Vector, n1: Vector) -> bool { + const FLAT_ENOUGH: f32 = 1.41421356237 / 2. + 1. / 10.; + n0.dot(n1) <= FLAT_ENOUGH + } + too_curvy(normal_ab, normal_bc) || too_curvy(normal_bc, normal(self.c, self.d)) + } +} + +fn satf32(x: f32) -> f32 { + x.max(0.).min(1.) +} + +/// Marker that allows regrouping of previously split segments due to simplification. +pub type SegmentId = u8; + +/// Segment of a path. +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Segment { + /// Line segment.. + Line(SegmentId, Line), + /// Cubic bezier segment. + Curve(SegmentId, Curve), + /// Marks the end of a subpath. Contains the value `true` if the subpath + /// is closed. + End(bool), +} + +impl Segment { + pub fn length(&self) -> f32 { + match self { + Self::Line(_, line) => line.length(), + Self::Curve(_, curve) => curve.length(), + _ => 0., + } + } + + pub fn slice(&self, start: f32, end: f32) -> Self { + match self { + Self::Line(id, line) => Self::Line(*id, line.slice(start, end)), + Self::Curve(id, curve) => Self::Curve(*id, curve.slice(start, end)), + Self::End(..) => *self, + } + } + + pub fn reverse(&self) -> Self { + match self { + Self::Line(id, line) => Self::Line(*id, line.reverse()), + Self::Curve(id, curve) => Self::Curve(*id, curve.reverse()), + Self::End(..) => *self, + } + } + + pub fn time(&self, distance: f32, tolerance: f32) -> SegmentTime { + match self { + Self::Line(_, line) => line.time(distance), + Self::Curve(_, curve) => curve.time(distance, tolerance), + _ => SegmentTime { + distance: 0., + time: 0., + }, + } + } + + pub fn point_normal(&self, time: f32) -> (Point, Vector) { + match self { + Self::Line(_, line) => { + let dir = line.b - line.a; + let p = line.a + dir * time; + let n = normal(line.a, line.b); + (p, n) + } + Self::Curve(_, curve) => { + let p = curve.evaluate(time); + let a = curve.evaluate(time - 0.05); + let b = curve.evaluate(time + 0.05); + let n = normal(a, b); + (p, n) + } + Self::End(..) => (Point::ZERO, Vector::ZERO), + } + } +} + +impl Default for Segment { + fn default() -> Self { + Self::End(false) + } +} + +// This large epsilon trades fidelity for performance, visual continuity +// and numeric stability. +const MERGE_EPSILON: f32 = 0.01; + +/// Creates a segment iterator from a command iterator, optionally producing +/// simplified curves. +pub fn segments(commands: I, simplify_curves: bool) -> Segments +where + I: Iterator + Clone, + I::Item: Borrow, +{ + Segments::new(simplify_curves, commands) +} + +/// Iterator over path segments. +#[derive(Clone)] +pub struct Segments { + commands: I, + start: Vector, + prev: Vector, + close: bool, + split: bool, + splits: [Curve; 16], + split_count: usize, + split_index: usize, + last_was_end: bool, + id: u8, + count: u32, +} + +impl Segments +where + I: Iterator + Clone, + I::Item: Borrow, +{ + fn new(split: bool, commands: I) -> Self { + Self { + commands, + start: Vector::ZERO, + prev: Vector::ZERO, + close: false, + split, + splits: [Curve::default(); 16], + split_count: 0, + split_index: 0, + last_was_end: true, + id: 0, + count: 0, + } + } + + fn split_curve(&mut self, id: SegmentId, c: &Curve) -> Option { + if c.is_line(MERGE_EPSILON) { + if c.a.nearly_eq_by(c.d, MERGE_EPSILON) { + return None; + } + return Some(Segment::Line(id, Line::new(c.a, c.d))); + } + let mut splits = [Curve::default(); 4]; + let count = c.split_at_max_curvature(&mut splits); + let mut i = 0; + for j in 0..count { + let curve = splits[j]; + if curve.needs_split() { + let (a, b) = curve.split(0.5); + if a.needs_split() { + let (c, d) = a.split(0.5); + self.splits[i] = c; + self.splits[i + 1] = d; + i += 2; + } else { + self.splits[i] = a; + i += 1; + } + if b.needs_split() { + let (c, d) = b.split(0.5); + self.splits[i] = c; + self.splits[i + 1] = d; + i += 2; + } else { + self.splits[i] = b; + i += 1; + } + } else { + self.splits[i] = curve; + i += 1; + } + } + self.split_count = i; + self.split_index = 1; + return self.splits[0].to_segment(id); + } + + fn inc_id(&mut self) { + if self.id == 254 { + self.id = 0; + } else { + self.id += 1; + } + } +} + +impl Iterator for Segments +where + I: Iterator + Clone, + I::Item: Borrow, +{ + type Item = Segment; + + fn next(&mut self) -> Option { + use Command::*; + if self.close { + self.close = false; + self.last_was_end = true; + return Some(Segment::End(true)); + } + if self.split { + loop { + if self.split_index < self.split_count { + let curve = self.splits[self.split_index]; + self.split_index += 1; + if let Some(segment) = curve.to_segment(self.id) { + self.count += 1; + self.last_was_end = false; + self.prev = curve.d; + return Some(segment); + } + continue; + } + self.inc_id(); + let id = self.id; + let from = self.prev; + match *self.commands.next()?.borrow() { + MoveTo(to) => { + self.start = to; + self.prev = to; + self.count = 0; + if !self.last_was_end { + self.last_was_end = true; + return Some(Segment::End(false)); + } + } + LineTo(to) => { + if !from.nearly_eq_by(to, MERGE_EPSILON) { + self.count += 1; + self.prev = to; + self.last_was_end = false; + return Some(Segment::Line(id, Line::new(from, to))); + } + } + CurveTo(c1, c2, to) => { + if let Some(segment) = self.split_curve(id, &Curve::new(from, c1, c2, to)) { + self.count += 1; + self.prev = to; + self.last_was_end = false; + return Some(segment); + } + } + QuadTo(c, to) => { + if let Some(segment) = + self.split_curve(id, &Curve::from_quadratic(from, c, to)) + { + self.count += 1; + self.prev = to; + self.last_was_end = false; + return Some(segment); + } + } + Close => { + self.prev = self.start; + if self.count == 0 || !from.nearly_eq_by(self.start, MERGE_EPSILON) { + self.close = true; + return Some(Segment::Line(id, Line::new(from, self.start))); + } else { + self.count = 0; + self.last_was_end = true; + return Some(Segment::End(true)); + } + } + } + } + } else { + let id = self.id; + self.inc_id(); + loop { + let from = self.prev; + match *self.commands.next()?.borrow() { + MoveTo(to) => { + self.start = to; + self.prev = to; + self.count = 0; + if !self.last_was_end { + self.last_was_end = true; + return Some(Segment::End(false)); + } + } + LineTo(to) => { + if !from.nearly_eq_by(to, MERGE_EPSILON) { + self.count += 1; + self.prev = to; + self.last_was_end = false; + return Some(Segment::Line(id, Line::new(from, to))); + } + } + CurveTo(c1, c2, to) => { + let segment = Segment::Curve(id, Curve::new(from, c1, c2, to)); + self.count += 1; + self.prev = to; + self.last_was_end = false; + return Some(segment); + } + QuadTo(c, to) => { + let segment = Segment::Curve(id, Curve::from_quadratic(from, c, to)); + self.count += 1; + self.prev = to; + self.last_was_end = false; + return Some(segment); + } + Close => { + self.prev = self.start; + if self.count == 0 || !from.nearly_eq_by(self.start, 0.01) { + self.close = true; + return Some(Segment::Line(id, Line::new(from, self.start))); + } else { + self.count = 0; + self.last_was_end = true; + return Some(Segment::End(true)); + } + } + } + } + } + } +} diff --git a/src/stroke.rs b/src/stroke.rs new file mode 100644 index 0000000..8c80d24 --- /dev/null +++ b/src/stroke.rs @@ -0,0 +1,969 @@ +//! Stroking and dashing of paths. + +use super::command::Command; +use super::geometry::*; +use super::path_builder::*; +use super::segment::*; + +use std::borrow::Borrow; + +/// Defines the connection between two segments of a stroke. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Join { + /// A straight line connecting the segments. + Bevel, + /// The segments are extended to their natural intersection point. + Miter, + /// An arc between the segments. + Round, +} + +/// Defines the shape to be drawn at the beginning or end of a stroke. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Cap { + /// Flat cap. + Butt, + /// Square cap with dimensions equal to half the stroke width. + Square, + /// Rounded cap with radius equal to half the stroke width. + Round, +} + +/// Describes the visual style of a stroke. +#[derive(Copy, Clone, Debug)] +pub struct Stroke<'a> { + /// Width of the stroke. + pub width: f32, + /// Style for connecting segments of the stroke. + pub join: Join, + /// Limit for miter joins. + pub miter_limit: f32, + /// Style for capping the beginning of an open subpath. + pub start_cap: Cap, + /// Style for capping the end of an open subpath. + pub end_cap: Cap, + /// Lengths of dashes in alternating on/off order. + pub dashes: &'a [f32], + /// Offset of the first dash. + pub offset: f32, + /// True if the stroke width should be affected by the scale of a transform. + pub scale: bool, +} + +impl Default for Stroke<'_> { + fn default() -> Self { + Self { + width: 1., + join: Join::Miter, + miter_limit: 4., + start_cap: Cap::Butt, + end_cap: Cap::Butt, + dashes: &[], + offset: 0., + scale: true, + } + } +} + +impl<'a> Stroke<'a> { + /// Creates a new stroke style with the specified width. + pub fn new(width: f32) -> Self { + let mut s = Self::default(); + s.width = width; + s + } + + /// Sets the width of the stroke. The default is 1. + pub fn width(&mut self, width: f32) -> &mut Self { + self.width = width; + self + } + + /// Sets the join style that determines how individual segments of the path + /// will be connected. The default is miter. + pub fn join(&mut self, join: Join) -> &mut Self { + self.join = join; + self + } + + /// Sets the limit for miter joins beyond which a bevel will be generated. + /// The default is 4. + pub fn miter_limit(&mut self, limit: f32) -> &mut Self { + self.miter_limit = limit; + self + } + + /// Sets the cap style that will be generated at the start and end of the + /// stroke. Note that this will override the individual start and end cap + /// options. The default is butt. + pub fn cap(&mut self, cap: Cap) -> &mut Self { + self.start_cap = cap; + self.end_cap = cap; + self + } + + /// Sets both the start and end cap styles for the stroke. + pub fn caps(&mut self, start: Cap, end: Cap) -> &mut Self { + self.start_cap = start; + self.end_cap = end; + self + } + + /// Sets the dash array and offset of the stroke. The default is an empty + /// array, meaning that the stroke will be drawn as a continuous line. + pub fn dash(&mut self, dashes: &'a [f32], offset: f32) -> &mut Self { + self.dashes = dashes; + self.offset = offset; + self + } + + /// Sets whether or not scaling is applied to the stroke. The default is true. + pub fn scale(&mut self, scale: bool) -> &mut Self { + self.scale = scale; + self + } +} + +pub fn stroke_into<'a, I>(commands: I, style: &Stroke<'a>, sink: &mut impl PathBuilder) +where + I: Iterator + Clone, + I::Item: Borrow, +{ + let mut stroker = Stroker::new(segments(commands, true), sink, style); + let (dashes, dash_offset, empty_gaps) = validate_dashes(style.dashes, style.offset); + let mut segment_buf = SmallBuf::new(); + if dashes.len() > 0 { + stroker.dash(&mut segment_buf, dashes, dash_offset, empty_gaps); + } else { + stroker.stroke(&mut segment_buf); + } +} + +pub fn stroke_with_storage<'a, I>( + commands: I, + style: &Stroke<'a>, + sink: &mut impl PathBuilder, + storage: &mut impl StrokerStorage, +) where + I: Iterator + Clone, + I::Item: Borrow, +{ + let mut stroker = Stroker::new(segments(commands, true), sink, style); + let (dashes, dash_offset, empty_gaps) = validate_dashes(style.dashes, style.offset); + if dashes.len() > 0 { + stroker.dash(storage, dashes, dash_offset, empty_gaps); + } else { + stroker.stroke(storage); + } +} + +pub struct Stroker<'a, I, S> { + source: Segments, + sink: &'a mut S, + radius: f32, + radius_abs: f32, + join: Join, + inv_miter_limit: f32, + start_cap: Cap, + end_cap: Cap, +} + +impl<'a, I, S> Stroker<'a, I, S> +where + I: Iterator + Clone, + I::Item: Borrow, + S: PathBuilder, +{ + pub(super) fn new(source: Segments, sink: &'a mut S, style: &Stroke) -> Self { + let radius = style.width.max(0.1) * 0.5; + Self { + source, + sink, + radius, + radius_abs: radius.abs(), + join: style.join, + inv_miter_limit: if style.miter_limit >= 1. { + 1. / style.miter_limit + } else { + 1. + }, + start_cap: style.start_cap, + end_cap: style.end_cap, + } + } + + fn stroke(&mut self, segment_buf: &mut impl StrokerStorage) { + loop { + let (closed, done) = segment_buf.collect(&mut self.source); + self.stroke_segments(segment_buf.get(), closed); + if done { + break; + } + } + } + + fn stroke_segments(&mut self, segments: &[Segment], is_closed: bool) { + let len = segments.len(); + if len == 0 { + return; + } + if len == 1 + && segments[0].length() == 0. + && (self.start_cap != Cap::Butt || self.end_cap != Cap::Butt) + { + let segment = segments[0]; + let from = match &segment { + Segment::Line(_, line) => line.a, + Segment::Curve(_, curve) => curve.a, + Segment::End(..) => Point::ZERO, + }; + let n = Vector::new(0., 1.); + let nr = n * self.radius; + let start = from + nr; + let rstart = from - nr; + self.sink.move_to(start); + self.add_end_cap(start, rstart, n); + self.add_start_cap(rstart, start, n * -1.); + return; + } + let radius = self.radius; + let mut last_dir = Vector::ZERO; + let mut first_point = Point::ZERO; + let mut last_point = Point::ZERO; + let mut pivot = Point::ZERO; + let mut last_id = 0xFF; + if is_closed { + let segment = segments[len - 1].offset(radius); + let end_point = segment.end; + let out_dir = segment.end_normal; + pivot = segment.end_pivot; + last_dir = out_dir; + last_point = end_point; + first_point = end_point; + self.sink.move_to(last_point); + } + // Forward for the outer stroke. + let mut is_first = !is_closed; + for segment in segments { + let segment = segment.offset(radius); + let id = segment.id; + let start = segment.start; + if is_first { + self.sink.move_to(start); + first_point = start; + is_first = false; + } else { + self.add_join(last_point, start, pivot, last_dir, segment.start_normal); + } + last_id = id; + last_dir = segment.end_normal; + pivot = segment.end_pivot; + last_point = self.emit(&segment.segment); + } + // Now backward for the inner stroke. + is_first = true; + for segment in segments.iter().rev() { + let segment = segment.reverse().offset(radius); + let id = segment.id; + let start = segment.start; + if is_first { + if is_closed { + let init = segments[0].reverse().offset(self.radius); + last_point = init.end; + last_dir = init.end_normal; + pivot = init.end_pivot; + self.sink.line_to(init.end); + self.add_join(last_point, start, pivot, last_dir, segment.start_normal); + } else { + self.add_end_cap(last_point, start, last_dir); + } + is_first = false; + } else { + if id != last_id { + self.add_join(last_point, start, pivot, last_dir, segment.start_normal); + } else { + self.add_split_join(last_point, start, pivot, last_dir, segment.start_normal); + } + } + last_id = id; + last_dir = segment.end_normal; + pivot = segment.end_pivot; + last_point = self.emit(&segment.segment); + } + if !is_closed { + self.add_start_cap(last_point, first_point, last_dir); + } + self.sink.close(); + } + + fn dash( + &mut self, + segment_buf: &mut impl StrokerStorage, + dashes: &[f32], + offset: f32, + empty_gaps: bool, + ) { + let mut dasher = Dasher::default(); + dasher.empty_gaps = empty_gaps; + let mut done = false; + while !done { + let (is_closed, is_done) = segment_buf.collect(&mut self.source); + done = is_done; + let segments = segment_buf.get(); + if segments.is_empty() { + continue; + } + dasher.init(is_closed, dashes, offset); + loop { + match dasher.next(segments, dashes) { + DashOp::Done => break, + DashOp::Continue => {} + DashOp::Emit => { + let (start, end) = dasher.range; + let (t0, t1) = dasher.trange; + self.dash_segments(segments, start, end, t0, t1); + } + DashOp::Stroke => { + self.stroke_segments(segments, true); + break; + } + } + } + } + } + + fn dash_segments(&mut self, segments: &[Segment], start: isize, end: isize, t0: f32, t1: f32) { + let radius = self.radius; + if t0 == t1 && start == end { + if self.start_cap == Cap::Butt && self.end_cap == Cap::Butt { + return; + } + let (t0, t1) = if t0 >= 1. { + (t0 - 0.001, t0) + } else { + (t0, t0 + 0.001) + }; + let segment = get_signed(segments, start).slice(t0, t1).offset(radius); + let start = segment.start; + let rstart = segment.start - (segment.start_normal * (2. * radius)); + self.sink.move_to(start); + self.add_end_cap(start, rstart, segment.start_normal); + self.add_start_cap(rstart, start, segment.start_normal * -1.); + self.sink.close(); + return; + } + let mut last_dir = Vector::ZERO; + let mut first_point = Point::ZERO; + let mut last_point = Point::ZERO; + let mut pivot = Point::ZERO; + let mut is_first = true; + let mut last_id = 0xFF; + for i in start..=end { + let t0 = if i == start { t0 } else { 0. }; + let t1 = if i == end { t1 } else { 1. }; + if t0 >= 1. { + continue; + } + let segment = get_signed(segments, i).slice(t0, t1).offset(radius); + let id = segment.id; + let start = segment.start; + if is_first { + self.sink.move_to(start); + first_point = start; + is_first = false; + } else { + if id != last_id { + self.add_join(last_point, start, pivot, last_dir, segment.start_normal); + } else { + self.add_split_join(last_point, start, pivot, last_dir, segment.start_normal); + } + } + last_id = id; + pivot = segment.end_pivot; + last_dir = segment.end_normal; + last_point = self.emit(&segment.segment); + } + is_first = true; + last_id = 0xFF; + for i in (start..=end).rev() { + let t0 = if i == start { t0 } else { 0. }; + let t1 = if i == end { t1 } else { 1. }; + if t0 >= 1. { + continue; + } + let segment = get_signed(segments, i) + .slice(t0, t1) + .reverse() + .offset(radius); + let id = segment.id; + let start = segment.start; + if is_first { + self.add_end_cap(last_point, start, last_dir); + is_first = false; + } else { + if id != last_id { + self.add_join(last_point, start, pivot, last_dir, segment.start_normal); + } else { + self.add_split_join(last_point, start, pivot, last_dir, segment.start_normal); + } + } + last_id = id; + pivot = segment.end_pivot; + last_dir = segment.end_normal; + last_point = self.emit(&segment.segment); + } + self.add_start_cap(last_point, first_point, last_dir); + self.sink.close(); + } + + #[inline(always)] + fn emit(&mut self, segment: &Segment) -> Point { + match segment { + Segment::Line(_, line) => { + self.sink.line_to(line.b); + line.b + } + Segment::Curve(_, curve) => { + self.sink.curve_to(curve.b, curve.c, curve.d); + curve.d + } + _ => Point::ZERO, + } + } + + fn add_join( + &mut self, + from: Point, + to: Point, + pivot: Point, + from_normal: Vector, + to_normal: Vector, + ) -> Point { + if from.nearly_eq(to) { + return from; + } + if !is_clockwise(from_normal, to_normal) { + self.sink.line_to(pivot); + self.sink.line_to(to); + return to; + } + match self.join { + Join::Bevel => { + self.sink.line_to(to); + return to; + } + Join::Round => { + let r = self.radius_abs; + let (size, sweep) = (ArcSize::Small, ArcSweep::Positive); + arc(self.sink, from, r, r, 0., size, sweep, to); + return to; + } + Join::Miter => { + let inv_limit = self.inv_miter_limit; + let dot = from_normal.dot(to_normal); + let sin_half = ((1. + dot) * 0.5).sqrt(); + if sin_half < inv_limit { + self.sink.line_to(to); + return to; + } else { + let mid = (from_normal + to_normal).normalize() * (self.radius / sin_half); + let p = pivot + mid; + self.sink.line_to(p); + self.sink.line_to(to); + return to; + } + } + } + } + + fn add_split_join( + &mut self, + from: Point, + to: Point, + pivot: Point, + from_normal: Vector, + to_normal: Vector, + ) -> Point { + if from.nearly_eq(to) { + return from; + } + if !is_clockwise(from_normal, to_normal) { + self.sink.line_to(pivot); + self.sink.line_to(to); + return to; + } + let r = self.radius_abs; + let (size, sweep) = (ArcSize::Small, ArcSweep::Positive); + arc(self.sink, from, r, r, 0., size, sweep, to); + return to; + } + + fn add_cap(&mut self, from: Point, to: Point, dir: Vector, cap: Cap) { + match cap { + Cap::Butt => { + self.sink.line_to(to); + } + Cap::Square => { + let dir = Vector::new(-dir.y, dir.x); + self.sink.line_to(from + dir * self.radius_abs); + self.sink.line_to(to + dir * self.radius_abs); + self.sink.line_to(to); + } + Cap::Round => { + let r = self.radius_abs; + let (size, sweep) = (ArcSize::Small, ArcSweep::Positive); + arc(self.sink, from, r, r, 0., size, sweep, to); + } + } + } + + fn add_start_cap(&mut self, from: Point, to: Point, dir: Vector) { + self.add_cap(from, to, dir, self.start_cap); + } + + fn add_end_cap(&mut self, from: Point, to: Point, dir: Vector) { + self.add_cap(from, to, dir, self.end_cap); + } +} + +enum DashOp { + Done, + Continue, + Emit, + Stroke, +} + +#[derive(Copy, Clone, Default)] +struct Dasher { + done: bool, + is_closed: bool, + empty_gaps: bool, + on: bool, + cur: isize, + t0: f32, + t0_offset: f32, + index: usize, + is_first: bool, + first_on: bool, + first_dash: f32, + is_dot: bool, + range: (isize, isize), + trange: (f32, f32), +} + +impl Dasher { + fn init(&mut self, is_closed: bool, dashes: &[f32], offset: f32) { + self.done = false; + self.is_closed = is_closed; + self.on = true; + self.cur = 0; + self.t0 = 0.; + self.t0_offset = 0.; + self.index = 0; + self.is_first = true; + self.first_on = true; + let mut first_dash = self.next_dash(dashes); + if offset > 0. { + let mut accum = first_dash; + while accum < offset { + self.on = !self.on; + accum += self.next_dash(dashes); + } + self.first_on = self.on; + first_dash = accum - offset; + } + self.first_dash = first_dash; + } + + #[inline(always)] + fn next_dash(&mut self, dashes: &[f32]) -> f32 { + let len = dashes.len(); + let mut dash = dashes[self.index % len]; + if self.on && self.empty_gaps { + loop { + let next_dash = dashes[(self.index + 1) % len]; + if next_dash != 0. { + break; + } + self.index += 2; + dash += dashes[self.index % len]; + } + } + self.index += 1; + dash + } + + #[inline(always)] + fn next_segments( + dash: f32, + segments: &[Segment], + limit: isize, + start: isize, + start_offset: f32, + ) -> (bool, isize, f32, f32) { + let mut cur = start; + let mut goal = dash + start_offset; + let mut segment = get_signed(segments, cur); + loop { + let td = segment.time(goal, 1.); + let dist = td.distance; + let t2 = td.time; + goal -= dist; + if goal <= 0. { + return (true, cur, dist, t2); + } + if cur + 1 >= limit { + return (false, cur, dist, t2); + } + cur += 1; + segment = get_signed(segments, cur); + } + } + + #[inline(always)] + fn next(&mut self, segments: &[Segment], dashes: &[f32]) -> DashOp { + if self.done { + return DashOp::Done; + } + let first = self.is_first; + let first_and_closed = first && self.is_closed; + let mut dash = if first { + self.first_dash + } else { + self.next_dash(dashes) + }; + let mut on = self.on; + let mut start = self.cur; + let limit = segments.len() as isize; + if self.t0 == 1. && start < limit - 1 { + start += 1; + self.t0 = 0.; + self.t0_offset = 0.; + self.cur = start; + } + let (cont, mut end, mut t1_offset, mut t1) = if dash == 0. { + (true, start, self.t0_offset, self.t0) + } else { + Self::next_segments(dash, segments, limit, start, self.t0_offset) + }; + if !cont { + self.done = true; + } + // This is tricky. If the subpath is closed and the last dash is + // "on" we need to join the last dash to the first. Otherwise, we + // need to go back and produce the initial dash that was skipped + // in anticipation of joining to the final dash. + if self.done && self.is_closed { + if on { + // Recompute the final dash including the first. + if first_and_closed { + // The first dash consumed the whole path: emit a single stroke. + return DashOp::Stroke; + } + if self.first_on { + self.cur = start - limit; + start = self.cur; + let (_, end2, end_offset, end_t) = + Self::next_segments(self.first_dash, segments, limit, 0, 0.); + end = end2; + t1_offset = end_offset; + t1 = end_t; + } + } else { + // Emit the first dash. + if !self.first_on { + return DashOp::Done; + } + dash = self.first_dash; + self.cur = 0; + self.t0 = 0.; + self.t0_offset = 0.; + self.on = true; + on = true; + start = self.cur; + let (_, end2, end_offset, end_t) = + Self::next_segments(self.first_dash, segments, limit, 0, 0.); + end = end2; + t1_offset = end_offset; + t1 = end_t; + } + } else if self.done && !on { + return DashOp::Done; + } + self.is_dot = dash == 0.; + let t0 = self.t0; + + self.is_first = false; + self.cur = end; + self.t0 = t1; + self.t0_offset = t1_offset; + self.on = !self.on; + if on && !first_and_closed { + self.range = (start, end); + self.trange = (t0, t1); + return DashOp::Emit; + } + return DashOp::Continue; + } +} + +fn validate_dashes(dashes: &[f32], offset: f32) -> (&[f32], f32, bool) { + let len = dashes.len(); + if len > 0 { + // Generate a full stroke under any of the following conditions: + // 1. The array contains any negative values. + // 2. All dashes are less than 1 unit. + // 3. All gap dashes are less than 1 unit. + let mut small_count = 0; + let mut gap_sum = 0.; + let mut empty_gaps = false; + let is_odd = len & 1 != 0; + for (i, dash) in dashes.iter().enumerate() { + let is_gap = i & 1 == 1; + if *dash < 1. { + small_count += 1; + if *dash < 0. { + return (&[], 0., false); + } else if *dash == 0. && (is_gap || is_odd) { + empty_gaps = true; + } + } else if is_gap { + gap_sum += *dash; + } + } + if dashes.len() == 1 { + gap_sum = 1.; + } + if small_count < dashes.len() && gap_sum > 0. { + let offset = if offset != 0. { + let mut s: f32 = dashes.iter().sum(); + if is_odd { + s *= 2.; + } + if offset < 0. { + s - (offset.abs() % s) + } else { + offset % s + } + } else { + 0. + }; + return (dashes, offset, empty_gaps); + } + } + return (&[], 0., false); +} + +#[inline(always)] +fn get_signed(segments: &[Segment], index: isize) -> Segment { + let index = if index < 0 { + segments.len() - (-index) as usize + } else { + index as usize + }; + segments[index] +} + +fn is_clockwise(a: Vector, b: Vector) -> bool { + a.x * b.y > a.y * b.x +} + +impl Segment { + fn offset(&self, radius: f32) -> OffsetSegment { + OffsetSegment::new(self, radius) + } +} + +#[derive(Copy, Clone)] +pub struct OffsetSegment { + pub segment: Segment, + pub id: u8, + pub start: Point, + pub end: Point, + pub start_normal: Vector, + pub end_normal: Vector, + pub end_pivot: Point, +} + +impl OffsetSegment { + fn new(segment: &Segment, radius: f32) -> Self { + match segment { + Segment::Line(id, Line { a, b }) => { + let n = normal(*a, *b); + let nr = n * radius; + let start = *a + nr; + let end = *b + nr; + Self { + segment: Segment::Line(*id, Line { a: start, b: end }), + id: *id, + start, + end, + start_normal: n, + end_normal: n, + end_pivot: *b, + } + } + Segment::Curve(id, c) => { + const EPS: f32 = 0.5; + //const EPS: f32 = CURVE_EPSILON; + let normal_ab = if c.a.nearly_eq_by(c.b, EPS) { + if c.a.nearly_eq_by(c.c, EPS) { + normal(c.a, c.d) + } else { + normal(c.a, c.c) + } + } else { + normal(c.a, c.b) + }; + let normal_bc = if c.b.nearly_eq_by(c.c, EPS) { + if c.b.nearly_eq_by(c.d, EPS) { + normal(c.a, c.d) + } else { + normal(c.b, c.d) + } + } else { + normal(c.b, c.c) + }; + let normal_cd = if c.c.nearly_eq_by(c.d, EPS) { + if c.b.nearly_eq_by(c.d, EPS) { + normal(c.a, c.d) + } else { + normal(c.b, c.d) + } + } else { + normal(c.c, c.d) + }; + let mut normal_b = normal_ab + normal_bc; + let mut normal_c = normal_cd + normal_bc; + let dot = normal_ab.dot(normal_bc); + normal_b = normal_b.normalize() * (radius / ((1. + dot) * 0.5).sqrt()); + let dot = normal_cd.dot(normal_bc); + normal_c = normal_c.normalize() * (radius / ((1. + dot) * 0.5).sqrt()); + let start = c.a + normal_ab * radius; + let end = c.d + normal_cd * radius; + Self { + segment: Segment::Curve( + *id, + Curve::new(start, c.b + normal_b, c.c + normal_c, end), + ), + id: *id, + start, + end, + start_normal: normal_ab, + end_normal: normal_cd, + end_pivot: c.d, + } + } + Segment::End(..) => Self { + segment: *segment, + id: 0, + start: Point::ZERO, + end: Point::ZERO, + start_normal: Vector::ZERO, + end_normal: Vector::ZERO, + end_pivot: Point::ZERO, + }, + } + } +} + +pub trait StrokerStorage { + fn clear(&mut self); + fn push(&mut self, segment: &Segment); + fn get(&self) -> &[Segment]; + + fn collect(&mut self, segments: &mut impl Iterator) -> (bool, bool) { + self.clear(); + let mut is_closed = false; + let mut done = false; + loop { + if let Some(segment) = segments.next() { + match segment { + Segment::End(closed) => { + is_closed = closed; + break; + } + _ => self.push(&segment), + } + } else { + done = true; + break; + } + } + (is_closed, done) + } +} + +impl StrokerStorage for SmallBuf { + fn clear(&mut self) { + self.clear(); + } + + #[inline(always)] + fn push(&mut self, segment: &Segment) { + self.push(*segment); + } + + fn get(&self) -> &[Segment] { + self.data() + } +} + +impl StrokerStorage for Vec { + fn clear(&mut self) { + self.clear(); + } + + #[inline(always)] + fn push(&mut self, segment: &Segment) { + self.push(*segment); + } + + fn get(&self) -> &[Segment] { + self + } +} + +const MAX_SMALL_BUF: usize = 128; + +#[derive(Clone)] +enum SmallBuf { + Array([T; MAX_SMALL_BUF], usize), + Vec(Vec), +} + +impl SmallBuf { + pub fn new() -> Self { + Self::Array([T::default(); MAX_SMALL_BUF], 0) + } + + pub fn data(&self) -> &[T] { + match self { + &Self::Array(ref buf, len) => &buf[..len], + &Self::Vec(ref buf) => &buf, + } + } + + pub fn push(&mut self, value: T) { + match self { + &mut Self::Vec(ref mut buf) => buf.push(value), + &mut Self::Array(ref mut buf, ref mut len) => { + if *len == MAX_SMALL_BUF { + let mut vec = Vec::from(&buf[..]); + vec.push(value); + *self = Self::Vec(vec); + } else { + buf[*len] = value; + *len += 1; + } + } + } + } + + pub fn clear(&mut self) { + match self { + &mut Self::Array(_, ref mut len) => *len = 0, + &mut Self::Vec(ref mut buf) => buf.clear(), + } + } +} diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..65feebc --- /dev/null +++ b/src/style.rs @@ -0,0 +1,51 @@ +//! Path styles. + +pub use super::fill::Fill; +pub use super::stroke::{Cap, Join, Stroke}; + +/// Represents the style of a path for rendering or hit testing. +#[derive(Copy, Clone, Debug)] +pub enum Style<'a> { + Fill(Fill), + Stroke(Stroke<'a>), +} + +impl Default for Style<'_> { + fn default() -> Self { + Self::Fill(Fill::NonZero) + } +} + +impl Style<'_> { + /// Returns true if the style is a stroke. + pub fn is_stroke(&self) -> bool { + match self { + Self::Stroke(_) => true, + _ => false, + } + } +} + +impl From for Style<'_> { + fn from(style: Fill) -> Self { + Self::Fill(style) + } +} + +impl<'a> From> for Style<'a> { + fn from(style: Stroke<'a>) -> Self { + Self::Stroke(style) + } +} + +impl<'a> From<&'a Stroke<'a>> for Style<'a> { + fn from(style: &'a Stroke<'a>) -> Self { + Self::Stroke(*style) + } +} + +impl<'a> From<&'a mut Stroke<'a>> for Style<'a> { + fn from(style: &'a mut Stroke<'a>) -> Self { + Self::Stroke(*style) + } +} diff --git a/src/svg_parser.rs b/src/svg_parser.rs new file mode 100644 index 0000000..efdeac2 --- /dev/null +++ b/src/svg_parser.rs @@ -0,0 +1,657 @@ +//! SVG path data parser. + +use super::command::Command; +use super::geometry::Vector; +use super::path_builder::{Arc, ArcSize, ArcSweep}; + +#[derive(Copy, Clone)] +enum State { + Initial, + Next, + Continue(u8), +} + +#[derive(Clone)] +pub struct SvgCommands<'a> { + buf: &'a [u8], + cur: u8, + pub pos: usize, + cmd_pos: usize, + pub error: bool, + pub done: bool, + start_point: Vector, + cur_point: Vector, + last_control: Vector, + last_cmd: u8, + state: State, + arc: Arc, +} + +impl<'a> Iterator for SvgCommands<'a> { + type Item = Command; + + fn next(&mut self) -> Option { + self.parse() + } +} + +impl<'a> SvgCommands<'a> { + pub(crate) fn new(source: &'a str) -> Self { + Self { + buf: source.as_bytes(), + cur: 0, + pos: 0, + cmd_pos: 0, + error: false, + done: false, + start_point: Vector::ZERO, + cur_point: Vector::ZERO, + last_control: Vector::ZERO, + last_cmd: 0, + state: State::Initial, + arc: Arc::default(), + } + } + + fn parse(&mut self) -> Option { + use Command::*; + let mut cmd = self.cur; + loop { + if let Some(cmd) = self.arc.next() { + return Some(cmd); + } + self.last_cmd = cmd; + match self.state { + State::Initial => { + self.advance(); + self.skip_whitespace(); + self.state = State::Next; + continue; + } + State::Next => { + self.skip_whitespace(); + self.cmd_pos = self.pos; + cmd = self.cur; + self.advance(); + self.skip_whitespace(); + self.state = State::Continue(cmd); + match cmd { + b'z' | b'Z' => { + self.state = State::Next; + self.cur_point = self.start_point; + return Some(Close); + } + b'M' => { + let to = self.point_to()?; + self.start_point = to; + self.skip_comma_whitespace(); + return Some(MoveTo(to)); + } + b'm' => { + let to = self.rel_point_to()?; + self.start_point = to; + self.skip_comma_whitespace(); + return Some(MoveTo(to)); + } + b'L' => { + let to = self.point_to()?; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } + b'l' => { + let to = self.rel_point_to()?; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } + b'H' => { + let x = self.coord()?; + let to = Vector::new(x, self.cur_point.y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } + b'h' => { + let x = self.coord()?; + let to = Vector::new(self.cur_point.x + x, self.cur_point.y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } + b'V' => { + let y = self.coord()?; + let to = Vector::new(self.cur_point.x, y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } + b'v' => { + let y = self.coord()?; + let to = Vector::new(self.cur_point.x, self.cur_point.y + y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } + b'C' => { + let (c1, c2, to) = self.three_points_to()?; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } + b'c' => { + let (c1, c2, to) = self.rel_three_points_to()?; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } + b'S' => { + let (c2, to) = self.two_points()?; + let c1 = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } + b's' => { + let (c2, to) = self.rel_two_points()?; + let c1 = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } + b'Q' => { + let (c, to) = self.two_points_to()?; + self.last_control = c; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } + b'q' => { + let (c, to) = self.rel_two_points_to()?; + self.last_control = c; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } + b'T' => { + let to = self.point()?; + let c = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } + b't' => { + let to = self.rel_point()?; + let c = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } + b'A' => { + let from = self.cur_point; + let (rx, ry, a, size, sweep, to) = self.arc_arguments(false)?; + self.arc = Arc::new(from, rx, ry, a.to_radians(), size, sweep, to); + self.skip_comma_whitespace(); + continue; + } + b'a' => { + let from = self.cur_point; + let (rx, ry, a, size, sweep, to) = self.arc_arguments(true)?; + self.arc = Arc::new(from, rx, ry, a.to_radians(), size, sweep, to); + self.skip_comma_whitespace(); + continue; + } + _ => { + if !self.done || cmd != 0 { + self.error = true; + self.pos = self.cmd_pos; + } + return None; + } + } + } + State::Continue(cmd) => match cmd { + b'M' => { + if let Some(to) = self.point_to() { + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'm' => { + if let Some(to) = self.rel_point_to() { + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'L' => { + if let Some(to) = self.point_to() { + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'l' => { + if let Some(to) = self.rel_point_to() { + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'H' => { + if let Some(x) = self.coord() { + let to = Vector::new(x, self.cur_point.y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'h' => { + if let Some(x) = self.coord() { + let to = Vector::new(self.cur_point.x + x, self.cur_point.y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'V' => { + if let Some(y) = self.coord() { + let to = Vector::new(self.cur_point.x, y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'v' => { + if let Some(y) = self.coord() { + let to = Vector::new(self.cur_point.x, self.cur_point.y + y); + self.cur_point = to; + self.skip_comma_whitespace(); + return Some(LineTo(to)); + } else { + self.state = State::Next; + } + } + b'C' => { + if let Some(c1) = self.point() { + self.skip_comma_whitespace(); + let (c2, to) = self.two_points_to()?; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } else { + self.state = State::Next; + } + } + b'c' => { + if let Some(c1) = self.rel_point() { + self.skip_comma_whitespace(); + let (c2, to) = self.rel_two_points_to()?; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } else { + self.state = State::Next; + } + } + b'S' => { + if let Some(c2) = self.point() { + self.skip_comma_whitespace(); + let to = self.point()?; + let c1 = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } else { + self.state = State::Next; + } + } + b's' => { + if let Some(c2) = self.rel_point() { + self.skip_comma_whitespace(); + let to = self.rel_point()?; + let c1 = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c2; + self.skip_comma_whitespace(); + return Some(CurveTo(c1, c2, to)); + } else { + self.state = State::Next; + } + } + b'Q' => { + if let Some(c) = self.point() { + self.last_control = c; + self.skip_comma_whitespace(); + let to = self.point_to()?; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } else { + self.state = State::Next; + } + } + b'q' => { + if let Some(c) = self.rel_point() { + self.last_control = c; + self.skip_comma_whitespace(); + let to = self.rel_point_to()?; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } else { + self.state = State::Next; + } + } + b'T' => { + if let Some(to) = self.point() { + let c = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } else { + self.state = State::Next; + } + } + b't' => { + if let Some(to) = self.rel_point() { + let c = self.reflected_control(cmd); + self.cur_point = to; + self.last_control = c; + self.skip_comma_whitespace(); + return Some(QuadTo(c, to)); + } else { + self.state = State::Next; + } + } + b'A' => { + if let Some(rx) = self.coord() { + let from = self.cur_point; + let (ry, a, size, sweep, to) = self.arc_rest_arguments(false)?; + self.arc = Arc::new(from, rx, ry, a.to_radians(), size, sweep, to); + self.skip_comma_whitespace(); + } else { + self.state = State::Next; + } + } + b'a' => { + if let Some(rx) = self.coord() { + let from = self.cur_point; + let (ry, a, size, sweep, to) = self.arc_rest_arguments(true)?; + self.arc = Arc::new(from, rx, ry, a.to_radians(), size, sweep, to); + self.skip_comma_whitespace(); + } else { + self.state = State::Next; + } + } + _ => { + if !self.done || cmd != 0 { + self.error = true; + self.pos = self.cmd_pos; + } + return None; + } + }, + } + } + } + + fn reflected_control(&self, cmd: u8) -> Vector { + let cur = self.cur_point; + let old = self.last_control; + if cmd == b'S' || cmd == b's' { + match self.last_cmd { + b'C' | b'c' | b'S' | b's' => (2. * cur.x - old.x, 2. * cur.y - old.y).into(), + _ => self.cur_point, + } + } else { + match self.last_cmd { + b'Q' | b'q' | b'T' | b't' => (2. * cur.x - old.x, 2. * cur.y - old.y).into(), + _ => self.cur_point, + } + } + } + + fn arc_arguments(&mut self, rel: bool) -> Option<(f32, f32, f32, ArcSize, ArcSweep, Vector)> { + let rx = self.coord()?; + self.skip_comma_whitespace(); + let ry = self.coord()?; + self.skip_comma_whitespace(); + let a = self.coord()?; + self.skip_comma_whitespace(); + let large_arc = self.boolean()?; + self.skip_comma_whitespace(); + let sweep = self.boolean()?; + self.skip_comma_whitespace(); + let to = if rel { + self.rel_point_to()? + } else { + self.point_to()? + }; + let size = if large_arc { + ArcSize::Large + } else { + ArcSize::Small + }; + let sweep = if sweep { + ArcSweep::Positive + } else { + ArcSweep::Negative + }; + Some((rx, ry, a, size, sweep, to)) + } + + fn arc_rest_arguments(&mut self, rel: bool) -> Option<(f32, f32, ArcSize, ArcSweep, Vector)> { + let ry = self.coord()?; + self.skip_comma_whitespace(); + let a = self.coord()?; + self.skip_comma_whitespace(); + let large_arc = self.boolean()?; + self.skip_comma_whitespace(); + let sweep = self.boolean()?; + self.skip_comma_whitespace(); + let to = if rel { + self.rel_point_to()? + } else { + self.point_to()? + }; + let size = if large_arc { + ArcSize::Large + } else { + ArcSize::Small + }; + let sweep = if sweep { + ArcSweep::Positive + } else { + ArcSweep::Negative + }; + Some((ry, a, size, sweep, to)) + } + + fn point(&mut self) -> Option { + let a = self.coord()?; + self.skip_comma_whitespace(); + let b = self.coord()?; + Some((a, b).into()) + } + + fn point_to(&mut self) -> Option { + let p = self.point()?; + self.cur_point = p; + Some(p) + } + + fn rel_point(&mut self) -> Option { + let p = self.point()?; + Some(p + self.cur_point) + } + + fn rel_point_to(&mut self) -> Option { + let p = self.rel_point()?; + self.cur_point = p; + Some(p) + } + + fn two_points_to(&mut self) -> Option<(Vector, Vector)> { + let a = self.point()?; + self.skip_comma_whitespace(); + let b = self.point_to()?; + Some((a, b)) + } + + fn two_points(&mut self) -> Option<(Vector, Vector)> { + let a = self.point()?; + self.skip_comma_whitespace(); + let b = self.point()?; + Some((a, b)) + } + + fn rel_two_points_to(&mut self) -> Option<(Vector, Vector)> { + let a = self.rel_point()?; + self.skip_comma_whitespace(); + let b = self.rel_point_to()?; + Some((a, b)) + } + fn rel_two_points(&mut self) -> Option<(Vector, Vector)> { + let a = self.rel_point()?; + self.skip_comma_whitespace(); + let b = self.rel_point()?; + Some((a, b)) + } + + fn three_points_to(&mut self) -> Option<(Vector, Vector, Vector)> { + let a = self.point()?; + self.skip_comma_whitespace(); + let b = self.point()?; + self.skip_comma_whitespace(); + let c = self.point_to()?; + Some((a, b, c)) + } + + fn rel_three_points_to(&mut self) -> Option<(Vector, Vector, Vector)> { + let a = self.rel_point()?; + self.skip_comma_whitespace(); + let b = self.rel_point()?; + self.skip_comma_whitespace(); + let c = self.rel_point_to()?; + Some((a, b, c)) + } + + fn coord(&mut self) -> Option { + match self.cur { + b'+' => { + self.advance(); + self.number() + } + b'-' => { + self.advance(); + Some(-self.number()?) + } + _ => self.number(), + } + } + + fn number(&mut self) -> Option { + let mut buf = [0u8; 32]; + let mut pos = 0; + let mut has_decimal = false; + loop { + match self.cur { + b'.' => { + if has_decimal { + break; + } else { + buf[pos] = self.cur; + pos += 1; + has_decimal = true; + } + } + b'0'..=b'9' => { + buf[pos] = self.cur; + pos += 1; + } + _ => break, + } + self.advance(); + } + let s = std::str::from_utf8(&buf[..pos]).ok()?; + s.parse::().ok() + } + + fn boolean(&mut self) -> Option { + match self.cur { + b'0' => { + self.advance(); + Some(false) + } + b'1' => { + self.advance(); + Some(true) + } + _ => None, + } + } + + fn skip_comma_whitespace(&mut self) { + self.skip_whitespace(); + if self.accept(b',') { + self.skip_whitespace(); + } + } + + fn skip_whitespace(&mut self) { + while self.accept_by(|b| match b { + 0x9 | 0x20 | 0xA | 0xC | 0xD => true, + _ => false, + }) {} + } + + fn accept(&mut self, b: u8) -> bool { + if self.cur == b { + self.advance(); + return true; + } + false + } + + fn accept_by(&mut self, f: impl Fn(u8) -> bool) -> bool { + if f(self.cur) { + self.advance(); + return true; + } + false + } + + fn advance(&mut self) { + if self.pos == self.buf.len() { + self.done = true; + self.cur = 0; + return; + } + self.cur = self.buf[self.pos]; + self.pos += 1; + } +} + +/// Returns an error indicating the first position of invalid SVG path data. +pub fn validate_svg(svg: &str) -> Result<(), usize> { + let cmds = &mut SvgCommands::new(svg); + cmds.count(); + let pos = cmds.pos; + if cmds.error || pos != svg.len() { + Err(pos.saturating_sub(1)) + } else { + Ok(()) + } +} diff --git a/src/traversal.rs b/src/traversal.rs new file mode 100644 index 0000000..7c70612 --- /dev/null +++ b/src/traversal.rs @@ -0,0 +1,234 @@ +//! Path traversal algorithms. + +use super::command::{Command, TransformCommands}; +use super::geometry::*; +use super::path_data::PathData; +use super::segment::{segments, Segment, Segments}; + +use std::borrow::Borrow; +use std::cell::RefCell; + +/// A vertex of a path. +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Vertex { + /// The start point and direction of a subpath. + Start(Point, Vector), + /// The incoming direction, location, and outgoing direction of an + /// intermediate vertex in a subpath. + Middle(Vector, Point, Vector), + /// The incoming direction and location of the final vertex in a subpath. + /// The boolean value is true if the subpath is closed. + End(Vector, Point, bool), +} + +/// An iterator over the vertices of a path. +#[derive(Clone)] +pub struct Vertices { + segments: Segments, + prev_point: Point, + prev_dir: Vector, + is_first: bool, +} + +impl Vertices +where + D: Iterator + Clone, + D::Item: Borrow, +{ + /// Creates a new iterator over the vertices of a path. + pub fn new(data: D) -> Self { + Self { + segments: segments(data, false), + prev_point: Point::ZERO, + prev_dir: Vector::new(1., 0.), + is_first: true, + } + } +} + +impl Vertices> +where + D: Iterator + Clone, + D::Item: Borrow, +{ + /// Creates a new iterator over the vertices of a transformed path. + pub fn with_transform(data: D, transform: Transform) -> Self { + let data = TransformCommands { data, transform }; + Self { + segments: segments(data, false), + prev_point: Point::ZERO, + prev_dir: Vector::new(1., 0.), + is_first: true, + } + } +} + +impl Iterator for Vertices +where + D: Iterator + Clone, + D::Item: Borrow, +{ + type Item = Vertex; + + fn next(&mut self) -> Option { + use Segment::*; + if self.is_first { + self.is_first = false; + match self.segments.next()?.borrow() { + End(closed) => { + self.is_first = true; + return Some(Vertex::End(self.prev_dir, self.prev_point, *closed)); + } + segment => { + let (start, in_dir, out_dir, end) = get_components(segment); + self.prev_dir = out_dir; + self.prev_point = end; + return Some(Vertex::Start(start, in_dir)); + } + } + } else { + match self.segments.next()?.borrow() { + End(closed) => { + self.is_first = true; + return Some(Vertex::End(self.prev_dir, self.prev_point, *closed)); + } + segment => { + let (start, in_dir, out_dir, end) = get_components(segment); + let prev_dir = self.prev_dir; + self.prev_dir = out_dir; + self.prev_point = end; + return Some(Vertex::Middle(prev_dir, start, in_dir)); + } + } + } + } +} + +fn get_components(segment: &Segment) -> (Point, Vector, Vector, Point) { + match segment { + Segment::Curve(_, curve) => { + let a = curve.evaluate(0.05); + let b = curve.evaluate(0.95); + let a_dir = (a - curve.a).normalize(); + let b_dir = (curve.d - b).normalize(); + (curve.a, a_dir, b_dir, curve.d) + } + Segment::Line(_, line) => { + let dir = (line.b - line.a).normalize(); + (line.a, dir, dir, line.b) + } + Segment::End(..) => (Point::ZERO, Vector::ZERO, Vector::ZERO, Point::ZERO), + } +} + +/// An iterator like type that walks along a path by arbitrary steps. +pub struct Walk { + init: Segments, + iter: Segments, + segment: Segment, + segment_offset: f32, + first: bool, + length: RefCell>, + walked: f32, +} + +impl Walk +where + D: Iterator + Clone, +{ + /// Creates a new iterator like type that steps along a path by abitrary distances. + pub fn new(data: impl PathData) -> Self { + let data = data.commands(); + Self { + init: segments(data.clone(), false), + iter: segments(data, false), + segment: Segment::default(), + segment_offset: 0., + first: true, + length: RefCell::new(None), + walked: 0., + } + } +} + +impl Walk> +where + D: Iterator + Clone, +{ + /// Creates a new iterator like type that steps along a transformed path by abitrary distances. + pub fn with_transform(data: impl PathData, transform: Transform) -> Self { + let data = data.commands(); + let data = TransformCommands { data, transform }; + Self { + init: segments(data.clone(), false), + iter: segments(data, false), + segment: Segment::default(), + segment_offset: 0., + first: true, + length: RefCell::new(None), + walked: 0., + } + } +} + +impl Walk +where + D: Iterator + Clone, + D::Item: Borrow, +{ + /// Steps by the specified distance and returns the point at the new + /// location and the normal vector describing the left-ward direction at + /// that point. Returns `None` if the distance steps beyond the end + /// of the path. + pub fn step(&mut self, distance: f32) -> Option<(Point, Vector)> { + if self.first { + self.segment = self.next_segment()?; + self.segment_offset = 0.; + self.first = false; + } + let mut t; + let mut offset = self.segment_offset; + let mut segment = self.segment; + let mut remaining = distance; + loop { + let dt = segment.time(offset + remaining, 1.); + remaining -= dt.distance - offset; + t = dt.time; + offset = dt.distance; + if remaining <= 0. { + break; + } + segment = self.next_segment()?; + offset = 0.; + } + self.segment = segment; + self.segment_offset = offset; + self.walked += distance; + let (p, n) = segment.point_normal(t); + Some((p, n)) + } + + /// Returns the remaining distance available to walk on the path. + pub fn remaining(&self) -> f32 { + let mut l = self.length.borrow_mut(); + if l.is_none() { + let iter = self.init.clone(); + let mut sum = 0.; + for s in iter { + sum += s.length(); + } + *l = Some(sum); + } + l.unwrap() - self.walked + } + + fn next_segment(&mut self) -> Option { + while let Some(s) = self.iter.next() { + match s { + Segment::End(..) => continue, + _ => return Some(s), + } + } + None + } +}