├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── fishyb.rs ├── examples ├── cubic.rs ├── curve.rs ├── drop.rs ├── figure.rs ├── fishy.rs ├── fishy2.rs ├── fishyp.rs ├── heptagram.rs ├── letter.rs ├── over.rs ├── png │ └── mod.rs ├── quad.rs ├── round.rs ├── stroke.rs ├── stroke2.rs └── teeth.rs ├── rustfmt.toml └── src ├── fig.rs ├── fixed.rs ├── geom.rs ├── imgbuf.rs ├── lib.rs ├── path.rs ├── plotter.rs ├── stroker.rs └── vid.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo/ 2 | Cargo.lock 3 | target/ 4 | *.png 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ### Changed 4 | * Updated `pointy` to v0.7 5 | * Update to Rust 2024 edition 6 | 7 | ## [0.7.0] - 2022-06-01 8 | ### Added 9 | * `Plotter.raster()` and `raster_mut()` (returning reference) 10 | ### Changed 11 | * Old `Plotter.raster()` to `Plotter.into_raster()` 12 | * Use `pointy` crate for 2D geometry 13 | 14 | ## [0.6.0] - 2020-09-19 15 | ### Added 16 | * PathOp, FillRule, JoinStyle, etc. now implement Debug 17 | ### Changed 18 | * Renamed PathBuilder to Path2D 19 | ### Removed 20 | * Old Path2D (just use `Vec` instead) 21 | 22 | ## [0.5.0] - 2020-05-19 23 | ### Changed 24 | * Replaced PathBuilder::new() with default() (Default impl) 25 | * Replaced Transform::new() with default() (Default implt) 26 | * Renamed Vec2/Vec2w to Pt/WidePt (to avoid confustion with Vec) 27 | * Simplify Plotter API -- allow plotting directly onto provided Raster 28 | 29 | ## [0.4.0] - 2020-04-24 30 | ### Changed 31 | * Renamed "mask" to "matte" 32 | ### Removed 33 | * Moved Raster and supporting code to pix crate 34 | 35 | ## [0.3.1] - 2019-03-07 36 | ### Added 37 | * Support `target_arch` = "wasm32" 38 | * New use-simd feature 39 | 40 | ## [0.3.0] - 2019-01-21 41 | ### Added 42 | * Rgb8 pixel format. 43 | ### Changed 44 | * PixFmt::over mask parameter changed from &Mask to &[u8]. 45 | ### Fixed 46 | * Rendering bug (issue #15) 47 | * Rendering bug (issue #17) 48 | 49 | ## [0.2.0] - 2018-11-20 50 | ### Added 51 | * PixFmt, Rgba8, Gray8 pixel formats. 52 | * Raster now has a pixel format type parameter. 53 | * Raster::width(), height() and `as_*_slice` methods. 54 | * RasterB (for borrowed pixels). 55 | ### Removed 56 | * Plotter::over, raster and `write_png` methods. 57 | * Plotter no longer has an associated Raster 58 | ### Changed 59 | * Plotter::fill and stroke now return Mask reference. 60 | * Raster::over clears the mask before returning. 61 | 62 | ## [0.1.1] - 2018-11-15 63 | ### Fixed 64 | * Fixed several rendering bugs. 65 | ### Changed 66 | * Moved fixed-point code to fixed module. 67 | * Code cleanups. 68 | 69 | ## [0.1.0] - 2018-11-11 70 | ### Added 71 | * Plotter::raster, `Plotter::write_png` 72 | ### Removed 73 | * `Plotter::add_path`, Plotter::reset 74 | ### Changed 75 | * Plotter::fill/stroke now take a PathOp iterator. 76 | * Plotter: renamed clear to `clear_mask`. 77 | * Converted SIMD code from C to rust. 78 | * Converted benchmarks to use criterion-rs. 79 | * Moved stroker into its own module. 80 | * Optimized alpha blending using SIMD. 81 | 82 | ## [0.0.10] - 2017-10-25 83 | ### Added 84 | * PathBuilder, Path2D 85 | ### Removed 86 | * PlotterBuilder 87 | ### Changed 88 | * Renamed Vec3 to Vec2w. 89 | 90 | ## [0.0.9] - 2017-10-20 91 | ### Added 92 | * Transform struct 93 | * `Plotter::set_transform` 94 | ### Removed 95 | * Plotter::{scale,translate,rotate,`skew_x`,`skew_y`} 96 | ### Changed 97 | * Implemented more accurate rendering algorithm (cumulative sum). 98 | * Cleaned up example programs. 99 | 100 | ## [0.0.8] - 2017-10-11 101 | ### Added 102 | * Plotter::{scale,translate,rotate,`skew_x`,`skew_y`} 103 | 104 | ## [0.0.7] - 2017-10-10 105 | ### Fixed 106 | * Fixed some rendering glitches. 107 | ### Added 108 | * Added C SIMD code for rendering mask. 109 | * Added plotting benchmarks. 110 | ### Removed 111 | * `Plotter::write_png` 112 | ### Changed 113 | * Use type alias for vertex IDs. 114 | * Moved test code into separate modules. 115 | 116 | ## [0.0.6] - 2017-10-06 117 | ### Added 118 | * Mask struct 119 | * Raster struct 120 | * JoinStyle::Round 121 | * license file 122 | ### Removed 123 | * Removed Vec2 from public API. 124 | 125 | ## [0.0.5] - 2017-10-04 126 | ### Fixed 127 | * Fixed stroking problems. 128 | ### Added 129 | * Added PNG output for masks. 130 | 131 | ## [0.0.4] - 2017-10-03 132 | ### Changed 133 | * Reworked Plotter API. 134 | 135 | ## [0.0.3] - 2017-10-02 136 | ### Added 137 | * Added support for miter limits. 138 | ### Removed 139 | * Removed Fig from public API. 140 | 141 | ## [0.0.2] - 2017-10-01 142 | ### Added 143 | * Added some example programs. 144 | ### Changed 145 | * Cleaned up public API. 146 | * Improved documentation. 147 | 148 | ## [0.0.1] - 2017-10-01 149 | * Initial conversion of C code to rust. 150 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "footile" 3 | version = "0.7.0" 4 | description = "A 2D vector graphics library" 5 | license = "MIT OR Apache-2.0" 6 | documentation = "https://docs.rs/footile" 7 | homepage = "https://github.com/DougLau/footile" 8 | repository = "https://github.com/DougLau/footile" 9 | readme = "README.md" 10 | categories = ["multimedia::images", "rendering::graphics-api"] 11 | keywords = ["vector-graphics"] 12 | edition = "2024" 13 | 14 | [dependencies] 15 | pix = "0.14" 16 | pointy = "0.7" 17 | 18 | [dev-dependencies] 19 | criterion = "0.5" 20 | png_pong = "0.9" 21 | 22 | [[bench]] 23 | name = "fishyb" 24 | harness = false 25 | 26 | [features] 27 | default = ["simd"] 28 | simd = [] 29 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Douglas Lau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # footile 2 | 3 | A 2D vector graphics library written in Rust. It uses [pix] for the underlying 4 | raster images. 5 | 6 | See the [documentation] for examples and API usage. 7 | 8 | [documentation]: https://docs.rs/footile 9 | [pix]: https://docs.rs/pix 10 | 11 | ## Rasterizing: Bird's Eye View 12 | 13 | There is nothing novel here — this is merely a guide to the code. 14 | 15 | We have a 2D *path* made up of lines, bézier splines, arcs, etc., and we want to 16 | make a high-quality raster image out of it. But how? 17 | 18 | ### Modules 19 | 20 | * `path`: Defines path operations and `Path2D`s 21 | * `plotter`: Defines `Plotter` struct and flattens curves 22 | * `stroker`: Creates *stroked* paths for plotter 23 | * `fixed`: Defines `Fixed` struct used by `fig` module 24 | * `fig`: Rasterizes paths 25 | 26 | ### Curve Flattening 27 | 28 | *Flattening* refers to approximating a curve with a series of line segments. 29 | 30 | We use the recursive algorithm described by De Casteljau. There might be 31 | opportunities for optimization here if we ever determine this is a bottleneck. 32 | One other thing to note: this method could cause a stack overflow with the wrong 33 | input data. 34 | 35 | Once complete, we have a series of line segments forming one or more closed 36 | polygons. 37 | 38 | ### Sorting Vertices 39 | 40 | For the next step, we create a sorted list of the vertices in (Y, X) order. 41 | This is needed because we will *scan* the polygon onto a grid one row at a time. 42 | 43 | Every path has a **winding order**: either *clockwise* or the other direction, 44 | typically called *counter-* or *anti-clockwise*. Let's avoid that debate by 45 | calling it *widdershins*, since clocks rarely go backwards. 46 | 47 | The first vertex must be on the outside of the path, so we can check the angle 48 | between its neighbors to determine the winding order. 49 | 50 | ### Active Edges 51 | 52 | As rows are scanned from top to bottom, we keep track of a list of *active 53 | edges*. If an edge crosses the current row, it is added to the list, otherwise, 54 | it is removed. Since horizontal edges cannot *cross* a row, they can safely be 55 | ignored. 56 | 57 | For each row, vertices from the list are compared to its top and bottom. If 58 | the new vertex is above the bottom, one or more edges are *added*. When the 59 | new vertex is above the top, existing edges are *removed*. 60 | 61 | ```bob 62 | v0 63 | /\ 64 | / \ (a) 65 | / \ v2 66 | (b) / +-----+ 67 | / v1 \ 68 | / \ (c) 69 | / \ 70 | +--------------------+ 71 | v3 v4 72 | ``` 73 | 74 | Example: 75 | * Starting with `v0`, add edges `(a)` and `(b)` 76 | * Scan until the bottom of the current row is below `v1` / `v2` 77 | * Add edge `(c)` 78 | * Scan until the row top is below `v1` 79 | * Remove edge `(a)` 80 | * Scan until the row top is below `v3` / `v4` 81 | * Remove edges `(b)` and `(c)` 82 | 83 | ### Signed Area 84 | 85 | The active edges are used to find the *signed area* at any point. Count the 86 | number of edge crossings needed to reach the exterior of the path. For example: 87 | 88 | ```bob 89 | +------+ 90 | | | 91 | | +1 | 92 | | | 93 | +------+ 94 | ``` 95 | 96 | A self-intersecting polygon looks like this: 97 | 98 | ```bob 99 | +-----------------+ 100 | | | 101 | | +------+ | 102 | | | \ / 103 | | +1 | +2 X 104 | | | / \ 105 | | +------+ | 106 | | | 107 | +-----------------+ 108 | ``` 109 | 110 | What about *sub-paths* with opposite winding order? In that case, subtract 111 | instead of adding: 112 | 113 | ```bob 114 | +----------------+ 115 | | +-----+ | 116 | | | | | 117 | | +1 | 0 | | 118 | | | | | 119 | | +-----+ | 120 | | <-- | 121 | | | 122 | +----------------+ 123 | --> 124 | ``` 125 | 126 | ### Scanning Rows 127 | 128 | When scanning a row, the signed area is sampled for each pixel. The direction 129 | of each edge from top to bottom determines whether it adds to or subtracts from 130 | the area. In the normal winding order, it adds to the area; otherwise, it 131 | subtracts. 132 | 133 | This row is 4 pixels wide: 134 | 135 | ```bob 136 | - - - | - - - - - - | - - - 137 | | | | | | 138 | | +1 | -1 139 | | | | | | 140 | | | 141 | | - - - | - - - | - - - | - - - | 142 | 0 1 1 0 143 | ``` 144 | 145 | The *cumulative sum* of these values is the signed area of each pixel. 146 | 147 | #### Anti-Aliasing 148 | 149 | Sometimes edges don't fall on pixel boundaries. In this case, the trick is to 150 | use fractional numbers for anti-aliasing. 151 | 152 | ```bob 153 | - - - | - | - - - - - - - 154 | | | | | | | 155 | | +1 | -½ -½ 156 | | | | | | | 157 | | | 158 | | - - - | - | - | - - - | - - - | 159 | 0 ½ 0 0 160 | ``` 161 | 162 | Notice how the remainder of the second edge coverage is added to the pixel to 163 | the right (third pixel). This is necessary to keep the cumulative sum correct. 164 | 165 | ```bob 166 | - - - | - \ - - - - - - - 167 | | | \ | | | 168 | | +1 \-¼ -¾ 169 | | | \ | | | 170 | | \ 171 | | - - - | - - - \ - - - | - - - | 172 | 0 ¾ 0 0 173 | ``` 174 | 175 | ### Compositing 176 | 177 | The signed area buffer can be composited with a raster, using a source color. 178 | -------------------------------------------------------------------------------- /benches/fishyb.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | 4 | use criterion::Criterion; 5 | use footile::*; 6 | use pix::Raster; 7 | use pix::matte::Matte8; 8 | use pointy::Transform; 9 | 10 | fn fill_16(c: &mut Criterion) { 11 | c.bench_function("fill_16", |b| b.iter(|| fill(16))); 12 | } 13 | 14 | fn fill_256(c: &mut Criterion) { 15 | c.bench_function("fill_256", |b| b.iter(|| fill(256))); 16 | } 17 | 18 | fn fill(i: u32) { 19 | make_plotter(i).fill(FillRule::NonZero, &make_fishy(), Matte8::new(255)); 20 | } 21 | 22 | fn stroke_16(c: &mut Criterion) { 23 | c.bench_function("stroke_16", |b| b.iter(|| gray_stroke(16))); 24 | } 25 | 26 | fn stroke_256(c: &mut Criterion) { 27 | c.bench_function("stroke_256", |b| b.iter(|| gray_stroke(256))); 28 | } 29 | 30 | fn gray_stroke(i: u32) { 31 | make_plotter(i).stroke(&make_fishy(), Matte8::new(255)); 32 | } 33 | 34 | fn make_plotter(i: u32) -> Plotter { 35 | let r = Raster::with_clear(i, i); 36 | let mut p = Plotter::new(r); 37 | p.set_transform(Transform::with_scale(2.0, 2.0)); 38 | p 39 | } 40 | 41 | fn make_fishy() -> Vec { 42 | Path2D::default() 43 | .relative() 44 | .move_to(112.0, 16.0) 45 | .line_to(-48.0, 32.0) 46 | .cubic_to(-64.0, -48.0, -64.0, 80.0, 0.0, 32.0) 47 | .line_to(48.0, 32.0) 48 | .line_to(-32.0, -48.0) 49 | .close() 50 | .finish() 51 | } 52 | 53 | criterion_group!(benches, fill_16, fill_256, stroke_16, stroke_256); 54 | criterion_main!(benches); 55 | -------------------------------------------------------------------------------- /examples/cubic.rs: -------------------------------------------------------------------------------- 1 | // drop.rs 2 | use footile::{Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(2.0) 12 | .move_to(8.0, 16.0) 13 | .cubic_to(64.0, -16.0, 64.0, 48.0, 0.0, 32.0) 14 | .finish(); 15 | let r = Raster::with_clear(64, 64); 16 | let mut p = Plotter::new(r); 17 | png::write_matte(p.stroke(&path, Matte8::new(255)), "./cubic.png") 18 | } 19 | -------------------------------------------------------------------------------- /examples/curve.rs: -------------------------------------------------------------------------------- 1 | // curve.rs 2 | use footile::{Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(0.0) 12 | .move_to(64.0, 48.0) 13 | .pen_width(18.0) 14 | .cubic_to(-64.0, -48.0, -64.0, 80.0, 0.0, 32.0) 15 | .finish(); 16 | let r = Raster::with_clear(128, 128); 17 | let mut p = Plotter::new(r); 18 | png::write_matte(p.stroke(&path, Matte8::new(255)), "./curve.png") 19 | } 20 | -------------------------------------------------------------------------------- /examples/drop.rs: -------------------------------------------------------------------------------- 1 | // drop.rs 2 | use footile::{FillRule, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::gray::{Graya8p, SGray8}; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(3.0) 12 | .move_to(50.0, 34.0) 13 | .cubic_to(4.0, 16.0, 16.0, 28.0, 0.0, 32.0) 14 | .cubic_to(-16.0, -4.0, -4.0, -16.0, 0.0, -32.0) 15 | .close() 16 | .finish(); 17 | let r = Raster::::with_clear(100, 100); 18 | let mut p = Plotter::new(r); 19 | p.fill(FillRule::NonZero, &path, Graya8p::new(128, 255)); 20 | p.stroke(&path, Graya8p::new(255, 255)); 21 | 22 | let r = Raster::::with_raster(&p.raster()); 23 | png::write(&r, "./drop.png") 24 | } 25 | -------------------------------------------------------------------------------- /examples/figure.rs: -------------------------------------------------------------------------------- 1 | // figure.rs 2 | use footile::{FillRule, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .move_to(4.0, 4.0) 12 | .line_to(28.0, 12.0) 13 | .line_to(28.0, -12.0) 14 | .line_to(-12.0, 28.0) 15 | .line_to(12.0, 28.0) 16 | .line_to(-28.0, -4.0) 17 | .line_to(-28.0, 4.0) 18 | .line_to(12.0, -28.0) 19 | .close() 20 | .finish(); 21 | let r = Raster::with_clear(64, 64); 22 | let mut p = Plotter::new(r); 23 | p.fill(FillRule::NonZero, &path, Matte8::new(255)); 24 | png::write_matte(&p.raster(), "./figure.png") 25 | } 26 | -------------------------------------------------------------------------------- /examples/fishy.rs: -------------------------------------------------------------------------------- 1 | // fishy.rs 2 | use footile::{FillRule, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::rgb::{Rgba8p, SRgba8}; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let fish = Path2D::default() 10 | .relative() 11 | .pen_width(3.0) 12 | .move_to(112.0, 24.0) 13 | .line_to(-32.0, 24.0) 14 | .cubic_to(-96.0, -48.0, -96.0, 80.0, 0.0, 32.0) 15 | .line_to(32.0, 24.0) 16 | .line_to(-16.0, -40.0) 17 | .close() 18 | .finish(); 19 | let eye = Path2D::default() 20 | .relative() 21 | .pen_width(2.0) 22 | .move_to(24.0, 48.0) 23 | .line_to(8.0, 8.0) 24 | .move_to(0.0, -8.0) 25 | .line_to(-8.0, 8.0) 26 | .finish(); 27 | let raster = Raster::with_clear(128, 128); 28 | let mut p = Plotter::new(raster); 29 | p.fill(FillRule::NonZero, &fish, Rgba8p::new(127, 96, 96, 255)); 30 | p.stroke(&fish, Rgba8p::new(255, 208, 208, 255)); 31 | p.stroke(&eye, Rgba8p::new(0, 0, 0, 255)); 32 | 33 | let r = Raster::::with_raster(&p.raster()); 34 | png::write(&r, "./fishy.png") 35 | } 36 | -------------------------------------------------------------------------------- /examples/fishy2.rs: -------------------------------------------------------------------------------- 1 | // fishy2.rs 2 | use footile::{FillRule, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::rgb::{Rgba8p, SRgba8}; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let fish = Path2D::default() 10 | .relative() 11 | .pen_width(3.0) 12 | .move_to(112.0, 24.0) 13 | .line_to(-32.0, 24.0) 14 | .cubic_to(-96.0, -48.0, -96.0, 80.0, 0.0, 32.0) 15 | .line_to(32.0, 24.0) 16 | .line_to(-16.0, -40.0) 17 | .close() 18 | .finish(); 19 | let eye = Path2D::default() 20 | .relative() 21 | .pen_width(2.0) 22 | .move_to(24.0, 48.0) 23 | .line_to(8.0, 8.0) 24 | .move_to(0.0, -8.0) 25 | .line_to(-8.0, 8.0) 26 | .finish(); 27 | let v = vec![Rgba8p::new(0, 0, 0, 0); 128 * 128]; 28 | let r = Raster::::with_pixels(128, 128, v); 29 | let mut p = Plotter::new(r); 30 | p.fill(FillRule::NonZero, &fish, Rgba8p::new(127, 96, 96, 255)); 31 | p.stroke(&fish, Rgba8p::new(255, 208, 208, 255)); 32 | p.stroke(&eye, Rgba8p::new(0, 0, 0, 255)); 33 | 34 | let r = Raster::::with_raster(&p.raster()); 35 | png::write(&r, "./fishy2.png") 36 | } 37 | -------------------------------------------------------------------------------- /examples/fishyp.rs: -------------------------------------------------------------------------------- 1 | // fishyp.rs 2 | use footile::{FillRule, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::rgb::{Rgba8p, SRgba8}; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let fish = Path2D::default() 10 | .relative() 11 | .pen_width(3.0) 12 | .move_to(112.0, 24.0) 13 | .line_to(-32.0, 24.0) 14 | .cubic_to(-96.0, -48.0, -96.0, 80.0, 0.0, 32.0) 15 | .line_to(32.0, 24.0) 16 | .line_to(-16.0, -40.0) 17 | .close() 18 | .finish(); 19 | let eye = Path2D::default() 20 | .relative() 21 | .pen_width(2.0) 22 | .move_to(24.0, 48.0) 23 | .line_to(8.0, 8.0) 24 | .move_to(0.0, -8.0) 25 | .line_to(-8.0, 8.0) 26 | .finish(); 27 | 28 | // Emulate Non-owned Pointer to Vulkan Buffer: 29 | let mut array = [Rgba8p::new(0, 0, 0, 0); 128 * 128]; 30 | let buffer: *mut Rgba8p = array.as_mut_ptr(); 31 | 32 | // Safely convert our Vulkan Pointer into a Box<[T]>, then into a Vec. 33 | // This is safe because slice & box are fat ptrs. 34 | let slice: &mut [Rgba8p] = 35 | unsafe { std::slice::from_raw_parts_mut(buffer, 128 * 128) }; 36 | let v: Box<[Rgba8p]> = 37 | unsafe { std::mem::transmute::<_, Box<[Rgba8p]>>(slice) }; 38 | 39 | // Plot on the buffer. 40 | let r = Raster::::with_pixels(128, 128, v); 41 | let mut p = Plotter::new(r); 42 | p.fill(FillRule::NonZero, &fish, Rgba8p::new(127, 96, 96, 255)); 43 | p.stroke(&fish, Rgba8p::new(255, 208, 208, 255)); 44 | p.stroke(&eye, Rgba8p::new(0, 0, 0, 255)); 45 | 46 | let r = p.into_raster(); 47 | let out = Raster::::with_raster(&r); 48 | png::write(&out, "./fishyp.png")?; 49 | 50 | // Convert raster back to slice to avoid double free. 51 | let b: Box<[Rgba8p]> = r.into(); 52 | let _: &mut [Rgba8p] = unsafe { std::mem::transmute(b) }; 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /examples/heptagram.rs: -------------------------------------------------------------------------------- 1 | // heptagram.rs 2 | use footile::{FillRule, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | use pointy::Transform; 6 | 7 | mod png; 8 | 9 | const PI: f32 = std::f32::consts::PI; 10 | 11 | fn main() -> Result<(), std::io::Error> { 12 | let r = Raster::with_clear(100, 100); 13 | let mut p = Plotter::new(r); 14 | let h = (p.width() / 2) as f32; 15 | let q = h / 2.0; 16 | p.set_transform(Transform::with_scale(h, h).translate(q, q)); 17 | let mut pb = Path2D::default(); 18 | pb = pb.move_to(0f32.cos(), 0f32.sin()); 19 | for n in 1..7 { 20 | let th = PI * 4.0 * (n as f32) / 7.0; 21 | pb = pb.line_to(th.cos(), th.sin()); 22 | } 23 | let path = pb.close().finish(); 24 | p.fill(FillRule::EvenOdd, &path, Matte8::new(255)); 25 | png::write_matte(&p.raster(), "./heptagram.png") 26 | } 27 | -------------------------------------------------------------------------------- /examples/letter.rs: -------------------------------------------------------------------------------- 1 | // letter.rs Example plotting the letter C 2 | use footile::{FillRule, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .absolute() 11 | .move_to(88.61539, 64.895096) 12 | .quad_to(62.433567, 64.895096, 47.88811, 81.79021) 13 | .quad_to(33.342655, 98.57342, 33.342655, 127.88811) 14 | .quad_to(33.342655, 156.86713, 48.44755, 174.54544) 15 | .quad_to(63.664333, 192.11188, 89.51049, 192.11188) 16 | .quad_to(122.62937, 192.11188, 139.3007, 159.32866) 17 | .line_to(156.75525, 168.05594) 18 | .quad_to(147.02098, 188.41957, 129.34265, 199.04895) 19 | .quad_to(111.77622, 209.67831, 88.503494, 209.67831) 20 | .quad_to(64.671326, 209.67831, 47.21678, 199.83215) 21 | .quad_to(29.874126, 189.87411, 20.6993, 171.52448) 22 | .quad_to(11.636364, 153.06293, 11.636364, 127.88811) 23 | .quad_to(11.636364, 90.18181, 32.0, 68.81119) 24 | .quad_to(52.363636, 47.44055, 88.39161, 47.44055) 25 | .quad_to(113.56643, 47.44055, 130.46153, 57.286713) 26 | .quad_to(147.35664, 67.13286, 155.3007, 86.4895) 27 | .line_to(135.04895, 93.20279) 28 | .quad_to(129.56644, 79.44055, 117.37063, 72.16783) 29 | .quad_to(105.28671, 64.895096, 88.61539, 64.895096) 30 | .finish(); 31 | let r = Raster::with_clear(165, 256); 32 | let mut p = Plotter::new(r); 33 | p.fill(FillRule::NonZero, &path, Matte8::new(255)); 34 | png::write_matte(&p.raster(), "./letter.png") 35 | } 36 | -------------------------------------------------------------------------------- /examples/over.rs: -------------------------------------------------------------------------------- 1 | // over.rs 2 | use footile::{Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(8.0) 12 | .move_to(32.0, 16.0) 13 | .line_to(16.0, 16.0) 14 | .line_to(-16.0, 16.0) 15 | .line_to(-16.0, -16.0) 16 | .line_to(16.0, -16.0) 17 | .line_to(0.0, 32.0) 18 | .finish(); 19 | let r = Raster::with_clear(64, 64); 20 | let mut p = Plotter::new(r); 21 | png::write_matte(p.stroke(&path, Matte8::new(255)), "./over.png") 22 | } 23 | -------------------------------------------------------------------------------- /examples/png/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use pix::Raster; 4 | use pix::el::Pixel; 5 | use pix::gray::SGray8; 6 | use pix::matte::Matte8; 7 | use png_pong::Encoder; 8 | use std::fs::File; 9 | use std::io; 10 | 11 | /// Write a `Raster` to a file. 12 | pub fn write

