#!/usr/bin/env python# -*- coding: utf-8 -*-"""Classes and functions for working with colors."""__all__=["colorExamples","colorNames","colorSpaces","isValidColor","hex2rgb255","Color"]importrefrommathimportinffrompsychopyimportloggingimportpsychopy.tools.colorspacetoolsasctfrompsychopy.tools.mathtoolsimportinfrangeimportnumpyasnp# Dict of examples of Psychopy Red at 12% opacity in different formatscolorExamples={'named':'crimson','hex':'#F2545B','hexa':'#F2545B1E','rgb':(0.89,-0.35,-0.28),'rgba':(0.89,-0.35,-0.28,-0.76),'rgb1':(0.95,0.32,0.36),'rgba1':(0.95,0.32,0.36,0.12),'rgb255':(242,84,91),'rgba255':(242,84,91,30),'hsv':(357,0.65,0.95),'hsva':(357,0.65,0.95,0.12),}# Dict of named colourscolorNames={"none":(0,0,0,0),"transparent":(0,0,0,0),"aliceblue":(0.882352941176471,0.945098039215686,1),"antiquewhite":(0.96078431372549,0.843137254901961,0.686274509803922),"aqua":(-1,1,1),"aquamarine":(-0.00392156862745097,1,0.662745098039216),"azure":(0.882352941176471,1,1),"beige":(0.92156862745098,0.92156862745098,0.725490196078431),"bisque":(1,0.788235294117647,0.537254901960784),"black":(-1,-1,-1),"blanchedalmond":(1,0.843137254901961,0.607843137254902),"blue":(-1,-1,1),"blueviolet":(0.0823529411764705,-0.662745098039216,0.772549019607843),"brown":(0.294117647058824,-0.670588235294118,-0.670588235294118),"burlywood":(0.741176470588235,0.443137254901961,0.0588235294117647),"cadetblue":(-0.254901960784314,0.23921568627451,0.254901960784314),"chartreuse":(-0.00392156862745097,1,-1),"chestnut":(0.607843137254902,-0.27843137254902,-0.27843137254902),"chocolate":(0.647058823529412,-0.176470588235294,-0.764705882352941),"coral":(1,-0.00392156862745097,-0.372549019607843),"cornflowerblue":(-0.215686274509804,0.168627450980392,0.858823529411765),"cornsilk":(1,0.945098039215686,0.725490196078431),"crimson":(0.725490196078431,-0.843137254901961,-0.529411764705882),"cyan":(-1,1,1),"darkblue":(-1,-1,0.0901960784313725),"darkcyan":(-1,0.0901960784313725,0.0901960784313725),"darkgoldenrod":(0.443137254901961,0.0509803921568628,-0.913725490196078),"darkgray":(0.325490196078431,0.325490196078431,0.325490196078431),"darkgreen":(-1,-0.215686274509804,-1),"darkgrey":(0.325490196078431,0.325490196078431,0.325490196078431),"darkkhaki":(0.482352941176471,0.435294117647059,-0.16078431372549),"darkmagenta":(0.0901960784313725,-1,0.0901960784313725),"darkolivegreen":(-0.333333333333333,-0.16078431372549,-0.631372549019608),"darkorange":(1,0.0980392156862746,-1),"darkorchid":(0.2,-0.607843137254902,0.6),"darkred":(0.0901960784313725,-1,-1),"darksalmon":(0.827450980392157,0.176470588235294,-0.0431372549019607),"darkseagreen":(0.12156862745098,0.474509803921569,0.12156862745098),"darkslateblue":(-0.435294117647059,-0.52156862745098,0.0901960784313725),"darkslategray":(-0.631372549019608,-0.380392156862745,-0.380392156862745),"darkslategrey":(-0.631372549019608,-0.380392156862745,-0.380392156862745),"darkturquoise":(-1,0.615686274509804,0.63921568627451),"darkviolet":(0.16078431372549,-1,0.654901960784314),"deeppink":(1,-0.843137254901961,0.152941176470588),"deepskyblue":(-1,0.498039215686275,1),"dimgray":(-0.176470588235294,-0.176470588235294,-0.176470588235294),"dimgrey":(-0.176470588235294,-0.176470588235294,-0.176470588235294),"dodgerblue":(-0.764705882352941,0.129411764705882,1),"firebrick":(0.396078431372549,-0.733333333333333,-0.733333333333333),"floralwhite":(1,0.96078431372549,0.882352941176471),"forestgreen":(-0.733333333333333,0.0901960784313725,-0.733333333333333),"fuchsia":(1,-1,1),"gainsboro":(0.725490196078431,0.725490196078431,0.725490196078431),"ghostwhite":(0.945098039215686,0.945098039215686,1),"gold":(1,0.686274509803922,-1),"goldenrod":(0.709803921568627,0.294117647058824,-0.749019607843137),"gray":(0.00392156862745097,0.00392156862745097,0.00392156862745097),"grey":(0.00392156862745097,0.00392156862745097,0.00392156862745097),"green":(-1,0.00392156862745097,-1),"greenyellow":(0.356862745098039,1,-0.631372549019608),"honeydew":(0.882352941176471,1,0.882352941176471),"hotpink":(1,-0.176470588235294,0.411764705882353),"indigo":(-0.411764705882353,-1,0.0196078431372548),"ivory":(1,1,0.882352941176471),"khaki":(0.882352941176471,0.803921568627451,0.0980392156862746),"lavender":(0.803921568627451,0.803921568627451,0.96078431372549),"lavenderblush":(1,0.882352941176471,0.92156862745098),"lawngreen":(-0.0274509803921569,0.976470588235294,-1),"lemonchiffon":(1,0.96078431372549,0.607843137254902),"lightblue":(0.356862745098039,0.694117647058824,0.803921568627451),"lightcoral":(0.882352941176471,0.00392156862745097,0.00392156862745097),"lightcyan":(0.756862745098039,1,1),"lightgoldenrodyellow":(0.96078431372549,0.96078431372549,0.647058823529412),"lightgray":(0.654901960784314,0.654901960784314,0.654901960784314),"lightgreen":(0.129411764705882,0.866666666666667,0.129411764705882),"lightgrey":(0.654901960784314,0.654901960784314,0.654901960784314),"lightpink":(1,0.427450980392157,0.513725490196078),"lightsalmon":(1,0.254901960784314,-0.0431372549019607),"lightseagreen":(-0.749019607843137,0.396078431372549,0.333333333333333),"lightskyblue":(0.0588235294117647,0.615686274509804,0.96078431372549),"lightslategray":(-0.0666666666666667,0.0666666666666667,0.2),"lightslategrey":(-0.0666666666666667,0.0666666666666667,0.2),"lightsteelblue":(0.380392156862745,0.537254901960784,0.741176470588235),"lightyellow":(1,1,0.756862745098039),"lime":(-1,1,-1),"limegreen":(-0.607843137254902,0.607843137254902,-0.607843137254902),"linen":(0.96078431372549,0.882352941176471,0.803921568627451),"magenta":(1,-1,1),"maroon":(0.00392156862745097,-1,-1),"mediumaquamarine":(-0.2,0.607843137254902,0.333333333333333),"mediumblue":(-1,-1,0.607843137254902),"mediumorchid":(0.458823529411765,-0.333333333333333,0.654901960784314),"mediumpurple":(0.152941176470588,-0.12156862745098,0.717647058823529),"mediumseagreen":(-0.529411764705882,0.403921568627451,-0.113725490196078),"mediumslateblue":(-0.0352941176470588,-0.184313725490196,0.866666666666667),"mediumspringgreen":(-1,0.96078431372549,0.207843137254902),"mediumturquoise":(-0.435294117647059,0.63921568627451,0.6),"mediumvioletred":(0.56078431372549,-0.835294117647059,0.0431372549019609),"midnightblue":(-0.803921568627451,-0.803921568627451,-0.12156862745098),"mintcream":(0.92156862745098,1,0.96078431372549),"mistyrose":(1,0.788235294117647,0.764705882352941),"moccasin":(1,0.788235294117647,0.419607843137255),"navajowhite":(1,0.741176470588235,0.356862745098039),"navy":(-1,-1,0.00392156862745097),"oldlace":(0.984313725490196,0.92156862745098,0.803921568627451),"olive":(0.00392156862745097,0.00392156862745097,-1),"olivedrab":(-0.16078431372549,0.113725490196078,-0.725490196078431),"orange":(1,0.294117647058824,-1),"orangered":(1,-0.458823529411765,-1),"orchid":(0.709803921568627,-0.12156862745098,0.67843137254902),"palegoldenrod":(0.866666666666667,0.819607843137255,0.333333333333333),"palegreen":(0.192156862745098,0.968627450980392,0.192156862745098),"paleturquoise":(0.372549019607843,0.866666666666667,0.866666666666667),"palevioletred":(0.717647058823529,-0.12156862745098,0.152941176470588),"papayawhip":(1,0.874509803921569,0.670588235294118),"peachpuff":(1,0.709803921568627,0.450980392156863),"peru":(0.607843137254902,0.0431372549019609,-0.505882352941176),"pink":(1,0.505882352941176,0.592156862745098),"plum":(0.733333333333333,0.254901960784314,0.733333333333333),"powderblue":(0.380392156862745,0.756862745098039,0.803921568627451),"purple":(0.00392156862745097,-1,0.00392156862745097),"red":(1,-1,-1),"rosybrown":(0.474509803921569,0.12156862745098,0.12156862745098),"royalblue":(-0.490196078431373,-0.176470588235294,0.764705882352941),"saddlebrown":(0.0901960784313725,-0.458823529411765,-0.850980392156863),"salmon":(0.96078431372549,0.00392156862745097,-0.105882352941176),"sandybrown":(0.913725490196079,0.286274509803922,-0.247058823529412),"seagreen":(-0.63921568627451,0.0901960784313725,-0.317647058823529),"seashell":(1,0.92156862745098,0.866666666666667),"sienna":(0.254901960784314,-0.356862745098039,-0.647058823529412),"silver":(0.505882352941176,0.505882352941176,0.505882352941176),"skyblue":(0.0588235294117647,0.615686274509804,0.843137254901961),"slateblue":(-0.168627450980392,-0.294117647058823,0.607843137254902),"slategray":(-0.12156862745098,0.00392156862745097,0.129411764705882),"slategrey":(-0.12156862745098,0.00392156862745097,0.129411764705882),"snow":(1,0.96078431372549,0.96078431372549),"springgreen":(-1,1,-0.00392156862745097),"steelblue":(-0.450980392156863,0.0196078431372548,0.411764705882353),"tan":(0.647058823529412,0.411764705882353,0.0980392156862746),"teal":(-1,0.00392156862745097,0.00392156862745097),"thistle":(0.694117647058824,0.498039215686275,0.694117647058824),"tomato":(1,-0.223529411764706,-0.443137254901961),"turquoise":(-0.498039215686275,0.756862745098039,0.631372549019608),"violet":(0.866666666666667,0.0196078431372548,0.866666666666667),"wheat":(0.92156862745098,0.741176470588235,0.403921568627451),"white":(1,1,1),"whitesmoke":(0.92156862745098,0.92156862745098,0.92156862745098),"yellow":(1,1,-1),"yellowgreen":(0.207843137254902,0.607843137254902,-0.607843137254902)}# Convert all named colors to numpy arraysforkeyincolorNames:colorNames[key]=np.array(colorNames[key])# Dict of regexpressions/ranges for different formatscolorSpaces={'named':re.compile("|".join(list(colorNames))),# A named colour space'hex':re.compile(r'#[\dabcdefABCDEF]{6}'),# Hex'rgb':[infrange(-1,1),infrange(-1,1),infrange(-1,1)],# RGB from -1 to 1'rgba':[infrange(-1,1),infrange(-1,1),infrange(-1,1),infrange(0,1)],# RGB + alpha from -1 to 1'rgb1':[infrange(0,1),infrange(0,1),infrange(0,1)],# RGB from 0 to 1'rgba1':[infrange(0,1),infrange(0,1),infrange(0,1),infrange(0,1)],# RGB + alpha from 0 to 1'rgb255':[infrange(0,255,1),infrange(0,255,1),infrange(0,255,1)],# RGB from 0 to 255'rgba255':[infrange(0,255,1),infrange(0,255,1),infrange(0,255,1),infrange(0,1)],# RGB + alpha from 0 to 255'hsv':[infrange(0,360,1),infrange(0,1),infrange(0,1)],# HSV with hue from 0 to 360 and saturation/vibrancy from 0 to 1'hsva':[infrange(0,360,1),infrange(0,1),infrange(0,1),infrange(0,1)],# HSV with hue from 0 to 360 and saturation/vibrancy from 0 to 1 + alpha from 0 to 1# 'rec709TF': [infrange(-4.5, 1), infrange(-4.5, 1), infrange(-4.5, 1)], # rec709TF adjusted RGB from -4.5 to 1# 'rec709TFa': [infrange(-4.5, 1), infrange(-4.5, 1), infrange(-4.5, 1), infrange(0, 1)], # rec709TF adjusted RGB from -4.5 to 1 + alpha from 0 to 1'srgb':[infrange(-1,1),infrange(-1,1),infrange(-1,1)],# srgb from -1 to 1'srgba':[infrange(-1,1),infrange(-1,1),infrange(-1,1),infrange(0,1)],# srgb from -1 to 1 + alpha from 0 to 1'lms':[infrange(-1,1),infrange(-1,1),infrange(-1,1),infrange(0,1)],# LMS from -1 to 1'lmsa':[infrange(-1,1),infrange(-1,1),infrange(-1,1),infrange(0,1)],# LMS + alpha from 0 to 1'dkl':[infrange(-inf,inf),infrange(-inf,inf),infrange(-inf,inf),infrange(0,1)],# DKL placeholder: Accepts any values'dkla':[infrange(-inf,inf),infrange(-inf,inf),infrange(-inf,inf),infrange(0,1)],# DKLA placeholder: Accepts any values + alpha from 0 to 1'dklCart':[infrange(-inf,inf),infrange(-inf,inf),infrange(-inf,inf),infrange(0,1)],# Cartesian DKL placeholder: Accepts any values'dklaCart':[infrange(-inf,inf),infrange(-inf,inf),infrange(-inf,inf),infrange(0,1)],# Cartesian DKLA placeholder: Accepts any values + alpha from 0 to 1}# Create subgroups of spaces for easy referenceintegerSpaces=[]strSpaces=[]forkey,valincolorSpaces.items():ifisinstance(val,re.compile("").__class__):# Add any spaces which are str to a liststrSpaces.append(key)elifisinstance(val,(list,tuple)):# Add any spaces which are integer-only to a listforcellinval:ifisinstance(cell,infrange):ifcell.step==1andkeynotinintegerSpaces:integerSpaces.append(key)alphaSpaces=['rgba','rgba1','rgba255','hsva','srgba','lmsa','dkla','dklaCart']nonAlphaSpaces=list(colorSpaces)forvalinalphaSpaces:nonAlphaSpaces.remove(val)
[docs]classColor:"""A class to store color details, knows what colour space it's in and can supply colours in any space. Parameters ---------- color : ArrayLike or None Color values (coordinates). Value must be in a format applicable to the specified `space`. space : str or None Colorspace to interpret the value of `color` as being within. contrast : int or float Factor to modulate the contrast of the color. conematrix : ArrayLike or None Cone matrix for colorspaces which require it. Must be a 3x3 array. """def__init__(self,color=None,space=None,contrast=None,conematrix=None):self._cache={}self._renderCache={}self.contrast=contrastifisinstance(contrast,(int,float))else1self.alpha=1self.valid=Falseself.conematrix=conematrix# defined here but set laterself._requested=Noneself._requestedSpace=Noneself.set(color=color,space=space)
[docs]defvalidate(self,color,space=None):""" Check that a color value is valid in the given space, or all spaces if space==None. """# Treat None as a named colorifcolorisNone:color="none"ifisinstance(color,str):ifcolor=="":color="none"# Handle everything as an arrayifnotisinstance(color,np.ndarray):color=np.array(color)ifcolor.ndim<=1:color=np.reshape(color,(1,-1))# If data type is string, check against named and hex as these override other spacesifcolor.dtype.char=='U':# Remove superfluous quotesforiinrange((len(color[:,0]))):color[i,0]=color[i,0].replace("\"","").replace("'","")# If colors are all named, override color spacenamedMatch=np.vectorize(lambdacol:bool(colorSpaces['named'].fullmatch(str(col).lower())))# match regex against namedifall(namedMatch(color[:,0])):space='named'# If colors are all hex, override color spacehexMatch=np.vectorize(lambdacol:bool(colorSpaces['hex'].fullmatch(str(col))))# match regex against hexifall(hexMatch(color[:,0])):space='hex'# If color is a string but does not match any string space, it's invalidifspacenotinstrSpaces:self.valid=False# Error if space still not setifnotspace:self.valid=FalseraiseValueError("Please specify a color space.")# Check that color space is validifnotspaceincolorSpaces:self.valid=FalseraiseValueError("{} is not a valid color space.".format(space))# Get number of columnsifcolor.ndim==1:ncols=len(color)else:ncols=color.shape[1]# Extract alpha if setifspaceinstrSpacesandncols>1:# If color should only be one value, extract second rowself.alpha=color[:,1]color=color[:,0]ncols-=1elifspacenotinstrSpacesandncols>3:# If color should be triplet, extract fourth rowself.alpha=color[:,3]color=color[:,:3]ncols-=1elifspacenotinstrSpacesandncols==2:# If color should be triplet but is single value, extract second rowself.alpha=color[:,1]color=color[:,1]ncols-=1# If single value given in place of triplet, duplicate itifspacenotinstrSpacesandncols==1:color=np.tile(color,(1,3))# ncols = 3 # unused?# If values should be integers, round themifspaceinintegerSpaces:color.round()# Finally, if array is only 1 long, remove extraneous dimensionifcolor.shape[0]==1:color=color[0]returncolor,space
[docs]defset(self,color=None,space=None):"""Set the colour of this object - essentially the same as what happens on creation, but without having to initialise a new object. """# If input is a Color object, duplicate all settingsifisinstance(color,Color):self._requested=color._requestedself._requestedSpace=color._requestedSpaceself.valid=color.validifcolor.valid:self.rgba=color.rgbareturn# Store requested colour and space (or defaults, if none given)self._requested=colorself._requestedSpace=space# Validate and prepare valuescolor,space=self.validate(color,space)# Convert to lingua francaifspaceincolorSpaces:self.valid=Truesetattr(self,space,color)else:self.valid=FalseraiseValueError("{} is not a valid color space.".format(space))
[docs]defrender(self,space='rgb'):"""Apply contrast to the base color value and return the adjusted color value. """ifspacenotincolorSpaces:raiseValueError(f"{space} is not a valid color space")# If value is cached, return it rather than doing calculations againifspaceinself._renderCache:returnself._renderCache[space]# Transform contrast to match rgbcontrast=self.contrastcontrast=np.reshape(contrast,(-1,1))contrast=np.hstack((contrast,contrast,contrast))# Multiplyadj=np.clip(self.rgb*contrast,-1,1)buffer=self.copy()buffer.rgb=adjself._renderCache[space]=getattr(buffer,space)returnself._renderCache[space]
def__repr__(self):"""If colour is printed, it will display its class and value. """ifself.valid:ifself.named:return(f"<{self.__class__.__module__}."f"{self.__class__.__name__}: {self.named}, "f"alpha={self.alpha}>")else:return(f"<{self.__class__.__module__}."f"{self.__class__.__name__}: "f"{tuple(np.round(self.rgba,2))}>")else:return(f"<{self.__class__.__module__}."f"{self.__class__.__name__}: Invalid>")def__bool__(self):"""Determines truth value of object"""returnself.validdef__len__(self):"""Determines the length of object"""iflen(self.rgb.shape)>1:returnself.rgb.shape[0]else:returnint(bool(self.rgb.shape))# --------------------------------------------------------------------------# Rich comparisons#def__eq__(self,target):"""`==` will compare RGBA values, rounded to 2dp"""ifisinstance(target,Color):returnnp.all(np.round(target.rgba,2)==np.round(self.rgba,2))eliftarget==None:returnself._requestedisNoneelse:returnFalsedef__ne__(self,target):"""`!=` will return the opposite of `==`"""returnnotself==target# --------------------------------------------------------------------------# Operators#def__add__(self,other):buffer=self.copy()# If target is a list or tuple, convert it to an arrayifisinstance(other,(list,tuple)):other=np.array(other)# If target is a single number, add it to each rgba valueifisinstance(other,(int,float)):buffer.rgba=self.rgba+other# If target is an array, add the arrays provided they are viableifisinstance(other,np.ndarray):ifother.shapein[(len(self),1),self.rgb.shape,self.rgba.shape]:buffer.rgba=self.rgba+other# If target is a Color object, add together the rgba valuesifisinstance(other,Color):iflen(self)==len(other):buffer.rgba=self.rgba+other.rgbareturnbufferdef__sub__(self,other):buffer=self.copy()# If target is a list or tuple, convert it to an arrayifisinstance(other,(list,tuple)):other=np.array(other)# If target is a single number, subtract it from each rgba valueifisinstance(other,(int,float)):buffer.rgba=self.rgba-other# If target is an array, subtract the arrays provided they are viableifisinstance(other,np.ndarray):ifother.shapein[(len(self),1),self.rgb.shape,self.rgba.shape]:buffer.rgba=self.rgba-other# If target is a Color object, add together the rgba valuesifisinstance(other,Color):iflen(self)==len(other):buffer.rgb=self.rgb-other.rgbreturnbuffer# --------------------------------------------------------------------------# Methods and properties#
[docs]defcopy(self):"""Return a duplicate of this colour"""returnself.__copy__()
[docs]defgetReadable(self,contrast=4.5/21):""" Get a color which will stand out and be easily readable against this one. Useful for choosing text colors based on background color. Parameters ---------- contrast : float Desired perceived contrast between the two colors, between 0 (the same color) and 1 (as opposite as possible). Default is the w3c recommended minimum of 4.5/21 (dividing by 21 to adjust for sRGB units). Returns ------- colors.Color A contrasting color to this color. """# adjust contrast to sRGBcontrast*=21# get value as rgb1rgb=self.rgb1# convert to srgbsrgb=rgb**2.2*[0.2126,0.7151,0.0721]# apply contrast adjustmentifnp.sum(srgb)<0.5:srgb=(srgb+0.05)*contrastelse:srgb=(srgb+0.05)/contrast# convert backrgb=(srgb/[0.2126,0.7151,0.0721])**(1/2.2)# caprgb=np.clip(rgb,0,1)# Return new colorreturnColor(rgb,"rgb1")
@propertydefalpha(self):"""How opaque (1) or transparent (0) this color is. Synonymous with `opacity`. """returnself._alpha@alpha.setterdefalpha(self,value):# Treat 1x1 arrays as a floatifisinstance(value,np.ndarray):ifvalue.size==1:value=float(value[0])else:try:value=float(value)# If coercible to float, do soexcept(TypeError,ValueError)aserr:raiseTypeError("Could not set alpha as value `{}` of type `{}`".format(value,type(value).__name__))value=np.clip(value,0,1)# Clip value(s) to within range# Set valueself._alpha=value# Clear render cacheself._renderCache={}@propertydefcontrast(self):ifhasattr(self,"_contrast"):returnself._contrast@contrast.setterdefcontrast(self,value):# Set valueself._contrast=value# Clear render cacheself._renderCache={}@propertydefopacity(self):"""How opaque (1) or transparent (0) this color is (`float`). Synonymous with `alpha`. """returnself.alpha@opacity.setterdefopacity(self,value):self.alpha=valuedef_appendAlpha(self,space):# Get alpha, if necessary transform to an array of same length as coloralpha=self.alphaifisinstance(alpha,(int,float)):iflen(self)>1:alpha=np.tile([alpha],(len(self),1))else:alpha=np.array([alpha])ifisinstance(alpha,np.ndarray)andlen(self)>1:alpha=alpha.reshape((len(self),1))# Get colorcolor=getattr(self,space)# Append alpha to colorreturnnp.append(color,alpha,axis=1ifcolor.ndim>1else0)#---spaces---# Lingua franca is rgb@propertydefrgba(self):"""Color value expressed as an RGB triplet from -1 to 1, with alpha values (0 to 1). """returnself._appendAlpha('rgb')@rgba.setterdefrgba(self,color):self.rgb=color@propertydefrgb(self):"""Color value expressed as an RGB triplet from -1 to 1. """ifnotself.valid:returnifhasattr(self,'_franca'):rgb=self._francareturnrgbelse:returnnp.array([0,0,0])@rgb.setterdefrgb(self,color):# Validatecolor,space=self.validate(color,space='rgb')ifspace!='rgb':setattr(self,space,color)return# Set colorself._franca=color# Clear outdated values from cacheself._cache={'rgb':color}self._renderCache={}@propertydefrgba255(self):"""Color value expressed as an RGB triplet from 0 to 255, with alpha value (0 to 1). """returnself._appendAlpha('rgb255')@rgba255.setterdefrgba255(self,color):self.rgb255=color@propertydefrgb255(self):"""Color value expressed as an RGB triplet from 0 to 255. """ifnotself.valid:return# Recalculate if not cachedif'rgb255'notinself._cache:self._cache['rgb255']=np.round(255*(self.rgb+1)/2)returnself._cache['rgb255']@rgb255.setterdefrgb255(self,color):# Validatecolor,space=self.validate(color,space='rgb255')ifspace!='rgb255':setattr(self,space,color)return# Iterate through values and do conversionself.rgb=2*(color/255-0.5)# Clear outdated values from cacheself._cache={'rgb255':color}self._renderCache={}@propertydefrgba1(self):"""Color value expressed as an RGB triplet from 0 to 1, with alpha value (0 to 1). """returnself._appendAlpha('rgb1')@rgba1.setterdefrgba1(self,color):self.rgb1=color@propertydefrgb1(self):"""Color value expressed as an RGB triplet from 0 to 1. """ifnotself.valid:return# Recalculate if not cachedif'rgb1'notinself._cache:self._cache['rgb1']=(self.rgb+1)/2returnself._cache['rgb1']@rgb1.setterdefrgb1(self,color):# Validatecolor,space=self.validate(color,space='rgb1')ifspace!='rgb1':setattr(self,space,color)return# Iterate through values and do conversionself.rgb=2*(color-0.5)# Clear outdated values from cacheself._cache={'rgb1':color}self._renderCache={}@propertydefhex(self):"""Color value expressed as a hex string. Can be a '#' followed by 6 values from 0 to F (e.g. #F2545B). """ifnotself.valid:returnif'hex'notinself._cache:# Map rgb255 values to corresponding letters in hexhexmap={10:'a',11:'b',12:'c',13:'d',14:'e',15:'f'}# Handle arraysifself.rgb255.ndim>1:rgb255=self.rgb255# Iterate through rows of rgb255self._cache['hex']=np.array([])forrowinrgb255:rowHex='#'# Convert each value to hex and appendforvalinrow:dig=hex(int(val)).strip('0x')rowHex+=digiflen(dig)==2else'0'+dig# Append full hex value to new arrayself._cache['hex']=np.append(self._cache['hex'],[rowHex],0)else:rowHex='#'# Convert each value to hex and appendforvalinself.rgb255:dig=hex(int(val))[2:]rowHex+=digiflen(dig)==2else'0'+dig# Append full hex value to new arrayself._cache['hex']=rowHexreturnself._cache['hex']@hex.setterdefhex(self,color):# Validatecolor,space=self.validate(color,space='hex')ifspace!='hex':setattr(self,space,color)returniflen(color)>1:# Handle arraysrgb255=np.array([""])forrowincolor:ifisinstance(row,np.ndarray):row=row[0]row=row.strip('#')# Convert string to list of stringshexList=[row[:2],row[2:4],row[4:6]]# Convert strings to inthexInt=[int(val,16)forvalinhexList]# Convert to array and appendrgb255=np.append(rgb255,np.array(hexInt),0)else:# Handle single valuesifisinstance(color,np.ndarray):# Strip away any extraneous numpy layerscolor=color[(0,)*color.ndim]color=color.strip('#')# Convert string to list of stringshexList=[color[:2],color[2:4],color[4:6]]# Convert strings to inthexInt=[int(val,16)forvalinhexList]# Convert to arrayrgb255=np.array(hexInt)# Set rgb255 accordinglyself.rgb255=rgb255# Clear outdated values from cacheself._cache={'hex':color}self._renderCache={}@propertydefnamed(self):"""The name of this color, if it has one (`str`). """if'named'notinself._cache:self._cache['named']=None# If alpha is 0, then we know that the color is Noneifisinstance(self.alpha,np.ndarray):invis=all(self.alpha==0)elifisinstance(self.alpha,(int,float)):invis=self.alpha==0else:invis=Falseifinvis:self._cache['named']='none'returnself._cache['named']self._cache['named']=np.array([])# Handle arrayiflen(self)>1:forrowinself.rgb:forname,valincolorNames.items():ifall(val[:3]==row):self._cache['named']=np.append(self._cache['named'],[name],0)continueself._cache['named']=np.reshape(self._cache['named'],(-1,1))else:rgb=self.rgbforname,valincolorNames.items():ifname=='none':# skip Nonecontinueifall(val[:3]==rgb):self._cache['named']=namecontinuereturnself._cache['named']@named.setterdefnamed(self,color):# Validatecolor,space=self.validate(color=color,space='named')ifspace!='named':setattr(self,space,color)return# Retrieve named colouriflen(color)>1:# Handle arraysforrowincolor:row=str(np.reshape(row,()))# Enforce strifstr(row).lower()incolorNames:self.rgb=colorNames[str(row).lower()]ifrow.lower()=='none':self.alpha=0else:color=str(np.reshape(color,()))# Enforce strifcolor.lower()incolorNames:self.rgb=colorNames[str(color).lower()]ifcolor.lower()=='none':self.alpha=0# Clear outdated values from cacheself._cache={'named':color}self._renderCache={}@propertydefhsva(self):"""Color value expressed as an HSV triplet, with alpha value (0 to 1). """returnself._appendAlpha('hsv')@hsva.setterdefhsva(self,color):self.hsv=color@propertydefhsv(self):"""Color value expressed as an HSV triplet. """if'hsva'notinself._cache:self._cache['hsv']=ct.rgb2hsv(self.rgb)returnself._cache['hsv']@hsv.setterdefhsv(self,color):# Validatecolor,space=self.validate(color=color,space='hsv')ifspace!='hsv':setattr(self,space,color)return# Apply via rgba255self.rgb=ct.hsv2rgb(color)# Clear outdated values from cacheself._cache={'hsv':color}self._renderCache={}@propertydeflmsa(self):"""Color value expressed as an LMS triplet, with alpha value (0 to 1). """returnself._appendAlpha('lms')@lmsa.setterdeflmsa(self,color):self.lms=color@propertydeflms(self):"""Color value expressed as an LMS triplet. """if'lms'notinself._cache:self._cache['lms']=ct.rgb2lms(self.rgb)returnself._cache['lms']@lms.setterdeflms(self,color):# Validatecolor,space=self.validate(color=color,space='lms')ifspace!='lms':setattr(self,space,color)return# Apply via rgba255self.rgb=ct.lms2rgb(color,self.conematrix)# Clear outdated values from cacheself._cache={'lms':color}self._renderCache={}@propertydefdkla(self):"""Color value expressed as a DKL triplet, with alpha value (0 to 1). """returnself._appendAlpha('dkl')@dkla.setterdefdkla(self,color):self.dkl=color@propertydefdkl(self):"""Color value expressed as a DKL triplet. """if'dkl'notinself._cache:raiseNotImplementedError("Conversion from rgb to dkl is not yet implemented.")returnself._cache['dkl']@dkl.setterdefdkl(self,color):# Validatecolor,space=self.validate(color=color,space='dkl')ifspace!='dkl':setattr(self,space,color)return# Apply via rgba255self.rgb=ct.dkl2rgb(color,self.conematrix)# Clear outdated values from cacheself._cache={'dkl':color}self._renderCache={}@propertydefdklaCart(self):"""Color value expressed as a cartesian DKL triplet, with alpha value (0 to 1). """returnself.dklCart@dklaCart.setterdefdklaCart(self,color):self.dklCart=color@propertydefdklCart(self):"""Color value expressed as a cartesian DKL triplet. """if'dklCart'notinself._cache:self._cache['dklCart']=ct.rgb2dklCart(self.rgb)returnself._cache['dklCart']@dklCart.setterdefdklCart(self,color):# Validatecolor,space=self.validate(color=color,space='dklCart')ifspace!='dkl':setattr(self,space,color)return# Apply via rgba255self.rgb=ct.dklCart2rgb(color,self.conematrix)# Clear outdated values from cacheself._cache={'dklCart':color}self._renderCache={}@propertydefsrgb(self):""" Color value expressed as an sRGB triplet """if'srgb'notinself._cache:self._cache['srgb']=ct.srgbTF(self.rgb)returnself._cache['srgb']@srgb.setterdefsrgb(self,color):# Validatecolor,space=self.validate(color=color,space='srgb')ifspace!='srgb':setattr(self,space,color)return# Apply via rgba255self.rgb=ct.srgbTF(color,reverse=True)# Clear outdated values from cacheself._cache={'srgb':color}self._renderCache={}
# removing for now# @property# def rec709TF(self):# if 'rec709TF' not in self._cache:# self._cache['rec709TF'] = ct.rec709TF(self.rgb)# return self._cache['rec709TF']## @rec709TF.setter# def rec709TF(self, color):# # Validate# color, space = self.validate(color=color, space='rec709TF')# if space != 'rec709TF':# setattr(self, space, color)# return# # Apply via rgba255# self.rgb = ct.rec709TF(color, reverse=True)# # Clear outdated values from cache# self._cache = {'rec709TF': color}# self._renderCache = {}# ------------------------------------------------------------------------------# Legacy functions## Old reference tablescolors=colorNames# colorsHex = {key: Color(key, 'named').hex for key in colors}# colors255 = {key: Color(key, 'named').rgb255 for key in colors}# Old conversion functions
[docs]defhex2rgb255(hexColor):"""Depreciated as of 2021.0 Converts a hex color string (e.g. "#05ff66") into an rgb triplet ranging from 0:255 """col=Color(hexColor,'hex')iflen(hexColor.strip('#'))==6:returncol.rgb255eliflen(hexColor.strip('#'))==8:returncol.rgba255
[docs]defisValidColor(color,space='rgb'):"""Depreciated as of 2021.0 """logging.warning("DEPRECIATED: While psychopy.colors.isValidColor will still roughly ""work, you should use a Color object, allowing you to check its ""validity simply by converting it to a `bool` (e.g. `bool(myColor)` or ""`if myColor:`). If you use this function for colors in any space ""other than hex, named or rgb, please specify the color space.")try:buffer=Color(color,space)returnbool(buffer)except:returnFalse