Replace 'Srgba8' with generic 'Rgba'; Add 'Component' trait; Add 'f16' and 'f128' features; Add 'to_u32' conversion destructor to 'Rgba<u8>'; Add 'from_u32' constructor to 'Rgba<u8>'; Replace 'zerocopy::FromBytes' with 'zerocopy::FromZeros' for 'Rgba'; Replace 'bytemuck::AnyBitPattern' with 'bytemuck::Zeroable' for 'Rgba'; Do not implement 'bytemuck::NoUninit' for 'Rgba'; Implement 'Display' for 'Rgba<u8>'; Implement 'Component' for 'u8', 'i8', 'u16', 'i16', 'u32', 'i32', 'u64', 'i64', 'u128', 'i128', 'f16', 'f32', 'f64', and 'f128'; Implement 'FromStr' for 'Rgba<u8>'; Add 'error' module; Add 'RgbaU8FromStrError' error type; Add tests; Rename project to *Polywave* (from *Sibgha*); Rename 'sibgha' crate to 'polywave'; Add docs; Update lints; Add readme; Add 'to_f16_lossy', 'to_f32_lossy', 'to_f64_lossy', and 'to_f128_lossy' methods to 'Component';

This commit is contained in:
Gabriel Bjørnager Jensen 2025-03-19 11:25:23 +01:00
parent 391857d313
commit 2517204e74
10 changed files with 441 additions and 103 deletions

View file

@ -1,8 +1,31 @@
# Changelog
This is the changelog of [Sibgha](https://crates.io/crates/sibgha/).
This is the changelog of [Polywave](https://crates.io/crates/polywave/).
See `README.md` for more information.
## 0.1.0
* Replace `Srgba8` with generic `Rgba`
* Add `Component` trait
* Add `f16` and `f128` features
* Add `to_u32` conversion destructor to `Rgba<u8>`
* Add `from_u32` constructor to `Rgba<u8>`
* Replace `zerocopy::FromBytes` with `zerocopy::FromZeros` for `Rgba`
* Replace `bytemuck::AnyBitPattern` with `bytemuck::Zeroable` for `Rgba`
* Do not implement `bytemuck::NoUninit` for `Rgba`
* Implement `Display` for `Rgba<u8>`
* Implement `Component` for `u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u64`, `i64`, `u128`, `i128`, `f16`, `f32`, `f64`, and `f128`
* Implement `FromStr` for `Rgba<u8>`
* Add `error` module
* Add `RgbaU8FromStrError` error type
* Add tests
* Rename project to *Polywave* (from *Sibgha*)
* Rename `sibgha` crate to `polywave`
* Add docs
* Update lints
* Add readme
* Add `to_f16_lossy`, `to_f32_lossy`, `to_f64_lossy`, and `to_f128_lossy` methods to `Component`
## 0.0.0
* Configure lints

View file

@ -7,28 +7,30 @@
# <https://mozilla.org/MPL/2.0/>.
[package]
name = "sibgha"
version = "0.0.0"
name = "polywave"
version = "0.1.0"
authors = ["Gabriel Bjørnager Jensen"]
edition = "2021"
rust-version = "1.82"
description = "Vector-based colour manipulators."
documentation = "https://docs.rs/sibgha/"
homepage = "https://crates.io/crates/sibgha/"
repository = "https://gitlab.com/bjoernager/sibgha/"
documentation = "https://docs.rs/polywave/"
homepage = "https://crates.io/crates/polywave/"
repository = "https://gitlab.com/bjoernager/polywave/"
license = "MPL-2.0"
[package.metadata.docs.rs]
all-features = true
[dependencies]
bytemuck = { version = "1.22", optional = true, features = ["derive"] }
serde = { version = "1.0", optional = true, features = ["derive"] }
zerocopy = { version = "0.8", optional = true, features = ["derive"] }
wgpu-types = { version = "24.0", optional = true }
bytemuck = { version = "1.22", optional = true, default-features = false, features = ["derive"] }
serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] }
zerocopy = { version = "0.8", optional = true, default-features = false, features = ["derive", "simd"] }
wgpu-types = { version = "24.0", optional = true, default-features = false }
[features]
bytemuck = ["dep:bytemuck"]
f128 = []
f16 = []
serde = ["dep:serde"]
zerocopy = ["dep:zerocopy"]
wgpu = ["dep:wgpu-types"]

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# Polywave
Vector-based colour manipulators.

