├── .gitignore ├── rustfmt.toml ├── .cargo └── config.toml ├── examples ├── fonts │ ├── OpenSans-Regular.ttf │ └── OpenSans-LICENSE.txt ├── showcase │ ├── index.html │ ├── server.js │ └── main.rs └── application_framework.rs ├── src ├── lib.rs ├── error.rs ├── vertex.rs ├── convex_hull.rs ├── safe_float.rs ├── utils.rs ├── shaders.wgsl ├── text.rs ├── curve.rs ├── fill.rs ├── stroke.rs └── path.rs ├── LICENSE ├── Cargo.toml ├── .github └── workflows │ └── actions.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width=150 -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = [ "--cfg=web_sys_unstable_apis" ] 3 | rustdocflags = [ "--cfg=web_sys_unstable_apis" ] 4 | -------------------------------------------------------------------------------- /examples/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lichtso/contrast_renderer/HEAD/examples/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Contrast is a web-gpu based 2D render engine written in Rust. 2 | #![warn(missing_docs)] 3 | 4 | pub extern crate geometric_algebra; 5 | #[cfg(feature = "text")] 6 | pub extern crate ttf_parser; 7 | pub extern crate wgpu; 8 | 9 | pub mod convex_hull; 10 | pub mod curve; 11 | pub mod error; 12 | mod fill; 13 | pub mod path; 14 | pub mod renderer; 15 | pub mod safe_float; 16 | mod stroke; 17 | #[cfg(feature = "text")] 18 | pub mod text; 19 | pub mod utils; 20 | mod vertex; 21 | -------------------------------------------------------------------------------- /examples/showcase/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Contrast Renderer - Showcase 7 | 8 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error handling 2 | 3 | /// Miscellaneous errors 4 | #[derive(Debug)] 5 | pub enum Error { 6 | /// The choice of the parameters `clip_nesting_counter_bits` or `winding_counter_bits` is not supported. 7 | NumberOfStencilBitsIsUnsupported, 8 | /// Trying to render with more than 2 to the power of `clip_nesting_counter_bits` nested clip [Shape](crate::renderer::Shape)s. 9 | ClipStackOverflow, 10 | /// Trying to render with more than `opacity_layer_count` nested opacity groups 11 | TooManyNestedOpacityGroups, 12 | /// Exceeded the maximum number of [DashInterval](crate::path::DashInterval)s in [DynamicStrokeOptions](crate::path::DynamicStrokeOptions). 13 | TooManyDashIntervals, 14 | /// The passed [DynamicStrokeOptions](crate::path::DynamicStrokeOptions) index is invalid. 15 | DynamicStrokeOptionsIndexOutOfBounds, 16 | } 17 | 18 | /// Used for floating point comparison. 19 | pub const ERROR_MARGIN: f32 = 0.0001; 20 | -------------------------------------------------------------------------------- /src/vertex.rs: -------------------------------------------------------------------------------- 1 | pub type Vertex0 = [f32; 2]; 2 | 3 | #[allow(dead_code)] 4 | #[derive(Clone, Copy)] 5 | #[repr(C, packed)] 6 | pub struct Vertex2f(pub [f32; 2], pub [f32; 2]); 7 | 8 | #[allow(dead_code)] 9 | #[derive(Clone, Copy)] 10 | #[repr(C, packed)] 11 | pub struct Vertex2f1i(pub [f32; 2], pub [f32; 2], pub u32); 12 | 13 | #[allow(dead_code)] 14 | #[derive(Clone, Copy)] 15 | #[repr(C, packed)] 16 | pub struct Vertex3f(pub [f32; 2], pub [f32; 3]); 17 | 18 | #[allow(dead_code)] 19 | #[derive(Clone, Copy)] 20 | #[repr(C, packed)] 21 | pub struct Vertex3f1i(pub [f32; 2], pub [f32; 3], pub u32); 22 | 23 | #[allow(dead_code)] 24 | #[derive(Clone, Copy)] 25 | #[repr(C, packed)] 26 | pub struct Vertex4f(pub [f32; 2], pub [f32; 4]); 27 | 28 | pub fn triangle_fan_to_strip(vertices: Vec) -> Vec { 29 | let gather_indices = (0..vertices.len()).map(|i| if (i & 1) == 0 { i >> 1 } else { vertices.len() - 1 - (i >> 1) }); 30 | let mut result = Vec::with_capacity(vertices.len()); 31 | for src in gather_indices { 32 | result.push(vertices[src]); 33 | } 34 | result 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alexander Meißner 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 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | resolver = "2" 3 | name = "contrast_renderer" 4 | version = "0.1.4" 5 | authors = ["Alexander Meißner "] 6 | description = "A web-gpu based 2D render engine" 7 | repository = "https://github.com/Lichtso/contrast_renderer/" 8 | keywords = ["2d", "graphics", "geometry", "bezier"] 9 | license = "MIT" 10 | edition = "2018" 11 | 12 | [features] 13 | default = ["text"] 14 | text = ["ttf-parser"] 15 | 16 | [dependencies] 17 | wgpu = { version = "25.0.0" } 18 | geometric_algebra = "0.3.0" 19 | ttf-parser = { version = "0.14.0", optional = true } 20 | 21 | 22 | 23 | ### Showcase Example ### 24 | 25 | [dev-dependencies] 26 | winit = { version = "0.29" } 27 | log = "0.4" 28 | 29 | [package.metadata.wasm-pack.profile.release] 30 | wasm-opt = ["-Oz", "--enable-mutable-globals"] 31 | 32 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 33 | async-executor = "1.0" 34 | pollster = "0.2" 35 | 36 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 37 | web-sys = { version = "0.3.77", features = ["Performance"] } 38 | js-sys = "0.3.77" 39 | wasm-bindgen = "0.2.100" 40 | wasm-bindgen-futures = "0.4.45" 41 | console_error_panic_hook = "0.1.7" 42 | console_log = "0.1.2" 43 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: actions 2 | on: [push, pull_request] 3 | jobs: 4 | compile: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | - name: Install WASM Toolchain 10 | uses: actions-rs/toolchain@v1 11 | with: 12 | toolchain: stable 13 | target: wasm32-unknown-unknown 14 | - name: Install WASM Binding Generator 15 | uses: actions-rs/install@v0.1 16 | with: 17 | crate: wasm-bindgen-cli 18 | version: 0.2.100 19 | # use-tool-cache: true 20 | - name: Compile Native 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: build 24 | args: --release --examples 25 | - name: Compile WASM 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: build 29 | args: --release --examples --target wasm32-unknown-unknown 30 | - name: Generate WASM Binding 31 | run: wasm-bindgen --no-typescript --out-dir target/wasm --web target/wasm32-unknown-unknown/release/examples/showcase.wasm 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | path: | 35 | target/wasm/ 36 | target/release/examples/ -------------------------------------------------------------------------------- /examples/showcase/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'), 2 | path = require('path'), 3 | http = require('http'), 4 | contentTypeByFileExtension = { 5 | '.html': 'text/html', 6 | '.css': 'text/css', 7 | '.js': 'text/javascript', 8 | '.mjs': 'text/javascript', 9 | '.json': 'application/json', 10 | '.wasm': 'application/wasm', 11 | '.png': 'image/png', 12 | '.svg': 'image/svg+xml' 13 | }; 14 | 15 | const PORT = 8080, 16 | server = http.createServer((request, response) => { 17 | const filePath = '../../'+request.url, 18 | fileExtension = path.extname(filePath), 19 | contentType = (contentTypeByFileExtension[fileExtension]) ? contentTypeByFileExtension[fileExtension] : 'text/plain'; 20 | fs.readFile(filePath, (error, content) => { 21 | if(error) { 22 | if(error.code == 'ENOENT') 23 | response.writeHead(404); 24 | else 25 | response.writeHead(500); 26 | } else { 27 | response.writeHead(200, { 28 | 'Content-Type': contentType 29 | }); 30 | response.write(content); 31 | } 32 | response.end(); 33 | }); 34 | }).on('clientError', (err, socket) => { 35 | socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); 36 | }).listen(PORT, () => { 37 | console.log(`http://localhost:${PORT}/examples/showcase/index.html`); 38 | }); 39 | -------------------------------------------------------------------------------- /src/convex_hull.rs: -------------------------------------------------------------------------------- 1 | //! 2D convex hull algorithms 2 | 3 | use crate::{error::ERROR_MARGIN, safe_float::SafeFloat, utils::vec_to_point}; 4 | use geometric_algebra::RegressiveProduct; 5 | 6 | /// Andrew's (monotone chain) convex hull algorithm 7 | pub fn andrew(input_points: &[SafeFloat]) -> Vec<[f32; 2]> { 8 | let mut input_points = input_points.to_owned(); 9 | if input_points.len() < 3 { 10 | return input_points.iter().map(|input_point| input_point.unwrap()).collect(); 11 | } 12 | input_points.sort(); 13 | let mut hull = Vec::with_capacity(2 * input_points.len()); 14 | for input_point in input_points.iter().cloned() { 15 | while hull.len() > 1 16 | && vec_to_point(hull[hull.len() - 2]) 17 | .regressive_product(vec_to_point(hull[hull.len() - 1])) 18 | .regressive_product(vec_to_point(input_point.unwrap())) 19 | <= ERROR_MARGIN 20 | { 21 | hull.pop(); 22 | } 23 | hull.push(input_point.unwrap()); 24 | } 25 | hull.pop(); 26 | let t = hull.len() + 1; 27 | for input_point in input_points.iter().rev().cloned() { 28 | while hull.len() > t 29 | && vec_to_point(hull[hull.len() - 2]) 30 | .regressive_product(vec_to_point(hull[hull.len() - 1])) 31 | .regressive_product(vec_to_point(input_point.unwrap())) 32 | <= ERROR_MARGIN 33 | { 34 | hull.pop(); 35 | } 36 | hull.push(input_point.unwrap()); 37 | } 38 | hull.pop(); 39 | hull 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![actions](https://github.com/Lichtso/contrast_renderer/actions/workflows/actions.yml/badge.svg)](https://github.com/Lichtso/contrast_renderer/actions/workflows/actions.yml) 2 | [![Docs](https://docs.rs/contrast_renderer/badge.svg)](https://docs.rs/contrast_renderer/) 3 | [![crates.io](https://img.shields.io/crates/v/contrast_renderer.svg)](https://crates.io/crates/contrast_renderer) 4 | 5 | # Contrast Renderer 6 | Contrast is a [web-gpu](https://gpuweb.github.io/gpuweb/) based 2D renderer written in [Rust](https://www.rust-lang.org/). 7 | It renders planar [vector graphics](https://en.wikipedia.org/wiki/Vector_graphics) and can easily be integrated with other forward-rendering code. 8 | Filling uses [implicit curves](https://en.wikipedia.org/wiki/Implicit_curve) and is resolution-independent, while stroking uses [parametric curves](https://en.wikipedia.org/wiki/Parametric_equation) and is approximated via polygon [tesselation](https://en.wikipedia.org/wiki/Tessellation_(computer_graphics)). 9 | This way you can have non-diegetic, diegetic and spacial [GUI](https://en.wikipedia.org/wiki/Graphical_user_interface) elements on any flat plane: 10 | - As classic 2D menu overlay on top of the 3D scene. 11 | - As [HUD](https://en.wikipedia.org/wiki/Head-up_display) fixed relative to the camera movement but occluded by the 3D scene. 12 | - As decals on walls or [holograms](https://en.wikipedia.org/wiki/Holography_in_fiction) hovering in the 3D scene. 13 | 14 | To get started, checkout the [showcase example](examples/showcase/main.rs). 15 | 16 | 17 | ## Feature Roadmap 18 | ✓ Supported and implemented 19 | ◯ Rudimentary support 20 | ✗ Planned support, not implemented 21 | 22 | - Rendering 23 | - Anti Aliasing ◯ 24 | - MSAA ✓ 25 | - Instancing ✓ 26 | - Customizable (User Defined) 27 | - Shaders ✓ 28 | - Blending ✓ 29 | - Depth Test ✓ 30 | - Back Face Culling ✓ 31 | - Nestable Clipping ✓ 32 | - Nestable Transparency Layers (Group Opacity) ✓ 33 | - Filling 34 | - Paths 35 | - Polygons ✓ 36 | - Bezier Curves 37 | - Integral (Normal) 38 | - Quadratic ✓ 39 | - Cubic ✓ 40 | - Rational (Weighted) 41 | - Quadratic ✓ 42 | - Cubic ✓ 43 | - Winding Fill Rules ✓ 44 | - Stroking 45 | - Paths 46 | - Polygons ✓ 47 | - Bezier Curves 48 | - Approximation 49 | - Uniformly Spaced Parameters ✓ 50 | - Uniform Tangent Angle ✓ 51 | - Uniform Arc Length ✗ 52 | - Integral (Normal) 53 | - Quadratic ✓ 54 | - Cubic ✓ 55 | - Rational (Weighted) 56 | - Quadratic ✓ 57 | - Cubic ✓ 58 | - Stroke Width ✓ 59 | - Stroke Offset ◯ 60 | - Closed / Open ✓ 61 | - Line Joins 62 | - (Clipped) Miter ✓ 63 | - Bevel ✓ 64 | - Round ✓ 65 | - Line Caps (Square, Round, Out, In, Right, Left, Butt) ✓ 66 | - Dashing 67 | - Phase Translation ✓ 68 | - Repeating Gap Intervals ✓ 69 | - Dynamically Adjustable (for Animations) ✓ 70 | - Path Constructors 71 | - Polygon ✓ 72 | - Bezier Curves 73 | - Integral (Normal) 74 | - Quadratic ✓ 75 | - Cubic ✓ 76 | - Rational (Weighted) 77 | - Quadratic ✓ 78 | - Cubic ✓ 79 | - Rect ✓ 80 | - Rounded Rect ✓ 81 | - Ellipse ✓ 82 | - Circle ✓ 83 | - Elliptical Arc ✓ 84 | - [Optional] Font (TTF) 85 | - Glyph ✓ 86 | - Text ◯ 87 | - Graphical User Interface ✗ 88 | 89 | 90 | ## Dependencies 91 | 92 | ### Dependencies of the Library 93 | - Graphics API: [wgpu](https://wgpu.rs/) 94 | - Geometric Algebra: [geometric_algebra](https://github.com/Lichtso/geometric_algebra) 95 | - [Optional] Font Loader: [ttf-parser](https://github.com/RazrFalcon/ttf-parser) 96 | 97 | ### Dependencies of the Examples 98 | - Window API: [winit](https://github.com/rust-windowing/winit) 99 | - Logging: [log](https://github.com/rust-lang/log) 100 | -------------------------------------------------------------------------------- /src/safe_float.rs: -------------------------------------------------------------------------------- 1 | //! Floats which are guaranteed to be finite (no NaNs or Infinity) 2 | 3 | use geometric_algebra::{ppga2d, ppga3d}; 4 | 5 | /// Unlike `f32` and `f64` this guarantees that `x == x` so it can be used to sort and hash 6 | #[derive(Clone, Copy)] 7 | pub struct SafeFloat { 8 | values: [DataType; N], 9 | } 10 | 11 | macro_rules! implement { 12 | ($f_type:ty, 1) => { 13 | impl Default for SafeFloat<$f_type, 1> { 14 | fn default() -> Self { 15 | Self { values: [0.0] } 16 | } 17 | } 18 | 19 | impl std::fmt::Debug for SafeFloat<$f_type, 1> { 20 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 21 | self.values.fmt(f) 22 | } 23 | } 24 | 25 | impl SafeFloat<$f_type, 1> { 26 | /// Returns the unpacked value 27 | pub fn unwrap(self) -> $f_type { 28 | self.values[0] 29 | } 30 | } 31 | 32 | impl std::convert::From> for $f_type { 33 | fn from(safe_float: SafeFloat<$f_type, 1>) -> Self { 34 | safe_float.values[0] 35 | } 36 | } 37 | 38 | impl std::convert::From<&SafeFloat<$f_type, 1>> for $f_type { 39 | fn from(safe_float: &SafeFloat<$f_type, 1>) -> Self { 40 | safe_float.values[0] 41 | } 42 | } 43 | 44 | impl std::convert::From<$f_type> for SafeFloat<$f_type, 1> { 45 | fn from(mut value: $f_type) -> Self { 46 | assert!(value.is_finite()); 47 | if value.to_bits() == 1 << (std::mem::size_of::<$f_type>() * 8 - 1) { 48 | value = 0.0; 49 | } 50 | Self { values: [value] } 51 | } 52 | } 53 | 54 | impl std::convert::From<&$f_type> for SafeFloat<$f_type, 1> { 55 | fn from(value: &$f_type) -> Self { 56 | Self::from(*value) 57 | } 58 | } 59 | 60 | impl std::hash::Hash for SafeFloat<$f_type, 1> { 61 | fn hash(&self, state: &mut H) { 62 | self.values[0].to_bits().hash(state); 63 | } 64 | } 65 | 66 | impl PartialEq for SafeFloat<$f_type, 1> { 67 | fn eq(&self, other: &Self) -> bool { 68 | self.values[0].eq(&other.values[0]) 69 | } 70 | } 71 | 72 | impl Eq for SafeFloat<$f_type, 1> {} 73 | 74 | impl PartialOrd for SafeFloat<$f_type, 1> { 75 | fn partial_cmp(&self, other: &Self) -> Option { 76 | Some(self.cmp(other)) 77 | } 78 | } 79 | 80 | impl Ord for SafeFloat<$f_type, 1> { 81 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 82 | self.values[0].partial_cmp(&other.values[0]).unwrap() 83 | } 84 | } 85 | }; 86 | ($f_type:ty, $n:literal) => { 87 | impl Default for SafeFloat<$f_type, $n> { 88 | fn default() -> Self { 89 | Self { values: [0.0; $n] } 90 | } 91 | } 92 | 93 | impl std::fmt::Debug for SafeFloat<$f_type, $n> { 94 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 95 | self.values.fmt(f) 96 | } 97 | } 98 | 99 | impl std::convert::From> for [$f_type; $n] { 100 | fn from(safe_float: SafeFloat<$f_type, $n>) -> Self { 101 | safe_float.values 102 | } 103 | } 104 | 105 | impl std::convert::From<&SafeFloat<$f_type, $n>> for [$f_type; $n] { 106 | fn from(safe_float: &SafeFloat<$f_type, $n>) -> Self { 107 | safe_float.values 108 | } 109 | } 110 | 111 | impl std::convert::From<[$f_type; $n]> for SafeFloat<$f_type, $n> { 112 | fn from(mut values: [$f_type; $n]) -> Self { 113 | for value in values.iter_mut() { 114 | assert!(value.is_finite()); 115 | if value.to_bits() == 1 << (std::mem::size_of::<$f_type>() * 8 - 1) { 116 | *value = 0.0; 117 | } 118 | } 119 | Self { values } 120 | } 121 | } 122 | 123 | impl SafeFloat<$f_type, $n> { 124 | /// Returns the unpacked values 125 | pub fn unwrap(self) -> [$f_type; $n] { 126 | self.values 127 | } 128 | } 129 | 130 | impl std::convert::From<&[$f_type; $n]> for SafeFloat<$f_type, $n> { 131 | fn from(values: &[$f_type; $n]) -> Self { 132 | Self::from(*values) 133 | } 134 | } 135 | 136 | impl std::hash::Hash for SafeFloat<$f_type, $n> { 137 | fn hash(&self, state: &mut H) { 138 | for value in self.values.iter() { 139 | value.to_bits().hash(state); 140 | } 141 | } 142 | } 143 | 144 | impl PartialEq for SafeFloat<$f_type, $n> { 145 | fn eq(&self, other: &Self) -> bool { 146 | for (a, b) in self.values.iter().zip(other.values.iter()) { 147 | if !a.eq(b) { 148 | return false; 149 | } 150 | } 151 | true 152 | } 153 | } 154 | 155 | impl Eq for SafeFloat<$f_type, $n> {} 156 | 157 | impl PartialOrd for SafeFloat<$f_type, $n> { 158 | fn partial_cmp(&self, other: &Self) -> Option { 159 | Some(self.cmp(other)) 160 | } 161 | } 162 | 163 | impl Ord for SafeFloat<$f_type, $n> { 164 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 165 | for (a, b) in self.values.iter().zip(other.values.iter()) { 166 | let order = a.partial_cmp(b).unwrap(); 167 | if order != std::cmp::Ordering::Equal { 168 | return order; 169 | } 170 | } 171 | std::cmp::Ordering::Equal 172 | } 173 | } 174 | }; 175 | } 176 | 177 | implement!(f32, 1); 178 | implement!(f32, 2); 179 | implement!(f32, 3); 180 | implement!(f32, 4); 181 | 182 | implement!(f64, 1); 183 | implement!(f64, 2); 184 | implement!(f64, 3); 185 | implement!(f64, 4); 186 | 187 | impl std::convert::From> for ppga2d::Point { 188 | fn from(safe_float: SafeFloat) -> Self { 189 | Self::from(safe_float.unwrap()) 190 | } 191 | } 192 | 193 | impl std::convert::From for SafeFloat { 194 | fn from(point: ppga2d::Point) -> Self { 195 | [point[0], point[1], point[2]].into() 196 | } 197 | } 198 | 199 | impl std::convert::From> for ppga3d::Point { 200 | fn from(safe_float: SafeFloat) -> Self { 201 | Self::from(safe_float.unwrap()) 202 | } 203 | } 204 | 205 | impl std::convert::From for SafeFloat { 206 | fn from(point: ppga3d::Point) -> Self { 207 | [point[0], point[1], point[2], point[3]].into() 208 | } 209 | } 210 | 211 | impl std::convert::From> for ppga2d::Motor { 212 | fn from(safe_float: SafeFloat) -> Self { 213 | Self::from(safe_float.unwrap()) 214 | } 215 | } 216 | 217 | impl std::convert::From for SafeFloat { 218 | fn from(motor: ppga2d::Motor) -> Self { 219 | [motor[0], motor[1], motor[2], motor[3]].into() 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Miscellaneous utility and helper functions 2 | 3 | use geometric_algebra::{ppga2d, ppga3d, GeometricQuotient, OuterProduct, RegressiveProduct, Transformation, Zero}; 4 | use std::convert::TryInto; 5 | 6 | /// Like `vec!` but for `HashSet` 7 | #[macro_export] 8 | macro_rules! hash_set( 9 | [ $($key:expr),*$(,)? ] => { 10 | { 11 | #[allow(unused_mut)] 12 | let mut set = ::std::collections::HashSet::new(); 13 | $(set.insert($key);)* 14 | set 15 | } 16 | }; 17 | ); 18 | 19 | /// Like `vec!` but for `HashMap` 20 | #[macro_export] 21 | macro_rules! hash_map( 22 | { $($key:expr => $value:expr),*$(,)? } => { 23 | { 24 | #[allow(unused_mut)] 25 | let mut map = ::std::collections::HashMap::new(); 26 | $(map.insert($key, $value);)* 27 | map 28 | } 29 | }; 30 | ); 31 | 32 | /// Like `matches!` but returns an option of the matched value 33 | #[macro_export] 34 | macro_rules! match_option { 35 | ($value:expr, $value_kind:path) => { 36 | match $value { 37 | $value_kind(value) => Some(value), 38 | _ => None, 39 | } 40 | }; 41 | } 42 | 43 | /// Transmutes a vector. 44 | pub fn transmute_vec(mut vec: Vec) -> Vec { 45 | let ptr = vec.as_mut_ptr() as *mut T; 46 | let len = vec.len() * std::mem::size_of::() / std::mem::size_of::(); 47 | let capacity = vec.capacity() * std::mem::size_of::() / std::mem::size_of::(); 48 | std::mem::forget(vec); 49 | unsafe { Vec::from_raw_parts(ptr, len, capacity) } 50 | } 51 | 52 | /// Transmutes a slice. 53 | pub fn transmute_slice(slice: &[S]) -> &[T] { 54 | let ptr = slice.as_ptr() as *const T; 55 | let len = std::mem::size_of_val(slice) / std::mem::size_of::(); 56 | unsafe { std::slice::from_raw_parts(ptr, len) } 57 | } 58 | 59 | /// Transmutes a mutable slice. 60 | pub fn transmute_slice_mut(slice: &mut [S]) -> &mut [T] { 61 | let ptr = slice.as_mut_ptr() as *mut T; 62 | let len = std::mem::size_of_val(slice) / std::mem::size_of::(); 63 | unsafe { std::slice::from_raw_parts_mut(ptr, len) } 64 | } 65 | 66 | /// Returns the intersection point of two 2D lines (origin, direction). 67 | pub fn line_line_intersection(a: ppga2d::Plane, b: ppga2d::Plane) -> ppga2d::Point { 68 | let p = a.outer_product(b); 69 | p * (1.0 / p[0]) 70 | } 71 | 72 | /// Converts a axis aligned bounding box into 4 vertices. 73 | pub fn aabb_to_convex_polygon(bounding_box: &[f32; 4]) -> [ppga2d::Point; 4] { 74 | [ 75 | ppga2d::Point::new(1.0, bounding_box[0], bounding_box[1]), 76 | ppga2d::Point::new(1.0, bounding_box[0], bounding_box[3]), 77 | ppga2d::Point::new(1.0, bounding_box[2], bounding_box[3]), 78 | ppga2d::Point::new(1.0, bounding_box[2], bounding_box[1]), 79 | ] 80 | } 81 | 82 | /// Implements the separating axis theorem. 83 | /// 84 | /// Expects the vertices to be ordered clockwise. 85 | pub fn do_convex_polygons_overlap(a: &[ppga2d::Point], b: &[ppga2d::Point]) -> bool { 86 | for (a, b) in [(a, b), (b, a)] { 87 | 'outer: for index in 0..a.len() { 88 | let plane = a[(index + 1) % a.len()].regressive_product(a[index]); 89 | for point in b { 90 | if point.regressive_product(plane) <= 0.0 { 91 | continue 'outer; 92 | } 93 | } 94 | return false; 95 | } 96 | } 97 | true 98 | } 99 | 100 | /// Rotates a [ppga2d::Plane] 90° clockwise. 101 | pub fn rotate_90_degree_clockwise(v: ppga2d::Plane) -> ppga2d::Plane { 102 | ppga2d::Plane::new(0.0, v[2], -v[1]) 103 | } 104 | 105 | /// Projects a [ppga2d::Point]. 106 | pub fn point_to_vec(p: ppga2d::Point) -> [f32; 2] { 107 | [p[1] / p[0], p[2] / p[0]] 108 | } 109 | 110 | /// Creates an unweighted [ppga2d::Point]. 111 | pub fn vec_to_point(v: [f32; 2]) -> ppga2d::Point { 112 | ppga2d::Point::new(1.0, v[0], v[1]) 113 | } 114 | 115 | /// Creates a weighted [ppga2d::Point]. 116 | pub fn weighted_vec_to_point(w: f32, v: [f32; 2]) -> ppga2d::Point { 117 | ppga2d::Point::new(w, v[0] * w, v[1] * w) 118 | } 119 | 120 | /// Creates a [ppga2d::Motor] from an angle in radians. 121 | pub fn rotate2d(mut angle: f32) -> ppga2d::Motor { 122 | angle *= 0.5; 123 | ppga2d::Motor::new(angle.cos(), angle.sin(), 0.0, 0.0) 124 | } 125 | 126 | /// Creates a [ppga2d::Motor] from a vector. 127 | pub fn translate2d(v: [f32; 2]) -> ppga2d::Motor { 128 | ppga2d::Motor::new(1.0, 0.0, -0.5 * v[1], 0.5 * v[0]) 129 | } 130 | 131 | /// Returns the rotation angle in radians of the given [ppga2d::Motor]. 132 | pub fn rotation2d(motor: ppga2d::Motor) -> f32 { 133 | 2.0 * motor[1].atan2(motor[0]) 134 | } 135 | 136 | /// Returns the translation of the given [ppga2d::Motor]. 137 | pub fn translation2d(mut motor: ppga2d::Motor) -> [f32; 2] { 138 | motor = motor.geometric_quotient(ppga2d::Rotor::new(motor[0], motor[1])); 139 | [2.0 * motor[3], -2.0 * motor[2]] 140 | } 141 | 142 | /// Creates a [ppga3d::Rotor] which represents a rotation by `angle` radians around `axis`. 143 | pub fn rotate_around_axis(angle: f32, axis: &[f32; 3]) -> ppga3d::Rotor { 144 | let sinus = (angle * 0.5).sin(); 145 | ppga3d::Rotor::new((angle * 0.5).cos(), axis[0] * sinus, axis[1] * sinus, axis[2] * sinus) 146 | } 147 | 148 | /// Converts a [ppga2d::Motor] to a [ppga3d::Motor]. 149 | pub fn motor2d_to_motor3d(motor: &ppga2d::Motor) -> ppga3d::Motor { 150 | ppga3d::Motor::new(motor[0], 0.0, 0.0, motor[1], 0.0, -motor[3], motor[2], 0.0) 151 | } 152 | 153 | /// Converts a [ppga2d::Motor] to a 3x3 matrix for WebGPU. 154 | pub fn motor2d_to_mat3(motor: &ppga2d::Motor) -> [ppga2d::Point; 3] { 155 | let result = [1, 2, 0] 156 | .iter() 157 | .map(|index| { 158 | let mut point = ppga2d::Point::zero(); 159 | point[*index] = 1.0; 160 | let row = motor.transformation(point); 161 | ppga2d::Point::new(row[1], row[2], row[0]) 162 | }) 163 | .collect::>(); 164 | result.try_into().unwrap() 165 | } 166 | 167 | /// Converts a [ppga3d::Motor] to a 4x4 matrix for WebGPU. 168 | pub fn motor3d_to_mat4(motor: &ppga3d::Motor) -> [ppga3d::Point; 4] { 169 | let result = [1, 2, 3, 0] 170 | .iter() 171 | .map(|index| { 172 | let mut point = ppga3d::Point::zero(); 173 | point[*index] = 1.0; 174 | let row = motor.transformation(point); 175 | ppga3d::Point::new(row[1], row[2], row[3], row[0]) 176 | }) 177 | .collect::>(); 178 | result.try_into().unwrap() 179 | } 180 | 181 | /// Creates a 4x4 perspective projection matrix for GLSL. 182 | pub fn perspective_projection(field_of_view_y: f32, aspect_ratio: f32, near: f32, far: f32) -> [ppga3d::Point; 4] { 183 | let height = 1.0 / (field_of_view_y * 0.5).tan(); 184 | let denominator = 1.0 / (near - far); 185 | [ 186 | ppga3d::Point::new(height / aspect_ratio, 0.0, 0.0, 0.0), 187 | ppga3d::Point::new(0.0, height, 0.0, 0.0), 188 | ppga3d::Point::new(0.0, 0.0, -far * denominator, 1.0), 189 | ppga3d::Point::new(0.0, 0.0, near * far * denominator, 0.0), 190 | ] 191 | } 192 | 193 | /// Calculates the product of two 4x4 matrices for GLSL. 194 | pub fn matrix_multiplication(a: &[ppga3d::Point; 4], b: &[ppga3d::Point; 4]) -> [ppga3d::Point; 4] { 195 | [ 196 | a[0] * b[0][0] + a[1] * b[0][1] + a[2] * b[0][2] + a[3] * b[0][3], 197 | a[0] * b[1][0] + a[1] * b[1][1] + a[2] * b[1][2] + a[3] * b[1][3], 198 | a[0] * b[2][0] + a[1] * b[2][1] + a[2] * b[2][2] + a[3] * b[2][3], 199 | a[0] * b[3][0] + a[1] * b[3][1] + a[2] * b[3][2] + a[3] * b[3][3], 200 | ] 201 | } 202 | 203 | /// Converts from srgb color space to linear color space 204 | pub fn srgb_to_linear(mut color: [f32; 4]) -> [f32; 4] { 205 | for channel in color.iter_mut().take(3) { 206 | *channel = if *channel > 0.04045 { 207 | ((*channel + 0.055) / 1.055).powf(2.4) 208 | } else { 209 | *channel / 12.92 210 | }; 211 | } 212 | color 213 | } 214 | 215 | /// Converts from linear color space to srgb color space 216 | pub fn linear_to_srgb(mut color: [f32; 4]) -> [f32; 4] { 217 | for channel in color.iter_mut().take(3) { 218 | *channel = if *channel > 0.0031308 { 219 | 1.055 * (*channel).powf(1.0 / 2.4) - 0.055 220 | } else { 221 | 12.92 * *channel 222 | }; 223 | } 224 | color 225 | } 226 | -------------------------------------------------------------------------------- /src/shaders.wgsl: -------------------------------------------------------------------------------- 1 | struct DynamicStrokeDescriptor { 2 | gap_start: array, 3 | gap_end: array, 4 | caps: u32, 5 | count_dashed_join: u32, 6 | phase: f32, 7 | }; 8 | @group(0) @binding(0) 9 | var u_stroke: array; 10 | 11 | 12 | 13 | struct Instance { 14 | @location(0) transform_row_0: vec4, 15 | @location(1) transform_row_1: vec4, 16 | @location(2) transform_row_2: vec4, 17 | @location(3) transform_row_3: vec4, 18 | }; 19 | 20 | fn instance_transform(instance: Instance) -> mat4x4 { 21 | return mat4x4( 22 | instance.transform_row_0, 23 | instance.transform_row_1, 24 | instance.transform_row_2, 25 | instance.transform_row_3, 26 | ); 27 | } 28 | 29 | struct Fragment0 { 30 | @builtin(position) gl_Position: vec4, 31 | }; 32 | 33 | struct Fragment2f { 34 | @builtin(position) gl_Position: vec4, 35 | @location(0) @interpolate(perspective, sample) weights: vec2, 36 | }; 37 | 38 | struct Fragment2f1u { 39 | @builtin(position) gl_Position: vec4, 40 | @location(0) @interpolate(perspective, sample) texcoord: vec2, 41 | @location(1) @interpolate(flat) bevel_path_index: u32, 42 | @location(2) @interpolate(flat) end_texcoord_y: f32, 43 | }; 44 | 45 | struct Fragment3f { 46 | @builtin(position) gl_Position: vec4, 47 | @location(0) @interpolate(perspective, sample) weights: vec3, 48 | }; 49 | 50 | struct Fragment3f1u { 51 | @builtin(position) gl_Position: vec4, 52 | @location(0) @interpolate(perspective, sample) texcoord: vec3, 53 | @location(1) @interpolate(flat) bevel_path_index: u32, 54 | }; 55 | 56 | struct Fragment4f { 57 | @builtin(position) gl_Position: vec4, 58 | @location(0) @interpolate(perspective, sample) weights: vec4, 59 | }; 60 | 61 | struct FragmentColor { 62 | @builtin(position) gl_Position: vec4, 63 | @location(0) @interpolate(flat) color: vec4, 64 | }; 65 | 66 | @vertex 67 | fn vertex0( 68 | instance: Instance, 69 | @location(4) position: vec2, 70 | ) -> Fragment0 { 71 | var stage_out: Fragment0; 72 | stage_out.gl_Position = instance_transform(instance) * vec4(position, 0.0, 1.0); 73 | return stage_out; 74 | } 75 | 76 | @vertex 77 | fn vertex2f( 78 | instance: Instance, 79 | @location(4) position: vec2, 80 | @location(5) weights: vec2, 81 | ) -> Fragment2f { 82 | var stage_out: Fragment2f; 83 | stage_out.gl_Position = instance_transform(instance) * vec4(position, 0.0, 1.0); 84 | stage_out.weights = weights; 85 | return stage_out; 86 | } 87 | 88 | @vertex 89 | fn vertex2f1u( 90 | instance: Instance, 91 | @location(4) position: vec2, 92 | @location(5) texcoord: vec2, 93 | @location(6) bevel_path_index: u32, 94 | ) -> Fragment2f1u { 95 | var stage_out: Fragment2f1u; 96 | stage_out.gl_Position = instance_transform(instance) * vec4(position, 0.0, 1.0); 97 | stage_out.texcoord = texcoord; 98 | stage_out.bevel_path_index = bevel_path_index; 99 | stage_out.end_texcoord_y = texcoord.y; 100 | return stage_out; 101 | } 102 | 103 | @vertex 104 | fn vertex3f( 105 | instance: Instance, 106 | @location(4) position: vec2, 107 | @location(5) weights: vec3, 108 | ) -> Fragment3f { 109 | var stage_out: Fragment3f; 110 | stage_out.gl_Position = instance_transform(instance) * vec4(position, 0.0, 1.0); 111 | stage_out.weights = weights; 112 | return stage_out; 113 | } 114 | 115 | @vertex 116 | fn vertex3f1u( 117 | instance: Instance, 118 | @location(4) position: vec2, 119 | @location(5) texcoord: vec3, 120 | @location(6) bevel_path_index: u32, 121 | ) -> Fragment3f1u { 122 | var stage_out: Fragment3f1u; 123 | stage_out.gl_Position = instance_transform(instance) * vec4(position, 0.0, 1.0); 124 | stage_out.texcoord = texcoord; 125 | stage_out.bevel_path_index = bevel_path_index; 126 | return stage_out; 127 | } 128 | 129 | @vertex 130 | fn vertex4f( 131 | instance: Instance, 132 | @location(4) position: vec2, 133 | @location(5) weights: vec4, 134 | ) -> Fragment4f { 135 | var stage_out: Fragment4f; 136 | stage_out.gl_Position = instance_transform(instance) * vec4(position, 0.0, 1.0); 137 | stage_out.weights = weights; 138 | return stage_out; 139 | } 140 | 141 | @vertex 142 | fn vertex_color( 143 | instance: Instance, 144 | @location(4) position: vec2, 145 | @location(5) color: vec4, 146 | ) -> FragmentColor { 147 | var stage_out: FragmentColor; 148 | stage_out.gl_Position = instance_transform(instance) * vec4(position, 0.0, 1.0); 149 | stage_out.color = color; 150 | return stage_out; 151 | } 152 | 153 | 154 | 155 | struct Coverage { 156 | @builtin(sample_mask) mask: u32, 157 | }; 158 | 159 | fn coverage(gl_SampleID: u32, keep: bool) -> Coverage { 160 | var stage_out: Coverage; 161 | stage_out.mask = select(0u, 1u << (gl_SampleID & 31u), keep); 162 | return stage_out; 163 | } 164 | 165 | fn cap(texcoord: vec2, cap_type: u32) -> bool { 166 | switch(i32(cap_type & 15u)) { 167 | case 0: { // Square 168 | return texcoord.y > 0.5; 169 | } 170 | case 1: { // Round 171 | return dot(texcoord, texcoord) < 0.25; 172 | } 173 | case 2: { // Out 174 | return 0.5 - texcoord.y > abs(texcoord.x); 175 | } 176 | case 3: { // In 177 | return texcoord.y < abs(texcoord.x); 178 | } 179 | case 4: { // Right 180 | return 0.5 - texcoord.y > texcoord.x; 181 | } 182 | case 5: { // Left 183 | return texcoord.y - 0.5 < texcoord.x; 184 | } 185 | default: { // Butt 186 | return texcoord.y < 0.0; 187 | } 188 | } 189 | } 190 | 191 | fn joint(radius: f32, bevel: bool, count_dashed_join: u32) -> bool { 192 | switch(i32(count_dashed_join)) { 193 | default: { // Miter 194 | return true; 195 | } 196 | case 1: { // Bevel 197 | return bevel; 198 | } 199 | case 2: { // Round 200 | return radius <= 0.5; 201 | } 202 | } 203 | } 204 | 205 | fn stroke_dashed(path_index: u32, texcoord: vec2) -> bool { 206 | let last_interval_index: u32 = u_stroke[path_index].count_dashed_join >> 3u; 207 | let pattern_length: f32 = u_stroke[path_index].gap_end[last_interval_index]; 208 | var interval_index: u32 = 0u; 209 | var gap_start: f32; 210 | var gap_end: f32; 211 | var position_in_pattern = (texcoord.y - u_stroke[path_index].phase) % pattern_length; 212 | if(position_in_pattern < 0.0) { 213 | position_in_pattern = position_in_pattern + pattern_length; 214 | } 215 | loop { 216 | gap_end = u_stroke[path_index].gap_end[interval_index] - position_in_pattern; 217 | if(gap_end >= 0.0 || interval_index >= last_interval_index) { 218 | break; 219 | } 220 | interval_index = interval_index + 1u; 221 | } 222 | gap_start = position_in_pattern - u_stroke[path_index].gap_start[interval_index]; 223 | if(gap_start > 0.0) { 224 | let caps = u_stroke[path_index].caps >> (interval_index * 8u); 225 | let start_cap = cap(vec2(texcoord.x, gap_start), caps >> 4u); 226 | let end_cap = cap(vec2(texcoord.x, gap_end), caps); 227 | return start_cap || end_cap; 228 | } else { 229 | return true; 230 | } 231 | } 232 | 233 | @fragment 234 | fn stencil_solid() {} 235 | 236 | @fragment 237 | fn stencil_integral_quadratic_curve( 238 | stage_in: Fragment2f, 239 | @builtin(sample_index) gl_SampleID: u32, 240 | ) -> Coverage { 241 | return coverage(gl_SampleID, stage_in.weights.x * stage_in.weights.x - stage_in.weights.y <= 0.0); 242 | } 243 | 244 | @fragment 245 | fn stencil_integral_cubic_curve( 246 | stage_in: Fragment3f, 247 | @builtin(sample_index) gl_SampleID: u32, 248 | ) -> Coverage { 249 | return coverage(gl_SampleID, stage_in.weights.x * stage_in.weights.x * stage_in.weights.x - stage_in.weights.y * stage_in.weights.z <= 0.0); 250 | } 251 | 252 | @fragment 253 | fn stencil_rational_quadratic_curve( 254 | stage_in: Fragment3f, 255 | @builtin(sample_index) gl_SampleID: u32, 256 | ) -> Coverage { 257 | return coverage(gl_SampleID, stage_in.weights.x * stage_in.weights.x - stage_in.weights.y * stage_in.weights.z <= 0.0); 258 | } 259 | 260 | @fragment 261 | fn stencil_rational_cubic_curve( 262 | stage_in: Fragment4f, 263 | @builtin(sample_index) gl_SampleID: u32, 264 | ) -> Coverage { 265 | return coverage(gl_SampleID, stage_in.weights.x * stage_in.weights.x * stage_in.weights.x - stage_in.weights.y * stage_in.weights.z * stage_in.weights.w <= 0.0); 266 | } 267 | 268 | @fragment 269 | fn stencil_stroke_line( 270 | stage_in: Fragment2f1u, 271 | @builtin(sample_index) gl_SampleID: u32, 272 | ) -> Coverage { 273 | let path_index = stage_in.bevel_path_index & 65535u; 274 | var fill: bool; 275 | if((u_stroke[path_index].count_dashed_join & 4u) != 0u) { 276 | fill = stroke_dashed(path_index, stage_in.texcoord); 277 | } else if((stage_in.bevel_path_index & 65536u) != 0u) { 278 | fill = cap(vec2(stage_in.texcoord.x, stage_in.texcoord.y - stage_in.end_texcoord_y), u_stroke[path_index].caps >> 4u); 279 | } else if(stage_in.texcoord.y < 0.0) { 280 | fill = cap(vec2(stage_in.texcoord.x, -stage_in.texcoord.y), u_stroke[path_index].caps); 281 | } else { 282 | fill = true; 283 | } 284 | return coverage(gl_SampleID, fill); 285 | } 286 | 287 | @fragment 288 | fn stencil_stroke_joint( 289 | stage_in: Fragment3f1u, 290 | @builtin(sample_index) gl_SampleID: u32, 291 | ) -> Coverage { 292 | let radius = length(stage_in.texcoord.xy); 293 | let path_index = stage_in.bevel_path_index & 65535u; 294 | var fill: bool = joint(radius, (stage_in.bevel_path_index & 65536u) != 0u, u_stroke[path_index].count_dashed_join & 3u); 295 | let TAU: f32 = acos(-1.0) * 2.0; 296 | if(fill && (u_stroke[path_index].count_dashed_join & 4u) != 0u) { 297 | fill = stroke_dashed(path_index, vec2(radius, stage_in.texcoord.z + atan2(stage_in.texcoord.y, stage_in.texcoord.x) / TAU)); 298 | } 299 | return coverage(gl_SampleID, fill); 300 | } 301 | 302 | 303 | 304 | @fragment 305 | fn color_cover( 306 | stage_in: FragmentColor, 307 | ) -> @location(0) vec4 { 308 | return vec4(stage_in.color.rgb * stage_in.color.a, stage_in.color.a); 309 | } 310 | 311 | @fragment 312 | fn scale_alpha_context_cover( 313 | stage_in: FragmentColor, 314 | ) -> @location(0) vec4 { 315 | return vec4(0.0, 0.0, 0.0, 1.0 - stage_in.color.a); 316 | } 317 | 318 | @group(0) @binding(0) 319 | var frame: texture_2d; 320 | @group(0) @binding(0) 321 | var multisampled_frame: texture_multisampled_2d; 322 | 323 | @fragment 324 | fn save_alpha_context_cover( 325 | stage_in: Fragment0, 326 | ) -> @location(0) f32 { 327 | let alpha: f32 = textureLoad(frame, vec2(stage_in.gl_Position.xy), 0).a; 328 | return alpha; 329 | } 330 | 331 | @fragment 332 | fn multisampled_save_alpha_context_cover( 333 | stage_in: Fragment0, 334 | @builtin(sample_index) gl_SampleID: u32, 335 | ) -> @location(0) f32 { 336 | let alpha: f32 = textureLoad(multisampled_frame, vec2(stage_in.gl_Position.xy), i32(gl_SampleID)).a; 337 | return alpha; 338 | } 339 | 340 | @fragment 341 | fn restore_alpha_context_cover( 342 | stage_in: FragmentColor, 343 | ) -> @location(0) vec4 { 344 | let alpha: f32 = textureLoad(frame, vec2(stage_in.gl_Position.xy), 0).x; 345 | return vec4(0.0, 0.0, 0.0, (1.0 - alpha) * (1.0 - stage_in.color.a)); 346 | } 347 | 348 | @fragment 349 | fn multisampled_restore_alpha_context_cover( 350 | stage_in: FragmentColor, 351 | @builtin(sample_index) gl_SampleID: u32, 352 | ) -> @location(0) vec4 { 353 | let alpha: f32 = textureLoad(multisampled_frame, vec2(stage_in.gl_Position.xy), i32(gl_SampleID)).x; 354 | return vec4(0.0, 0.0, 0.0, (1.0 - alpha) * (1.0 - stage_in.color.a)); 355 | } 356 | -------------------------------------------------------------------------------- /examples/application_framework.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_arch = "wasm32"))] 2 | struct StdOutLogger; 3 | 4 | #[cfg(not(target_arch = "wasm32"))] 5 | impl log::Log for StdOutLogger { 6 | fn enabled(&self, metadata: &log::Metadata) -> bool { 7 | metadata.level() <= log::Level::Info 8 | } 9 | 10 | fn log(&self, record: &log::Record) { 11 | if self.enabled(record.metadata()) { 12 | println!("{} - {}", record.level(), record.args()); 13 | } 14 | } 15 | 16 | fn flush(&self) {} 17 | } 18 | 19 | #[cfg(not(target_arch = "wasm32"))] 20 | static LOGGER: StdOutLogger = StdOutLogger; 21 | 22 | #[cfg(not(target_arch = "wasm32"))] 23 | pub struct Spawner<'a> { 24 | executor: async_executor::LocalExecutor<'a>, 25 | } 26 | 27 | #[cfg(not(target_arch = "wasm32"))] 28 | impl<'a> Spawner<'a> { 29 | fn new() -> Self { 30 | Self { 31 | executor: async_executor::LocalExecutor::new(), 32 | } 33 | } 34 | 35 | #[allow(dead_code)] 36 | pub fn spawn_local(&self, future: impl std::future::Future + 'a) { 37 | self.executor.spawn(future).detach(); 38 | } 39 | 40 | fn run_until_stalled(&self) { 41 | while self.executor.try_tick() {} 42 | } 43 | } 44 | 45 | #[cfg(target_arch = "wasm32")] 46 | pub struct Spawner {} 47 | 48 | #[cfg(target_arch = "wasm32")] 49 | impl Spawner { 50 | fn new() -> Self { 51 | Self {} 52 | } 53 | 54 | #[allow(dead_code)] 55 | pub fn spawn_local(&self, future: impl std::future::Future + 'static) { 56 | wasm_bindgen_futures::spawn_local(future); 57 | } 58 | 59 | fn run_until_stalled(&self) {} 60 | } 61 | 62 | pub trait Application { 63 | fn new(device: &wgpu::Device, queue: &mut wgpu::Queue, surface_configuration: &wgpu::SurfaceConfiguration) -> Self; 64 | fn resize(&mut self, device: &wgpu::Device, queue: &mut wgpu::Queue, surface_configuration: &wgpu::SurfaceConfiguration); 65 | fn render(&mut self, device: &wgpu::Device, queue: &mut wgpu::Queue, frame: &wgpu::SurfaceTexture, animation_time: f64); 66 | fn window_event(&mut self, event: winit::event::WindowEvent); 67 | } 68 | 69 | pub struct ApplicationManager { 70 | window: std::sync::Arc, 71 | instance: wgpu::Instance, 72 | size: winit::dpi::PhysicalSize, 73 | surface: wgpu::Surface<'static>, 74 | adapter: wgpu::Adapter, 75 | device: wgpu::Device, 76 | queue: wgpu::Queue, 77 | } 78 | 79 | impl ApplicationManager { 80 | pub fn run(title: &'static str) { 81 | let event_loop = winit::event_loop::EventLoop::new().unwrap(); 82 | 83 | #[cfg(not(target_arch = "wasm32"))] 84 | { 85 | let setup = pollster::block_on(ApplicationManager::setup(&event_loop, title)); 86 | setup.start_loop::(event_loop); 87 | } 88 | 89 | #[cfg(target_arch = "wasm32")] 90 | { 91 | wasm_bindgen_futures::spawn_local(async move { 92 | let setup = ApplicationManager::setup(&event_loop, title).await; 93 | setup.start_loop::(event_loop); 94 | }); 95 | } 96 | } 97 | 98 | async fn setup(event_loop: &winit::event_loop::EventLoop<()>, title: &'static str) -> Self { 99 | let mut builder = winit::window::WindowBuilder::new(); 100 | builder = builder.with_title(title); 101 | #[cfg(target_arch = "wasm32")] 102 | { 103 | console_log::init().expect("Could not initialize logger"); 104 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 105 | use wasm_bindgen::JsCast; 106 | use winit::platform::web::WindowBuilderExtWebSys; 107 | let canvas = web_sys::window() 108 | .unwrap() 109 | .document() 110 | .unwrap() 111 | .get_element_by_id("canvas") 112 | .unwrap() 113 | .dyn_into::() 114 | .unwrap(); 115 | builder = builder.with_canvas(Some(canvas)); 116 | } 117 | let window = std::sync::Arc::new(builder.build(event_loop).unwrap()); 118 | 119 | #[cfg(not(target_arch = "wasm32"))] 120 | log::set_logger(&LOGGER) 121 | .map(|()| log::set_max_level(log::LevelFilter::Info)) 122 | .expect("Could not initialize logger"); 123 | 124 | let instance = wgpu::Instance::default(); 125 | let size = window.inner_size(); 126 | let surface = instance.create_surface(window.clone()).expect("WebGPU is not supported or not enabled"); 127 | let adapter = instance 128 | .request_adapter(&wgpu::RequestAdapterOptions { 129 | power_preference: wgpu::PowerPreference::default(), 130 | force_fallback_adapter: false, 131 | compatible_surface: Some(&surface), 132 | }) 133 | .await 134 | .expect("No suitable GPU adapters found on the system!"); 135 | 136 | let adapter_info = adapter.get_info(); 137 | log::info!("Using {} ({:?})", adapter_info.name, adapter_info.backend); 138 | 139 | let mut optional_features = wgpu::Features::default(); 140 | optional_features |= wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES; 141 | let required_features = wgpu::Features::default(); 142 | let required_limits = wgpu::Limits { ..wgpu::Limits::default() }; 143 | let adapter_features = adapter.features(); 144 | assert!( 145 | adapter_features.contains(required_features), 146 | "Adapter does not support required features: {:?}", 147 | required_features - adapter_features 148 | ); 149 | optional_features = optional_features - (optional_features - adapter_features); 150 | let (device, queue) = adapter 151 | .request_device(&wgpu::DeviceDescriptor { 152 | label: None, 153 | required_features: required_features | optional_features, 154 | required_limits, 155 | memory_hints: wgpu::MemoryHints::MemoryUsage, 156 | trace: wgpu::Trace::Off, 157 | }) 158 | .await 159 | .expect("Unable to find a suitable GPU adapter!"); 160 | 161 | Self { 162 | window, 163 | instance, 164 | size, 165 | surface, 166 | adapter, 167 | device, 168 | queue, 169 | } 170 | } 171 | 172 | fn generate_surface_configuration(&self) -> wgpu::SurfaceConfiguration { 173 | wgpu::SurfaceConfiguration { 174 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 175 | format: wgpu::TextureFormat::Bgra8Unorm, // self.surface.get_supported_formats(&self.adapter)[0], 176 | view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], 177 | width: self.size.width, 178 | height: self.size.height, 179 | present_mode: wgpu::PresentMode::Fifo, // self.surface.get_supported_present_modes(&self.adapter)[0], 180 | alpha_mode: wgpu::CompositeAlphaMode::Opaque, // self.surface.get_supported_alpha_modes(&adapter)[0], 181 | desired_maximum_frame_latency: 2, 182 | } 183 | } 184 | 185 | fn resize(&mut self, application: &mut A, size: winit::dpi::PhysicalSize) { 186 | self.size.width = size.width.max(1); 187 | self.size.height = size.height.max(1); 188 | self.window.request_redraw(); 189 | let surface_configuration = self.generate_surface_configuration(); 190 | self.surface.configure(&self.device, &surface_configuration); 191 | application.resize(&self.device, &mut self.queue, &surface_configuration); 192 | } 193 | 194 | fn start_loop(mut self, event_loop: winit::event_loop::EventLoop<()>) { 195 | let surface_configuration = self.generate_surface_configuration(); 196 | let mut application = A::new(&self.device, &mut self.queue, &surface_configuration); 197 | self.resize(&mut application, self.size); 198 | #[cfg(not(target_arch = "wasm32"))] 199 | let start_time = std::time::Instant::now(); 200 | #[cfg(target_arch = "wasm32")] 201 | let (clock, start_time) = { 202 | let clock = web_sys::window().and_then(|window| window.performance()).unwrap(); 203 | let start_time = clock.now(); 204 | (clock, start_time) 205 | }; 206 | let mut prev_frame = start_time; 207 | let mut rolling_average = 0u32; 208 | let mut average_window = [0u32; 64]; 209 | let mut average_window_slot = 0; 210 | 211 | let spawner = Spawner::new(); 212 | let _ = event_loop.run(move |event, target| { 213 | let _ = (&self.instance, &self.adapter); 214 | match event { 215 | winit::event::Event::AboutToWait => { 216 | spawner.run_until_stalled(); 217 | } 218 | winit::event::Event::WindowEvent { event, .. } => match event { 219 | winit::event::WindowEvent::KeyboardInput { 220 | event: 221 | winit::event::KeyEvent { 222 | logical_key: winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape), 223 | state: winit::event::ElementState::Pressed, 224 | .. 225 | }, 226 | .. 227 | } 228 | | winit::event::WindowEvent::CloseRequested => { 229 | target.exit(); 230 | } 231 | winit::event::WindowEvent::Resized(new_inner_size) => { 232 | self.resize(&mut application, new_inner_size); 233 | } 234 | winit::event::WindowEvent::RedrawRequested => { 235 | #[cfg(not(target_arch = "wasm32"))] 236 | let (animation_time, frame_time) = { 237 | let now = std::time::Instant::now(); 238 | let animation_time = (now - start_time).as_secs_f64(); 239 | let frame_time = (now - prev_frame).subsec_micros(); 240 | prev_frame = now; 241 | (animation_time, frame_time) 242 | }; 243 | #[cfg(target_arch = "wasm32")] 244 | let (animation_time, frame_time) = { 245 | let now = clock.now(); 246 | let animation_time = (now - start_time) * 0.001; 247 | let frame_time = ((now - prev_frame) * 1000.0) as u32; 248 | prev_frame = now; 249 | (animation_time, frame_time) 250 | }; 251 | rolling_average -= average_window[average_window_slot]; 252 | average_window[average_window_slot] = frame_time; 253 | rolling_average += average_window[average_window_slot]; 254 | average_window_slot = (average_window_slot + 1) % average_window.len(); 255 | log::info!( 256 | "rolling_average={} frame_time={}", 257 | rolling_average / average_window.len() as u32, 258 | frame_time 259 | ); 260 | 261 | let frame = self.surface.get_current_texture().unwrap(); 262 | application.render(&self.device, &mut self.queue, &frame, animation_time); 263 | frame.present(); 264 | } 265 | _ => { 266 | application.window_event(event); 267 | self.window.request_redraw(); 268 | } 269 | }, 270 | _ => {} 271 | } 272 | }); 273 | } 274 | } 275 | 276 | #[allow(dead_code)] 277 | fn main() {} 278 | -------------------------------------------------------------------------------- /examples/fonts/OpenSans-LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /examples/showcase/main.rs: -------------------------------------------------------------------------------- 1 | #[path = "../application_framework.rs"] 2 | mod application_framework; 3 | 4 | use contrast_renderer::renderer::RenderOperation; 5 | use geometric_algebra::{ 6 | ppga3d::{Motor, Rotor, Translator}, 7 | GeometricProduct, One, 8 | }; 9 | 10 | const OPEN_SANS_TTF: &[u8] = include_bytes!("../fonts/OpenSans-Regular.ttf"); 11 | const MSAA_SAMPLE_COUNT: u32 = 4; 12 | 13 | struct Application { 14 | depth_stencil_texture_view: Option, 15 | msaa_color_texture_view: Option, 16 | renderer: contrast_renderer::renderer::Renderer, 17 | dynamic_stroke_options: [contrast_renderer::path::DynamicStrokeOptions; 1], 18 | instance_buffers: [contrast_renderer::renderer::Buffer; 2], 19 | shape: contrast_renderer::renderer::Shape, 20 | viewport_size: wgpu::Extent3d, 21 | view_rotation: Rotor, 22 | view_distance: f32, 23 | } 24 | 25 | impl application_framework::Application for Application { 26 | fn new(device: &wgpu::Device, _queue: &mut wgpu::Queue, surface_configuration: &wgpu::SurfaceConfiguration) -> Self { 27 | let renderer = contrast_renderer::renderer::Renderer::new( 28 | device, 29 | contrast_renderer::renderer::Configuration { 30 | blending: wgpu::ColorTargetState { 31 | format: surface_configuration.format, 32 | blend: Some(wgpu::BlendState { 33 | color: wgpu::BlendComponent { 34 | src_factor: wgpu::BlendFactor::One, 35 | dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, 36 | operation: wgpu::BlendOperation::Add, 37 | }, 38 | alpha: wgpu::BlendComponent { 39 | src_factor: wgpu::BlendFactor::One, 40 | dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, 41 | operation: wgpu::BlendOperation::Add, 42 | }, 43 | }), 44 | write_mask: wgpu::ColorWrites::ALL, 45 | }, 46 | cull_mode: Some(wgpu::Face::Back), 47 | depth_stencil_format: wgpu::TextureFormat::Depth24PlusStencil8, 48 | depth_compare: wgpu::CompareFunction::LessEqual, 49 | depth_write_enabled: true, 50 | color_attachment_in_stencil_pass: true, 51 | msaa_sample_count: MSAA_SAMPLE_COUNT, 52 | clip_nesting_counter_bits: 4, 53 | winding_counter_bits: 4, 54 | alpha_layer_count: 0, 55 | }, 56 | ) 57 | .unwrap(); 58 | 59 | let dynamic_stroke_options = [contrast_renderer::path::DynamicStrokeOptions::Dashed { 60 | join: contrast_renderer::path::Join::Miter, 61 | pattern: vec![contrast_renderer::path::DashInterval { 62 | gap_start: 3.0.into(), 63 | gap_end: 4.0.into(), 64 | dash_start: contrast_renderer::path::Cap::Butt, 65 | dash_end: contrast_renderer::path::Cap::Butt, 66 | }], 67 | phase: 0.0.into(), 68 | }]; 69 | 70 | let font_face = ttf_parser::Face::from_slice(OPEN_SANS_TTF, 0).unwrap(); 71 | let mut paths = contrast_renderer::text::paths_of_text( 72 | &font_face, 73 | &contrast_renderer::text::Layout { 74 | size: 2.7.into(), 75 | orientation: contrast_renderer::text::Orientation::LeftToRight, 76 | major_alignment: contrast_renderer::text::Alignment::Center, 77 | minor_alignment: contrast_renderer::text::Alignment::Center, 78 | }, 79 | "Hello World", 80 | None, 81 | ); 82 | for path in &mut paths { 83 | path.reverse(); 84 | } 85 | paths.insert(0, contrast_renderer::path::Path::from_rounded_rect([0.0, 0.0], [5.8, 1.3], 0.5)); 86 | paths[0].stroke_options = Some(contrast_renderer::path::StrokeOptions { 87 | width: 0.1.into(), 88 | offset: 0.0.into(), 89 | miter_clip: 1.0.into(), 90 | closed: true, 91 | dynamic_stroke_options_group: 0, 92 | curve_approximation: contrast_renderer::path::CurveApproximation::UniformTangentAngle(0.1.into()), 93 | }); 94 | let shape = contrast_renderer::renderer::Shape::from_paths(device, &renderer, &dynamic_stroke_options, paths.as_slice(), None).unwrap(); 95 | 96 | Self { 97 | depth_stencil_texture_view: None, 98 | msaa_color_texture_view: None, 99 | renderer, 100 | dynamic_stroke_options, 101 | instance_buffers: [ 102 | contrast_renderer::renderer::Buffer::new(device, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, &[]), 103 | contrast_renderer::renderer::Buffer::new(device, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, &[]), 104 | ], 105 | shape, 106 | viewport_size: wgpu::Extent3d::default(), 107 | view_rotation: Rotor::one(), 108 | view_distance: 5.0, 109 | } 110 | } 111 | 112 | fn resize(&mut self, device: &wgpu::Device, _queue: &mut wgpu::Queue, surface_configuration: &wgpu::SurfaceConfiguration) { 113 | self.viewport_size = wgpu::Extent3d { 114 | width: surface_configuration.width, 115 | height: surface_configuration.height, 116 | depth_or_array_layers: 1, 117 | }; 118 | let depth_stencil_texture_descriptor = wgpu::TextureDescriptor { 119 | size: self.viewport_size, 120 | mip_level_count: 1, 121 | sample_count: MSAA_SAMPLE_COUNT, 122 | dimension: wgpu::TextureDimension::D2, 123 | format: self.renderer.get_config().depth_stencil_format, 124 | view_formats: &[self.renderer.get_config().depth_stencil_format], 125 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 126 | label: None, 127 | }; 128 | let depth_stencil_texture = device.create_texture(&depth_stencil_texture_descriptor); 129 | self.depth_stencil_texture_view = Some(depth_stencil_texture.create_view(&wgpu::TextureViewDescriptor { 130 | dimension: Some(wgpu::TextureViewDimension::D2), 131 | ..wgpu::TextureViewDescriptor::default() 132 | })); 133 | if MSAA_SAMPLE_COUNT > 1 { 134 | let msaa_color_texture_descriptor = wgpu::TextureDescriptor { 135 | size: self.viewport_size, 136 | mip_level_count: 1, 137 | sample_count: MSAA_SAMPLE_COUNT, 138 | dimension: wgpu::TextureDimension::D2, 139 | format: surface_configuration.format, 140 | view_formats: &[surface_configuration.format], 141 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, 142 | label: None, 143 | }; 144 | let msaa_color_texture = device.create_texture(&msaa_color_texture_descriptor); 145 | self.msaa_color_texture_view = Some(msaa_color_texture.create_view(&wgpu::TextureViewDescriptor { 146 | dimension: Some(wgpu::TextureViewDimension::D2), 147 | ..wgpu::TextureViewDescriptor::default() 148 | })); 149 | } 150 | self.renderer 151 | .resize_internal_buffers(device, self.viewport_size, self.msaa_color_texture_view.as_ref().unwrap()); 152 | } 153 | 154 | fn render(&mut self, device: &wgpu::Device, queue: &mut wgpu::Queue, frame: &wgpu::SurfaceTexture, animation_time: f64) { 155 | match &mut self.dynamic_stroke_options[0] { 156 | contrast_renderer::path::DynamicStrokeOptions::Dashed { phase, .. } => { 157 | *phase = (animation_time as f32 * 2.0).into(); 158 | } 159 | _ => unreachable!(), 160 | } 161 | self.shape.set_dynamic_stroke_options(queue, 0, &self.dynamic_stroke_options[0]).unwrap(); 162 | let projection_matrix = contrast_renderer::utils::matrix_multiplication( 163 | &contrast_renderer::utils::perspective_projection( 164 | std::f32::consts::PI * 0.5, 165 | self.viewport_size.width as f32 / self.viewport_size.height as f32, 166 | 1.0, 167 | 1000.0, 168 | ), 169 | &contrast_renderer::utils::motor3d_to_mat4( 170 | &Translator::new(1.0, 0.0, 0.0, -0.5 * self.view_distance).geometric_product(self.view_rotation), 171 | ), 172 | ); 173 | const ROWS: usize = 9; 174 | const COLUMNS: usize = 5; 175 | let mut instances_transform: Vec<[geometric_algebra::ppga3d::Point; 4]> = Vec::with_capacity(1 + ROWS * COLUMNS); 176 | let mut instances_color: Vec = Vec::with_capacity(1 + ROWS * COLUMNS); 177 | instances_transform.push(projection_matrix); 178 | instances_color.push([1.0, 1.0, 1.0, 1.0].into()); 179 | for y in 0..ROWS { 180 | for x in 0..COLUMNS { 181 | instances_transform.push(contrast_renderer::utils::matrix_multiplication( 182 | &projection_matrix, 183 | &contrast_renderer::utils::motor3d_to_mat4( 184 | &(Motor::new( 185 | 1.0, 186 | 0.0, 187 | 0.0, 188 | 0.0, 189 | 0.0, 190 | (x as f32 + 0.5 - COLUMNS as f32 * 0.5) * 7.0, 191 | (y as f32 + 0.5 - ROWS as f32 * 0.5) * 3.0, 192 | -5.0, 193 | )), 194 | ), 195 | )); 196 | let red = x as f32 / COLUMNS as f32; 197 | let green = y as f32 / ROWS as f32; 198 | instances_color.push([red, green, 1.0 - red - green, 1.0].into()); 199 | } 200 | } 201 | self.instance_buffers[0].update(device, queue, &contrast_renderer::concat_buffers!([&instances_transform]).1); 202 | self.instance_buffers[1].update(device, queue, &contrast_renderer::concat_buffers!([&instances_color]).1); 203 | let frame_view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); 204 | let msaa_frame_view = if MSAA_SAMPLE_COUNT == 1 { 205 | &frame_view 206 | } else { 207 | self.msaa_color_texture_view.as_ref().unwrap() 208 | }; 209 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); 210 | { 211 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 212 | label: None, 213 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 214 | view: msaa_frame_view, 215 | resolve_target: if MSAA_SAMPLE_COUNT == 1 { None } else { Some(&frame_view) }, 216 | ops: wgpu::Operations { 217 | load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), 218 | store: wgpu::StoreOp::Store, 219 | }, 220 | })], 221 | depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 222 | view: self.depth_stencil_texture_view.as_ref().unwrap(), 223 | depth_ops: Some(wgpu::Operations { 224 | load: wgpu::LoadOp::Clear(1.0), 225 | store: wgpu::StoreOp::Store, 226 | }), 227 | stencil_ops: Some(wgpu::Operations { 228 | load: wgpu::LoadOp::Clear(0), 229 | store: wgpu::StoreOp::Store, 230 | }), 231 | }), 232 | timestamp_writes: None, 233 | occlusion_query_set: None, 234 | }); 235 | render_pass.set_vertex_buffer(0, self.instance_buffers[0].buffer.slice(..)); 236 | for instance_index in 0..(1 + ROWS * COLUMNS) as u32 { 237 | self.shape.render( 238 | &self.renderer, 239 | &mut render_pass, 240 | instance_index..instance_index + 1, 241 | RenderOperation::Stencil, 242 | ); 243 | render_pass.set_vertex_buffer(1, self.instance_buffers[1].buffer.slice(..)); 244 | self.shape.render( 245 | &self.renderer, 246 | &mut render_pass, 247 | instance_index..instance_index + 1, 248 | RenderOperation::Color, 249 | ); 250 | } 251 | } 252 | queue.submit(Some(encoder.finish())); 253 | } 254 | 255 | fn window_event(&mut self, event: winit::event::WindowEvent) { 256 | match event { 257 | winit::event::WindowEvent::CursorMoved { position, .. } => { 258 | let position = [ 259 | std::f32::consts::PI * 1.2 * (position.x as f32 / self.viewport_size.width as f32 - 0.5), 260 | std::f32::consts::PI * 1.2 * (position.y as f32 / self.viewport_size.height as f32 - 0.5), 261 | ]; 262 | self.view_rotation = contrast_renderer::utils::rotate_around_axis(position[0], &[0.0, 1.0, 0.0]) 263 | .geometric_product(contrast_renderer::utils::rotate_around_axis(position[1], &[1.0, 0.0, 0.0])); 264 | } 265 | winit::event::WindowEvent::MouseWheel { delta, .. } => { 266 | let difference = match delta { 267 | winit::event::MouseScrollDelta::LineDelta(x, y) => [x, y], 268 | winit::event::MouseScrollDelta::PixelDelta(delta) => [delta.x as f32 * 0.1, delta.y as f32 * 0.1], 269 | }; 270 | self.view_distance = (self.view_distance + difference[1]).clamp(2.0, 100.0); 271 | } 272 | _ => {} 273 | } 274 | } 275 | } 276 | 277 | fn main() { 278 | application_framework::ApplicationManager::run::("Contrast Renderer - Showcase"); 279 | } 280 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | //! (Optional) Working with [font faces](ttf_parser::Face), converting glyphs and text to [Path]s 2 | 3 | use crate::{ 4 | path::{IntegralCubicCurveSegment, IntegralQuadraticCurveSegment, LineSegment, Path}, 5 | safe_float::SafeFloat, 6 | utils::{aabb_to_convex_polygon, do_convex_polygons_overlap, translate2d}, 7 | }; 8 | use geometric_algebra::ppga2d; 9 | 10 | /// Heap allocated font with a closed lifetime. 11 | pub struct Font { 12 | name: String, 13 | _backing_store: std::pin::Pin>, 14 | parsed_face: ttf_parser::Face<'static>, 15 | } 16 | 17 | impl Font { 18 | /// Load a TTF font face. 19 | pub fn new(name: String, font_data: &[u8]) -> Self { 20 | let backing_store = std::pin::Pin::new(font_data.to_vec().into_boxed_slice()); 21 | let backing_slice = unsafe { std::mem::transmute::<&[u8], &[u8]>(&backing_store) }; 22 | Self { 23 | name, 24 | _backing_store: backing_store, 25 | parsed_face: ttf_parser::Face::from_slice(backing_slice, 0).unwrap(), 26 | } 27 | } 28 | 29 | /// Get the name of the font. 30 | pub fn name(&self) -> &str { 31 | &self.name 32 | } 33 | 34 | /// Get the parsed font face. 35 | pub fn face(&self) -> &ttf_parser::Face { 36 | &self.parsed_face 37 | } 38 | } 39 | 40 | impl std::fmt::Debug for Font { 41 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 42 | write!(f, "Font({:?})", self.name) 43 | } 44 | } 45 | 46 | impl PartialEq for Font { 47 | fn eq(&self, other: &Self) -> bool { 48 | self.name == other.name 49 | } 50 | } 51 | 52 | impl Eq for Font {} 53 | 54 | impl std::hash::Hash for Font { 55 | fn hash(&self, state: &mut H) { 56 | self.name.hash(state); 57 | } 58 | } 59 | 60 | #[derive(Default)] 61 | struct OutlineBuilder { 62 | path: Path, 63 | paths: Vec, 64 | } 65 | 66 | impl ttf_parser::OutlineBuilder for OutlineBuilder { 67 | fn move_to(&mut self, x: f32, y: f32) { 68 | self.path.start = [x, y].into(); 69 | } 70 | 71 | fn line_to(&mut self, x: f32, y: f32) { 72 | self.path.push_line(LineSegment { 73 | control_points: [[x, y].into()], 74 | }); 75 | } 76 | 77 | fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { 78 | self.path.push_integral_quadratic_curve(IntegralQuadraticCurveSegment { 79 | control_points: [[x1, y1].into(), [x, y].into()], 80 | }); 81 | } 82 | 83 | fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { 84 | self.path.push_integral_cubic_curve(IntegralCubicCurveSegment { 85 | control_points: [[x1, y1].into(), [x2, y2].into(), [x, y].into()], 86 | }); 87 | } 88 | 89 | fn close(&mut self) { 90 | let mut path = Path::default(); 91 | std::mem::swap(&mut path, &mut self.path); 92 | self.paths.push(path); 93 | } 94 | } 95 | 96 | /// Returns the paths of a given glyph in a given font face. 97 | pub fn paths_of_glyph(face: &ttf_parser::Face, glyph_id: ttf_parser::GlyphId) -> Vec { 98 | let mut outline_builder = OutlineBuilder::default(); 99 | if let Some(_bounding_box) = face.outline_glyph(glyph_id, &mut outline_builder) { 100 | outline_builder.paths 101 | } else { 102 | Vec::new() 103 | } 104 | } 105 | 106 | /// Defines the axis and direction of text flow. 107 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 108 | pub enum Orientation { 109 | /// The text flows from right to left (-X). 110 | RightToLeft, 111 | /// The text flows from left to right (+X). 112 | LeftToRight, 113 | /// The text flows from top to bottom (-Y). 114 | TopToBottom, 115 | /// The text flows from bottom to top (+Y). 116 | BottomToTop, 117 | } 118 | 119 | /// Defines where the origin of the text is. 120 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 121 | pub enum Alignment { 122 | /// Origin is to the bottom left of the text (when [Orientation::LeftToRight]). 123 | Begin, 124 | /// Origin is on the baseline of the text. 125 | Baseline, 126 | /// Origin is at the center of the text. 127 | Center, 128 | /// Origin is to the top right of the text (when [Orientation::LeftToRight]). 129 | End, 130 | } 131 | 132 | /// Defines the geometric layout of a text. 133 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 134 | pub struct Layout { 135 | /// Font size 136 | pub size: SafeFloat, 137 | /// Axis and direction of text flow 138 | pub orientation: Orientation, 139 | /// Alignment along the axis defined by [Orientation] 140 | pub major_alignment: Alignment, 141 | /// Alignment along the other axis 142 | pub minor_alignment: Alignment, 143 | } 144 | 145 | macro_rules! calculate_aligned_positions { 146 | ($face:expr, $layout:expr, $text:expr) => {{ 147 | let replacement_glyph_id = $face.glyph_index('�'); 148 | let kerning_table = $face.tables().kern.and_then(|table| table.subtables.into_iter().next()); 149 | let (major_axis, sign_x, sign_y) = match $layout.orientation { 150 | Orientation::RightToLeft => (0, -1, -1), 151 | Orientation::LeftToRight => (0, 1, -1), 152 | Orientation::TopToBottom => (1, 1, -1), 153 | Orientation::BottomToTop => (1, 1, 1), 154 | }; 155 | let (line_minor_extent, line_gap) = if major_axis == 0 { 156 | ($face.height() as i64, $face.line_gap() as i64) 157 | } else { 158 | ($face.vertical_height().unwrap_or(0) as i64, $face.vertical_line_gap().unwrap_or(0) as i64) 159 | }; 160 | // let line_count = $text.chars().filter(|(index, char)| char == '\n').fold(1, |counter, _| counter + 1); 161 | let mut lines = Vec::new(); 162 | let mut line_major_extent = 0; 163 | let mut extent = [0; 2]; 164 | let mut glyph_positions = Vec::new(); 165 | let mut prev_glyph_id = None; 166 | let mut index = 0; 167 | for char in $text.chars() { 168 | index += 1; 169 | let mut glyph_position = extent; 170 | glyph_position[major_axis] = line_major_extent; 171 | if char == '\n' { 172 | glyph_positions.push((glyph_position, ttf_parser::GlyphId(0))); 173 | let mut line_glyph_positions = Vec::new(); 174 | std::mem::swap(&mut glyph_positions, &mut line_glyph_positions); 175 | lines.push((index, line_glyph_positions)); 176 | extent[major_axis] = extent[major_axis].max(line_major_extent); 177 | extent[1 - major_axis] += line_minor_extent + line_gap; 178 | line_major_extent = 0; 179 | prev_glyph_id = None; 180 | } else { 181 | let glyph_id = $face.glyph_index(char).or(replacement_glyph_id).unwrap(); 182 | if let (Some(kerning_table), Some(prev_glyph_id)) = (kerning_table, prev_glyph_id) { 183 | if let Some(kerning) = kerning_table.glyphs_kerning(prev_glyph_id, glyph_id) { 184 | line_major_extent += kerning as i64; 185 | } 186 | } 187 | prev_glyph_id = Some(glyph_id); 188 | let advance = if major_axis == 0 { 189 | $face.glyph_hor_advance(glyph_id) 190 | } else { 191 | $face.glyph_ver_advance(glyph_id) 192 | }; 193 | if let Some(advance) = advance { 194 | line_major_extent += advance as i64; 195 | } 196 | glyph_positions.push((glyph_position, glyph_id)); 197 | }; 198 | } 199 | { 200 | let mut glyph_position = extent; 201 | glyph_position[major_axis] = line_major_extent; 202 | glyph_positions.push((glyph_position, ttf_parser::GlyphId(0))); 203 | lines.push((index + 1, glyph_positions)); 204 | extent[major_axis] = extent[major_axis].max(line_major_extent); 205 | extent[1 - major_axis] += line_minor_extent; 206 | } 207 | let mut offset = [0, 0]; 208 | offset[1 - major_axis] = match $layout.minor_alignment { 209 | Alignment::Begin => -$face.descender() as i64, 210 | Alignment::Baseline => 0, 211 | Alignment::Center => $face.x_height().unwrap() as i64 / 2, 212 | Alignment::End => -line_minor_extent, 213 | }; 214 | for (_line_range_end, glyph_positions) in lines.iter_mut() { 215 | let line_major_extent = glyph_positions.last().unwrap().0[major_axis]; 216 | let mut offset = offset; 217 | offset[major_axis] = match $layout.major_alignment { 218 | Alignment::Begin => -extent[major_axis] / 2, 219 | Alignment::Baseline | Alignment::Center => -line_major_extent / 2, 220 | Alignment::End => extent[major_axis] / 2 - line_major_extent, 221 | }; 222 | offset[1 - major_axis] -= (extent[1 - major_axis] - line_minor_extent) / 2; 223 | for (position, _glyph_id) in glyph_positions.iter_mut() { 224 | position[0] = sign_x * (position[0] + offset[0]); 225 | position[1] = sign_y * (position[1] + offset[1]); 226 | } 227 | } 228 | (extent, [sign_x * offset[0], sign_y * offset[1]], lines) 229 | }}; 230 | } 231 | 232 | /// Arranges a given string to a set of paths (of its glyphs), according to the given alignment and font face. 233 | /// 234 | /// If a `clipping_area` convex polygon is given, then glyphs which are completely outside are discarded. 235 | /// Other glyphs will stay at the same position. 236 | pub fn paths_of_text(face: &ttf_parser::Face, layout: &Layout, text: &str, clipping_area: Option<&[ppga2d::Point]>) -> Vec { 237 | let (_extent, _offset, lines) = calculate_aligned_positions!(face, layout, text); 238 | let scale = layout.size.unwrap() / face.height() as f32; 239 | let mut result = Vec::new(); 240 | for ([x, y], glyph_id) in lines 241 | .iter() 242 | .flat_map(|(_line_range_end, glyph_positions)| glyph_positions[0..glyph_positions.len() - 1].iter()) 243 | { 244 | if let (Some(clipping_area), Some(glyph_bounding_box)) = (clipping_area, face.glyph_bounding_box(*glyph_id)) { 245 | let aabb = [ 246 | (glyph_bounding_box.x_min as i64 + x) as f32 * scale, 247 | (glyph_bounding_box.y_min as i64 + y) as f32 * scale, 248 | (glyph_bounding_box.x_max as i64 + x) as f32 * scale, 249 | (glyph_bounding_box.y_max as i64 + y) as f32 * scale, 250 | ]; 251 | if !do_convex_polygons_overlap(&aabb_to_convex_polygon(&aabb), clipping_area) { 252 | continue; 253 | } 254 | } 255 | let motor = translate2d([*x as f32 * scale, *y as f32 * scale]); 256 | let mut paths = paths_of_glyph(face, *glyph_id); 257 | for path in &mut paths { 258 | path.transform(scale, &motor); 259 | } 260 | result.append(&mut paths); 261 | } 262 | result 263 | } 264 | 265 | /// Bounding box of the entire text and glyph positions of the characters separated into lines 266 | pub struct TextGeometry { 267 | /// Primary flow direction of text 268 | /// 269 | /// 0 if horizontal, 1 if vertical. 270 | pub major_axis: usize, 271 | /// Half extent of the entire text 272 | pub half_extent: SafeFloat, 273 | /// Line ending character index and glyph positions per line 274 | /// 275 | /// The glyph positions include the line break at the end, so there is one more entry than there are printable characters. 276 | pub lines: Vec<(usize, Vec>)>, 277 | } 278 | 279 | impl TextGeometry { 280 | /// Calculates the bounding box in which [paths_of_text] fits, including preemptive spacing. 281 | /// 282 | /// Texts like "hello" and "bye" use the same vertical space even though the "y" in "bye" extends further downward. 283 | /// This way they are easier to align in the same row. 284 | pub fn new(face: &ttf_parser::Face, layout: &Layout, text: &str) -> Self { 285 | let major_axis = match layout.orientation { 286 | Orientation::RightToLeft | Orientation::LeftToRight => 0, 287 | Orientation::TopToBottom | Orientation::BottomToTop => 1, 288 | }; 289 | let scale = layout.size.unwrap() / face.height() as f32; 290 | let (extent, offset, lines) = calculate_aligned_positions!(face, layout, text); 291 | Self { 292 | major_axis, 293 | half_extent: [extent[0] as f32 * scale * 0.5, extent[1] as f32 * scale * 0.5].into(), 294 | lines: lines 295 | .iter() 296 | .map(|(line_range_end, glyph_positions)| { 297 | ( 298 | *line_range_end, 299 | glyph_positions 300 | .iter() 301 | .map(|(position, _glyph_id)| [(position[0] - offset[0]) as f32 * scale, (position[1] - offset[1]) as f32 * scale].into()) 302 | .collect(), 303 | ) 304 | }) 305 | .collect(), 306 | } 307 | } 308 | 309 | /// Returns the line index of a given character index 310 | pub fn line_index_from_char_index(&self, char_index: usize) -> usize { 311 | self.lines 312 | .iter() 313 | .position(|(line_range_end, _glyph_positions)| *line_range_end > char_index) 314 | .unwrap() 315 | } 316 | 317 | /// Finds the character index of a given position 318 | pub fn char_index_from_position(&self, cursor: SafeFloat) -> usize { 319 | let cursor = cursor.unwrap(); 320 | let minor_half_extent = self.half_extent.unwrap()[1 - self.major_axis]; 321 | let line_index = ((minor_half_extent - cursor[1 - self.major_axis]) * self.lines.len() as f32 / (minor_half_extent * 2.0)) 322 | .max(0.0) 323 | .min((self.lines.len() - 1) as f32) as usize; 324 | let glyph_positions = &self.lines[line_index].1; 325 | glyph_positions 326 | .iter() 327 | .zip(glyph_positions.iter().skip(1)) 328 | .position(|(prev, next)| (prev.unwrap()[self.major_axis] + next.unwrap()[self.major_axis]) * 0.5 > cursor[self.major_axis]) 329 | .unwrap_or(glyph_positions.len() - 1) 330 | + if line_index == 0 { 0 } else { self.lines[line_index - 1].0 } 331 | } 332 | 333 | /// Used to jump into the previous or next line 334 | pub fn advance_char_index_by_line_index(&self, char_index: usize, relative_line_index: isize) -> usize { 335 | let line_index = self.line_index_from_char_index(char_index); 336 | if relative_line_index < 0 && line_index == 0 { 337 | return 0; 338 | } else if relative_line_index > 0 && line_index == self.lines.len() - 1 { 339 | return self.lines.last().unwrap().0 - 1; 340 | } 341 | let (line_range_end, glyph_positions) = &self.lines[line_index]; 342 | let mut cursor = glyph_positions[char_index + glyph_positions.len() - *line_range_end].unwrap(); 343 | let line_minor_extent = self.half_extent.unwrap()[1 - self.major_axis] * 2.0 / self.lines.len() as f32; 344 | cursor[1 - self.major_axis] -= line_minor_extent * relative_line_index as f32; 345 | self.char_index_from_position(cursor.into()) 346 | } 347 | } 348 | 349 | /// Calculates the byte offsets of chars in an UTF8 string 350 | pub fn byte_offset_of_char_index(string: &str, char_index: usize) -> usize { 351 | string.char_indices().nth(char_index).map(|(index, _char)| index).unwrap_or(string.len()) 352 | } 353 | -------------------------------------------------------------------------------- /src/curve.rs: -------------------------------------------------------------------------------- 1 | //! Various math helper functions to work with bezier curves. 2 | //! 3 | //! All of these functions use the power basis form. 4 | 5 | use crate::{error::ERROR_MARGIN, utils::rotate_90_degree_clockwise}; 6 | use geometric_algebra::{ 7 | epga1d, 8 | polynomial::{solve_cubic, solve_linear, solve_quadratic, solve_quartic, Root}, 9 | ppga2d, ppga3d, Dual, GeometricProduct, GeometricQuotient, InnerProduct, Powf, Powi, RegressiveProduct, Signum, Zero, 10 | }; 11 | 12 | macro_rules! mat_vec_transform { 13 | (expand, $power_basis:expr, $i:expr, $at:expr) => { 14 | $power_basis[$i] * $at 15 | }; 16 | (expand, $power_basis:expr, $i:expr, $at:expr $(, $rest:expr)+) => { 17 | mat_vec_transform!(expand, $power_basis, $i, $at) + 18 | mat_vec_transform!(expand, $power_basis, $i + 1 $(, $rest)+) 19 | }; 20 | ($power_basis:expr, $($at:expr),+ $(,)?) => { 21 | mat_vec_transform!(expand, $power_basis, 0, $($at),+) 22 | }; 23 | } 24 | 25 | /// Transforms the given control points of a rational quadratic bezier curve into the power basis form. 26 | pub fn rational_quadratic_control_points_to_power_basis(control_points: &[ppga2d::Point; 3]) -> [ppga2d::Point; 3] { 27 | [ 28 | mat_vec_transform!(control_points, 1.0), 29 | mat_vec_transform!(control_points, -2.0, 2.0), 30 | mat_vec_transform!(control_points, 1.0, -2.0, 1.0), 31 | ] 32 | } 33 | 34 | /// Transforms the given control points of a rational cubic bezier curve into the power basis form. 35 | pub fn rational_cubic_control_points_to_power_basis(control_points: &[ppga2d::Point; 4]) -> [ppga2d::Point; 4] { 36 | [ 37 | mat_vec_transform!(control_points, 1.0), 38 | mat_vec_transform!(control_points, -3.0, 3.0), 39 | mat_vec_transform!(control_points, 3.0, -6.0, 3.0), 40 | mat_vec_transform!(control_points, -1.0, 3.0, -3.0, 1.0), 41 | ] 42 | } 43 | 44 | /// Reparametrizes a rational quadratic bezier curve linearly to a new parameter interval between `a` and `b`. 45 | /// 46 | /// Can be used for splitting, trimming and bloosoming. 47 | pub fn reparametrize_rational_quadratic(power_basis: &[ppga2d::Point; 3], a: f32, b: f32) -> [ppga2d::Point; 3] { 48 | [ 49 | mat_vec_transform!(power_basis, 1.0, a, a.powi(2)), 50 | mat_vec_transform!(power_basis, 0.0, b - a, -2.0 * a.powi(2) + 2.0 * a * b), 51 | mat_vec_transform!(power_basis, 0.0, 0.0, (a - b).powi(2)), 52 | ] 53 | } 54 | 55 | /// Reparametrizes a rational cubic bezier curve linearly to a new parameter interval between `a` and `b`. 56 | /// 57 | /// Can be used for splitting, trimming and bloosoming. 58 | pub fn reparametrize_rational_cubic(power_basis: &[ppga2d::Point; 4], a: f32, b: f32) -> [ppga2d::Point; 4] { 59 | [ 60 | mat_vec_transform!(power_basis, 1.0, a, a.powi(2), a.powi(3)), 61 | mat_vec_transform!( 62 | power_basis, 63 | 0.0, 64 | b - a, 65 | -2.0 * a.powi(2) + 2.0 * a * b, 66 | 3.0 * a.powi(2) * b - 3.0 * a.powi(3), 67 | ), 68 | mat_vec_transform!( 69 | power_basis, 70 | 0.0, 71 | 0.0, 72 | (a - b).powi(2), 73 | -6.0 * a.powi(2) * b + 3.0 * a * b.powi(2) + 3.0 * a.powi(3), 74 | ), 75 | mat_vec_transform!( 76 | power_basis, 77 | 0.0, 78 | 0.0, 79 | 0.0, 80 | 3.0 * a.powi(2) * b - 3.0 * a * b.powi(2) - a.powi(3) + b.powi(3), 81 | ), 82 | ] 83 | } 84 | 85 | /// Calculates the point at parameter t of a rational quadratic bezier curve. 86 | pub fn rational_quadratic_point(power_basis: &[ppga2d::Point; 3], t: f32) -> ppga2d::Point { 87 | mat_vec_transform!(power_basis, 1.0, t, t.powi(2)) 88 | } 89 | 90 | /// Calculates the first order derivative at parameter t of a rational quadratic bezier curve. 91 | pub fn rational_quadratic_first_order_derivative(power_basis: &[ppga2d::Point; 3], t: f32) -> ppga2d::Plane { 92 | let p = mat_vec_transform!(power_basis, 1.0, t, t.powi(2)); 93 | let d1 = mat_vec_transform!(power_basis, 0.0, 1.0, 2.0 * t); 94 | p.regressive_product(d1) 95 | } 96 | 97 | /// Calculates the second order derivative at parameter t of a rational quadratic bezier curve. 98 | pub fn rational_quadratic_second_order_derivative(power_basis: &[ppga2d::Point; 3], t: f32) -> ppga2d::Plane { 99 | let p = mat_vec_transform!(power_basis, 1.0, t, t.powi(2)); 100 | let d2 = mat_vec_transform!(power_basis, 0.0, 0.0, 2.0); 101 | p.regressive_product(d2) 102 | } 103 | 104 | /// Calculates the point at parameter t of a rational cubic bezier curve. 105 | pub fn rational_cubic_point(power_basis: &[ppga2d::Point; 4], t: f32) -> ppga2d::Point { 106 | mat_vec_transform!(power_basis, 1.0, t, t.powi(2), t.powi(3)) 107 | } 108 | 109 | /// Calculates the first order derivative at parameter t of a rational cubic bezier curve. 110 | pub fn rational_cubic_first_order_derivative(power_basis: &[ppga2d::Point; 4], t: f32) -> ppga2d::Plane { 111 | let p = mat_vec_transform!(power_basis, 1.0, t, t.powi(2), t.powi(3)); 112 | let d1 = mat_vec_transform!(power_basis, 0.0, 1.0, 2.0 * t, 3.0 * t.powi(2)); 113 | p.regressive_product(d1) 114 | } 115 | 116 | /// Calculates the second order derivative at parameter t of a rational cubic bezier curve. 117 | pub fn rational_cubic_second_order_derivative(power_basis: &[ppga2d::Point; 4], t: f32) -> ppga2d::Plane { 118 | let p = mat_vec_transform!(power_basis, 1.0, t, t.powi(2), t.powi(3)); 119 | let d2 = mat_vec_transform!(power_basis, 0.0, 0.0, 2.0, 6.0 * t); 120 | p.regressive_product(d2) 121 | } 122 | 123 | /// Calculates the third order derivative at parameter t of a rational cubic bezier curve. 124 | pub fn rational_cubic_third_order_derivative(power_basis: &[ppga2d::Point; 4], t: f32) -> ppga2d::Plane { 125 | let p = mat_vec_transform!(power_basis, 1.0, t, t.powi(2), t.powi(3)); 126 | let d1 = mat_vec_transform!(power_basis, 0.0, 1.0, 2.0 * t, 3.0 * t.powi(2)); 127 | let d2 = mat_vec_transform!(power_basis, 0.0, 0.0, 2.0, 6.0 * t); 128 | let d3 = mat_vec_transform!(power_basis, 0.0, 0.0, 0.0, 6.0); 129 | p.regressive_product(d3) + d1.regressive_product(d2) 130 | } 131 | 132 | /// Calculates the coefficients of the inflection point polynomial for an integral or rational cubic bezier curve. 133 | pub fn inflection_point_polynomial_coefficients(power_basis: &[ppga2d::Point; 4], integral: bool) -> [f32; 4] { 134 | let mut ippc = ppga3d::Rotor::zero(); 135 | for j in (integral as usize)..4 { 136 | let mut iter = (0..4).filter(|i| *i != j).map(|i| power_basis[i]); 137 | ippc[j] = (iter.next().unwrap()) 138 | .regressive_product(iter.next().unwrap()) 139 | .regressive_product(iter.next().unwrap()) 140 | * (j as isize % 2 * 2 - 1) as f32; 141 | } 142 | ippc = ippc.signum(); 143 | ippc.into() 144 | } 145 | 146 | /// Finds the roots of the inflection point polynomial for an integral cubic bezier curve. 147 | /// 148 | /// It also returns the discriminant which can be used for classification of the curve. 149 | /// In case there is a loop (discriminant < 0.0) and the `loop_self_intersection` flag is [true], 150 | /// two different roots will be located at the self intersection point. 151 | pub fn integral_inflection_points(ippc: &[f32; 4], loop_self_intersection: bool) -> (f32, [Root; 3]) { 152 | let discriminant = 3.0 * ippc[2].powi(2) - 4.0 * ippc[1] * ippc[3]; 153 | if ippc[1].abs() <= ERROR_MARGIN { 154 | if ippc[2].abs() <= ERROR_MARGIN { 155 | ( 156 | -1.0, 157 | [Root::new([-1.0, 0.0], 1.0), Root::new([1.0, 0.0], 0.0), Root::new([1.0, 0.0], 0.0)], 158 | ) 159 | } else { 160 | ( 161 | 1.0, 162 | [ 163 | Root::new([ippc[3], 0.0], 3.0 * ippc[2]), 164 | Root::new([1.0, 0.0], 0.0), 165 | Root::new([1.0, 0.0], 0.0), 166 | ], 167 | ) 168 | } 169 | } else { 170 | let d = (discriminant 171 | * if discriminant < 0.0 { 172 | if loop_self_intersection { 173 | -1.0 174 | } else { 175 | 0.0 176 | } 177 | } else { 178 | 1.0f32 / 3.0f32 179 | }) 180 | .sqrt(); 181 | ( 182 | discriminant, 183 | [ 184 | Root::new([ippc[2] + d, 0.0], 2.0 * ippc[1]), 185 | Root::new([ippc[2] - d, 0.0], 2.0 * ippc[1]), 186 | Root::new([1.0, 0.0], 0.0), 187 | ], 188 | ) 189 | } 190 | } 191 | 192 | /// Finds the roots of the inflection point polynomial for a rational cubic bezier curve. 193 | /// 194 | /// It also returns the discriminant which can be used for classification of the curve. 195 | /// In case there is a loop (discriminant < 0.0) and the `loop_self_intersection` flag is [true], 196 | /// two different roots will be located at the self intersection point. 197 | pub fn rational_inflection_points(ippc: &[f32; 4], loop_self_intersection: bool) -> (f32, [Root; 3]) { 198 | if ippc[0].abs() <= ERROR_MARGIN { 199 | return integral_inflection_points(ippc, loop_self_intersection); 200 | } 201 | let (discriminant, roots, real_root) = solve_cubic([ippc[3] * -1.0, ippc[2] * 3.0, ippc[1] * -3.0, ippc[0]], ERROR_MARGIN); 202 | let mut roots = [roots[0], roots[1], roots[2]]; 203 | if !loop_self_intersection { 204 | return (discriminant, roots); 205 | } 206 | let (discriminant, hessian_roots) = solve_quadratic( 207 | [ 208 | ippc[1] * ippc[3] - ippc[2] * ippc[2], 209 | ippc[1] * ippc[2] - ippc[0] * ippc[3], 210 | ippc[0] * ippc[2] - ippc[1] * ippc[1], 211 | ], 212 | ERROR_MARGIN, 213 | ); 214 | if discriminant > 0.0 { 215 | roots[2] = roots[real_root]; 216 | match hessian_roots.len() { 217 | 2 => roots[0..2].clone_from_slice(hessian_roots.as_slice()), 218 | 1 => { 219 | roots[0] = hessian_roots[0]; 220 | roots[1] = Root::new([1.0, 0.0], 0.0); 221 | } 222 | _ => {} 223 | } 224 | } 225 | (-discriminant, roots) 226 | } 227 | 228 | macro_rules! interpolate_normal { 229 | ($start_tangent:expr, $end_tangent:expr, $angle_step:expr, $normal:ident, $solutions:expr $(,)?) => {{ 230 | let polar_start = epga1d::ComplexNumber::new($start_tangent[1], $start_tangent[2]); 231 | let polar_end = epga1d::ComplexNumber::new($end_tangent[1], $end_tangent[2]); 232 | let polar_range = polar_end.geometric_quotient(polar_start); 233 | let steps = ((polar_range.arg() / $angle_step).abs() + 0.5) as usize; 234 | let polar_step = polar_range.powf(1.0 / steps as f32); 235 | (1..steps) 236 | .map(|i| { 237 | let interpolated = polar_start.geometric_product(polar_step.powi(i as isize)); 238 | let $normal = ppga2d::Plane::new(0.0, interpolated.real(), interpolated.imaginary()); 239 | for solution in $solutions { 240 | if solution.denominator == 0.0 { 241 | continue; 242 | } 243 | let parameter: f32 = solution.numerator.real() / solution.denominator; 244 | if (0.0..=1.0).contains(¶meter) { 245 | return parameter; 246 | } 247 | } 248 | 0.0 249 | }) 250 | .collect::>() 251 | }}; 252 | } 253 | 254 | macro_rules! cubic_uniform_tangent_angle { 255 | ($power_basis:expr, $angle_step:expr, $discriminant_and_roots:expr, $trimmed_power_basis:ident, 256 | $per_interval:expr, $normal:ident, $solutions:expr $(,)?) => {{ 257 | let mut split_parameters = $discriminant_and_roots 258 | .1 259 | .iter() 260 | .filter(|root| root.denominator != 0.0) 261 | .map(|root| root.numerator.real() / root.denominator) 262 | .filter(|parameter| (0.0..=1.0).contains(parameter)) 263 | .collect::>(); 264 | split_parameters.sort_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| unreachable!())); 265 | { 266 | let mut i = 1; 267 | while i < split_parameters.len() { 268 | if split_parameters[i] - split_parameters[i - 1] < ERROR_MARGIN { 269 | split_parameters.remove(i); 270 | } else { 271 | i += 1; 272 | } 273 | } 274 | } 275 | let mut previous_split = 0.0; 276 | let mut intervals = Vec::with_capacity(split_parameters.len() + 1); 277 | for split_parameter in &split_parameters { 278 | if $discriminant_and_roots.0.abs() < ERROR_MARGIN { 279 | intervals.push((previous_split, *split_parameter - f32::EPSILON)); 280 | previous_split = *split_parameter + f32::EPSILON; 281 | } else { 282 | intervals.push((previous_split, *split_parameter)); 283 | previous_split = *split_parameter; 284 | } 285 | } 286 | intervals.push((previous_split, 1.0)); 287 | let mut parameters = Vec::new(); 288 | for (a, b) in intervals.iter() { 289 | let $trimmed_power_basis = reparametrize_rational_cubic($power_basis, *a, *b); 290 | let start_tangent = rational_cubic_first_order_derivative($power_basis, *a).signum(); 291 | let end_tangent = rational_cubic_first_order_derivative($power_basis, *b).signum(); 292 | $per_interval 293 | let mut interval_parameters = interpolate_normal!(start_tangent, end_tangent, $angle_step, $normal, $solutions) 294 | .iter() 295 | .map(|t| *a + (*b - *a) * t) 296 | .collect::>(); 297 | interval_parameters.sort_by(|a, b| a.partial_cmp(b).unwrap_or_else(|| unreachable!())); 298 | parameters.append(&mut interval_parameters); 299 | parameters.push(*b); 300 | } 301 | parameters 302 | }}; 303 | } 304 | 305 | /// Returns parameters of an integral quadratic bezier curve, distributed so that they have uniform tangent angles of the given angle step size 306 | pub fn integral_quadratic_uniform_tangent_angle( 307 | power_basis: &[ppga2d::Point; 3], 308 | start_tangent: ppga2d::Plane, 309 | end_tangent: ppga2d::Plane, 310 | angle_step: f32, 311 | ) -> Vec { 312 | let planes = [power_basis[1].dual(), power_basis[2].dual() * 2.0]; 313 | let mut parameters = interpolate_normal!( 314 | start_tangent, 315 | end_tangent, 316 | angle_step, 317 | normal, 318 | solve_linear([normal.inner_product(planes[0]), normal.inner_product(planes[1])], ERROR_MARGIN).1, 319 | ); 320 | parameters.push(1.0); 321 | parameters 322 | } 323 | 324 | /// Returns parameters of an integral cubic bezier curve, distributed so that they have uniform tangent angles of the given angle step size 325 | pub fn integral_cubic_uniform_tangent_angle(power_basis: &[ppga2d::Point; 4], angle_step: f32) -> Vec { 326 | let ippc = inflection_point_polynomial_coefficients(power_basis, true); 327 | let discriminant_and_roots = integral_inflection_points(&ippc, false); 328 | let mut planes; 329 | cubic_uniform_tangent_angle!( 330 | power_basis, 331 | angle_step, 332 | discriminant_and_roots, 333 | trimmed_power_basis, 334 | { 335 | planes = [ 336 | trimmed_power_basis[1].dual(), 337 | trimmed_power_basis[2].dual() * 2.0, 338 | trimmed_power_basis[3].dual() * 3.0, 339 | ]; 340 | }, 341 | normal, 342 | solve_quadratic( 343 | [ 344 | normal.inner_product(planes[0]), 345 | normal.inner_product(planes[1]), 346 | normal.inner_product(planes[2]), 347 | ], 348 | ERROR_MARGIN 349 | ) 350 | .1 351 | ) 352 | } 353 | 354 | /// Returns parameters of an rational quadratic bezier curve, distributed so that they have uniform tangent angles of the given angle step size 355 | pub fn rational_quadratic_uniform_tangent_angle( 356 | power_basis: &[ppga2d::Point; 3], 357 | start_tangent: ppga2d::Plane, 358 | end_tangent: ppga2d::Plane, 359 | angle_step: f32, 360 | ) -> Vec { 361 | let planes = [ 362 | power_basis[1].regressive_product(power_basis[0]), 363 | power_basis[2].regressive_product(power_basis[0]) * 2.0, 364 | power_basis[2].regressive_product(power_basis[1]), 365 | ]; 366 | let mut parameters = interpolate_normal!(start_tangent, end_tangent, angle_step, normal, { 367 | let normal = rotate_90_degree_clockwise(normal); 368 | solve_quadratic( 369 | [ 370 | normal.inner_product(planes[0]), 371 | normal.inner_product(planes[1]), 372 | normal.inner_product(planes[2]), 373 | ], 374 | ERROR_MARGIN, 375 | ) 376 | .1 377 | }); 378 | parameters.push(1.0); 379 | parameters 380 | } 381 | 382 | /// Returns parameters of an rational cubic bezier curve, distributed so that they have uniform tangent angles of the given angle step size 383 | pub fn rational_cubic_uniform_tangent_angle(power_basis: &[ppga2d::Point; 4], angle_step: f32) -> Vec { 384 | let ippc = inflection_point_polynomial_coefficients(power_basis, false); 385 | let discriminant_and_roots = rational_inflection_points(&ippc, false); 386 | let mut planes; 387 | cubic_uniform_tangent_angle!( 388 | power_basis, 389 | angle_step, 390 | discriminant_and_roots, 391 | trimmed_power_basis, 392 | { 393 | planes = [ 394 | trimmed_power_basis[1].regressive_product(trimmed_power_basis[0]), 395 | trimmed_power_basis[2].regressive_product(trimmed_power_basis[0]) * 2.0, 396 | trimmed_power_basis[2].regressive_product(trimmed_power_basis[1]) 397 | + trimmed_power_basis[3].regressive_product(trimmed_power_basis[0]) * 3.0, 398 | trimmed_power_basis[3].regressive_product(trimmed_power_basis[1]) * 2.0, 399 | trimmed_power_basis[3].regressive_product(trimmed_power_basis[2]), 400 | ]; 401 | }, 402 | normal, 403 | { 404 | let normal = rotate_90_degree_clockwise(normal); 405 | solve_quartic( 406 | [ 407 | normal.inner_product(planes[0]), 408 | normal.inner_product(planes[1]), 409 | normal.inner_product(planes[2]), 410 | normal.inner_product(planes[3]), 411 | normal.inner_product(planes[4]), 412 | ], 413 | ERROR_MARGIN, 414 | ) 415 | .1 416 | } 417 | ) 418 | } 419 | -------------------------------------------------------------------------------- /src/fill.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | curve::{ 3 | inflection_point_polynomial_coefficients, integral_inflection_points, rational_cubic_control_points_to_power_basis, 4 | rational_cubic_first_order_derivative, rational_inflection_points, 5 | }, 6 | error::{Error, ERROR_MARGIN}, 7 | path::{Path, SegmentType}, 8 | safe_float::SafeFloat, 9 | utils::{point_to_vec, vec_to_point, weighted_vec_to_point}, 10 | vertex::{triangle_fan_to_strip, Vertex0, Vertex2f, Vertex3f, Vertex4f}, 11 | }; 12 | use geometric_algebra::{polynomial::Root, ppga2d, ppga3d, InnerProduct, RegressiveProduct, SquaredMagnitude, Zero}; 13 | 14 | fn find_double_point_issue(discriminant: f32, roots: &[Root; 3]) -> Option { 15 | if discriminant < 0.0 { 16 | let mut result = -1.0; 17 | let mut inside = 0; 18 | for root in roots { 19 | if root.denominator != 0.0 { 20 | let parameter = root.numerator.real() / root.denominator; 21 | if 0.0 < parameter && parameter < 1.0 { 22 | result = parameter; 23 | inside += 1; 24 | } 25 | } 26 | } 27 | if inside == 1 { 28 | return Some(result); 29 | } 30 | } 31 | None 32 | } 33 | 34 | fn weight_derivatives(weights: &mut [[f32; 4]; 4], column: usize, roots: [Root; 3]) { 35 | let power_basis = [ 36 | roots[0].numerator.real() * roots[1].numerator.real() * roots[2].numerator.real(), 37 | -roots[0].denominator * roots[1].numerator.real() * roots[2].numerator.real() 38 | - roots[0].numerator.real() * roots[1].denominator * roots[2].numerator.real() 39 | - roots[0].numerator.real() * roots[1].numerator.real() * roots[2].denominator, 40 | roots[0].numerator.real() * roots[1].denominator * roots[2].denominator 41 | + roots[0].denominator * roots[1].numerator.real() * roots[2].denominator 42 | + roots[0].denominator * roots[1].denominator * roots[2].numerator.real(), 43 | -roots[0].denominator * roots[1].denominator * roots[2].denominator, 44 | ]; 45 | weights[0][column] = power_basis[0]; 46 | weights[1][column] = power_basis[0] + power_basis[1] * 1.0 / 3.0; 47 | weights[2][column] = power_basis[0] + power_basis[1] * 2.0 / 3.0 + power_basis[2] * 1.0 / 3.0; 48 | weights[3][column] = power_basis[0] + power_basis[1] + power_basis[2] + power_basis[3]; 49 | } 50 | 51 | fn weights(discriminant: f32, roots: &[Root; 3]) -> [[f32; 4]; 4] { 52 | let mut weights = [[0.0; 4]; 4]; 53 | if discriminant == 0.0 { 54 | weight_derivatives(&mut weights, 0, [roots[0], roots[0], roots[2]]); 55 | weight_derivatives(&mut weights, 1, [roots[0], roots[0], roots[0]]); 56 | weight_derivatives(&mut weights, 2, [roots[0], roots[0], roots[0]]); 57 | } else if discriminant < 0.0 { 58 | weight_derivatives(&mut weights, 0, [roots[0], roots[1], roots[2]]); 59 | weight_derivatives(&mut weights, 1, [roots[0], roots[0], roots[1]]); 60 | weight_derivatives(&mut weights, 2, [roots[1], roots[1], roots[0]]); 61 | } else { 62 | weight_derivatives(&mut weights, 0, [roots[0], roots[1], roots[2]]); 63 | weight_derivatives(&mut weights, 1, [roots[0], roots[0], roots[0]]); 64 | weight_derivatives(&mut weights, 2, [roots[1], roots[1], roots[1]]); 65 | } 66 | weight_derivatives(&mut weights, 3, [roots[2], roots[2], roots[2]]); 67 | weights 68 | } 69 | 70 | fn weight_planes(control_points: &[ppga2d::Point; 4], weights: &[[f32; 4]; 4]) -> [ppga2d::Plane; 4] { 71 | let mut planes = [ppga2d::Plane::zero(); 4]; 72 | let mut points = [ppga3d::Point::zero(); 4]; 73 | for (i, plane_2d) in planes.iter_mut().enumerate() { 74 | for (j, control_point) in control_points.iter().enumerate() { 75 | points[j] = ppga3d::Point::from([control_point[0], control_point[1], control_point[2], weights[j][i]]); 76 | } 77 | let mut plane_3d = points[0].regressive_product(points[1]).regressive_product(points[2]); 78 | if plane_3d.squared_magnitude() < ERROR_MARGIN { 79 | plane_3d = points[0].regressive_product(points[1]).regressive_product(points[3]); 80 | } 81 | plane_3d = plane_3d * (1.0 / -plane_3d[3]); 82 | *plane_2d = ppga2d::Plane::new(plane_3d[0], plane_3d[1], plane_3d[2]); 83 | } 84 | planes 85 | } 86 | 87 | fn implicit_curve_value(weights: ppga3d::Point) -> f32 { 88 | weights[0].powi(3) - weights[1] * weights[2] * weights[3] 89 | } 90 | 91 | fn implicit_curve_gradient(planes: &[ppga2d::Plane; 4], weights: &[f32; 4]) -> ppga2d::Plane { 92 | planes[0] * (3.0 * weights[0] * weights[0]) 93 | - planes[1] * (weights[2] * weights[3]) 94 | - planes[2] * (weights[1] * weights[3]) 95 | - planes[3] * (weights[1] * weights[2]) 96 | } 97 | 98 | fn normalize_implicit_curve_side( 99 | planes: &mut [ppga2d::Plane; 4], 100 | weights: &mut [[f32; 4]; 4], 101 | power_basis: &[ppga2d::Point; 4], 102 | gradient: ppga2d::Plane, 103 | ) { 104 | let tangent = rational_cubic_first_order_derivative(power_basis, 0.0); 105 | if tangent.inner_product(gradient) > 0.0 { 106 | for plane in planes { 107 | *plane = -*plane; 108 | } 109 | for row in weights { 110 | row[0] *= -1.0; 111 | row[1] *= -1.0; 112 | } 113 | } 114 | } 115 | 116 | macro_rules! emit_cubic_curve_triangle { 117 | ($triangles:expr, $signed_triangle_areas:expr, $control_points:expr, $weights:expr, $v:ident, $w:ident, $emit_vertex:expr, $triangle_index:expr) => { 118 | let mut triangle = Vec::new(); 119 | for vertex_index in (0..4).filter(|i| *i != $triangle_index) { 120 | let $v = point_to_vec($control_points[vertex_index]); 121 | let $w = $weights[vertex_index]; 122 | triangle.push($emit_vertex); 123 | } 124 | let signed_triangle_area = $signed_triangle_areas[$triangle_index]; 125 | if signed_triangle_area.abs() > ERROR_MARGIN { 126 | if signed_triangle_area < 0.0 { 127 | triangle.reverse(); 128 | } 129 | $triangles.append(&mut triangle); 130 | } 131 | }; 132 | } 133 | 134 | macro_rules! triangulate_cubic_curve_quadrilateral { 135 | ($fill_solid_vertices:expr, $cubic_vertices:expr, 136 | $control_points:expr, $weights:expr, $v:ident, $w:ident, $emit_vertex:expr) => {{ 137 | for (weights, control_point) in $weights.iter_mut().zip($control_points.iter()) { 138 | *weights *= (1.0 / control_point[0]); 139 | } 140 | let mut triangles = Vec::new(); 141 | let signed_triangle_areas: Vec = (0..4) 142 | .map(|i| { 143 | // $control_points[j].signum() 144 | let points: Vec<_> = (0..4).filter(|j| i != *j).map(|j| $control_points[j]).collect(); 145 | points[0].regressive_product(points[1]).regressive_product(points[2]) 146 | }) 147 | .collect(); 148 | let triangle_area_sum = 149 | signed_triangle_areas[0].abs() + signed_triangle_areas[1].abs() + signed_triangle_areas[2].abs() + signed_triangle_areas[3].abs(); 150 | let mut enclosing_triangle = None; 151 | for (triangle_index, signed_triangle_area) in signed_triangle_areas.iter().enumerate() { 152 | let equilibrium: f32 = 0.5 * triangle_area_sum; 153 | if (equilibrium - signed_triangle_area.abs()).abs() <= ERROR_MARGIN { 154 | enclosing_triangle = if enclosing_triangle.is_none() { Some(triangle_index) } else { None }; 155 | } 156 | } 157 | if let Some(enclosing_triangle) = enclosing_triangle { 158 | emit_cubic_curve_triangle!( 159 | triangles, 160 | signed_triangle_areas, 161 | $control_points, 162 | $weights, 163 | $v, 164 | $w, 165 | $emit_vertex, 166 | enclosing_triangle 167 | ); 168 | } else { 169 | let mut opposite_triangle = 0; 170 | for j in 1..4 { 171 | let side_of_a = signed_triangle_areas[j]; 172 | let side_of_d = signed_triangle_areas[0] * if j == 2 { -1.0 } else { 1.0 }; 173 | if side_of_a * side_of_d < 0.0 { 174 | assert_eq!(opposite_triangle, 0); 175 | opposite_triangle = j; 176 | } 177 | } 178 | assert_ne!(opposite_triangle, 0); 179 | emit_cubic_curve_triangle!(triangles, signed_triangle_areas, $control_points, $weights, $v, $w, $emit_vertex, 0); 180 | emit_cubic_curve_triangle!( 181 | triangles, 182 | signed_triangle_areas, 183 | $control_points, 184 | $weights, 185 | $v, 186 | $w, 187 | $emit_vertex, 188 | opposite_triangle 189 | ); 190 | } 191 | let mut additional_vertices = 0; 192 | for i in 1..3 { 193 | if enclosing_triangle != Some(i) && implicit_curve_value($weights[i]) < 0.0 { 194 | $fill_solid_vertices.push(point_to_vec($control_points[i])); 195 | additional_vertices += 1; 196 | } 197 | } 198 | if additional_vertices == 2 && signed_triangle_areas[0] * signed_triangle_areas[1] < 0.0 { 199 | let length = $fill_solid_vertices.len(); 200 | $fill_solid_vertices.swap(length - 2, length - 1); 201 | } 202 | $cubic_vertices.append(&mut triangles); 203 | }}; 204 | } 205 | 206 | macro_rules! split_curve_at { 207 | ($algebra:ident, $control_points:expr, $param:expr) => {{ 208 | let p10 = $control_points[0] * (1.0 - $param) + $control_points[1] * $param; 209 | let p11 = $control_points[1] * (1.0 - $param) + $control_points[2] * $param; 210 | let p12 = $control_points[2] * (1.0 - $param) + $control_points[3] * $param; 211 | let p20 = p10 * (1.0 - $param) + p11 * $param; 212 | let p21 = p11 * (1.0 - $param) + p12 * $param; 213 | let p30 = p20 * (1.0 - $param) + p21 * $param; 214 | ([$control_points[0], p10, p20, p30], [p30, p21, p12, $control_points[3]]) 215 | }}; 216 | } 217 | 218 | macro_rules! emit_cubic_curve { 219 | ($proto_hull:expr, $fill_solid_vertices:expr, $cubic_vertices:expr, 220 | $control_points:expr, $c:expr, $discriminant:expr, $roots:expr, 221 | $v:ident, $w:ident, $emit_vertex:expr) => {{ 222 | let mut weights = weights($discriminant, &$roots); 223 | let mut planes = weight_planes(&$control_points, &weights); 224 | let gradient = implicit_curve_gradient(&planes, &weights[0]); 225 | normalize_implicit_curve_side(&mut planes, &mut weights, &$c, gradient); 226 | let mut weights = [ 227 | ppga3d::Point::from(<[f32; 4] as Into<[f32; 4]>>::into(weights[0])), 228 | ppga3d::Point::from(<[f32; 4] as Into<[f32; 4]>>::into(weights[1])), 229 | ppga3d::Point::from(<[f32; 4] as Into<[f32; 4]>>::into(weights[2])), 230 | ppga3d::Point::from(<[f32; 4] as Into<[f32; 4]>>::into(weights[3])), 231 | ]; 232 | if let Some(param) = find_double_point_issue($discriminant, &$roots) { 233 | let (control_points_a, control_points_b) = split_curve_at!(ppga2d, &$control_points, param); 234 | let (mut weights_a, mut weights_b) = split_curve_at!(ppga3d, &weights, param); 235 | triangulate_cubic_curve_quadrilateral!($fill_solid_vertices, $cubic_vertices, &control_points_a, weights_a, $v, $w, $emit_vertex); 236 | $fill_solid_vertices.push(point_to_vec(control_points_b[0])); 237 | for weights in &mut weights_b { 238 | weights[0] *= -1.0; 239 | weights[1] *= -1.0; 240 | } 241 | triangulate_cubic_curve_quadrilateral!($fill_solid_vertices, $cubic_vertices, &control_points_b, weights_b, $v, $w, $emit_vertex); 242 | } else { 243 | triangulate_cubic_curve_quadrilateral!($fill_solid_vertices, $cubic_vertices, $control_points, weights, $v, $w, $emit_vertex); 244 | } 245 | $proto_hull.push(point_to_vec($control_points[1]).into()); 246 | $proto_hull.push(point_to_vec($control_points[2]).into()); 247 | $proto_hull.push(point_to_vec($control_points[3]).into()); 248 | $fill_solid_vertices.push(point_to_vec($control_points[3])); 249 | }}; 250 | } 251 | 252 | #[derive(Default)] 253 | pub struct FillBuilder { 254 | pub solid_indices: Vec, 255 | pub solid_vertices: Vec, 256 | pub integral_quadratic_vertices: Vec, 257 | pub integral_cubic_vertices: Vec, 258 | pub rational_quadratic_vertices: Vec, 259 | pub rational_cubic_vertices: Vec, 260 | } 261 | 262 | impl FillBuilder { 263 | pub fn add_path(&mut self, proto_hull: &mut Vec>, path: &Path) -> Result<(), Error> { 264 | let mut path_solid_vertices: Vec = Vec::with_capacity( 265 | 1 + path.line_segments.len() 266 | + path.integral_quadratic_curve_segments.len() 267 | + path.integral_cubic_curve_segments.len() * 5 268 | + path.rational_quadratic_curve_segments.len() 269 | + path.rational_cubic_curve_segments.len() * 5, 270 | ); 271 | path_solid_vertices.push(path.start.unwrap()); 272 | proto_hull.push(path.start); 273 | let mut line_segment_iter = path.line_segments.iter(); 274 | let mut integral_quadratic_curve_segment_iter = path.integral_quadratic_curve_segments.iter(); 275 | let mut integral_cubic_curve_segment_iter = path.integral_cubic_curve_segments.iter(); 276 | let mut rational_quadratic_curve_segment_iter = path.rational_quadratic_curve_segments.iter(); 277 | let mut rational_cubic_curve_segment_iter = path.rational_cubic_curve_segments.iter(); 278 | for segment_type in &path.segment_types { 279 | match segment_type { 280 | SegmentType::Line => { 281 | let segment = line_segment_iter.next().unwrap(); 282 | proto_hull.push(segment.control_points[0]); 283 | path_solid_vertices.push(segment.control_points[0].unwrap()); 284 | } 285 | SegmentType::IntegralQuadraticCurve => { 286 | let segment = integral_quadratic_curve_segment_iter.next().unwrap(); 287 | self.integral_quadratic_vertices 288 | .push(Vertex2f(segment.control_points[1].unwrap(), [1.0, 1.0])); 289 | self.integral_quadratic_vertices 290 | .push(Vertex2f(segment.control_points[0].unwrap(), [0.5, 0.0])); 291 | self.integral_quadratic_vertices 292 | .push(Vertex2f(*path_solid_vertices.last().unwrap(), [0.0, 0.0])); 293 | proto_hull.push(segment.control_points[0]); 294 | proto_hull.push(segment.control_points[1]); 295 | path_solid_vertices.push(segment.control_points[1].unwrap()); 296 | } 297 | SegmentType::IntegralCubicCurve => { 298 | let segment = integral_cubic_curve_segment_iter.next().unwrap(); 299 | let control_points = [ 300 | vec_to_point(*path_solid_vertices.last().unwrap()), 301 | vec_to_point(segment.control_points[0].unwrap()), 302 | vec_to_point(segment.control_points[1].unwrap()), 303 | vec_to_point(segment.control_points[2].unwrap()), 304 | ]; 305 | let power_basis = rational_cubic_control_points_to_power_basis(&control_points); 306 | let ippc = inflection_point_polynomial_coefficients(&power_basis, true); 307 | let (discriminant, roots) = integral_inflection_points(&ippc, true); 308 | emit_cubic_curve!( 309 | proto_hull, 310 | path_solid_vertices, 311 | self.integral_cubic_vertices, 312 | control_points, 313 | power_basis, 314 | discriminant, 315 | roots, 316 | v, 317 | w, 318 | Vertex3f(v, [w[0], w[1], w[2]]) 319 | ); 320 | } 321 | SegmentType::RationalQuadraticCurve => { 322 | let segment = rational_quadratic_curve_segment_iter.next().unwrap(); 323 | let weight = 1.0 / segment.weight.unwrap(); 324 | self.rational_quadratic_vertices 325 | .push(Vertex3f(segment.control_points[1].unwrap(), [1.0, 1.0, 1.0])); 326 | self.rational_quadratic_vertices 327 | .push(Vertex3f(segment.control_points[0].unwrap(), [0.5 * weight, 0.0, weight])); 328 | self.rational_quadratic_vertices 329 | .push(Vertex3f(*path_solid_vertices.last().unwrap(), [0.0, 0.0, 1.0])); 330 | proto_hull.push(segment.control_points[0]); 331 | proto_hull.push(segment.control_points[1]); 332 | path_solid_vertices.push(segment.control_points[1].unwrap()); 333 | } 334 | SegmentType::RationalCubicCurve => { 335 | let segment = rational_cubic_curve_segment_iter.next().unwrap(); 336 | let weights = segment.weights.unwrap(); 337 | let control_points = [ 338 | weighted_vec_to_point(weights[0], *path_solid_vertices.last().unwrap()), 339 | weighted_vec_to_point(weights[1], segment.control_points[0].unwrap()), 340 | weighted_vec_to_point(weights[2], segment.control_points[1].unwrap()), 341 | weighted_vec_to_point(weights[3], segment.control_points[2].unwrap()), 342 | ]; 343 | let power_basis = rational_cubic_control_points_to_power_basis(&control_points); 344 | let ippc = inflection_point_polynomial_coefficients(&power_basis, false); 345 | let (discriminant, roots) = rational_inflection_points(&ippc, true); 346 | emit_cubic_curve!( 347 | proto_hull, 348 | path_solid_vertices, 349 | self.rational_cubic_vertices, 350 | control_points, 351 | power_basis, 352 | discriminant, 353 | roots, 354 | v, 355 | w, 356 | Vertex4f(v, w.into()) 357 | ); 358 | } 359 | } 360 | } 361 | let start_index = self.solid_vertices.len(); 362 | self.solid_vertices.append(&mut triangle_fan_to_strip(path_solid_vertices)); 363 | let mut indices: Vec = (start_index as u16..(self.solid_vertices.len() + 1) as u16).collect(); 364 | *indices.iter_mut().last().unwrap() = (-1isize) as u16; 365 | self.solid_indices.append(&mut indices); 366 | Ok(()) 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /src/stroke.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | curve::{ 3 | integral_cubic_uniform_tangent_angle, integral_quadratic_uniform_tangent_angle, rational_cubic_control_points_to_power_basis, 4 | rational_cubic_first_order_derivative, rational_cubic_point, rational_cubic_uniform_tangent_angle, 5 | rational_quadratic_control_points_to_power_basis, rational_quadratic_first_order_derivative, rational_quadratic_point, 6 | rational_quadratic_uniform_tangent_angle, 7 | }, 8 | error::{Error, ERROR_MARGIN}, 9 | path::{CurveApproximation, Path, SegmentType, StrokeOptions}, 10 | safe_float::SafeFloat, 11 | utils::{line_line_intersection, point_to_vec, rotate_90_degree_clockwise, vec_to_point, weighted_vec_to_point}, 12 | vertex::{Vertex2f1i, Vertex3f1i}, 13 | }; 14 | use geometric_algebra::{ 15 | ppga2d, Dual, GeometricProduct, GeometricQuotient, InnerProduct, Magnitude, OuterProduct, RegressiveProduct, Signum, SquaredMagnitude, Zero, 16 | }; 17 | 18 | fn offset_control_point(control_point: ppga2d::Point, tangent: ppga2d::Plane, offset: f32) -> ppga2d::Point { 19 | let mut direction = tangent.dual(); 20 | direction[0] = 0.0; 21 | control_point + direction * offset 22 | } 23 | 24 | fn emit_stroke_vertex(path_line_vertices: &mut Vec, path_index: usize, offset_along_path: f32, vertex: ppga2d::Point, side: f32) { 25 | path_line_vertices.push(Vertex2f1i(point_to_vec(vertex), [side, offset_along_path], path_index as u32)); 26 | } 27 | 28 | fn emit_stroke_vertices( 29 | builder: &mut StrokeBuilder, 30 | stroke_options: &StrokeOptions, 31 | path_index: usize, 32 | length_accumulator: f32, 33 | point: ppga2d::Point, 34 | tangent: ppga2d::Plane, 35 | ) { 36 | let offset_along_path = length_accumulator / stroke_options.width.unwrap(); 37 | emit_stroke_vertex( 38 | &mut builder.path_line_vertices, 39 | path_index, 40 | offset_along_path, 41 | offset_control_point(point, tangent, (stroke_options.offset.unwrap() - 0.5) * stroke_options.width.unwrap()), 42 | -0.5, 43 | ); 44 | emit_stroke_vertex( 45 | &mut builder.path_line_vertices, 46 | path_index, 47 | offset_along_path, 48 | offset_control_point(point, tangent, (stroke_options.offset.unwrap() + 0.5) * stroke_options.width.unwrap()), 49 | 0.5, 50 | ); 51 | } 52 | 53 | fn emit_stroke_join( 54 | builder: &mut StrokeBuilder, 55 | proto_hull: &mut Vec>, 56 | stroke_options: &StrokeOptions, 57 | length_accumulator: &mut f32, 58 | control_point: ppga2d::Point, 59 | previous_tangent: ppga2d::Plane, 60 | next_tangent: ppga2d::Plane, 61 | ) { 62 | let tangets_dot_product = previous_tangent.inner_product(next_tangent); 63 | if (tangets_dot_product - 1.0).abs() <= ERROR_MARGIN { 64 | return; 65 | } 66 | let side_sign = previous_tangent.outer_product(next_tangent)[0].signum(); 67 | let miter_clip = stroke_options.width.unwrap() * stroke_options.miter_clip.unwrap(); 68 | let side_offset = (stroke_options.offset.unwrap() - side_sign * 0.5) * stroke_options.width.unwrap(); 69 | let previous_edge_vertex = offset_control_point(control_point, previous_tangent, side_offset); 70 | let next_edge_vertex = offset_control_point(control_point, next_tangent, side_offset); 71 | let previous_edge_tangent: ppga2d::Plane = previous_tangent 72 | .inner_product(previous_edge_vertex) 73 | .geometric_product(previous_edge_vertex) 74 | .into(); 75 | let next_edge_tangent: ppga2d::Plane = next_tangent.inner_product(next_edge_vertex).geometric_product(next_edge_vertex).into(); 76 | let intersection = line_line_intersection(previous_edge_tangent, next_edge_tangent); 77 | let mut vertices = [control_point, previous_edge_vertex, next_edge_vertex, intersection, intersection]; 78 | let anti_parallel = (tangets_dot_product + 1.0).abs() <= ERROR_MARGIN; 79 | if anti_parallel || control_point.regressive_product(intersection).magnitude() > miter_clip { 80 | let mid_tangent = if anti_parallel { 81 | -rotate_90_degree_clockwise(previous_tangent) 82 | } else { 83 | (previous_tangent + next_tangent).signum() 84 | }; 85 | let clipping_vertex = offset_control_point(control_point, mid_tangent, -side_sign * miter_clip); 86 | let clipping_plane: ppga2d::Plane = mid_tangent.inner_product(clipping_vertex).geometric_product(clipping_vertex).into(); 87 | vertices[3] = line_line_intersection(previous_edge_tangent, clipping_plane); 88 | vertices[4] = line_line_intersection(clipping_plane, next_edge_tangent); 89 | proto_hull.push(point_to_vec(vertices[3]).into()); 90 | proto_hull.push(point_to_vec(vertices[4]).into()); 91 | } else { 92 | proto_hull.push(point_to_vec(vertices[3]).into()); 93 | } 94 | let scaled_tangent = previous_tangent * (1.0 / -stroke_options.width.unwrap()); 95 | let start_index = builder.joint_vertices.len(); 96 | let offset_along_path = *length_accumulator / stroke_options.width.unwrap(); 97 | for vertex in vertices.iter() { 98 | builder.joint_vertices.push(Vertex3f1i( 99 | point_to_vec(*vertex), 100 | [ 101 | side_sign * vertex.regressive_product(scaled_tangent), 102 | vertex.regressive_product(control_point).inner_product(scaled_tangent), 103 | offset_along_path, 104 | ], 105 | stroke_options.dynamic_stroke_options_group as u32, 106 | )); 107 | } 108 | let mut indices: Vec = (start_index as u16..(builder.joint_vertices.len() + 1) as u16).collect(); 109 | *indices.iter_mut().last().unwrap() = (-1isize) as u16; 110 | builder.joint_indices.append(&mut indices); 111 | *length_accumulator += tangets_dot_product.acos() / (std::f32::consts::PI * 2.0) * stroke_options.width.unwrap(); 112 | cut_stroke_polygon(builder, proto_hull); 113 | emit_stroke_vertices( 114 | builder, 115 | stroke_options, 116 | stroke_options.dynamic_stroke_options_group, 117 | *length_accumulator, 118 | control_point, 119 | next_tangent, 120 | ); 121 | } 122 | 123 | fn cut_stroke_polygon(builder: &mut StrokeBuilder, proto_hull: &mut Vec>) { 124 | if !builder.path_line_vertices.is_empty() { 125 | proto_hull.append(&mut builder.path_line_vertices.iter().map(|vertex| vertex.0.into()).collect()); 126 | let start_index = builder.line_vertices.len(); 127 | builder.line_vertices.append(&mut builder.path_line_vertices); 128 | let mut indices: Vec = (start_index as u16..(builder.line_vertices.len() + 1) as u16).collect(); 129 | *indices.iter_mut().last().unwrap() = (-1isize) as u16; 130 | builder.line_indices.append(&mut indices); 131 | } 132 | } 133 | 134 | macro_rules! emit_curve_stroke { 135 | ($builder:expr, $stroke_options:expr, $length_accumulator:expr, 136 | $previous_control_point:expr, $power_basis:expr, $angle_step:ident, $uniform_tangent_angle:expr, 137 | $point:ident, $tangent:ident $(,)?) => { 138 | let parameters: Vec = match $stroke_options.curve_approximation { 139 | CurveApproximation::UniformlySpacedParameters(steps) => (1..steps + 1).map(|i| i as f32 / steps as f32).collect(), 140 | CurveApproximation::UniformTangentAngle($angle_step) => $uniform_tangent_angle, 141 | }; 142 | let mut previous_point = $previous_control_point; 143 | for mut t in parameters { 144 | let mut tangent = $tangent(&$power_basis, t); 145 | if tangent.squared_magnitude() == 0.0 { 146 | if t < 0.5 { 147 | t += f32::EPSILON; 148 | } else { 149 | t -= f32::EPSILON; 150 | } 151 | tangent = $tangent(&$power_basis, t); 152 | } 153 | tangent = tangent.signum(); 154 | let mut point = $point(&$power_basis, t); 155 | point = point * (1.0 / point[0]); 156 | $length_accumulator += previous_point.regressive_product(point).magnitude(); 157 | emit_stroke_vertices( 158 | $builder, 159 | &$stroke_options, 160 | $stroke_options.dynamic_stroke_options_group, 161 | $length_accumulator, 162 | point, 163 | tangent, 164 | ); 165 | previous_point = point; 166 | } 167 | }; 168 | } 169 | 170 | #[derive(Default)] 171 | pub struct StrokeBuilder { 172 | pub line_indices: Vec, 173 | pub joint_indices: Vec, 174 | pub line_vertices: Vec, 175 | pub joint_vertices: Vec, 176 | path_line_vertices: Vec, 177 | } 178 | 179 | fn get_quadratic_tangents(control_points: &[ppga2d::Point; 3]) -> (ppga2d::Plane, ppga2d::Plane) { 180 | let mut segment_start_tangent = control_points[0].regressive_product(control_points[1]).signum(); 181 | let mut segment_end_tangent = control_points[1].regressive_product(control_points[2]).signum(); 182 | if segment_start_tangent[0].is_nan() || segment_end_tangent[0].is_nan() { 183 | segment_start_tangent = control_points[0].regressive_product(control_points[2]).signum(); 184 | segment_end_tangent = segment_start_tangent; 185 | } 186 | (segment_start_tangent, segment_end_tangent) 187 | } 188 | 189 | fn get_cubic_tangents(control_points: &[ppga2d::Point; 4]) -> (ppga2d::Plane, ppga2d::Plane) { 190 | let mut segment_start_tangent = control_points[0].regressive_product(control_points[1]).signum(); 191 | if segment_start_tangent[0].is_nan() { 192 | segment_start_tangent = control_points[0].regressive_product(control_points[2]).signum(); 193 | } 194 | let mut segment_end_tangent = control_points[2].regressive_product(control_points[3]).signum(); 195 | if segment_end_tangent[0].is_nan() { 196 | segment_end_tangent = control_points[1].regressive_product(control_points[3]).signum(); 197 | } 198 | if segment_start_tangent[0].is_nan() || segment_end_tangent[0].is_nan() { 199 | segment_end_tangent = control_points[0].regressive_product(control_points[3]).signum(); 200 | } 201 | (segment_start_tangent, segment_end_tangent) 202 | } 203 | 204 | impl StrokeBuilder { 205 | pub fn add_path(&mut self, proto_hull: &mut Vec>, path: &Path) -> Result<(), Error> { 206 | let stroke_options = path.stroke_options.as_ref().unwrap(); 207 | let mut previous_control_point = vec_to_point(path.start.unwrap()); 208 | let mut first_tangent = ppga2d::Plane::zero(); 209 | let mut previous_tangent = ppga2d::Plane::zero(); 210 | let mut line_segment_iter = path.line_segments.iter(); 211 | let mut integral_quadratic_curve_segment_iter = path.integral_quadratic_curve_segments.iter().peekable(); 212 | let mut integral_cubic_curve_segment_iter = path.integral_cubic_curve_segments.iter().peekable(); 213 | let mut rational_quadratic_curve_segment_iter = path.rational_quadratic_curve_segments.iter().peekable(); 214 | let mut rational_cubic_curve_segment_iter = path.rational_cubic_curve_segments.iter().peekable(); 215 | let mut length_accumulator = 0.0; 216 | let mut is_first_segment = true; 217 | for segment_type in path.segment_types.iter() { 218 | let next_control_point; 219 | let segment_start_tangent; 220 | let segment_end_tangent; 221 | match segment_type { 222 | SegmentType::Line => { 223 | let segment = line_segment_iter.next().unwrap(); 224 | next_control_point = vec_to_point(segment.control_points[0].unwrap()); 225 | segment_start_tangent = previous_control_point.regressive_product(next_control_point).signum(); 226 | segment_end_tangent = segment_start_tangent; 227 | } 228 | SegmentType::IntegralQuadraticCurve => { 229 | let segment = integral_quadratic_curve_segment_iter.peek().unwrap(); 230 | next_control_point = vec_to_point(segment.control_points[1].unwrap()); 231 | (segment_start_tangent, segment_end_tangent) = get_quadratic_tangents(&[ 232 | previous_control_point, 233 | vec_to_point(segment.control_points[0].unwrap()), 234 | next_control_point, 235 | ]); 236 | } 237 | SegmentType::IntegralCubicCurve => { 238 | let segment = integral_cubic_curve_segment_iter.peek().unwrap(); 239 | next_control_point = vec_to_point(segment.control_points[2].unwrap()); 240 | (segment_start_tangent, segment_end_tangent) = get_cubic_tangents(&[ 241 | previous_control_point, 242 | vec_to_point(segment.control_points[0].unwrap()), 243 | vec_to_point(segment.control_points[1].unwrap()), 244 | next_control_point, 245 | ]); 246 | } 247 | SegmentType::RationalQuadraticCurve => { 248 | let segment = rational_quadratic_curve_segment_iter.peek().unwrap(); 249 | next_control_point = vec_to_point(segment.control_points[1].unwrap()); 250 | (segment_start_tangent, segment_end_tangent) = get_quadratic_tangents(&[ 251 | previous_control_point, 252 | vec_to_point(segment.control_points[0].unwrap()), 253 | next_control_point, 254 | ]); 255 | } 256 | SegmentType::RationalCubicCurve => { 257 | let segment = rational_cubic_curve_segment_iter.peek().unwrap(); 258 | next_control_point = vec_to_point(segment.control_points[2].unwrap()); 259 | (segment_start_tangent, segment_end_tangent) = get_cubic_tangents(&[ 260 | previous_control_point, 261 | vec_to_point(segment.control_points[0].unwrap()), 262 | vec_to_point(segment.control_points[1].unwrap()), 263 | next_control_point, 264 | ]); 265 | } 266 | } 267 | if segment_start_tangent[0].is_nan() || segment_end_tangent[0].is_nan() { 268 | continue; 269 | } 270 | if is_first_segment { 271 | is_first_segment = false; 272 | first_tangent = segment_start_tangent; 273 | if !stroke_options.closed { 274 | let normal = rotate_90_degree_clockwise(segment_start_tangent); 275 | emit_stroke_vertices( 276 | self, 277 | stroke_options, 278 | stroke_options.dynamic_stroke_options_group, 279 | length_accumulator - 0.5 * stroke_options.width.unwrap(), 280 | offset_control_point(previous_control_point, normal, 0.5 * stroke_options.width.unwrap().abs()), 281 | segment_start_tangent, 282 | ); 283 | } 284 | if stroke_options.closed || *segment_type != SegmentType::Line { 285 | emit_stroke_vertices( 286 | self, 287 | stroke_options, 288 | stroke_options.dynamic_stroke_options_group, 289 | length_accumulator, 290 | previous_control_point, 291 | segment_start_tangent, 292 | ); 293 | } 294 | } else { 295 | emit_stroke_join( 296 | self, 297 | proto_hull, 298 | stroke_options, 299 | &mut length_accumulator, 300 | previous_control_point, 301 | previous_tangent, 302 | segment_start_tangent, 303 | ); 304 | } 305 | match segment_type { 306 | SegmentType::Line => { 307 | length_accumulator += previous_control_point.regressive_product(next_control_point).magnitude(); 308 | emit_stroke_vertices( 309 | self, 310 | stroke_options, 311 | stroke_options.dynamic_stroke_options_group, 312 | length_accumulator, 313 | next_control_point, 314 | segment_end_tangent, 315 | ); 316 | } 317 | SegmentType::IntegralQuadraticCurve => { 318 | let segment = integral_quadratic_curve_segment_iter.next().unwrap(); 319 | let power_basis = rational_quadratic_control_points_to_power_basis(&[ 320 | previous_control_point, 321 | vec_to_point(segment.control_points[0].unwrap()), 322 | vec_to_point(segment.control_points[1].unwrap()), 323 | ]); 324 | emit_curve_stroke!( 325 | self, 326 | stroke_options, 327 | length_accumulator, 328 | previous_control_point, 329 | power_basis, 330 | angle_step, 331 | integral_quadratic_uniform_tangent_angle(&power_basis, segment_start_tangent, segment_end_tangent, angle_step.unwrap()), 332 | rational_quadratic_point, 333 | rational_quadratic_first_order_derivative, 334 | ); 335 | } 336 | SegmentType::IntegralCubicCurve => { 337 | let segment = integral_cubic_curve_segment_iter.next().unwrap(); 338 | let power_basis = rational_cubic_control_points_to_power_basis(&[ 339 | previous_control_point, 340 | vec_to_point(segment.control_points[0].unwrap()), 341 | vec_to_point(segment.control_points[1].unwrap()), 342 | vec_to_point(segment.control_points[2].unwrap()), 343 | ]); 344 | emit_curve_stroke!( 345 | self, 346 | stroke_options, 347 | length_accumulator, 348 | previous_control_point, 349 | power_basis, 350 | angle_step, 351 | integral_cubic_uniform_tangent_angle(&power_basis, angle_step.unwrap()), 352 | rational_cubic_point, 353 | rational_cubic_first_order_derivative, 354 | ); 355 | } 356 | SegmentType::RationalQuadraticCurve => { 357 | let segment = rational_quadratic_curve_segment_iter.next().unwrap(); 358 | let power_basis = rational_quadratic_control_points_to_power_basis(&[ 359 | previous_control_point, 360 | weighted_vec_to_point(segment.weight.unwrap(), segment.control_points[0].unwrap()), 361 | vec_to_point(segment.control_points[1].unwrap()), 362 | ]); 363 | emit_curve_stroke!( 364 | self, 365 | stroke_options, 366 | length_accumulator, 367 | previous_control_point, 368 | power_basis, 369 | angle_step, 370 | rational_quadratic_uniform_tangent_angle(&power_basis, segment_start_tangent, segment_end_tangent, angle_step.unwrap()), 371 | rational_quadratic_point, 372 | rational_quadratic_first_order_derivative, 373 | ); 374 | } 375 | SegmentType::RationalCubicCurve => { 376 | let segment = rational_cubic_curve_segment_iter.next().unwrap(); 377 | let weights = segment.weights.unwrap(); 378 | let power_basis = rational_cubic_control_points_to_power_basis(&[ 379 | weighted_vec_to_point(weights[0], point_to_vec(previous_control_point)), 380 | weighted_vec_to_point(weights[1], segment.control_points[0].unwrap()), 381 | weighted_vec_to_point(weights[2], segment.control_points[1].unwrap()), 382 | weighted_vec_to_point(weights[3], segment.control_points[2].unwrap()), 383 | ]); 384 | emit_curve_stroke!( 385 | self, 386 | stroke_options, 387 | length_accumulator, 388 | previous_control_point, 389 | power_basis, 390 | angle_step, 391 | rational_cubic_uniform_tangent_angle(&power_basis, angle_step.unwrap()), 392 | rational_cubic_point, 393 | rational_cubic_first_order_derivative, 394 | ); 395 | } 396 | } 397 | previous_control_point = next_control_point; 398 | previous_tangent = segment_end_tangent; 399 | } 400 | if stroke_options.closed { 401 | let line_segment = previous_control_point.regressive_product(vec_to_point(path.start.unwrap())); 402 | let length = line_segment.magnitude(); 403 | if length > 0.0 { 404 | let segment_tangent = line_segment.geometric_quotient(length); 405 | emit_stroke_join( 406 | self, 407 | proto_hull, 408 | stroke_options, 409 | &mut length_accumulator, 410 | previous_control_point, 411 | previous_tangent, 412 | segment_tangent, 413 | ); 414 | length_accumulator += length; 415 | emit_stroke_vertices( 416 | self, 417 | stroke_options, 418 | stroke_options.dynamic_stroke_options_group, 419 | length_accumulator, 420 | vec_to_point(path.start.unwrap()), 421 | segment_tangent, 422 | ); 423 | emit_stroke_join( 424 | self, 425 | proto_hull, 426 | stroke_options, 427 | &mut length_accumulator, 428 | vec_to_point(path.start.unwrap()), 429 | segment_tangent, 430 | first_tangent, 431 | ); 432 | } else { 433 | emit_stroke_join( 434 | self, 435 | proto_hull, 436 | stroke_options, 437 | &mut length_accumulator, 438 | vec_to_point(path.start.unwrap()), 439 | previous_tangent, 440 | first_tangent, 441 | ); 442 | } 443 | } else { 444 | cut_stroke_polygon(self, proto_hull); 445 | emit_stroke_vertices( 446 | self, 447 | stroke_options, 448 | stroke_options.dynamic_stroke_options_group | 0x10000, 449 | length_accumulator, 450 | previous_control_point, 451 | previous_tangent, 452 | ); 453 | let normal = rotate_90_degree_clockwise(previous_tangent); 454 | emit_stroke_vertices( 455 | self, 456 | stroke_options, 457 | stroke_options.dynamic_stroke_options_group | 0x10000, 458 | length_accumulator + 0.5 * stroke_options.width.unwrap(), 459 | offset_control_point(previous_control_point, normal, -0.5 * stroke_options.width.unwrap().abs()), 460 | previous_tangent, 461 | ); 462 | } 463 | cut_stroke_polygon(self, proto_hull); 464 | Ok(()) 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | //! Defining the geometry and rendering options of [Path]s 2 | 3 | use crate::{ 4 | error::ERROR_MARGIN, 5 | safe_float::SafeFloat, 6 | utils::{motor2d_to_mat3, point_to_vec, rotate2d, rotate_90_degree_clockwise, vec_to_point, weighted_vec_to_point}, 7 | }; 8 | use geometric_algebra::{ 9 | epga1d, ppga2d, Dual, GeometricProduct, GeometricQuotient, Inverse, Powf, Powi, RegressiveProduct, Reversal, Signum, SquaredMagnitude, 10 | Transformation, Zero, 11 | }; 12 | 13 | /// A line 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 15 | pub struct LineSegment { 16 | /// The start is excluded as it is implicitly defined as the end of the previous [Path] segment. 17 | pub control_points: [SafeFloat; 1], 18 | } 19 | 20 | /// An integral quadratic bezier curve 21 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 22 | pub struct IntegralQuadraticCurveSegment { 23 | /// The start is excluded as it is implicitly defined as the end of the previous [Path] segment. 24 | pub control_points: [SafeFloat; 2], 25 | } 26 | 27 | /// An integral cubic bezier curve 28 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 29 | pub struct IntegralCubicCurveSegment { 30 | /// The start is excluded as it is implicitly defined as the end of the previous [Path] segment. 31 | pub control_points: [SafeFloat; 3], 32 | } 33 | 34 | /// A rational quadratic bezier curve 35 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 36 | pub struct RationalQuadraticCurveSegment { 37 | /// Weight of `control_points[0]` (the middle). 38 | /// 39 | /// The weights of the start and end control points are fixed to [1.0]. 40 | pub weight: SafeFloat, 41 | /// The start is excluded as it is implicitly defined as the end of the previous [Path] segment. 42 | pub control_points: [SafeFloat; 2], 43 | } 44 | 45 | /// A rational cubic bezier curve 46 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 47 | pub struct RationalCubicCurveSegment { 48 | /// Weights including the start, thus shifted by one compared to the control_points. 49 | pub weights: SafeFloat, 50 | /// The start is excluded as it is implicitly defined as the end of the previous [Path] segment. 51 | pub control_points: [SafeFloat; 3], 52 | } 53 | 54 | /// Different types of [Path] segments as an enum 55 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 56 | pub enum SegmentType { 57 | /// For lines 58 | Line, 59 | /// For integral quadratic bezier curves 60 | IntegralQuadraticCurve, 61 | /// For integral cubic bezier curves 62 | IntegralCubicCurve, 63 | /// For rational quadratic bezier curves 64 | RationalQuadraticCurve, 65 | /// For rational cubic bezier curves 66 | RationalCubicCurve, 67 | } 68 | 69 | /// Defines what geometry is generated where [Path] segments meet. 70 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 71 | pub enum Join { 72 | /// Polygon of the intersection of the adjacent [Path] segments 73 | /// 74 | /// To prevent the intersection from extending too far out at sharp angles, 75 | /// the polygon is clipped by a line which is perpendicular to the angle bisector of the adjacent [Path] segments. 76 | /// Where this line is located is defined by [miter_clip](StrokeOptions::miter_clip). 77 | Miter, 78 | /// Polygon of the vertices perpendicular to the tangents of the adjacent [Path] segments 79 | Bevel, 80 | /// Circular arc with a radius of half the [width](StrokeOptions::width), centered where the adjacent [Path] segments meet 81 | Round, 82 | } 83 | 84 | /// Defines what geometry is generated at the start and the end of a dash. 85 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 86 | pub enum Cap { 87 | /// Rectangular polygon extending half the [width](StrokeOptions::width) beyond the end of the dash 88 | Square, 89 | /// Circular arc with a radius of half the [width](StrokeOptions::width), centered at the end of the dash 90 | Round, 91 | /// Triangular polygon extending half the [width](StrokeOptions::width) beyond the end of the dash 92 | Out, 93 | /// Triangular cut out from a rectangular polygon extending half the [width](StrokeOptions::width) beyond the end of the dash 94 | In, 95 | /// Ramp shaped polygon extending [width](StrokeOptions::width) beyond the end of the dash, facing to the right of the [Path]s forward direction 96 | Right, 97 | /// Ramp shaped polygon extending [width](StrokeOptions::width) beyond the end of the dash, facing to the left of the [Path]s forward direction 98 | Left, 99 | /// Perpendicular clean cut exactly at the end of the dash 100 | Butt, 101 | } 102 | 103 | /// Defines the gaps in a stroked [Path]. 104 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 105 | pub struct DashInterval { 106 | /// Start of the gap to the next dash, thus end of the current dash. 107 | /// 108 | /// It is measured in terms of the [StrokeOptions::width]. 109 | pub gap_start: SafeFloat, 110 | /// End of the current gap, thus start of the next dash. 111 | /// 112 | /// It is measured in terms of the [StrokeOptions::width]. 113 | pub gap_end: SafeFloat, 114 | /// Cap at the start of the current dash, thus at the end of the last gap. 115 | pub dash_start: Cap, 116 | /// Cap at the end of the current dash, thus at the start of the next gap. 117 | pub dash_end: Cap, 118 | } 119 | 120 | /// Maximum number of [DashInterval]s in [DynamicStrokeOptions] 121 | pub const MAX_DASH_INTERVALS: usize = 4; 122 | 123 | /// Dynamic part of [StrokeOptions]. 124 | /// 125 | /// It is grouped and can be used by multiple [Path]s in the same [Shape](crate::renderer::Shape). 126 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 127 | pub enum DynamicStrokeOptions { 128 | /// Defines a dashed stroke pattern. 129 | Dashed { 130 | /// Defines what geometry is generated where [Path] segments meet. 131 | join: Join, 132 | /// Defines the [DashInterval]s which will be repeated along the stroked [Path]. 133 | pattern: Vec, 134 | /// Translates the [DashInterval]s along the stroked [Path]. 135 | /// 136 | /// Positive values translate towards the forward direction of the stroked [Path]. 137 | /// It is measured in terms of the [width](StrokeOptions::width). 138 | phase: SafeFloat, 139 | }, 140 | /// Defines a solid stroke pattern. 141 | Solid { 142 | /// Defines what geometry is generated where [Path] segments meet. 143 | join: Join, 144 | /// Defines what geometry is generated at the start of the [Path]. 145 | start: Cap, 146 | /// Defines what geometry is generated at the end of the [Path]. 147 | end: Cap, 148 | }, 149 | } 150 | 151 | /// Defines the parametric sampling strategy for stroking curves. 152 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 153 | pub enum CurveApproximation { 154 | /// Parametric step size is `1.0 / n`. 155 | /// 156 | /// Thus there are `n + 1` parameters (including start and end). 157 | UniformlySpacedParameters(usize), 158 | /// Tangent step angle in radians is `a`. 159 | /// 160 | /// Thus there are `(polar_range.arg() / a + 0.5) as usize + 1` parameters (including start and end). 161 | UniformTangentAngle(SafeFloat), 162 | /* 163 | /// Euclidian distance is `d`. 164 | /// 165 | /// Thus there are `(arc_length / d + 0.5) as usize + 1` parameters (including start and end). 166 | UniformArcLength(f32),*/ 167 | } 168 | 169 | /// Defines how a [Path] is stroked. 170 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 171 | pub struct StrokeOptions { 172 | /// The width of the stroked [Path] 173 | /// 174 | /// The absolute value is used, so the sign has no effect. 175 | pub width: SafeFloat, 176 | /// Offsets the stroke relative to the actual [Path]. 177 | /// 178 | /// It is measured in terms of the [width](StrokeOptions::width) and clamped to [-0.5, 0.5]. 179 | /// Negative values shift the stroke to the left and positive value shift the stroke to the right (in forward direction). 180 | pub offset: SafeFloat, 181 | /// Distance from the point where the adjacent [Path] segments meet to the clip line. 182 | /// 183 | /// It is measured in terms of the [width](StrokeOptions::width). 184 | /// The absolute value is used, so the sign has no effect. 185 | pub miter_clip: SafeFloat, 186 | /// If set to [true] the start and the end of the [Path] will be connected by an implicit [LineSegment]. 187 | pub closed: bool, 188 | /// Index of the [DynamicStrokeOptions] group to use 189 | pub dynamic_stroke_options_group: usize, 190 | /// Defines the parametric sampling strategy for stroking curves. 191 | pub curve_approximation: CurveApproximation, 192 | } 193 | 194 | impl StrokeOptions { 195 | /// Call this to make sure all parameters are within the allowed limits 196 | pub fn legalize(&mut self) { 197 | self.width = self.width.unwrap().abs().into(); 198 | self.offset = self.offset.unwrap().clamp(-0.5, 0.5).into(); 199 | self.miter_clip = self.miter_clip.unwrap().abs().into(); 200 | } 201 | } 202 | 203 | fn tangent_from_points(a: [f32; 2], b: [f32; 2]) -> ppga2d::Plane { 204 | vec_to_point(a).regressive_product(vec_to_point(b)) 205 | } 206 | 207 | /// A sequence of segments that can be either stroked or filled 208 | /// 209 | /// Every "move to" command requires a new [Path]. 210 | /// The order of the segments defines the direction of the [Path] and its clockwise or counterclockwise orientation. 211 | /// Filled [Path]s increment the winding counter when they are counterclockwise and decrement it when they are clockwise. 212 | #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] 213 | pub struct Path { 214 | /// If [Some] then the [Path] will be stroked otherwise (if [None]) it will be filled. 215 | pub stroke_options: Option, 216 | /// Beginning of the [Path] (position of "move to" command). 217 | pub start: SafeFloat, 218 | /// Storage for all the line segments of the [Path]. 219 | pub line_segments: Vec, 220 | /// Storage for all the integral quadratic curve segments of the [Path]. 221 | pub integral_quadratic_curve_segments: Vec, 222 | /// Storage for all the integral cubic curve segments of the [Path]. 223 | pub integral_cubic_curve_segments: Vec, 224 | /// Storage for all the rational quadratic curve segments of the [Path]. 225 | pub rational_quadratic_curve_segments: Vec, 226 | /// Storage for all the rational cubic curve segments of the [Path]. 227 | pub rational_cubic_curve_segments: Vec, 228 | /// Defines how the segments of different types are interleaved. 229 | pub segment_types: Vec, 230 | } 231 | 232 | impl Path { 233 | /// "line to" command 234 | pub fn push_line(&mut self, segment: LineSegment) { 235 | self.line_segments.push(segment); 236 | self.segment_types.push(SegmentType::Line); 237 | } 238 | 239 | /// "quadratic to" command 240 | pub fn push_integral_quadratic_curve(&mut self, segment: IntegralQuadraticCurveSegment) { 241 | self.integral_quadratic_curve_segments.push(segment); 242 | self.segment_types.push(SegmentType::IntegralQuadraticCurve); 243 | } 244 | 245 | /// "cubic to" command 246 | pub fn push_integral_cubic_curve(&mut self, segment: IntegralCubicCurveSegment) { 247 | self.integral_cubic_curve_segments.push(segment); 248 | self.segment_types.push(SegmentType::IntegralCubicCurve); 249 | } 250 | 251 | /// "quadratic to" command with weights 252 | pub fn push_rational_quadratic_curve(&mut self, segment: RationalQuadraticCurveSegment) { 253 | self.rational_quadratic_curve_segments.push(segment); 254 | self.segment_types.push(SegmentType::RationalQuadraticCurve); 255 | } 256 | 257 | /// "cubic to" command with weights 258 | pub fn push_rational_cubic_curve(&mut self, segment: RationalCubicCurveSegment) { 259 | self.rational_cubic_curve_segments.push(segment); 260 | self.segment_types.push(SegmentType::RationalCubicCurve); 261 | } 262 | 263 | /// Returns the current end of the [Path]. 264 | /// 265 | /// Returns the `start` if the [Path] is empty (has no segments). 266 | pub fn get_end(&self) -> [f32; 2] { 267 | match self.segment_types.last() { 268 | Some(SegmentType::Line) => { 269 | let segment = self.line_segments.last().unwrap(); 270 | segment.control_points[0].unwrap() 271 | } 272 | Some(SegmentType::IntegralQuadraticCurve) => { 273 | let segment = self.integral_quadratic_curve_segments.last().unwrap(); 274 | segment.control_points[1].unwrap() 275 | } 276 | Some(SegmentType::IntegralCubicCurve) => { 277 | let segment = self.integral_cubic_curve_segments.last().unwrap(); 278 | segment.control_points[2].unwrap() 279 | } 280 | Some(SegmentType::RationalQuadraticCurve) => { 281 | let segment = self.rational_quadratic_curve_segments.last().unwrap(); 282 | segment.control_points[1].unwrap() 283 | } 284 | Some(SegmentType::RationalCubicCurve) => { 285 | let segment = self.rational_cubic_curve_segments.last().unwrap(); 286 | segment.control_points[2].unwrap() 287 | } 288 | None => self.start.unwrap(), 289 | } 290 | } 291 | 292 | /// Returns the normalized tangent at the start in direction of the [Path]. 293 | /// 294 | /// Returns zero if the [Path] is empty (has no segments). 295 | /// Useful for arrow heads / tails. 296 | pub fn get_start_tangent(&self) -> ppga2d::Plane { 297 | match self.segment_types.last() { 298 | Some(SegmentType::Line) => { 299 | let segment = self.line_segments.last().unwrap(); 300 | tangent_from_points(self.start.unwrap(), segment.control_points[0].unwrap()).signum() 301 | } 302 | Some(SegmentType::IntegralQuadraticCurve) => { 303 | let segment = self.integral_quadratic_curve_segments.last().unwrap(); 304 | tangent_from_points(self.start.unwrap(), segment.control_points[0].unwrap()).signum() 305 | } 306 | Some(SegmentType::IntegralCubicCurve) => { 307 | let segment = self.integral_cubic_curve_segments.last().unwrap(); 308 | tangent_from_points(self.start.unwrap(), segment.control_points[0].unwrap()).signum() 309 | } 310 | Some(SegmentType::RationalQuadraticCurve) => { 311 | let segment = self.rational_quadratic_curve_segments.last().unwrap(); 312 | tangent_from_points(self.start.unwrap(), segment.control_points[0].unwrap()).signum() 313 | } 314 | Some(SegmentType::RationalCubicCurve) => { 315 | let segment = self.rational_cubic_curve_segments.last().unwrap(); 316 | tangent_from_points(self.start.unwrap(), segment.control_points[0].unwrap()).signum() 317 | } 318 | None => ppga2d::Plane::zero(), 319 | } 320 | } 321 | 322 | /// Returns the normalized tangent at the end in direction of the [Path]. 323 | /// 324 | /// Returns zero if the [Path] is empty (has no segments). 325 | /// Useful for arrow heads / tails. 326 | pub fn get_end_tangent(&self) -> ppga2d::Plane { 327 | match self.segment_types.last() { 328 | Some(SegmentType::Line) => { 329 | let previous_point = match self.segment_types.iter().rev().nth(1) { 330 | Some(SegmentType::Line) => { 331 | let segment = self.line_segments.iter().rev().nth(1).unwrap(); 332 | segment.control_points[0].unwrap() 333 | } 334 | Some(SegmentType::IntegralQuadraticCurve) => { 335 | let segment = self.integral_quadratic_curve_segments.last().unwrap(); 336 | segment.control_points[1].unwrap() 337 | } 338 | Some(SegmentType::IntegralCubicCurve) => { 339 | let segment = self.integral_cubic_curve_segments.last().unwrap(); 340 | segment.control_points[2].unwrap() 341 | } 342 | Some(SegmentType::RationalQuadraticCurve) => { 343 | let segment = self.rational_quadratic_curve_segments.last().unwrap(); 344 | segment.control_points[1].unwrap() 345 | } 346 | Some(SegmentType::RationalCubicCurve) => { 347 | let segment = self.rational_cubic_curve_segments.last().unwrap(); 348 | segment.control_points[2].unwrap() 349 | } 350 | None => self.start.unwrap(), 351 | }; 352 | let segment = self.line_segments.last().unwrap(); 353 | tangent_from_points(previous_point, segment.control_points[0].unwrap()).signum() 354 | } 355 | Some(SegmentType::IntegralQuadraticCurve) => { 356 | let segment = self.integral_quadratic_curve_segments.last().unwrap(); 357 | tangent_from_points(segment.control_points[0].unwrap(), segment.control_points[1].unwrap()).signum() 358 | } 359 | Some(SegmentType::IntegralCubicCurve) => { 360 | let segment = self.integral_cubic_curve_segments.last().unwrap(); 361 | tangent_from_points(segment.control_points[1].unwrap(), segment.control_points[2].unwrap()).signum() 362 | } 363 | Some(SegmentType::RationalQuadraticCurve) => { 364 | let segment = self.rational_quadratic_curve_segments.last().unwrap(); 365 | tangent_from_points(segment.control_points[0].unwrap(), segment.control_points[1].unwrap()).signum() 366 | } 367 | Some(SegmentType::RationalCubicCurve) => { 368 | let segment = self.rational_cubic_curve_segments.last().unwrap(); 369 | tangent_from_points(segment.control_points[1].unwrap(), segment.control_points[2].unwrap()).signum() 370 | } 371 | None => ppga2d::Plane::zero(), 372 | } 373 | } 374 | 375 | /// Concatenates two [Path]s, leaving the `other` [Path] empty. 376 | pub fn append(&mut self, other: &mut Self) { 377 | self.line_segments.append(&mut other.line_segments); 378 | self.integral_quadratic_curve_segments 379 | .append(&mut other.integral_quadratic_curve_segments); 380 | self.integral_cubic_curve_segments.append(&mut other.integral_cubic_curve_segments); 381 | self.rational_quadratic_curve_segments 382 | .append(&mut other.rational_quadratic_curve_segments); 383 | self.rational_cubic_curve_segments.append(&mut other.rational_cubic_curve_segments); 384 | } 385 | 386 | /// Transforms all control points of the [Path] (including the `start` and all segments). 387 | pub fn transform(&mut self, scale: f32, motor: &ppga2d::Motor) { 388 | let mut transform = motor2d_to_mat3(motor); 389 | transform[0][0] *= scale; 390 | transform[1][1] *= scale; 391 | fn transform_point(transform: &[ppga2d::Point; 3], p: SafeFloat) -> SafeFloat { 392 | let p = p.unwrap(); 393 | [ 394 | transform[2][0] + p[0] * transform[0][0] + p[1] * transform[1][0], 395 | transform[2][1] + p[0] * transform[0][1] + p[1] * transform[1][1], 396 | ] 397 | .into() 398 | } 399 | self.start = transform_point(&transform, self.start); 400 | let mut line_segment_iter = self.line_segments.iter_mut(); 401 | let mut integral_quadratic_curve_segment_iter = self.integral_quadratic_curve_segments.iter_mut(); 402 | let mut integral_cubic_curve_segment_iter = self.integral_cubic_curve_segments.iter_mut(); 403 | let mut rational_quadratic_curve_segment_iter = self.rational_quadratic_curve_segments.iter_mut(); 404 | let mut rational_cubic_curve_segment_iter = self.rational_cubic_curve_segments.iter_mut(); 405 | for segment_type in &mut self.segment_types { 406 | match *segment_type { 407 | SegmentType::Line => { 408 | let segment = line_segment_iter.next().unwrap(); 409 | for control_point in &mut segment.control_points { 410 | *control_point = transform_point(&transform, *control_point); 411 | } 412 | } 413 | SegmentType::IntegralQuadraticCurve => { 414 | let segment = integral_quadratic_curve_segment_iter.next().unwrap(); 415 | for control_point in &mut segment.control_points { 416 | *control_point = transform_point(&transform, *control_point); 417 | } 418 | } 419 | SegmentType::IntegralCubicCurve => { 420 | let segment = integral_cubic_curve_segment_iter.next().unwrap(); 421 | for control_point in &mut segment.control_points { 422 | *control_point = transform_point(&transform, *control_point); 423 | } 424 | } 425 | SegmentType::RationalQuadraticCurve => { 426 | let segment = rational_quadratic_curve_segment_iter.next().unwrap(); 427 | for control_point in &mut segment.control_points { 428 | *control_point = transform_point(&transform, *control_point); 429 | } 430 | } 431 | SegmentType::RationalCubicCurve => { 432 | let segment = rational_cubic_curve_segment_iter.next().unwrap(); 433 | for control_point in &mut segment.control_points { 434 | *control_point = transform_point(&transform, *control_point); 435 | } 436 | } 437 | } 438 | } 439 | } 440 | 441 | /// Reverses the direction of the [Path] and all its segments. 442 | /// 443 | /// Thus, swaps the values of `start` and `get_end()`. 444 | /// Also flips the clockwise or counterclockwise orientation. 445 | pub fn reverse(&mut self) { 446 | let mut previous_control_point = self.start; 447 | let mut line_segment_iter = self.line_segments.iter_mut(); 448 | let mut integral_quadratic_curve_segment_iter = self.integral_quadratic_curve_segments.iter_mut(); 449 | let mut integral_cubic_curve_segment_iter = self.integral_cubic_curve_segments.iter_mut(); 450 | let mut rational_quadratic_curve_segment_iter = self.rational_quadratic_curve_segments.iter_mut(); 451 | let mut rational_cubic_curve_segment_iter = self.rational_cubic_curve_segments.iter_mut(); 452 | for segment_type in &mut self.segment_types { 453 | match *segment_type { 454 | SegmentType::Line => { 455 | let segment = line_segment_iter.next().unwrap(); 456 | std::mem::swap(&mut previous_control_point, &mut segment.control_points[0]); 457 | } 458 | SegmentType::IntegralQuadraticCurve => { 459 | let segment = integral_quadratic_curve_segment_iter.next().unwrap(); 460 | std::mem::swap(&mut previous_control_point, &mut segment.control_points[1]); 461 | } 462 | SegmentType::IntegralCubicCurve => { 463 | let segment = integral_cubic_curve_segment_iter.next().unwrap(); 464 | segment.control_points.swap(0, 1); 465 | std::mem::swap(&mut previous_control_point, &mut segment.control_points[2]); 466 | } 467 | SegmentType::RationalQuadraticCurve => { 468 | let segment = rational_quadratic_curve_segment_iter.next().unwrap(); 469 | std::mem::swap(&mut previous_control_point, &mut segment.control_points[1]); 470 | } 471 | SegmentType::RationalCubicCurve => { 472 | let segment = rational_cubic_curve_segment_iter.next().unwrap(); 473 | let mut weights = segment.weights.unwrap(); 474 | weights.reverse(); 475 | segment.weights = weights.into(); 476 | segment.control_points.swap(0, 1); 477 | std::mem::swap(&mut previous_control_point, &mut segment.control_points[2]); 478 | } 479 | } 480 | } 481 | self.start = previous_control_point; 482 | self.segment_types.reverse(); 483 | self.line_segments.reverse(); 484 | self.integral_quadratic_curve_segments.reverse(); 485 | self.integral_cubic_curve_segments.reverse(); 486 | self.rational_quadratic_curve_segments.reverse(); 487 | self.rational_cubic_curve_segments.reverse(); 488 | } 489 | 490 | /// Turns integral quadratic curve segments into rational quadratic curve segments and 491 | /// integral cubic curve segments into rational cubic curve segments. 492 | pub fn convert_integral_curves_to_rational_curves(&mut self) { 493 | let mut integral_quadratic_curve_segment_iter = self.integral_quadratic_curve_segments.iter(); 494 | let mut integral_cubic_curve_segment_iter = self.integral_cubic_curve_segments.iter(); 495 | let mut rational_quadratic_curve_segment_index = 0; 496 | let mut rational_cubic_curve_segment_index = 0; 497 | for segment_type in &mut self.segment_types { 498 | match *segment_type { 499 | SegmentType::Line => {} 500 | SegmentType::IntegralQuadraticCurve => { 501 | let segment = integral_quadratic_curve_segment_iter.next().unwrap(); 502 | self.rational_quadratic_curve_segments.insert( 503 | rational_quadratic_curve_segment_index, 504 | RationalQuadraticCurveSegment { 505 | weight: 1.0.into(), 506 | control_points: segment.control_points, 507 | }, 508 | ); 509 | rational_quadratic_curve_segment_index += 1; 510 | *segment_type = SegmentType::RationalQuadraticCurve; 511 | } 512 | SegmentType::IntegralCubicCurve => { 513 | let segment = integral_cubic_curve_segment_iter.next().unwrap(); 514 | self.rational_cubic_curve_segments.insert( 515 | rational_cubic_curve_segment_index, 516 | RationalCubicCurveSegment { 517 | weights: [1.0, 1.0, 1.0, 1.0].into(), 518 | control_points: segment.control_points, 519 | }, 520 | ); 521 | rational_cubic_curve_segment_index += 1; 522 | *segment_type = SegmentType::RationalCubicCurve; 523 | } 524 | SegmentType::RationalQuadraticCurve => { 525 | rational_quadratic_curve_segment_index += 1; 526 | } 527 | SegmentType::RationalCubicCurve => { 528 | rational_cubic_curve_segment_index += 1; 529 | } 530 | } 531 | } 532 | self.integral_quadratic_curve_segments.clear(); 533 | self.integral_cubic_curve_segments.clear(); 534 | } 535 | 536 | /// Turns integral quadratic curve segments into integral cubic curve segments and 537 | /// rational quadratic curve segments into rational cubic curve segments. 538 | pub fn convert_quadratic_curves_to_cubic_curves(&mut self) { 539 | let mut line_segment_iter = self.line_segments.iter(); 540 | let mut integral_quadratic_curve_segment_iter = self.integral_quadratic_curve_segments.iter(); 541 | let mut integral_cubic_curve_segment_index = 0; 542 | let mut rational_quadratic_curve_segment_iter = self.rational_quadratic_curve_segments.iter(); 543 | let mut rational_cubic_curve_segment_index = 0; 544 | let mut previous_control_point = self.start.unwrap(); 545 | for segment_type in &mut self.segment_types { 546 | match *segment_type { 547 | SegmentType::Line => { 548 | let segment = line_segment_iter.next().unwrap(); 549 | previous_control_point = segment.control_points[0].unwrap(); 550 | } 551 | SegmentType::IntegralQuadraticCurve => { 552 | let segment = integral_quadratic_curve_segment_iter.next().unwrap(); 553 | let control_point_a = segment.control_points[0].unwrap(); 554 | let control_point_b = segment.control_points[1].unwrap(); 555 | self.integral_cubic_curve_segments.insert( 556 | integral_cubic_curve_segment_index, 557 | IntegralCubicCurveSegment { 558 | control_points: [ 559 | [ 560 | previous_control_point[0] + (control_point_a[0] - previous_control_point[0]) * 2.0 / 3.0, 561 | previous_control_point[1] + (control_point_a[1] - previous_control_point[1]) * 2.0 / 3.0, 562 | ] 563 | .into(), 564 | [ 565 | control_point_b[0] + (control_point_a[0] - control_point_b[0]) * 2.0 / 3.0, 566 | control_point_b[1] + (control_point_a[1] - control_point_b[1]) * 2.0 / 3.0, 567 | ] 568 | .into(), 569 | segment.control_points[1], 570 | ], 571 | }, 572 | ); 573 | integral_cubic_curve_segment_index += 1; 574 | *segment_type = SegmentType::IntegralCubicCurve; 575 | previous_control_point = segment.control_points[1].unwrap(); 576 | } 577 | SegmentType::IntegralCubicCurve => { 578 | previous_control_point = self.integral_cubic_curve_segments[integral_cubic_curve_segment_index].control_points[2].unwrap(); 579 | integral_cubic_curve_segment_index += 1; 580 | } 581 | SegmentType::RationalQuadraticCurve => { 582 | let segment = rational_quadratic_curve_segment_iter.next().unwrap(); 583 | let control_points = [ 584 | vec_to_point(previous_control_point), 585 | weighted_vec_to_point(segment.weight.unwrap(), segment.control_points[0].unwrap()), 586 | vec_to_point(segment.control_points[1].unwrap()), 587 | ]; 588 | let new_control_points = [ 589 | control_points[0] + (control_points[1] - control_points[0]) * (2.0 / 3.0), 590 | control_points[2] + (control_points[1] - control_points[2]) * (2.0 / 3.0), 591 | ]; 592 | self.rational_cubic_curve_segments.insert( 593 | rational_cubic_curve_segment_index, 594 | RationalCubicCurveSegment { 595 | weights: [1.0, new_control_points[0][0], new_control_points[1][0], 1.0].into(), 596 | control_points: [ 597 | point_to_vec(new_control_points[0]).into(), 598 | point_to_vec(new_control_points[1]).into(), 599 | segment.control_points[1], 600 | ], 601 | }, 602 | ); 603 | rational_cubic_curve_segment_index += 1; 604 | *segment_type = SegmentType::RationalCubicCurve; 605 | previous_control_point = segment.control_points[1].unwrap(); 606 | } 607 | SegmentType::RationalCubicCurve => { 608 | previous_control_point = self.rational_cubic_curve_segments[rational_cubic_curve_segment_index].control_points[2].unwrap(); 609 | rational_cubic_curve_segment_index += 1; 610 | } 611 | } 612 | } 613 | self.integral_quadratic_curve_segments.clear(); 614 | self.rational_quadratic_curve_segments.clear(); 615 | } 616 | 617 | /// "close" command 618 | /// 619 | /// A filled [Path] or a closed stroked [Path] already has an implicit [LineSegment] at the end. 620 | /// But this method can still be useful for reversing a closed stroked [Path] when the start and end should stay at the same location. 621 | pub fn close(&mut self) { 622 | if tangent_from_points(self.start.unwrap(), self.get_end()).squared_magnitude() <= ERROR_MARGIN { 623 | return; 624 | } 625 | self.push_line(LineSegment { 626 | control_points: [self.start], 627 | }); 628 | } 629 | 630 | /// "arc to" command for rectangular angles defined by the point where the start and end tangents of the arc cross 631 | pub fn push_quarter_ellipse(&mut self, tangent_crossing: [f32; 2], to: [f32; 2]) { 632 | self.push_rational_quadratic_curve(RationalQuadraticCurveSegment { 633 | weight: std::f32::consts::FRAC_1_SQRT_2.into(), 634 | control_points: [tangent_crossing.into(), to.into()], 635 | }); 636 | } 637 | 638 | /// "arc to" command for general elliptical arcs 639 | pub fn push_elliptical_arc(&mut self, half_extent: [f32; 2], rotation: f32, large_arc: bool, sweep: bool, to: [f32; 2]) { 640 | // https://www.w3.org/TR/SVG/implnote.html 641 | let mut radii = ppga2d::Plane::new(0.0, half_extent[0].abs(), half_extent[1].abs()); 642 | if radii[1] == 0.0 || radii[2] == 0.0 { 643 | self.push_line(LineSegment { control_points: [to.into()] }); 644 | return; 645 | } 646 | let from = vec_to_point(self.get_end()); 647 | let to = vec_to_point(to); 648 | let rotor = rotate2d(rotation); 649 | let vertex_unoriented = (to - from).dual() * 0.5; 650 | let vertex = rotor.inverse().transformation(vertex_unoriented); 651 | let vertex_squared = vertex * vertex; 652 | let mut radii_squared = radii * radii; 653 | let scale_factor_squared = vertex_squared[1] / radii_squared[1] + vertex_squared[2] / radii_squared[2]; 654 | if scale_factor_squared > 1.0 { 655 | // Scale radii up if they can not cover the distance between from and to 656 | radii *= scale_factor_squared.sqrt(); 657 | radii_squared = radii * radii; 658 | } 659 | let one_over_radii = ppga2d::Plane::new(0.0, 1.0, 1.0) / radii; 660 | let radii_squared_vertex_squared = radii_squared[1] * vertex_squared[2] + radii_squared[2] * vertex_squared[1]; 661 | let mut offset = ((radii_squared[1] * radii_squared[2] - radii_squared_vertex_squared) / radii_squared_vertex_squared) 662 | .max(0.0) 663 | .sqrt(); 664 | if large_arc == sweep { 665 | offset = -offset; 666 | } 667 | let center_offset_unoriented = radii * rotate_90_degree_clockwise(vertex * one_over_radii) * offset; 668 | let center = (to + from) * 0.5 + rotor.transformation(center_offset_unoriented).dual(); 669 | let start_normal = (-vertex - center_offset_unoriented) * one_over_radii; 670 | let end_normal = (vertex - center_offset_unoriented) * one_over_radii; 671 | let polar_start = epga1d::ComplexNumber::new(start_normal[1], start_normal[2]).signum(); 672 | let polar_end = epga1d::ComplexNumber::new(end_normal[1], end_normal[2]).signum(); 673 | let mut polar_range = polar_end.geometric_quotient(polar_start); 674 | let mut small_arc = polar_range.arg(); 675 | if small_arc < 0.0 { 676 | polar_range = polar_range.reversal(); 677 | small_arc = -small_arc; 678 | } 679 | let mut angle = small_arc; 680 | if large_arc { 681 | angle -= std::f32::consts::TAU; 682 | } 683 | let step_radians = std::f32::consts::PI * 2.0 / 3.0; 684 | let steps = (angle.abs() / step_radians).ceil() as usize; 685 | if large_arc != sweep { 686 | angle = -angle; 687 | } 688 | let polar_step = polar_range.powf(angle / (small_arc * steps as f32)); 689 | let half_polar_step_back = polar_step.powf(-0.5); 690 | let weight = (angle.abs() / steps as f32 * 0.5).cos(); 691 | let tangent_crossing_radii = radii * (1.0 / weight); 692 | for i in 1..=steps { 693 | let mut interpolated = polar_start.geometric_product(polar_step.powi(i as isize)); 694 | let vertex_unoriented = ppga2d::Plane::new(0.0, interpolated[0], interpolated[1]) * radii; 695 | let vertex = center + rotor.transformation(vertex_unoriented).dual(); 696 | // let tangent = rotor 697 | // .transformation(rotate_90_degree_clockwise(vertex_unoriented) * radii_squared) 698 | // .inner_product(vertex) 699 | // .signum(); 700 | interpolated = interpolated.geometric_product(half_polar_step_back); 701 | let tangent_crossing_unoriented = ppga2d::Plane::new(0.0, interpolated[0], interpolated[1]) * tangent_crossing_radii; 702 | let tangent_crossing = center + rotor.transformation(tangent_crossing_unoriented).dual(); 703 | self.push_rational_quadratic_curve(RationalQuadraticCurveSegment { 704 | weight: weight.into(), 705 | control_points: [point_to_vec(tangent_crossing).into(), point_to_vec(vertex).into()], 706 | }); 707 | } 708 | } 709 | 710 | /// Construct a polygon [Path] from a sequence of points. 711 | pub fn from_polygon(vertices: &[[f32; 2]]) -> Self { 712 | let mut vertices = vertices.iter(); 713 | let mut result = Path { 714 | start: vertices.next().unwrap().into(), 715 | ..Path::default() 716 | }; 717 | for control_point in vertices { 718 | result.push_line(LineSegment { 719 | control_points: [control_point.into()], 720 | }); 721 | } 722 | result 723 | } 724 | 725 | /// Construct a polygon [Path] by approximating a circle using a finite number of points. 726 | pub fn from_regular_polygon(center: [f32; 2], radius: f32, rotation: f32, vertex_count: usize) -> Self { 727 | let mut vertices = Vec::with_capacity(vertex_count); 728 | for i in 0..vertex_count { 729 | let angle = rotation + i as f32 / vertex_count as f32 * std::f32::consts::PI * 2.0; 730 | vertices.push([center[0] + radius * angle.cos(), center[1] + radius * angle.sin()]); 731 | } 732 | Self::from_polygon(&vertices) 733 | } 734 | 735 | /// Construct a polygon [Path] from a rectangle. 736 | pub fn from_rect(center: [f32; 2], half_extent: [f32; 2]) -> Self { 737 | Self::from_polygon(&[ 738 | [center[0] - half_extent[0], center[1] - half_extent[1]], 739 | [center[0] - half_extent[0], center[1] + half_extent[1]], 740 | [center[0] + half_extent[0], center[1] + half_extent[1]], 741 | [center[0] + half_extent[0], center[1] - half_extent[1]], 742 | ]) 743 | } 744 | 745 | /// Construct a [Path] from a rectangle with quarter circle roundings at the corners. 746 | pub fn from_rounded_rect(center: [f32; 2], half_extent: [f32; 2], radius: f32) -> Self { 747 | let vertices = [ 748 | ( 749 | [center[0] - half_extent[0] + radius, center[1] - half_extent[1]], 750 | [center[0] - half_extent[0], center[1] - half_extent[1]], 751 | [center[0] - half_extent[0], center[1] - half_extent[1] + radius], 752 | ), 753 | ( 754 | [center[0] - half_extent[0], center[1] + half_extent[1] - radius], 755 | [center[0] - half_extent[0], center[1] + half_extent[1]], 756 | [center[0] - half_extent[0] + radius, center[1] + half_extent[1]], 757 | ), 758 | ( 759 | [center[0] + half_extent[0] - radius, center[1] + half_extent[1]], 760 | [center[0] + half_extent[0], center[1] + half_extent[1]], 761 | [center[0] + half_extent[0], center[1] + half_extent[1] - radius], 762 | ), 763 | ( 764 | [center[0] + half_extent[0], center[1] - half_extent[1] + radius], 765 | [center[0] + half_extent[0], center[1] - half_extent[1]], 766 | [center[0] + half_extent[0] - radius, center[1] - half_extent[1]], 767 | ), 768 | ]; 769 | let mut result = Path { 770 | start: vertices[3].2.into(), 771 | ..Path::default() 772 | }; 773 | for (from, corner, to) in &vertices { 774 | result.push_line(LineSegment { 775 | control_points: [from.into()], 776 | }); 777 | result.push_quarter_ellipse(*corner, *to); 778 | } 779 | result 780 | } 781 | 782 | /// Constructs a [Path] from an ellipse. 783 | pub fn from_ellipse(center: [f32; 2], half_extent: [f32; 2]) -> Self { 784 | let vertices = [ 785 | ( 786 | [center[0] - half_extent[0], center[1] - half_extent[1]], 787 | [center[0] - half_extent[0], center[1]], 788 | ), 789 | ( 790 | [center[0] - half_extent[0], center[1] + half_extent[1]], 791 | [center[0], center[1] + half_extent[1]], 792 | ), 793 | ( 794 | [center[0] + half_extent[0], center[1] + half_extent[1]], 795 | [center[0] + half_extent[0], center[1]], 796 | ), 797 | ( 798 | [center[0] + half_extent[0], center[1] - half_extent[1]], 799 | [center[0], center[1] - half_extent[1]], 800 | ), 801 | ]; 802 | let mut result = Path { 803 | start: vertices[3].1.into(), 804 | ..Path::default() 805 | }; 806 | for (corner, to) in &vertices { 807 | result.push_quarter_ellipse(*corner, *to); 808 | } 809 | result 810 | } 811 | 812 | /// Constructs a [Path] from a circle. 813 | pub fn from_circle(center: [f32; 2], radius: f32) -> Self { 814 | Self::from_ellipse(center, [radius, radius]) 815 | } 816 | } 817 | --------------------------------------------------------------------------------