(raster: &Raster

, filename: &str) -> io::Result<()> 13 | where 14 | P: Pixel, 15 | { 16 | let mut file = File::create(filename)?; 17 | Encoder::new(&mut file).into_step_enc().still(raster); 18 | Ok(()) 19 | } 20 | 21 | /// Write a `Raster` to a grayscale file. 22 | pub fn write_matte(raster: &Raster, filename: &str) -> io::Result<()> { 23 | let pix = raster.as_u8_slice(); 24 | let raster = 25 | Raster::::with_u8_buffer(raster.width(), raster.height(), pix); 26 | write(&raster, filename) 27 | } 28 | -------------------------------------------------------------------------------- /examples/quad.rs: -------------------------------------------------------------------------------- 1 | // quad.rs 2 | use footile::{Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(2.0) 12 | .move_to(0.0, 16.0) 13 | .quad_to(100.0, 16.0, 0.0, 32.0) 14 | .finish(); 15 | let r = Raster::with_clear(64, 64); 16 | let mut p = Plotter::new(r); 17 | png::write_matte(p.stroke(&path, Matte8::new(255)), "./quad.png") 18 | } 19 | -------------------------------------------------------------------------------- /examples/round.rs: -------------------------------------------------------------------------------- 1 | // round.rs 2 | use footile::{JoinStyle, Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(40.0) 12 | .move_to(10.0, 60.0) 13 | .line_to(50.0, 0.0) 14 | .line_to(0.0, -50.0) 15 | .finish(); 16 | let mut p = Plotter::new(Raster::with_clear(100, 100)); 17 | p.set_join(JoinStyle::Round); 18 | png::write_matte(p.stroke(&path, Matte8::new(255)), "./round.png") 19 | } 20 | -------------------------------------------------------------------------------- /examples/stroke.rs: -------------------------------------------------------------------------------- 1 | // stroke.rs 2 | use footile::{Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::matte::Matte8; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(5.0) 12 | .move_to(16.0, 48.0) 13 | .line_to(32.0, 0.0) 14 | .line_to(-16.0, -32.0) 15 | .close() 16 | .finish(); 17 | let mut p = Plotter::new(Raster::with_clear(64, 64)); 18 | png::write_matte(p.stroke(&path, Matte8::new(255)), "./stroke.png") 19 | } 20 | -------------------------------------------------------------------------------- /examples/stroke2.rs: -------------------------------------------------------------------------------- 1 | // stroke2.rs 2 | use footile::{Path2D, Plotter}; 3 | use pix::Raster; 4 | use pix::rgb::{Rgba8p, SRgba8}; 5 | 6 | mod png; 7 | 8 | fn main() -> Result<(), std::io::Error> { 9 | let path = Path2D::default() 10 | .relative() 11 | .pen_width(6.0) 12 | .move_to(16.0, 15.0) 13 | .line_to(32.0, 1.0) 14 | .line_to(-32.0, 1.0) 15 | .line_to(32.0, 15.0) 16 | .line_to(-32.0, 15.0) 17 | .line_to(32.0, 1.0) 18 | .line_to(-32.0, 1.0) 19 | .finish(); 20 | let clr = Rgba8p::new(64, 128, 64, 255); 21 | let mut p = Plotter::new(Raster::with_color(64, 64, clr)); 22 | p.stroke(&path, Rgba8p::new(255, 255, 0, 255)); 23 | let r = Raster::::with_raster(&p.raster()); 24 | png::write(&r, "./stroke2.png") 25 | } 26 | -------------------------------------------------------------------------------- /examples/teeth.rs: -------------------------------------------------------------------------------- 1 | use footile::{Path2D, Plotter}; 2 | use pix::Raster; 3 | use pix::matte::Matte8; 4 | 5 | mod png; 6 | 7 | fn main() -> Result<(), std::io::Error> { 8 | let path = Path2D::default() 9 | .relative() 10 | .move_to(0.0, 8.0) 11 | .line_to(8.0, 8.0) 12 | .line_to(8.0, -8.0) 13 | .line_to(8.0, 8.0) 14 | .line_to(8.0, -8.0) 15 | .line_to(8.0, 8.0) 16 | .line_to(8.0, -8.0) 17 | .line_to(8.0, 8.0) 18 | .line_to(8.0, -8.0) 19 | .move_to(-64.0, 32.0) 20 | .line_to(8.0, 8.0) 21 | .line_to(8.0, -8.0) 22 | .line_to(8.0, 8.0) 23 | .line_to(8.0, -8.0) 24 | .line_to(8.0, 8.0) 25 | .line_to(8.0, -8.0) 26 | .line_to(8.0, 8.0) 27 | .line_to(8.0, -8.0) 28 | .finish(); 29 | let mut p = Plotter::new(Raster::with_clear(64, 64)); 30 | png::write_matte(p.stroke(&path, Matte8::new(255)), "./teeth.png") 31 | } 32 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | fn_params_layout = "Tall" 2 | hard_tabs = false 3 | max_width = 80 4 | use_field_init_shorthand = true 5 | -------------------------------------------------------------------------------- /src/fig.rs: -------------------------------------------------------------------------------- 1 | // fig.rs A 2D rasterizer. 2 | // 3 | // Copyright (c) 2017-2025 Douglas P Lau 4 | // 5 | use crate::fixed::Fixed; 6 | use crate::imgbuf::{matte_src_over_even_odd, matte_src_over_non_zero}; 7 | use crate::path::FillRule; 8 | use crate::vid::Vid; 9 | use pix::chan::{Ch8, Linear, Premultiplied}; 10 | use pix::el::Pixel; 11 | use pix::matte::Matte8; 12 | use pix::ops::SrcOver; 13 | use pix::{Raster, RowsMut}; 14 | use pointy::Pt; 15 | use std::any::TypeId; 16 | use std::cmp::Ordering; 17 | use std::cmp::Ordering::*; 18 | use std::fmt; 19 | use std::ops::Sub; 20 | 21 | /// A 2D point with fixed-point values 22 | #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] 23 | struct FxPt { 24 | x: Fixed, 25 | y: Fixed, 26 | } 27 | 28 | /// Figure direction enum 29 | #[derive(Clone, Copy, Debug, PartialEq)] 30 | enum FigDir { 31 | Forward, 32 | Reverse, 33 | } 34 | 35 | /// Sub-figure structure 36 | struct SubFig { 37 | /// Starting point 38 | start: Vid, 39 | /// Number of points 40 | n_points: usize, 41 | /// Done flag 42 | done: bool, 43 | } 44 | 45 | /// Edge structure 46 | #[derive(Debug)] 47 | struct Edge { 48 | /// Lower vertex ID 49 | v1: Vid, 50 | /// Upper vertex Y 51 | y_upper: Fixed, 52 | /// Lower vertex Y 53 | y_lower: Fixed, 54 | /// Figure direction from upper to lower 55 | dir: FigDir, 56 | /// Change in cov per pix on current row 57 | step_pix: Fixed, 58 | /// Inverse slope (delta_x / delta_y) 59 | inv_slope: Fixed, 60 | /// X at bottom of current row 61 | x_bot: Fixed, 62 | /// Minimum X on current row 63 | min_x: Fixed, 64 | /// Maximum X on current row 65 | max_x: Fixed, 66 | } 67 | 68 | /// A Fig is a series of 2D points which can be rendered to an image raster. 69 | pub struct Fig { 70 | /// All pionts 71 | points: Vec, 72 | /// All sub-figures 73 | subs: Vec, 74 | } 75 | 76 | /// Figure scanner structure 77 | struct Scanner<'a, P> 78 | where 79 | P: Pixel, 80 | { 81 | /// The figure 82 | fig: &'a Fig, 83 | /// Fill rule 84 | rule: FillRule, 85 | /// Figure direction 86 | dir: FigDir, 87 | /// Destination raster rows 88 | rows: RowsMut<'a, P>, 89 | /// Color to fill 90 | clr: P, 91 | /// Signed area buffer 92 | sgn_area: &'a mut [i16], 93 | /// Active edges 94 | edges: Vec, 95 | } 96 | 97 | impl Sub for FxPt { 98 | type Output = Self; 99 | 100 | fn sub(self, rhs: Self) -> Self { 101 | FxPt::new(self.x - rhs.x, self.y - rhs.y) 102 | } 103 | } 104 | 105 | impl FxPt { 106 | /// Create a new point. 107 | fn new(x: Fixed, y: Fixed) -> Self { 108 | FxPt { x, y } 109 | } 110 | 111 | /// Calculate winding order for two vectors. 112 | /// 113 | /// The vectors should be initialized as edges pointing toward the same 114 | /// point. 115 | /// Returns true if the winding order is widdershins (counter-clockwise). 116 | fn widdershins(self, rhs: Self) -> bool { 117 | // Cross product (with Z zero) is used to determine the winding order. 118 | (self.x * rhs.y) > (rhs.x * self.y) 119 | } 120 | } 121 | 122 | impl FigDir { 123 | /// Get the opposite direction 124 | fn opposite(self) -> Self { 125 | match self { 126 | FigDir::Forward => FigDir::Reverse, 127 | FigDir::Reverse => FigDir::Forward, 128 | } 129 | } 130 | } 131 | 132 | impl SubFig { 133 | /// Create a new sub-figure 134 | fn new(start: Vid) -> SubFig { 135 | SubFig { 136 | start, 137 | n_points: 0, 138 | done: false, 139 | } 140 | } 141 | 142 | /// Get next vertex within a sub-figure 143 | fn next(&self, vid: Vid, dir: FigDir) -> Vid { 144 | match dir { 145 | FigDir::Forward => { 146 | let v = vid + 1; 147 | if v < self.start + self.n_points { 148 | v 149 | } else { 150 | self.start 151 | } 152 | } 153 | FigDir::Reverse => { 154 | if vid > self.start { 155 | vid - 1 156 | } else if self.n_points > 0 { 157 | self.start + self.n_points - 1 158 | } else { 159 | self.start 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | /// Get the row of a Y value 167 | fn row_of(y: Fixed) -> i32 { 168 | y.into() 169 | } 170 | 171 | impl Edge { 172 | /// Create a new edge 173 | /// 174 | /// * `v0` Upper vertex. 175 | /// * `v1` Lower vertex. 176 | /// * `p0` Upper point. 177 | /// * `p1` Lower point. 178 | /// * `dir` Direction from upper to lower vertex. 179 | fn new(v0: Vid, v1: Vid, p0: FxPt, p1: FxPt, dir: FigDir) -> Edge { 180 | debug_assert_ne!(v0, v1); 181 | let delta_x = p1.x - p0.x; 182 | let delta_y = p1.y - p0.y; 183 | debug_assert!(delta_y > Fixed::ZERO); 184 | let step_pix = Edge::calculate_step(delta_x, delta_y); 185 | let inv_slope = delta_x / delta_y; 186 | let y_upper = p0.y; 187 | let y_lower = p1.y; 188 | let y_bot = (y_upper + Fixed::ONE).floor() - y_upper; 189 | let x_bot = p0.x + inv_slope * y_bot; 190 | Edge { 191 | v1, 192 | y_upper, 193 | y_lower, 194 | dir, 195 | step_pix, 196 | inv_slope, 197 | x_bot, 198 | min_x: Fixed::ZERO, 199 | max_x: Fixed::ZERO, 200 | } 201 | } 202 | 203 | /// Calculate the step for each pixel on an edge 204 | fn calculate_step(delta_x: Fixed, delta_y: Fixed) -> Fixed { 205 | if delta_x != Fixed::ZERO { 206 | (delta_y / delta_x).abs().min(Fixed::ONE) 207 | } else { 208 | Fixed::ZERO 209 | } 210 | } 211 | 212 | /// Get the minimum X pixel 213 | fn min_pix(&self) -> i32 { 214 | self.min_x.into() 215 | } 216 | 217 | /// Get the maximum X pixel 218 | fn max_pix(&self) -> i32 { 219 | self.max_x.into() 220 | } 221 | 222 | /// Get the X midpoint for the current row. 223 | fn mid_x(&self) -> Fixed { 224 | self.max_x.avg(self.min_x) 225 | } 226 | 227 | /// Check for the edge starting row. 228 | fn is_starting(&self, y_row: i32) -> bool { 229 | row_of(self.y_upper) == y_row 230 | } 231 | 232 | /// Check for the edge ending row. 233 | fn is_ending(&self, y_row: i32) -> bool { 234 | row_of(self.y_lower) == y_row 235 | } 236 | 237 | /// Get pixel coverage of starting row. 238 | fn starting_cov(&self) -> i16 { 239 | let y_row = row_of(self.y_upper); 240 | self.continuing_cov(y_row) - pixel_cov(self.y_upper.fract()) 241 | } 242 | 243 | /// Calculate X limits for the starting row. 244 | fn calculate_x_limits_starting(&mut self) { 245 | let y_row = row_of(self.y_upper); 246 | let y0 = Fixed::ONE - self.y_upper.fract(); 247 | let x0 = self.x_bot - self.inv_slope * y0; 248 | self.set_x_limits(x0, y_row); 249 | } 250 | 251 | /// Get pixel coverage of continuing row. 252 | fn continuing_cov(&self, y_row: i32) -> i16 { 253 | debug_assert!(y_row <= row_of(self.y_lower)); 254 | if self.is_ending(y_row) { 255 | pixel_cov(self.y_lower.fract()) 256 | } else { 257 | 256 258 | } 259 | } 260 | 261 | /// Calculate X limits for a continuing row. 262 | fn calculate_x_limits_continuing(&mut self, y_row: i32) { 263 | debug_assert!(!self.is_starting(y_row)); 264 | let x0 = self.x_bot - self.inv_slope; 265 | self.set_x_limits(x0, y_row); 266 | } 267 | 268 | /// Set X limits 269 | fn set_x_limits(&mut self, x0: Fixed, y_row: i32) { 270 | let x1 = if self.is_ending(y_row) { 271 | let y1 = self.y_lower.ceil() - self.y_lower; 272 | self.x_bot - self.inv_slope * y1 273 | } else { 274 | self.x_bot 275 | }; 276 | self.min_x = x0.min(x1); 277 | self.max_x = x0.max(x1); 278 | } 279 | 280 | /// Scan signed area of current row. 281 | /// 282 | /// * `dir` Direction of edge. 283 | /// * `cov` Pixel coverage of current row (1 - 256). 284 | /// * `area` Signed area buffer. 285 | fn scan_area(&self, dir: FigDir, cov: i16, area: &mut [i16]) { 286 | let ed = if self.dir == dir { 1 } else { -1 }; 287 | let full_cov = Fixed::from(cov as f32 / 256.0); 288 | let mut x_cov = self.first_cov(full_cov); // total coverage at X 289 | let step_cov = self.step_cov(Fixed::ONE); // coverage change per step 290 | debug_assert!(step_cov > Fixed::ZERO); 291 | let mut sum_pix = 0; // cumulative sum of pixel coverage 292 | for x in self.min_pix()..area.len() as i32 { 293 | let x_pix = pixel_cov(x_cov).min(cov); 294 | let p = x_pix - sum_pix; // pixel coverage at X 295 | area[x.max(0) as usize] += p * ed; 296 | sum_pix += p; 297 | if sum_pix >= cov { 298 | break; 299 | } 300 | x_cov = (x_cov + step_cov).min(Fixed::ONE); 301 | } 302 | } 303 | 304 | /// Get coverage of first pixel on edge. 305 | fn first_cov(&self, full_cov: Fixed) -> Fixed { 306 | let r = if self.min_pix() == self.max_pix() { 307 | (Fixed::ONE - self.mid_x().fract()) * full_cov 308 | } else { 309 | (Fixed::ONE - self.min_x.fract()) * Fixed::HALF 310 | }; 311 | self.step_cov(r) 312 | } 313 | 314 | /// Get pixel coverage. 315 | fn step_cov(&self, r: Fixed) -> Fixed { 316 | if self.step_pix > Fixed::ZERO { 317 | r * self.step_pix 318 | } else { 319 | r 320 | } 321 | } 322 | } 323 | 324 | impl fmt::Debug for Fig { 325 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 326 | for sub in &self.subs { 327 | write!(f, "sub {:?}+{:?} ", sub.start, sub.n_points)?; 328 | let end = sub.start + sub.n_points; 329 | for v in usize::from(sub.start)..usize::from(end) { 330 | write!(f, "{:?} ", self.point(Vid::from(v)))?; 331 | } 332 | } 333 | Ok(()) 334 | } 335 | } 336 | 337 | impl Fig { 338 | /// Create a figure rasterizer 339 | pub fn new() -> Fig { 340 | let points = Vec::with_capacity(1024); 341 | let mut subs = Vec::with_capacity(16); 342 | subs.push(SubFig::new(Vid(0))); 343 | Fig { points, subs } 344 | } 345 | 346 | /// Get the current sub-figure 347 | fn sub_current(&self) -> &SubFig { 348 | self.subs.last().unwrap() 349 | } 350 | 351 | /// Get the current sub-figure mutably 352 | fn sub_current_mut(&mut self) -> &mut SubFig { 353 | self.subs.last_mut().unwrap() 354 | } 355 | 356 | /// Add a new sub-figure 357 | fn sub_add(&mut self) { 358 | let vid = Vid::from(self.points.len()); 359 | self.subs.push(SubFig::new(vid)); 360 | } 361 | 362 | /// Add a point to the current sub-figure 363 | fn sub_add_point(&mut self) { 364 | self.sub_current_mut().n_points += 1; 365 | } 366 | 367 | /// Check if current sub-figure is done. 368 | fn sub_is_done(&self) -> bool { 369 | self.subs.last().unwrap().done 370 | } 371 | 372 | /// Mark sub-figure done. 373 | fn sub_set_done(&mut self) { 374 | let sub = self.sub_current(); 375 | if sub.n_points > 0 { 376 | let pt = self.point(sub.start); 377 | if self.is_coincident(pt) { 378 | self.points.pop(); 379 | self.sub_current_mut().n_points -= 1; 380 | } 381 | self.sub_current_mut().done = true; 382 | } 383 | } 384 | 385 | /// Get the sub-figure at a specified vertex ID. 386 | fn sub_at(&self, vid: Vid) -> &SubFig { 387 | for sub in self.subs.iter() { 388 | if vid < sub.start + sub.n_points { 389 | return sub; 390 | } 391 | } 392 | // Invalid vid indicates bug 393 | unreachable!(); 394 | } 395 | 396 | /// Get the next vertex. 397 | fn next(&self, vid: Vid, dir: FigDir) -> Vid { 398 | self.sub_at(vid).next(vid, dir) 399 | } 400 | 401 | /// Get direction from top-left vertex. 402 | fn get_dir(&self, vid: Vid) -> FigDir { 403 | let p = self.point(vid); 404 | let p0 = self.point(self.next(vid, FigDir::Forward)); 405 | let p1 = self.point(self.next(vid, FigDir::Reverse)); 406 | if (p1 - p).widdershins(p0 - p) { 407 | FigDir::Forward 408 | } else { 409 | FigDir::Reverse 410 | } 411 | } 412 | 413 | /// Get a point. 414 | /// 415 | /// * `vid` Vertex ID. 416 | fn point(&self, vid: Vid) -> FxPt { 417 | self.points[usize::from(vid)] 418 | } 419 | 420 | /// Get Y value at a vertex. 421 | fn get_y(&self, vid: Vid) -> Fixed { 422 | self.point(vid).y 423 | } 424 | 425 | /// Add a point. 426 | /// 427 | /// * `pt` Point to add. 428 | pub fn add_point>>(&mut self, pt: P) { 429 | let n_pts = self.points.len(); 430 | if n_pts < usize::from(Vid::MAX) { 431 | let done = self.sub_is_done(); 432 | if done { 433 | self.sub_add(); 434 | } 435 | let pt = pt.into(); 436 | let pt = FxPt::new(Fixed::from(pt.x), Fixed::from(pt.y)); 437 | if done || !self.is_coincident(pt) { 438 | self.points.push(pt); 439 | self.sub_add_point(); 440 | } 441 | } 442 | } 443 | 444 | /// Check if a point is coincident with previous point. 445 | fn is_coincident(&self, pt: FxPt) -> bool { 446 | if let Some(p) = self.points.last() { 447 | pt == *p 448 | } else { 449 | false 450 | } 451 | } 452 | 453 | /// Close the current sub-figure. 454 | /// 455 | /// NOTE: This must be called before filling in order to handle coincident 456 | /// start/end points. 457 | pub fn close(&mut self) { 458 | if !self.points.is_empty() { 459 | self.sub_set_done(); 460 | } 461 | } 462 | 463 | /// Compare two figure vertex IDs 464 | fn compare_vids(&self, v0: Vid, v1: Vid) -> Ordering { 465 | let p0 = self.point(v0); 466 | let p1 = self.point(v1); 467 | match p0.y.cmp(&p1.y) { 468 | Less => Less, 469 | Greater => Greater, 470 | Equal => p0.x.cmp(&p1.x), 471 | } 472 | } 473 | 474 | /// Fill the figure to an image raster. 475 | /// 476 | /// * `rule` Fill rule. 477 | /// * `raster` Output raster. 478 | /// * `clr` Color to fill. 479 | /// * `sgn_area` Signed area buffer. 480 | pub fn fill