150
src/component/mod.rs Normal file
View file

@ -0,0 +1,150 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, you
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.
mod seal {
/// Denotes a type suitable for use as a colour component.
pub trait Component: Copy + Sized { }
}
pub(crate) use seal::Component as SealedComponent;
/// Denotes a type suitable for use as a colour component.
///
/// Importantly, all components must be
///
/// Note that not all components are necessarily transformable; most colour spaces and formats require some form of floating- or fixed-point arithmetic for transformations to be at least be somewhat accurate.
/// Only the most basic of colours spaces can thus be translated using simple integers.
pub trait Component: SealedComponent + Copy + Sized {
/// Lossily converts the component to [`f16`].
///
/// If `Self` additionally implements <code>[Into]&lt;f16&gt;</code>, then this method must also be lossless.
#[cfg(feature = "f16")]
#[must_use]
fn to_f16_lossy(self) -> f16;
/// Lossily converts the component to [`f32`].
///
/// If `Self` additionally implements <code>[Into]&lt;f32&gt;</code>, then this method must also be lossless.
#[must_use]
fn to_f32_lossy(self) -> f32;
/// Lossily converts the component to [`f64`].
///
/// If `Self` additionally implements <code>[Into]&lt;f64&gt;</code>, then this method must also be lossless.
#[must_use]
fn to_f64_lossy(self) -> f64;
/// Lossily converts the component to [`f128`].
///
/// If `Self` additionally implements <code>[Into]&lt;f128&gt;</code>, then this method must also be lossless.
#[cfg(feature = "f128")]
#[must_use]
fn to_f128_lossy(self) -> f128;
}
macro_rules! impl_integer_component {
{
$(
$(#[$attrs:meta])*
$tys:ty
),+$(,)?
} => {
$(
$(#[$attrs])*
impl ::polywave::SealedComponent for $tys { }
$(#[$attrs])*
impl ::polywave::Component for $tys {
#[cfg(feature = "f16")]
#[inline(always)]
fn to_f16_lossy(self) -> f16 {
self as f16 / Self::MAX as f16
}
#[inline(always)]
fn to_f32_lossy(self) -> f32 {
self as f32 / Self::MAX as f32
}
#[inline(always)]
fn to_f64_lossy(self) -> f64 {
self as f64 / Self::MAX as f64
}
#[cfg(feature = "f128")]
#[inline(always)]
fn to_f128_lossy(self) -> f128 {
self as f128 / Self::MAX as f128
}
}
)*
};
}
macro_rules! impl_float_component {
{
$(
$(#[$attrs:meta])*
$tys:ty
),+$(,)?
} => {
$(
$(#[$attrs])*
impl ::polywave::SealedComponent for $tys { }
$(#[$attrs])*
impl ::polywave::Component for $tys {
#[cfg(feature = "f16")]
#[inline(always)]
fn to_f16_lossy(self) -> f16 {
self as f16
}
#[inline(always)]
fn to_f32_lossy(self) -> f32 {
self as f32
}
#[inline(always)]
fn to_f64_lossy(self) -> f64 {
self as f64
}
#[cfg(feature = "f128")]
#[inline(always)]
fn to_f128_lossy(self) -> f128 {
self as f128
}
}
)*
};
}
impl_integer_component! {
u8,
i8,
u16,
i16,
u32,
i32,
u64,
i64,
u128,
i128,
}
impl_float_component! {
f32,
f64,
#[cfg(feature = "f16")]
f16,
#[cfg(feature = "f128")]
f128,
}

13
src/error/mod.rs Normal file
View file

@ -0,0 +1,13 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, you
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.
//! Error types.
mod rgba_u8_from_str_error;
pub use rgba_u8_from_str_error::RgbaU8FromStrError;

View file

@ -0,0 +1,37 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
use core::error::Error;
use core::fmt::{self, Display, Formatter};
/// A RGBA colour code could not be parsed.
#[derive(Debug, Eq, PartialEq)]
pub enum RgbaU8FromStrError {
/// The RGBA colour code had an invalid length.
InvalidLength(usize),
/// The RGBA colour code had no prefixed hash `#`.
MissingHash,
/// The RGBA colour code had an otherwise unknown format.
///
/// This error is only emitted if neither of the two others is appropriate.
UnknownFormat,
}
impl Error for RgbaU8FromStrError { }
impl Display for RgbaU8FromStrError {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Self::InvalidLength(len)
=> write!(f, "rgba code has length `{len}` but should have been neither `4` or `7` or `9` octets long"),
Self::MissingHash
=> write!(f, "rgba code is missing prefixed hash `#`"),
Self::UnknownFormat
=> write!(f, "rgba code is of an otherwise unknown format"),
}
}
}

View file

@ -6,8 +6,26 @@
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.
//! Vector-based colour manipulators.
#![warn(missing_docs)]
#![no_std]
mod srgba8;
#![cfg_attr(feature = "f16", feature(f16))]
#![cfg_attr(feature = "f128", feature(f128))]
pub use srgba8::Srgba8;
extern crate self as polywave;
#[cfg(test)]
extern crate alloc;
pub mod error;
mod component;
mod rgba;
pub use component::Component;
pub use rgba::Rgba;
use component::SealedComponent;

150
src/rgba/mod.rs Normal file
View file

@ -0,0 +1,150 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, you
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.
mod test;
use crate::Component;
use crate::error::RgbaU8FromStrError;
use core::fmt::{self, Debug, Display, Formatter};
use core::ops::RangeInclusive;
use core::str::FromStr;
/// An RGBA colour.
///
/// This type guarantees that its four channels -- red, green, blue, and alpha -- are stored sequentially in memory (in this order).
#[repr(transparent)]
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "zerocopy", derive(zerocopy::FromZeros, zerocopy::Immutable, zerocopy::IntoBytes))]
pub struct Rgba<T: Component>([T; 0x4]);
impl<T: Component> Rgba<T> {
/// Constructs a new RGBA colour.
#[inline(always)]
#[must_use]
pub const fn new(r: T, g: T, b: T, a: T) -> Self {
let data = [r, g, b, a];
Self(data)
}
/// Deconstructs an RGBA colour.
#[inline(always)]
#[must_use]
pub const fn get(self) -> (T, T, T, T) {
let [r, g, b, a] = self.0;
(r, g, b, a)
}
}
impl Rgba<u8> {
/// Constructs a new RGBA colour from [`u32`].
///
/// The `u32` value is reinterpreted as a four contiguous `u8` objects corresponding to each of the four channels.
#[inline(always)]
#[must_use]
pub const fn from_u32(value: u32) -> Self {
let data = value.to_be_bytes();
Self(data)
}
/// Converts an RGBA colour to [`u32`].
///
/// This function is the inverse of [`from_u32`](Self::from_u32) (see there for more information).
#[inline(always)]
#[must_use]
pub const fn to_u32(self) -> u32 {
let data = self.0;
u32::from_be_bytes(data)
}
}
impl Display for Rgba<u8> {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let value = self.to_u32();
write!(f, "#{value:08X}")
}
}
impl<T: Component> From<(T, T, T, T)> for Rgba<T> {
#[inline(always)]
fn from((r, g, b, a): (T, T, T, T)) -> Self {
Self::new(r, g, b, a)
}
}
impl FromStr for Rgba<u8> {
type Err = RgbaU8FromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.starts_with('#') {
return Err(RgbaU8FromStrError::MissingHash);
}
let get_int_in_range = |range: RangeInclusive<usize>| -> Result<u8, Self::Err> {
let value = s.get(range).map(|s| u8::from_str_radix(s, 0x10));
if let Some(Ok(value)) = value {
Ok(value)
} else {
Err(RgbaU8FromStrError::UnknownFormat)
}
};
let colour = match s.len() {
0x4 => {
let r = get_int_in_range(0x1..=0x1)? * 0x11;
let g = get_int_in_range(0x2..=0x2)? * 0x11;
let b = get_int_in_range(0x3..=0x3)? * 0x11;
Self::new(r, g, b, 0xFF)
}
0x7 => {
let r = get_int_in_range(0x1..=0x2)?;
let g = get_int_in_range(0x3..=0x4)?;
let b = get_int_in_range(0x5..=0x6)?;
Self::new(r, g, b, 0xFF)
}
0x9 => {
let r = get_int_in_range(0x1..=0x2)?;
let g = get_int_in_range(0x3..=0x4)?;
let b = get_int_in_range(0x5..=0x6)?;
let a = get_int_in_range(0x7..=0x8)?;
Self::new(r, g, b, a)
}
len => return Err(RgbaU8FromStrError::InvalidLength(len)),
};
Ok(colour)
}
}
// NOTE: We require `Into<f64>` as that also guran-
// tees the losslessness of `to_f64_lossy`.
#[cfg(feature = "wgpu")]
impl<T: Component + Into<f64>> From<Rgba<T>> for wgpu_types::Color {
#[inline]
fn from(value: Rgba<T>) -> Self {
let (r, g, b, a) = value.get();
let r = r.to_f64_lossy();
let g = g.to_f64_lossy();
let b = b.to_f64_lossy();
let a = a.to_f64_lossy();
Self { r, g, b, a }
}
}

