@@ -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