1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! Color representation and color space utilities
/// An RGBA color in the sRGB space. The four fields are red, green, blue and alpha, each from 0.0
/// to 1.0. Compose's `Color`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Color(pub f32, pub f32, pub f32, pub f32);
impl Color {
/// Creates an opaque color from red, green and blue channels, each from 0.0 to 1.0.
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self(r, g, b, 1.0)
}
/// Creates a color from red, green, blue and alpha channels, each from 0.0 to 1.0.
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self(r, g, b, a)
}
/// Creates a color from red, green, blue and alpha channels of 0 to 255.
pub const fn from_rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
)
}
/// Creates an opaque color from red, green and blue channels of 0 to 255.
pub const fn from_rgb_u8(r: u8, g: u8, b: u8) -> Self {
Self::from_rgba_u8(r, g, b, 255)
}
/// Red channel, from 0.0 to 1.0.
pub fn r(&self) -> f32 {
self.0
}
/// Green channel, from 0.0 to 1.0.
pub fn g(&self) -> f32 {
self.1
}
/// Blue channel, from 0.0 to 1.0.
pub fn b(&self) -> f32 {
self.2
}
/// Alpha (opacity) channel, from 0.0 (transparent) to 1.0 (opaque).
pub fn a(&self) -> f32 {
self.3
}
/// Returns this color with its alpha channel replaced by `alpha`, from 0.0 (transparent) to 1.0
/// (opaque).
pub fn with_alpha(&self, alpha: f32) -> Self {
Self(self.0, self.1, self.2, alpha)
}
/// This colour as the platform's colour type actually holds it: **eight
/// bits per channel**.
///
/// `androidx.compose.ui.graphics.Color` is a float-shaped API over an
/// 8-bit value. Building one in the sRGB space snaps every channel on the
/// spot — the bytecode of `ColorKt.Color(float, float, float, float,
/// ColorSpace)` is `coerceIn(0f, 1f)`, `* 255.0f`, `+ 0.5f`, `f2i`, packed
/// into an ARGB int — so a colour an app computes in float is already a
/// whole channel value before anything paints with it, and the rasterizer
/// never sees the fraction.
///
/// A renderer that carries the fraction to the framebuffer instead leaves
/// the rounding to whatever converts float to unorm there. That agrees
/// nearly everywhere and disagrees on an exact half. It is not a rare
/// shape: a theme that lerps between two byte colours lands on one
/// routinely — `mix(rail, background, 0.55)` puts a Wear settings capsule
/// at exactly 22.5/255 on green, where this rule gives 23 and a converter
/// that breaks ties to even gives 22. One level, and then the row's layer
/// alpha multiplies it and keeps it.
///
/// Ties go **up**, which is what `(int)(x + 0.5f)`, Skia's
/// `SkScalarRoundToInt` and Rust's `f32::round` all give for a channel
/// clamped into 0..=1.
pub fn srgb_8bit(self) -> Self {
Self(
srgb_channel_8bit(self.0),
srgb_channel_8bit(self.1),
srgb_channel_8bit(self.2),
srgb_channel_8bit(self.3),
)
}
/// Opaque black.
pub const BLACK: Color = Color(0.0, 0.0, 0.0, 1.0);
/// Opaque white.
pub const WHITE: Color = Color(1.0, 1.0, 1.0, 1.0);
/// Opaque red.
pub const RED: Color = Color(1.0, 0.0, 0.0, 1.0);
/// Opaque green.
pub const GREEN: Color = Color(0.0, 1.0, 0.0, 1.0);
/// Opaque blue.
pub const BLUE: Color = Color(0.0, 0.0, 1.0, 1.0);
/// Fully transparent black, with every channel 0.0.
pub const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
}
/// `level / 255.0` for each 8-bit level, as the division gives it: a chip
/// with no float divider looks the quotient up instead.
const LEVEL_FRACTIONS: [f32; 256] = {
let mut fractions = [0.0; 256];
let mut level = 0;
while level < 256 {
fractions[level] = level as f32 / 255.0;
level += 1;
}
fractions
};
/// One channel snapped to the 8-bit value an sRGB colour holds, by the
/// platform's own `* 255.0f`, `+ 0.5f`, `f2i`: the saturating cast to a
/// byte clamps as its `coerceIn(0f, 1f)` does.
fn srgb_channel_8bit(channel: f32) -> f32 {
if channel.is_nan() {
return channel;
}
LEVEL_FRACTIONS[usize::from((channel * 255.0 + 0.5) as u8)]
}
#[cfg(test)]
#[path = "tests/color_tests.rs"]
mod tests;