33
src/rgba/test.rs Normal file
View file

@ -0,0 +1,33 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, you
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.
#![cfg(test)]
use crate::Rgba;
use alloc::format;
use core::str::FromStr;
#[test]
fn test_rgba_u8_display() {
assert_eq!(format!("{}", Rgba::<u8>::from_u32(0xF0F8FFFF)), "#F0F8FFFF");
}
#[test]
fn test_rgba_u8_from_str() {
assert_eq!(Rgba::<u8>::from_str("#639"), Ok(Rgba::new(0x66, 0x33, 0x99, 0xFF)));
assert_eq!(Rgba::<u8>::from_str("#00FF7F"), Ok(Rgba::new(0x00, 0xFF, 0x7F, 0xFF)));
assert_eq!(Rgba::<u8>::from_str("#FfD7007f"), Ok(Rgba::new(0xFF, 0xD7, 0x00, 0x7F)));
}
#[test]
fn test_rgba_u8_from_u32() {
assert_eq!(Rgba::<u8>::from_u32(0x80808080), Rgba::<u8>::new(0x80, 0x80, 0x80, 0x80));
}

View file

@ -1,91 +0,0 @@
// Copyright 2025 Gabriel Bjørnager Jensen.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, you
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.
use core::fmt::{self, Debug, Display, Formatter};
type Buffer = [u8; 0x4];
#[repr(align(0x4), C)]
#[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::AnyBitPattern, bytemuck::NoUninit))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "zerocopy", derive(zerocopy::FromBytes, zerocopy::Immutable, zerocopy::IntoBytes))]
pub struct Srgba8(Buffer);
impl Srgba8 {
#[inline(always)]
#[must_use]
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
let buf = [r, g, b, a];
Self(buf)
}
#[inline(always)]
#[must_use]
pub const fn from_u32(value: u32) -> Self {
let buf = value.to_be_bytes();
Self(buf)
}
#[inline(always)]
#[must_use]
pub const fn get(self) -> (u8, u8, u8, u8) {
let [r, g, b, a] = self.0;
(r, g, b, a)
}
#[inline(always)]
#[must_use]
pub const fn into_u32(self) -> u32 {
let data = self.0;
u32::from_be_bytes(data)
}
}
impl Debug for Srgba8 {
#[inline(always)]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Debug::fmt(&self.get(), f)
}
}
impl Display for Srgba8 {
#[inline]
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "#{:08X}", self.into_u32())
}
}
impl From<(u8, u8, u8, u8)> for Srgba8 {
#[inline(always)]
fn from((r, g, b, a): (u8, u8, u8, u8)) -> Self {
Self::new(r, g, b, a)
}
}
impl From<u32> for Srgba8 {
#[inline(always)]
fn from(value: u32) -> Self {
Self::from_u32(value)
}
}
#[cfg(feature = "wgpu")]
impl From<Srgba8> for wgpu_types::Color {
#[inline]
fn from(value: Srgba8) -> Self {
let (r, g, b, a) = value.get();
Self {
r: f64::from(r) / f64::from(u8::MAX),
g: f64::from(g) / f64::from(u8::MAX),
b: f64::from(b) / f64::from(u8::MAX),
a: f64::from(a) / f64::from(u8::MAX),
}
}
}