1515from itertools import zip_longest as zipl
1616from .css import parse
1717from .types import VectorLike , Vector , ColorInput
18- from .spaces import Space , Cylindrical
18+ from .spaces import Space
1919from .spaces .hsv import HSV
2020from .spaces .srgb .css import sRGB
2121from .spaces .srgb_linear import sRGBLinear
6565class ColorMatch :
6666 """Color match object."""
6767
68+ __slots__ = ('color' , 'start' , 'end' )
69+
6870 def __init__ (self , color : 'Color' , start : int , end : int ) -> None :
6971 """Initialize."""
7072
@@ -588,9 +590,8 @@ def chromatic_adaptation(
588590 ) -> Vector :
589591 """Chromatic adaptation."""
590592
591- try :
592- adapter = cls .CAT_MAP [method if method is not None else cls .CHROMATIC_ADAPTATION ]
593- except KeyError :
593+ adapter = cls .CAT_MAP .get (method if method is not None else cls .CHROMATIC_ADAPTATION )
594+ if not adapter :
594595 raise ValueError ("'{}' is not a supported CAT" .format (method ))
595596
596597 return adapter .adapt (w1 , w2 , xyz )
@@ -604,14 +605,7 @@ def clip(self, space: Optional[str] = None) -> 'Color':
604605
605606 # Convert to desired space
606607 c = self .convert (space , in_place = True )
607-
608- # If we are perfectly in gamut, don't waste time clipping.
609- if c .in_gamut (tolerance = 0.0 ):
610- if isinstance (c ._space , Cylindrical ):
611- name = c ._space .hue_name ()
612- c .set (name , util .constrain_hue (c [name ]))
613- else :
614- gamut .clip_channels (c )
608+ gamut .clip_channels (c )
615609
616610 # Adjust "this" color
617611 return c .convert (orig_space , in_place = True )
@@ -625,60 +619,48 @@ def fit(
625619 ) -> 'Color' :
626620 """Fit the gamut using the provided method."""
627621
622+ if method is None :
623+ method = self .FIT
624+
628625 # Dedicated clip method.
629- orig_space = self . space ()
630- if method == 'clip' or ( method is None and self . FIT == "clip" ):
626+ if method == 'clip' :
627+
631628 return self .clip (space )
632629
630+ orig_space = self .space ()
633631 if space is None :
634632 space = self .space ()
635633
636- if method is None :
637- method = self .FIT
638-
639634 # Select appropriate mapping algorithm
640- if method in self .FIT_MAP :
641- func = self . FIT_MAP [ method ]. fit
642- else :
635+ mapping = self .FIT_MAP . get ( method )
636+ if not mapping :
637+
643638 # Unknown fit method
644639 raise ValueError ("'{}' gamut mapping is not currently supported" .format (method ))
645640
646641 # Convert to desired space
647- c = self .convert (space , in_place = True )
642+ self .convert (space , in_place = True )
648643
649- # If we are perfectly in gamut, don't waste time fitting, just normalize hues.
650- # If out of gamut, apply mapping/clipping/etc.
651- if c .in_gamut (tolerance = 0.0 ):
652- if isinstance (c ._space , Cylindrical ):
653- name = c ._space .hue_name ()
654- c .set (name , util .constrain_hue (c [name ]))
655- else :
656- # Doesn't seem to be an easy way that `mypy` can know whether this is the ABC class or not
657- func (c , ** kwargs )
644+ # Call the appropriate gamut mapping algorithm
645+ mapping .fit (self , ** kwargs )
658646
659- # Adjust "this" color
660- return c .convert (orig_space , in_place = True )
647+ # Convert back to the original color space
648+ return self .convert (orig_space , in_place = True )
661649
662650 def in_gamut (self , space : Optional [str ] = None , * , tolerance : float = util .DEF_FIT_TOLERANCE ) -> bool :
663651 """Check if current color is in gamut."""
664652
665653 if space is None :
666654 space = self .space ()
667655
668- # Check gamut in the provided space
669- if space is not None and space != self .space ():
670- c = self .convert (space )
671- return c .in_gamut (tolerance = tolerance )
656+ # Check if gamut is in the provided space
657+ c = self .convert (space ) if space is not None and space != self .space () else self
672658
673659 # Check the color space specified for gamut checking.
674660 # If it proves to be in gamut, we will then test if the current
675661 # space is constrained properly.
676- if self ._space .GAMUT_CHECK is not None :
677- c = self .convert (self ._space .GAMUT_CHECK )
678- if not c .in_gamut (tolerance = tolerance ):
679- return False
680-
681- return gamut .verify (self , tolerance )
662+ if c ._space .GAMUT_CHECK is not None and not c .convert (c ._space .GAMUT_CHECK ).in_gamut (tolerance = tolerance ):
663+ return False
682664
683665 def mask (self , channel : Union [str , Sequence [str ]], * , invert : bool = False , in_place : bool = False ) -> 'Color' :
684666 """Mask color channels."""
@@ -710,7 +692,11 @@ def mix(
710692
711693 if not self ._is_color (color ) and not isinstance (color , (str , Mapping )):
712694 raise TypeError ("Unexpected type '{}'" .format (type (color )))
713- mixed = self .interpolate ([self , color ], ** interpolate_args )(percent )
695+ i = self .interpolate ([self , color ], ** interpolate_args )
696+ # Scale really needs to be between 0 and 1 as mix deals in percentages.
697+ if i .domain :
698+ i .domain = interpolate .normalize_domain (i .domain )
699+ mixed = i (percent )
714700 return self .mutate (mixed ) if in_place else mixed
715701
716702 @classmethod
@@ -726,7 +712,11 @@ def steps(
726712 ) -> List ['Color' ]:
727713 """Discrete steps."""
728714
729- return cls .interpolate (colors , ** interpolate_args ).steps (steps , max_steps , max_delta_e , delta_e )
715+ i = cls .interpolate (colors , ** interpolate_args )
716+ # Scale really needs to be between 0 and 1 or steps will break
717+ if i .domain :
718+ i .domain = interpolate .normalize_domain (i .domain )
719+ return i .steps (steps , max_steps , max_delta_e , delta_e )
730720
731721 @classmethod
732722 def interpolate (
@@ -739,6 +729,7 @@ def interpolate(
739729 hue : str = util .DEF_HUE_ADJ ,
740730 premultiplied : bool = True ,
741731 extrapolate : bool = False ,
732+ domain : Optional [List [float ]] = None ,
742733 method : str = "linear" ,
743734 ** kwargs : Any
744735 ) -> Interpolator :
@@ -764,6 +755,7 @@ def interpolate(
764755 hue = hue ,
765756 premultiplied = premultiplied ,
766757 extrapolate = extrapolate ,
758+ domain = domain ,
767759 ** kwargs
768760 )
769761
@@ -828,10 +820,10 @@ def delta_e(
828820 if method is None :
829821 method = self .DELTA_E
830822
831- try :
832- return self .DE_MAP [method ].distance (self , color , ** kwargs )
833- except KeyError :
823+ delta = self .DE_MAP .get (method )
824+ if not delta :
834825 raise ValueError ("'{}' is not currently a supported distancing algorithm." .format (method ))
826+ return delta .distance (self , color , ** kwargs )
835827
836828 def distance (self , color : ColorInput , * , space : str = "lab" ) -> float :
837829 """Delta."""
@@ -860,33 +852,84 @@ def contrast(self, color: ColorInput, method: Optional[str] = None) -> float:
860852 color = self ._handle_color_input (color )
861853 return contrast .contrast (method , self , color )
862854
863- def get (self , name : str ) -> float :
855+ @overload
856+ def get (self , name : str ) -> float : # noqa: D105
857+ ...
858+
859+ @overload
860+ def get (self , name : Union [List [str ], Tuple [str , ...]]) -> List [float ]: # noqa: D105
861+ ...
862+
863+ def get (self , name : Union [str , List [str ], Tuple [str , ...]]) -> Union [float , List [float ]]:
864864 """Get channel."""
865865
866- # Handle space.attribute
867- if '.' in name :
868- space , channel = name .split ('.' , 1 )
869- obj = self .convert (space )
870- return obj [channel ]
866+ # Handle single channel
867+ if isinstance (name , str ):
868+ # Handle space.channel
869+ if '.' in name :
870+ space , channel = name .split ('.' , 1 )
871+ return self .convert (space )[channel ]
872+ return self [name ]
871873
872- return self [name ]
874+ # Handle list of channels
875+ else :
876+ original_space = current_space = self .space ()
877+ obj = self
878+ values = []
879+
880+ for n in name :
881+ # Handle space.channel
882+ space , channel = n .split ('.' , 1 ) if '.' in n else (original_space , n )
883+ if space != current_space :
884+ obj = self if space == original_space else self .convert (space )
885+ current_space = space
886+ values .append (obj [channel ])
887+ return values
873888
874889 def set ( # noqa: A003
875890 self ,
876- name : str ,
877- value : Union [float , Callable [..., float ]]
891+ name : Union [ str , Dict [ str , Union [ float , Callable [..., float ]]]] ,
892+ value : Optional [ Union [float , Callable [..., float ]]] = None
878893 ) -> 'Color' :
879894 """Set channel."""
880895
881- # Handle space.attribute
882- if '.' in name :
883- space , channel = name .split ('.' , 1 )
884- obj = self .convert (space )
885- obj [channel ] = value (obj [channel ]) if callable (value ) else value
886- return self .update (obj )
896+ # Set all the channels in a dictionary.
897+ # Sort by name to reduce how many times we convert
898+ # when dealing with different color spaces.
899+ if value is None :
900+ if isinstance (name , str ):
901+ raise ValueError ("Missing the positional 'value' argument for channel '{}'" .format (name ))
902+
903+ original_space = current_space = self .space ()
904+ obj = self .clone ()
905+
906+ for k , v in name .items ():
907+
908+ # Handle space.channel
909+ space , channel = k .split ('.' , 1 ) if '.' in k else (original_space , k )
910+ if space != current_space :
911+ obj .convert (space , in_place = True )
912+ current_space = space
913+ obj [channel ] = v (obj [channel ]) if callable (v ) else v
914+
915+ # Update the original color
916+ self .update (obj )
917+
918+ # Set a single channel value
919+ else :
920+ if isinstance (name , dict ):
921+ raise ValueError ("A dict of channels and values cannot be used with the positional 'value' parameter" )
922+
923+ # Handle space.channel
924+ if '.' in name :
925+ space , channel = name .split ('.' , 1 )
926+ obj = self .convert (space )
927+ obj [channel ] = value (obj [channel ]) if callable (value ) else value
928+ return self .update (obj )
929+
930+ # Handle a function that modifies the value or a direct value
931+ self [name ] = value (self [name ]) if callable (value ) else value
887932
888- # Handle a function that modifies the value or a direct value
889- self [name ] = value (self [name ]) if callable (value ) else value
890933 return self
891934
892935
0 commit comments