id: 0001 title: Component Precision status: active tags: [core, precision, numerics] created: 2026-02-04 superseded-by: #
ADR-0001: Component Precision #
Status #
Summary #
All color space components use a Component newtype wrapping f64 for internal representation. This type
accepts any standard numeric input via From implementations, enabling ergonomic construction while
maintaining precision for color science calculations.
Context #
Color science involves matrix multiplications, gamma corrections, and iterative algorithms where numerical
precision matters. At the same time, users constructing colors expect natural syntax—they shouldn't need to
write 255.0_f64 when 255 is obviously an integer that can be promoted.
The tension is between:
- Precision: f64 provides sufficient precision for colorimetric calculations (15-17 significant digits)
- Ergonomics: Users want to write
srgb(255, 0, 0)notsrgb(255.0, 0.0, 0.0) - Simplicity: Avoiding generics over
f32/f64reduces complexity and compile times
Color libraries in other languages often use f32 (sufficient for display) or make precision a generic parameter (complex API). We choose f64 everywhere because:
- Modern CPUs handle f64 efficiently (often same throughput as f32)
- Intermediate calculations benefit from extra precision
- Output to f32 for GPU/image use is a simple cast at the boundary
Decision #
The Component Type #
A newtype wrapper around f64:
#[derive(Clone, Copy, Debug)]
pub struct Component(pub(crate) f64);
The inner value is pub(crate) to allow direct access within the library while maintaining encapsulation
for external users.
Universal Numeric Input #
Component implements From<T> for all standard numeric types:
| Type Family | Types |
|---|---|
| Floats | f32, f64 |
| Signed | i8, i16, i32, i64, i128, isize |
| Unsigned | u8, u16, u32, u64, u128, usize |
This enables APIs that accept impl Into<Component>:
impl Xyz {
pub fn new(
x: impl Into<Component>,
y: impl Into<Component>,
z: impl Into<Component>,
) -> Self {
// ...
}
}
// All of these work:
let a = Xyz::new(0.95047, 1.0, 1.08883); // f64
let b = Xyz::new(0.95047_f32, 1.0, 1.08883); // mixed f32/f64
let c = Xyz::new(0, 1, 0); // integers
let d = Xyz::new(95047_u32, 100000, 108883); // large integers
Arithmetic Operations #
Component implements standard arithmetic traits with generic right-hand sides:
impl<T> Add<T> for Component where T: Into<Self> {
type Output = Self;
fn add(self, rhs: T) -> Self {
Self(self.0 + rhs.into().0)
}
}
This allows natural arithmetic:
let c = Component::new(1.0);
let result = c + 0.5; // f64
let result = c * 2; // integer
let result = c / 3.0_f32; // f32
Supported operations: Add, AddAssign, Sub, SubAssign, Mul, MulAssign, Div, DivAssign, Neg
Comparison #
PartialEq and PartialOrd compare the underlying f64 values. Note that Component does not implement
Eq or Ord because f64 has NaN values.
impl<T> PartialEq<T> for Component where T: Into<Self> + Copy {
fn eq(&self, other: &T) -> bool {
self.0 == (*other).into().0
}
}
Display #
Display uses configurable precision, defaulting to 4 decimal places:
impl Display for Component {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{:.precision$}", self.0, precision = f.precision().unwrap_or(4))
}
}
Usage:
let c = Component::new(1.23456789);
format!("{}", c) // "1.2346" (default)
format!("{:.2}", c) // "1.23"
format!("{:.6}", c) // "1.234568"
Const Construction #
For compile-time color definitions, Component::new_const accepts only f64. Color spaces expose their own
new_const functions that wrap this internally:
impl Component {
pub const fn new_const(value: f64) -> Self {
Self(value)
}
}
impl Xyz {
pub const fn new_const(x: f64, y: f64, z: f64) -> Self {
Self {
x: Component::new_const(x),
y: Component::new_const(y),
z: Component::new_const(z),
}
}
}
// Enables const color definitions
const D65_WHITE: Xyz = Xyz::new_const(0.95047, 1.0, 1.08883);
Implementation Phases #
- Phase 1: Core Type - Implement Component with From traits and arithmetic
- Phase 2: Integration - Use Component in color space structs
- Phase 3: Specialized Methods - Add utility methods (e.g.,
clamp,abs)
Consequences #
Positive #
- Ergonomic API: Users write
srgb(255, 0, 0)instead ofsrgb(255.0, 0.0, 0.0) - Consistent precision: All calculations use f64, eliminating precision-related bugs
- No generic complexity: Library code is simpler without
T: Floatbounds everywhere - Compile-time support:
new_constenables const color definitions
Negative #
- Memory overhead: f64 uses 8 bytes per component vs 4 for f32 (24 bytes vs 12 bytes for RGB)
- Conversion cost: Integer/f32 inputs convert to f64 (negligible in practice)
- No f32 optimization path: Applications that only need f32 precision still pay for f64
Open Questions #
- Should
Componentimplement approximate equality (within epsilon) for floating-point comparisons?
Rejected Alternatives #
Angular Component Type: A specialized AngularComponent that automatically wraps values to 0-360° was
considered but rejected. Color spaces with angular components (like hue in Oklch) handle wrapping in their
own methods. Additionally, automatic wrapping cannot be implemented in const fn because rem_euclid is
not const-stable, which would prevent const construction of colors with hue components.
Future Work #
- SIMD Batch Operations: Vectorized operations for processing arrays of components