fix(text): render CJK glyphs at non-regular weights on macOS
Vendor femtovg 0.23.2 with a targeted patch: the swash raster path can fail to scale some variable fonts at a non-default variation instance (the CFF2-based CJK system fonts on modern macOS), and femtovg cached those glyphs as permanently missing — so track titles and any other text at medium/semibold/bold weight rendered blank for CJK while regular weight (empty coords) worked. render_glyph now retries the glyph at the font's default instance before giving up: worst case the glyph draws at regular weight instead of not at all. Fonts and instances that already render are untouched. Fixes #543 cc
This commit is contained in:
38 files changed
+18170
-2
No files matched your search
Generated
-2
@@ -2011,8 +2011,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "femtovg"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727c5dd9d601f3f4ea133b8a6ba37d37ca67ed59781675dc1c9c9f1701e0d197"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bytemuck",
|
||||
|
||||
@@ -121,3 +121,12 @@ i-slint-renderer-femtovg = { path = "vendor/i-slint-renderer-femtovg-1.16.1" }
|
||||
# collection creation, which takes priority over the CoreText query. macOS-only
|
||||
# (#[cfg]); Linux keeps its working fontconfig fallback.
|
||||
i-slint-common = { path = "vendor/i-slint-common-1.16.1" }
|
||||
# Slint compiles femtovg with the swash raster path. Swash fails to scale
|
||||
# some variable fonts at a non-default variation instance (the CFF2-based
|
||||
# CJK system fonts on modern macOS), and femtovg cached those glyphs as
|
||||
# permanently missing — so CJK text at any non-regular weight rendered
|
||||
# blank (#543; regular weight = empty coords = renders fine). The patch
|
||||
# retries the glyph at the font's DEFAULT instance before giving up: worst
|
||||
# case the glyph draws at regular weight instead of not at all. No effect
|
||||
# on fonts/instances that already render.
|
||||
femtovg = { path = "vendor/femtovg-0.23.2" }
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "902c9dda464088159d7426f4b04648ba893d0b8b"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.23.2] - 2026-04-13
|
||||
|
||||
- Fall back to path rendering for glyphs under non-translation transforms.
|
||||
|
||||
## [0.23.1] - 2026-03-31
|
||||
|
||||
- Fix variable font glyph caching returning wrong variation instance when
|
||||
using swash. Stale normalized coordinates in swash's ScaleContext could
|
||||
cause glyphs from a previous variation (e.g. bold) to appear for
|
||||
subsequent default-instance renders.
|
||||
|
||||
## [0.23.0] - 2026-03-30
|
||||
|
||||
- Added variable font support. New methods on `Paint`: `set_font_weight()`,
|
||||
`set_font_italic()`, `set_font_slant()`, and generic `set_font_variation()`
|
||||
with corresponding getters, `with_` builders, and `clear_` methods.
|
||||
- Added named font weight constants on `Paint` (`FONT_WEIGHT_THIN` through
|
||||
`FONT_WEIGHT_BLACK`).
|
||||
- Added `Canvas::font_variation_axes()` to query available variation axes
|
||||
for variable fonts, and new `VariationAxisInfo` public type.
|
||||
- **Breaking:** `Canvas::fill_glyph_run()` and `Canvas::stroke_glyph_run()`
|
||||
now take an additional `normalized_coords: &[i16]` parameter for variable
|
||||
font axis positions. Pass `&[]` for default behavior.
|
||||
- Replaced bundled Roboto with Roboto Flex for examples and added slant
|
||||
support with italic fallback.
|
||||
|
||||
## [0.22.0] - 2026-03-26
|
||||
|
||||
- WGPU: Changed API from `flush_to_surface()` to `flush_to_output()` and
|
||||
accept a type that can be converted to a new WGPURenderOutput struct,
|
||||
making it possible to render into texture views.
|
||||
- WGPU: Simplified CommandBuffer to be an Option<>, so that it can be
|
||||
passed to queue's `submit()` without additional wrapping.
|
||||
|
||||
## [0.21.0] - 2026-03-23
|
||||
|
||||
- Re-release 0.20.5 as 0.21 as the glow dependency upgrade is a public
|
||||
dependency that came with a new major version.
|
||||
|
||||
## [0.20.5] - 2026-03-23
|
||||
|
||||
- Fix text rendering with wgpu and mesa versions that have difficulties
|
||||
with pipeline override constants.
|
||||
- Make the ttf-parser dependency optional.
|
||||
|
||||
## [0.20.4] - 2026-02-23
|
||||
|
||||
- Fix occasional fringes around swash rendered glyphs caused by uninitialized
|
||||
padding.
|
||||
- Ported examples to latest winit/glutin/parley/cosmic-text versions.
|
||||
|
||||
## [0.20.3] - 2026-02-18
|
||||
|
||||
- Upgraded swash to the latest version.
|
||||
|
||||
## [0.20.2] - 2026-02-18
|
||||
|
||||
- Added `swash` feature to enable rasterization of glyphs with swash instead of
|
||||
the built-in path renderer.
|
||||
|
||||
## [0.20.1] - 2026-01-15
|
||||
|
||||
- Lowered MSRV requirement as it's only opt-in via wgpu feature.
|
||||
|
||||
## [0.20.0] - 2026-01-14
|
||||
|
||||
- Bumped wgpu dependency to wgpu-28
|
||||
- Bumped MSRV to 1.92
|
||||
|
||||
## [0.19.3] - 2025-10-13
|
||||
|
||||
- Fix regression in text rendering performance.
|
||||
|
||||
## [0.19.2] - 2025-10-10
|
||||
|
||||
- Fix erroneous glyph position regression of commit a1e215782a60df7e4fde9271fe7f95c134ac832f
|
||||
that accidentally subtracted bearing.
|
||||
|
||||
## [0.19.1] - 2025-10-08
|
||||
|
||||
- Fix docs.rs build
|
||||
|
||||
## [0.19.0] - 2025-10-07
|
||||
|
||||
- Bump MSRV to 1.88.
|
||||
- breaking: Upgraded WGPU renderer to use WGPU 27.x.
|
||||
- breaking: Make text layout an optional feature
|
||||
|
||||
## [0.18.1] - 2025-09-25
|
||||
|
||||
- Fix regression causing panic when rendering text.
|
||||
|
||||
## [0.18.0] - 2025-09-25
|
||||
|
||||
- Added API to drawing runs of glyphs from the same font.
|
||||
|
||||
## [0.17.0] - 2025-09-05
|
||||
|
||||
- Bump MSRV to 1.85.
|
||||
- Added support for conical gradients.
|
||||
|
||||
## [0.16.0] - 2025-08-03
|
||||
|
||||
- Bumped WGPU renderer to use WGPU 26.x.
|
||||
|
||||
## [0.15.0] - 2025-07-03
|
||||
|
||||
- Bumped WGPU renderer to use WGPU 25.x.
|
||||
|
||||
## [0.14.1] - 2025-06-14
|
||||
|
||||
- Fixed accidental rendering of newline (and other control characters) when using the Inter font. (#236) (thanks @peterprototypes)
|
||||
- WGPU renderer: Fixed panic when rendering empty scenes.
|
||||
|
||||
## [0.14.0] - 2025-03-24
|
||||
|
||||
- Bump MSRV to 1.84.
|
||||
- Fixed WGPU web rendering (thanks @JoshBurbidge)
|
||||
|
||||
## [0.13.0] - 2025-01-29
|
||||
|
||||
- Bump MSRV to 1.81.
|
||||
- Bump wgpu to 0.24.
|
||||
- **breaking**: The WGPU renderer is now constructed with a wgpu Device/Queue
|
||||
ithat's not wrapped in an Arc anymore. These types implement clone themselves.
|
||||
|
||||
## [0.12.0] - 2025-01-14
|
||||
|
||||
- WGPU renderer: Changed `flush_to_surface()` API to return a command buffer,
|
||||
to let the application decide when to submit.
|
||||
- Bumped glow dependency.
|
||||
|
||||
## [0.11.3] - 2024-12-26
|
||||
|
||||
- WGPU renderer: Fix crash when rendering without always calling `set_size()`. (#226)
|
||||
|
||||
## [0.11.1] - 2024-11-17
|
||||
|
||||
- No code changes, just a release for docs.rs.
|
||||
|
||||
## [0.11.0] - 2024-11-17
|
||||
|
||||
- Added WGPU renderer, behind `wgpu` feature flag.
|
||||
- Fixed rendering of glyphs with overlaps (#183). Thanks to Richard Hozák.
|
||||
- Bumped MSRV to 1.76.
|
||||
|
||||
## [0.10.1] - 2024-10-24
|
||||
|
||||
- Fix accidental breakage with scissor clipping.
|
||||
|
||||
## [0.10.0] - 2024-10-23
|
||||
|
||||
- **breaking**: Removed the mutable reference of self in `new` constructor for Transform2D
|
||||
- Implemented arithmetic operations for `Transform2D`
|
||||
- Completed and improved documentation
|
||||
- **breaking**: Removed methods `multipy` from `Transform2D`, since arithmetic operations are defined for `Transform2D` now.
|
||||
- **breaking**: Renamed `Transform2D` methods `inversed` to `inverse` and `inverse` to `invert`.
|
||||
- **breaking**: Renamed `Transform2D` constructor `new_translation` to `translation` and added new constructors `rotation` and `scaling`.
|
||||
- Reimplemented `Transform2D` transformation functions (`translate`, `rotate`, `scale`, `skew_x`, `skew_y`) to do what they are supposed to.
|
||||
- **breaking**: glow dependency bumped.
|
||||
|
||||
## [0.9.2] - 2024-06-27
|
||||
|
||||
- Fix path rendering where the default path solidity would interfere with the path's
|
||||
own winding direction (https://github.com/femtovg/femtovg/issues/124)
|
||||
- Fix blurry text rendering when drawing on non-integer coordinates
|
||||
- Bumped MSRV to 1.68.
|
||||
|
||||
## [0.9.1] - 2024-04-12
|
||||
|
||||
- Fixed inability to introspect `Path` verbs by making `PathIter` and `Verb` public.
|
||||
- Fixed rendering of text strokes with large font sizes.
|
||||
|
||||
## [0.9.0] - 2024-02-27
|
||||
|
||||
- **breaking**: Removed pub key field in ImageId. This accidentally
|
||||
exposed the implementation detail of the image store (generational-arena),
|
||||
which has been replaced with slotmap.
|
||||
- For WASM builds, require WebGL 2. This is supported by all major browsers
|
||||
and needed to make `ImageFlags::REPEAT_X/Y` work.
|
||||
- Bumped MSRV to 1.66.
|
||||
|
||||
## [0.8.2] - 2024-01-20
|
||||
|
||||
- Improved performance when rendering large texts.
|
||||
- Replace error logging to stderr with use of log crate.
|
||||
|
||||
## [0.8.1] - 2023-12-18
|
||||
|
||||
- Fix documentation build on docs.rs.
|
||||
|
||||
## [0.8.0] - 2023-11-02
|
||||
|
||||
- Re-release 0.7.2 with major version bump. 0.7.2 was yanked because
|
||||
glow is a re-exported public dependency, that was bumped.
|
||||
|
||||
## [0.7.2] - 2023-11-02
|
||||
|
||||
- Bump internal dependencies.
|
||||
|
||||
## [0.7.1] - 2023-06-14
|
||||
|
||||
- Fix performance regression when drawing unclipped image path fills.
|
||||
|
||||
## [0.7.0] - 2023-05-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Path drawing functions now take a `&Path` instead of a `&mut Path` and use interior mutability
|
||||
for caching.
|
||||
|
||||
## [0.6.0] - 2023-02-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `linear_gradient_stops` and `radial_gradient_stops` to take an `IntoIterator`
|
||||
instead of a slice slice for the color stops.
|
||||
|
||||
## [0.5.0] - 2023-02-06
|
||||
|
||||
### Added
|
||||
|
||||
- added a new `Size` struct, having a `width` and a `height`.
|
||||
- added `size` function to `Image` type, which returns both, `width` and `height` as a `Size`
|
||||
|
||||
### Changed
|
||||
|
||||
- Renamed `draw_glyph_cmds` to `draw_glyph_commands`.
|
||||
- Renamed `DrawCmd` to `DrawCommand`.
|
||||
- `set_transform` takes a value of type `Transform2D` now instead of a parameter list.
|
||||
- `dimensions` of `ImageSource` returns a new `Size` type now.
|
||||
|
||||
## [0.4.0] - 2023-01-27
|
||||
|
||||
### Added
|
||||
|
||||
- `OpenGl::new_from_function_cstr` to create the renderer from a GL loading function that
|
||||
takes an `&std::ffi::CStr`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed erroneously multiply applied global alpha when mixing color glyphs with regular glyphs.
|
||||
|
||||
### Changed
|
||||
|
||||
- MRSV was bumped to Rust 1.63, the crate now uses Rust Edition 2021.
|
||||
- `new_from_glutin_context` can now be used with headless contexts.
|
||||
- All const-safe `Color` constructors are now const.
|
||||
- `Canvas`'s text layout methods no longer require a mutable reference.
|
||||
- Removed the copy trait from `Paint` to avoid accidental copies.
|
||||
- `Paint` is always supplied by reference now.
|
||||
- `TextContext`'s `resize_shaping_run_cache` and `resize_shaped_words_cache` functions now take a
|
||||
`std::num::NonZeroUsize` for the capacity value.
|
||||
- As part of the glutin update, `OpenGL::new_from_glutin_context` was renamed to `new_from_glutin_display` and takes a glutin display now.
|
||||
- Removed `glutin` from the default features.
|
||||
|
||||
## [0.3.7] - 2022-10-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix build with latest rustybuzz release after 0.5.2 breakage. 0.5.3 doesn't
|
||||
re-export the ttf_parser module anymore.
|
||||
|
||||
## [0.3.6] - 2022-10-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix build with latest rustybuzz release.
|
||||
|
||||
## [0.3.5] - 2022-05-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Optimized the OpenGL renderer to perform better on older GPUs by splitting the large fragment shader
|
||||
into smaller programs.
|
||||
|
||||
## [0.3.4] - 2022-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for importing backend-specific textures into the rendering of a scene with `Canvas::create_image_from_native_texture`.
|
||||
- Added functions to `TextContext` to configure the text shaping caches: `resize_shaping_run_cache` and `resize_shaped_words_cache`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Added optimized rendering code path for the common case of filling a rectangular path with an image and anti-aliasing
|
||||
on the paint disabled.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed line breaking to permit a break in the middle of a word if it is the first word in the paragraph
|
||||
and it doesn't fit otherwise.
|
||||
|
||||
## [0.3.3] - 2022-02-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Bumped rustybuzz and ttf-parser dependencies.
|
||||
|
||||
## [0.3.2] - 2022-02-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- Correctly detect when WebGL is disabled in a web browser in the `renderer::OpenGL::new_from_html_canvas` function.
|
||||
|
||||
## [0.3.1] - 2022-02-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't require default features of glutin. We don't need any and this way other users of glutin
|
||||
have the ability to opt out.
|
||||
|
||||
## [0.3.0] - 2022-02-04
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking:** The dependency to the `image` crate was bumped from `0.23` to `0.24`.
|
||||
Since the types of this crate are used in public femtovg API, users need to upgrade
|
||||
their dependency to the `image` crate as well.
|
||||
- **Breaking**: Removed deprecated `renderer::OpenGL::new` function. Use `renderer::OpenGl::new_from_function`
|
||||
or `renderer::OpenGl::new_from_glutin_context`.
|
||||
|
||||
### Added
|
||||
|
||||
- Use `Paint::image_tint` to create an image paint that not only applies an alpha but an entire color (tint).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved performance of `fill_path` and `stroke_path`
|
||||
|
||||
[0.3.0]: https://github.com/femtovg/femtovg/releases/tag/v0.3.0
|
||||
[0.3.1]: https://github.com/femtovg/femtovg/releases/tag/v0.3.1
|
||||
[0.3.2]: https://github.com/femtovg/femtovg/releases/tag/v0.3.2
|
||||
[0.3.3]: https://github.com/femtovg/femtovg/releases/tag/v0.3.3
|
||||
[0.3.4]: https://github.com/femtovg/femtovg/releases/tag/v0.3.4
|
||||
[0.3.5]: https://github.com/femtovg/femtovg/releases/tag/v0.3.5
|
||||
[0.3.6]: https://github.com/femtovg/femtovg/releases/tag/v0.3.6
|
||||
[0.3.7]: https://github.com/femtovg/femtovg/releases/tag/v0.3.7
|
||||
[0.4.0]: https://github.com/femtovg/femtovg/releases/tag/v0.4.0
|
||||
[0.5.0]: https://github.com/femtovg/femtovg/releases/tag/v0.5.0
|
||||
[0.6.0]: https://github.com/femtovg/femtovg/releases/tag/v0.6.0
|
||||
[0.7.0]: https://github.com/femtovg/femtovg/releases/tag/v0.7.0
|
||||
[0.7.1]: https://github.com/femtovg/femtovg/releases/tag/v0.7.1
|
||||
[0.7.2]: https://github.com/femtovg/femtovg/releases/tag/v0.7.2
|
||||
[0.8.0]: https://github.com/femtovg/femtovg/releases/tag/v0.8.0
|
||||
[0.8.1]: https://github.com/femtovg/femtovg/releases/tag/v0.8.1
|
||||
[0.8.2]: https://github.com/femtovg/femtovg/releases/tag/v0.8.2
|
||||
[0.9.0]: https://github.com/femtovg/femtovg/releases/tag/v0.9.0
|
||||
[0.9.1]: https://github.com/femtovg/femtovg/releases/tag/v0.9.1
|
||||
[0.9.2]: https://github.com/femtovg/femtovg/releases/tag/v0.9.2
|
||||
[0.10.0]: https://github.com/femtovg/femtovg/releases/tag/v0.10.0
|
||||
[0.10.1]: https://github.com/femtovg/femtovg/releases/tag/v0.10.1
|
||||
[0.11.0]: https://github.com/femtovg/femtovg/releases/tag/v0.11.0
|
||||
[0.11.1]: https://github.com/femtovg/femtovg/releases/tag/v0.11.1
|
||||
[0.11.3]: https://github.com/femtovg/femtovg/releases/tag/v0.11.3
|
||||
[0.12.0]: https://github.com/femtovg/femtovg/releases/tag/v0.12.0
|
||||
[0.13.0]: https://github.com/femtovg/femtovg/releases/tag/v0.13.0
|
||||
[0.14.0]: https://github.com/femtovg/femtovg/releases/tag/v0.14.0
|
||||
[0.14.1]: https://github.com/femtovg/femtovg/releases/tag/v0.14.1
|
||||
[0.15.0]: https://github.com/femtovg/femtovg/releases/tag/v0.15.0
|
||||
[0.16.0]: https://github.com/femtovg/femtovg/releases/tag/v0.16.0
|
||||
[0.17.0]: https://github.com/femtovg/femtovg/releases/tag/v0.17.0
|
||||
[0.18.0]: https://github.com/femtovg/femtovg/releases/tag/v0.18.0
|
||||
[0.18.1]: https://github.com/femtovg/femtovg/releases/tag/v0.18.1
|
||||
[0.19.0]: https://github.com/femtovg/femtovg/releases/tag/v0.19.0
|
||||
[0.19.1]: https://github.com/femtovg/femtovg/releases/tag/v0.19.1
|
||||
[0.19.2]: https://github.com/femtovg/femtovg/releases/tag/v0.19.2
|
||||
[0.19.3]: https://github.com/femtovg/femtovg/releases/tag/v0.19.3
|
||||
[0.20.0]: https://github.com/femtovg/femtovg/releases/tag/v0.20.0
|
||||
[0.20.1]: https://github.com/femtovg/femtovg/releases/tag/v0.20.1
|
||||
[0.20.2]: https://github.com/femtovg/femtovg/releases/tag/v0.20.2
|
||||
[0.20.3]: https://github.com/femtovg/femtovg/releases/tag/v0.20.3
|
||||
[0.20.4]: https://github.com/femtovg/femtovg/releases/tag/v0.20.4
|
||||
[0.20.5]: https://github.com/femtovg/femtovg/releases/tag/v0.20.5
|
||||
[0.21.0]: https://github.com/femtovg/femtovg/releases/tag/v0.21.0
|
||||
[0.22.0]: https://github.com/femtovg/femtovg/releases/tag/v0.22.0
|
||||
[0.23.0]: https://github.com/femtovg/femtovg/releases/tag/v0.23.0
|
||||
[0.23.1]: https://github.com/femtovg/femtovg/releases/tag/v0.23.1
|
||||
[0.23.2]: https://github.com/femtovg/femtovg/releases/tag/v0.23.2
|
||||
+3833
File diff suppressed because it is too large.
Load diff
+271
@@ -0,0 +1,271 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
name = "femtovg"
|
||||
version = "0.23.2"
|
||||
authors = [
|
||||
"Peter Todorov <ptodorov@cytec.bg>",
|
||||
"Adam Nemecek <adamnemecek@gmail.com>",
|
||||
]
|
||||
build = false
|
||||
exclude = [
|
||||
"assets",
|
||||
"examples",
|
||||
]
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Antialiased 2D vector drawing library"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"graphics",
|
||||
"canvas",
|
||||
"vector",
|
||||
"drawing",
|
||||
"gpu",
|
||||
]
|
||||
categories = ["graphics"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/femtovg/femtovg"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["glutin/egl"]
|
||||
all-features = true
|
||||
rustdoc-args = [
|
||||
"--cfg",
|
||||
"docsrs",
|
||||
]
|
||||
|
||||
[features]
|
||||
debug_inspector = []
|
||||
default = [
|
||||
"image-loading",
|
||||
"textlayout",
|
||||
]
|
||||
image-loading = ["image"]
|
||||
swash = ["dep:swash"]
|
||||
textlayout = [
|
||||
"dep:rustybuzz",
|
||||
"dep:unicode-bidi",
|
||||
"dep:unicode-segmentation",
|
||||
"dep:lru",
|
||||
"dep:ttf-parser",
|
||||
]
|
||||
wgpu = ["dep:wgpu"]
|
||||
|
||||
[lib]
|
||||
name = "femtovg"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "book_example_1_1"
|
||||
path = "book/src/1_getting_started/1_setting_up.rs"
|
||||
|
||||
[[example]]
|
||||
name = "book_example_1_2"
|
||||
path = "book/src/1_getting_started/2_rendering.rs"
|
||||
|
||||
[[example]]
|
||||
name = "book_example_1_3"
|
||||
path = "book/src/1_getting_started/3_event_loop.rs"
|
||||
|
||||
[[test]]
|
||||
name = "integration"
|
||||
path = "tests/integration.rs"
|
||||
|
||||
[[test]]
|
||||
name = "transform2D"
|
||||
path = "tests/transform2D.rs"
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2.6.0"
|
||||
|
||||
[dependencies.bytemuck]
|
||||
version = "1.16"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.fnv]
|
||||
version = "1.0.7"
|
||||
|
||||
[dependencies.glow]
|
||||
version = "0.17.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.image]
|
||||
version = "0.25.0"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.imgref]
|
||||
version = "1.11.0"
|
||||
|
||||
[dependencies.itertools]
|
||||
version = "0.14"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.lru]
|
||||
version = "0.16.0"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.rgb]
|
||||
version = "0.8.50"
|
||||
|
||||
[dependencies.rustybuzz]
|
||||
version = "0.20.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = [
|
||||
"derive",
|
||||
"rc",
|
||||
]
|
||||
optional = true
|
||||
|
||||
[dependencies.slotmap]
|
||||
version = "1.0.7"
|
||||
|
||||
[dependencies.swash]
|
||||
version = "0.2.6"
|
||||
optional = true
|
||||
|
||||
[dependencies.ttf-parser]
|
||||
version = "0.25.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.unicode-bidi]
|
||||
version = "0.3.17"
|
||||
optional = true
|
||||
|
||||
[dependencies.unicode-segmentation]
|
||||
version = "1.12.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.wgpu]
|
||||
version = "28"
|
||||
features = ["wgsl"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.cosmic-text]
|
||||
version = "0.18.2"
|
||||
|
||||
[dev-dependencies.euclid]
|
||||
version = "0.22.3"
|
||||
|
||||
[dev-dependencies.image]
|
||||
version = "0.25.0"
|
||||
features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.instant]
|
||||
version = "0.1"
|
||||
features = ["now"]
|
||||
|
||||
[dev-dependencies.lazy_static]
|
||||
version = "1.4.0"
|
||||
|
||||
[dev-dependencies.pollster]
|
||||
version = "0.4"
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.10"
|
||||
|
||||
[dev-dependencies.resource]
|
||||
version = "0.6.1"
|
||||
|
||||
[dev-dependencies.swash]
|
||||
version = "0.2.6"
|
||||
|
||||
[dev-dependencies.usvg]
|
||||
version = "0.47.0"
|
||||
|
||||
[dev-dependencies.wgpu]
|
||||
version = "28"
|
||||
|
||||
[dev-dependencies.winit]
|
||||
version = "0.30.12"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.glutin]
|
||||
version = "0.32"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.glutin]
|
||||
version = "0.32.3"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.glutin-winit]
|
||||
version = "0.5.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.parley]
|
||||
version = "0.8.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies.raw-window-handle]
|
||||
version = "0.6.2"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen]
|
||||
version = "0.2"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.web_sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"WebGlContextAttributes",
|
||||
"HtmlImageElement",
|
||||
"WebGl2RenderingContext",
|
||||
]
|
||||
package = "web-sys"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.console_error_panic_hook]
|
||||
version = "0.1.5"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.getrandom]
|
||||
version = "0.4.1"
|
||||
features = ["wasm_js"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.instant]
|
||||
version = "0.1"
|
||||
features = [
|
||||
"wasm-bindgen",
|
||||
"now",
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.resource]
|
||||
version = "0.6.1"
|
||||
features = ["force-static"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.wasm-bindgen-futures]
|
||||
version = "0.4.45"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.web_sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"console",
|
||||
"WebGlContextAttributes",
|
||||
]
|
||||
package = "web-sys"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies.wgpu]
|
||||
version = "28"
|
||||
features = ["webgl"]
|
||||
|
||||
[profile.release]
|
||||
debug = 2
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
[package]
|
||||
name = "femtovg"
|
||||
description = "Antialiased 2D vector drawing library"
|
||||
version = "0.23.2"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
"Peter Todorov <ptodorov@cytec.bg>",
|
||||
"Adam Nemecek <adamnemecek@gmail.com>",
|
||||
]
|
||||
keywords = ["graphics", "canvas", "vector", "drawing", "gpu"]
|
||||
categories = ["graphics"]
|
||||
repository = "https://github.com/femtovg/femtovg"
|
||||
edition = "2021"
|
||||
exclude = ["assets", "examples"]
|
||||
rust-version = "1.88"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
[dependencies]
|
||||
fnv = { version = "1.0.7" }
|
||||
rgb = "0.8.50"
|
||||
bytemuck = { version = "1.16", features = ["derive"] }
|
||||
imgref = "1.11.0"
|
||||
bitflags = "2.6.0"
|
||||
rustybuzz = { version = "0.20.0", optional = true }
|
||||
unicode-bidi = { version = "0.3.17", optional = true }
|
||||
unicode-segmentation = { version = "1.12.0", optional = true }
|
||||
slotmap = "1.0.7"
|
||||
lru = { version = "0.16.0", optional = true, default-features = false }
|
||||
image = { version = "0.25.0", optional = true, default-features = false }
|
||||
serde = { version = "1.0", optional = true, features = ["derive", "rc"] }
|
||||
glow = { version = "0.17.0", default-features = false }
|
||||
log = "0.4"
|
||||
wgpu = { version = "28", optional = true, default-features = false, features = ["wgsl"] }
|
||||
itertools = "0.14"
|
||||
ttf-parser = { version = "0.25.1", optional = true }
|
||||
swash = { version = "0.2.6", optional = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
glutin = { version = "0.32", optional = true, default-features = false }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
web_sys = { version = "0.3", package = "web-sys", features = [
|
||||
"WebGlContextAttributes",
|
||||
"HtmlImageElement",
|
||||
"WebGl2RenderingContext",
|
||||
] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["image-loading", "textlayout"]
|
||||
image-loading = ["image"]
|
||||
debug_inspector = []
|
||||
wgpu = ["dep:wgpu"]
|
||||
textlayout = ["dep:rustybuzz", "dep:unicode-bidi", "dep:unicode-segmentation", "dep:lru", "dep:ttf-parser"]
|
||||
swash = ["dep:swash"]
|
||||
|
||||
[dev-dependencies]
|
||||
winit = { version = "0.30.12" }
|
||||
euclid = "0.22.3"
|
||||
rand = "0.10"
|
||||
usvg = { version = "0.47.0" }
|
||||
instant = { version = "0.1", features = ["now"] }
|
||||
resource = "0.6.1"
|
||||
image = { version = "0.25.0", default-features = false, features = [
|
||||
"jpeg",
|
||||
"png",
|
||||
] }
|
||||
cosmic-text = "0.18.2"
|
||||
swash = "0.2.6"
|
||||
lazy_static = "1.4.0"
|
||||
pollster = "0.4"
|
||||
wgpu = { version = "28" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
glutin = "0.32.3"
|
||||
glutin-winit = "0.5.0"
|
||||
raw-window-handle = "0.6.2"
|
||||
parley = "0.8.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
web_sys = { version = "0.3", package = "web-sys", features = [
|
||||
"console",
|
||||
"WebGlContextAttributes",
|
||||
] }
|
||||
console_error_panic_hook = "0.1.5"
|
||||
instant = { version = "0.1", features = ["wasm-bindgen", "now"] }
|
||||
resource = { version = "0.6.1", features = ["force-static"] }
|
||||
getrandom = { version = "0.4.1", features = ["wasm_js"] }
|
||||
wgpu = { version = "28", features = ["webgl"] }
|
||||
wasm-bindgen-futures = { version = "0.4.45" }
|
||||
|
||||
[[example]]
|
||||
name = "book_example_1_1"
|
||||
path = "book/src/1_getting_started/1_setting_up.rs"
|
||||
|
||||
[[example]]
|
||||
name = "book_example_1_2"
|
||||
path = "book/src/1_getting_started/2_rendering.rs"
|
||||
|
||||
[[example]]
|
||||
name = "book_example_1_3"
|
||||
path = "book/src/1_getting_started/3_event_loop.rs"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["glutin/egl"]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Copyright 2021 femtovg contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
Copyright 2021 Developers of the femtovg project
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
[](https://crates.io/crates/femtovg)
|
||||
[](https://docs.rs/femtovg)
|
||||
[](https://deps.rs/repo/github/femtovg/femtovg)
|
||||
[](#msrv)
|
||||
[](https://discord.gg/V69VdVu)
|
||||
|
||||
<h2 align="center">femtovg</h2>
|
||||
<p align="center">GPU Antialiased 2D vector drawing library written in Rust.</p>
|
||||
|
||||
- [Cargo crate](https://crates.io/crates/femtovg)
|
||||
- [Book](https://femtovg.github.io)
|
||||
- [Join the femtovg Discord channel](https://discord.gg/V69VdVu)
|
||||
|
||||
---
|
||||
|
||||
Ported from https://github.com/memononen/nanovg
|
||||
|
||||
Most of the implementation is the same as the original C code with some bug fixes, some features added and several parts have been made more Rust-y. Rendering is done via one OpenGl (ES) 3.0+ backend.
|
||||
|
||||
## Screenshots
|
||||
### Demo
|
||||

|
||||
Run with `cargo run --example demo`
|
||||
|
||||
### Breakout
|
||||

|
||||
Run with `cargo run --example breakout`
|
||||
|
||||
### SVG
|
||||

|
||||
Run with `cargo run --example svg`
|
||||
|
||||
### Text
|
||||

|
||||
Run with `cargo run --example text`
|
||||
|
||||
## Features
|
||||
* [x] Anti-aliasing
|
||||
* [x] Bézier paths filling and stroking
|
||||
* [x] Solid color and image pattern fills and strokes
|
||||
* [x] Gradients - box, linear and radial
|
||||
* [x] Stroke width and miterlimit
|
||||
* [x] Stroke caps: butt, round and square
|
||||
* [x] Stroke joins: miter, round and bevel
|
||||
* [x] Fill rules - EvenOdd/NonZero
|
||||
* [x] Rectangle scissoring
|
||||
* [x] Composition modes (SourceOver, SourceIn, SourceOut, Atop, etc..)
|
||||
* [x] Global alpha
|
||||
* [x] Text filling and stroking
|
||||
* [x] Text shaping
|
||||
* [x] Text alignment: (left center right), (top, middle, alphabetic, bottom)
|
||||
* [x] Nearest font matching
|
||||
* [x] Path hit testing
|
||||
* [x] OpenGl ES2 backend
|
||||
* [x] WGPU backend
|
||||
* [x] Color fonts
|
||||
|
||||
## Not supported
|
||||
* [ ] Stroke dashing
|
||||
* [ ] Path scissoring
|
||||
* [ ] Custom shaders
|
||||
* [ ] 3D transforms
|
||||
|
||||
## Ecosystem
|
||||
|
||||
FemtoVG is used by these projects:
|
||||
|
||||
- pn-editor <https://gitlab.com/porky11/pn-editor>:
|
||||
view, edit, and simulate petri nets
|
||||
- Slint <https://slint.dev>:
|
||||
A declarative GUI toolkit to build native user interfaces for embedded, desktop, and mobile application
|
||||
- inlottie <https://github.com/mhfan/inlottie>:
|
||||
lib/tool in Rust for Lottie (Bodymovin) Animation
|
||||
- Mapvas <https://github.com/UdHo/mapvas>:
|
||||
A map canvas showing OSM tiles with drawing functionality
|
||||
|
||||
## License
|
||||
Licensed under either of
|
||||
* Apache License, Version 2.0 ([LICENSE-APACHE](docs/LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](docs/LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
at your option.
|
||||
|
||||
Fonts used in examples:
|
||||
- Roboto licensed under [Apache license](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- Entypo licensed under CC BY-SA 4.0.
|
||||
- Amiri licensed under [SIL Open Font License, Version 1.1](http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
max_width = 120
|
||||
reorder_modules = true
|
||||
# Only available in +nightly.
|
||||
#imports_layout = "Vertical"
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/// Struct representing a color with red, green, blue, and alpha components.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Color {
|
||||
/// Red component of the color (0.0 to 1.0)
|
||||
pub r: f32,
|
||||
/// Green component of the color (0.0 to 1.0)
|
||||
pub g: f32,
|
||||
/// Blue component of the color (0.0 to 1.0)
|
||||
pub b: f32,
|
||||
/// Alpha (opacity) component of the color (0.0 to 1.0)
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// Creates a color from red, green, and blue u8 values. Alpha is set to 255.
|
||||
pub fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self::rgbf(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
|
||||
}
|
||||
|
||||
/// Creates a color from red, green, and blue f32 values. Alpha is set to 1.0.
|
||||
pub const fn rgbf(r: f32, g: f32, b: f32) -> Self {
|
||||
Self { r, g, b, a: 1.0 }
|
||||
}
|
||||
|
||||
/// Creates a color from red, green, blue, and alpha u8 values.
|
||||
pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self::rgbaf(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)
|
||||
}
|
||||
|
||||
/// Creates a color from red, green, blue, and alpha f32 values.
|
||||
pub const fn rgbaf(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Creates a color from hue, saturation, and lightness f32 values. Alpha is set to 1.0.
|
||||
/// All values are all in range [0..1].
|
||||
pub fn hsl(h: f32, s: f32, l: f32) -> Self {
|
||||
Self::hsla(h, s, l, 1.0)
|
||||
}
|
||||
|
||||
/// Creates a color from hue, saturation, lightness, and alpha f32 values.
|
||||
/// All values are all in range [0..1].
|
||||
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Self {
|
||||
let mut h = h % 1.0;
|
||||
|
||||
if h < 0.0 {
|
||||
h += 1.0;
|
||||
}
|
||||
|
||||
let s = s.clamp(0.0, 1.0);
|
||||
let l = l.clamp(0.0, 1.0);
|
||||
|
||||
let m2 = if l <= 0.5 { l * (1.0 + s) } else { l + s - l * s };
|
||||
let m1 = 2.0 * l - m2;
|
||||
|
||||
Self {
|
||||
r: hue(h + 1.0 / 3.0, m1, m2).clamp(0.0, 1.0),
|
||||
g: hue(h, m1, m2).clamp(0.0, 1.0),
|
||||
b: hue(h - 1.0 / 3.0, m1, m2).clamp(0.0, 1.0),
|
||||
a,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a color from a 6-digit (`RRGGBB`) or 8-digit (`RRGGBBAA`) HTML hexadecimal string.
|
||||
/// Any other length produces `rgb(0,0,0)`.
|
||||
/// The “#” is optional.
|
||||
pub fn hex(raw_hex: &str) -> Self {
|
||||
let hex = raw_hex.trim_start_matches('#');
|
||||
|
||||
if hex.len() == 8 {
|
||||
Self::rgba(
|
||||
hex_to_u8(&hex[0..2]),
|
||||
hex_to_u8(&hex[2..4]),
|
||||
hex_to_u8(&hex[4..6]),
|
||||
hex_to_u8(&hex[6..8]),
|
||||
)
|
||||
} else if hex.len() == 6 {
|
||||
Self::rgb(hex_to_u8(&hex[0..2]), hex_to_u8(&hex[2..4]), hex_to_u8(&hex[4..6]))
|
||||
} else {
|
||||
Self::rgb(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a white color (1.0, 1.0, 1.0, 1.0)
|
||||
pub const fn white() -> Self {
|
||||
Self::rgbaf(1.0, 1.0, 1.0, 1.0)
|
||||
}
|
||||
|
||||
/// Returns a black color (0.0, 0.0, 0.0, 1.0)
|
||||
pub const fn black() -> Self {
|
||||
Self::rgbaf(0.0, 0.0, 0.0, 1.0)
|
||||
}
|
||||
|
||||
/// Sets the alpha (opacity) component of the color from a u8 value.
|
||||
pub fn set_alpha(&mut self, a: u8) {
|
||||
self.set_alphaf(a as f32 / 255.0);
|
||||
}
|
||||
|
||||
/// Sets the alpha (opacity) component of the color from an f32 value.
|
||||
pub fn set_alphaf(&mut self, a: f32) {
|
||||
self.a = a;
|
||||
}
|
||||
|
||||
/// Returns a color with premultiplied alpha components.
|
||||
pub fn premultiplied(self) -> Self {
|
||||
Self {
|
||||
r: self.r * self.a,
|
||||
g: self.g * self.a,
|
||||
b: self.b * self.a,
|
||||
a: self.a,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the color to a [f32; 4] array.
|
||||
pub const fn to_array(self) -> [f32; 4] {
|
||||
[self.r, self.g, self.b, self.a]
|
||||
}
|
||||
|
||||
/// Checks if the color is black (0.0, 0.0, 0.0, 0.0)
|
||||
pub fn is_black(&self) -> bool {
|
||||
self.r == 0.0 && self.g == 0.0 && self.b == 0.0 && self.a == 0.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Color {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hue(mut h: f32, m1: f32, m2: f32) -> f32 {
|
||||
if h < 0.0 {
|
||||
h += 1.0;
|
||||
}
|
||||
if h > 1.0 {
|
||||
h -= 1.0;
|
||||
}
|
||||
|
||||
if h < 1.0 / 6.0 {
|
||||
return m1 + (m2 - m1) * h * 6.0;
|
||||
}
|
||||
if h < 3.0 / 6.0 {
|
||||
return m2;
|
||||
}
|
||||
if h < 4.0 / 6.0 {
|
||||
return m1 + (m2 - m1) * (2.0 / 3.0 - h) * 6.0;
|
||||
}
|
||||
|
||||
m1
|
||||
}
|
||||
|
||||
// Convert a hex string to decimal. Eg. "00" -> 0. "FF" -> 255.
|
||||
fn hex_to_u8(hex_string: &str) -> u8 {
|
||||
u8::from_str_radix(hex_string, 16).unwrap_or(0)
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
ffi::NulError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
};
|
||||
|
||||
/// Enum representing different types of errors that can occur in the canvas.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ErrorKind {
|
||||
/// An unknown error occurred.
|
||||
UnknownError,
|
||||
/// A general error with a custom error message.
|
||||
GeneralError(String),
|
||||
/// An error related to image loading (requires "image-loading" feature).
|
||||
#[cfg(feature = "image-loading")]
|
||||
ImageError(::image::ImageError),
|
||||
/// An I/O error.
|
||||
IoError(io::Error),
|
||||
/// An error occurred while parsing a font.
|
||||
FontParseError,
|
||||
/// No font was found.
|
||||
NoFontFound,
|
||||
/// An error occurred while extracting font information.
|
||||
FontInfoExtractionError,
|
||||
/// The font size is too large for the font atlas.
|
||||
FontSizeTooLargeForAtlas,
|
||||
/// An error occurred while compiling a shader.
|
||||
ShaderCompileError(String),
|
||||
/// An error occurred while linking a shader.
|
||||
ShaderLinkError(String),
|
||||
/// An error related to a render target.
|
||||
RenderTargetError(String),
|
||||
/// The specified image ID was not found.
|
||||
ImageIdNotFound,
|
||||
/// An error occurred while updating an image, as it is out of bounds.
|
||||
ImageUpdateOutOfBounds,
|
||||
/// An error occurred while updating an image with a different format.
|
||||
ImageUpdateWithDifferentFormat,
|
||||
/// The specified image format is not supported.
|
||||
UnsupportedImageFormat,
|
||||
/// The requested operation is not supported (for example screenshot by wgpu renderer).
|
||||
UnsupportedOperation,
|
||||
}
|
||||
|
||||
impl Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "canvas error")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "image-loading")]
|
||||
impl From<::image::ImageError> for ErrorKind {
|
||||
fn from(error: ::image::ImageError) -> Self {
|
||||
Self::ImageError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for ErrorKind {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::IoError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NulError> for ErrorKind {
|
||||
fn from(error: NulError) -> Self {
|
||||
Self::GeneralError(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ErrorKind {}
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign},
|
||||
};
|
||||
|
||||
use fnv::FnvHasher;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Position {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Add<Vector> for Position {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn add(self, other: Vector) -> Self {
|
||||
Self {
|
||||
x: self.x + other.x,
|
||||
y: self.y + other.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Vector> for Position {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, other: Vector) -> Self {
|
||||
Self {
|
||||
x: self.x - other.x,
|
||||
y: self.y - other.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Position {
|
||||
type Output = Vector;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, other: Self) -> Vector {
|
||||
Vector {
|
||||
x: self.x - other.x,
|
||||
y: self.y - other.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub(crate) fn equals(p1: Self, p2: Self, tol: f32) -> bool {
|
||||
(p2 - p1).mag2() < tol * tol
|
||||
}
|
||||
|
||||
pub(crate) fn segment_distance(pos: Self, ppos: Self, qpos: Self) -> f32 {
|
||||
let pq = qpos - ppos;
|
||||
let dpos = pos - ppos;
|
||||
let d = pq.mag2();
|
||||
let mut t = pq.dot(dpos);
|
||||
|
||||
if d > 0.0 {
|
||||
t /= d;
|
||||
}
|
||||
|
||||
t = t.clamp(0.0, 1.0);
|
||||
|
||||
let dpos = (ppos - pos) + pq * t;
|
||||
|
||||
dpos.mag2()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct Vector {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Vector {
|
||||
pub fn zero() -> Self {
|
||||
Self { x: 0.0, y: 0.0 }
|
||||
}
|
||||
|
||||
pub fn x(x: f32) -> Self {
|
||||
Self { x, y: 0.0 }
|
||||
}
|
||||
pub fn y(y: f32) -> Self {
|
||||
Self { x: 0.0, y }
|
||||
}
|
||||
|
||||
pub fn with_basis(self, basis_x: Self, basis_y: Self) -> Self {
|
||||
basis_x * self.x + basis_y * self.y
|
||||
}
|
||||
|
||||
pub fn cross(self, other: Self) -> f32 {
|
||||
self.orthogonal().dot(other)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn dot(self, other: Self) -> f32 {
|
||||
self.x * other.x + self.y * other.y
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mag2(self) -> f32 {
|
||||
self.dot(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn orthogonal(self) -> Self {
|
||||
Self { x: self.y, y: -self.x }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_angle(angle: f32) -> Self {
|
||||
let (y, x) = angle.sin_cos();
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn angle(&self) -> f32 {
|
||||
self.y.atan2(self.x)
|
||||
}
|
||||
|
||||
pub fn normalize(&mut self) -> f32 {
|
||||
let d = self.x.hypot(self.y);
|
||||
|
||||
if d > 1e-6 {
|
||||
let id = 1.0 / d;
|
||||
self.x *= id;
|
||||
self.y *= id;
|
||||
}
|
||||
|
||||
d
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Vector {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
x: self.x + other.x,
|
||||
y: self.y + other.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Vector {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, other: Self) -> Self {
|
||||
Self {
|
||||
x: self.x - other.x,
|
||||
y: self.y - other.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Vector {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn neg(self) -> Self {
|
||||
Self { x: -self.x, y: -self.y }
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<f32> for Vector {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, other: f32) -> Self {
|
||||
Self {
|
||||
x: self.x * other,
|
||||
y: self.y * other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign<f32> for Vector {
|
||||
#[inline]
|
||||
fn mul_assign(&mut self, other: f32) {
|
||||
self.x *= other;
|
||||
self.y *= other;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quantize(a: f32, d: f32) -> f32 {
|
||||
(a / d + 0.5).trunc() * d
|
||||
}
|
||||
|
||||
/// 2×3 matrix (2 rows, 3 columns) used for 2D linear transformations. It can represent transformations such as translation, rotation, or scaling.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Transform2D(pub [f32; 6]);
|
||||
|
||||
impl Transform2D {
|
||||
/// Creates an identity transformation with no translation, rotation, or scaling applied.
|
||||
pub fn identity() -> Self {
|
||||
Self([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
|
||||
}
|
||||
|
||||
/// Creates a new transformation matrix.
|
||||
///
|
||||
/// The parameters are interpreted as matrix elements as follows:
|
||||
/// [a c x]
|
||||
/// [b d y]
|
||||
/// [0 0 1]
|
||||
pub fn new(a: f32, b: f32, c: f32, d: f32, x: f32, y: f32) -> Self {
|
||||
Self([a, b, c, d, x, y])
|
||||
}
|
||||
|
||||
/// Creates a translation transformation matrix.
|
||||
pub fn translation(tx: f32, ty: f32) -> Self {
|
||||
Self([1.0, 0.0, 0.0, 1.0, tx, ty])
|
||||
}
|
||||
|
||||
/// Creates a rotation transformation matrix.
|
||||
pub fn rotation(a: f32) -> Self {
|
||||
let (sin, cos) = a.sin_cos();
|
||||
|
||||
Self([cos, sin, -sin, cos, 0.0, 0.0])
|
||||
}
|
||||
|
||||
/// Creates a scaling transformation matrix.
|
||||
pub fn scaling(sx: f32, sy: f32) -> Self {
|
||||
Self([sx, 0.0, 0.0, sy, 0.0, 0.0])
|
||||
}
|
||||
|
||||
/// Translates the matrix.
|
||||
pub fn translate(&mut self, tx: f32, ty: f32) {
|
||||
let Self([.., x, y]) = self;
|
||||
|
||||
*x += tx;
|
||||
*y += ty;
|
||||
}
|
||||
|
||||
/// Rotates the matrix.
|
||||
pub fn rotate(&mut self, a: f32) {
|
||||
let (sin, cos) = a.sin_cos();
|
||||
|
||||
let Self([a, b, c, d, x, y]) = self;
|
||||
|
||||
[*a, *b] = [*a * cos - *b * sin, *a * sin + *b * cos];
|
||||
[*c, *d] = [*c * cos - *d * sin, *c * sin + *d * cos];
|
||||
[*x, *y] = [*x * cos - *y * sin, *x * sin + *y * cos];
|
||||
}
|
||||
|
||||
/// Scales the matrix.
|
||||
pub fn scale(&mut self, sx: f32, sy: f32) {
|
||||
let Self([a, b, c, d, x, y]) = self;
|
||||
|
||||
*a *= sx;
|
||||
*b *= sy;
|
||||
*c *= sx;
|
||||
*d *= sy;
|
||||
*x *= sx;
|
||||
*y *= sy;
|
||||
}
|
||||
|
||||
/// Skews the matrix horizontally.
|
||||
pub fn skew_x(&mut self, a: f32) {
|
||||
let tan = a.tan();
|
||||
|
||||
let Self([a, b, c, d, x, y]) = self;
|
||||
|
||||
*a += *b * tan;
|
||||
*c += *d * tan;
|
||||
*x += *y * tan;
|
||||
}
|
||||
|
||||
/// Skews the matrix vertically.
|
||||
pub fn skew_y(&mut self, a: f32) {
|
||||
let tan = a.tan();
|
||||
|
||||
let Self([a, b, c, d, x, y]) = self;
|
||||
|
||||
*b += *a * tan;
|
||||
*d += *c * tan;
|
||||
*y += *x * tan;
|
||||
}
|
||||
|
||||
/// Premultiplies the current transformation matrix with another matrix.
|
||||
#[inline]
|
||||
pub fn premultiply(&mut self, other: &Self) {
|
||||
*self = *other * *self;
|
||||
}
|
||||
|
||||
/// Inverts the current transformation matrix.
|
||||
#[inline]
|
||||
pub fn invert(&mut self) {
|
||||
*self = self.inverse()
|
||||
}
|
||||
|
||||
/// Returns the inverse of the current transformation matrix.
|
||||
pub fn inverse(&self) -> Self {
|
||||
let &Self([a, b, c, d, x, y]) = self;
|
||||
let [a, b, c, d, x, y] = [a as f64, b as f64, c as f64, d as f64, x as f64, y as f64];
|
||||
|
||||
let det = a * d - c * b;
|
||||
|
||||
if det > -1e-6 && det < 1e-6 {
|
||||
return Self::identity();
|
||||
}
|
||||
|
||||
let invdet = 1.0 / det;
|
||||
|
||||
Self([
|
||||
(d * invdet) as f32,
|
||||
(-b * invdet) as f32,
|
||||
(-c * invdet) as f32,
|
||||
(a * invdet) as f32,
|
||||
((c * y - d * x) * invdet) as f32,
|
||||
((b * x - a * y) * invdet) as f32,
|
||||
])
|
||||
}
|
||||
|
||||
/// Transforms a point using the current transformation matrix.
|
||||
pub fn transform_point(&self, sx: f32, sy: f32) -> (f32, f32) {
|
||||
let &Self([a, b, c, d, x, y]) = self;
|
||||
|
||||
let dx = sx * a + sy * c + x;
|
||||
let dy = sx * b + sy * d + y;
|
||||
(dx, dy)
|
||||
}
|
||||
|
||||
/// Calculates the average scale factor of the current transformation matrix.
|
||||
pub fn average_scale(&self) -> f32 {
|
||||
let &Self([a, b, c, d, ..]) = self;
|
||||
|
||||
let sx = a.hypot(c);
|
||||
let sy = b.hypot(d);
|
||||
|
||||
(sx + sy) * 0.5
|
||||
}
|
||||
|
||||
/// Returns true if this transform is approximately a pure translation,
|
||||
/// i.e. no rotation, scaling, or skew.
|
||||
pub(crate) fn is_pure_translation(&self, epsilon: f32) -> bool {
|
||||
let &Self([a, b, c, d, ..]) = self;
|
||||
(a - 1.0).abs() < epsilon && b.abs() < epsilon && c.abs() < epsilon && (d - 1.0).abs() < epsilon
|
||||
}
|
||||
|
||||
/// Converts the current transformation matrix to a 3×4 matrix format.
|
||||
pub fn to_mat3x4(self) -> [f32; 12] {
|
||||
let Self([a, b, c, d, x, y]) = self;
|
||||
[a, b, 0.0, 0.0, c, d, 0.0, 0.0, x, y, 1.0, 0.0]
|
||||
}
|
||||
|
||||
/// Generates a cache key for the current transformation matrix.
|
||||
pub fn cache_key(&self) -> u64 {
|
||||
let mut hasher = FnvHasher::default();
|
||||
|
||||
for i in 0..6 {
|
||||
self.0[i].to_bits().hash(&mut hasher);
|
||||
}
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Transform2D {
|
||||
fn default() -> Self {
|
||||
Self::identity()
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for Transform2D {
|
||||
type Output = f32;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<usize> for Transform2D {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
&mut self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Transform2D {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self::Output {
|
||||
let Self([a0, b0, c0, d0, x0, y0]) = self;
|
||||
let Self([a1, b1, c1, d1, x1, y1]) = other;
|
||||
|
||||
Self([a0 + a1, b0 + b1, c0 + c1, d0 + d1, x0 + x1, y0 + y1])
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Transform2D {
|
||||
#[inline]
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
*self = *self + other;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Transform2D {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, other: Self) -> Self::Output {
|
||||
let Self([a0, b0, c0, d0, x0, y0]) = self;
|
||||
let Self([a1, b1, c1, d1, x1, y1]) = other;
|
||||
|
||||
Self([a0 - a1, b0 - b1, c0 - c1, d0 - d1, x0 - x1, y0 - y1])
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign for Transform2D {
|
||||
#[inline]
|
||||
fn sub_assign(&mut self, other: Self) {
|
||||
*self = *self - other;
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for Transform2D {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn mul(mut self, other: Self) -> Self::Output {
|
||||
self *= other;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign for Transform2D {
|
||||
fn mul_assign(&mut self, other: Self) {
|
||||
let Self([a0, b0, c0, d0, x0, y0]) = self;
|
||||
let Self([a1, b1, c1, d1, x1, y1]) = other;
|
||||
|
||||
[*a0, *b0] = [*a0 * a1 + *b0 * c1, *a0 * b1 + *b0 * d1];
|
||||
[*c0, *d0] = [*c0 * a1 + *d0 * c1, *c0 * b1 + *d0 * d1];
|
||||
[*x0, *y0] = [*x0 * a1 + *y0 * c1 + x1, *x0 * b1 + *y0 * d1 + y1];
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for Transform2D {
|
||||
type Output = Self;
|
||||
|
||||
#[allow(clippy::suspicious_arithmetic_impl)]
|
||||
fn div(self, other: Self) -> Self::Output {
|
||||
self * other.inverse()
|
||||
}
|
||||
}
|
||||
|
||||
impl DivAssign for Transform2D {
|
||||
#[inline]
|
||||
fn div_assign(&mut self, other: Self) {
|
||||
*self = *self / other;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Rect {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub w: f32,
|
||||
pub h: f32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
|
||||
Self { x, y, w, h }
|
||||
}
|
||||
|
||||
pub fn intersect(&self, other: Self) -> Self {
|
||||
let minx = self.x.max(other.x);
|
||||
let miny = self.y.max(other.y);
|
||||
let maxx = (self.x + self.w).min(other.x + other.w);
|
||||
let maxy = (self.y + self.h).min(other.y + other.h);
|
||||
|
||||
Self::new(minx, miny, 0.0f32.max(maxx - minx), 0.0f32.max(maxy - miny))
|
||||
}
|
||||
|
||||
pub fn contains_rect(&self, other: &Self) -> bool {
|
||||
other.is_empty()
|
||||
|| (self.x <= other.x
|
||||
&& other.x + other.w <= self.x + self.w
|
||||
&& self.y <= other.y
|
||||
&& other.y + other.h <= self.y + self.h)
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Self) -> Option<Self> {
|
||||
let x = self.x.max(other.x);
|
||||
let y = self.y.max(other.y);
|
||||
let w = (self.x + self.w).min(other.x + other.w) - x;
|
||||
let h = (self.y + self.h).min(other.y + other.h) - y;
|
||||
|
||||
let result = Self { x, y, w, h };
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.w <= 0. || self.h <= 0.
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Bounds {
|
||||
pub minx: f32,
|
||||
pub miny: f32,
|
||||
pub maxx: f32,
|
||||
pub maxy: f32,
|
||||
}
|
||||
|
||||
impl Default for Bounds {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
minx: 1e6,
|
||||
miny: 1e6,
|
||||
maxx: -1e6,
|
||||
maxy: -1e6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
pub(crate) fn contains(&self, x: f32, y: f32) -> bool {
|
||||
(self.minx..=self.maxx).contains(&x) && (self.miny..=self.maxy).contains(&y)
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{
|
||||
image::ImageStore,
|
||||
paint::{GradientStop, MultiStopGradient},
|
||||
Color, ErrorKind, ImageFlags, ImageId, ImageInfo, ImageSource, Renderer,
|
||||
};
|
||||
|
||||
/// `GradientStore` holds image ids for multi-stop gradients. The actual image/textures
|
||||
/// are contained by the Canvas's `ImageStore`.
|
||||
//
|
||||
// If many gradients are used in a frame, we could combine them into a single texture
|
||||
// and update the texture immediately prior to giving the renderer the command list.
|
||||
#[derive(Debug)]
|
||||
pub struct GradientStore {
|
||||
this_frame: BTreeMap<MultiStopGradient, ImageId>,
|
||||
prev_frame: BTreeMap<MultiStopGradient, ImageId>,
|
||||
}
|
||||
|
||||
impl GradientStore {
|
||||
/// Create a new empty gradient store
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
this_frame: BTreeMap::new(),
|
||||
prev_frame: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup or add a multi-stop gradient in this gradient store.
|
||||
pub fn lookup_or_add<R: Renderer>(
|
||||
&mut self,
|
||||
colors: &MultiStopGradient,
|
||||
images: &mut ImageStore<R::Image>,
|
||||
renderer: &mut R,
|
||||
) -> Result<ImageId, ErrorKind> {
|
||||
if let Some(gradient_image_id) = self.prev_frame.remove(colors) {
|
||||
// See if we already have this texture from the previous frame. If we find
|
||||
// it then we migrate it to the current frame so we don't release it and
|
||||
// return the texture id to the caller.
|
||||
self.this_frame.insert(colors.clone(), gradient_image_id);
|
||||
Ok(gradient_image_id)
|
||||
} else if let Some(gradient_image_id) = self.this_frame.get(colors) {
|
||||
// See if we already used this gradient in this frame, and return the texture
|
||||
// id if we do.
|
||||
Ok(*gradient_image_id)
|
||||
} else {
|
||||
// We need to allocate a texture and synthesize the gradient image.
|
||||
let info = ImageInfo::new(ImageFlags::REPEAT_Y, 256, 1, crate::PixelFormat::Rgba8);
|
||||
let gradient_image_id = images.alloc(renderer, info)?;
|
||||
let image = linear_gradient_stops(colors);
|
||||
images.update(renderer, gradient_image_id, ImageSource::Rgba(image.as_ref()), 0, 0)?;
|
||||
|
||||
self.this_frame.insert(colors.clone(), gradient_image_id);
|
||||
Ok(gradient_image_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Release the textures that were not used in the most recently rendered frame. This
|
||||
/// method should be called when all the commands have been submitted.
|
||||
pub fn release_old_gradients<R: Renderer>(&mut self, images: &mut ImageStore<R::Image>, renderer: &mut R) {
|
||||
let mut prev_textures = BTreeMap::new();
|
||||
std::mem::swap(&mut prev_textures, &mut self.prev_frame);
|
||||
for (_, gradient_image_id) in prev_textures {
|
||||
images.remove(renderer, gradient_image_id);
|
||||
}
|
||||
// Move the "this_frame" textures to "prev_frame". "prev_frame" is already empty.
|
||||
std::mem::swap(&mut self.this_frame, &mut self.prev_frame);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
// Gradient filling, adapted from https://github.com/lieff/lvg/blob/master/render/common.c#L147
|
||||
fn gradient_span(dest: &mut [rgb::RGBA8; 256], color0: Color, color1: Color, offset0: f32, offset1: f32) {
|
||||
let s0o = offset0.clamp(0.0, 1.0);
|
||||
let s1o = offset1.clamp(0.0, 1.0);
|
||||
|
||||
if s1o < s0o {
|
||||
return;
|
||||
}
|
||||
|
||||
let s = (s0o * 256.0) as usize;
|
||||
let e = (s1o * 256.0) as usize;
|
||||
|
||||
let mut r = color0.r;
|
||||
let mut g = color0.g;
|
||||
let mut b = color0.b;
|
||||
let mut a = color0.a;
|
||||
|
||||
let steps = (e - s) as f32;
|
||||
|
||||
let dr = (color1.r - r) / steps;
|
||||
let dg = (color1.g - g) / steps;
|
||||
let db = (color1.b - b) / steps;
|
||||
let da = (color1.a - a) / steps;
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in s..e {
|
||||
// The output must be premultiplied, but we don't premultiply until this point
|
||||
// so that we can do gradients from transparent colors correctly -- for example
|
||||
// if we have a stop that is fully transparent red and it transitions to opaque
|
||||
// blue, we should see some red in the gradient. If we premultiply the stops
|
||||
// then we won't see any red, because we will have already multiplied it to zero.
|
||||
// This way we'll get the red contribution.
|
||||
dest[i] = rgb::RGBA8::new(
|
||||
(r * a * 255.0) as u8,
|
||||
(g * a * 255.0) as u8,
|
||||
(b * a * 255.0) as u8,
|
||||
(a * 255.0) as u8,
|
||||
);
|
||||
r += dr;
|
||||
g += dg;
|
||||
b += db;
|
||||
a += da;
|
||||
}
|
||||
}
|
||||
fn linear_gradient_stops(gradient: &MultiStopGradient) -> imgref::Img<Vec<rgb::RGBA8>> {
|
||||
let mut dest = [rgb::RGBA8::new(0, 0, 0, 0); 256];
|
||||
|
||||
// Fill the gradient up to the first stop.
|
||||
let first_stop = gradient.get(0);
|
||||
if first_stop.0 > 0.0 {
|
||||
let s0 = first_stop.0;
|
||||
let color0 = first_stop.1;
|
||||
gradient_span(&mut dest, color0, color0, 0.0, s0);
|
||||
}
|
||||
|
||||
// Iterate over the stops in overlapping pairs and fill out the rest of the
|
||||
// gradient. If the stop position is > 1.0 then we have exhausted the stops
|
||||
// and should break. As a special case, if the second stop is > 1.0 then we
|
||||
// fill the current color to the end of the gradient.
|
||||
for [GradientStop(s0, color0), GradientStop(s1, color1)] in gradient.pairs() {
|
||||
// Catch the case where the last stop doesn't go all the way to 1.0 and
|
||||
// pad it.
|
||||
if s0 < 1.0 && s1 > 1.0 {
|
||||
gradient_span(&mut dest, color0, color0, s0, 1.0);
|
||||
} else {
|
||||
gradient_span(&mut dest, color0, color1, s0, s1);
|
||||
}
|
||||
|
||||
// If the first stop is >1.0 then we're done.
|
||||
if s0 > 1.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
imgref::Img::new(dest.to_vec(), 256, 1)
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
use bitflags::bitflags;
|
||||
use imgref::*;
|
||||
use rgb::alt::Gray;
|
||||
use rgb::*;
|
||||
use slotmap::{DefaultKey, SlotMap};
|
||||
|
||||
#[cfg(feature = "image-loading")]
|
||||
use ::image::DynamicImage;
|
||||
|
||||
#[cfg(feature = "image-loading")]
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::{ErrorKind, Renderer};
|
||||
|
||||
/// An image handle.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct ImageId(DefaultKey);
|
||||
|
||||
/// Specifies the format of an image's pixels.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum PixelFormat {
|
||||
/// 24-bit RGB image format (8 bits per channel)
|
||||
Rgb8,
|
||||
/// 32-bit RGBA image format (8 bits per channel, including alpha)
|
||||
Rgba8,
|
||||
/// 8-bit grayscale image format
|
||||
Gray8,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Represents a set of flags that modify the behavior of an image.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ImageFlags: u32 {
|
||||
/// Generates mipmaps during the creation of the image.
|
||||
const GENERATE_MIPMAPS = 1;
|
||||
/// Repeats the image in the X direction when rendered.
|
||||
const REPEAT_X = 1 << 1;
|
||||
/// Repeats the image in the Y direction when rendered.
|
||||
const REPEAT_Y = 1 << 2;
|
||||
/// Flips (inverses) the image in the Y direction when rendered.
|
||||
const FLIP_Y = 1 << 3;
|
||||
/// Indicates that the image data has premultiplied alpha.
|
||||
const PREMULTIPLIED = 1 << 4;
|
||||
/// Uses nearest-neighbor interpolation instead of linear interpolation when rendering the image.
|
||||
const NEAREST = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the source of an image.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ImageSource<'a> {
|
||||
/// Image source with RGB image format (8 bits per channel)
|
||||
Rgb(ImgRef<'a, RGB8>),
|
||||
/// Image source with RGBA image format (8 bits per channel, including alpha)
|
||||
Rgba(ImgRef<'a, RGBA8>),
|
||||
/// Image source with 8-bit grayscale image format
|
||||
Gray(ImgRef<'a, Gray<u8>>),
|
||||
/// Image source referencing a HTML image element (only available on `wasm32` target)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
HtmlImageElement(&'a web_sys::HtmlImageElement),
|
||||
}
|
||||
|
||||
impl ImageSource<'_> {
|
||||
/// Returns the format of the image source.
|
||||
pub fn format(&self) -> PixelFormat {
|
||||
match self {
|
||||
Self::Rgb(_) => PixelFormat::Rgb8,
|
||||
Self::Rgba(_) => PixelFormat::Rgba8,
|
||||
Self::Gray(_) => PixelFormat::Gray8,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Self::HtmlImageElement(_) => PixelFormat::Rgba8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the dimensions (width and height) of the image source.
|
||||
pub fn dimensions(&self) -> Size {
|
||||
match self {
|
||||
Self::Rgb(imgref) => Size::new(imgref.width(), imgref.height()),
|
||||
Self::Rgba(imgref) => Size::new(imgref.width(), imgref.height()),
|
||||
Self::Gray(imgref) => Size::new(imgref.width(), imgref.height()),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Self::HtmlImageElement(element) => Size::new(element.width() as usize, element.height() as usize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImgRef<'a, RGB8>> for ImageSource<'a> {
|
||||
fn from(src: ImgRef<'a, RGB8>) -> Self {
|
||||
Self::Rgb(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImgRef<'a, RGBA8>> for ImageSource<'a> {
|
||||
fn from(src: ImgRef<'a, RGBA8>) -> Self {
|
||||
Self::Rgba(src)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ImgRef<'a, Gray<u8>>> for ImageSource<'a> {
|
||||
fn from(src: ImgRef<'a, Gray<u8>>) -> Self {
|
||||
Self::Gray(src)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<'a> From<&'a web_sys::HtmlImageElement> for ImageSource<'a> {
|
||||
fn from(src: &'a web_sys::HtmlImageElement) -> Self {
|
||||
Self::HtmlImageElement(src)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "image-loading")]
|
||||
impl<'a> TryFrom<&'a DynamicImage> for ImageSource<'a> {
|
||||
type Error = ErrorKind;
|
||||
|
||||
fn try_from(src: &'a DynamicImage) -> Result<Self, ErrorKind> {
|
||||
Ok(match src {
|
||||
::image::DynamicImage::ImageLuma8(img) => {
|
||||
let src: Img<&[Gray<u8>]> = Img::new(img.as_pixels(), img.width() as usize, img.height() as usize);
|
||||
ImageSource::from(src)
|
||||
}
|
||||
::image::DynamicImage::ImageRgb8(img) => {
|
||||
let src = Img::new(img.as_rgb(), img.width() as usize, img.height() as usize);
|
||||
ImageSource::from(src)
|
||||
}
|
||||
::image::DynamicImage::ImageRgba8(img) => {
|
||||
let src = Img::new(img.as_rgba(), img.width() as usize, img.height() as usize);
|
||||
ImageSource::from(src)
|
||||
}
|
||||
// TODO: if format is not supported maybe we should convert it here,
|
||||
// But that is an expensive operation on the render thread that will remain hidden from the user
|
||||
_ => return Err(ErrorKind::UnsupportedImageFormat),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct Size {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about an image.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct ImageInfo {
|
||||
flags: ImageFlags,
|
||||
size: Size,
|
||||
format: PixelFormat,
|
||||
}
|
||||
|
||||
impl ImageInfo {
|
||||
/// Creates a new `ImageInfo` with the specified flags, width, height, and format.
|
||||
pub fn new(flags: ImageFlags, width: usize, height: usize, format: PixelFormat) -> Self {
|
||||
Self {
|
||||
flags,
|
||||
size: Size { width, height },
|
||||
format,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the image flags.
|
||||
pub fn flags(&self) -> ImageFlags {
|
||||
self.flags
|
||||
}
|
||||
|
||||
/// Returns the image width in pixels.
|
||||
pub fn width(&self) -> usize {
|
||||
self.size.width
|
||||
}
|
||||
|
||||
/// Returns the image height in pixels.
|
||||
pub fn height(&self) -> usize {
|
||||
self.size.height
|
||||
}
|
||||
|
||||
/// Returns the image size (width and height) in pixels.
|
||||
pub fn size(&self) -> Size {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// Returns the image format.
|
||||
pub fn format(&self) -> PixelFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
/// Sets the image format.
|
||||
pub fn set_format(&mut self, format: PixelFormat) {
|
||||
self.format = format;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImageStore<T>(SlotMap<DefaultKey, (ImageInfo, T)>);
|
||||
|
||||
impl<T> Default for ImageStore<T> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ImageStore<T> {
|
||||
pub fn new() -> Self {
|
||||
Self(SlotMap::new())
|
||||
}
|
||||
|
||||
pub fn alloc<R: Renderer<Image = T>>(&mut self, renderer: &mut R, info: ImageInfo) -> Result<ImageId, ErrorKind> {
|
||||
let image = renderer.alloc_image(info)?;
|
||||
Ok(ImageId(self.0.insert((info, image))))
|
||||
}
|
||||
|
||||
pub fn register_native_texture<R: Renderer<Image = T>>(
|
||||
&mut self,
|
||||
renderer: &mut R,
|
||||
texture: R::NativeTexture,
|
||||
info: ImageInfo,
|
||||
) -> Result<ImageId, ErrorKind> {
|
||||
let image = renderer.create_image_from_native_texture(texture, info)?;
|
||||
Ok(ImageId(self.0.insert((info, image))))
|
||||
}
|
||||
|
||||
// Reallocates the image without changing the id.
|
||||
pub fn realloc<R: Renderer<Image = T>>(
|
||||
&mut self,
|
||||
renderer: &mut R,
|
||||
id: ImageId,
|
||||
info: ImageInfo,
|
||||
) -> Result<(), ErrorKind> {
|
||||
if let Some(old) = self.0.get_mut(id.0) {
|
||||
let new = renderer.alloc_image(info)?;
|
||||
old.0 = info;
|
||||
old.1 = new;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ErrorKind::ImageIdNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, id: ImageId) -> Option<&T> {
|
||||
self.0.get(id.0).map(|inner| &inner.1)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: ImageId) -> Option<&mut T> {
|
||||
self.0.get_mut(id.0).map(|inner| &mut inner.1)
|
||||
}
|
||||
|
||||
pub fn update<R: Renderer<Image = T>>(
|
||||
&mut self,
|
||||
renderer: &mut R,
|
||||
id: ImageId,
|
||||
data: ImageSource,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> Result<(), ErrorKind> {
|
||||
if let Some(image) = self.0.get_mut(id.0) {
|
||||
renderer.update_image(&mut image.1, data, x, y)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ErrorKind::ImageIdNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(&self, id: ImageId) -> Option<ImageInfo> {
|
||||
self.0.get(id.0).map(|inner| inner.0)
|
||||
}
|
||||
|
||||
pub fn remove<R: Renderer<Image = T>>(&mut self, renderer: &mut R, id: ImageId) {
|
||||
if let Some(image) = self.0.remove(id.0) {
|
||||
renderer.delete_image(image.1, id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear<R: Renderer<Image = T>>(&mut self, renderer: &mut R) {
|
||||
for (idx, image) in self.0.drain() {
|
||||
renderer.delete_image(image.1, ImageId(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies the type of filter to apply to images with `crate::Canvas::filter_image`.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ImageFilter {
|
||||
/// Applies a Gaussian blur filter with the specified standard deviation.
|
||||
GaussianBlur {
|
||||
/// The standard deviation of the Gaussian blur filter.
|
||||
sigma: f32,
|
||||
},
|
||||
}
|
||||
+1830
File diff suppressed because it is too large.
Load diff
+1185
File diff suppressed because it is too large.
Load diff
+550
@@ -0,0 +1,550 @@
|
||||
use std::{
|
||||
cell::{RefCell, RefMut},
|
||||
f32::consts::PI,
|
||||
slice,
|
||||
};
|
||||
|
||||
use crate::geometry::{Position, Transform2D, Vector};
|
||||
#[cfg(feature = "textlayout")]
|
||||
use rustybuzz::ttf_parser;
|
||||
|
||||
mod cache;
|
||||
pub use cache::{Convexity, PathCache};
|
||||
|
||||
// Length proportional to radius of a cubic bezier handle for 90deg arcs.
|
||||
const KAPPA90: f32 = 0.552_284_8; // 0.552_284_749_3;
|
||||
|
||||
/// Specifies whether a shape is solid or a hole when adding it to a path.
|
||||
///
|
||||
/// The default value is `Solid`.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Default)]
|
||||
pub enum Solidity {
|
||||
/// The shape is solid (filled).
|
||||
#[default]
|
||||
Solid = 1,
|
||||
/// The shape is a hole (not filled).
|
||||
Hole = 2,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum PackedVerb {
|
||||
MoveTo,
|
||||
LineTo,
|
||||
BezierTo,
|
||||
Solid,
|
||||
Hole,
|
||||
Close,
|
||||
}
|
||||
|
||||
/// A verb describes how to interpret one or more points to continue the countour
|
||||
/// of a [`Path`].
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Verb {
|
||||
/// Terminates the current sub-path and defines the new current point by the
|
||||
/// given x/y f32 coordinates.
|
||||
MoveTo(f32, f32),
|
||||
/// Describes that the contour of the path should continue as a line from the
|
||||
/// current point to the given x/y f32 coordinates.
|
||||
LineTo(f32, f32),
|
||||
/// Describes that the contour of the path should continue as a cubie bezier segment from the
|
||||
/// current point via two control points (as f32 pairs) to the point in the last f32 pair.
|
||||
BezierTo(f32, f32, f32, f32, f32, f32),
|
||||
/// Sets the current sub-path winding to be solid.
|
||||
Solid,
|
||||
/// Sets the current sub-path winding to be hole.
|
||||
Hole,
|
||||
/// Closes the current sub-path.
|
||||
Close,
|
||||
}
|
||||
|
||||
impl Verb {
|
||||
fn num_coordinates(&self) -> usize {
|
||||
match *self {
|
||||
Self::MoveTo(..) => 1,
|
||||
Self::LineTo(..) => 1,
|
||||
Self::BezierTo(..) => 3,
|
||||
Self::Solid => 0,
|
||||
Self::Hole => 0,
|
||||
Self::Close => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_packed(packed: &PackedVerb, coords: &[Position]) -> Self {
|
||||
match *packed {
|
||||
PackedVerb::MoveTo => Self::MoveTo(coords[0].x, coords[0].y),
|
||||
PackedVerb::LineTo => Self::LineTo(coords[0].x, coords[0].y),
|
||||
PackedVerb::BezierTo => Self::BezierTo(
|
||||
coords[0].x,
|
||||
coords[0].y,
|
||||
coords[1].x,
|
||||
coords[1].y,
|
||||
coords[2].x,
|
||||
coords[2].y,
|
||||
),
|
||||
PackedVerb::Solid => Self::Solid,
|
||||
PackedVerb::Hole => Self::Hole,
|
||||
PackedVerb::Close => Self::Close,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of verbs (`move_to()`, `line_to()`, `bezier_to()`, etc.)
|
||||
/// describing one or more contours.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Path {
|
||||
verbs: Vec<PackedVerb>,
|
||||
coords: Vec<Position>,
|
||||
last_pos: Position,
|
||||
dist_tol: f32,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pub(crate) cache: RefCell<Option<(u64, PathCache)>>,
|
||||
}
|
||||
|
||||
impl Path {
|
||||
/// Creates a new empty path with a distance tolerance of 0.01.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
dist_tol: 0.01,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the memory size in bytes used by the path.
|
||||
pub fn size(&self) -> usize {
|
||||
std::mem::size_of::<PackedVerb>() * self.verbs.len() + std::mem::size_of::<f32>() * self.coords.len()
|
||||
}
|
||||
|
||||
/// Checks if the path is empty (contains no verbs).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.verbs.is_empty()
|
||||
}
|
||||
|
||||
/// Sets the distance tolerance used for path operations.
|
||||
pub fn set_distance_tolerance(&mut self, value: f32) {
|
||||
self.dist_tol = value;
|
||||
}
|
||||
|
||||
/// Returns an iterator over the path's verbs.
|
||||
pub fn verbs(&self) -> PathIter<'_> {
|
||||
PathIter {
|
||||
verbs: self.verbs.iter(),
|
||||
coords: &self.coords,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cache<'a>(&'a self, transform: &Transform2D, tess_tol: f32, dist_tol: f32) -> RefMut<'a, PathCache> {
|
||||
// The path cache saves a flattened and transformed version of the path. If client code calls
|
||||
// (fill|stroke)_path repeatedly with the same Path under the same transform circumstances then it will be
|
||||
// retrieved from cache. I'm not sure if transform.cache_key() is actually good enough for this
|
||||
// and if it will produce the correct cache keys under different float edge cases.
|
||||
|
||||
let key = transform.cache_key();
|
||||
|
||||
// this shouldn't need a bool once non lexic lifetimes are stable
|
||||
let needs_rebuild = if let Some((transform_cache_key, _cache)) = &*self.cache.borrow() {
|
||||
key != *transform_cache_key
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if needs_rebuild {
|
||||
let path_cache = PathCache::new(self.verbs(), transform, tess_tol, dist_tol);
|
||||
*self.cache.borrow_mut() = Some((key, path_cache));
|
||||
}
|
||||
|
||||
RefMut::map(self.cache.borrow_mut(), |cache| &mut cache.as_mut().unwrap().1)
|
||||
}
|
||||
|
||||
// Path funcs
|
||||
|
||||
/// Starts a new sub-path with the specified point as the first point.
|
||||
pub fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.append(&[PackedVerb::MoveTo], &[Position { x, y }]);
|
||||
}
|
||||
|
||||
/// Adds a line segment from the last point in the path to the specified point.
|
||||
pub fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.append(&[PackedVerb::LineTo], &[Position { x, y }]);
|
||||
}
|
||||
|
||||
/// Adds a cubic bezier segment from the last point in the path via two control points to the specified point.
|
||||
pub fn bezier_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
|
||||
self.append(
|
||||
&[PackedVerb::BezierTo],
|
||||
&[
|
||||
Position { x: c1x, y: c1y },
|
||||
Position { x: c2x, y: c2y },
|
||||
Position { x, y },
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a quadratic bezier segment from the last point in the path via a control point to the specified point.
|
||||
pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
|
||||
let pos0 = self.last_pos;
|
||||
let cpos = Position { x: cx, y: cy };
|
||||
let pos = Position { x, y };
|
||||
let pos1 = pos0 + (cpos - pos0) * (2.0 / 3.0);
|
||||
let pos2 = pos + (cpos - pos) * (2.0 / 3.0);
|
||||
|
||||
self.append(&[PackedVerb::BezierTo], &[pos1, pos2, pos]);
|
||||
}
|
||||
|
||||
/// Closes the current sub-path with a line segment.
|
||||
pub fn close(&mut self) {
|
||||
self.append(&[PackedVerb::Close], &[]);
|
||||
}
|
||||
|
||||
/// Sets the current sub-path winding, see [`Solidity`].
|
||||
pub fn solidity(&mut self, solidity: Solidity) {
|
||||
match solidity {
|
||||
Solidity::Solid => self.append(&[PackedVerb::Solid], &[]),
|
||||
Solidity::Hole => self.append(&[PackedVerb::Hole], &[]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates new circle arc shaped sub-path. The arc center is at `cx`,`cy`, the arc radius is `r`,
|
||||
/// and the arc is drawn from angle `a0` to `a1`, and swept in direction `dir` (Winding)
|
||||
/// Angles are specified in radians.
|
||||
pub fn arc(&mut self, cx: f32, cy: f32, r: f32, a0: f32, a1: f32, dir: Solidity) {
|
||||
let cpos = Position { x: cx, y: cy };
|
||||
|
||||
let mut da = a1 - a0;
|
||||
|
||||
if dir == Solidity::Hole {
|
||||
if da.abs() >= PI * 2.0 {
|
||||
da = PI * 2.0;
|
||||
} else {
|
||||
while da < 0.0 {
|
||||
da += PI * 2.0
|
||||
}
|
||||
}
|
||||
} else if da.abs() >= PI * 2.0 {
|
||||
da = -PI * 2.0;
|
||||
} else {
|
||||
while da > 0.0 {
|
||||
da -= PI * 2.0
|
||||
}
|
||||
}
|
||||
|
||||
// Split arc into max 90 degree segments.
|
||||
let ndivs = ((da.abs() / (PI * 0.5) + 0.5) as i32).clamp(1, 5);
|
||||
let hda = (da / ndivs as f32) / 2.0;
|
||||
let mut kappa = (4.0 / 3.0 * (1.0 - hda.cos()) / hda.sin()).abs();
|
||||
|
||||
let mut commands = Vec::with_capacity(ndivs as usize);
|
||||
let mut coords = Vec::with_capacity(ndivs as usize);
|
||||
|
||||
if dir == Solidity::Solid {
|
||||
kappa = -kappa;
|
||||
}
|
||||
|
||||
let (mut ppos, mut ptanpos) = (Position { x: 0.0, y: 0.0 }, Vector::zero());
|
||||
|
||||
for i in 0..=ndivs {
|
||||
let a = a0 + da * (i as f32 / ndivs as f32);
|
||||
let dpos = Vector::from_angle(a);
|
||||
let pos = cpos + dpos * r;
|
||||
let tanpos = -dpos.orthogonal() * r * kappa;
|
||||
|
||||
if i == 0 {
|
||||
let first_move = if self.verbs.is_empty() {
|
||||
PackedVerb::MoveTo
|
||||
} else {
|
||||
PackedVerb::LineTo
|
||||
};
|
||||
|
||||
commands.push(first_move);
|
||||
coords.extend_from_slice(&[pos]);
|
||||
} else {
|
||||
commands.push(PackedVerb::BezierTo);
|
||||
let pos1 = ppos + ptanpos;
|
||||
let pos2 = pos - tanpos;
|
||||
coords.extend_from_slice(&[pos1, pos2, pos]);
|
||||
}
|
||||
|
||||
ppos = pos;
|
||||
ptanpos = tanpos;
|
||||
}
|
||||
|
||||
self.append(&commands, &coords);
|
||||
}
|
||||
|
||||
/// Adds an arc segment at the corner defined by the last path point and two specified points.
|
||||
pub fn arc_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) {
|
||||
if self.verbs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let pos0 = self.last_pos;
|
||||
let pos1 = Position { x: x1, y: y1 };
|
||||
let pos2 = Position { x: x2, y: y2 };
|
||||
|
||||
// Handle degenerate cases.
|
||||
if Position::equals(pos0, pos1, self.dist_tol)
|
||||
|| Position::equals(pos1, pos2, self.dist_tol)
|
||||
|| Position::segment_distance(pos1, pos0, pos2) < self.dist_tol * self.dist_tol
|
||||
|| radius < self.dist_tol
|
||||
{
|
||||
self.line_to(pos1.x, pos1.y);
|
||||
}
|
||||
|
||||
let mut dpos0 = pos0 - pos1;
|
||||
let mut dpos1 = pos2 - pos1;
|
||||
|
||||
dpos0.normalize();
|
||||
dpos1.normalize();
|
||||
|
||||
let a = dpos0.dot(dpos1).acos();
|
||||
let d = radius / (a / 2.0).tan();
|
||||
|
||||
if d > 10000.0 {
|
||||
return self.line_to(pos1.x, pos1.y);
|
||||
}
|
||||
|
||||
let (cpos, a0, a1, dir);
|
||||
|
||||
if dpos0.cross(dpos1) > 0.0 {
|
||||
cpos = pos1 + dpos0 * d + dpos0.orthogonal() * radius;
|
||||
a0 = dpos0.angle();
|
||||
a1 = (-dpos1).angle();
|
||||
dir = Solidity::Hole;
|
||||
} else {
|
||||
cpos = pos1 + dpos0 * d - dpos0.orthogonal() * radius;
|
||||
a0 = (-dpos0).angle();
|
||||
a1 = dpos1.angle();
|
||||
dir = Solidity::Solid;
|
||||
}
|
||||
|
||||
self.arc(cpos.x, cpos.y, radius, a0 + PI / 2.0, a1 + PI / 2.0, dir);
|
||||
}
|
||||
|
||||
/// Creates a new rectangle shaped sub-path.
|
||||
pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
|
||||
self.append(
|
||||
&[
|
||||
PackedVerb::MoveTo,
|
||||
PackedVerb::LineTo,
|
||||
PackedVerb::LineTo,
|
||||
PackedVerb::LineTo,
|
||||
PackedVerb::Close,
|
||||
],
|
||||
&{
|
||||
let hoffset = Vector::x(w);
|
||||
let voffset = Vector::y(h);
|
||||
|
||||
let tl = Position { x, y };
|
||||
let tr = tl + hoffset;
|
||||
let br = tr + voffset;
|
||||
let bl = tl + voffset;
|
||||
|
||||
[tl, bl, br, tr]
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new rounded rectangle shaped sub-path.
|
||||
pub fn rounded_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
|
||||
self.rounded_rect_varying(x, y, w, h, r, r, r, r);
|
||||
}
|
||||
|
||||
/// Creates a new rounded rectangle shaped sub-path with varying radii for each corner.
|
||||
pub fn rounded_rect_varying(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
rad_top_left: f32,
|
||||
rad_top_right: f32,
|
||||
rad_bottom_right: f32,
|
||||
rad_bottom_left: f32,
|
||||
) {
|
||||
if rad_top_left < 0.1 && rad_top_right < 0.1 && rad_bottom_right < 0.1 && rad_bottom_left < 0.1 {
|
||||
self.rect(x, y, w, h);
|
||||
} else {
|
||||
let halfw = w.abs() * 0.5;
|
||||
let halfh = h.abs() * 0.5;
|
||||
|
||||
let rx_bl = rad_bottom_left.min(halfw) * w.signum();
|
||||
let ry_bl = rad_bottom_left.min(halfh) * h.signum();
|
||||
|
||||
let rx_br = rad_bottom_right.min(halfw) * w.signum();
|
||||
let ry_br = rad_bottom_right.min(halfh) * h.signum();
|
||||
|
||||
let rx_tr = rad_top_right.min(halfw) * w.signum();
|
||||
let ry_tr = rad_top_right.min(halfh) * h.signum();
|
||||
|
||||
let rx_tl = rad_top_left.min(halfw) * w.signum();
|
||||
let ry_tl = rad_top_left.min(halfh) * h.signum();
|
||||
|
||||
self.append(
|
||||
&[
|
||||
PackedVerb::MoveTo,
|
||||
PackedVerb::LineTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::LineTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::LineTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::LineTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::Close,
|
||||
],
|
||||
&[
|
||||
Position { x, y: y + ry_tl },
|
||||
Position { x, y: y + h - ry_bl },
|
||||
//
|
||||
Position {
|
||||
x,
|
||||
y: y + h - ry_bl * (1.0 - KAPPA90),
|
||||
},
|
||||
Position {
|
||||
x: x + rx_bl * (1.0 - KAPPA90),
|
||||
y: y + h,
|
||||
},
|
||||
Position { x: x + rx_bl, y: y + h },
|
||||
//
|
||||
Position {
|
||||
x: x + w - rx_br,
|
||||
y: y + h,
|
||||
},
|
||||
//
|
||||
Position {
|
||||
x: x + w - rx_br * (1.0 - KAPPA90),
|
||||
y: y + h,
|
||||
},
|
||||
Position {
|
||||
x: x + w,
|
||||
y: y + h - ry_br * (1.0 - KAPPA90),
|
||||
},
|
||||
Position {
|
||||
x: x + w,
|
||||
y: y + h - ry_br,
|
||||
},
|
||||
//
|
||||
Position { x: x + w, y: y + ry_tr },
|
||||
//
|
||||
Position {
|
||||
x: x + w,
|
||||
y: y + ry_tr * (1.0 - KAPPA90),
|
||||
},
|
||||
Position {
|
||||
x: x + w - rx_tr * (1.0 - KAPPA90),
|
||||
y,
|
||||
},
|
||||
Position { x: x + w - rx_tr, y },
|
||||
//
|
||||
Position { x: x + rx_tl, y },
|
||||
//
|
||||
Position {
|
||||
x: x + rx_tl * (1.0 - KAPPA90),
|
||||
y,
|
||||
},
|
||||
Position {
|
||||
x,
|
||||
y: y + ry_tl * (1.0 - KAPPA90),
|
||||
},
|
||||
Position { x, y: y + ry_tl },
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new ellipse shaped sub-path.
|
||||
pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) {
|
||||
self.append(
|
||||
&[
|
||||
PackedVerb::MoveTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::BezierTo,
|
||||
PackedVerb::Close,
|
||||
],
|
||||
&{
|
||||
let cpos = Position { x: cx, y: cy };
|
||||
let hoffset = Vector::x(rx);
|
||||
let voffset = Vector::y(ry);
|
||||
[
|
||||
cpos - hoffset,
|
||||
cpos - hoffset + voffset * KAPPA90,
|
||||
cpos - hoffset * KAPPA90 + voffset,
|
||||
cpos + voffset,
|
||||
cpos + hoffset * KAPPA90 + voffset,
|
||||
cpos + hoffset + voffset * KAPPA90,
|
||||
cpos + hoffset,
|
||||
cpos + hoffset - voffset * KAPPA90,
|
||||
cpos + hoffset * KAPPA90 - voffset,
|
||||
cpos - voffset,
|
||||
cpos - hoffset * KAPPA90 - voffset,
|
||||
cpos - hoffset - voffset * KAPPA90,
|
||||
cpos - hoffset,
|
||||
]
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new circle shaped sub-path.
|
||||
pub fn circle(&mut self, cx: f32, cy: f32, r: f32) {
|
||||
self.ellipse(cx, cy, r, r);
|
||||
}
|
||||
|
||||
/// Appends a slice of verbs and coordinates to the path.
|
||||
fn append(&mut self, verbs: &[PackedVerb], coords: &[Position]) {
|
||||
if !coords.is_empty() {
|
||||
self.last_pos = coords[coords.len() - 1];
|
||||
}
|
||||
|
||||
self.verbs.extend_from_slice(verbs);
|
||||
self.coords.extend_from_slice(coords);
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the verbs and coordinates of a path.
|
||||
#[derive(Debug)]
|
||||
pub struct PathIter<'a> {
|
||||
verbs: slice::Iter<'a, PackedVerb>,
|
||||
coords: &'a [Position],
|
||||
}
|
||||
|
||||
impl Iterator for PathIter<'_> {
|
||||
type Item = Verb;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(verb) = self.verbs.next() {
|
||||
let verb = Verb::from_packed(verb, self.coords);
|
||||
let num_coords = verb.num_coordinates();
|
||||
self.coords = &self.coords[num_coords..];
|
||||
Some(verb)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
impl ttf_parser::OutlineBuilder for Path {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.move_to(x, y);
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.line_to(x, y);
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
|
||||
self.quad_to(x1, y1, x, y);
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
|
||||
self.bezier_to(x1, y1, x2, y2, x, y);
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.close();
|
||||
}
|
||||
}
|
||||
+1176
File diff suppressed because it is too large.
Load diff
+257
@@ -0,0 +1,257 @@
|
||||
//! Module containing renderer implementations.
|
||||
|
||||
use imgref::ImgVec;
|
||||
use rgb::RGBA8;
|
||||
|
||||
use crate::{
|
||||
geometry::Position, paint::GlyphTexture, Color, CompositeOperationState, ErrorKind, FillRule, ImageFilter, ImageId,
|
||||
ImageInfo, ImageSource, ImageStore,
|
||||
};
|
||||
|
||||
mod opengl;
|
||||
pub use opengl::OpenGl;
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod wgpu;
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub use wgpu::{WGPURenderOutput, WGPURenderer};
|
||||
|
||||
mod void;
|
||||
pub use void::Void;
|
||||
|
||||
mod params;
|
||||
pub(crate) use params::Params;
|
||||
|
||||
/// Represents a drawable object.
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
pub struct Drawable {
|
||||
pub(crate) fill_verts: Option<(usize, usize)>,
|
||||
pub(crate) stroke_verts: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
/// Defines different types of commands that can be executed by the renderer.
|
||||
#[derive(Debug)]
|
||||
pub enum CommandType {
|
||||
/// Set the render target (screen or image).
|
||||
SetRenderTarget(RenderTarget),
|
||||
/// Clear a rectangle with the specified color.
|
||||
ClearRect {
|
||||
/// Color to fill the rectangle with.
|
||||
color: Color,
|
||||
},
|
||||
/// Fill a convex shape.
|
||||
ConvexFill {
|
||||
/// Rendering parameters for the fill operation.
|
||||
params: Params,
|
||||
},
|
||||
/// Fill a concave shape.
|
||||
ConcaveFill {
|
||||
/// Rendering parameters for the stencil operation.
|
||||
stencil_params: Params,
|
||||
/// Rendering parameters for the fill operation.
|
||||
fill_params: Params,
|
||||
},
|
||||
/// Stroke a shape.
|
||||
Stroke {
|
||||
/// Rendering parameters for the stroke operation.
|
||||
params: Params,
|
||||
},
|
||||
/// Stroke a shape using stencil.
|
||||
StencilStroke {
|
||||
/// Rendering parameters for the first stroke operation.
|
||||
params1: Params,
|
||||
/// Rendering parameters for the second stroke operation.
|
||||
params2: Params,
|
||||
},
|
||||
/// Render triangles.
|
||||
Triangles {
|
||||
/// Rendering parameters for the triangle operation.
|
||||
params: Params,
|
||||
},
|
||||
/// Render a filtered image.
|
||||
RenderFilteredImage {
|
||||
/// ID of the target image.
|
||||
target_image: ImageId,
|
||||
/// Image filter to apply.
|
||||
filter: ImageFilter,
|
||||
},
|
||||
}
|
||||
|
||||
/// Represents a command that can be executed by the renderer.
|
||||
#[derive(Debug)]
|
||||
pub struct Command {
|
||||
pub(crate) cmd_type: CommandType,
|
||||
pub(crate) drawables: Vec<Drawable>,
|
||||
pub(crate) triangles_verts: Option<(usize, usize)>,
|
||||
pub(crate) image: Option<ImageId>,
|
||||
pub(crate) glyph_texture: GlyphTexture,
|
||||
pub(crate) fill_rule: FillRule,
|
||||
pub(crate) composite_operation: CompositeOperationState,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// Creates a new command with the specified command type.
|
||||
pub fn new(flavor: CommandType) -> Self {
|
||||
Self {
|
||||
cmd_type: flavor,
|
||||
drawables: Vec::new(),
|
||||
triangles_verts: None,
|
||||
image: None,
|
||||
glyph_texture: GlyphTexture::default(),
|
||||
fill_rule: FillRule::default(),
|
||||
composite_operation: CompositeOperationState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents different render targets (screen or image).
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum RenderTarget {
|
||||
/// Render to the screen.
|
||||
Screen,
|
||||
/// Render to a specific image.
|
||||
Image(ImageId),
|
||||
}
|
||||
|
||||
/// The main renderer trait that the [Canvas](../struct.Canvas.html) draws to.
|
||||
pub trait Renderer {
|
||||
/// Associated image type.
|
||||
type Image;
|
||||
|
||||
/// Associated native texture type.
|
||||
type NativeTexture;
|
||||
|
||||
/// Associated render output type.
|
||||
type RenderOutput;
|
||||
|
||||
/// Associated type to hold commands created via `flush_to_output`.
|
||||
type CommandBuffer;
|
||||
|
||||
/// Set the size of the renderer.
|
||||
fn set_size(&mut self, width: u32, height: u32, dpi: f32);
|
||||
|
||||
/// Render the specified commands.
|
||||
fn render(
|
||||
&mut self,
|
||||
output: impl Into<Self::RenderOutput>,
|
||||
images: &mut ImageStore<Self::Image>,
|
||||
verts: &[Vertex],
|
||||
commands: Vec<Command>,
|
||||
) -> Self::CommandBuffer;
|
||||
|
||||
/// Allocate a new image with the specified image info.
|
||||
fn alloc_image(&mut self, info: ImageInfo) -> Result<Self::Image, ErrorKind>;
|
||||
|
||||
/// Create a new image from a native texture.
|
||||
fn create_image_from_native_texture(
|
||||
&mut self,
|
||||
native_texture: Self::NativeTexture,
|
||||
info: ImageInfo,
|
||||
) -> Result<Self::Image, ErrorKind>;
|
||||
|
||||
/// Update an image with new data.
|
||||
fn update_image(&mut self, image: &mut Self::Image, data: ImageSource, x: usize, y: usize)
|
||||
-> Result<(), ErrorKind>;
|
||||
|
||||
/// Get the native texture associated with an image (default implementation returns an error).
|
||||
#[allow(unused_variables)]
|
||||
fn get_native_texture(&self, image: &Self::Image) -> Result<Self::NativeTexture, ErrorKind> {
|
||||
Err(ErrorKind::UnsupportedImageFormat)
|
||||
}
|
||||
|
||||
/// Delete an image.
|
||||
fn delete_image(&mut self, image: Self::Image, image_id: ImageId);
|
||||
|
||||
/// Take a screenshot of the current render target.
|
||||
fn screenshot(&mut self) -> Result<ImgVec<RGBA8>, ErrorKind>;
|
||||
}
|
||||
|
||||
/// Marker trait for renderers that don't have a surface.
|
||||
pub trait SurfacelessRenderer: Renderer {
|
||||
/// Render the specified commands.
|
||||
fn render_surfaceless(&mut self, images: &mut ImageStore<Self::Image>, verts: &[Vertex], commands: Vec<Command>);
|
||||
}
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
/// Vertex struct for specifying triangle geometry.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default, Pod, Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct Vertex {
|
||||
/// X-coordinate of the vertex.
|
||||
pub x: f32,
|
||||
/// Y-coordinate of the vertex.
|
||||
pub y: f32,
|
||||
/// U-coordinate of the vertex (for texture mapping).
|
||||
pub u: f32,
|
||||
/// V-coordinate of the vertex (for texture mapping).
|
||||
pub v: f32,
|
||||
}
|
||||
|
||||
impl Vertex {
|
||||
pub(crate) fn pos(position: Position, u: f32, v: f32) -> Self {
|
||||
let Position { x, y } = position;
|
||||
Self { x, y, u, v }
|
||||
}
|
||||
|
||||
/// Create a new vertex with the specified coordinates.
|
||||
pub fn new(x: f32, y: f32, u: f32, v: f32) -> Self {
|
||||
Self { x, y, u, v }
|
||||
}
|
||||
|
||||
/// Set the coordinates of the vertex.
|
||||
pub fn set(&mut self, x: f32, y: f32, u: f32, v: f32) {
|
||||
*self = Self { x, y, u, v };
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents different types of shaders used by the renderer.
|
||||
///
|
||||
/// The default value is `FillGradient`.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
|
||||
pub enum ShaderType {
|
||||
/// Fill gradient shader.
|
||||
#[default]
|
||||
FillGradient,
|
||||
/// Fill image shader.
|
||||
FillImage,
|
||||
/// Stencil shader.
|
||||
Stencil,
|
||||
/// Fill image gradient shader.
|
||||
FillImageGradient,
|
||||
/// Filter image shader.
|
||||
FilterImage,
|
||||
/// Fill color shader.
|
||||
FillColor,
|
||||
/// Texture copy unclipped shader.
|
||||
TextureCopyUnclipped,
|
||||
/// Fill color shader without clipping, used for `clear_rect()`
|
||||
FillColorUnclipped,
|
||||
/// Fill conic gradient shader.
|
||||
FillGradientConic,
|
||||
/// Fill image conic gradient shader.
|
||||
FillImageGradientConic,
|
||||
}
|
||||
|
||||
impl ShaderType {
|
||||
/// Convert the shader type to a u8 value.
|
||||
pub fn to_u8(self) -> u8 {
|
||||
match self {
|
||||
Self::FillGradient => 0,
|
||||
Self::FillImage => 1,
|
||||
Self::Stencil => 2,
|
||||
Self::FillImageGradient => 3,
|
||||
Self::FilterImage => 4,
|
||||
Self::FillColor => 5,
|
||||
Self::TextureCopyUnclipped => 6,
|
||||
Self::FillColorUnclipped => 7,
|
||||
Self::FillGradientConic => 8,
|
||||
Self::FillImageGradientConic => 9,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the shader type to a f32 value.
|
||||
pub fn to_f32(self) -> f32 {
|
||||
self.to_u8() as f32
|
||||
}
|
||||
}
|
||||
+909
@@ -0,0 +1,909 @@
|
||||
use std::{collections::HashMap, mem, rc::Rc};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::ffi::c_void;
|
||||
|
||||
#[cfg(all(feature = "glutin", not(target_arch = "wasm32")))]
|
||||
use glutin::display::GlDisplay;
|
||||
|
||||
use fnv::FnvHashMap;
|
||||
use imgref::ImgVec;
|
||||
use rgb::RGBA8;
|
||||
|
||||
use crate::{
|
||||
renderer::{GlyphTexture, ImageId, Vertex},
|
||||
BlendFactor, Color, CompositeOperationState, ErrorKind, FillRule, ImageFilter, ImageInfo, ImageSource, ImageStore,
|
||||
Scissor, Transform2D,
|
||||
};
|
||||
|
||||
use glow::HasContext;
|
||||
|
||||
use super::{Command, CommandType, Params, RenderTarget, Renderer, ShaderType, SurfacelessRenderer};
|
||||
|
||||
mod program;
|
||||
use program::MainProgram;
|
||||
|
||||
mod gl_texture;
|
||||
use gl_texture::GlTexture;
|
||||
|
||||
mod framebuffer;
|
||||
use framebuffer::Framebuffer;
|
||||
|
||||
mod uniform_array;
|
||||
use uniform_array::UniformArray;
|
||||
|
||||
/// Represents an OpenGL renderer.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug)]
|
||||
pub struct OpenGl {
|
||||
debug: bool,
|
||||
antialias: bool,
|
||||
is_opengles_2_0: bool,
|
||||
view: [f32; 2],
|
||||
screen_view: [f32; 2],
|
||||
// All types of the vertex/fragment shader, indexed by shader_type when has_glyph_texture is true
|
||||
main_programs_with_glyph_texture: [Option<MainProgram>; 10],
|
||||
// Same shader programs but with has_glyph_texture being false
|
||||
main_programs_without_glyph_texture: [Option<MainProgram>; 10],
|
||||
current_program: u8,
|
||||
current_program_needs_glyph_texture: bool,
|
||||
vert_arr: Option<<glow::Context as glow::HasContext>::VertexArray>,
|
||||
vert_buff: Option<<glow::Context as glow::HasContext>::Buffer>,
|
||||
framebuffers: FnvHashMap<ImageId, Result<Framebuffer, ErrorKind>>,
|
||||
context: Rc<glow::Context>,
|
||||
screen_target: Option<Framebuffer>,
|
||||
current_render_target: RenderTarget,
|
||||
}
|
||||
|
||||
impl OpenGl {
|
||||
/// Creates a new OpenGL renderer from a function loader.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is unsafe because it requires a function loader that can load OpenGL functions
|
||||
/// and create a valid OpenGL context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub unsafe fn new_from_function<F>(load_fn: F) -> Result<Self, ErrorKind>
|
||||
where
|
||||
F: FnMut(&str) -> *const c_void,
|
||||
{
|
||||
let context = glow::Context::from_loader_function(load_fn);
|
||||
let version = context.get_parameter_string(glow::VERSION);
|
||||
let is_opengles_2_0 = version.starts_with("OpenGL ES 2.");
|
||||
Self::new_from_context(context, is_opengles_2_0)
|
||||
}
|
||||
|
||||
/// Creates a new OpenGL renderer from a function loader that takes C-style strings.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is unsafe because it requires a function loader that can load OpenGL functions
|
||||
/// and create a valid OpenGL context.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub unsafe fn new_from_function_cstr<F>(load_fn: F) -> Result<Self, ErrorKind>
|
||||
where
|
||||
F: FnMut(&std::ffi::CStr) -> *const c_void,
|
||||
{
|
||||
let context = glow::Context::from_loader_function_cstr(load_fn);
|
||||
let version = context.get_parameter_string(glow::VERSION);
|
||||
let is_opengles_2_0 = version.starts_with("OpenGL ES 2.");
|
||||
Self::new_from_context(context, is_opengles_2_0)
|
||||
}
|
||||
|
||||
/// Creates a new OpenGL renderer from a Glutin display.
|
||||
#[cfg(all(feature = "glutin", not(target_arch = "wasm32")))]
|
||||
pub fn new_from_glutin_display(display: &impl GlDisplay) -> Result<Self, ErrorKind> {
|
||||
unsafe { OpenGl::new_from_function_cstr(|s| display.get_proc_address(s).cast()) }
|
||||
}
|
||||
|
||||
/// Creates a new OpenGL renderer from an HTML canvas element in a WASM32 target.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn new_from_html_canvas(canvas: &web_sys::HtmlCanvasElement) -> Result<Self, ErrorKind> {
|
||||
let attrs = web_sys::WebGlContextAttributes::new();
|
||||
attrs.set_stencil(true);
|
||||
attrs.set_antialias(false);
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
let webgl2_context = match canvas.get_context_with_context_options("webgl2", &attrs) {
|
||||
Ok(Some(context)) => context.dyn_into::<web_sys::WebGl2RenderingContext>().unwrap(),
|
||||
_ => {
|
||||
return Err(ErrorKind::GeneralError(
|
||||
"Canvas::getContext failed to retrieve WebGL 2 context".to_owned(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let context = glow::Context::from_webgl2_context(webgl2_context);
|
||||
Self::new_from_context(context, true)
|
||||
}
|
||||
|
||||
fn new_from_context(context: glow::Context, is_opengles_2_0: bool) -> Result<Self, ErrorKind> {
|
||||
let debug = cfg!(debug_assertions);
|
||||
let antialias = true;
|
||||
|
||||
let context = Rc::new(context);
|
||||
|
||||
let generate_shader_program_variants = |with_glyph_texture| -> Result<_, ErrorKind> {
|
||||
Ok([
|
||||
Some(MainProgram::new(
|
||||
&context,
|
||||
antialias,
|
||||
ShaderType::FillGradient,
|
||||
with_glyph_texture,
|
||||
)?),
|
||||
Some(MainProgram::new(
|
||||
&context,
|
||||
antialias,
|
||||
ShaderType::FillImage,
|
||||
with_glyph_texture,
|
||||
)?),
|
||||
if with_glyph_texture {
|
||||
// No stencil fill with glyph texture
|
||||
None
|
||||
} else {
|
||||
Some(MainProgram::new(&context, antialias, ShaderType::Stencil, false)?)
|
||||
},
|
||||
Some(MainProgram::new(
|
||||
&context,
|
||||
antialias,
|
||||
ShaderType::FillImageGradient,
|
||||
with_glyph_texture,
|
||||
)?),
|
||||
if with_glyph_texture {
|
||||
// Image filter is unrelated to glyph rendering
|
||||
None
|
||||
} else {
|
||||
Some(MainProgram::new(&context, antialias, ShaderType::FilterImage, false)?)
|
||||
},
|
||||
Some(MainProgram::new(
|
||||
&context,
|
||||
antialias,
|
||||
ShaderType::FillColor,
|
||||
with_glyph_texture,
|
||||
)?),
|
||||
if with_glyph_texture {
|
||||
// Texture blitting is unrelated to glyph rendering
|
||||
None
|
||||
} else {
|
||||
Some(MainProgram::new(
|
||||
&context,
|
||||
antialias,
|
||||
ShaderType::TextureCopyUnclipped,
|
||||
false,
|
||||
)?)
|
||||
},
|
||||
None,
|
||||
Some(MainProgram::new(
|
||||
&context,
|
||||
antialias,
|
||||
ShaderType::FillGradientConic,
|
||||
with_glyph_texture,
|
||||
)?),
|
||||
Some(MainProgram::new(
|
||||
&context,
|
||||
antialias,
|
||||
ShaderType::FillImageGradientConic,
|
||||
with_glyph_texture,
|
||||
)?),
|
||||
])
|
||||
};
|
||||
|
||||
let main_programs_with_glyph_texture = generate_shader_program_variants(true)?;
|
||||
let main_programs_without_glyph_texture = generate_shader_program_variants(false)?;
|
||||
|
||||
let mut opengl = Self {
|
||||
debug,
|
||||
antialias,
|
||||
is_opengles_2_0: false,
|
||||
view: [0.0, 0.0],
|
||||
screen_view: [0.0, 0.0],
|
||||
main_programs_with_glyph_texture,
|
||||
main_programs_without_glyph_texture,
|
||||
current_program: 0,
|
||||
current_program_needs_glyph_texture: true,
|
||||
vert_arr: None,
|
||||
vert_buff: None,
|
||||
framebuffers: HashMap::default(),
|
||||
context,
|
||||
screen_target: None,
|
||||
current_render_target: RenderTarget::Screen,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
opengl.is_opengles_2_0 = is_opengles_2_0;
|
||||
|
||||
opengl.vert_arr = opengl.context.create_vertex_array().ok();
|
||||
opengl.vert_buff = opengl.context.create_buffer().ok();
|
||||
}
|
||||
|
||||
Ok(opengl)
|
||||
}
|
||||
|
||||
/// Checks if the renderer is using OpenGL ES.
|
||||
pub fn is_opengles(&self) -> bool {
|
||||
self.is_opengles_2_0
|
||||
}
|
||||
|
||||
fn check_error(&self, label: &str) {
|
||||
if !self.debug {
|
||||
return;
|
||||
}
|
||||
|
||||
let err = unsafe { self.context.get_error() };
|
||||
|
||||
if err == glow::NO_ERROR {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = match err {
|
||||
glow::INVALID_ENUM => "Invalid enum",
|
||||
glow::INVALID_VALUE => "Invalid value",
|
||||
glow::INVALID_OPERATION => "Invalid operation",
|
||||
glow::OUT_OF_MEMORY => "Out of memory",
|
||||
glow::INVALID_FRAMEBUFFER_OPERATION => "Invalid framebuffer operation",
|
||||
_ => "Unknown error",
|
||||
};
|
||||
|
||||
log::error!("({err}) Error on {label} - {message}");
|
||||
}
|
||||
|
||||
fn gl_factor(factor: BlendFactor) -> u32 {
|
||||
match factor {
|
||||
BlendFactor::Zero => glow::ZERO,
|
||||
BlendFactor::One => glow::ONE,
|
||||
BlendFactor::SrcColor => glow::SRC_COLOR,
|
||||
BlendFactor::OneMinusSrcColor => glow::ONE_MINUS_SRC_COLOR,
|
||||
BlendFactor::DstColor => glow::DST_COLOR,
|
||||
BlendFactor::OneMinusDstColor => glow::ONE_MINUS_DST_COLOR,
|
||||
BlendFactor::SrcAlpha => glow::SRC_ALPHA,
|
||||
BlendFactor::OneMinusSrcAlpha => glow::ONE_MINUS_SRC_ALPHA,
|
||||
BlendFactor::DstAlpha => glow::DST_ALPHA,
|
||||
BlendFactor::OneMinusDstAlpha => glow::ONE_MINUS_DST_ALPHA,
|
||||
BlendFactor::SrcAlphaSaturate => glow::SRC_ALPHA_SATURATE,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_composite_operation(&self, blend_state: CompositeOperationState) {
|
||||
unsafe {
|
||||
self.context.blend_func_separate(
|
||||
Self::gl_factor(blend_state.src_rgb),
|
||||
Self::gl_factor(blend_state.dst_rgb),
|
||||
Self::gl_factor(blend_state.src_alpha),
|
||||
Self::gl_factor(blend_state.dst_alpha),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn convex_fill(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, gpu_paint: &Params) {
|
||||
self.set_uniforms(images, gpu_paint, cmd.image, cmd.glyph_texture);
|
||||
|
||||
for drawable in &cmd.drawables {
|
||||
if let Some((start, count)) = drawable.fill_verts {
|
||||
unsafe {
|
||||
self.context.draw_arrays(glow::TRIANGLES, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((start, count)) = drawable.stroke_verts {
|
||||
unsafe {
|
||||
self.context
|
||||
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.check_error("convex_fill");
|
||||
}
|
||||
|
||||
fn concave_fill(
|
||||
&mut self,
|
||||
images: &ImageStore<GlTexture>,
|
||||
cmd: &Command,
|
||||
stencil_paint: &Params,
|
||||
fill_paint: &Params,
|
||||
) {
|
||||
unsafe {
|
||||
self.context.enable(glow::STENCIL_TEST);
|
||||
self.context.stencil_mask(0xff);
|
||||
self.context.stencil_func(glow::ALWAYS, 0, 0xff);
|
||||
self.context.color_mask(false, false, false, false);
|
||||
//glow::DepthMask(glow::FALSE);
|
||||
}
|
||||
|
||||
self.set_uniforms(images, stencil_paint, None, GlyphTexture::None);
|
||||
|
||||
unsafe {
|
||||
self.context
|
||||
.stencil_op_separate(glow::FRONT, glow::KEEP, glow::KEEP, glow::INCR_WRAP);
|
||||
self.context
|
||||
.stencil_op_separate(glow::BACK, glow::KEEP, glow::KEEP, glow::DECR_WRAP);
|
||||
self.context.disable(glow::CULL_FACE);
|
||||
}
|
||||
|
||||
for drawable in &cmd.drawables {
|
||||
if let Some((start, count)) = drawable.fill_verts {
|
||||
unsafe {
|
||||
self.context.draw_arrays(glow::TRIANGLES, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
self.context.enable(glow::CULL_FACE);
|
||||
// Draw anti-aliased pixels
|
||||
self.context.color_mask(true, true, true, true);
|
||||
//glow::DepthMask(glow::TRUE);
|
||||
}
|
||||
|
||||
self.set_uniforms(images, fill_paint, cmd.image, cmd.glyph_texture);
|
||||
|
||||
if self.antialias {
|
||||
unsafe {
|
||||
match cmd.fill_rule {
|
||||
FillRule::NonZero => self.context.stencil_func(glow::EQUAL, 0x0, 0xff),
|
||||
FillRule::EvenOdd => self.context.stencil_func(glow::EQUAL, 0x0, 0x1),
|
||||
}
|
||||
|
||||
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
|
||||
}
|
||||
|
||||
// draw fringes
|
||||
for drawable in &cmd.drawables {
|
||||
if let Some((start, count)) = drawable.stroke_verts {
|
||||
unsafe {
|
||||
self.context
|
||||
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
match cmd.fill_rule {
|
||||
FillRule::NonZero => self.context.stencil_func(glow::NOTEQUAL, 0x0, 0xff),
|
||||
FillRule::EvenOdd => self.context.stencil_func(glow::NOTEQUAL, 0x0, 0x1),
|
||||
}
|
||||
|
||||
self.context.stencil_op(glow::ZERO, glow::ZERO, glow::ZERO);
|
||||
|
||||
if let Some((start, count)) = cmd.triangles_verts {
|
||||
self.context
|
||||
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
|
||||
}
|
||||
|
||||
self.context.disable(glow::STENCIL_TEST);
|
||||
}
|
||||
|
||||
self.check_error("concave_fill");
|
||||
}
|
||||
|
||||
fn stroke(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, paint: &Params) {
|
||||
self.set_uniforms(images, paint, cmd.image, cmd.glyph_texture);
|
||||
|
||||
for drawable in &cmd.drawables {
|
||||
if let Some((start, count)) = drawable.stroke_verts {
|
||||
unsafe {
|
||||
self.context
|
||||
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.check_error("stroke");
|
||||
}
|
||||
|
||||
fn stencil_stroke(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, paint1: &Params, paint2: &Params) {
|
||||
unsafe {
|
||||
self.context.enable(glow::STENCIL_TEST);
|
||||
self.context.stencil_mask(0xff);
|
||||
|
||||
// Fill the stroke base without overlap
|
||||
self.context.stencil_func(glow::EQUAL, 0x0, 0xff);
|
||||
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::INCR);
|
||||
}
|
||||
|
||||
self.set_uniforms(images, paint2, cmd.image, cmd.glyph_texture);
|
||||
|
||||
for drawable in &cmd.drawables {
|
||||
if let Some((start, count)) = drawable.stroke_verts {
|
||||
unsafe {
|
||||
self.context
|
||||
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw anti-aliased pixels.
|
||||
self.set_uniforms(images, paint1, cmd.image, cmd.glyph_texture);
|
||||
|
||||
unsafe {
|
||||
self.context.stencil_func(glow::EQUAL, 0x0, 0xff);
|
||||
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
|
||||
}
|
||||
|
||||
for drawable in &cmd.drawables {
|
||||
if let Some((start, count)) = drawable.stroke_verts {
|
||||
unsafe {
|
||||
self.context
|
||||
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Clear stencil buffer.
|
||||
self.context.color_mask(false, false, false, false);
|
||||
self.context.stencil_func(glow::ALWAYS, 0x0, 0xff);
|
||||
self.context.stencil_op(glow::ZERO, glow::ZERO, glow::ZERO);
|
||||
}
|
||||
|
||||
for drawable in &cmd.drawables {
|
||||
if let Some((start, count)) = drawable.stroke_verts {
|
||||
unsafe {
|
||||
self.context
|
||||
.draw_arrays(glow::TRIANGLE_STRIP, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
self.context.color_mask(true, true, true, true);
|
||||
self.context.disable(glow::STENCIL_TEST);
|
||||
}
|
||||
|
||||
self.check_error("stencil_stroke");
|
||||
}
|
||||
|
||||
fn triangles(&mut self, images: &ImageStore<GlTexture>, cmd: &Command, paint: &Params) {
|
||||
self.set_uniforms(images, paint, cmd.image, cmd.glyph_texture);
|
||||
|
||||
if let Some((start, count)) = cmd.triangles_verts {
|
||||
unsafe {
|
||||
self.context.draw_arrays(glow::TRIANGLES, start as i32, count as i32);
|
||||
}
|
||||
}
|
||||
|
||||
self.check_error("triangles");
|
||||
}
|
||||
|
||||
fn set_uniforms(
|
||||
&mut self,
|
||||
images: &ImageStore<GlTexture>,
|
||||
paint: &Params,
|
||||
image_tex: Option<ImageId>,
|
||||
glyph_tex: GlyphTexture,
|
||||
) {
|
||||
self.select_main_program(paint);
|
||||
let arr = UniformArray::from(paint);
|
||||
self.main_program().set_config(arr.as_slice());
|
||||
self.check_error("set_uniforms uniforms");
|
||||
|
||||
let tex = image_tex.and_then(|id| images.get(id)).map(GlTexture::id);
|
||||
|
||||
unsafe {
|
||||
self.context.active_texture(glow::TEXTURE0);
|
||||
self.context.bind_texture(glow::TEXTURE_2D, tex);
|
||||
}
|
||||
|
||||
let glyphtex = glyph_tex.image_id().and_then(|id| images.get(id).map(GlTexture::id));
|
||||
|
||||
unsafe {
|
||||
self.context.active_texture(glow::TEXTURE0 + 1);
|
||||
self.context.bind_texture(glow::TEXTURE_2D, glyphtex);
|
||||
}
|
||||
|
||||
self.check_error("set_uniforms texture");
|
||||
}
|
||||
|
||||
fn clear_rect(&self, x: u32, y: u32, width: u32, height: u32, color: Color) {
|
||||
unsafe {
|
||||
self.context.enable(glow::SCISSOR_TEST);
|
||||
self.context.scissor(
|
||||
x as i32,
|
||||
self.view[1] as i32 - (height as i32 + y as i32),
|
||||
width as i32,
|
||||
height as i32,
|
||||
);
|
||||
self.context.clear_color(color.r, color.g, color.b, color.a);
|
||||
self.context.clear(glow::COLOR_BUFFER_BIT | glow::STENCIL_BUFFER_BIT);
|
||||
self.context.disable(glow::SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_target(&mut self, images: &ImageStore<GlTexture>, target: RenderTarget) {
|
||||
self.current_render_target = target;
|
||||
match (target, &self.screen_target) {
|
||||
(RenderTarget::Screen, None) => unsafe {
|
||||
Framebuffer::unbind(&self.context);
|
||||
self.view = self.screen_view;
|
||||
self.context.viewport(0, 0, self.view[0] as i32, self.view[1] as i32);
|
||||
},
|
||||
(RenderTarget::Screen, Some(framebuffer)) => {
|
||||
framebuffer.bind();
|
||||
self.view = self.screen_view;
|
||||
unsafe {
|
||||
self.context.viewport(0, 0, self.view[0] as i32, self.view[1] as i32);
|
||||
}
|
||||
}
|
||||
(RenderTarget::Image(id), _) => {
|
||||
let context = self.context.clone();
|
||||
if let Some(texture) = images.get(id) {
|
||||
if let Ok(fb) = self
|
||||
.framebuffers
|
||||
.entry(id)
|
||||
.or_insert_with(|| Framebuffer::new(&context, texture))
|
||||
{
|
||||
fb.bind();
|
||||
|
||||
self.view[0] = texture.info().width() as f32;
|
||||
self.view[1] = texture.info().height() as f32;
|
||||
|
||||
unsafe {
|
||||
self.context
|
||||
.viewport(0, 0, texture.info().width() as i32, texture.info().height() as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the "Screen" `RenderTarget` actually render to a framebuffer object. This is useful when
|
||||
/// embedding femtovg into another program where final composition is handled by an external task.
|
||||
/// The given `framebuffer_object` must refer to a Framebuffer Object created on the current OpenGL
|
||||
/// Context, and must have a depth & stencil attachment.
|
||||
///
|
||||
/// Pass `None` to clear any previous Framebuffer Object ID that was passed and target rendering to
|
||||
/// the default target (normally the window).
|
||||
pub fn set_screen_target(&mut self, framebuffer_object: Option<<glow::Context as glow::HasContext>::Framebuffer>) {
|
||||
match framebuffer_object {
|
||||
Some(fbo_id) => self.screen_target = Some(Framebuffer::from_external(&self.context, fbo_id)),
|
||||
None => self.screen_target = None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_filtered_image(
|
||||
&mut self,
|
||||
images: &mut ImageStore<GlTexture>,
|
||||
cmd: Command,
|
||||
target_image: ImageId,
|
||||
filter: ImageFilter,
|
||||
) {
|
||||
match filter {
|
||||
ImageFilter::GaussianBlur { sigma } => self.render_gaussian_blur(images, cmd, target_image, sigma),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_gaussian_blur(
|
||||
&mut self,
|
||||
images: &mut ImageStore<GlTexture>,
|
||||
mut cmd: Command,
|
||||
target_image: ImageId,
|
||||
sigma: f32,
|
||||
) {
|
||||
let original_render_target = self.current_render_target;
|
||||
|
||||
// The filtering happens in two passes, first a horizontal blur and then the vertial blur. The
|
||||
// first pass therefore renders into an intermediate, temporarily allocated texture.
|
||||
|
||||
let source_image_info = images.get(cmd.image.unwrap()).unwrap().info();
|
||||
|
||||
let image_paint = crate::Paint::image(
|
||||
cmd.image.unwrap(),
|
||||
0.,
|
||||
0.,
|
||||
source_image_info.width() as _,
|
||||
source_image_info.height() as _,
|
||||
0.,
|
||||
1.,
|
||||
);
|
||||
let mut blur_params = Params::new(
|
||||
images,
|
||||
&Transform2D::default(),
|
||||
&image_paint.flavor,
|
||||
&GlyphTexture::default(),
|
||||
&Scissor::default(),
|
||||
0.,
|
||||
0.,
|
||||
0.,
|
||||
);
|
||||
blur_params.shader_type = ShaderType::FilterImage;
|
||||
|
||||
let gauss_coeff_x = 1. / ((2. * std::f32::consts::PI).sqrt() * sigma);
|
||||
let gauss_coeff_y = f32::exp(-0.5 / (sigma * sigma));
|
||||
let gauss_coeff_z = gauss_coeff_y * gauss_coeff_y;
|
||||
|
||||
blur_params.image_blur_filter_coeff[0] = gauss_coeff_x;
|
||||
blur_params.image_blur_filter_coeff[1] = gauss_coeff_y;
|
||||
blur_params.image_blur_filter_coeff[2] = gauss_coeff_z;
|
||||
|
||||
blur_params.image_blur_filter_direction = [1.0, 0.0];
|
||||
|
||||
// GLES 2.0 does not allow non-constant loop indices, so limit the standard devitation to allow for a upper fixed limit
|
||||
// on the number of iterations in the fragment shader.
|
||||
blur_params.image_blur_filter_sigma = sigma.min(8.);
|
||||
|
||||
let horizontal_blur_buffer = images.alloc(self, source_image_info).unwrap();
|
||||
self.set_target(images, RenderTarget::Image(horizontal_blur_buffer));
|
||||
self.main_program().set_view(self.view);
|
||||
|
||||
self.clear_rect(
|
||||
0,
|
||||
0,
|
||||
source_image_info.width() as _,
|
||||
source_image_info.height() as _,
|
||||
Color::rgbaf(0., 0., 0., 0.),
|
||||
);
|
||||
|
||||
self.triangles(images, &cmd, &blur_params);
|
||||
|
||||
self.set_target(images, RenderTarget::Image(target_image));
|
||||
self.main_program().set_view(self.view);
|
||||
|
||||
self.clear_rect(
|
||||
0,
|
||||
0,
|
||||
source_image_info.width() as _,
|
||||
source_image_info.height() as _,
|
||||
Color::rgbaf(0., 0., 0., 0.),
|
||||
);
|
||||
|
||||
blur_params.image_blur_filter_direction = [0.0, 1.0];
|
||||
|
||||
cmd.image = Some(horizontal_blur_buffer);
|
||||
|
||||
self.triangles(images, &cmd, &blur_params);
|
||||
|
||||
images.remove(self, horizontal_blur_buffer);
|
||||
|
||||
// restore previous render target and view
|
||||
self.set_target(images, original_render_target);
|
||||
self.main_program().set_view(self.view);
|
||||
}
|
||||
|
||||
fn main_program(&self) -> &MainProgram {
|
||||
let programs = if self.current_program_needs_glyph_texture {
|
||||
&self.main_programs_with_glyph_texture
|
||||
} else {
|
||||
&self.main_programs_without_glyph_texture
|
||||
};
|
||||
programs[self.current_program as usize]
|
||||
.as_ref()
|
||||
.expect("internal error: invalid shader program selected for given paint")
|
||||
}
|
||||
|
||||
fn select_main_program(&mut self, params: &Params) {
|
||||
let program_index = params.shader_type.to_u8();
|
||||
if program_index != self.current_program
|
||||
|| params.uses_glyph_texture() != self.current_program_needs_glyph_texture
|
||||
{
|
||||
unsafe {
|
||||
self.context.active_texture(glow::TEXTURE0);
|
||||
self.context.bind_texture(glow::TEXTURE_2D, None);
|
||||
self.context.active_texture(glow::TEXTURE0 + 1);
|
||||
self.context.bind_texture(glow::TEXTURE_2D, None);
|
||||
}
|
||||
|
||||
self.main_program().unbind();
|
||||
self.current_program = program_index;
|
||||
self.current_program_needs_glyph_texture = params.uses_glyph_texture();
|
||||
|
||||
let program = self.main_program();
|
||||
program.bind();
|
||||
// Bind the two uniform samplers to texture units
|
||||
program.set_tex(0);
|
||||
program.set_glyphtex(1);
|
||||
program.set_view(self.view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for OpenGl {
|
||||
type Image = GlTexture;
|
||||
type NativeTexture = <glow::Context as glow::HasContext>::Texture;
|
||||
type RenderOutput = ();
|
||||
type CommandBuffer = ();
|
||||
|
||||
fn set_size(&mut self, width: u32, height: u32, _dpi: f32) {
|
||||
self.view[0] = width as f32;
|
||||
self.view[1] = height as f32;
|
||||
|
||||
self.screen_view = self.view;
|
||||
|
||||
unsafe {
|
||||
self.context.viewport(0, 0, width as i32, height as i32);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_native_texture(&self, image: &Self::Image) -> Result<Self::NativeTexture, ErrorKind> {
|
||||
Ok(image.id())
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
_output: impl Into<Self::RenderOutput>,
|
||||
images: &mut ImageStore<Self::Image>,
|
||||
verts: &[Vertex],
|
||||
commands: Vec<Command>,
|
||||
) {
|
||||
self.current_program = 0;
|
||||
self.main_program().bind();
|
||||
|
||||
unsafe {
|
||||
self.context.enable(glow::CULL_FACE);
|
||||
|
||||
self.context.cull_face(glow::BACK);
|
||||
self.context.front_face(glow::CCW);
|
||||
self.context.enable(glow::BLEND);
|
||||
self.context.disable(glow::DEPTH_TEST);
|
||||
self.context.disable(glow::SCISSOR_TEST);
|
||||
self.context.color_mask(true, true, true, true);
|
||||
self.context.stencil_mask(0xffff_ffff);
|
||||
self.context.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
|
||||
self.context.stencil_func(glow::ALWAYS, 0, 0xffff_ffff);
|
||||
self.context.active_texture(glow::TEXTURE0);
|
||||
self.context.bind_texture(glow::TEXTURE_2D, None);
|
||||
self.context.active_texture(glow::TEXTURE0 + 1);
|
||||
self.context.bind_texture(glow::TEXTURE_2D, None);
|
||||
|
||||
self.context.bind_vertex_array(self.vert_arr);
|
||||
|
||||
let vertex_size = mem::size_of::<Vertex>();
|
||||
|
||||
self.context.bind_buffer(glow::ARRAY_BUFFER, self.vert_buff);
|
||||
self.context
|
||||
.buffer_data_u8_slice(glow::ARRAY_BUFFER, verts.align_to().1, glow::STREAM_DRAW);
|
||||
|
||||
self.context.enable_vertex_attrib_array(0);
|
||||
self.context.enable_vertex_attrib_array(1);
|
||||
|
||||
self.context
|
||||
.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, vertex_size as i32, 0);
|
||||
self.context.vertex_attrib_pointer_f32(
|
||||
1,
|
||||
2,
|
||||
glow::FLOAT,
|
||||
false,
|
||||
vertex_size as i32,
|
||||
2 * mem::size_of::<f32>() as i32,
|
||||
);
|
||||
}
|
||||
|
||||
self.check_error("render prepare");
|
||||
|
||||
for cmd in commands {
|
||||
self.set_composite_operation(cmd.composite_operation);
|
||||
|
||||
match cmd.cmd_type {
|
||||
CommandType::ConvexFill { ref params } => self.convex_fill(images, &cmd, params),
|
||||
CommandType::ConcaveFill {
|
||||
ref stencil_params,
|
||||
ref fill_params,
|
||||
} => self.concave_fill(images, &cmd, stencil_params, fill_params),
|
||||
CommandType::Stroke { ref params } => self.stroke(images, &cmd, params),
|
||||
CommandType::StencilStroke {
|
||||
ref params1,
|
||||
ref params2,
|
||||
} => self.stencil_stroke(images, &cmd, params1, params2),
|
||||
CommandType::Triangles { ref params } => self.triangles(images, &cmd, params),
|
||||
CommandType::ClearRect { color } => {
|
||||
if let Some((start, _)) = cmd.triangles_verts {
|
||||
let x = verts[start].x as _;
|
||||
let y = verts[start].y as _;
|
||||
let width = verts[start + 1].x as u32 - x;
|
||||
let height = verts[start + 1].y as u32 - y;
|
||||
self.clear_rect(x, y, width, height, color);
|
||||
}
|
||||
}
|
||||
CommandType::SetRenderTarget(target) => {
|
||||
self.set_target(images, target);
|
||||
self.main_program().set_view(self.view);
|
||||
}
|
||||
CommandType::RenderFilteredImage { target_image, filter } => {
|
||||
self.render_filtered_image(images, cmd, target_image, filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
self.context.disable_vertex_attrib_array(0);
|
||||
self.context.disable_vertex_attrib_array(1);
|
||||
self.context.bind_vertex_array(None);
|
||||
|
||||
self.context.disable(glow::CULL_FACE);
|
||||
self.context.bind_buffer(glow::ARRAY_BUFFER, None);
|
||||
self.context.bind_texture(glow::TEXTURE_2D, None);
|
||||
}
|
||||
|
||||
self.main_program().unbind();
|
||||
|
||||
self.check_error("render done");
|
||||
}
|
||||
|
||||
fn alloc_image(&mut self, info: ImageInfo) -> Result<Self::Image, ErrorKind> {
|
||||
Self::Image::new(&self.context, info, self.is_opengles_2_0)
|
||||
}
|
||||
|
||||
fn create_image_from_native_texture(
|
||||
&mut self,
|
||||
native_texture: Self::NativeTexture,
|
||||
info: ImageInfo,
|
||||
) -> Result<Self::Image, ErrorKind> {
|
||||
Ok(Self::Image::new_from_native_texture(native_texture, info))
|
||||
}
|
||||
|
||||
fn update_image(
|
||||
&mut self,
|
||||
image: &mut Self::Image,
|
||||
data: ImageSource,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> Result<(), ErrorKind> {
|
||||
image.update(&self.context, data, x, y, self.is_opengles_2_0)
|
||||
}
|
||||
|
||||
fn delete_image(&mut self, image: Self::Image, image_id: ImageId) {
|
||||
self.framebuffers.remove(&image_id);
|
||||
image.delete(&self.context);
|
||||
}
|
||||
|
||||
fn screenshot(&mut self) -> Result<ImgVec<RGBA8>, ErrorKind> {
|
||||
//let mut image = image::RgbaImage::new(self.view[0] as u32, self.view[1] as u32);
|
||||
let w = self.view[0] as usize;
|
||||
let h = self.view[1] as usize;
|
||||
|
||||
let mut image = ImgVec::new(
|
||||
vec![
|
||||
RGBA8 {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
a: 255
|
||||
};
|
||||
w * h
|
||||
],
|
||||
w,
|
||||
h,
|
||||
);
|
||||
|
||||
unsafe {
|
||||
self.context.read_pixels(
|
||||
0,
|
||||
0,
|
||||
self.view[0] as i32,
|
||||
self.view[1] as i32,
|
||||
glow::RGBA,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelPackData::Slice(Some(image.buf_mut().align_to_mut().1)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut flipped = Vec::with_capacity(w * h);
|
||||
|
||||
for row in image.rows().rev() {
|
||||
flipped.extend_from_slice(row);
|
||||
}
|
||||
|
||||
Ok(ImgVec::new(flipped, w, h))
|
||||
}
|
||||
}
|
||||
|
||||
impl SurfacelessRenderer for OpenGl {
|
||||
fn render_surfaceless(&mut self, images: &mut ImageStore<Self::Image>, verts: &[Vertex], commands: Vec<Command>) {
|
||||
self.render((), images, verts, commands)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OpenGl {
|
||||
fn drop(&mut self) {
|
||||
if let Some(vert_arr) = self.vert_arr {
|
||||
unsafe {
|
||||
self.context.delete_vertex_array(vert_arr);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(vert_buff) = self.vert_buff {
|
||||
unsafe {
|
||||
self.context.delete_buffer(vert_buff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use glow::HasContext;
|
||||
|
||||
use crate::ErrorKind;
|
||||
|
||||
use super::GlTexture;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Framebuffer {
|
||||
context: Rc<glow::Context>,
|
||||
fbo: <glow::Context as glow::HasContext>::Framebuffer,
|
||||
stencil_rbo: Option<<glow::Context as glow::HasContext>::Renderbuffer>,
|
||||
}
|
||||
|
||||
impl Framebuffer {
|
||||
pub fn from_external(context: &Rc<glow::Context>, fbo: <glow::Context as glow::HasContext>::Framebuffer) -> Self {
|
||||
Self {
|
||||
context: context.clone(),
|
||||
fbo,
|
||||
stencil_rbo: None,
|
||||
}
|
||||
}
|
||||
pub fn new(context: &Rc<glow::Context>, texture: &GlTexture) -> Result<Self, ErrorKind> {
|
||||
let fbo = unsafe { context.create_framebuffer().unwrap() };
|
||||
unsafe {
|
||||
context.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
|
||||
}
|
||||
|
||||
let width = texture.info().width() as u32;
|
||||
let height = texture.info().height() as u32;
|
||||
|
||||
unsafe {
|
||||
context.framebuffer_texture_2d(
|
||||
glow::FRAMEBUFFER,
|
||||
glow::COLOR_ATTACHMENT0,
|
||||
glow::TEXTURE_2D,
|
||||
Some(texture.id()),
|
||||
0,
|
||||
);
|
||||
};
|
||||
|
||||
let stencil_rbo = unsafe { context.create_renderbuffer().unwrap() };
|
||||
unsafe {
|
||||
context.bind_renderbuffer(glow::RENDERBUFFER, Some(stencil_rbo));
|
||||
context.renderbuffer_storage(glow::RENDERBUFFER, glow::STENCIL_INDEX8, width as i32, height as i32);
|
||||
context.bind_renderbuffer(glow::RENDERBUFFER, None);
|
||||
|
||||
context.framebuffer_renderbuffer(
|
||||
glow::FRAMEBUFFER,
|
||||
glow::STENCIL_ATTACHMENT,
|
||||
glow::RENDERBUFFER,
|
||||
Some(stencil_rbo),
|
||||
);
|
||||
|
||||
let status = context.check_framebuffer_status(glow::FRAMEBUFFER);
|
||||
|
||||
if status != glow::FRAMEBUFFER_COMPLETE {
|
||||
let reason = match status {
|
||||
glow::FRAMEBUFFER_INCOMPLETE_ATTACHMENT => {
|
||||
format!("({status}) Framebuffer incomplete attachment")
|
||||
}
|
||||
//glow::FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER => format!("({}) Framebuffer incomplete draw buffer", status),
|
||||
//glow::FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS => format!("({}) Framebuffer incomplete layer targets", status),
|
||||
//FIXME: will be in next glow release: glow::FRAMEBUFFER_INCOMPLETE_DIMENSIONS => format!("({}) Framebuffer incomplete dimensions", status),
|
||||
glow::FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT => {
|
||||
format!("({status}) Framebuffer incomplete missing attachment")
|
||||
}
|
||||
glow::FRAMEBUFFER_INCOMPLETE_MULTISAMPLE => {
|
||||
format!("({status}) Framebuffer incomplete multisample")
|
||||
}
|
||||
//glow::FRAMEBUFFER_INCOMPLETE_READ_BUFFER => format!("({}) Framebuffer incomplete read buffer", status),
|
||||
glow::FRAMEBUFFER_UNSUPPORTED => format!("({status}) Framebuffer unsupported"),
|
||||
_ => format!("({status}) Framebuffer not complete!"),
|
||||
};
|
||||
|
||||
return Err(ErrorKind::RenderTargetError(reason));
|
||||
}
|
||||
|
||||
context.bind_framebuffer(glow::FRAMEBUFFER, None);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
context: context.clone(),
|
||||
fbo,
|
||||
stencil_rbo: Some(stencil_rbo),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn bind(&self) {
|
||||
unsafe {
|
||||
self.context.bind_framebuffer(glow::FRAMEBUFFER, Some(self.fbo));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unbind(context: &Rc<glow::Context>) {
|
||||
unsafe {
|
||||
context.bind_framebuffer(glow::FRAMEBUFFER, None);
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn blit_to_texture(&self, texture: &GlTexture) {
|
||||
// let dest_fbo = Self::new(texture);
|
||||
|
||||
// unsafe {
|
||||
// glow::BindFramebuffer(glow::READ_FRAMEBUFFER, self.fbo);
|
||||
// glow::BindFramebuffer(glow::DRAW_FRAMEBUFFER, dest_fbo.fbo);
|
||||
|
||||
// glow::BlitFramebuffer(
|
||||
// 0,
|
||||
// 0,
|
||||
// self.width as i32,
|
||||
// self.height as i32,
|
||||
// 0,
|
||||
// 0,
|
||||
// dest_fbo.width as i32,
|
||||
// dest_fbo.height as i32,
|
||||
// glow::COLOR_BUFFER_BIT,
|
||||
// glow::NEAREST
|
||||
// );
|
||||
|
||||
// glow::BindFramebuffer(glow::READ_FRAMEBUFFER, 0);
|
||||
// glow::BindFramebuffer(glow::DRAW_FRAMEBUFFER, 0);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
impl Drop for Framebuffer {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.context.delete_framebuffer(self.fbo);
|
||||
if let Some(stencil_rbo) = self.stencil_rbo {
|
||||
self.context.delete_renderbuffer(stencil_rbo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use glow::HasContext;
|
||||
|
||||
use crate::{ErrorKind, ImageFlags, ImageInfo, ImageSource, PixelFormat};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GlTexture {
|
||||
id: <glow::Context as glow::HasContext>::Texture,
|
||||
info: ImageInfo,
|
||||
owned: bool,
|
||||
}
|
||||
|
||||
impl GlTexture {
|
||||
pub fn new_from_native_texture(texture: <glow::Context as glow::HasContext>::Texture, info: ImageInfo) -> Self {
|
||||
Self {
|
||||
id: texture,
|
||||
info,
|
||||
owned: false,
|
||||
}
|
||||
}
|
||||
pub fn new(context: &Rc<glow::Context>, info: ImageInfo, opengles_2_0: bool) -> Result<Self, ErrorKind> {
|
||||
//let size = src.dimensions();
|
||||
|
||||
let id = unsafe {
|
||||
let id = context.create_texture().unwrap();
|
||||
context.bind_texture(glow::TEXTURE_2D, Some(id));
|
||||
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
|
||||
if !opengles_2_0 {
|
||||
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, info.width() as i32);
|
||||
context.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, 0);
|
||||
context.pixel_store_i32(glow::UNPACK_SKIP_ROWS, 0);
|
||||
}
|
||||
id
|
||||
};
|
||||
|
||||
let texture = Self { id, info, owned: true };
|
||||
|
||||
match info.format() {
|
||||
PixelFormat::Gray8 => unsafe {
|
||||
let internal_format = if opengles_2_0 { glow::LUMINANCE } else { glow::R8 };
|
||||
let format = if opengles_2_0 { internal_format } else { glow::RED };
|
||||
|
||||
context.tex_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
internal_format as i32,
|
||||
texture.info.width() as i32,
|
||||
texture.info.height() as i32,
|
||||
0,
|
||||
format,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice(None), //data.buf().as_ptr() as *const GLvoid
|
||||
);
|
||||
},
|
||||
PixelFormat::Rgb8 => unsafe {
|
||||
context.tex_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
glow::RGB as i32,
|
||||
texture.info.width() as i32,
|
||||
texture.info.height() as i32,
|
||||
0,
|
||||
glow::RGB,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice(None),
|
||||
//data.buf().as_ptr() as *const GLvoid
|
||||
);
|
||||
},
|
||||
PixelFormat::Rgba8 => unsafe {
|
||||
context.tex_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
glow::RGBA as i32,
|
||||
texture.info.width() as i32,
|
||||
texture.info.height() as i32,
|
||||
0,
|
||||
glow::RGBA,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice(None),
|
||||
//data.buf().as_ptr() as *const GLvoid
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
let flags = texture.info.flags();
|
||||
|
||||
if flags.contains(ImageFlags::GENERATE_MIPMAPS) {
|
||||
if flags.contains(ImageFlags::NEAREST) {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(
|
||||
glow::TEXTURE_2D,
|
||||
glow::TEXTURE_MIN_FILTER,
|
||||
glow::NEAREST_MIPMAP_NEAREST as i32,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(
|
||||
glow::TEXTURE_2D,
|
||||
glow::TEXTURE_MIN_FILTER,
|
||||
glow::LINEAR_MIPMAP_LINEAR as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if flags.contains(ImageFlags::NEAREST) {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(ImageFlags::NEAREST) {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(ImageFlags::REPEAT_X) {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(ImageFlags::REPEAT_Y) {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
context.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 4);
|
||||
if !opengles_2_0 {
|
||||
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, 0);
|
||||
context.pixel_store_i32(glow::UNPACK_SKIP_PIXELS, 0);
|
||||
context.pixel_store_i32(glow::UNPACK_SKIP_ROWS, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(ImageFlags::GENERATE_MIPMAPS) {
|
||||
unsafe {
|
||||
context.generate_mipmap(glow::TEXTURE_2D);
|
||||
//glow::TexParameteri(glow::TEXTURE_2D, glow::GENERATE_MIPMAP, glow::TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
context.bind_texture(glow::TEXTURE_2D, None);
|
||||
}
|
||||
|
||||
Ok(texture)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> <glow::Context as glow::HasContext>::Texture {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
context: &Rc<glow::Context>,
|
||||
src: ImageSource,
|
||||
x: usize,
|
||||
y: usize,
|
||||
opengles_2_0: bool,
|
||||
) -> Result<(), ErrorKind> {
|
||||
let size = src.dimensions();
|
||||
|
||||
if x + size.width > self.info.width() {
|
||||
return Err(ErrorKind::ImageUpdateOutOfBounds);
|
||||
}
|
||||
|
||||
if y + size.height > self.info.height() {
|
||||
return Err(ErrorKind::ImageUpdateOutOfBounds);
|
||||
}
|
||||
|
||||
if self.info.format() != src.format() {
|
||||
return Err(ErrorKind::ImageUpdateWithDifferentFormat);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
context.bind_texture(glow::TEXTURE_2D, Some(self.id));
|
||||
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);
|
||||
if !opengles_2_0 {
|
||||
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, size.width as i32);
|
||||
}
|
||||
}
|
||||
|
||||
match src {
|
||||
ImageSource::Gray(data) => unsafe {
|
||||
let format = if opengles_2_0 { glow::LUMINANCE } else { glow::R8 };
|
||||
|
||||
context.tex_sub_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
x as i32,
|
||||
y as i32,
|
||||
size.width as i32,
|
||||
size.height as i32,
|
||||
format,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice(Some(data.buf().align_to().1)),
|
||||
);
|
||||
},
|
||||
ImageSource::Rgb(data) => unsafe {
|
||||
context.tex_sub_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
x as i32,
|
||||
y as i32,
|
||||
size.width as i32,
|
||||
size.height as i32,
|
||||
glow::RGB,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice(Some(data.buf().align_to().1)),
|
||||
);
|
||||
},
|
||||
ImageSource::Rgba(data) => unsafe {
|
||||
context.tex_sub_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
x as i32,
|
||||
y as i32,
|
||||
size.width as i32,
|
||||
size.height as i32,
|
||||
glow::RGBA,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice(Some(data.buf().align_to().1)),
|
||||
);
|
||||
},
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
ImageSource::HtmlImageElement(image_element) => unsafe {
|
||||
context.tex_sub_image_2d_with_html_image(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
x as i32,
|
||||
y as i32,
|
||||
glow::RGBA,
|
||||
glow::UNSIGNED_BYTE,
|
||||
image_element,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
if self.info.flags().contains(ImageFlags::GENERATE_MIPMAPS) {
|
||||
unsafe {
|
||||
context.generate_mipmap(glow::TEXTURE_2D);
|
||||
//glow::TexParameteri(glow::TEXTURE_2D, glow::GENERATE_MIPMAP, glow::TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
context.pixel_store_i32(glow::UNPACK_ALIGNMENT, 4);
|
||||
if !opengles_2_0 {
|
||||
context.pixel_store_i32(glow::UNPACK_ROW_LENGTH, 0);
|
||||
}
|
||||
//glow::PixelStorei(glow::UNPACK_SKIP_PIXELS, 0);
|
||||
//glow::PixelStorei(glow::UNPACK_SKIP_ROWS, 0);
|
||||
context.bind_texture(glow::TEXTURE_2D, None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete(self, context: &Rc<glow::Context>) {
|
||||
if self.owned {
|
||||
unsafe {
|
||||
context.delete_texture(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(&self) -> ImageInfo {
|
||||
self.info
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
|
||||
precision highp float;
|
||||
|
||||
#define UNIFORMARRAY_SIZE 14
|
||||
|
||||
#define TAU 6.28318530717958647692528676655900577
|
||||
|
||||
uniform vec4 frag[UNIFORMARRAY_SIZE];
|
||||
|
||||
#define scissorMat mat3(frag[0].xyz, frag[1].xyz, frag[2].xyz)
|
||||
#define paintMat mat3(frag[3].xyz, frag[4].xyz, frag[5].xyz)
|
||||
#define innerCol frag[6]
|
||||
#define outerCol frag[7]
|
||||
#define scissorExt frag[8].xy
|
||||
#define scissorScale frag[8].zw
|
||||
#define extent frag[9].xy
|
||||
#define radius frag[9].z
|
||||
#define feather frag[9].w
|
||||
#define strokeMult frag[10].x
|
||||
#define strokeThr frag[10].y
|
||||
#define texType int(frag[10].z)
|
||||
#define shaderType int(frag[10].w)
|
||||
#define glyphTextureType int(frag[11].x)
|
||||
#define imageBlurFilterDirection frag[11].yz
|
||||
#define imageBlurFilterSigma frag[11].w
|
||||
#define imageBlurFilterCoeff frag[12].xyz
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform sampler2D glyphtex;
|
||||
uniform vec2 viewSize;
|
||||
|
||||
varying vec2 ftcoord;
|
||||
varying vec2 fpos;
|
||||
|
||||
#define SHADER_TYPE_FillGradient 0
|
||||
#define SHADER_TYPE_FillImage 1
|
||||
#define SHADER_TYPE_Stencil 2
|
||||
#define SHADER_TYPE_FillImageGradient 3
|
||||
#define SHADER_TYPE_FilterImage 4
|
||||
#define SHADER_TYPE_FillColor 5
|
||||
#define SHADER_TYPE_TextureCopyUnclipped 6
|
||||
#define SHADER_TYPE_FillGradientConic 8
|
||||
#define SHADER_TYPE_FillImageGradientConic 9
|
||||
|
||||
float sdroundrect(vec2 pt, vec2 ext, float rad) {
|
||||
vec2 ext2 = ext - vec2(rad,rad);
|
||||
vec2 d = abs(pt) - ext2;
|
||||
return min(max(d.x,d.y),0.0) + length(max(d,0.0)) - rad;
|
||||
}
|
||||
|
||||
// Scissoring
|
||||
float scissorMask(vec2 p) {
|
||||
vec2 sc = (abs((scissorMat * vec3(p,1.0)).xy) - scissorExt);
|
||||
sc = vec2(0.5,0.5) - sc * scissorScale;
|
||||
return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);
|
||||
}
|
||||
|
||||
#ifdef EDGE_AA
|
||||
// Stroke - from [0..1] to clipped pyramid, where the slope is 1px.
|
||||
float strokeMask() {
|
||||
return min(1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * min(1.0, ftcoord.y);
|
||||
// Using this smoothstep preduces maybe better results when combined with fringe_width of 2, but it may look blurrier
|
||||
// maybe this should be controlled via flag
|
||||
//return smoothstep(0.0, 1.0, (1.0-abs(ftcoord.x*2.0-1.0))*strokeMult) * smoothstep(0.0, 1.0, ftcoord.y);
|
||||
}
|
||||
#endif
|
||||
|
||||
vec4 renderGradient() {
|
||||
// Calculate gradient color using box gradient
|
||||
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy;
|
||||
|
||||
float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);
|
||||
return mix(innerCol,outerCol,d);
|
||||
}
|
||||
|
||||
// Image-based Gradient; sample a texture using the gradient position.
|
||||
vec4 renderImageGradient() {
|
||||
// Calculate gradient color using box gradient
|
||||
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy;
|
||||
|
||||
float d = clamp((sdroundrect(pt, extent, radius) + feather*0.5) / feather, 0.0, 1.0);
|
||||
return texture2D(tex, vec2(d, 0.0));//mix(innerCol,outerCol,d);
|
||||
}
|
||||
|
||||
float conicAngleFraction() {
|
||||
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy;
|
||||
// atan returns a value between -pi and pi.
|
||||
// normally you'd use atan(pt.y,pt.x) but its switched
|
||||
// around here to be clockwise and start from the top.
|
||||
return (-atan(pt.x,pt.y) / TAU) + 0.5;
|
||||
}
|
||||
|
||||
vec4 renderGradientConic() {
|
||||
float d = conicAngleFraction();
|
||||
return mix(innerCol,outerCol,d);
|
||||
}
|
||||
|
||||
vec4 renderImageGradientConic() {
|
||||
float d = conicAngleFraction();
|
||||
return texture2D(tex, vec2(d, 0.0));
|
||||
}
|
||||
|
||||
vec4 renderImage() {
|
||||
// Calculate color from texture
|
||||
vec2 pt = (paintMat * vec3(fpos, 1.0)).xy / extent;
|
||||
|
||||
vec4 color = texture2D(tex, pt);
|
||||
|
||||
if (texType == 1) color = vec4(color.xyz * color.w, color.w);
|
||||
if (texType == 2) color = vec4(color.x);
|
||||
|
||||
// Apply color tint and alpha.
|
||||
color *= innerCol;
|
||||
return color;
|
||||
}
|
||||
|
||||
vec4 renderPlainTextureCopy() {
|
||||
vec4 color = texture2D(tex, ftcoord);
|
||||
if (texType == 1) color = vec4(color.xyz * color.w, color.w);
|
||||
if (texType == 2) color = vec4(color.x);
|
||||
// Apply color tint and alpha.
|
||||
color *= innerCol;
|
||||
return color;
|
||||
}
|
||||
|
||||
vec4 renderFilteredImage() {
|
||||
float sampleCount = ceil(1.5 * imageBlurFilterSigma);
|
||||
|
||||
vec3 gaussian_coeff = imageBlurFilterCoeff;
|
||||
|
||||
vec4 color_sum = texture2D(tex, fpos.xy / extent) * gaussian_coeff.x;
|
||||
float coefficient_sum = gaussian_coeff.x;
|
||||
gaussian_coeff.xy *= gaussian_coeff.yz;
|
||||
|
||||
for (float i = 1.0; i <= 12.0; i += 1.) {
|
||||
// Work around GLES 2.0 limitation of only allowing constant loop indices
|
||||
// by breaking here. Sigma has an upper bound of 8, imposed on the Rust side.
|
||||
if (i >= sampleCount) {
|
||||
break;
|
||||
}
|
||||
color_sum += texture2D(tex, (fpos.xy - i * imageBlurFilterDirection) / extent) * gaussian_coeff.x;
|
||||
color_sum += texture2D(tex, (fpos.xy + i * imageBlurFilterDirection) / extent) * gaussian_coeff.x;
|
||||
coefficient_sum += 2.0 * gaussian_coeff.x;
|
||||
|
||||
// Compute the coefficients incrementally:
|
||||
// https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-40-incremental-computation-gaussian
|
||||
gaussian_coeff.xy *= gaussian_coeff.yz;
|
||||
}
|
||||
|
||||
vec4 color = color_sum / coefficient_sum;
|
||||
|
||||
if (texType == 1) color = vec4(color.xyz * color.w, color.w);
|
||||
if (texType == 2) color = vec4(color.x);
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
vec4 result;
|
||||
|
||||
#ifdef EDGE_AA
|
||||
float strokeAlpha = 1.0;
|
||||
#if SELECT_SHADER != 6
|
||||
strokeAlpha = strokeMask();
|
||||
if (strokeAlpha < strokeThr) discard;
|
||||
#endif
|
||||
#else
|
||||
float strokeAlpha = 1.0;
|
||||
#endif
|
||||
|
||||
#if SELECT_SHADER == SHADER_TYPE_FillGradient
|
||||
// Gradient
|
||||
result = renderGradient();
|
||||
#elif SELECT_SHADER == SHADER_TYPE_FillImageGradient
|
||||
// Image-based Gradient; sample a texture using the gradient position.
|
||||
result = renderImageGradient();
|
||||
#elif SELECT_SHADER == SHADER_TYPE_FillImage
|
||||
// Image
|
||||
result = renderImage();
|
||||
#elif SELECT_SHADER == SHADER_TYPE_FillColor
|
||||
// Plain color fill
|
||||
result = innerCol;
|
||||
#elif SELECT_SHADER == SHADER_TYPE_TextureCopyUnclipped
|
||||
// Plain texture copy, unclipped
|
||||
gl_FragColor = renderPlainTextureCopy();
|
||||
return;
|
||||
#elif SELECT_SHADER == SHADER_TYPE_Stencil
|
||||
// Stencil fill
|
||||
result = vec4(1,1,1,1);
|
||||
#elif SELECT_SHADER == SHADER_TYPE_FilterImage
|
||||
// Filter Image
|
||||
result = renderFilteredImage();
|
||||
#elif SELECT_SHADER == SHADER_TYPE_FillGradientConic
|
||||
result = renderGradientConic();
|
||||
#elif SELECT_SHADER == SHADER_TYPE_FillImageGradientConic
|
||||
result = renderImageGradientConic();
|
||||
#else
|
||||
#error A shader variant must be selected with the SELECT_SHADER pre-processor variable
|
||||
#endif
|
||||
|
||||
float scissor = scissorMask(fpos);
|
||||
|
||||
#ifdef ENABLE_GLYPH_TEXTURE
|
||||
// Textured tris
|
||||
vec4 mask = texture2D(glyphtex, ftcoord);
|
||||
|
||||
if (glyphTextureType == 1) {
|
||||
mask = vec4(mask.x);
|
||||
} else {
|
||||
result = vec4(1, 1, 1, 1);
|
||||
mask = vec4(mask.xyz * mask.w, mask.w);
|
||||
}
|
||||
|
||||
mask *= scissor;
|
||||
result *= mask;
|
||||
#else
|
||||
#if SELECT_SHADER != SHADER_TYPE_Stencil && SELECT_SHADER != SHADER_TYPE_FilterImage
|
||||
// Not stencil fill
|
||||
// Combine alpha
|
||||
result *= strokeAlpha * scissor;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
gl_FragColor = result;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
uniform vec2 viewSize;
|
||||
|
||||
attribute vec2 vertex;
|
||||
attribute vec2 tcoord;
|
||||
|
||||
varying vec2 ftcoord;
|
||||
varying vec2 fpos;
|
||||
|
||||
void main(void) {
|
||||
ftcoord = tcoord;
|
||||
fpos = vertex;
|
||||
|
||||
gl_Position = vec4(2.0 * vertex.x / viewSize.x - 1.0, 1.0 - 2.0 * vertex.y / viewSize.y, 0, 1);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use glow::HasContext;
|
||||
|
||||
use crate::{renderer::ShaderType, ErrorKind};
|
||||
|
||||
const GLSL_VERSION: &str = "#version 100";
|
||||
|
||||
pub struct Shader {
|
||||
context: Rc<glow::Context>,
|
||||
id: <glow::Context as glow::HasContext>::Shader,
|
||||
}
|
||||
|
||||
impl Shader {
|
||||
pub fn new(context: &Rc<glow::Context>, src: &str, kind: u32) -> Result<Self, ErrorKind> {
|
||||
let id = unsafe { context.create_shader(kind).unwrap() };
|
||||
|
||||
// Compile
|
||||
unsafe {
|
||||
context.shader_source(id, src);
|
||||
context.compile_shader(id);
|
||||
}
|
||||
|
||||
// Validate
|
||||
|
||||
let success = unsafe { context.get_shader_compile_status(id) };
|
||||
if !success {
|
||||
let error = unsafe { context.get_shader_info_log(id) };
|
||||
|
||||
let name = match kind {
|
||||
glow::VERTEX_SHADER => "Vertex stage",
|
||||
glow::FRAGMENT_SHADER => "Fragment stage",
|
||||
_ => "Shader stage",
|
||||
};
|
||||
|
||||
return Err(ErrorKind::ShaderCompileError(format!("{name}: {error}")));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
context: context.clone(),
|
||||
id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> <glow::Context as glow::HasContext>::Shader {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Shader {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.context.delete_shader(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
context: Rc<glow::Context>,
|
||||
id: <glow::Context as glow::HasContext>::Program,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn new(context: &Rc<glow::Context>, shaders: &[Shader], attrib_locations: &[&str]) -> Result<Self, ErrorKind> {
|
||||
let program = Self {
|
||||
context: context.clone(),
|
||||
id: unsafe { context.create_program().unwrap() },
|
||||
};
|
||||
|
||||
// Attach stages
|
||||
for shader in shaders {
|
||||
unsafe {
|
||||
context.attach_shader(program.id, shader.id());
|
||||
}
|
||||
}
|
||||
|
||||
for (i, loc) in attrib_locations.iter().enumerate() {
|
||||
unsafe {
|
||||
context.bind_attrib_location(program.id, i as u32, loc);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
context.link_program(program.id);
|
||||
}
|
||||
|
||||
// Check for error
|
||||
|
||||
let success = unsafe { context.get_program_link_status(program.id) };
|
||||
|
||||
if !success {
|
||||
let error = unsafe { context.get_program_info_log(program.id) };
|
||||
|
||||
return Err(ErrorKind::ShaderLinkError(error));
|
||||
}
|
||||
|
||||
// Detach stages
|
||||
for shader in shaders {
|
||||
unsafe {
|
||||
context.detach_shader(program.id, shader.id());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
pub(crate) fn bind(&self) {
|
||||
unsafe {
|
||||
self.context.use_program(Some(self.id));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unbind(&self) {
|
||||
unsafe {
|
||||
self.context.use_program(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn uniform_location(&self, name: &str) -> Option<<glow::Context as glow::HasContext>::UniformLocation> {
|
||||
unsafe { self.context.get_uniform_location(self.id, name) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Program {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.context.delete_program(self.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MainProgram {
|
||||
context: Rc<glow::Context>,
|
||||
program: Program,
|
||||
loc_viewsize: <glow::Context as glow::HasContext>::UniformLocation,
|
||||
loc_tex: Option<<glow::Context as glow::HasContext>::UniformLocation>,
|
||||
loc_glyphtex: Option<<glow::Context as glow::HasContext>::UniformLocation>,
|
||||
loc_frag: Option<<glow::Context as glow::HasContext>::UniformLocation>,
|
||||
}
|
||||
|
||||
impl MainProgram {
|
||||
pub(crate) fn new(
|
||||
context: &Rc<glow::Context>,
|
||||
antialias: bool,
|
||||
shader_type: ShaderType,
|
||||
with_glyph_texture: bool,
|
||||
) -> Result<Self, ErrorKind> {
|
||||
let shader_defs = if antialias { "#define EDGE_AA 1" } else { "" };
|
||||
let select_shader_type = format!(
|
||||
"#define SELECT_SHADER {}\n{}",
|
||||
shader_type.to_u8(),
|
||||
if with_glyph_texture {
|
||||
"#define ENABLE_GLYPH_TEXTURE"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
let vert_shader_src = format!("{}\n{}\n{}", GLSL_VERSION, shader_defs, include_str!("main-vs.glsl"));
|
||||
let frag_shader_src = format!(
|
||||
"{}\n{}\n{}\n{}",
|
||||
GLSL_VERSION,
|
||||
shader_defs,
|
||||
select_shader_type,
|
||||
include_str!("main-fs.glsl")
|
||||
);
|
||||
|
||||
let vert_shader = Shader::new(context, &vert_shader_src, glow::VERTEX_SHADER)?;
|
||||
let frag_shader = Shader::new(context, &frag_shader_src, glow::FRAGMENT_SHADER)?;
|
||||
|
||||
let program = Program::new(context, &[vert_shader, frag_shader], &["vertex", "tcoord"])?;
|
||||
|
||||
let loc_viewsize = program.uniform_location("viewSize").unwrap();
|
||||
let loc_tex = program.uniform_location("tex");
|
||||
let loc_glyphtex = program.uniform_location("glyphtex");
|
||||
let loc_frag = program.uniform_location("frag");
|
||||
|
||||
Ok(Self {
|
||||
context: context.clone(),
|
||||
program,
|
||||
loc_viewsize,
|
||||
loc_tex,
|
||||
loc_glyphtex,
|
||||
loc_frag,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn set_tex(&self, tex: i32) {
|
||||
unsafe {
|
||||
self.context.uniform_1_i32(self.loc_tex.as_ref(), tex);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_glyphtex(&self, tex: i32) {
|
||||
unsafe {
|
||||
self.context.uniform_1_i32(self.loc_glyphtex.as_ref(), tex);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_view(&self, view: [f32; 2]) {
|
||||
unsafe {
|
||||
self.context.uniform_2_f32_slice(Some(&self.loc_viewsize), &view);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_config(&self, config: &[f32]) {
|
||||
unsafe {
|
||||
self.context.uniform_4_f32_slice(self.loc_frag.as_ref(), config);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bind(&self) {
|
||||
self.program.bind();
|
||||
}
|
||||
|
||||
pub(crate) fn unbind(&self) {
|
||||
self.program.unbind();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::Params;
|
||||
|
||||
const UNIFORMARRAY_SIZE: usize = 14;
|
||||
|
||||
pub struct UniformArray([f32; UNIFORMARRAY_SIZE * 4]);
|
||||
|
||||
impl Default for UniformArray {
|
||||
fn default() -> Self {
|
||||
Self([
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl UniformArray {
|
||||
pub fn as_slice(&self) -> &[f32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn set_scissor_mat(&mut self, mat: [f32; 12]) {
|
||||
self.0[0..12].copy_from_slice(&mat);
|
||||
}
|
||||
|
||||
pub fn set_paint_mat(&mut self, mat: [f32; 12]) {
|
||||
self.0[12..24].copy_from_slice(&mat);
|
||||
}
|
||||
|
||||
pub fn set_inner_col(&mut self, col: [f32; 4]) {
|
||||
self.0[24..28].copy_from_slice(&col);
|
||||
}
|
||||
|
||||
pub fn set_outer_col(&mut self, col: [f32; 4]) {
|
||||
self.0[28..32].copy_from_slice(&col);
|
||||
}
|
||||
|
||||
pub fn set_scissor_ext(&mut self, ext: [f32; 2]) {
|
||||
self.0[32..34].copy_from_slice(&ext);
|
||||
}
|
||||
|
||||
pub fn set_scissor_scale(&mut self, scale: [f32; 2]) {
|
||||
self.0[34..36].copy_from_slice(&scale);
|
||||
}
|
||||
|
||||
pub fn set_extent(&mut self, ext: [f32; 2]) {
|
||||
self.0[36..38].copy_from_slice(&ext);
|
||||
}
|
||||
|
||||
pub fn set_radius(&mut self, radius: f32) {
|
||||
self.0[38] = radius;
|
||||
}
|
||||
|
||||
pub fn set_feather(&mut self, feather: f32) {
|
||||
self.0[39] = feather;
|
||||
}
|
||||
|
||||
pub fn set_stroke_mult(&mut self, stroke_mult: f32) {
|
||||
self.0[40] = stroke_mult;
|
||||
}
|
||||
|
||||
pub fn set_stroke_thr(&mut self, stroke_thr: f32) {
|
||||
self.0[41] = stroke_thr;
|
||||
}
|
||||
|
||||
pub fn set_tex_type(&mut self, tex_type: f32) {
|
||||
self.0[42] = tex_type;
|
||||
}
|
||||
|
||||
pub fn set_shader_type(&mut self, shader_type: f32) {
|
||||
self.0[43] = shader_type;
|
||||
}
|
||||
|
||||
pub fn set_glyph_texture_type(&mut self, glyph_texture_type: f32) {
|
||||
self.0[44] = glyph_texture_type;
|
||||
}
|
||||
|
||||
pub fn set_image_blur_filter_direction(&mut self, direction: [f32; 2]) {
|
||||
self.0[45..47].copy_from_slice(&direction);
|
||||
}
|
||||
|
||||
pub fn set_image_blur_filter_sigma(&mut self, sigma: f32) {
|
||||
self.0[47] = sigma;
|
||||
}
|
||||
|
||||
pub fn set_image_blur_filter_coeff(&mut self, coeff: [f32; 3]) {
|
||||
self.0[48..51].copy_from_slice(&coeff);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Params> for UniformArray {
|
||||
fn from(params: &Params) -> Self {
|
||||
let mut arr = Self::default();
|
||||
|
||||
arr.set_scissor_mat(params.scissor_mat);
|
||||
arr.set_paint_mat(params.paint_mat);
|
||||
arr.set_inner_col(params.inner_col);
|
||||
arr.set_outer_col(params.outer_col);
|
||||
arr.set_scissor_ext(params.scissor_ext);
|
||||
arr.set_scissor_scale(params.scissor_scale);
|
||||
arr.set_extent(params.extent);
|
||||
arr.set_radius(params.radius);
|
||||
arr.set_feather(params.feather);
|
||||
arr.set_stroke_mult(params.stroke_mult);
|
||||
arr.set_stroke_thr(params.stroke_thr);
|
||||
arr.set_shader_type(params.shader_type.to_f32());
|
||||
arr.set_tex_type(params.tex_type);
|
||||
arr.set_glyph_texture_type(params.glyph_texture_type as f32);
|
||||
arr.set_image_blur_filter_direction(params.image_blur_filter_direction);
|
||||
arr.set_image_blur_filter_sigma(params.image_blur_filter_sigma);
|
||||
arr.set_image_blur_filter_coeff(params.image_blur_filter_coeff);
|
||||
|
||||
arr
|
||||
}
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
use crate::{
|
||||
geometry::Position,
|
||||
paint::{GlyphTexture, GradientColors},
|
||||
ImageFlags, ImageStore, PaintFlavor, PixelFormat, Scissor, Transform2D,
|
||||
};
|
||||
|
||||
use super::ShaderType;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct Params {
|
||||
pub(crate) scissor_mat: [f32; 12],
|
||||
pub(crate) paint_mat: [f32; 12],
|
||||
pub(crate) inner_col: [f32; 4],
|
||||
pub(crate) outer_col: [f32; 4],
|
||||
pub(crate) scissor_ext: [f32; 2],
|
||||
pub(crate) scissor_scale: [f32; 2],
|
||||
pub(crate) extent: [f32; 2],
|
||||
pub(crate) radius: f32,
|
||||
pub(crate) feather: f32,
|
||||
pub(crate) stroke_mult: f32,
|
||||
pub(crate) stroke_thr: f32,
|
||||
pub(crate) tex_type: f32,
|
||||
pub(crate) shader_type: ShaderType,
|
||||
pub(crate) glyph_texture_type: u8, // 0 -> no glyph rendering, 1 -> alpha mask, 2 -> color texture
|
||||
pub(crate) image_blur_filter_direction: [f32; 2],
|
||||
pub(crate) image_blur_filter_sigma: f32,
|
||||
pub(crate) image_blur_filter_coeff: [f32; 3],
|
||||
}
|
||||
|
||||
impl Params {
|
||||
pub(crate) fn new<T>(
|
||||
images: &ImageStore<T>,
|
||||
global_transform: &Transform2D,
|
||||
paint_flavor: &PaintFlavor,
|
||||
glyph_texture: &GlyphTexture,
|
||||
scissor: &Scissor,
|
||||
stroke_width: f32,
|
||||
fringe_width: f32,
|
||||
stroke_thr: f32,
|
||||
) -> Self {
|
||||
let mut params = Self::default();
|
||||
|
||||
// Scissor
|
||||
let (scissor_ext, scissor_scale) = if let Some(ext) = scissor.extent {
|
||||
if ext[0] < -0.5 || ext[1] < -0.5 {
|
||||
([1.0, 1.0], [1.0, 1.0])
|
||||
} else {
|
||||
params.scissor_mat = scissor.transform.inverse().to_mat3x4();
|
||||
|
||||
let Transform2D([a, b, c, d, ..]) = scissor.transform;
|
||||
let scissor_scale = [a.hypot(c) / fringe_width, b.hypot(d) / fringe_width];
|
||||
|
||||
(ext, scissor_scale)
|
||||
}
|
||||
} else {
|
||||
([1.0, 1.0], [1.0, 1.0])
|
||||
};
|
||||
|
||||
params.scissor_ext = scissor_ext;
|
||||
params.scissor_scale = scissor_scale;
|
||||
|
||||
params.stroke_mult = (stroke_width * 0.5 + fringe_width * 0.5) / fringe_width;
|
||||
params.stroke_thr = stroke_thr;
|
||||
|
||||
params.glyph_texture_type = match glyph_texture {
|
||||
GlyphTexture::None => 0,
|
||||
GlyphTexture::AlphaMask(_) => 1,
|
||||
GlyphTexture::ColorTexture(_) => 2,
|
||||
};
|
||||
|
||||
let inv_transform;
|
||||
|
||||
match &paint_flavor {
|
||||
PaintFlavor::Color(color) => {
|
||||
let color = color.premultiplied().to_array();
|
||||
params.inner_col = color;
|
||||
params.outer_col = color;
|
||||
params.shader_type = ShaderType::FillColor;
|
||||
inv_transform = global_transform.inverse();
|
||||
}
|
||||
&PaintFlavor::Image {
|
||||
id,
|
||||
center: Position { x: cx, y: cy },
|
||||
width,
|
||||
height,
|
||||
angle,
|
||||
tint,
|
||||
} => {
|
||||
let Some(image_info) = images.info(*id) else {
|
||||
return params;
|
||||
};
|
||||
|
||||
params.extent[0] = *width;
|
||||
params.extent[1] = *height;
|
||||
|
||||
let color = tint;
|
||||
|
||||
params.inner_col = color.premultiplied().to_array();
|
||||
params.outer_col = color.premultiplied().to_array();
|
||||
|
||||
let mut transform = Transform2D::rotation(*angle);
|
||||
transform.translate(*cx, *cy);
|
||||
transform *= *global_transform;
|
||||
|
||||
if image_info.flags().contains(ImageFlags::FLIP_Y) {
|
||||
let mut m1 = Transform2D::translation(0.0, height * 0.5);
|
||||
m1 *= transform;
|
||||
|
||||
let mut m2 = Transform2D::scaling(1.0, -1.0);
|
||||
m2 *= m1;
|
||||
|
||||
let mut m1 = Transform2D::translation(0.0, -height * 0.5);
|
||||
m1 *= m2;
|
||||
|
||||
inv_transform = m1.inverse();
|
||||
} else {
|
||||
inv_transform = transform.inverse();
|
||||
}
|
||||
|
||||
params.shader_type = ShaderType::FillImage;
|
||||
|
||||
params.tex_type = match image_info.format() {
|
||||
PixelFormat::Rgba8 => {
|
||||
if image_info.flags().contains(ImageFlags::PREMULTIPLIED) {
|
||||
0.0
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
PixelFormat::Gray8 => 2.0,
|
||||
PixelFormat::Rgb8 => 0.0,
|
||||
};
|
||||
}
|
||||
PaintFlavor::LinearGradient {
|
||||
start: Position { x: start_x, y: start_y },
|
||||
end: Position { x: end_x, y: end_y },
|
||||
colors,
|
||||
} => {
|
||||
let large = 1e5f32;
|
||||
let mut dx = end_x - start_x;
|
||||
let mut dy = end_y - start_y;
|
||||
let d = dx.hypot(dy);
|
||||
|
||||
if d > 0.0001 {
|
||||
dx /= d;
|
||||
dy /= d;
|
||||
} else {
|
||||
dx = 0.0;
|
||||
dy = 1.0;
|
||||
}
|
||||
|
||||
let mut transform = Transform2D([dy, -dx, dx, dy, start_x - dx * large, start_y - dy * large]);
|
||||
|
||||
transform *= *global_transform;
|
||||
|
||||
inv_transform = transform.inverse();
|
||||
|
||||
params.extent[0] = large;
|
||||
params.extent[1] = large + d * 0.5;
|
||||
params.feather = 1.0f32.max(d);
|
||||
|
||||
match colors {
|
||||
GradientColors::TwoStop { start_color, end_color } => {
|
||||
params.inner_col = start_color.premultiplied().to_array();
|
||||
params.outer_col = end_color.premultiplied().to_array();
|
||||
params.shader_type = ShaderType::FillGradient;
|
||||
}
|
||||
GradientColors::MultiStop { .. } => {
|
||||
params.shader_type = ShaderType::FillImageGradient;
|
||||
}
|
||||
}
|
||||
}
|
||||
&PaintFlavor::BoxGradient {
|
||||
pos: Position { x, y },
|
||||
width,
|
||||
height,
|
||||
radius,
|
||||
feather,
|
||||
colors,
|
||||
} => {
|
||||
let mut transform = Transform2D::translation(x + width * 0.5, y + height * 0.5);
|
||||
transform *= *global_transform;
|
||||
inv_transform = transform.inverse();
|
||||
|
||||
params.extent[0] = width * 0.5;
|
||||
params.extent[1] = height * 0.5;
|
||||
params.radius = *radius;
|
||||
params.feather = *feather;
|
||||
match colors {
|
||||
GradientColors::TwoStop { start_color, end_color } => {
|
||||
params.inner_col = start_color.premultiplied().to_array();
|
||||
params.outer_col = end_color.premultiplied().to_array();
|
||||
params.shader_type = ShaderType::FillGradient;
|
||||
}
|
||||
GradientColors::MultiStop { .. } => {
|
||||
params.shader_type = ShaderType::FillImageGradient;
|
||||
}
|
||||
}
|
||||
}
|
||||
&PaintFlavor::RadialGradient {
|
||||
center: Position { x: cx, y: cy },
|
||||
in_radius,
|
||||
out_radius,
|
||||
colors,
|
||||
} => {
|
||||
let r = (in_radius + out_radius) * 0.5;
|
||||
let f = out_radius - in_radius;
|
||||
|
||||
let mut transform = Transform2D::translation(*cx, *cy);
|
||||
transform *= *global_transform;
|
||||
inv_transform = transform.inverse();
|
||||
|
||||
params.extent[0] = r;
|
||||
params.extent[1] = r;
|
||||
params.radius = r;
|
||||
params.feather = 1.0f32.max(f);
|
||||
match colors {
|
||||
GradientColors::TwoStop { start_color, end_color } => {
|
||||
params.inner_col = start_color.premultiplied().to_array();
|
||||
params.outer_col = end_color.premultiplied().to_array();
|
||||
params.shader_type = ShaderType::FillGradient;
|
||||
}
|
||||
GradientColors::MultiStop { .. } => {
|
||||
params.shader_type = ShaderType::FillImageGradient;
|
||||
}
|
||||
}
|
||||
}
|
||||
&PaintFlavor::ConicGradient {
|
||||
center: Position { x: cx, y: cy },
|
||||
colors,
|
||||
} => {
|
||||
let mut transform = Transform2D::translation(*cx, *cy);
|
||||
transform *= *global_transform;
|
||||
inv_transform = transform.inverse();
|
||||
|
||||
match colors {
|
||||
GradientColors::TwoStop { start_color, end_color } => {
|
||||
params.inner_col = start_color.premultiplied().to_array();
|
||||
params.outer_col = end_color.premultiplied().to_array();
|
||||
params.shader_type = ShaderType::FillGradientConic;
|
||||
}
|
||||
GradientColors::MultiStop { .. } => {
|
||||
params.shader_type = ShaderType::FillImageGradientConic;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.paint_mat = inv_transform.to_mat3x4();
|
||||
|
||||
params
|
||||
}
|
||||
|
||||
pub(crate) fn uses_glyph_texture(self) -> bool {
|
||||
self.glyph_texture_type != 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use imgref::ImgVec;
|
||||
use rgb::RGBA8;
|
||||
|
||||
use crate::{ErrorKind, ImageInfo, ImageSource, ImageStore};
|
||||
|
||||
use super::{Command, ImageId, Renderer, Vertex};
|
||||
|
||||
/// Void renderer used for testing
|
||||
#[derive(Debug)]
|
||||
pub struct Void;
|
||||
|
||||
impl Renderer for Void {
|
||||
type Image = VoidImage;
|
||||
type NativeTexture = ();
|
||||
type RenderOutput = ();
|
||||
type CommandBuffer = ();
|
||||
|
||||
fn set_size(&mut self, width: u32, height: u32, dpi: f32) {}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
_output: impl Into<Self::RenderOutput>,
|
||||
images: &mut ImageStore<VoidImage>,
|
||||
verts: &[Vertex],
|
||||
commands: Vec<Command>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn alloc_image(&mut self, info: ImageInfo) -> Result<Self::Image, ErrorKind> {
|
||||
Ok(VoidImage { info })
|
||||
}
|
||||
|
||||
fn create_image_from_native_texture(
|
||||
&mut self,
|
||||
_native_texture: Self::NativeTexture,
|
||||
_info: ImageInfo,
|
||||
) -> Result<Self::Image, ErrorKind> {
|
||||
Err(ErrorKind::UnsupportedImageFormat)
|
||||
}
|
||||
|
||||
fn update_image(
|
||||
&mut self,
|
||||
image: &mut Self::Image,
|
||||
data: ImageSource,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> Result<(), ErrorKind> {
|
||||
let size = data.dimensions();
|
||||
|
||||
if x + size.width > image.info.width() {
|
||||
return Err(ErrorKind::ImageUpdateOutOfBounds);
|
||||
}
|
||||
|
||||
if y + size.height > image.info.height() {
|
||||
return Err(ErrorKind::ImageUpdateOutOfBounds);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_image(&mut self, image: Self::Image, _image_id: ImageId) {}
|
||||
|
||||
fn screenshot(&mut self) -> Result<ImgVec<RGBA8>, ErrorKind> {
|
||||
Ok(ImgVec::new(Vec::new(), 0, 0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VoidImage {
|
||||
info: ImageInfo,
|
||||
}
|
||||
+1713
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,280 @@
|
||||
struct Params {
|
||||
scissor_mat: mat3x4<f32>,
|
||||
paint_mat: mat3x4<f32>,
|
||||
inner_col: vec4<f32>,
|
||||
outer_col: vec4<f32>,
|
||||
scissor_ext: vec2<f32>,
|
||||
scissor_scale: vec2<f32>,
|
||||
extent: vec2<f32>,
|
||||
radius: f32,
|
||||
feather: f32,
|
||||
stroke_mult: f32,
|
||||
stroke_thr: f32,
|
||||
tex_type: f32,
|
||||
shader_type: f32,
|
||||
glyph_texture_type: f32, // 0 -> no glyph rendering, 1 -> alpha mask, 2 -> color texture
|
||||
image_blur_filter_sigma: f32,
|
||||
image_blur_filter_direction: vec2<f32>,
|
||||
image_blur_filter_coeff: vec3<f32>,
|
||||
}
|
||||
|
||||
const SHADER_TYPE_FillGradient: i32 = 0;
|
||||
const SHADER_TYPE_FillImage: i32 = 1;
|
||||
const SHADER_TYPE_Stencil: i32 = 2;
|
||||
const SHADER_TYPE_FillImageGradient: i32 = 3;
|
||||
const SHADER_TYPE_FilterImage: i32 = 4;
|
||||
const SHADER_TYPE_FillColor: i32 = 5;
|
||||
const SHADER_TYPE_TextureCopyUnclipped: i32 = 6;
|
||||
const SHADER_TYPE_FillColorUnclipped: i32 = 7;
|
||||
const SHADER_TYPE_FillGradientConic: i32 = 8;
|
||||
const SHADER_TYPE_FillImageGradientConic: i32 = 9;
|
||||
|
||||
const TAU: f32 = 6.28318530717958647692528676655900577;
|
||||
|
||||
struct ViewSize {
|
||||
x: f32,
|
||||
y: f32,
|
||||
pad: vec2<f32>,
|
||||
}
|
||||
|
||||
@group(0)
|
||||
@binding(0)
|
||||
var<uniform> viewSize: ViewSize;
|
||||
|
||||
@group(1)
|
||||
@binding(0)
|
||||
var<uniform> params: Params;
|
||||
|
||||
struct Vertex {
|
||||
vertex: vec2<f32>,
|
||||
tcoord: vec2<f32>,
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) ftcoord: vec2<f32>,
|
||||
@location(1) fpos: vec2<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@location(0) vertex: vec2<f32>,
|
||||
@location(1) tcoord: vec2<f32>,
|
||||
) -> VertexOutput {
|
||||
var result: VertexOutput;
|
||||
result.ftcoord = tcoord;
|
||||
result.fpos = vertex;
|
||||
result.position = vec4<f32>(2.0 * vertex.x / viewSize.x - 1.0, 1.0 - 2.0 * vertex.y / viewSize.y, 0, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main_texture(
|
||||
@location(0) vertex: vec2<f32>,
|
||||
@location(1) tcoord: vec2<f32>,
|
||||
) -> VertexOutput {
|
||||
var result: VertexOutput;
|
||||
result.ftcoord = tcoord;
|
||||
result.fpos = vertex;
|
||||
result.position = vec4<f32>(2.0 * vertex.x / viewSize.x - 1.0, 2.0 * vertex.y / viewSize.y - 1.0, 0, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
@group(1)
|
||||
@binding(1)
|
||||
var image_texture: texture_2d<f32>;
|
||||
@group(1)
|
||||
@binding(2)
|
||||
var image_sampler: sampler;
|
||||
|
||||
@group(1)
|
||||
@binding(3)
|
||||
var glyph_texture: texture_2d<f32>;
|
||||
@group(1)
|
||||
@binding(4)
|
||||
var glyph_sampler: sampler;
|
||||
|
||||
|
||||
@fragment
|
||||
fn fs_main(vertex: VertexOutput) -> @location(0) vec4<f32> {
|
||||
var result: vec4<f32>;
|
||||
let shader_type_int: i32 = i32(params.shader_type);
|
||||
|
||||
var strokeAlpha: f32 = 1.0;
|
||||
if (shader_type_int != SHADER_TYPE_TextureCopyUnclipped && shader_type_int != SHADER_TYPE_FillColorUnclipped && shader_type_int != SHADER_TYPE_FilterImage) {
|
||||
strokeAlpha = strokeMask(vertex, params);
|
||||
if (strokeAlpha < params.stroke_thr) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
switch (shader_type_int) {
|
||||
case SHADER_TYPE_FillGradient: {
|
||||
// Gradient
|
||||
result = renderGradient(vertex, params);
|
||||
}
|
||||
case SHADER_TYPE_FillImageGradient: {
|
||||
// Image-based Gradient; sample a texture using the gradient position.
|
||||
result = renderImageGradient(vertex, params);
|
||||
}
|
||||
case SHADER_TYPE_FillImage: {
|
||||
// Image
|
||||
result = renderImage(vertex, params);
|
||||
}
|
||||
case SHADER_TYPE_FillColor: {
|
||||
// Plain color fill
|
||||
result = params.inner_col;
|
||||
}
|
||||
case SHADER_TYPE_TextureCopyUnclipped: {
|
||||
// Plain texture copy, unclipped
|
||||
return renderPlainTextureCopy(vertex, params);
|
||||
}
|
||||
case SHADER_TYPE_Stencil: {
|
||||
// Stencil fill
|
||||
result = vec4<f32>(1,1,1,1);
|
||||
}
|
||||
case SHADER_TYPE_FilterImage: {
|
||||
// Filter Image
|
||||
return renderFilteredImage(vertex, params);
|
||||
}
|
||||
case SHADER_TYPE_FillColorUnclipped: {
|
||||
// Plain color fill
|
||||
return params.inner_col;
|
||||
}
|
||||
case SHADER_TYPE_FillGradientConic: {
|
||||
let d = conicAngleFraction(vertex, params);
|
||||
return mix(params.inner_col,params.outer_col,d);
|
||||
}
|
||||
case SHADER_TYPE_FillImageGradientConic: {
|
||||
let d = conicAngleFraction(vertex, params);
|
||||
return textureSample(image_texture, image_sampler, vec2<f32>(d, 0.0));
|
||||
}
|
||||
default: {
|
||||
result = vec4<f32>(0.0, 0.0, 1.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
var scissor: f32 = scissorMask(vertex.fpos, params);
|
||||
|
||||
if (params.glyph_texture_type != 0.0) {
|
||||
// Textured tris
|
||||
var mask: vec4<f32> = textureSample(glyph_texture, glyph_sampler, vertex.ftcoord);
|
||||
|
||||
if (params.glyph_texture_type == 1) {
|
||||
mask = vec4<f32>(mask.x);
|
||||
} else {
|
||||
result = vec4<f32>(1, 1, 1, 1);
|
||||
mask = vec4<f32>(mask.xyz * mask.w, mask.w);
|
||||
}
|
||||
|
||||
mask *= scissor;
|
||||
result *= mask;
|
||||
} else if (shader_type_int != SHADER_TYPE_Stencil && shader_type_int != SHADER_TYPE_FilterImage) {
|
||||
// Not stencil fill
|
||||
// Combine alpha
|
||||
result *= strokeAlpha * scissor;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn conicAngleFraction(vertex: VertexOutput, params: Params) -> f32 {
|
||||
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy;
|
||||
return (-atan2(pt.x,pt.y) / TAU) + 0.5;
|
||||
}
|
||||
|
||||
fn sdroundrect(pt: vec2<f32>, ext: vec2<f32>, rad: f32) -> f32 {
|
||||
let ext2: vec2<f32> = ext - vec2<f32>(rad,rad);
|
||||
let d: vec2<f32> = abs(pt) - ext2;
|
||||
return min(max(d.x,d.y),0.0) + length(max(d, vec2<f32>(0.0, 0.0))) - rad;
|
||||
}
|
||||
|
||||
// Scissoring
|
||||
fn scissorMask(p: vec2<f32>, params: Params) -> f32 {
|
||||
var sc: vec2<f32> = (abs((params.scissor_mat * vec3<f32>(p,1.0)).xy) - params.scissor_ext);
|
||||
sc = vec2(0.5,0.5) - sc * params.scissor_scale;
|
||||
return clamp(sc.x,0.0,1.0) * clamp(sc.y,0.0,1.0);
|
||||
}
|
||||
|
||||
// Stroke - from [0..1] to clipped pyramid, where the slope is 1px.
|
||||
fn strokeMask(vertex: VertexOutput, params: Params) -> f32 {
|
||||
return min(1.0, (1.0-abs(vertex.ftcoord.x*2.0-1.0))*params.stroke_mult) * min(1.0, vertex.ftcoord.y);
|
||||
// Using this smoothstep preduces maybe better results when combined with fringe_width of 2, but it may look blurrier
|
||||
// maybe this should be controlled via flag
|
||||
//return smoothstep(0.0, 1.0, (1.0-abs(vertex.ftcoord.x*2.0-1.0))*params.stroke_mult) * smoothstep(0.0, 1.0, vertex.ftcoord.y);
|
||||
}
|
||||
|
||||
fn renderGradient(vertex: VertexOutput, params: Params) -> vec4<f32> {
|
||||
// Calculate gradient color using box gradient
|
||||
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy;
|
||||
|
||||
let d: f32 = clamp((sdroundrect(pt, params.extent, params.radius) + params.feather*0.5) / params.feather, 0.0, 1.0);
|
||||
return mix(params.inner_col,params.outer_col,d);
|
||||
}
|
||||
|
||||
// Image-based Gradient; sample a texture using the gradient position.
|
||||
fn renderImageGradient(vertex: VertexOutput, params: Params) -> vec4<f32> {
|
||||
// Calculate gradient color using box gradient
|
||||
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy;
|
||||
|
||||
let d: f32 = clamp((sdroundrect(pt, params.extent, params.radius) + params.feather*0.5) / params.feather, 0.0, 1.0);
|
||||
return textureSample(image_texture, image_sampler, vec2<f32>(d, 0.0));//mix(innerCol,outerCol,d);
|
||||
}
|
||||
|
||||
fn renderImage(vertex: VertexOutput, params: Params) -> vec4<f32> {
|
||||
// Calculate color from texture
|
||||
let pt: vec2<f32> = (params.paint_mat * vec3<f32>(vertex.fpos, 1.0)).xy / params.extent;
|
||||
|
||||
var color: vec4<f32> = textureSample(image_texture, image_sampler, pt);
|
||||
|
||||
if (params.tex_type == 1) { color = vec4(color.xyz * color.w, color.w); }
|
||||
if (params.tex_type == 2) { color = vec4(color.x); }
|
||||
|
||||
// Apply color tint and alpha.
|
||||
color *= params.inner_col;
|
||||
return color;
|
||||
}
|
||||
|
||||
fn renderPlainTextureCopy(vertex: VertexOutput, params: Params) -> vec4<f32> {
|
||||
var color: vec4<f32> = textureSample(image_texture, image_sampler, vertex.ftcoord);
|
||||
|
||||
if (params.tex_type == 1) { color = vec4(color.xyz * color.w, color.w); }
|
||||
if (params.tex_type == 2) { color = vec4(color.x); }
|
||||
// Apply color tint and alpha.
|
||||
color *= params.inner_col;
|
||||
return color;
|
||||
}
|
||||
|
||||
fn renderFilteredImage(vertex: VertexOutput, params: Params) -> vec4<f32> {
|
||||
let sampleCount: f32 = ceil(1.5 * params.image_blur_filter_sigma);
|
||||
|
||||
var gaussian_coeff: vec3<f32> = params.image_blur_filter_coeff;
|
||||
|
||||
var color_sum: vec4<f32> = textureSample(image_texture, image_sampler, vertex.fpos.xy / params.extent) * gaussian_coeff.x;
|
||||
var coefficient_sum: f32 = gaussian_coeff.x;
|
||||
gaussian_coeff.x *= gaussian_coeff.y;
|
||||
gaussian_coeff.y *= gaussian_coeff.z;
|
||||
|
||||
for (var i: f32 = 1.0; i <= 12.0; i += 1.) {
|
||||
// Work around GLES 2.0 limitation of only allowing constant loop indices
|
||||
// by breaking here. Sigma has an upper bound of 8, imposed on the Rust side.
|
||||
if (i >= sampleCount) {
|
||||
break;
|
||||
}
|
||||
color_sum += textureSample(image_texture, image_sampler, (vertex.fpos.xy - i * params.image_blur_filter_direction) / params.extent) * gaussian_coeff.x;
|
||||
color_sum += textureSample(image_texture, image_sampler, (vertex.fpos.xy + i * params.image_blur_filter_direction) / params.extent) * gaussian_coeff.x;
|
||||
coefficient_sum += 2.0 * gaussian_coeff.x;
|
||||
|
||||
// Compute the coefficients incrementally:
|
||||
// https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-40-incremental-computation-gaussian
|
||||
gaussian_coeff.x *= gaussian_coeff.y;
|
||||
gaussian_coeff.y *= gaussian_coeff.z;
|
||||
}
|
||||
|
||||
var color: vec4<f32> = color_sum / coefficient_sum;
|
||||
|
||||
if (params.tex_type == 1) { color = vec4<f32>(color.xyz * color.w, color.w); }
|
||||
if (params.tex_type == 2) { color = vec4<f32>(color.x); }
|
||||
|
||||
return color;
|
||||
}
|
||||
+1070
File diff suppressed because it is too large.
Load diff
+166
@@ -0,0 +1,166 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
}
|
||||
|
||||
/// Represents an atlas for packing rectangles.
|
||||
#[derive(Debug)]
|
||||
pub struct Atlas {
|
||||
width: usize,
|
||||
height: usize,
|
||||
nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
impl Atlas {
|
||||
/// Creates a new atlas with the specified width and height.
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
nodes: vec![Node { x: 0, y: 0, width }],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the size (width and height) of the atlas.
|
||||
pub fn size(&self) -> (usize, usize) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Expands the atlas to the specified width and height.
|
||||
pub fn expand(&mut self, width: usize, height: usize) {
|
||||
// Insert node for empty space
|
||||
|
||||
if width > self.width {
|
||||
self.insert_node(self.nodes.len(), self.width, 0, width - self.width);
|
||||
}
|
||||
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
/// Resets the atlas to the specified width and height.
|
||||
pub fn reset(&mut self, width: usize, height: usize) {
|
||||
*self = Self::new(width, height);
|
||||
}
|
||||
|
||||
/// Adds a rectangle with the specified width and height to the atlas.
|
||||
///
|
||||
/// Returns the position (x, y) of the added rectangle, or `None` if the rectangle cannot be added.
|
||||
pub fn add_rect(&mut self, rect_width: usize, rect_height: usize) -> Option<(usize, usize)> {
|
||||
let mut besth = self.height;
|
||||
let mut bestw = self.width;
|
||||
let mut besti = None;
|
||||
let mut bestx = 0;
|
||||
let mut besty = 0;
|
||||
|
||||
// Bottom left fit heuristic.
|
||||
for i in 0..self.nodes.len() {
|
||||
if let Some(y) = self.rect_fits(i, rect_width, rect_height) {
|
||||
if y + rect_height < besth || (y + rect_height == besth && self.nodes[i].width < bestw) {
|
||||
besti = Some(i);
|
||||
bestw = self.nodes[i].width;
|
||||
besth = y + rect_height;
|
||||
bestx = self.nodes[i].x;
|
||||
besty = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(besti) = besti {
|
||||
// Perform the actual packing.
|
||||
self.add_skyline_level(besti, bestx, besty, rect_width, rect_height);
|
||||
return Some((bestx, besty));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn insert_node(&mut self, idx: usize, x: usize, y: usize, width: usize) {
|
||||
self.nodes.insert(idx, Node { x, y, width });
|
||||
}
|
||||
|
||||
fn remove_node(&mut self, idx: usize) {
|
||||
self.nodes.remove(idx);
|
||||
}
|
||||
|
||||
fn add_skyline_level(&mut self, idx: usize, x: usize, y: usize, width: usize, height: usize) {
|
||||
// Insert new node
|
||||
self.insert_node(idx, x, y + height, width);
|
||||
|
||||
// Delete skyline segments that fall under the shadow of the new segment.
|
||||
let mut i = idx + 1;
|
||||
|
||||
while i < self.nodes.len() {
|
||||
if self.nodes[i].x < self.nodes[i - 1].x + self.nodes[i - 1].width {
|
||||
let shrink = self.nodes[i - 1].x + self.nodes[i - 1].width - self.nodes[i].x;
|
||||
|
||||
self.nodes[i].x += shrink;
|
||||
let new_width = self.nodes[i].width as isize - shrink as isize;
|
||||
|
||||
if new_width <= 0 {
|
||||
self.remove_node(i);
|
||||
i -= 1;
|
||||
} else {
|
||||
self.nodes[i].width = new_width as usize;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Merge same height skyline segments that are next to each other.
|
||||
let mut i = 0isize;
|
||||
|
||||
while i < self.nodes.len() as isize - 1 {
|
||||
let index = i as usize;
|
||||
|
||||
if self.nodes[index].y == self.nodes[index + 1].y {
|
||||
self.nodes[index].width += self.nodes[index + 1].width;
|
||||
self.remove_node(index + 1);
|
||||
|
||||
i -= 1;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_fits(&self, mut idx: usize, width: usize, height: usize) -> Option<usize> {
|
||||
// Checks if there is enough space at the location of skyline span 'i',
|
||||
// and return the max height of all skyline spans under that at that location,
|
||||
// (think tetris block being dropped at that position). Or -1 if no space found.
|
||||
|
||||
let x = self.nodes[idx].x;
|
||||
let mut y = self.nodes[idx].y;
|
||||
|
||||
if x + width > self.width {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut space_left = width as isize;
|
||||
|
||||
while space_left > 0 {
|
||||
if idx == self.nodes.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
y = y.max(self.nodes[idx].y);
|
||||
|
||||
if y + height > self.height {
|
||||
return None;
|
||||
}
|
||||
|
||||
space_left -= self.nodes[idx].width as isize;
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
Some(y)
|
||||
}
|
||||
}
|
||||
+665
@@ -0,0 +1,665 @@
|
||||
use fnv::FnvHashMap;
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fmt;
|
||||
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
|
||||
use std::rc::Rc;
|
||||
#[cfg(feature = "textlayout")]
|
||||
use ttf_parser::{Face as TtfFont, GlyphId};
|
||||
|
||||
use crate::{paint::FontVariations, ErrorKind, Path};
|
||||
|
||||
/// Abstraction over the parsed font face, so callers don't need cfg blocks.
|
||||
/// With `textlayout`, this wraps a `ttf_parser::Face`.
|
||||
/// Otherwise, this is a zero-sized type.
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub(crate) struct FontFaceRef<'a>(pub(crate) ttf_parser::Face<'a>);
|
||||
|
||||
#[cfg(not(feature = "textlayout"))]
|
||||
pub(crate) struct FontFaceRef<'a>(std::marker::PhantomData<&'a ()>);
|
||||
|
||||
/// Information about a font variation axis (e.g. weight, width, optical size).
|
||||
///
|
||||
/// Returned by [`Canvas::font_variation_axes`](crate::Canvas::font_variation_axes)
|
||||
/// in the order they appear in the font's OpenType `fvar` table. The position
|
||||
/// of each axis in that vector determines the index of the corresponding
|
||||
/// normalized coordinate (`i16` in F2DOT14 format) when calling
|
||||
/// [`Canvas::fill_glyph_run`](crate::Canvas::fill_glyph_run) or
|
||||
/// [`Canvas::stroke_glyph_run`](crate::Canvas::stroke_glyph_run).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VariationAxisInfo {
|
||||
/// Four-byte axis tag (e.g. `*b"wght"` for weight).
|
||||
pub tag: [u8; 4],
|
||||
/// Minimum value for this axis.
|
||||
pub min_value: f32,
|
||||
/// Default value for this axis.
|
||||
pub def_value: f32,
|
||||
/// Maximum value for this axis.
|
||||
pub max_value: f32,
|
||||
/// Name table ID for this axis's name.
|
||||
pub name_id: u16,
|
||||
/// Whether this axis is hidden from the user.
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlyphMetrics {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub bearing_x: f32,
|
||||
pub bearing_y: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Glyph {
|
||||
pub path: Option<Path>, // None means render as image
|
||||
pub metrics: GlyphMetrics,
|
||||
}
|
||||
|
||||
pub enum GlyphRendering<'a> {
|
||||
RenderAsPath(Ref<'a, Path>),
|
||||
#[cfg(feature = "image-loading")]
|
||||
RenderAsImage(image::DynamicImage),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct FontFlags(u8);
|
||||
|
||||
// TODO: underline, strikeout, subscript, superscript metrics
|
||||
impl FontFlags {
|
||||
fn new(regular: bool, italic: bool, bold: bool, oblique: bool, variable: bool) -> Self {
|
||||
let mut flags = 0;
|
||||
if regular {
|
||||
flags |= 0x1;
|
||||
}
|
||||
if italic {
|
||||
flags |= 0x2;
|
||||
}
|
||||
if bold {
|
||||
flags |= 0x4;
|
||||
}
|
||||
if oblique {
|
||||
flags |= 0x8;
|
||||
}
|
||||
if variable {
|
||||
flags |= 0x10;
|
||||
}
|
||||
Self(flags)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn regular(&self) -> bool {
|
||||
self.0 & 0x1 > 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn italic(&self) -> bool {
|
||||
self.0 & 0x2 > 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bold(&self) -> bool {
|
||||
self.0 & 0x4 > 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn oblique(&self) -> bool {
|
||||
self.0 & 0x8 > 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn variable(&self) -> bool {
|
||||
self.0 & 0x10 > 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a font.
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
pub struct FontMetrics {
|
||||
ascender: f32,
|
||||
descender: f32,
|
||||
height: f32,
|
||||
flags: FontFlags,
|
||||
weight: u16,
|
||||
width: u16,
|
||||
}
|
||||
|
||||
impl FontMetrics {
|
||||
fn scale(&mut self, scale: f32) {
|
||||
self.ascender *= scale;
|
||||
self.descender *= scale;
|
||||
self.height *= scale;
|
||||
}
|
||||
|
||||
/// Returns the distance from the baseline to the top of the highest glyph.
|
||||
pub fn ascender(&self) -> f32 {
|
||||
self.ascender
|
||||
}
|
||||
|
||||
/// Returns the distance from the baseline to the bottom of the lowest descenders on the glyphs.
|
||||
pub fn descender(&self) -> f32 {
|
||||
self.descender
|
||||
}
|
||||
|
||||
/// Returns the height of the font.
|
||||
pub fn height(&self) -> f32 {
|
||||
self.height.round()
|
||||
}
|
||||
|
||||
/// Returns if the font is regular.
|
||||
pub fn regular(&self) -> bool {
|
||||
self.flags.regular()
|
||||
}
|
||||
|
||||
/// Returns if the font is italic.
|
||||
pub fn italic(&self) -> bool {
|
||||
self.flags.italic()
|
||||
}
|
||||
|
||||
/// Returns if the font is bold.
|
||||
pub fn bold(&self) -> bool {
|
||||
self.flags.bold()
|
||||
}
|
||||
|
||||
/// Returns if the font is oblique.
|
||||
pub fn oblique(&self) -> bool {
|
||||
self.flags.oblique()
|
||||
}
|
||||
|
||||
/// Returns if the font is a variable font.
|
||||
pub fn variable(&self) -> bool {
|
||||
self.flags.variable()
|
||||
}
|
||||
|
||||
/// Returns the weight of the font.
|
||||
pub fn weight(&self) -> u16 {
|
||||
self.weight
|
||||
}
|
||||
|
||||
/// Returns the width of the font.
|
||||
pub fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Font {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Font")
|
||||
.field("data", &format_args!(".."))
|
||||
.field("face_index", &self.face_index)
|
||||
.field("units_per_em", &self.units_per_em)
|
||||
.field("metrics", &self.metrics)
|
||||
.field("glyphs", &self.glyphs)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Key for the glyph cache: (glyph_id, normalized_coords_hash).
|
||||
/// The hash covers all normalized variation coordinates, or 0 for defaults.
|
||||
type GlyphCacheKey = (u16, u64);
|
||||
|
||||
pub struct Font {
|
||||
data: Box<dyn AsRef<[u8]>>,
|
||||
face_index: u32,
|
||||
units_per_em: u16,
|
||||
metrics: FontMetrics,
|
||||
glyphs: RefCell<FnvHashMap<GlyphCacheKey, Glyph>>,
|
||||
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
|
||||
swash_scale_context: Rc<RefCell<swash::scale::ScaleContext>>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub fn new_with_data<T: AsRef<[u8]> + 'static>(
|
||||
data: T,
|
||||
face_index: u32,
|
||||
_text_context: &super::TextContextImpl,
|
||||
) -> Result<Self, ErrorKind> {
|
||||
let ttf_font = TtfFont::parse(data.as_ref(), face_index).map_err(|_| ErrorKind::FontParseError)?;
|
||||
|
||||
let units_per_em = ttf_font.units_per_em();
|
||||
|
||||
let metrics = FontMetrics {
|
||||
ascender: ttf_font.ascender() as f32,
|
||||
descender: ttf_font.descender() as f32,
|
||||
height: ttf_font.height() as f32,
|
||||
flags: FontFlags::new(
|
||||
ttf_font.is_regular(),
|
||||
ttf_font.is_italic(),
|
||||
ttf_font.is_bold(),
|
||||
ttf_font.is_oblique(),
|
||||
ttf_font.is_variable(),
|
||||
),
|
||||
width: ttf_font.width().to_number(),
|
||||
weight: ttf_font.weight().to_number(),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
data: Box::new(data),
|
||||
face_index,
|
||||
units_per_em,
|
||||
metrics,
|
||||
glyphs: RefCell::default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
|
||||
pub fn new_with_data<T: AsRef<[u8]> + 'static>(
|
||||
data: T,
|
||||
face_index: u32,
|
||||
text_context: &super::TextContextImpl,
|
||||
) -> Result<Self, ErrorKind> {
|
||||
let font_ref =
|
||||
swash::FontRef::from_index(data.as_ref(), face_index as usize).ok_or(ErrorKind::FontParseError)?;
|
||||
|
||||
let swash_metrics = font_ref.metrics(&[]);
|
||||
let attrs = font_ref.attributes();
|
||||
|
||||
let units_per_em = swash_metrics.units_per_em;
|
||||
|
||||
let weight = attrs.weight().0;
|
||||
let stretch = attrs.stretch();
|
||||
let style = attrs.style();
|
||||
|
||||
let is_bold = weight >= 700;
|
||||
let is_italic = matches!(style, swash::Style::Italic);
|
||||
let is_oblique = matches!(style, swash::Style::Oblique(_));
|
||||
let is_variable = font_ref.variations().next().is_some();
|
||||
|
||||
let is_regular = weight == 400 && matches!(style, swash::Style::Normal) && stretch == swash::Stretch::NORMAL;
|
||||
|
||||
// Map swash::Stretch to usWidthClass values as per
|
||||
// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
|
||||
let width: u16 = match stretch {
|
||||
swash::Stretch::ULTRA_CONDENSED => 1,
|
||||
swash::Stretch::EXTRA_CONDENSED => 2,
|
||||
swash::Stretch::CONDENSED => 3,
|
||||
swash::Stretch::SEMI_CONDENSED => 4,
|
||||
swash::Stretch::NORMAL => 5,
|
||||
swash::Stretch::SEMI_EXPANDED => 6,
|
||||
swash::Stretch::EXPANDED => 7,
|
||||
swash::Stretch::EXTRA_EXPANDED => 8,
|
||||
swash::Stretch::ULTRA_EXPANDED => 9,
|
||||
_ => 5,
|
||||
};
|
||||
|
||||
let metrics = FontMetrics {
|
||||
ascender: swash_metrics.ascent,
|
||||
descender: -swash_metrics.descent,
|
||||
// swash ascent and descent are both positive (distance from baseline),
|
||||
// unlike ttf-parser where descent is negative, so this is a sum not a difference.
|
||||
height: swash_metrics.ascent + swash_metrics.descent + swash_metrics.leading,
|
||||
flags: FontFlags::new(is_regular, is_italic, is_bold, is_oblique, is_variable),
|
||||
weight,
|
||||
width,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
data: Box::new(data),
|
||||
face_index,
|
||||
units_per_em,
|
||||
metrics,
|
||||
glyphs: RefCell::default(),
|
||||
swash_scale_context: text_context.swash_scale_context(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
|
||||
pub fn new_with_data<T: AsRef<[u8]> + 'static>(
|
||||
_data: T,
|
||||
_face_index: u32,
|
||||
_text_context: &super::TextContextImpl,
|
||||
) -> Result<Self, ErrorKind> {
|
||||
Err(ErrorKind::FontParseError)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn data(&self) -> &[u8] {
|
||||
(*self.data).as_ref()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn face_index(&self) -> u32 {
|
||||
self.face_index
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub(crate) fn face_ref(&self) -> FontFaceRef<'_> {
|
||||
FontFaceRef(ttf_parser::Face::parse(self.data.as_ref().as_ref(), self.face_index).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "textlayout"))]
|
||||
pub(crate) fn face_ref(&self) -> FontFaceRef<'_> {
|
||||
FontFaceRef(std::marker::PhantomData)
|
||||
}
|
||||
|
||||
#[cfg(feature = "swash")]
|
||||
pub(crate) fn swash_font_ref(&self) -> Option<swash::FontRef<'_>> {
|
||||
swash::FontRef::from_index(self.data.as_ref().as_ref(), self.face_index as usize)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
|
||||
fn swash_scale_context(&self) -> &RefCell<swash::scale::ScaleContext> {
|
||||
&self.swash_scale_context
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub(crate) fn face_ref_with_variations(&self, variations: &FontVariations) -> FontFaceRef<'_> {
|
||||
let mut face = self.face_ref();
|
||||
for (tag, value) in variations.iter() {
|
||||
face.0.set_variation(ttf_parser::Tag(tag), value);
|
||||
}
|
||||
face
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "textlayout"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn face_ref_with_variations(&self, _variations: &FontVariations) -> FontFaceRef<'_> {
|
||||
// Without ttf-parser, face is a phantom type. Variations are applied
|
||||
// through the swash scaler in the glyph() method instead.
|
||||
self.face_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub(crate) fn face_ref_with_normalized_coords(&self, coords: &[i16]) -> FontFaceRef<'_> {
|
||||
let mut face = self.face_ref();
|
||||
// Round-trip: convert normalized coords back to design-space values
|
||||
// so ttf-parser can re-normalize them (including avar remapping).
|
||||
// Coords are zipped with axes in order — the i-th coord maps to the i-th axis.
|
||||
for (axis, coord) in face.0.variation_axes().into_iter().zip(coords.iter()) {
|
||||
let value = *coord as f32 / 16384.0;
|
||||
let design_value = if value >= 0.0 {
|
||||
axis.def_value + value * (axis.max_value - axis.def_value)
|
||||
} else {
|
||||
axis.def_value + value * (axis.def_value - axis.min_value)
|
||||
};
|
||||
face.0.set_variation(axis.tag, design_value);
|
||||
}
|
||||
face
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "textlayout"))]
|
||||
pub(crate) fn face_ref_with_normalized_coords(&self, _coords: &[i16]) -> FontFaceRef<'_> {
|
||||
self.face_ref()
|
||||
}
|
||||
|
||||
pub fn metrics(&self, size: f32) -> FontMetrics {
|
||||
let mut metrics = self.metrics;
|
||||
|
||||
metrics.scale(self.scale(size));
|
||||
|
||||
metrics
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub fn metrics_with_variations(&self, size: f32, variations: &FontVariations) -> FontMetrics {
|
||||
if variations.is_empty() {
|
||||
return self.metrics(size);
|
||||
}
|
||||
|
||||
let face = self.face_ref_with_variations(variations);
|
||||
let mut metrics = FontMetrics {
|
||||
ascender: face.0.ascender() as f32,
|
||||
descender: face.0.descender() as f32,
|
||||
height: face.0.height() as f32,
|
||||
..self.metrics
|
||||
};
|
||||
metrics.scale(self.scale(size));
|
||||
metrics
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
|
||||
pub fn metrics_with_variations(&self, size: f32, variations: &FontVariations) -> FontMetrics {
|
||||
if variations.is_empty() {
|
||||
return self.metrics(size);
|
||||
}
|
||||
let Some(font_ref) = self.swash_font_ref() else {
|
||||
return self.metrics(size);
|
||||
};
|
||||
let settings: Vec<swash::Setting<f32>> = variations
|
||||
.iter()
|
||||
.map(|(tag, value)| swash::Setting {
|
||||
tag: swash::tag_from_bytes(&tag.to_be_bytes()),
|
||||
value,
|
||||
})
|
||||
.collect();
|
||||
let coords: Vec<swash::NormalizedCoord> = font_ref.variations().normalized_coords(settings).collect();
|
||||
let swash_metrics = font_ref.metrics(&coords);
|
||||
let mut metrics = FontMetrics {
|
||||
ascender: swash_metrics.ascent,
|
||||
descender: -swash_metrics.descent,
|
||||
height: swash_metrics.ascent + swash_metrics.descent + swash_metrics.leading,
|
||||
..self.metrics
|
||||
};
|
||||
metrics.scale(self.scale(size));
|
||||
metrics
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
|
||||
pub fn metrics_with_variations(&self, size: f32, _variations: &FontVariations) -> FontMetrics {
|
||||
self.metrics(size)
|
||||
}
|
||||
|
||||
pub fn scale(&self, size: f32) -> f32 {
|
||||
size / self.units_per_em as f32
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub fn variation_axes(&self) -> Vec<VariationAxisInfo> {
|
||||
let face = self.face_ref();
|
||||
let mut axes = Vec::new();
|
||||
if let Some(table) = face.0.tables().fvar {
|
||||
for axis in table.axes {
|
||||
axes.push(VariationAxisInfo {
|
||||
tag: axis.tag.to_bytes(),
|
||||
min_value: axis.min_value,
|
||||
def_value: axis.def_value,
|
||||
max_value: axis.max_value,
|
||||
name_id: axis.name_id,
|
||||
hidden: axis.hidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
axes
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
|
||||
pub fn variation_axes(&self) -> Vec<VariationAxisInfo> {
|
||||
let Some(font_ref) = self.swash_font_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
font_ref
|
||||
.variations()
|
||||
.map(|v| {
|
||||
let name_id = match v.name_id() {
|
||||
swash::StringId::Other(id) => id,
|
||||
_ => 0,
|
||||
};
|
||||
VariationAxisInfo {
|
||||
tag: v.tag().to_be_bytes(),
|
||||
min_value: v.min_value(),
|
||||
def_value: v.default_value(),
|
||||
max_value: v.max_value(),
|
||||
name_id,
|
||||
hidden: v.is_hidden(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
|
||||
pub fn variation_axes(&self) -> Vec<VariationAxisInfo> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub(crate) fn normalize_variations(&self, variations: &FontVariations) -> Vec<i16> {
|
||||
let face = self.face_ref_with_variations(variations);
|
||||
face.0.variation_coordinates().iter().map(|c| c.get()).collect()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn hash_normalized_coords(coords: &[i16]) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = fnv::FnvHasher::default();
|
||||
coords.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub(crate) fn glyph(
|
||||
&self,
|
||||
face: &FontFaceRef<'_>,
|
||||
codepoint: u16,
|
||||
normalized_coords: &[i16],
|
||||
) -> Option<Ref<'_, Glyph>> {
|
||||
let cache_key: GlyphCacheKey = (codepoint, Self::hash_normalized_coords(normalized_coords));
|
||||
|
||||
if let Entry::Vacant(entry) = self.glyphs.borrow_mut().entry(cache_key) {
|
||||
let mut path = Path::new();
|
||||
|
||||
let id = GlyphId(codepoint);
|
||||
|
||||
let maybe_glyph = if let Some(image) = face
|
||||
.0
|
||||
.glyph_raster_image(id, u16::MAX)
|
||||
.filter(|img| img.format == ttf_parser::RasterImageFormat::PNG)
|
||||
{
|
||||
let scale = if image.pixels_per_em != 0 {
|
||||
self.units_per_em as f32 / image.pixels_per_em as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
Some(Glyph {
|
||||
path: None,
|
||||
metrics: GlyphMetrics {
|
||||
width: image.width as f32 * scale,
|
||||
height: image.height as f32 * scale,
|
||||
bearing_x: image.x as f32 * scale,
|
||||
bearing_y: (image.y as f32 + image.height as f32) * scale,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
face.0.outline_glyph(id, &mut path).map(|bbox| Glyph {
|
||||
path: Some(path),
|
||||
metrics: GlyphMetrics {
|
||||
width: bbox.width() as f32,
|
||||
height: bbox.height() as f32,
|
||||
bearing_x: bbox.x_min as f32,
|
||||
bearing_y: bbox.y_max as f32,
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(glyph) = maybe_glyph {
|
||||
entry.insert(glyph);
|
||||
}
|
||||
}
|
||||
|
||||
Ref::filter_map(self.glyphs.borrow(), |glyphs| glyphs.get(&cache_key)).ok()
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "textlayout", feature = "swash")))]
|
||||
pub(crate) fn glyph(
|
||||
&self,
|
||||
_face: &FontFaceRef<'_>,
|
||||
_codepoint: u16,
|
||||
_normalized_coords: &[i16],
|
||||
) -> Option<Ref<'_, Glyph>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "swash", not(feature = "textlayout")))]
|
||||
pub(crate) fn glyph(
|
||||
&self,
|
||||
_face: &FontFaceRef<'_>,
|
||||
codepoint: u16,
|
||||
normalized_coords: &[i16],
|
||||
) -> Option<Ref<'_, Glyph>> {
|
||||
let cache_key: GlyphCacheKey = (codepoint, Self::hash_normalized_coords(normalized_coords));
|
||||
if let Entry::Vacant(entry) = self.glyphs.borrow_mut().entry(cache_key) {
|
||||
let font_ref = self.swash_font_ref()?;
|
||||
|
||||
let mut scale_context = self.swash_scale_context().borrow_mut();
|
||||
let scaler_builder = scale_context
|
||||
.builder(font_ref)
|
||||
.size(self.units_per_em as f32)
|
||||
.hint(false)
|
||||
.normalized_coords(normalized_coords);
|
||||
let mut scaler = scaler_builder.build();
|
||||
|
||||
let maybe_glyph = if let Some(outline) = scaler.scale_outline(codepoint) {
|
||||
use swash::zeno::{Command, PathData};
|
||||
let bounds = outline.bounds();
|
||||
let mut path = Path::new();
|
||||
for cmd in outline.path().commands() {
|
||||
match cmd {
|
||||
Command::MoveTo(p) => path.move_to(p.x, p.y),
|
||||
Command::LineTo(p) => path.line_to(p.x, p.y),
|
||||
Command::QuadTo(c, p) => path.quad_to(c.x, c.y, p.x, p.y),
|
||||
Command::CurveTo(c1, c2, p) => path.bezier_to(c1.x, c1.y, c2.x, c2.y, p.x, p.y),
|
||||
Command::Close => path.close(),
|
||||
}
|
||||
}
|
||||
Some(Glyph {
|
||||
path: Some(path),
|
||||
metrics: GlyphMetrics {
|
||||
width: bounds.width(),
|
||||
height: bounds.height(),
|
||||
bearing_x: bounds.min.x,
|
||||
bearing_y: bounds.max.y,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(glyph) = maybe_glyph {
|
||||
entry.insert(glyph);
|
||||
}
|
||||
}
|
||||
|
||||
Ref::filter_map(self.glyphs.borrow(), |glyphs| glyphs.get(&cache_key)).ok()
|
||||
}
|
||||
|
||||
#[cfg(feature = "textlayout")]
|
||||
pub(crate) fn glyph_rendering_representation(
|
||||
&self,
|
||||
face: &FontFaceRef<'_>,
|
||||
codepoint: u16,
|
||||
#[allow(unused_variables)] pixels_per_em: u16,
|
||||
normalized_coords: &[i16],
|
||||
) -> Option<GlyphRendering<'_>> {
|
||||
#[cfg(feature = "image-loading")]
|
||||
if let Some(image) =
|
||||
face.0
|
||||
.glyph_raster_image(GlyphId(codepoint), pixels_per_em)
|
||||
.and_then(|raster_glyph_image| {
|
||||
image::load_from_memory_with_format(raster_glyph_image.data, image::ImageFormat::Png).ok()
|
||||
})
|
||||
{
|
||||
return Some(GlyphRendering::RenderAsImage(image));
|
||||
}
|
||||
|
||||
self.glyph(face, codepoint, normalized_coords).and_then(|glyph| {
|
||||
Ref::filter_map(glyph, |glyph| glyph.path.as_ref())
|
||||
.ok()
|
||||
.map(GlyphRendering::RenderAsPath)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "textlayout"))]
|
||||
pub(crate) fn glyph_rendering_representation(
|
||||
&self,
|
||||
_face: &FontFaceRef<'_>,
|
||||
codepoint: u16,
|
||||
#[allow(unused_variables)] _pixels_per_em: u16,
|
||||
normalized_coords: &[i16],
|
||||
) -> Option<GlyphRendering<'_>> {
|
||||
self.glyph(_face, codepoint, normalized_coords).and_then(|glyph| {
|
||||
Ref::filter_map(glyph, |glyph| glyph.path.as_ref())
|
||||
.ok()
|
||||
.map(GlyphRendering::RenderAsPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
+558
@@ -0,0 +1,558 @@
|
||||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
use fnv::FnvHasher;
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::{
|
||||
paint::{FontVariations, TextSettings},
|
||||
Align, Baseline, ErrorKind, FontId, Paint,
|
||||
};
|
||||
|
||||
use unicode_bidi::BidiInfo;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct ShapedGlyph {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub c: char,
|
||||
pub byte_index: usize,
|
||||
pub font_id: FontId,
|
||||
pub glyph_id: u16,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub advance_x: f32,
|
||||
pub advance_y: f32,
|
||||
pub offset_x: f32,
|
||||
pub offset_y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(super) struct ShapedWord {
|
||||
glyphs: Vec<ShapedGlyph>,
|
||||
width: f32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub(super) struct ShapingId {
|
||||
size: u32,
|
||||
word_hash: u64,
|
||||
font_ids: [Option<FontId>; 8],
|
||||
variation_hash: u64,
|
||||
}
|
||||
|
||||
impl ShapingId {
|
||||
fn new(
|
||||
font_size: f32,
|
||||
font_ids: [Option<FontId>; 8],
|
||||
word: &str,
|
||||
max_width: Option<f32>,
|
||||
variations: &FontVariations,
|
||||
) -> Self {
|
||||
let mut hasher = FnvHasher::default();
|
||||
word.hash(&mut hasher);
|
||||
if let Some(max_width) = max_width {
|
||||
(max_width.trunc() as i32).hash(&mut hasher);
|
||||
}
|
||||
|
||||
Self {
|
||||
size: (font_size * 10.0).trunc() as u32,
|
||||
word_hash: hasher.finish(),
|
||||
font_ids,
|
||||
variation_hash: variations.hash(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type ShapedWordsCache<H> = LruCache<ShapingId, Result<ShapedWord, ErrorKind>, H>;
|
||||
pub(super) type ShapingRunCache<H> = LruCache<ShapingId, TextMetrics, H>;
|
||||
|
||||
impl super::TextContext {
|
||||
/// Returns information on how the provided text will be drawn with the specified paint.
|
||||
pub fn measure_text<S: AsRef<str>>(
|
||||
&self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
text: S,
|
||||
paint: &Paint,
|
||||
) -> Result<TextMetrics, ErrorKind> {
|
||||
self.0.borrow_mut().measure_text(x, y, text, &paint.text)
|
||||
}
|
||||
|
||||
/// Returns the maximum index-th byte of text that will fit inside `max_width`.
|
||||
///
|
||||
/// The retuned index will always lie at the start and/or end of a UTF-8 code point sequence or at the start or end of the text
|
||||
pub fn break_text<S: AsRef<str>>(&self, max_width: f32, text: S, paint: &Paint) -> Result<usize, ErrorKind> {
|
||||
self.0.borrow_mut().break_text(max_width, text, &paint.text)
|
||||
}
|
||||
|
||||
/// Returnes a list of ranges representing each line of text that will fit inside `max_width`
|
||||
pub fn break_text_vec<S: AsRef<str>>(
|
||||
&self,
|
||||
max_width: f32,
|
||||
text: S,
|
||||
paint: &Paint,
|
||||
) -> Result<Vec<Range<usize>>, ErrorKind> {
|
||||
self.0.borrow_mut().break_text_vec(max_width, text, &paint.text)
|
||||
}
|
||||
|
||||
/// Adjusts the capacity of the shaping run cache. This is a cache for measurements of whole
|
||||
/// strings.
|
||||
pub fn resize_shaping_run_cache(&self, capacity: std::num::NonZeroUsize) {
|
||||
self.0.borrow_mut().resize_shaping_run_cache(capacity)
|
||||
}
|
||||
|
||||
/// Adjusts the capacity of the shaped words cache. This is a cache for measurements of
|
||||
/// individual words. Words are separated by
|
||||
/// [UAX#29 word boundaries](http://www.unicode.org/reports/tr29/#Word_Boundaries).
|
||||
pub fn resize_shaped_words_cache(&self, capacity: std::num::NonZeroUsize) {
|
||||
self.0.borrow_mut().resize_shaped_words_cache(capacity)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::TextContextImpl {
|
||||
pub fn resize_shaping_run_cache(&mut self, capacity: std::num::NonZeroUsize) {
|
||||
self.shaping_run_cache.resize(capacity);
|
||||
}
|
||||
|
||||
pub fn resize_shaped_words_cache(&mut self, capacity: std::num::NonZeroUsize) {
|
||||
self.shaped_words_cache.resize(capacity);
|
||||
}
|
||||
|
||||
pub fn measure_text<S: AsRef<str>>(
|
||||
&mut self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
text: S,
|
||||
text_settings: &TextSettings,
|
||||
) -> Result<TextMetrics, ErrorKind> {
|
||||
shape(x, y, self, text_settings, text.as_ref(), None)
|
||||
}
|
||||
|
||||
pub fn break_text<S: AsRef<str>>(
|
||||
&mut self,
|
||||
max_width: f32,
|
||||
text: S,
|
||||
text_settings: &TextSettings,
|
||||
) -> Result<usize, ErrorKind> {
|
||||
let layout = shape(0.0, 0.0, self, text_settings, text.as_ref(), Some(max_width))?;
|
||||
|
||||
Ok(layout.final_byte_index)
|
||||
}
|
||||
|
||||
pub fn break_text_vec<S: AsRef<str>>(
|
||||
&mut self,
|
||||
max_width: f32,
|
||||
text: S,
|
||||
text_settings: &TextSettings,
|
||||
) -> Result<Vec<Range<usize>>, ErrorKind> {
|
||||
let text = text.as_ref();
|
||||
|
||||
let mut res = Vec::new();
|
||||
let mut start = 0;
|
||||
|
||||
while start < text.len() {
|
||||
let Ok(index) = self.break_text(max_width, &text[start..], text_settings) else {
|
||||
break;
|
||||
};
|
||||
|
||||
if index == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let index = start + index;
|
||||
res.push(start..index);
|
||||
start += &text[start..index].len();
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the result of a text shaping run.
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct TextMetrics {
|
||||
/// X-coordinate of the starting position for the shaped text.
|
||||
pub x: f32,
|
||||
/// Y-coordinate of the starting position for the shaped text.
|
||||
pub y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
/// Vector of shaped glyphs resulting from the text shaping run.
|
||||
pub glyphs: Vec<ShapedGlyph>,
|
||||
pub(crate) final_byte_index: usize,
|
||||
}
|
||||
|
||||
impl TextMetrics {
|
||||
pub(crate) fn scale(&mut self, scale: f32) {
|
||||
self.x *= scale;
|
||||
self.y *= scale;
|
||||
self.width *= scale;
|
||||
self.height *= scale;
|
||||
|
||||
for glyph in &mut self.glyphs {
|
||||
glyph.x *= scale;
|
||||
glyph.y *= scale;
|
||||
glyph.width *= scale;
|
||||
glyph.height *= scale;
|
||||
}
|
||||
}
|
||||
|
||||
/// width of the glyphs as drawn
|
||||
pub fn width(&self) -> f32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// height of the glyphs as drawn
|
||||
pub fn height(&self) -> f32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
// Shaper
|
||||
|
||||
pub fn shape(
|
||||
x: f32,
|
||||
y: f32,
|
||||
context: &mut super::TextContextImpl,
|
||||
text_settings: &TextSettings,
|
||||
text: &str,
|
||||
max_width: Option<f32>,
|
||||
) -> Result<TextMetrics, ErrorKind> {
|
||||
let variations = &text_settings.font_variations;
|
||||
let id = ShapingId::new(
|
||||
text_settings.font_size,
|
||||
text_settings.font_ids,
|
||||
text,
|
||||
max_width,
|
||||
variations,
|
||||
);
|
||||
|
||||
if !context.shaping_run_cache.contains(&id) {
|
||||
let metrics = shape_run(
|
||||
context,
|
||||
text_settings.font_size,
|
||||
text_settings.font_ids,
|
||||
text_settings.letter_spacing,
|
||||
text,
|
||||
max_width,
|
||||
variations,
|
||||
);
|
||||
context.shaping_run_cache.put(id, metrics);
|
||||
}
|
||||
|
||||
if let Some(mut metrics) = context.shaping_run_cache.get(&id).cloned() {
|
||||
layout(x, y, context, &mut metrics, text_settings)?;
|
||||
|
||||
return Ok(metrics);
|
||||
}
|
||||
|
||||
Err(ErrorKind::UnknownError)
|
||||
}
|
||||
|
||||
fn shape_run(
|
||||
context: &mut super::TextContextImpl,
|
||||
font_size: f32,
|
||||
font_ids: [Option<FontId>; 8],
|
||||
letter_spacing: f32,
|
||||
text: &str,
|
||||
max_width: Option<f32>,
|
||||
variations: &FontVariations,
|
||||
) -> TextMetrics {
|
||||
let mut result = TextMetrics {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
glyphs: Vec::with_capacity(text.len()),
|
||||
final_byte_index: 0,
|
||||
};
|
||||
|
||||
let bidi_info = BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
|
||||
|
||||
// this controls whether we should break within words
|
||||
let mut first_word_in_paragraph = true;
|
||||
|
||||
let Some(paragraph) = bidi_info.paragraphs.first() else {
|
||||
return result;
|
||||
};
|
||||
|
||||
let line = paragraph.range.clone();
|
||||
|
||||
let (levels, runs) = bidi_info.visual_runs(paragraph, line);
|
||||
|
||||
for run in runs {
|
||||
let sub_text = &text[run.clone()];
|
||||
|
||||
if sub_text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hb_direction = if levels[run.start].is_rtl() {
|
||||
rustybuzz::Direction::RightToLeft
|
||||
} else {
|
||||
rustybuzz::Direction::LeftToRight
|
||||
};
|
||||
|
||||
let mut words = Vec::new();
|
||||
let mut word_break_reached = false;
|
||||
let mut byte_index = run.start;
|
||||
|
||||
for mut word_txt in sub_text.split_word_bounds() {
|
||||
let id = ShapingId::new(font_size, font_ids, word_txt, max_width, variations);
|
||||
|
||||
if !context.shaped_words_cache.contains(&id) {
|
||||
let word = shape_word(
|
||||
word_txt,
|
||||
hb_direction,
|
||||
context,
|
||||
font_size,
|
||||
&font_ids,
|
||||
letter_spacing,
|
||||
variations,
|
||||
);
|
||||
context.shaped_words_cache.put(id, word);
|
||||
}
|
||||
|
||||
if let Some(Ok(word)) = context.shaped_words_cache.get(&id) {
|
||||
let mut word = word.clone();
|
||||
|
||||
if let Some(max_width) = max_width {
|
||||
if result.width + word.width >= max_width {
|
||||
word_break_reached = true;
|
||||
if first_word_in_paragraph {
|
||||
// search for the largest prefix of the word that can fit
|
||||
let mut bytes_included = 0;
|
||||
let mut subword_width = 0.0;
|
||||
let target_width = max_width - result.width;
|
||||
for glyph in word.glyphs {
|
||||
bytes_included = glyph.byte_index;
|
||||
let glyph_width = glyph.advance_x + letter_spacing;
|
||||
|
||||
// nuance: we want to include the first glyph even if it breaks
|
||||
// the bounds. this is to allow pathologically small bounds to
|
||||
// at least complete rendering
|
||||
if subword_width + glyph_width >= target_width && bytes_included != 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
subword_width += glyph_width;
|
||||
}
|
||||
|
||||
if bytes_included == 0 {
|
||||
// just in case - never mind!
|
||||
break;
|
||||
}
|
||||
|
||||
let subword_txt = &word_txt[..bytes_included];
|
||||
let id = ShapingId::new(font_size, font_ids, subword_txt, Some(max_width), variations);
|
||||
if !context.shaped_words_cache.contains(&id) {
|
||||
let subword = shape_word(
|
||||
subword_txt,
|
||||
hb_direction,
|
||||
context,
|
||||
font_size,
|
||||
&font_ids,
|
||||
letter_spacing,
|
||||
variations,
|
||||
);
|
||||
context.shaped_words_cache.put(id, subword);
|
||||
}
|
||||
|
||||
if let Some(Ok(subword)) = context.shaped_words_cache.get(&id) {
|
||||
// replace the outer variables so we can continue normally
|
||||
word = subword.clone();
|
||||
word_txt = subword_txt;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if word.glyphs.iter().all(|g| g.c.is_whitespace()) {
|
||||
// the last word we've broken in the middle of is whitespace.
|
||||
// include this word for now, but we will discard its metrics in a moment.
|
||||
} else {
|
||||
// we are not breaking up words - discard this word
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we have broken in the middle of whitespace, do not include this word in metrics
|
||||
if !word_break_reached || !word.glyphs.iter().all(|g| g.c.is_whitespace()) {
|
||||
result.width += word.width;
|
||||
}
|
||||
|
||||
for glyph in &mut word.glyphs {
|
||||
glyph.byte_index += byte_index;
|
||||
debug_assert!(text.get(glyph.byte_index..).is_some());
|
||||
}
|
||||
words.push(word);
|
||||
first_word_in_paragraph = false;
|
||||
}
|
||||
|
||||
byte_index += word_txt.len();
|
||||
|
||||
if word_break_reached {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if levels[run.start].is_rtl() {
|
||||
words.reverse();
|
||||
}
|
||||
|
||||
for word in words {
|
||||
result.glyphs.extend(word.glyphs.clone());
|
||||
}
|
||||
|
||||
result.final_byte_index = byte_index;
|
||||
|
||||
if word_break_reached {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn shape_word(
|
||||
word: &str,
|
||||
hb_direction: rustybuzz::Direction,
|
||||
context: &mut super::TextContextImpl,
|
||||
font_size: f32,
|
||||
font_ids: &[Option<FontId>; 8],
|
||||
letter_spacing: f32,
|
||||
variations: &FontVariations,
|
||||
) -> Result<ShapedWord, ErrorKind> {
|
||||
// find_font will call the closure with each font matching the provided style
|
||||
// until a font capable of shaping the word is found
|
||||
context.find_font(font_ids, |(font_id, font)| {
|
||||
let font_face = font.face_ref_with_variations(variations);
|
||||
let normalized_coords = font.normalize_variations(variations);
|
||||
let face = rustybuzz::Face::from_face(font_face.0.clone());
|
||||
// Call harfbuzz
|
||||
let output = {
|
||||
let mut buffer = rustybuzz::UnicodeBuffer::new();
|
||||
buffer.push_str(word);
|
||||
buffer.set_direction(hb_direction);
|
||||
|
||||
rustybuzz::shape(&face, &[], buffer)
|
||||
};
|
||||
|
||||
let positions = output.glyph_positions();
|
||||
let infos = output.glyph_infos();
|
||||
|
||||
let mut shaped_word = ShapedWord {
|
||||
glyphs: Vec::with_capacity(positions.len()),
|
||||
width: 0.0,
|
||||
};
|
||||
|
||||
let mut has_missing = false;
|
||||
|
||||
for (position, (info, c)) in positions.iter().zip(infos.iter().zip(word.chars())) {
|
||||
if info.glyph_id == 0 {
|
||||
has_missing = true;
|
||||
}
|
||||
|
||||
let scale = font.scale(font_size);
|
||||
|
||||
let mut g = ShapedGlyph {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
c,
|
||||
byte_index: info.cluster as usize,
|
||||
font_id,
|
||||
glyph_id: info
|
||||
.glyph_id
|
||||
.try_into()
|
||||
.expect("rustybuzz guarantees the output glyph id is u16"),
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
advance_x: position.x_advance as f32 * scale,
|
||||
advance_y: position.y_advance as f32 * scale,
|
||||
offset_x: position.x_offset as f32 * scale,
|
||||
offset_y: position.y_offset as f32 * scale,
|
||||
};
|
||||
|
||||
if let Some(glyph) = font.glyph(&font_face, g.glyph_id, &normalized_coords) {
|
||||
g.width = glyph.metrics.width * scale;
|
||||
g.height = glyph.metrics.height * scale;
|
||||
}
|
||||
|
||||
shaped_word.width += g.advance_x + letter_spacing;
|
||||
shaped_word.glyphs.push(g);
|
||||
}
|
||||
|
||||
(has_missing, shaped_word)
|
||||
})
|
||||
}
|
||||
|
||||
// Calculates the x,y coordinates for each glyph based on their advances. Calculates total width and height of the shaped text run
|
||||
fn layout(
|
||||
x: f32,
|
||||
y: f32,
|
||||
context: &mut super::TextContextImpl,
|
||||
res: &mut TextMetrics,
|
||||
text_settings: &TextSettings,
|
||||
) -> Result<(), ErrorKind> {
|
||||
let mut cursor_x = x;
|
||||
let mut cursor_y = y;
|
||||
|
||||
// Horizontal alignment
|
||||
match text_settings.text_align {
|
||||
Align::Center => cursor_x -= res.width / 2.0,
|
||||
Align::Right => cursor_x -= res.width,
|
||||
Align::Left => (),
|
||||
}
|
||||
|
||||
res.x = cursor_x;
|
||||
|
||||
let mut min_y = cursor_y;
|
||||
let mut max_y = cursor_y;
|
||||
|
||||
let mut ascender: f32 = 0.;
|
||||
let mut descender: f32 = 0.;
|
||||
|
||||
for glyph in &mut res.glyphs {
|
||||
let font = context.font_mut(glyph.font_id).ok_or(ErrorKind::NoFontFound)?;
|
||||
let metrics = font.metrics(text_settings.font_size);
|
||||
ascender = ascender.max(metrics.ascender());
|
||||
descender = descender.min(metrics.descender());
|
||||
}
|
||||
|
||||
let primary_metrics = context.find_font(&text_settings.font_ids, |(_, font)| {
|
||||
(false, font.metrics(text_settings.font_size))
|
||||
})?;
|
||||
if ascender.abs() < f32::EPSILON {
|
||||
ascender = primary_metrics.ascender();
|
||||
}
|
||||
if descender.abs() < f32::EPSILON {
|
||||
descender = primary_metrics.descender();
|
||||
}
|
||||
|
||||
// Baseline alignment
|
||||
let alignment_offset_y = match text_settings.text_baseline {
|
||||
Baseline::Top => ascender,
|
||||
Baseline::Middle => ascender.midpoint(descender),
|
||||
Baseline::Alphabetic => 0.0,
|
||||
Baseline::Bottom => descender,
|
||||
};
|
||||
|
||||
for glyph in &mut res.glyphs {
|
||||
glyph.x = cursor_x + glyph.offset_x;
|
||||
glyph.y = (cursor_y + alignment_offset_y).round() + glyph.offset_y;
|
||||
|
||||
min_y = min_y.min(glyph.y);
|
||||
max_y = max_y.max(glyph.y + glyph.height);
|
||||
|
||||
cursor_x += glyph.advance_x + text_settings.letter_spacing;
|
||||
cursor_y += glyph.advance_y;
|
||||
}
|
||||
|
||||
res.y = min_y;
|
||||
res.height = max_y - min_y;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
use femtovg::{renderer::Void, Baseline, Canvas, Color, FillRule, Paint, Path, Solidity};
|
||||
|
||||
#[test]
|
||||
fn path_with_single_move_to() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(10.0, 10.0);
|
||||
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_with_two_lines() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.line_to(10.0, 10.0);
|
||||
path.line_to(10.0, 10.0);
|
||||
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_with_close_points() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(10.0, 10.0);
|
||||
path.line_to(10.0001, 10.0);
|
||||
path.line_to(10.0001, 10.000001);
|
||||
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_with_points_at_limits() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(10.0, 10.0);
|
||||
path.line_to(f32::MAX, f32::MAX);
|
||||
path.quad_to(10.0, 10.0, -f32::MAX, f32::MAX);
|
||||
path.bezier_to(10.0, 10.0, f32::MAX, 5000.0, -f32::MAX, -f32::MAX);
|
||||
path.rounded_rect_varying(
|
||||
-f32::MAX,
|
||||
-f32::MAX,
|
||||
f32::MAX,
|
||||
f32::MAX,
|
||||
f32::MAX,
|
||||
f32::MAX,
|
||||
f32::MAX,
|
||||
f32::MAX,
|
||||
);
|
||||
path.close();
|
||||
|
||||
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_with_points_around_zero() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(0.0, 0.0);
|
||||
path.line_to(0.0, 0.0);
|
||||
path.line_to(0.0001, 0.0000003);
|
||||
path.quad_to(0.002, 0.0001, -0.002, 0.0001);
|
||||
path.bezier_to(0.0001, 0.002, -0.002, 0.0001, -0.002, 0.0001);
|
||||
path.rounded_rect_varying(
|
||||
-f32::MAX,
|
||||
-f32::MAX,
|
||||
f32::MAX,
|
||||
f32::MAX,
|
||||
f32::MAX,
|
||||
0.0001,
|
||||
0.0001,
|
||||
0.0001,
|
||||
);
|
||||
|
||||
path.close();
|
||||
|
||||
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_stroke() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(0.5, 0.5);
|
||||
path.line_to(2., 2.);
|
||||
path.line_to(2., 2.);
|
||||
path.line_to(4., 2.);
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_arc_to() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(10.0, 10.0);
|
||||
path.arc_to(10.0, 10.0001, 10.0, 10.0001, 2.0);
|
||||
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_arc() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
let mut path = Path::new();
|
||||
path.move_to(10.0, 10.0);
|
||||
path.arc(10.0, 10.0, 10.0, 0.0, f32::MAX, Solidity::Hole);
|
||||
|
||||
canvas.fill_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
canvas.stroke_path(&path, &Paint::color(Color::rgb(100, 100, 100)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_contains_point() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
// without setting size contains_point will early out on the bounds check and report false
|
||||
canvas.set_size(100, 100, 1.0);
|
||||
|
||||
// Star - cancave & self crossing
|
||||
let mut path = Path::new();
|
||||
path.move_to(50.0, 0.0);
|
||||
path.line_to(21.0, 90.0);
|
||||
path.line_to(98.0, 35.0);
|
||||
path.line_to(2.0, 35.0);
|
||||
path.line_to(79.0, 90.0);
|
||||
path.close();
|
||||
|
||||
// Center of the star should be hollow for even-odd rule
|
||||
assert!(!canvas.contains_point(&path, 50.0, 45.0, FillRule::EvenOdd));
|
||||
assert!(canvas.contains_point(&path, 50.0, 5.0, FillRule::EvenOdd));
|
||||
|
||||
// Center of the star should be fill for NonZero rule
|
||||
assert!(canvas.contains_point(&path, 50.0, 45.0, FillRule::NonZero));
|
||||
assert!(canvas.contains_point(&path, 50.0, 5.0, FillRule::NonZero));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_location_respects_scale() {
|
||||
let mut canvas = Canvas::new(Void).unwrap();
|
||||
|
||||
canvas
|
||||
.add_font("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
let paint = Paint::color(Color::black()).with_text_baseline(Baseline::Top);
|
||||
canvas.scale(5.0, 5.0);
|
||||
|
||||
let res = canvas.measure_text(100.0, 100.0, "Hello", &paint).unwrap();
|
||||
|
||||
assert_eq!(res.x, 100.0);
|
||||
assert_eq!(res.y, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_measure_without_canvas() {
|
||||
let text_context = femtovg::TextContext::default();
|
||||
|
||||
let font_id = text_context
|
||||
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
let test_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
|
||||
|
||||
let metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &test_paint)
|
||||
.expect("text shaping failed unexpectedly");
|
||||
|
||||
assert_eq!(metrics.width().ceil(), 83.);
|
||||
assert_eq!(metrics.height().ceil(), 13.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_measure_without_canvas() {
|
||||
let text_context = femtovg::TextContext::default();
|
||||
|
||||
let font_id = text_context
|
||||
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
let test_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
|
||||
|
||||
let metrics = text_context
|
||||
.measure_font(&test_paint)
|
||||
.expect("font measuring failed unexpectedly");
|
||||
|
||||
assert_eq!(metrics.ascender().ceil(), 15.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn break_text_without_canvas() {
|
||||
let text_context = femtovg::TextContext::default();
|
||||
|
||||
let font_id = text_context
|
||||
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
let test_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
|
||||
|
||||
let text = "Multiple Lines Broken";
|
||||
|
||||
let breaks = text_context
|
||||
.break_text_vec(60., text, &test_paint)
|
||||
.expect("text shaping failed unexpectedly");
|
||||
|
||||
assert_eq!(
|
||||
breaks
|
||||
.iter()
|
||||
.map(|range| &text[range.start..range.end])
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Multiple ", "Lines ", "Broken"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_font_weight_affects_measurement() {
|
||||
let text_context = femtovg::TextContext::default();
|
||||
|
||||
let font_id = text_context
|
||||
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
// Verify the font has a wght axis
|
||||
let axes = text_context.font_variation_axes(font_id).unwrap();
|
||||
let wght_axis = axes.iter().find(|a| &a.tag == b"wght");
|
||||
assert!(wght_axis.is_some(), "Font should have a wght axis");
|
||||
|
||||
let light_paint = femtovg::Paint::default()
|
||||
.with_font(&[font_id])
|
||||
.with_font_size(16.)
|
||||
.with_font_weight(femtovg::Paint::FONT_WEIGHT_LIGHT);
|
||||
|
||||
let bold_paint = femtovg::Paint::default()
|
||||
.with_font(&[font_id])
|
||||
.with_font_size(16.)
|
||||
.with_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
|
||||
|
||||
let light_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &light_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
let bold_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &bold_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
// Bold text should be wider than light text
|
||||
assert!(
|
||||
bold_metrics.width() > light_metrics.width(),
|
||||
"Bold ({}) should be wider than light ({})",
|
||||
bold_metrics.width(),
|
||||
light_metrics.width()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_variation_generic_api_matches_named_weight() {
|
||||
let text_context = femtovg::TextContext::default();
|
||||
|
||||
let font_id = text_context
|
||||
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
// Using the generic variation API for wght should produce the same measurement
|
||||
// as the named font_weight API
|
||||
let named_paint = femtovg::Paint::default()
|
||||
.with_font(&[font_id])
|
||||
.with_font_size(16.)
|
||||
.with_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
|
||||
|
||||
let generic_paint = femtovg::Paint::default()
|
||||
.with_font(&[font_id])
|
||||
.with_font_size(16.)
|
||||
.with_font_variation(b"wght", 700.0);
|
||||
|
||||
let named_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &named_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
let generic_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &generic_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
assert!(
|
||||
(named_metrics.width() - generic_metrics.width()).abs() < f32::EPSILON,
|
||||
"Named weight API ({}) and generic variation API ({}) should produce identical measurements",
|
||||
named_metrics.width(),
|
||||
generic_metrics.width()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_variation_italic_and_slant_api() {
|
||||
// Test the Paint API for italic and slant methods
|
||||
let mut paint = femtovg::Paint::default();
|
||||
|
||||
// Initially no italic set
|
||||
assert!(paint.font_italic().is_none());
|
||||
assert!(paint.font_slant().is_none());
|
||||
|
||||
// Set italic — also sets slnt as fallback
|
||||
paint.set_font_italic(true);
|
||||
assert_eq!(paint.font_italic(), Some(true));
|
||||
assert_eq!(paint.font_slant(), Some(-12.0));
|
||||
|
||||
paint.set_font_italic(false);
|
||||
assert_eq!(paint.font_italic(), Some(false));
|
||||
assert_eq!(paint.font_slant(), Some(0.0));
|
||||
|
||||
// clear_font_italic removes both ital and slnt
|
||||
paint.clear_font_italic();
|
||||
assert!(paint.font_italic().is_none());
|
||||
assert!(paint.font_slant().is_none());
|
||||
|
||||
// Explicit slant overrides the italic fallback
|
||||
paint.set_font_italic(true);
|
||||
paint.set_font_slant(-5.0);
|
||||
assert_eq!(paint.font_italic(), Some(true));
|
||||
assert_eq!(paint.font_slant(), Some(-5.0));
|
||||
|
||||
paint.clear_font_variations();
|
||||
|
||||
// Set slant independently
|
||||
paint.set_font_slant(-12.0);
|
||||
assert_eq!(paint.font_slant(), Some(-12.0));
|
||||
|
||||
paint.clear_font_slant();
|
||||
assert!(paint.font_slant().is_none());
|
||||
|
||||
// Multiple variations at once
|
||||
paint.set_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
|
||||
paint.set_font_italic(true);
|
||||
|
||||
assert_eq!(paint.font_weight(), Some(700.0));
|
||||
assert_eq!(paint.font_italic(), Some(true));
|
||||
assert_eq!(paint.font_slant(), Some(-12.0));
|
||||
|
||||
// Clear all
|
||||
paint.clear_font_variations();
|
||||
assert!(paint.font_weight().is_none());
|
||||
assert!(paint.font_italic().is_none());
|
||||
assert!(paint.font_slant().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_variation_hash_stability() {
|
||||
// Setting the same variations in different order should produce the same hash
|
||||
let mut paint_a = femtovg::Paint::default();
|
||||
paint_a.set_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
|
||||
paint_a.set_font_italic(true);
|
||||
|
||||
let mut paint_b = femtovg::Paint::default();
|
||||
paint_b.set_font_italic(true);
|
||||
paint_b.set_font_weight(femtovg::Paint::FONT_WEIGHT_BOLD);
|
||||
|
||||
// Both paints should have the same variation hash (verified via the generic API)
|
||||
assert_eq!(paint_a.font_weight(), paint_b.font_weight());
|
||||
assert_eq!(paint_a.font_italic(), paint_b.font_italic());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_font_slant_affects_measurement() {
|
||||
let text_context = femtovg::TextContext::default();
|
||||
|
||||
let font_id = text_context
|
||||
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
// Verify the font has a slnt axis
|
||||
let axes = text_context.font_variation_axes(font_id).unwrap();
|
||||
let slnt_axis = axes.iter().find(|a| &a.tag == b"slnt");
|
||||
assert!(slnt_axis.is_some(), "Font should have a slnt axis");
|
||||
|
||||
let upright_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
|
||||
|
||||
let slanted_paint = femtovg::Paint::default()
|
||||
.with_font(&[font_id])
|
||||
.with_font_size(16.)
|
||||
.with_font_slant(-10.0);
|
||||
|
||||
let upright_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &upright_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
let slanted_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &slanted_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
// Slanted text should produce different measurements than upright text
|
||||
// (the widths may differ slightly due to slant-adjusted glyph metrics)
|
||||
assert!(
|
||||
(upright_metrics.width() - slanted_metrics.width()).abs() > 0.0
|
||||
|| upright_metrics.glyphs.len() == slanted_metrics.glyphs.len(),
|
||||
"Slanted text should shape successfully and produce valid metrics"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_italic_falls_back_to_slnt_axis() {
|
||||
// Roboto Flex has no `ital` axis but has `slnt`. Calling set_font_italic
|
||||
// should still produce slanted text via the slnt fallback.
|
||||
let text_context = femtovg::TextContext::default();
|
||||
|
||||
let font_id = text_context
|
||||
.add_font_file("examples/assets/RobotoFlex-VariableFont.ttf")
|
||||
.expect("Font not found");
|
||||
|
||||
let axes = text_context.font_variation_axes(font_id).unwrap();
|
||||
assert!(
|
||||
axes.iter().find(|a| &a.tag == b"ital").is_none(),
|
||||
"Font should NOT have an ital axis"
|
||||
);
|
||||
assert!(
|
||||
axes.iter().find(|a| &a.tag == b"slnt").is_some(),
|
||||
"Font should have a slnt axis"
|
||||
);
|
||||
|
||||
let upright_paint = femtovg::Paint::default().with_font(&[font_id]).with_font_size(16.);
|
||||
|
||||
let italic_paint = femtovg::Paint::default()
|
||||
.with_font(&[font_id])
|
||||
.with_font_size(16.)
|
||||
.with_font_italic(true);
|
||||
|
||||
let upright_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &upright_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
let italic_metrics = text_context
|
||||
.measure_text(0., 0., "Hello World", &italic_paint)
|
||||
.expect("text shaping failed");
|
||||
|
||||
// The slnt axis changes glyph outlines (tilts them) but doesn't change advance
|
||||
// widths, so we verify that shaping succeeds and produces the same glyph count.
|
||||
// The visual difference (slanted glyphs) is only visible in rendering.
|
||||
assert_eq!(
|
||||
upright_metrics.glyphs.len(),
|
||||
italic_metrics.glyphs.len(),
|
||||
"Italic fallback via slnt should shape the same number of glyphs"
|
||||
);
|
||||
assert!(italic_metrics.width() > 0.0, "Italic text should have positive width");
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use femtovg::Transform2D;
|
||||
|
||||
#[test]
|
||||
fn test_multiplication() {
|
||||
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
|
||||
let expected = Transform2D::new(25.0, 28.0, 57.0, 64.0, 100.0, 112.0);
|
||||
|
||||
let result = a * b;
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiplication_assignment() {
|
||||
let mut a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
|
||||
let expected = Transform2D::new(25.0, 28.0, 57.0, 64.0, 100.0, 112.0);
|
||||
|
||||
a *= b;
|
||||
assert_eq!(a, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_premultiply() {
|
||||
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
|
||||
|
||||
let mut premultiplied = a;
|
||||
premultiplied.premultiply(&b);
|
||||
|
||||
assert_eq!(premultiplied, b * a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_division() {
|
||||
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
|
||||
let expected = Transform2D::new(-2.0, 3.0, -3.0, 4.0, -3.0, 3.0);
|
||||
|
||||
let result = b / a;
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_division_assignment() {
|
||||
let a = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let mut b = Transform2D::new(7.0, 8.0, 9.0, 10.0, 11.0, 12.0);
|
||||
let expected = Transform2D::new(-2.0, 3.0, -3.0, 4.0, -3.0, 3.0);
|
||||
|
||||
b /= a;
|
||||
assert_eq!(b, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_translation() {
|
||||
let transform = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let (tx, ty) = (2.0, 5.0);
|
||||
|
||||
let mut translated = transform;
|
||||
translated.translate(tx, ty);
|
||||
|
||||
assert_eq!(translated, transform * Transform2D::translation(tx, ty))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rotation() {
|
||||
let transform = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let a = std::f32::consts::TAU * 0.75;
|
||||
|
||||
let mut scaled = transform;
|
||||
scaled.rotate(a);
|
||||
|
||||
assert_eq!(scaled, transform * Transform2D::rotation(a))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scaling() {
|
||||
let transform = Transform2D::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
|
||||
let (sx, sy) = (2.0, 0.5);
|
||||
|
||||
let mut scaled = transform;
|
||||
scaled.scale(sx, sy);
|
||||
|
||||
assert_eq!(scaled, transform * Transform2D::scaling(sx, sy))
|
||||
}
|
||||
Reference in new issue
Block a user