Repository: psychopy/psychopy

Issue:
ElementArrayStim is ignoring contrast parameter
`visual.ElementArrayStim` is ignoring the `contrs` parameter, and it is not possible to set the contrast of the visual stimuli. 

Example:

```python
from numpy.random import rand
from psychopy.visual import Window, ElementArrayStim
from psychopy.event import waitKeys

win = Window()
stim = ElementArrayStim(win, sfs=3, sizes=.2, contrs=rand(100))
stim.draw()
win.flip()
waitKeys(keyList=["space"])
```

![image](https://user-images.githubusercontent.com/315810/119058652-ca008300-b99c-11eb-8d87-5a76d3913dbb.png)

Indeed, it looks like the code where `contrs` took effect was removed recently:

```
$ git diff -U10 2020.2.10 release -- psychopy/visual/elementarray.py
```
```diff

     def updateElementColors(self):
         """Create a new array of self._RGBAs based on self.rgbs.

         Not needed by the user (simple call setColors())

         For element arrays the self.rgbs values correspond to one
         element so this function also converts them to be one for
         each vertex of each element.
         """
         N = self.nElements
-        self._RGBAs = numpy.zeros([N, 4], 'd')
-        if self.colorSpace in ('rgb', 'dkl', 'lms', 'hsv'):
-            # these spaces are 0-centred
-            self._RGBAs[:, 0:3] = (self.rgbs[:, :] *
-                self.contrs[:].reshape([N, 1]).repeat(3, 1) / 2 + 0.5)
-        else:
-            self._RGBAs[:, 0:3] = (self.rgbs *
-                self.contrs[:].reshape([N, 1]).repeat(3, 1) / 255.0)
-
-        self._RGBAs[:, -1] = self.opacities.reshape([N, ])
-        # repeat for the 4 vertices in the grid
-        self._RGBAs = self._RGBAs.reshape([N, 1, 4]).repeat(4, 1)
-
+        _RGBAs = numpy.zeros([len(self.verticesPix), 4], 'd')
+        _RGBAs[:,:] = self._colors.render('rgba1')
+        _RGBAs[:, -1] = self.opacities.reshape([N, ])
+        self._RGBAs = _RGBAs.reshape([len(self.verticesPix), 1, 4]).repeat(4, 1)
         self._needColorUpdate = False
```

If you search around that file, I don't think `self.contrs`  gets used anywhere, it is just set and then ignored.

It looks like maybe this is a mistake too, since `ElementArrayStim` does not have a `contrast` property:

```diff
+    @colors.setter
+    def colors(self, value):
+        # Create blank array of colors
+        self._colors = Color(value, self.colorSpace, self.contrast)
+        self._needColorUpdate = True
```


Discussion:
Ah I see the problem - `self._colors.render('rgba1')` applies the value of `self._colors.contrast` to the colours, but the value of `self._colors.contrast` and `self.contrs` aren't linked. Will put in a PR with the necessary changes :) In the meantime, you can get this example to work by adding this line after the stim is created:
```
stim._colors.contrast = numpy.hstack([rand(100)]*3)
```
(and importing numpy)

Resolving patch:
diff --git a/psychopy/colors.py b/psychopy/colors.py
--- a/psychopy/colors.py
+++ b/psychopy/colors.py
@@ -348,7 +348,12 @@ def render(self, space='rgb'):
         """
         if space not in colorSpaces:
             raise ValueError(f"{space} is not a valid color space")
-        adj = np.clip(self.rgb * self.contrast, -1, 1)
+        # Transform contrast to match rgb
+        contrast = self.contrast
+        contrast = np.reshape(contrast, (-1, 1))
+        contrast = np.hstack((contrast, contrast, contrast))
+        # Multiply
+        adj = np.clip(self.rgb * contrast, -1, 1)
         buffer = self.copy()
         buffer.rgb = adj
         return getattr(buffer, space)
diff --git a/psychopy/visual/elementarray.py b/psychopy/visual/elementarray.py
--- a/psychopy/visual/elementarray.py
+++ b/psychopy/visual/elementarray.py
@@ -429,6 +429,7 @@ def setColors(self, colors, colorSpace=None, operation='', log=None):
     def opacity(self):
         if hasattr(self, "_opacity"):
             return self._opacity
+
     @opacity.setter
     def opacity(self, value):
         self._opacity = value
@@ -436,7 +437,6 @@ def opacity(self, value):
             # Set the alpha value of each color to be the desired opacity
             self._colors.alpha = value
 
-
     @attributeSetter
     def contrs(self, value):
         """The contrasts of the elements, ranging -1 to +1. Should either be:
@@ -446,7 +446,14 @@ def contrs(self, value):
 
         :ref:`Operations <attrib-operations>` are supported.
         """
-        self.__dict__['contrs'] = self._makeNx1(value)
+        # Convert to an Nx1 numpy array
+        value = self._makeNx1(value)
+        # If colors is too short, extend it
+        self._colors.rgb = numpy.resize(self._colors.rgb, (len(value), 3))
+        # Set
+        self._colors.contrast = value
+        # Store value and update
+        self.__dict__['contrs'] = value
         self._needColorUpdate = True
 
     def setContrs(self, value, operation='', log=None):


Test patch: