@@ -15,7 +15,7 @@ use crate::color::ColorFloat;
1515
1616#[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
1717pub enum BlendMode {
18- /// Standard Porter-Duff over (equivalent to `Color::over`)
18+ // separable blend modes
1919 Normal ,
2020 Multiply ,
2121 Screen ,
@@ -28,6 +28,11 @@ pub enum BlendMode {
2828 SoftLight ,
2929 Difference ,
3030 Exclusion ,
31+ // non-separable blend modes
32+ Hue ,
33+ Saturation ,
34+ Color ,
35+ Luminosity ,
3136}
3237
3338// stores sRGB under the hood, with lots of conversion funcs
@@ -189,7 +194,7 @@ impl Color {
189194 } ;
190195
191196 // Encode back to sRGB u8 (your encoders already clamp)
192- Color :: from_linear ( [ cr, cg, cb, ca] )
197+ Self :: from_linear ( [ cr, cg, cb, ca] )
193198 }
194199
195200 // Faster (but slightly less accurate) "over" in sRGB space.
@@ -641,6 +646,130 @@ impl Color {
641646 }
642647 }
643648
649+ // helpers for non-separable blend modes
650+ fn lum ( c : [ ColorFloat ; 3 ] ) -> ColorFloat {
651+ let [ r, g, b] = c;
652+ 0.3 * r + 0.59 * g + 0.11 * b
653+ }
654+
655+ fn clip_color ( c : [ ColorFloat ; 3 ] ) -> [ ColorFloat ; 3 ] {
656+ let [ r, g, b] = c;
657+ let l = Self :: lum ( c) ;
658+ let n = r. min ( g) . min ( b) ;
659+ let x = r. max ( g) . max ( b) ;
660+ if n < 0.0 {
661+ c. iter ( )
662+ . map ( |& v| l + ( ( ( v - l) * l) / ( l - n) ) )
663+ . collect :: < Vec < _ > > ( )
664+ . try_into ( )
665+ . unwrap ( )
666+ } else if x > 1.0 {
667+ c. iter ( )
668+ . map ( |& v| l + ( ( ( v - l) * ( 1.0 - l) ) / ( x - l) ) )
669+ . collect :: < Vec < _ > > ( )
670+ . try_into ( )
671+ . unwrap ( )
672+ } else {
673+ c
674+ }
675+ }
676+
677+ fn set_lum ( c : [ ColorFloat ; 3 ] , l : ColorFloat ) -> [ ColorFloat ; 3 ] {
678+ let [ r, g, b] = c;
679+ let d = l - Self :: lum ( c) ;
680+ Self :: clip_color ( [ r + d, g + d, b + d] )
681+ }
682+
683+ fn sat ( c : [ ColorFloat ; 3 ] ) -> ColorFloat {
684+ let [ r, g, b] = c;
685+ r. max ( g) . max ( b) - r. min ( g) . min ( b)
686+ }
687+
688+ fn set_sat ( c : [ ColorFloat ; 3 ] , s : ColorFloat ) -> [ ColorFloat ; 3 ] {
689+ let [ r, g, b] = c;
690+ let max: ColorFloat ;
691+ let max_ch: char ;
692+ let min: ColorFloat ;
693+ let min_ch: char ;
694+ let mid: ColorFloat ;
695+ let mid_ch: char ;
696+
697+ if r >= g && r >= b {
698+ max = r;
699+ max_ch = 'r' ;
700+
701+ if g <= b {
702+ min = g;
703+ min_ch = 'g' ;
704+ mid = b;
705+ mid_ch = 'b' ;
706+ } else {
707+ min = b;
708+ min_ch = 'b' ;
709+ mid = g;
710+ mid_ch = 'g' ;
711+ }
712+ } else if g >= r && g >= b {
713+ max = g;
714+ max_ch = 'g' ;
715+ if r <= b {
716+ min = r;
717+ min_ch = 'r' ;
718+ mid = b;
719+ mid_ch = 'b' ;
720+ } else {
721+ min = b;
722+ min_ch = 'b' ;
723+ mid = r;
724+ mid_ch = 'r' ;
725+ }
726+ } else {
727+ max = b;
728+ max_ch = 'b' ;
729+ if r <= g {
730+ min = r;
731+ min_ch = 'r' ;
732+ mid = g;
733+ mid_ch = 'g' ;
734+ } else {
735+ min = g;
736+ min_ch = 'g' ;
737+ mid = r;
738+ mid_ch = 'r' ;
739+ }
740+ }
741+
742+ let chroma = max - min;
743+ if chroma == 0.0 {
744+ return [ 0.0 , 0.0 , 0.0 ] ;
745+ }
746+
747+ let scale = s / chroma;
748+
749+ let new_max = mid + ( max - mid) * scale;
750+ let new_min = mid - ( mid - min) * scale;
751+ let new_mid = mid;
752+
753+ let ( mut out_r, mut out_g, mut out_b) = ( 0.0 , 0.0 , 0.0 ) ;
754+ match max_ch {
755+ 'r' => out_r = new_max,
756+ 'g' => out_g = new_max,
757+ _ => out_b = new_max,
758+ }
759+ match min_ch {
760+ 'r' => out_r = new_min,
761+ 'g' => out_g = new_min,
762+ _ => out_b = new_min,
763+ }
764+ match mid_ch {
765+ 'r' => out_r = new_mid,
766+ 'g' => out_g = new_mid,
767+ _ => out_b = new_mid,
768+ }
769+
770+ [ out_r, out_g, out_b]
771+ }
772+
644773 #[ inline]
645774 fn blend_channel ( mode : BlendMode , b : ColorFloat , s : ColorFloat ) -> ColorFloat {
646775 use BlendMode :: * ;
@@ -694,6 +823,7 @@ impl Color {
694823 Difference => ( b - s) . abs ( ) ,
695824 Exclusion => b + s - 2.0 * b * s,
696825 // non-separable blend modes
826+ _ => s, // TODO placeholder
697827 }
698828 }
699829}
0 commit comments