( 481 | &self, 482 | rule: FillRule, 483 | raster: &mut Raster

, 484 | clr: P, 485 | sgn_area: &mut [i16], 486 | ) where 487 | P: Pixel, 488 | { 489 | assert!(raster.width() <= sgn_area.len() as u32); 490 | let n_points = self.points.len(); 491 | if n_points > 0 { 492 | assert!(self.sub_is_done()); 493 | let mut vids: Vec = (0..n_points).map(Vid::from).collect(); 494 | vids.sort_by(|a, b| self.compare_vids(*a, *b)); 495 | let dir = self.get_dir(vids[0]); 496 | let top_row = row_of(self.point(vids[0]).y); 497 | let region = (0, top_row.max(0), raster.width(), raster.height()); 498 | let rows = raster.rows_mut(region); 499 | let mut scan = Scanner::new(self, rule, dir, rows, clr, sgn_area); 500 | scan.scan_vertices(vids, top_row); 501 | } 502 | } 503 | } 504 | 505 | impl<'a, P> Scanner<'a, P> 506 | where 507 | P: Pixel, 508 | { 509 | /// Create a new figure scanner. 510 | fn new( 511 | fig: &'a Fig, 512 | rule: FillRule, 513 | dir: FigDir, 514 | rows: RowsMut<'a, P>, 515 | clr: P, 516 | sgn_area: &'a mut [i16], 517 | ) -> Scanner<'a, P> { 518 | let edges = Vec::with_capacity(16); 519 | Scanner { 520 | fig, 521 | rule, 522 | dir, 523 | rows, 524 | clr, 525 | sgn_area, 526 | edges, 527 | } 528 | } 529 | 530 | /// Get Y value at a vertex. 531 | fn get_y(&self, vid: Vid) -> Fixed { 532 | self.fig.get_y(vid) 533 | } 534 | 535 | /// Scan all vertices in order. 536 | fn scan_vertices(&mut self, vids: Vec, top_row: i32) { 537 | let mut vids = vids.iter().peekable(); 538 | let mut y_row = top_row; 539 | while let Some(row_buf) = self.rows.next() { 540 | self.scan_continuing_edges(y_row); 541 | while let Some(vid) = vids.peek() { 542 | let y_vtx = self.get_y(**vid); 543 | if row_of(y_vtx) > y_row { 544 | break; 545 | } 546 | let vid = *vids.next().unwrap(); 547 | self.update_edges(vid, FigDir::Forward); 548 | self.update_edges(vid, FigDir::Reverse); 549 | } 550 | self.rasterize_row(row_buf); 551 | self.advance_edges(); 552 | y_row += 1; 553 | } 554 | } 555 | 556 | /// Scan edges continuing on this row. 557 | fn scan_continuing_edges(&mut self, y_row: i32) { 558 | let area = &mut self.sgn_area; 559 | for e in self.edges.iter_mut() { 560 | let cov = e.continuing_cov(y_row); 561 | if cov > 0 { 562 | e.calculate_x_limits_continuing(y_row); 563 | e.scan_area(self.dir, cov, area); 564 | } 565 | } 566 | } 567 | 568 | /// Advance all edges to the next row. 569 | fn advance_edges(&mut self) { 570 | for e in self.edges.iter_mut() { 571 | e.x_bot = e.x_bot + e.inv_slope; 572 | } 573 | } 574 | 575 | /// Update edges at a given vertex. 576 | fn update_edges(&mut self, vid: Vid, dir: FigDir) { 577 | let v = self.fig.next(vid, dir); 578 | if v != vid { 579 | let y = self.get_y(vid); 580 | match self.get_y(v).cmp(&y) { 581 | Greater => self.add_edge(vid, v, dir), 582 | Less => self.remove_edge(vid, dir.opposite()), 583 | _ => (), 584 | } 585 | } 586 | } 587 | 588 | /// Add an edge. 589 | fn add_edge(&mut self, v0: Vid, v1: Vid, dir: FigDir) { 590 | let fig = &self.fig; 591 | let p0 = fig.point(v0); // Upper point 592 | let p1 = fig.point(v1); // Lower point 593 | let mut e = Edge::new(v0, v1, p0, p1, dir); 594 | let cov = e.starting_cov(); 595 | if cov > 0 { 596 | e.calculate_x_limits_starting(); 597 | e.scan_area(self.dir, cov, self.sgn_area); 598 | } 599 | self.edges.push(e); 600 | } 601 | 602 | /// Remove an edge. 603 | fn remove_edge(&mut self, v1: Vid, dir: FigDir) { 604 | if let Some(i) = self.find_edge(v1, dir) { 605 | self.edges.swap_remove(i); 606 | } 607 | } 608 | 609 | /// Find an active edge 610 | fn find_edge(&self, v1: Vid, dir: FigDir) -> Option { 611 | for (i, e) in self.edges.iter().enumerate() { 612 | if v1 == e.v1 && dir == e.dir { 613 | return Some(i); 614 | } 615 | } 616 | None 617 | } 618 | 619 | /// Rasterize the current row. 620 | /// Signed area is zeroed upon return. 621 | fn rasterize_row(&mut self, row_buf: &mut [P]) { 622 | match self.rule { 623 | FillRule::NonZero => self.scan_non_zero(row_buf), 624 | FillRule::EvenOdd => self.scan_even_odd(row_buf), 625 | } 626 | } 627 | 628 | /// Accumulate scan area with non-zero fill rule. 629 | fn scan_non_zero(&mut self, dst: &mut [P]) { 630 | let clr = self.clr; 631 | let sgn_area = &mut self.sgn_area; 632 | if TypeId::of::

() == TypeId::of::() { 633 | // FIXME: only if clr is Matte8::new(255) 634 | matte_src_over_non_zero(dst, sgn_area); 635 | return; 636 | } 637 | let mut sum = 0; 638 | for (d, s) in dst.iter_mut().zip(sgn_area.iter_mut()) { 639 | sum += *s; 640 | *s = 0; 641 | let alpha = Ch8::from(saturating_cast_i16_u8(sum)); 642 | d.composite_channels_alpha(&clr, SrcOver, &alpha); 643 | } 644 | } 645 | 646 | /// Accumulate scan area with even-odd fill rule. 647 | fn scan_even_odd(&mut self, dst: &mut [P]) { 648 | let clr = self.clr; 649 | let sgn_area = &mut self.sgn_area; 650 | if TypeId::of::

() == TypeId::of::() { 651 | // FIXME: only if clr is Matte8::new(255) 652 | matte_src_over_even_odd(dst, sgn_area); 653 | return; 654 | } 655 | let mut sum = 0; 656 | for (d, s) in dst.iter_mut().zip(sgn_area.iter_mut()) { 657 | sum += *s; 658 | *s = 0; 659 | let v = sum & 0xFF; 660 | let odd = sum & 0x100; 661 | let c = (v - odd).abs(); 662 | let alpha = Ch8::from(saturating_cast_i16_u8(c)); 663 | d.composite_channels_alpha(&clr, SrcOver, &alpha); 664 | } 665 | } 666 | } 667 | 668 | /// Cast an i16 to a u8 with saturation 669 | fn saturating_cast_i16_u8(v: i16) -> u8 { 670 | v.clamp(0, 255) as u8 671 | } 672 | 673 | /// Calculate pixel coverage 674 | /// 675 | /// fcov Total coverage (0 to 1 fixed-point). 676 | /// return Total pixel coverage (0 to 256). 677 | fn pixel_cov(fcov: Fixed) -> i16 { 678 | debug_assert!(fcov >= Fixed::ZERO && fcov <= Fixed::ONE); 679 | // Round to nearest pixel cov value 680 | let n: i32 = (fcov << 8).round().into(); 681 | n as i16 682 | } 683 | 684 | #[cfg(test)] 685 | mod test { 686 | use super::*; 687 | use pix::Raster; 688 | use pix::matte::Matte8; 689 | use pix::rgb::Rgba8p; 690 | 691 | #[test] 692 | fn fixed_pt() { 693 | let a = FxPt::new(2.0.into(), 1.0.into()); 694 | let b = FxPt::new(3.0.into(), 4.0.into()); 695 | let c = FxPt::new(Fixed::from(-1.0), 1.0.into()); 696 | assert_eq!(b - a, FxPt::new(1.0.into(), 3.0.into())); 697 | assert!(a.widdershins(b)); 698 | assert!(!b.widdershins(a)); 699 | assert!(b.widdershins(c)); 700 | } 701 | 702 | #[test] 703 | fn fig_3x3() { 704 | let clr = Rgba8p::new(99, 99, 99, 255); 705 | let mut m = Raster::with_clear(3, 3); 706 | let mut s = vec![0; 3]; 707 | let mut f = Fig::new(); 708 | f.add_point((1.0, 2.0)); 709 | f.add_point((1.0, 3.0)); 710 | f.add_point((2.0, 3.0)); 711 | f.add_point((2.0, 2.0)); 712 | f.close(); 713 | f.fill(FillRule::NonZero, &mut m, clr, &mut s); 714 | #[rustfmt::skip] 715 | let v = [ 716 | Rgba8p::default(), Rgba8p::default(), Rgba8p::default(), 717 | Rgba8p::default(), Rgba8p::default(), Rgba8p::default(), 718 | Rgba8p::default(), Rgba8p::new(99, 99, 99, 255), Rgba8p::default(), 719 | ]; 720 | assert_eq!(m.pixels(), &v); 721 | } 722 | 723 | #[test] 724 | fn fig_9x1() { 725 | let clr = Matte8::new(255); 726 | let mut m = Raster::::with_clear(9, 1); 727 | let mut s = vec![0; 16]; 728 | let mut f = Fig::new(); 729 | f.add_point((0.0, 0.0)); 730 | f.add_point((9.0, 1.0)); 731 | f.add_point((0.0, 1.0)); 732 | f.close(); 733 | f.fill(FillRule::NonZero, &mut m, clr, &mut s); 734 | assert_eq!([242, 213, 185, 156, 128, 100, 71, 43, 14], m.as_u8_slice()); 735 | } 736 | 737 | #[test] 738 | fn fig_x_bounds() { 739 | let clr = Matte8::new(255); 740 | let mut m = Raster::::with_clear(3, 3); 741 | let mut s = vec![0; 4]; 742 | let mut f = Fig::new(); 743 | f.add_point((-1.0, 0.0)); 744 | f.add_point((-1.0, 3.0)); 745 | f.add_point((3.0, 1.5)); 746 | f.close(); 747 | f.fill(FillRule::NonZero, &mut m, clr, &mut s); 748 | assert_eq!([112, 16, 0, 255, 224, 32, 112, 16, 0], m.as_u8_slice()); 749 | } 750 | 751 | #[test] 752 | fn fig_partial() { 753 | let clr = Matte8::new(255); 754 | let mut m = Raster::::with_clear(1, 3); 755 | let mut s = vec![0; 4]; 756 | let mut f = Fig::new(); 757 | f.add_point((0.5, 0.0)); 758 | f.add_point((0.5, 1.5)); 759 | f.add_point((1.0, 3.0)); 760 | f.add_point((1.0, 0.0)); 761 | f.close(); 762 | f.fill(FillRule::NonZero, &mut m, clr, &mut s); 763 | assert_eq!([128, 117, 43], m.as_u8_slice()); 764 | } 765 | 766 | #[test] 767 | fn fig_partial2() { 768 | let clr = Matte8::new(255); 769 | let mut m = Raster::::with_clear(3, 3); 770 | let mut s = vec![0; 3]; 771 | let mut f = Fig::new(); 772 | f.add_point((1.5, 0.0)); 773 | f.add_point((1.5, 1.5)); 774 | f.add_point((2.0, 3.0)); 775 | f.add_point((3.0, 3.0)); 776 | f.add_point((3.0, 0.0)); 777 | f.close(); 778 | f.fill(FillRule::NonZero, &mut m, clr, &mut s); 779 | assert_eq!([0, 128, 255, 0, 117, 255, 0, 43, 255], m.as_u8_slice()); 780 | } 781 | 782 | #[test] 783 | fn fig_partial3() { 784 | let clr = Matte8::new(255); 785 | let mut m = Raster::::with_clear(9, 1); 786 | let mut s = vec![0; 16]; 787 | let mut f = Fig::new(); 788 | f.add_point((0.0, 0.0)); 789 | f.add_point((0.0, 0.3)); 790 | f.add_point((9.0, 0.0)); 791 | f.close(); 792 | f.fill(FillRule::NonZero, &mut m, clr, &mut s); 793 | assert_eq!([73, 64, 56, 47, 39, 30, 22, 13, 4], m.as_u8_slice()); 794 | } 795 | } 796 | -------------------------------------------------------------------------------- /src/fixed.rs: -------------------------------------------------------------------------------- 1 | // fixed.rs Fixed-point type. 2 | // 3 | // Copyright (c) 2017-2020 Douglas P Lau 4 | // 5 | use std::fmt; 6 | use std::ops; 7 | 8 | /// Fixed-point type 9 | #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] 10 | pub struct Fixed(i32); 11 | 12 | /// Number of bits at fixed point (16.16) 13 | const FRACT_BITS: i32 = 16; 14 | 15 | /// Mask of fixed fractional bits 16 | const FRACT_MASK: i32 = (1 << FRACT_BITS) - 1; 17 | 18 | impl fmt::Debug for Fixed { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | write!(f, "{:?}", f32::from(*self)) 21 | } 22 | } 23 | 24 | impl ops::Add for Fixed { 25 | type Output = Self; 26 | 27 | fn add(self, rhs: Self) -> Self { 28 | Fixed(self.0 + rhs.0) 29 | } 30 | } 31 | 32 | impl ops::Sub for Fixed { 33 | type Output = Self; 34 | 35 | fn sub(self, rhs: Self) -> Self { 36 | Fixed(self.0 - rhs.0) 37 | } 38 | } 39 | 40 | impl ops::Mul for Fixed { 41 | type Output = Self; 42 | 43 | fn mul(self, rhs: Self) -> Self { 44 | let v = (self.0 as i64 * rhs.0 as i64) >> FRACT_BITS; 45 | Fixed(v as i32) 46 | } 47 | } 48 | 49 | impl ops::Div for Fixed { 50 | type Output = Self; 51 | 52 | fn div(self, rhs: Self) -> Self { 53 | let v = ((self.0 as i64) << (FRACT_BITS as i64)) / rhs.0 as i64; 54 | Fixed(v as i32) 55 | } 56 | } 57 | 58 | impl ops::Shl for Fixed { 59 | type Output = Self; 60 | 61 | fn shl(self, rhs: u32) -> Self { 62 | Fixed(self.0 << rhs) 63 | } 64 | } 65 | 66 | impl ops::Shr for Fixed { 67 | type Output = Self; 68 | 69 | fn shr(self, rhs: u32) -> Self { 70 | Fixed(self.0 >> rhs) 71 | } 72 | } 73 | 74 | impl From for Fixed { 75 | /// Get a fixed point value from an i32 76 | fn from(i: i32) -> Self { 77 | Fixed(i << FRACT_BITS) 78 | } 79 | } 80 | 81 | impl From for i32 { 82 | /// Get an i32 from a fixed point value 83 | fn from(f: Fixed) -> Self { 84 | f.0 >> FRACT_BITS 85 | } 86 | } 87 | 88 | impl From for Fixed { 89 | /// Get a fixed point value from an f32 90 | fn from(f: f32) -> Self { 91 | Fixed((f * (Self::ONE.0 as f32)) as i32) 92 | } 93 | } 94 | 95 | impl From for f32 { 96 | /// Get an f32 from a fixed point value 97 | fn from(f: Fixed) -> Self { 98 | f.0 as f32 / Fixed::ONE.0 as f32 99 | } 100 | } 101 | 102 | impl Fixed { 103 | /// Fixed value of 0. 104 | pub const ZERO: Self = Fixed(0); 105 | 106 | /// Fixed value of epsilon. 107 | pub const EPSILON: Self = Fixed(1); 108 | 109 | /// Fixed value of 1/2. 110 | pub const HALF: Self = Fixed(1 << (FRACT_BITS - 1)); 111 | 112 | /// Fixed value of 1. 113 | pub const ONE: Self = Fixed(1 << FRACT_BITS); 114 | 115 | /// Get the smallest value that can be represented by this type. 116 | pub const MIN: Self = Fixed(i32::MIN); 117 | 118 | /// Get the largest value that can be represented by this type. 119 | pub const MAX: Self = Fixed(i32::MAX); 120 | 121 | /// Get the absolute value of a number. 122 | pub fn abs(self) -> Self { 123 | Fixed(self.0.abs()) 124 | } 125 | 126 | /// Get the largest integer less than or equal to a number. 127 | pub fn floor(self) -> Self { 128 | Fixed(self.0 & !FRACT_MASK) 129 | } 130 | 131 | /// Get the smallest integer greater than or equal to a number. 132 | pub fn ceil(self) -> Self { 133 | (self + Self::ONE - Self::EPSILON).floor() 134 | } 135 | 136 | /// Round a number to the nearest integer. 137 | pub fn round(self) -> Self { 138 | (self + Self::HALF).floor() 139 | } 140 | 141 | /// Get the integer part of a number. 142 | pub fn trunc(self) -> Self { 143 | if self.0 >= 0 { 144 | self.floor() 145 | } else { 146 | self.ceil() 147 | } 148 | } 149 | 150 | /// Get the fractional part of a number. 151 | pub fn fract(self) -> Self { 152 | Fixed(self.0 & FRACT_MASK) 153 | } 154 | 155 | /// Get the average of two numbers. 156 | pub fn avg(self, rhs: Fixed) -> Self { 157 | Fixed((self.0 + rhs.0) >> 1) 158 | } 159 | } 160 | 161 | #[cfg(test)] 162 | mod test { 163 | use super::*; 164 | use std::cmp; 165 | 166 | #[test] 167 | fn fixed_add() { 168 | assert_eq!(Fixed::from(1) + Fixed::from(1), Fixed::from(2)); 169 | assert_eq!(Fixed::from(2) + Fixed::from(2), Fixed::from(4)); 170 | assert_eq!(Fixed::from(2) + Fixed::from(-2), Fixed::from(0)); 171 | assert_eq!(Fixed::from(2) + Fixed::from(-4), Fixed::from(-2)); 172 | assert_eq!(Fixed::from(1.5) + Fixed::from(1.5), Fixed::from(3)); 173 | assert_eq!(Fixed::from(3.5) + Fixed::from(-1.25), Fixed::from(2.25)); 174 | } 175 | 176 | #[test] 177 | fn fixed_sub() { 178 | assert_eq!(Fixed::from(1) - Fixed::from(1), Fixed::from(0)); 179 | assert_eq!(Fixed::from(3) - Fixed::from(2), Fixed::from(1)); 180 | assert_eq!(Fixed::from(2) - Fixed::from(-2), Fixed::from(4)); 181 | assert_eq!(Fixed::from(2) - Fixed::from(4), Fixed::from(-2)); 182 | assert_eq!(Fixed::from(1.5) - Fixed::from(1.5), Fixed::from(0)); 183 | assert_eq!(Fixed::from(3.5) - Fixed::from(1.25), Fixed::from(2.25)); 184 | } 185 | 186 | #[test] 187 | fn fixed_mul() { 188 | assert_eq!(Fixed::from(2) * Fixed::from(2), Fixed::from(4)); 189 | assert_eq!(Fixed::from(3) * Fixed::from(-2), Fixed::from(-6)); 190 | assert_eq!(Fixed::from(4) * Fixed::from(0.5), Fixed::from(2)); 191 | assert_eq!(Fixed::from(-16) * Fixed::from(-16), Fixed::from(256)); 192 | assert_eq!(Fixed::from(37) * Fixed::from(3), Fixed::from(111)); 193 | assert_eq!(Fixed::from(128) * Fixed::from(128), Fixed::from(16384)); 194 | } 195 | 196 | #[test] 197 | fn fixed_div() { 198 | assert_eq!(Fixed::from(4) / Fixed::from(2), Fixed::from(2)); 199 | assert_eq!(Fixed::from(-6) / Fixed::from(2), Fixed::from(-3)); 200 | assert_eq!(Fixed::from(2) / Fixed::from(0.5), Fixed::from(4)); 201 | assert_eq!(Fixed::from(256) / Fixed::from(-16), Fixed::from(-16)); 202 | assert_eq!(Fixed::from(111) / Fixed::from(3), Fixed::from(37)); 203 | assert_eq!(Fixed::from(37) / Fixed::from(3), Fixed::from(12.33333)); 204 | assert_eq!(Fixed::from(16384) / Fixed::from(128), Fixed::from(128)); 205 | } 206 | 207 | #[test] 208 | fn fixed_shl() { 209 | assert_eq!(Fixed::from(0) << 2, Fixed::from(0)); 210 | assert_eq!(Fixed::from(1) << 1, Fixed::from(2)); 211 | assert_eq!(Fixed::from(0.5) << 1, Fixed::from(1)); 212 | assert_eq!(Fixed::from(0.25) << 2, Fixed::from(1)); 213 | assert_eq!(Fixed::from(0.125) << 3, Fixed::from(1)); 214 | } 215 | 216 | #[test] 217 | fn fixed_shr() { 218 | assert_eq!(Fixed::from(0) >> 2, Fixed::from(0)); 219 | assert_eq!(Fixed::from(1) >> 1, Fixed::from(0.5)); 220 | assert_eq!(Fixed::from(2) >> 1, Fixed::from(1)); 221 | assert_eq!(Fixed::from(4) >> 2, Fixed::from(1)); 222 | assert_eq!(Fixed::from(8) >> 3, Fixed::from(1)); 223 | } 224 | 225 | #[test] 226 | fn fixed_abs() { 227 | assert_eq!(Fixed::from(1).abs(), Fixed::from(1)); 228 | assert_eq!(Fixed::from(500).abs(), Fixed::from(500)); 229 | assert_eq!(Fixed::from(-500).abs(), Fixed::from(500)); 230 | assert_eq!(Fixed::from(-1.5).abs(), Fixed::from(1.5)); 231 | assert_eq!(Fixed::from(-2.5).abs(), Fixed::from(2.5)); 232 | } 233 | 234 | #[test] 235 | fn fixed_floor() { 236 | assert_eq!(Fixed::from(1).floor(), Fixed::from(1)); 237 | assert_eq!(Fixed::from(500).floor(), Fixed::from(500)); 238 | assert_eq!(Fixed::from(1.5).floor(), Fixed::from(1)); 239 | assert_eq!(Fixed::from(1.99999).floor(), Fixed::from(1)); 240 | assert_eq!(Fixed::from(-0.0001).floor(), Fixed::from(-1)); 241 | assert_eq!(Fixed::from(-2.5).floor(), Fixed::from(-3)); 242 | } 243 | 244 | #[test] 245 | fn fixed_ceil() { 246 | assert_eq!(Fixed::from(1).ceil(), Fixed::from(1)); 247 | assert_eq!(Fixed::from(500).ceil(), Fixed::from(500)); 248 | assert_eq!(Fixed::from(1.5).ceil(), Fixed::from(2)); 249 | assert_eq!(Fixed::from(1.99999).ceil(), Fixed::from(2)); 250 | assert_eq!(Fixed::from(-0.0001).ceil(), Fixed::from(0)); 251 | assert_eq!(Fixed::from(-2.5).ceil(), Fixed::from(-2)); 252 | } 253 | 254 | #[test] 255 | fn fixed_round() { 256 | assert_eq!(Fixed::from(1).round(), Fixed::from(1)); 257 | assert_eq!(Fixed::from(500).round(), Fixed::from(500)); 258 | assert_eq!(Fixed::from(1.5).round(), Fixed::from(2)); 259 | assert_eq!(Fixed::from(1.49999).round(), Fixed::from(1)); 260 | assert_eq!(Fixed::from(1.99999).round(), Fixed::from(2)); 261 | assert_eq!(Fixed::from(-0.0001).round(), Fixed::from(0)); 262 | assert_eq!(Fixed::from(-2.5).round(), Fixed::from(-2)); 263 | assert_eq!(Fixed::from(-2.9).round(), Fixed::from(-3)); 264 | } 265 | 266 | #[test] 267 | fn fixed_trunc() { 268 | assert_eq!(Fixed::from(1).trunc(), Fixed::from(1)); 269 | assert_eq!(Fixed::from(500).trunc(), Fixed::from(500)); 270 | assert_eq!(Fixed::from(1.5).trunc(), Fixed::from(1)); 271 | assert_eq!(Fixed::from(1.49999).trunc(), Fixed::from(1)); 272 | assert_eq!(Fixed::from(1.99999).trunc(), Fixed::from(1)); 273 | assert_eq!(Fixed::from(-0.0001).trunc(), Fixed::from(0)); 274 | assert_eq!(Fixed::from(-2.5).trunc(), Fixed::from(-2)); 275 | assert_eq!(Fixed::from(-2.9).trunc(), Fixed::from(-2)); 276 | } 277 | 278 | #[test] 279 | fn fixed_fract() { 280 | assert_eq!(Fixed::from(0).fract(), Fixed::from(0)); 281 | assert_eq!(Fixed::from(0.1).fract(), Fixed::from(0.1)); 282 | assert_eq!(Fixed::from(0.9).fract(), Fixed::from(0.9)); 283 | assert_eq!(Fixed::from(1.5).fract(), Fixed::from(0.5)); 284 | assert_eq!(Fixed::from(-2.5).fract(), Fixed::from(0.5)); 285 | } 286 | 287 | #[test] 288 | fn fixed_avg() { 289 | assert_eq!(Fixed::from(1).avg(Fixed::from(2)), Fixed::from(1.5)); 290 | assert_eq!(Fixed::from(1).avg(Fixed::from(1)), Fixed::from(1)); 291 | assert_eq!(Fixed::from(5).avg(Fixed::from(-5)), Fixed::from(0)); 292 | assert_eq!(Fixed::from(3).avg(Fixed::from(37)), Fixed::from(20)); 293 | assert_eq!(Fixed::from(3).avg(Fixed::from(1.5)), Fixed::from(2.25)); 294 | } 295 | 296 | #[test] 297 | fn fixed_into() { 298 | let i: i32 = Fixed::from(37).into(); 299 | assert_eq!(i, 37); 300 | let f: f32 = Fixed::from(2.5).into(); 301 | assert_eq!(f, 2.5); 302 | let a: i32 = Fixed::from(2.5).into(); 303 | assert_eq!(a, 2); 304 | } 305 | 306 | #[test] 307 | fn fixed_cmp() { 308 | assert!(Fixed::from(37) > Fixed::from(3)); 309 | assert!(Fixed::from(3) < Fixed::from(37)); 310 | assert!(Fixed::from(-4) < Fixed::from(4)); 311 | assert_eq!(cmp::min(Fixed::from(37), Fixed::from(3)), Fixed::from(3)); 312 | assert_eq!(cmp::max(Fixed::from(37), Fixed::from(3)), Fixed::from(37)); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/geom.rs: -------------------------------------------------------------------------------- 1 | // geom.rs Simple geometry stuff. 2 | // 3 | // Copyright (c) 2017-2021 Douglas P Lau 4 | // 5 | use pointy::Pt; 6 | 7 | /// 2-dimensional vector / point with associated width. 8 | #[derive(Clone, Copy, Debug, PartialEq)] 9 | pub struct WidePt(pub Pt, pub f32); 10 | 11 | /// Calculate linear interpolation of two values 12 | /// 13 | /// The t value should be between 0 and 1. 14 | pub fn float_lerp(a: f32, b: f32, t: f32) -> f32 { 15 | b + (a - b) * t 16 | } 17 | 18 | impl Default for WidePt { 19 | fn default() -> Self { 20 | WidePt(Pt::default(), 1.0) 21 | } 22 | } 23 | 24 | impl WidePt { 25 | /// Get the width 26 | pub fn w(self) -> f32 { 27 | self.1 28 | } 29 | 30 | /// Find the midpoint between two wide points 31 | pub fn midpoint(self, rhs: Self) -> Self { 32 | let v = self.0.midpoint(rhs.0); 33 | let w = (self.w() + rhs.w()) / 2.0; 34 | WidePt(v, w) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/imgbuf.rs: -------------------------------------------------------------------------------- 1 | // imgbuf.rs Functions for blending image buffers. 2 | // 3 | // Copyright (c) 2017-2025 Douglas P Lau 4 | // 5 | use pix::chan::{Ch8, Linear, Premultiplied}; 6 | use pix::el::Pixel; 7 | use pix::matte::Matte8; 8 | use std::any::TypeId; 9 | use std::slice::from_raw_parts_mut; 10 | 11 | #[cfg(all(target_arch = "x86", feature = "simd"))] 12 | use std::arch::x86::*; 13 | #[cfg(all(target_arch = "x86_64", feature = "simd"))] 14 | use std::arch::x86_64::*; 15 | 16 | /// Blend to a Matte8 using a signed area with non-zero fill rule. 17 | /// Source buffer is zeroed upon return. 18 | /// 19 | /// * `dst` Destination buffer. 20 | /// * `sgn_area` Signed area. 21 | #[inline] 22 | pub fn matte_src_over_non_zero

(dst: &mut [P], sgn_area: &mut [i16]) 23 | where 24 | P: Pixel, 25 | { 26 | debug_assert_eq!(TypeId::of::

(), TypeId::of::()); 27 | let n_bytes = dst.len() * std::mem::size_of::(); 28 | let ptr = dst.as_mut_ptr() as *mut u8; 29 | let dst = unsafe { from_raw_parts_mut(ptr, n_bytes) }; 30 | accumulate_non_zero(dst, sgn_area); 31 | } 32 | 33 | /// Accumulate signed area with non-zero fill rule. 34 | /// Source buffer is zeroed upon return. 35 | /// 36 | /// * `dst` Destination buffer. 37 | /// * `src` Source buffer. 38 | fn accumulate_non_zero(dst: &mut [u8], src: &mut [i16]) { 39 | assert!(dst.len() <= src.len()); 40 | #[cfg(all( 41 | any(target_arch = "x86", target_arch = "x86_64"), 42 | feature = "simd" 43 | ))] 44 | { 45 | if is_x86_feature_detected!("ssse3") { 46 | unsafe { accumulate_non_zero_x86(dst, src) } 47 | return; 48 | } 49 | } 50 | accumulate_non_zero_fallback(dst, src) 51 | } 52 | 53 | /// Accumulate signed area with non-zero fill rule. 54 | fn accumulate_non_zero_fallback(dst: &mut [u8], src: &mut [i16]) { 55 | let mut sum = 0; 56 | for (d, s) in dst.iter_mut().zip(src.iter_mut()) { 57 | sum += *s; 58 | *s = 0; 59 | *d = saturating_cast_i16_u8(sum); 60 | } 61 | } 62 | 63 | /// Cast an i16 to a u8 with saturation 64 | fn saturating_cast_i16_u8(v: i16) -> u8 { 65 | v.clamp(0, 255) as u8 66 | } 67 | 68 | /// Accumulate signed area with non-zero fill rule. 69 | #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "simd"))] 70 | #[target_feature(enable = "ssse3")] 71 | unsafe fn accumulate_non_zero_x86(dst: &mut [u8], src: &mut [i16]) { 72 | unsafe { 73 | let zero = _mm_setzero_si128(); 74 | let mut sum = zero; 75 | let len = dst.len().min(src.len()); 76 | let dst = dst.as_mut_ptr(); 77 | let src = src.as_mut_ptr(); 78 | for i in (0..len).step_by(8) { 79 | let off = i as isize; 80 | let d = dst.offset(off) as *mut __m128i; 81 | let s = src.offset(off) as *mut __m128i; 82 | // get 8 values from src 83 | let mut a = _mm_loadu_si128(s); 84 | // zeroing now is faster than memset later 85 | _mm_storeu_si128(s, zero); 86 | // accumulate sum thru 8 pixels 87 | a = accumulate_i16x8_x86(a); 88 | // add in previous sum 89 | a = _mm_add_epi16(a, sum); 90 | // pack to u8 using saturation 91 | let b = _mm_packus_epi16(a, a); 92 | // store result to dest 93 | _mm_storel_epi64(d, b); 94 | // shuffle sum into all 16-bit lanes 95 | sum = _mm_shuffle_epi8(a, _mm_set1_epi16(0x0F_0E)); 96 | } 97 | } 98 | } 99 | 100 | /// Accumulate signed area sum thru 8 pixels. 101 | #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "simd"))] 102 | #[target_feature(enable = "ssse3")] 103 | unsafe fn accumulate_i16x8_x86(mut a: __m128i) -> __m128i { 104 | unsafe { 105 | // a7 a6 a5 a4 a3 a2 a1 a0 106 | // + a3 a2 a1 a0 __ __ __ __ 107 | a = _mm_add_epi16(a, _mm_slli_si128(a, 8)); 108 | // + a5 a4 a3 a2 a1 a0 __ __ 109 | // + a1 a0 __ __ __ __ __ __ 110 | a = _mm_add_epi16(a, _mm_slli_si128(a, 4)); 111 | // + a6 a5 a4 a3 a2 a1 a0 __ 112 | // + a2 a1 a0 __ __ __ __ __ 113 | // + a4 a3 a2 a1 a0 __ __ __ 114 | // + a0 __ __ __ __ __ __ __ 115 | _mm_add_epi16(a, _mm_slli_si128(a, 2)) 116 | } 117 | } 118 | 119 | /// Blend to a Matte8 using a signed area with even-odd fill rule. 120 | /// Source buffer is zeroed upon return. 121 | /// 122 | /// * `dst` Destination buffer. 123 | /// * `sgn_area` Signed area. 124 | #[inline] 125 | pub fn matte_src_over_even_odd

(dst: &mut [P], sgn_area: &mut [i16]) 126 | where 127 | P: Pixel, 128 | { 129 | debug_assert_eq!(TypeId::of::

(), TypeId::of::()); 130 | let n_bytes = std::mem::size_of_val(dst); 131 | let ptr = dst.as_mut_ptr() as *mut u8; 132 | let dst = unsafe { std::slice::from_raw_parts_mut(ptr, n_bytes) }; 133 | accumulate_even_odd(dst, sgn_area); 134 | } 135 | 136 | /// Accumulate signed area with even-odd fill rule. 137 | /// Source buffer is zeroed upon return. 138 | /// 139 | /// * `dst` Destination buffer. 140 | /// * `src` Source buffer. 141 | fn accumulate_even_odd(dst: &mut [u8], src: &mut [i16]) { 142 | assert!(dst.len() <= src.len()); 143 | #[cfg(all( 144 | any(target_arch = "x86", target_arch = "x86_64"), 145 | feature = "simd" 146 | ))] 147 | { 148 | if is_x86_feature_detected!("ssse3") { 149 | unsafe { accumulate_even_odd_x86(dst, src) } 150 | return; 151 | } 152 | } 153 | accumulate_even_odd_fallback(dst, src) 154 | } 155 | 156 | /// Accumulate signed area with even-odd fill rule. 157 | fn accumulate_even_odd_fallback(dst: &mut [u8], src: &mut [i16]) { 158 | let mut sum = 0; 159 | for (d, s) in dst.iter_mut().zip(src.iter_mut()) { 160 | sum += *s; 161 | *s = 0; 162 | let v = sum & 0xFF; 163 | let odd = sum & 0x100; 164 | let c = (v - odd).abs(); 165 | *d = saturating_cast_i16_u8(c); 166 | } 167 | } 168 | 169 | /// Accumulate signed area with even-odd fill rule. 170 | #[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), feature = "simd"))] 171 | #[target_feature(enable = "ssse3")] 172 | unsafe fn accumulate_even_odd_x86(dst: &mut [u8], src: &mut [i16]) { 173 | unsafe { 174 | let zero = _mm_setzero_si128(); 175 | let mut sum = zero; 176 | for (d, s) in dst.chunks_mut(8).zip(src.chunks_mut(8)) { 177 | let d = d.as_mut_ptr() as *mut __m128i; 178 | let s = s.as_mut_ptr() as *mut __m128i; 179 | // get 8 values from src 180 | let mut a = _mm_loadu_si128(s); 181 | // zeroing now is faster than memset later 182 | _mm_storeu_si128(s, zero); 183 | // accumulate sum thru 8 pixels 184 | a = accumulate_i16x8_x86(a); 185 | // add in previous sum 186 | a = _mm_add_epi16(a, sum); 187 | let mut val = _mm_and_si128(a, _mm_set1_epi16(0xFF)); 188 | let odd = _mm_and_si128(a, _mm_set1_epi16(0x100)); 189 | val = _mm_sub_epi16(val, odd); 190 | val = _mm_abs_epi16(val); 191 | // pack to u8 using saturation 192 | let b = _mm_packus_epi16(val, val); 193 | // store result to dest 194 | _mm_storel_epi64(d, b); 195 | // shuffle sum into all 16-bit lanes 196 | sum = _mm_shuffle_epi8(a, _mm_set1_epi16(0x0F_0E)); 197 | } 198 | } 199 | } 200 | 201 | #[cfg(test)] 202 | mod test { 203 | use super::*; 204 | 205 | #[test] 206 | fn non_zero() { 207 | let mut a = [0; 3000]; 208 | let mut b = [0; 3000]; 209 | b[0] = 200; 210 | accumulate_non_zero(&mut a, &mut b); 211 | for ai in a.iter() { 212 | assert_eq!(*ai, 200); 213 | } 214 | let mut c = [0; 5000]; 215 | let mut d = [0; 5000]; 216 | d[0] = 300; 217 | accumulate_non_zero(&mut c, &mut d); 218 | for ci in c.iter() { 219 | assert_eq!(*ci, 255); 220 | } 221 | } 222 | 223 | #[test] 224 | fn even_odd() { 225 | let mut a = [0; 3000]; 226 | let mut b = [0; 3000]; 227 | b[0] = 300; 228 | accumulate_even_odd(&mut a, &mut b); 229 | for ai in a.iter() { 230 | assert_eq!(*ai, 212); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // lib.rs Footile crate. 2 | // 3 | // Copyright (c) 2017-2021 Douglas P Lau 4 | // 5 | //! Footile is a 2D vector graphics library. It can be used to fill and stroke 6 | //! paths. These are created using typical vector drawing primitives such as 7 | //! lines and bézier splines. 8 | //! 9 | //! ## Example 10 | //! ```rust 11 | //! use footile::{FillRule, Path2D, Plotter}; 12 | //! use pix::{matte::Matte8, Raster}; 13 | //! 14 | //! let fish = Path2D::default() 15 | //! .relative() 16 | //! .pen_width(3.0) 17 | //! .move_to(112.0, 24.0) 18 | //! .line_to(-32.0, 24.0) 19 | //! .cubic_to(-96.0, -48.0, -96.0, 80.0, 0.0, 32.0) 20 | //! .line_to(32.0, 24.0) 21 | //! .line_to(-16.0, -40.0) 22 | //! .close() 23 | //! .finish(); 24 | //! let raster = Raster::with_clear(128, 128); 25 | //! let mut p = Plotter::new(raster); 26 | //! p.fill(FillRule::NonZero, &fish, Matte8::new(255)); 27 | //! ``` 28 | #![warn(missing_docs)] 29 | 30 | mod fig; 31 | mod fixed; 32 | mod geom; 33 | mod imgbuf; 34 | mod path; 35 | mod plotter; 36 | mod stroker; 37 | mod vid; 38 | 39 | pub use path::{FillRule, Path2D, PathOp}; 40 | pub use plotter::Plotter; 41 | pub use stroker::JoinStyle; 42 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | // path.rs 2D vector paths. 2 | // 3 | // Copyright (c) 2017-2025 Douglas P Lau 4 | // 5 | use pointy::Pt; 6 | 7 | /// Fill-rule for filling paths. 8 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 9 | pub enum FillRule { 10 | /// All points within bounds are filled 11 | NonZero, 12 | /// Alternate filling with path outline 13 | EvenOdd, 14 | } 15 | 16 | /// Path operation. 17 | #[derive(Clone, Copy, Debug, PartialEq)] 18 | pub enum PathOp { 19 | /// Close the path 20 | Close(), 21 | /// Move to a point 22 | Move(Pt), 23 | /// Straight line to end point 24 | Line(Pt), 25 | /// Quadratic bézier curve (control point and end point) 26 | Quad(Pt, Pt), 27 | /// Cubic bézier curve (two control points and end point) 28 | Cubic(Pt, Pt, Pt), 29 | /// Set pen width (for stroking) 30 | PenWidth(f32), 31 | } 32 | 33 | /// A `Path2D` is a builder for `Vec`. 34 | /// 35 | /// # Example 36 | /// ``` 37 | /// use footile::Path2D; 38 | /// 39 | /// let path = Path2D::default() 40 | /// .move_to(10.0, 10.0) 41 | /// .line_to(90.0, 90.0) 42 | /// .finish(); 43 | /// ``` 44 | pub struct Path2D { 45 | /// Vec of path operations 46 | ops: Vec, 47 | /// Absolute vs relative coordinates 48 | absolute: bool, 49 | /// Current pen position 50 | pen: Pt, 51 | } 52 | 53 | impl Default for Path2D { 54 | fn default() -> Path2D { 55 | let ops = Vec::with_capacity(32); 56 | Path2D { 57 | ops, 58 | absolute: false, 59 | pen: Pt::default(), 60 | } 61 | } 62 | } 63 | 64 | impl Path2D { 65 | /// Use absolute coordinates for subsequent operations. 66 | pub fn absolute(mut self) -> Self { 67 | self.absolute = true; 68 | self 69 | } 70 | 71 | /// Use relative coordinates for subsequent operations. 72 | /// 73 | /// This is the default setting. 74 | pub fn relative(mut self) -> Self { 75 | self.absolute = false; 76 | self 77 | } 78 | 79 | /// Get absolute point. 80 | fn pt(&self, x: f32, y: f32) -> Pt { 81 | if self.absolute { 82 | Pt::new(x, y) 83 | } else { 84 | Pt::new(self.pen.x + x, self.pen.y + y) 85 | } 86 | } 87 | 88 | /// Close current sub-path and move pen to origin. 89 | pub fn close(mut self) -> Self { 90 | self.ops.push(PathOp::Close()); 91 | self.pen = Pt::default(); 92 | self 93 | } 94 | 95 | /// Move the pen to a point. 96 | /// 97 | /// * `x` X-position of point. 98 | /// * `y` Y-position of point. 99 | pub fn move_to(mut self, x: f32, y: f32) -> Self { 100 | let pb = self.pt(x, y); 101 | self.ops.push(PathOp::Move(pb)); 102 | self.pen = pb; 103 | self 104 | } 105 | 106 | /// Add a line from pen to a point. 107 | /// 108 | /// * `x` X-position of end point. 109 | /// * `y` Y-position of end point. 110 | pub fn line_to(mut self, x: f32, y: f32) -> Self { 111 | let pb = self.pt(x, y); 112 | self.ops.push(PathOp::Line(pb)); 113 | self.pen = pb; 114 | self 115 | } 116 | 117 | /// Add a quadratic bézier spline. 118 | /// 119 | /// The points are: 120 | /// 121 | /// * Current pen position: Pa 122 | /// * Control point: Pb (`bx` / `by`) 123 | /// * Spline end point: Pc (`cx` / `cy`) 124 | pub fn quad_to(mut self, bx: f32, by: f32, cx: f32, cy: f32) -> Self { 125 | let pb = self.pt(bx, by); 126 | let pc = self.pt(cx, cy); 127 | self.ops.push(PathOp::Quad(pb, pc)); 128 | self.pen = pc; 129 | self 130 | } 131 | 132 | /// Add a cubic bézier spline. 133 | /// 134 | /// The points are: 135 | /// 136 | /// * Current pen position: Pa 137 | /// * First control point: Pb (`bx` / `by`) 138 | /// * Second control point: Pc (`cx` / `cy`) 139 | /// * Spline end point: Pd (`dx` / `dy`) 140 | pub fn cubic_to( 141 | mut self, 142 | bx: f32, 143 | by: f32, 144 | cx: f32, 145 | cy: f32, 146 | dx: f32, 147 | dy: f32, 148 | ) -> Self { 149 | let pb = self.pt(bx, by); 150 | let pc = self.pt(cx, cy); 151 | let pd = self.pt(dx, dy); 152 | self.ops.push(PathOp::Cubic(pb, pc, pd)); 153 | self.pen = pd; 154 | self 155 | } 156 | 157 | /// Set pen stroke width. 158 | /// 159 | /// All subsequent path points will be affected, until the stroke width 160 | /// is changed again. 161 | /// 162 | /// * `width` Pen stroke width. 163 | pub fn pen_width(mut self, width: f32) -> Self { 164 | self.ops.push(PathOp::PenWidth(width)); 165 | self 166 | } 167 | 168 | /// Finish path with specified operations. 169 | pub fn finish(self) -> Vec { 170 | self.ops 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/plotter.rs: -------------------------------------------------------------------------------- 1 | // plotter.rs Vector path plotter. 2 | // 3 | // Copyright (c) 2017-2025 Douglas P Lau 4 | // 5 | use crate::fig::Fig; 6 | use crate::geom::{WidePt, float_lerp}; 7 | use crate::path::{FillRule, PathOp}; 8 | use crate::stroker::{JoinStyle, Stroke}; 9 | use pix::Raster; 10 | use pix::chan::{Ch8, Linear, Premultiplied}; 11 | use pix::el::Pixel; 12 | use pointy::{Pt, Transform}; 13 | use std::borrow::Borrow; 14 | 15 | /// Plotter for 2D vector [path]s. 16 | /// 17 | /// This is a software vector rasterizer featuring anti-aliasing. The plotter 18 | /// contains a raster, which is drawn by fill and stroke calls. 19 | /// 20 | /// [path]: struct.Path2D.html 21 | /// 22 | /// # Example 23 | /// ``` 24 | /// use footile::{Path2D, Plotter}; 25 | /// use pix::rgb::Rgba8p; 26 | /// use pix::Raster; 27 | /// 28 | /// let path = Path2D::default() 29 | /// .pen_width(3.0) 30 | /// .move_to(50.0, 34.0) 31 | /// .cubic_to(4.0, 16.0, 16.0, 28.0, 0.0, 32.0) 32 | /// .cubic_to(-16.0, -4.0, -4.0, -16.0, 0.0, -32.0) 33 | /// .close() 34 | /// .finish(); 35 | /// let mut p = Plotter::new(Raster::with_clear(100, 100)); 36 | /// p.stroke(&path, Rgba8p::new(255, 128, 0, 255)); 37 | /// ``` 38 | pub struct Plotter

39 | where 40 | P: Pixel, 41 | { 42 | /// Image raster 43 | raster: Raster

, 44 | /// Signed area buffer 45 | sgn_area: Vec, 46 | /// Current pen position and width 47 | pen: WidePt, 48 | /// User to pixel affine transform 49 | transform: Transform, 50 | /// Curve decomposition tolerance squared 51 | tol_sq: f32, 52 | /// Current stroke width 53 | s_width: f32, 54 | /// Current join style 55 | join_style: JoinStyle, 56 | } 57 | 58 | /// Plot destination 59 | trait PlotDest { 60 | /// Add a point. 61 | /// 62 | /// * `pt` Point to add (w indicates stroke width). 63 | fn add_point(&mut self, pt: WidePt); 64 | 65 | /// Close the current sub-figure. 66 | /// 67 | /// * `joined` If true, join ends of sub-plot. 68 | fn close(&mut self, joined: bool); 69 | } 70 | 71 | impl PlotDest for Fig { 72 | fn add_point(&mut self, pt: WidePt) { 73 | Fig::add_point(self, pt.0); 74 | } 75 | fn close(&mut self, _joined: bool) { 76 | Fig::close(self); 77 | } 78 | } 79 | 80 | impl PlotDest for Stroke { 81 | fn add_point(&mut self, pt: WidePt) { 82 | Stroke::add_point(self, pt); 83 | } 84 | fn close(&mut self, joined: bool) { 85 | Stroke::close(self, joined); 86 | } 87 | } 88 | 89 | impl

Plotter

90 | where 91 | P: Pixel, 92 | { 93 | /// Create a new plotter. 94 | /// 95 | /// * `raster` Raster to draw. 96 | pub fn new(raster: Raster

) -> Self { 97 | let tol = 0.3; 98 | let len = raster.width() as usize; 99 | // Capacity must be 8-element multiple (for SIMD) 100 | let cap = ((len + 7) >> 3) << 3; 101 | let mut sgn_area = vec![0; cap]; 102 | // Remove excess elements 103 | for _ in 0..cap - len { 104 | sgn_area.pop(); 105 | } 106 | Plotter { 107 | raster, 108 | sgn_area, 109 | pen: WidePt::default(), 110 | transform: Transform::default(), 111 | tol_sq: tol * tol, 112 | s_width: 1.0, 113 | join_style: JoinStyle::Miter(4.0), 114 | } 115 | } 116 | 117 | /// Get width in pixels. 118 | pub fn width(&self) -> u32 { 119 | self.raster.width() 120 | } 121 | 122 | /// Get height in pixels. 123 | pub fn height(&self) -> u32 { 124 | self.raster.height() 125 | } 126 | 127 | /// Reset pen. 128 | fn reset(&mut self) { 129 | self.pen = WidePt(Pt::default(), self.s_width); 130 | } 131 | 132 | /// Set tolerance threshold for curve decomposition. 133 | pub fn set_tolerance(&mut self, t: f32) -> &mut Self { 134 | let tol = t.max(0.01); 135 | self.tol_sq = tol * tol; 136 | self 137 | } 138 | 139 | /// Set the transform. 140 | pub fn set_transform(&mut self, t: Transform) -> &mut Self { 141 | self.transform = t; 142 | self 143 | } 144 | 145 | /// Set pen stroke width. 146 | /// 147 | /// All subsequent path points will be affected, until the stroke width 148 | /// is changed again. 149 | /// 150 | /// * `width` Pen stroke width. 151 | fn pen_width(&mut self, width: f32) { 152 | self.s_width = width; 153 | } 154 | 155 | /// Set stroke join style. 156 | /// 157 | /// * `js` Join style. 158 | pub fn set_join(&mut self, js: JoinStyle) -> &mut Self { 159 | self.join_style = js; 160 | self 161 | } 162 | 163 | /// Move the pen. 164 | fn move_pen(&mut self, p: WidePt) { 165 | self.pen = p; 166 | } 167 | 168 | /// Transform a point. 169 | fn transform_point(&self, p: WidePt) -> WidePt { 170 | let pt = self.transform * p.0; 171 | WidePt(pt, p.w()) 172 | } 173 | 174 | /// Add a series of ops. 175 | fn add_ops(&mut self, ops: T, dst: &mut D) 176 | where 177 | T: IntoIterator, 178 | T::Item: Borrow, 179 | D: PlotDest, 180 | { 181 | self.reset(); 182 | for op in ops { 183 | self.add_op(dst, op.borrow()); 184 | } 185 | } 186 | 187 | /// Add a path operation. 188 | fn add_op(&mut self, dst: &mut D, op: &PathOp) { 189 | match *op { 190 | PathOp::Close() => self.close(dst), 191 | PathOp::Move(pb) => self.move_to(dst, pb), 192 | PathOp::Line(pb) => self.line_to(dst, pb), 193 | PathOp::Quad(pb, pc) => self.quad_to(dst, pb, pc), 194 | PathOp::Cubic(pb, pc, pd) => self.cubic_to(dst, pb, pc, pd), 195 | PathOp::PenWidth(w) => self.pen_width(w), 196 | }; 197 | } 198 | 199 | /// Close current sub-path and move pen to origin. 200 | fn close(&mut self, dst: &mut D) { 201 | dst.close(true); 202 | self.reset(); 203 | } 204 | 205 | /// Move pen to a point. 206 | /// 207 | /// * `pb` New point. 208 | fn move_to(&mut self, dst: &mut D, pb: Pt) { 209 | let p = WidePt(pb, self.s_width); 210 | dst.close(false); 211 | let b = self.transform_point(p); 212 | dst.add_point(b); 213 | self.move_pen(p); 214 | } 215 | 216 | /// Add a line from pen to a point. 217 | /// 218 | /// * `pb` End point. 219 | fn line_to(&mut self, dst: &mut D, pb: Pt) { 220 | let p = WidePt(pb, self.s_width); 221 | let b = self.transform_point(p); 222 | dst.add_point(b); 223 | self.move_pen(p); 224 | } 225 | 226 | /// Add a quadratic bézier spline. 227 | /// 228 | /// The points are A (current pen position), B (control point), and C 229 | /// (spline end point). 230 | /// 231 | /// * `cp` Control point. 232 | /// * `end` End point. 233 | fn quad_to(&mut self, dst: &mut D, cp: Pt, end: Pt) { 234 | let pen = self.pen; 235 | let bb = WidePt(cp, (pen.w() + self.s_width) / 2.0); 236 | let cc = WidePt(end, self.s_width); 237 | let a = self.transform_point(pen); 238 | let b = self.transform_point(bb); 239 | let c = self.transform_point(cc); 240 | self.quad_to_tran(dst, a, b, c); 241 | self.move_pen(cc); 242 | } 243 | 244 | /// Add a quadratic bézier spline. 245 | /// 246 | /// The spline is decomposed into a series of lines using the DeCastlejau 247 | /// method. 248 | fn quad_to_tran( 249 | &self, 250 | dst: &mut D, 251 | a: WidePt, 252 | b: WidePt, 253 | c: WidePt, 254 | ) { 255 | let ab = a.midpoint(b); 256 | let bc = b.midpoint(c); 257 | let ab_bc = ab.midpoint(bc); 258 | let ac = a.midpoint(c); 259 | if self.is_within_tolerance(ab_bc, ac) { 260 | dst.add_point(c); 261 | } else { 262 | self.quad_to_tran(dst, a, ab, ab_bc); 263 | self.quad_to_tran(dst, ab_bc, bc, c); 264 | } 265 | } 266 | 267 | /// Check if two points are within tolerance threshold. 268 | fn is_within_tolerance(&self, a: WidePt, b: WidePt) -> bool { 269 | self.is_within_tolerance2(a.0, b.0) 270 | } 271 | 272 | /// Check if two points are within tolerance threshold. 273 | fn is_within_tolerance2(&self, a: Pt, b: Pt) -> bool { 274 | assert!(self.tol_sq > 0.0); 275 | a.distance_sq(b) <= self.tol_sq 276 | } 277 | 278 | /// Add a cubic bézier spline. 279 | /// 280 | /// The points are A (current pen position), B (first control point), C 281 | /// (second control point) and D (spline end point). 282 | /// 283 | /// * `cp0` First control point. 284 | /// * `cp1` Second control point. 285 | /// * `end` End point. 286 | fn cubic_to( 287 | &mut self, 288 | dst: &mut D, 289 | cp0: Pt, 290 | cp1: Pt, 291 | end: Pt, 292 | ) { 293 | let pen = self.pen; 294 | let w0 = float_lerp(pen.w(), self.s_width, 1.0 / 3.0); 295 | let w1 = float_lerp(pen.w(), self.s_width, 2.0 / 3.0); 296 | let bb = WidePt(cp0, w0); 297 | let cc = WidePt(cp1, w1); 298 | let dd = WidePt(end, self.s_width); 299 | let a = self.transform_point(pen); 300 | let b = self.transform_point(bb); 301 | let c = self.transform_point(cc); 302 | let d = self.transform_point(dd); 303 | self.cubic_to_tran(dst, a, b, c, d); 304 | self.move_pen(dd); 305 | } 306 | 307 | /// Add a cubic bézier spline. 308 | /// 309 | /// The spline is decomposed into a series of lines using the DeCastlejau 310 | /// method. 311 | fn cubic_to_tran( 312 | &self, 313 | dst: &mut D, 314 | pa: WidePt, 315 | pb: WidePt, 316 | pc: WidePt, 317 | pd: WidePt, 318 | ) { 319 | let ab = pa.midpoint(pb); 320 | let bc = pb.midpoint(pc); 321 | let cd = pc.midpoint(pd); 322 | let ab_bc = ab.midpoint(bc); 323 | let bc_cd = bc.midpoint(cd); 324 | let pe = ab_bc.midpoint(bc_cd); 325 | let ad = pa.midpoint(pd); 326 | if self.is_within_tolerance(pe, ad) { 327 | dst.add_point(pd); 328 | } else { 329 | self.cubic_to_tran(dst, pa, ab, ab_bc, pe); 330 | self.cubic_to_tran(dst, pe, bc_cd, cd, pd); 331 | } 332 | } 333 | 334 | /// Fill path onto the raster. 335 | /// 336 | /// * `rule` Fill rule. 337 | /// * `ops` PathOp iterator. 338 | /// * `clr` Color to fill. 339 | pub fn fill(&mut self, rule: FillRule, ops: T, clr: P) -> &mut Raster

340 | where 341 | T: IntoIterator, 342 | T::Item: Borrow, 343 | { 344 | let mut fig = Fig::new(); 345 | self.add_ops(ops, &mut fig); 346 | // Closing figure required to handle coincident start/end points 347 | fig.close(); 348 | fig.fill(rule, &mut self.raster, clr, &mut self.sgn_area[..]); 349 | &mut self.raster 350 | } 351 | 352 | /// Stroke path onto the raster. 353 | /// 354 | /// * `ops` PathOp iterator. 355 | /// * `clr` Color to stroke. 356 | pub fn stroke(&mut self, ops: T, clr: P) -> &mut Raster

357 | where 358 | T: IntoIterator, 359 | T::Item: Borrow, 360 | { 361 | let mut stroke = Stroke::new(self.join_style, self.tol_sq); 362 | self.add_ops(ops, &mut stroke); 363 | let ops = stroke.path_ops(); 364 | self.fill(FillRule::NonZero, ops.iter(), clr) 365 | } 366 | 367 | /// Get a reference to the raster. 368 | pub fn raster(&self) -> &Raster

{ 369 | &self.raster 370 | } 371 | 372 | /// Get a mutable reference to the raster. 373 | pub fn raster_mut(&mut self) -> &mut Raster

{ 374 | &mut self.raster 375 | } 376 | 377 | /// Consume the plotter and get the raster. 378 | pub fn into_raster(self) -> Raster

{ 379 | self.raster 380 | } 381 | } 382 | 383 | #[cfg(test)] 384 | mod test { 385 | use crate::*; 386 | use pix::Raster; 387 | use pix::matte::Matte8; 388 | 389 | #[test] 390 | fn overlapping() { 391 | let path = Path2D::default() 392 | .absolute() 393 | .move_to(8.0, 4.0) 394 | .line_to(8.0, 3.0) 395 | .cubic_to(8.0, 3.0, 8.0, 3.0, 9.0, 3.75) 396 | .line_to(8.0, 3.75) 397 | .line_to(8.5, 3.75) 398 | .line_to(8.5, 3.5) 399 | .finish(); 400 | let r = Raster::with_clear(16, 16); 401 | let mut p = Plotter::new(r); 402 | p.fill(FillRule::NonZero, &path, Matte8::new(255)); 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/stroker.rs: -------------------------------------------------------------------------------- 1 | // stroker.rs A path stroker. 2 | // 3 | // Copyright (c) 2017-2025 Douglas P Lau 4 | // 5 | use crate::geom::WidePt; 6 | use crate::path::PathOp; 7 | use crate::vid::Vid; 8 | use pointy::{Line, Pt}; 9 | use std::fmt; 10 | 11 | /// Style for stroke joins. 12 | #[derive(Clone, Copy, Debug, PartialEq)] 13 | pub enum JoinStyle { 14 | /// Mitered join with limit (miter length to stroke width ratio) 15 | Miter(f32), 16 | /// Beveled join 17 | Bevel, 18 | /// Rounded join 19 | Round, 20 | } 21 | 22 | /// Stroke direction enum 23 | #[derive(Clone, Copy, Debug, PartialEq)] 24 | enum Dir { 25 | Forward, 26 | Reverse, 27 | } 28 | 29 | /// Sub-stroke struct 30 | #[derive(Clone)] 31 | struct SubStroke { 32 | /// Starting point 33 | start: Vid, 34 | /// Number of points 35 | n_points: Vid, 36 | /// Joined ends flag 37 | joined: bool, 38 | /// Done flag 39 | done: bool, 40 | } 41 | 42 | /// Stroke struct 43 | #[derive(Clone)] 44 | pub struct Stroke { 45 | /// Join style 46 | join_style: JoinStyle, 47 | /// Tolerance squared 48 | tol_sq: f32, 49 | /// All points 50 | points: Vec, 51 | /// All sub-strokes 52 | subs: Vec, 53 | } 54 | 55 | impl SubStroke { 56 | /// Create a new sub-stroke 57 | fn new(start: Vid) -> SubStroke { 58 | SubStroke { 59 | start, 60 | n_points: Vid(0), 61 | joined: false, 62 | done: false, 63 | } 64 | } 65 | 66 | /// Get next vertex within a sub-stroke 67 | fn next(&self, vid: Vid, dir: Dir) -> Vid { 68 | match dir { 69 | Dir::Forward => { 70 | let v = vid + 1; 71 | if v < self.start + self.n_points { 72 | v 73 | } else { 74 | self.start 75 | } 76 | } 77 | Dir::Reverse => { 78 | if vid > self.start { 79 | vid - 1 80 | } else { 81 | self.start + self.n_points - 1 82 | } 83 | } 84 | } 85 | } 86 | 87 | /// Get count of points 88 | fn len(&self) -> Vid { 89 | if self.joined { 90 | self.n_points + 1 91 | } else if self.n_points > Vid(0) { 92 | self.n_points - 1 93 | } else { 94 | Vid(0) 95 | } 96 | } 97 | } 98 | 99 | impl fmt::Debug for Stroke { 100 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 101 | for sub in &self.subs { 102 | write!(f, "sub {:?}+{:?} ", sub.start, sub.n_points)?; 103 | let end = sub.start + sub.n_points; 104 | for v in usize::from(sub.start)..usize::from(end) { 105 | write!(f, "{:?} ", self.point(Vid::from(v)))?; 106 | } 107 | } 108 | Ok(()) 109 | } 110 | } 111 | 112 | impl Stroke { 113 | /// Create a new stroke. 114 | pub fn new(join_style: JoinStyle, tol_sq: f32) -> Stroke { 115 | let points = Vec::with_capacity(1024); 116 | let mut subs = Vec::with_capacity(16); 117 | subs.push(SubStroke::new(Vid(0))); 118 | Stroke { 119 | join_style, 120 | tol_sq, 121 | points, 122 | subs, 123 | } 124 | } 125 | 126 | /// Check if two points are within tolerance threshold. 127 | fn is_within_tolerance2(&self, a: Pt, b: Pt) -> bool { 128 | assert!(self.tol_sq > 0.0); 129 | a.distance_sq(b) <= self.tol_sq 130 | } 131 | 132 | /// Get the count of sub-strokes 133 | fn len(&self) -> usize { 134 | self.subs.len() 135 | } 136 | 137 | /// Get start of a sub-strokes 138 | fn sub_start(&self, i: usize) -> Vid { 139 | self.subs[i].start 140 | } 141 | 142 | /// Get end of a sub-strokes 143 | fn sub_end(&self, i: usize) -> Vid { 144 | let sub = &self.subs[i]; 145 | sub.next(sub.start, Dir::Reverse) 146 | } 147 | 148 | /// Check if a sub-stroke is joined 149 | fn sub_joined(&self, i: usize) -> bool { 150 | self.subs[i].joined 151 | } 152 | 153 | /// Get the number of points in a sub-stroke 154 | fn sub_points(&self, i: usize) -> Vid { 155 | self.subs[i].len() 156 | } 157 | 158 | /// Get the current sub-stroke 159 | fn sub_current(&mut self) -> &mut SubStroke { 160 | self.subs.last_mut().unwrap() 161 | } 162 | 163 | /// Add a new sub-stroke 164 | fn sub_add(&mut self) { 165 | let vid = Vid::from(self.points.len()); 166 | self.subs.push(SubStroke::new(vid)); 167 | } 168 | 169 | /// Add a point to the current sub-stroke 170 | fn sub_add_point(&mut self) { 171 | let sub = self.sub_current(); 172 | sub.n_points += 1; 173 | } 174 | 175 | /// Get the sub-stroke at a specified vertex ID 176 | fn sub_at(&self, vid: Vid) -> &SubStroke { 177 | let n_subs = self.subs.len(); 178 | for i in 0..n_subs { 179 | let sub = &self.subs[i]; 180 | if vid < sub.start + sub.n_points { 181 | return sub; 182 | } 183 | } 184 | // Invalid vid indicates bug 185 | unreachable!(); 186 | } 187 | 188 | /// Get next vertex 189 | fn next(&self, vid: Vid, dir: Dir) -> Vid { 190 | let sub = self.sub_at(vid); 191 | sub.next(vid, dir) 192 | } 193 | 194 | /// Get a point. 195 | /// 196 | /// * `vid` Vertex ID. 197 | fn point(&self, vid: Vid) -> WidePt { 198 | self.points[usize::from(vid)] 199 | } 200 | 201 | /// Add a point. 202 | /// 203 | /// * `pt` Point to add (w indicates stroke width). 204 | pub fn add_point(&mut self, pt: WidePt) { 205 | let n_pts = self.points.len(); 206 | if n_pts < usize::from(Vid::MAX) { 207 | let done = self.sub_current().done; 208 | if done { 209 | self.sub_add(); 210 | } 211 | if done || !self.coincident(pt) { 212 | self.points.push(pt); 213 | self.sub_add_point(); 214 | } 215 | } 216 | } 217 | 218 | /// Check if a point is coincident with previous point. 219 | fn coincident(&self, pt: WidePt) -> bool { 220 | if let Some(p) = self.points.last() { 221 | pt.0 == p.0 222 | } else { 223 | false 224 | } 225 | } 226 | 227 | /// Close the current sub-stroke. 228 | /// 229 | /// * `joined` If true, join ends of sub-stroke. 230 | pub fn close(&mut self, joined: bool) { 231 | if !self.points.is_empty() { 232 | let sub = self.sub_current(); 233 | sub.joined = joined; 234 | sub.done = true; 235 | } 236 | } 237 | 238 | /// Create path ops of the stroke 239 | pub fn path_ops(&self) -> Vec { 240 | // FIXME: this should make a lazy iterator 241 | let mut ops = vec![]; 242 | let n_subs = self.len(); 243 | for i in 0..n_subs { 244 | self.stroke_sub(&mut ops, i); 245 | } 246 | ops 247 | } 248 | 249 | /// Stroke one sub-figure. 250 | fn stroke_sub(&self, ops: &mut Vec, i: usize) { 251 | if self.sub_points(i) > Vid(0) { 252 | let start = self.sub_start(i); 253 | let end = self.sub_end(i); 254 | let joined = self.sub_joined(i); 255 | self.stroke_side(ops, i, start, Dir::Forward); 256 | if joined { 257 | ops.push(PathOp::Close()); 258 | } 259 | self.stroke_side(ops, i, end, Dir::Reverse); 260 | ops.push(PathOp::Close()); 261 | } 262 | } 263 | 264 | /// Stroke one side of a sub-figure to another figure. 265 | fn stroke_side( 266 | &self, 267 | ops: &mut Vec, 268 | i: usize, 269 | start: Vid, 270 | dir: Dir, 271 | ) { 272 | let mut xr: Option<(Pt, Pt)> = None; 273 | let mut v0 = start; 274 | let mut v1 = self.next(v0, dir); 275 | let joined = self.sub_joined(i); 276 | for _ in 0..usize::from(self.sub_points(i)) { 277 | let p0 = self.point(v0); 278 | let p1 = self.point(v1); 279 | let bounds = self.stroke_offset(p0, p1); 280 | let (pr0, pr1) = bounds; 281 | if let Some((xr0, xr1)) = xr { 282 | self.stroke_join(ops, p0, xr0, xr1, pr0, pr1); 283 | } else if !joined { 284 | self.stroke_point(ops, pr0); 285 | } 286 | xr = Some(bounds); 287 | v0 = v1; 288 | v1 = self.next(v1, dir); 289 | } 290 | if !joined { 291 | if let Some((_, xr1)) = xr { 292 | self.stroke_point(ops, xr1); 293 | } 294 | } 295 | } 296 | 297 | /// Offset segment by half stroke width. 298 | /// 299 | /// * `p0` First point. 300 | /// * `p1` Second point. 301 | fn stroke_offset(&self, p0: WidePt, p1: WidePt) -> (Pt, Pt) { 302 | // FIXME: scale offset to allow user units as well as pixel units 303 | let pp0 = p0.0; 304 | let pp1 = p1.0; 305 | let vr = (pp1 - pp0).right().normalize(); 306 | let pr0 = pp0 + vr * (p0.w() / 2.0); 307 | let pr1 = pp1 + vr * (p1.w() / 2.0); 308 | (pr0, pr1) 309 | } 310 | 311 | /// Add a point to stroke figure. 312 | fn stroke_point(&self, ops: &mut Vec, pt: Pt) { 313 | ops.push(PathOp::Line(pt)); 314 | } 315 | 316 | /// Add a stroke join. 317 | /// 318 | /// * `p` Join point (with stroke width). 319 | /// * `a0` First point of A segment. 320 | /// * `a1` Second point of A segment. 321 | /// * `b0` First point of B segment. 322 | /// * `b1` Second point of B segment. 323 | fn stroke_join( 324 | &self, 325 | ops: &mut Vec, 326 | p: WidePt, 327 | a0: Pt, 328 | a1: Pt, 329 | b0: Pt, 330 | b1: Pt, 331 | ) { 332 | match self.join_style { 333 | JoinStyle::Miter(ml) => self.stroke_miter(ops, a0, a1, b0, b1, ml), 334 | JoinStyle::Bevel => self.stroke_bevel(ops, a1, b0), 335 | JoinStyle::Round => self.stroke_round(ops, p, a0, a1, b0, b1), 336 | } 337 | } 338 | 339 | /// Add a miter join. 340 | fn stroke_miter( 341 | &self, 342 | ops: &mut Vec, 343 | a0: Pt, 344 | a1: Pt, 345 | b0: Pt, 346 | b1: Pt, 347 | ml: f32, 348 | ) { 349 | // formula: miter_length / stroke_width = 1 / sin ( theta / 2 ) 350 | // so: stroke_width / miter_length = sin ( theta / 2 ) 351 | if ml > 0.0 { 352 | // Minimum stroke:miter ratio 353 | let sm_min = 1.0 / ml; 354 | let th = (a1 - a0).angle_rel(b0 - b1); 355 | let sm = (th / 2.0).sin().abs(); 356 | if sm >= sm_min && sm < 1.0 { 357 | let lna = Line::new(a0, a1); 358 | let lnb = Line::new(b0, b1); 359 | // Calculate miter point 360 | if let Some(xp) = lna.intersection(lnb) { 361 | self.stroke_point(ops, xp); 362 | return; 363 | } 364 | } 365 | } 366 | self.stroke_bevel(ops, a1, b0); 367 | } 368 | 369 | /// Add a bevel join. 370 | fn stroke_bevel(&self, ops: &mut Vec, a1: Pt, b0: Pt) { 371 | self.stroke_point(ops, a1); 372 | self.stroke_point(ops, b0); 373 | } 374 | 375 | /// Add a round join. 376 | /// 377 | /// * `p` Join point (with stroke width). 378 | /// * `a1` Second point of A segment. 379 | /// * `b0` First point of B segment. 380 | fn stroke_round( 381 | &self, 382 | ops: &mut Vec, 383 | p: WidePt, 384 | a0: Pt, 385 | a1: Pt, 386 | b0: Pt, 387 | b1: Pt, 388 | ) { 389 | let th = (a1 - a0).angle_rel(b0 - b1); 390 | if th <= 0.0 { 391 | self.stroke_bevel(ops, a1, b0); 392 | } else { 393 | self.stroke_point(ops, a1); 394 | self.stroke_arc(ops, p, a1, b0); 395 | } 396 | } 397 | 398 | /// Add a stroke arc. 399 | fn stroke_arc( 400 | &self, 401 | ops: &mut Vec, 402 | p: WidePt, 403 | a: Pt, 404 | b: Pt, 405 | ) { 406 | let p2 = p.0; 407 | let vr = (b - a).right().normalize(); 408 | let c = p2 + vr * (p.w() / 2.0); 409 | let ab = a.midpoint(b); 410 | if self.is_within_tolerance2(c, ab) { 411 | self.stroke_point(ops, b); 412 | } else { 413 | self.stroke_arc(ops, p, a, c); 414 | self.stroke_arc(ops, p, c, b); 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /src/vid.rs: -------------------------------------------------------------------------------- 1 | // vid.rs Vertex ID 2 | // 3 | // Copyright (c) 2020-2025 Douglas P Lau 4 | // 5 | use std::convert::TryFrom; 6 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 7 | 8 | /// Vertex ID 9 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd)] 10 | pub struct Vid(pub u16); 11 | 12 | impl Vid { 13 | /// Minimum vertex ID 14 | pub const MIN: Self = Vid(u16::MIN); 15 | 16 | /// Maximum vertex ID 17 | pub const MAX: Self = Vid(u16::MAX); 18 | } 19 | 20 | impl From for Vid { 21 | fn from(v: usize) -> Self { 22 | Vid(u16::try_from(v).expect("Invalid vertex ID")) 23 | } 24 | } 25 | 26 | impl From for usize { 27 | fn from(v: Vid) -> Self { 28 | usize::from(v.0) 29 | } 30 | } 31 | 32 | impl Add for Vid 33 | where 34 | R: Into, 35 | { 36 | type Output = Self; 37 | 38 | fn add(self, rhs: R) -> Self { 39 | Vid(self.0 + rhs.into().0) 40 | } 41 | } 42 | 43 | impl AddAssign for Vid 44 | where 45 | R: Into, 46 | { 47 | fn add_assign(&mut self, rhs: R) { 48 | self.0 = self.0 + rhs.into().0; 49 | } 50 | } 51 | 52 | impl Sub for Vid 53 | where 54 | R: Into, 55 | { 56 | type Output = Self; 57 | 58 | fn sub(self, rhs: R) -> Self { 59 | Vid(self.0 - rhs.into().0) 60 | } 61 | } 62 | 63 | impl SubAssign for Vid 64 | where 65 | R: Into, 66 | { 67 | fn sub_assign(&mut self, rhs: R) { 68 | self.0 = self.0 - rhs.into().0; 69 | } 70 | } 71 | --------------------------------------------------------------------------------