Skip to content

Commit a67710e

Browse files
committed
add oklch conversions and lerping to Color
1 parent f141f87 commit a67710e

File tree

1 file changed

+108
-4
lines changed

1 file changed

+108
-4
lines changed

src/color/model.rs

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ impl Color {
7979
])
8080
}
8181

82+
pub fn lerp_oklch(self, other: Color, t: ColorFloat) -> Color {
83+
let t = t.clamp(0.0, 1.0);
84+
let [l1, c1, h1] = self.into_oklch();
85+
let [l2, c2, h2] = other.into_oklch();
86+
87+
// If one is near gray, carry the other hue to avoid wild spins
88+
let (h1, h2) = if c1 < 1e-5 {
89+
(h2, h2)
90+
} else if c2 < 1e-5 {
91+
(h1, h1)
92+
} else {
93+
(h1, h2)
94+
};
95+
96+
// shortest hue delta
97+
let mut dh = h2 - h1;
98+
if dh > 180.0 {
99+
dh -= 360.0;
100+
}
101+
if dh <= -180.0 {
102+
dh += 360.0;
103+
}
104+
105+
let l = l1 + (l2 - l1) * t;
106+
let c = c1 + (c2 - c1) * t;
107+
let mut h = h1 + dh * t;
108+
if h < 0.0 {
109+
h += 360.0;
110+
}
111+
if h >= 360.0 {
112+
h -= 360.0;
113+
}
114+
115+
Self::from_oklch([l, c.max(0.0), h])
116+
}
117+
82118
// Porter-Duff "over" in linear space
83119
// for speed over accuracy, use `over_srgb_fast`
84120
// https://keithp.com/~keithp/porterduff/p253-porter.pdf
@@ -486,12 +522,12 @@ impl Color {
486522

487523
#[must_use]
488524
#[inline]
489-
pub fn from_oklab(ok: [ColorFloat; 3]) -> Self {
525+
pub fn from_oklab(lab: [ColorFloat; 3]) -> Self {
490526
// source: https://bottosson.github.io/posts/oklab/
491527

492-
let l_ = ok[0] + 0.39633778 * ok[1] + 0.21580376 * ok[2];
493-
let m_ = ok[0] - 0.105561346 * ok[1] - 0.06385417 * ok[2];
494-
let s_ = ok[0] - 0.08948418 * ok[1] - 1.2914856 * ok[2];
528+
let l_ = lab[0] + 0.39633778 * lab[1] + 0.21580376 * lab[2];
529+
let m_ = lab[0] - 0.105561346 * lab[1] - 0.06385417 * lab[2];
530+
let s_ = lab[0] - 0.08948418 * lab[1] - 1.2914856 * lab[2];
495531

496532
let l = l_ * l_ * l_;
497533
let m = m_ * m_ * m_;
@@ -523,6 +559,74 @@ impl Color {
523559
]
524560
}
525561

562+
#[must_use]
563+
#[inline]
564+
pub fn from_oklch(lch: [ColorFloat; 3]) -> Self {
565+
// Gamut mapping to keep rgb valid when converting
566+
// current method: chroma reduction at fixed L and H
567+
// switch to Björn Ottosson's "gamut mapping in OKLCH"
568+
// in the future if perfect ramping needed
569+
let within = |rgb: [ColorFloat; 3]| {
570+
rgb[0] >= 0.0
571+
&& rgb[0] <= 1.0
572+
&& rgb[1] >= 0.0
573+
&& rgb[1] <= 1.0
574+
&& rgb[2] >= 0.0
575+
&& rgb[2] <= 1.0
576+
};
577+
let to_srgb = |lch: [ColorFloat; 3]| {
578+
let lin = Self::from_oklab(Self::oklch_to_oklab(lch)).into_linear();
579+
[lin[0], lin[1], lin[2]]
580+
};
581+
582+
if within(to_srgb(lch)) {
583+
return Self::from_oklab(Self::oklch_to_oklab(lch));
584+
}
585+
586+
// shrink c
587+
let (mut lo, mut hi) = (0.0f32, lch[1]);
588+
for _ in 0..24 {
589+
// ~1e-7 precision
590+
let mid = 0.5 * (lo + hi);
591+
let test = [lch[0], mid, lch[2]];
592+
if within(to_srgb(test)) {
593+
lo = mid;
594+
} else {
595+
hi = mid;
596+
}
597+
}
598+
599+
Self::from_oklab(Self::oklch_to_oklab([lch[0], lo, lch[2]]))
600+
}
601+
602+
#[must_use]
603+
#[inline]
604+
pub fn into_oklch(self) -> [ColorFloat; 3] {
605+
Self::oklab_to_oklch(self.into_oklab())
606+
}
607+
608+
#[must_use]
609+
#[inline]
610+
pub fn oklab_to_oklch(ok: [ColorFloat; 3]) -> [ColorFloat; 3] {
611+
let (l, a, b) = (ok[0], ok[1], ok[2]);
612+
let c = (a * a + b * b).sqrt();
613+
let mut h = b.atan2(a).to_degrees();
614+
if h < 0.0 {
615+
h += 360.0;
616+
}
617+
[l, c, h]
618+
}
619+
620+
#[must_use]
621+
#[inline]
622+
pub fn oklch_to_oklab(lch: [ColorFloat; 3]) -> [ColorFloat; 3] {
623+
let (l, c, h) = (lch[0], lch[1], lch[2]);
624+
let h = h.to_radians();
625+
let a = c * h.cos();
626+
let b = c * h.sin();
627+
[l, a, b]
628+
}
629+
526630
// --- private methods --- //
527631

528632
/// Decode an 8 bit sRGB value into a linear float using a lookup table.

0 commit comments

Comments
 (0)