├── .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 | [](https://github.com/Lichtso/contrast_renderer/actions/workflows/actions.yml)
2 | [](https://docs.rs/contrast_renderer/)
3 | [](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