Repository: ManimCommunity/manim
Pull request: Improved setting of OpenGL colors
<!-- Thank you for contributing to Manim! Learn more about the process in our contributing guidelines: https://docs.manim.community/en/latest/contributing.html -->

## Overview: What does this pull request change?
<!-- If there is more information than the PR title that should be added to our release changelog, add it in the following changelog section. This is optional, but recommended for larger pull requests. -->
<!--changelog-start-->

<!--changelog-end-->

## Motivation and Explanation: Why and how do your changes improve the library?
<!-- Optional for bugfixes, small enhancements, and documentation-related PRs. Otherwise, please give a short reasoning for your changes. -->

Fixes #2087

In migrating tests from cairo to opengl some tests were failing due to `fill_color`, `stroke_color` and their respective opacities not being set. The background_color was also always being set from config, so I made it a property to make it easier to change without changing the config. 

```py
class Test(Scene):
    def construct(self):
        self.wait()
        self.renderer.background_color = YELLOW
        self.wait()
        self.renderer.background_color = GREEN
        self.wait()
```

After these changes I could run the tests for color with opengl and added some new ones for fill and stroke

https://user-images.githubusercontent.com/32387857/135334636-381e4325-b83d-4c14-8b53-0d684da4b02b.mp4

This also makes it more consistent with cairo with the exception of `background-opacity`. I couldn't set background opacity for opengl, and from what I can tell it isn't trivial to do.



## Links to added or changed documentation pages
<!-- Please add links to the affected documentation pages (edit the description after opening the PR). The link to the documentation for your PR is https://manimce--####.org.readthedocs.build/en/####/, where #### represents the PR number. -->


## Further Information and Comments
<!-- If applicable, put further comments for the reviewers here. -->



<!-- Thank you again for contributing! Do not modify the lines below, they are for reviewers. -->
## Reviewer Checklist
- [x] The PR title is descriptive enough for the changelog, and the PR is labeled correctly
- [ ] If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
- [ ] If applicable: newly added functions and classes are tested


Problem statement:
Setting colors and opacities in OpenGLVMobject doesn't change attributes
## Description of bug / unexpected behavior
<!-- Add a clear and concise description of the problem you encountered. -->

Calling `set_color`, `set_fill` and `set_stroke` on `OpenGLVMobject` doesn't change attributes like `self.opacity`, `self.fill_color` etc. It might not have been noticed because it does set rgbas which is used by the shaders, however having these attributes and not ever setting them is confusing and could cause other issues if some logic is trying to utilize them.

All the below assertions fail

```py
class Tests(Scene):
    def construct(self):
        m = OpenGLVMobject()
        m.set_stroke(color=ORANGE, width=2, opacity=0.8)
        m.set_fill(color=ORANGE, opacity=0.8)
        m.set_color(color=ORANGE)
        assert m.stroke_opacity == 0.8
        assert m.stroke_color == ORANGE
        assert m.fill_opacity == 0.8
        assert m.stroke_color == ORANGE
        assert m.color == ORANGE
```


## Expected behavior
<!-- Add a clear and concise description of what you expected to happen. -->

When passing colors and opacities, the rgbas should be set as it is, however the other corresponding color / opacity attributes should also be set.


## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->

Run above code with the opengl renderer and check the assertions, all the assertions fail.

<details><summary>Code for reproducing the problem</summary>

```py
class Tests(Scene):
    def construct(self):
        m = OpenGLVMobject()
        m.set_stroke(color=ORANGE, width=2, opacity=0.8)
        m.set_fill(color=ORANGE, opacity=0.8)
        m.set_color(color=ORANGE)
        assert m.stroke_opacity == 0.8
        assert m.stroke_color == ORANGE
        assert m.fill_opacity == 0.8
        assert m.stroke_color == ORANGE
        assert m.color == ORANGE
```

</details>


## Additional media files
<!-- Paste in the files manim produced on rendering the code above. -->

<details><summary>Images/GIFs</summary>

<!-- PASTE MEDIA HERE -->

</details>


## Logs
<details><summary>Terminal output</summary>
<!-- Add "-v DEBUG" when calling manim to generate more detailed logs -->

```
──────────────────────────────────────────────────────────────────────────────╮
│ File                                                                         │
│"/Users/ryan/PycharmProjects/manim-community/manim/cli/render/commands.py",   │
│line 108, in render                                                           │
│    105             while keep_running:                                       │
│    106                 for SceneClass in scene_classes_from_file(file):      │
│    107                     scene = SceneClass(renderer)                      │
│  ❱ 108                     rerun = scene.render()                            │
│    109                     if rerun or config["write_all"]:                  │
│    110                         renderer.num_plays = 0                        │
│    111                         continue                                      │
│ File "/Users/ryan/PycharmProjects/manim-community/manim/scene/scene.py", line│
│215, in render                                                                │
│     212         """                                                          │
│     213         self.setup()                                                 │
│     214         try:                                                         │
│  ❱  215             self.construct()                                         │
│     216         except EndSceneEarlyException:                               │
│     217             pass                                                     │
│     218         except RerunSceneException as e:                             │
│ File "../scenes/playground.py", line 74, in construct                        │
│     71         m.set_stroke(color=ORANGE, width=2, opacity=0.8)              │
│     72         m.set_fill(color=ORANGE, opacity=0.8)                         │
│     73         m.set_color(color=ORANGE)                                     │
│  ❱  74         assert m.stroke_opacity == 0.8                                │
│     75         assert m.stroke_color == ORANGE                               │
│     76         assert m.fill_opacity == 0.8                                  │
│     77         assert m.stroke_color == ORANGE                               │
╰──────────────────────────────────────────────────────────────────────────────╯
AssertionError: 
```

<!-- Insert screenshots here (only when absolutely necessary, we prefer copy/pasted output!) -->

</details>


## System specifications

<details><summary>System Details</summary>

- OS (with version, e.g Windows 10 v2004 or macOS 10.15 (Catalina)):
- RAM:
- Python version (`python/py/python3 --version`):
- Installed modules (provide output from `pip list`):
```
PASTE HERE
```
</details>

<details><summary>LaTeX details</summary>

+ LaTeX distribution (e.g. TeX Live 2020):
+ Installed LaTeX packages:
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
</details>

<details><summary>FFMPEG</summary>

Output of `ffmpeg -version`:

```
PASTE HERE
```
</details>

## Additional comments
<!-- Add further context that you think might be relevant for this issue here. -->

Hints from issue discussion:

Commit message: Improve setting of opengl colors

Patch to review:
diff --git a/manim/mobject/opengl_mobject.py b/manim/mobject/opengl_mobject.py
index e981e051e2..ae127a7544 100644
--- a/manim/mobject/opengl_mobject.py
+++ b/manim/mobject/opengl_mobject.py
@@ -1314,6 +1314,10 @@ def set_color(self, color, opacity=None, recurse=True):
         self.set_rgba_array(color, opacity, recurse=False)
         # Recurse to submobjects differently from how set_rgba_array
         # in case they implement set_color differently
+        if color is not None:
+            self.color = Color(color)
+        if opacity is not None:
+            self.opacity = opacity
         if recurse:
             for submob in self.submobjects:
                 submob.set_color(color, recurse=True)
diff --git a/manim/mobject/types/opengl_vectorized_mobject.py b/manim/mobject/types/opengl_vectorized_mobject.py
index 75b07f21f8..da3cc348c3 100644
--- a/manim/mobject/types/opengl_vectorized_mobject.py
+++ b/manim/mobject/types/opengl_vectorized_mobject.py
@@ -5,6 +5,7 @@
 
 import moderngl
 import numpy as np
+from colour import Color
 
 from ... import config
 from ...constants import *
@@ -152,6 +153,14 @@ def init_colors(self):
         return self
 
     def set_fill(self, color=None, opacity=None, recurse=True):
+        if color is not None:
+            self.fill_color = Color(color)
+        if opacity is not None:
+            self.fill_opacity = opacity
+        if recurse:
+            for submobject in self.submobjects:
+                submobject.set_fill(color, opacity, recurse)
+
         self.set_rgba_array(color, opacity, "fill_rgba", recurse)
         return self
 
@@ -163,6 +172,14 @@ def set_stroke(
         background=None,
         recurse=True,
     ):
+        if color is not None:
+            self.stroke_color = Color(color)
+        if opacity is not None:
+            self.stroke_opacity = opacity
+        if recurse:
+            for submobject in self.submobjects:
+                submobject.set_stroke(color, width, opacity, background, recurse)
+
         self.set_rgba_array(color, opacity, "stroke_rgba", recurse)
 
         if width is not None:
@@ -235,9 +252,12 @@ def match_style(self, vmobject, recurse=True):
                 sm1.match_style(sm2)
         return self
 
-    def set_color(self, color, recurse=True):
-        self.set_fill(color, recurse=recurse)
-        self.set_stroke(color, recurse=recurse)
+    def set_color(self, color, opacity=None, recurse=True):
+        self.color = Color(color)
+        if opacity is not None:
+            self.opacity = opacity
+        self.set_fill(color, opacity=opacity, recurse=recurse)
+        self.set_stroke(color, opacity=opacity, recurse=recurse)
         return self
 
     def set_opacity(self, opacity, recurse=True):
diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py
index 24bb90c62a..810adaba37 100644
--- a/manim/renderer/opengl_renderer.py
+++ b/manim/renderer/opengl_renderer.py
@@ -234,6 +234,8 @@ def __init__(self, file_writer_class=SceneFileWriter, skip_animations=False):
         # Initialize texture map.
         self.path_to_texture_id = {}
 
+        self._background_color = color_to_rgba(config["background_color"], 1.0)
+
     def init_scene(self, scene):
         self.partial_movie_files = []
         self.file_writer = self._file_writer_class(
@@ -405,8 +407,7 @@ def play(self, scene, *args, **kwargs):
             scene.play_internal()
 
     def clear_screen(self):
-        window_background_color = color_to_rgba(config["background_color"])
-        self.frame_buffer_object.clear(*window_background_color)
+        self.frame_buffer_object.clear(*self.background_color)
         self.window.swap_buffers()
 
     def render(self, scene, frame_offset, moving_mobjects):
@@ -424,8 +425,7 @@ def render(self, scene, frame_offset, moving_mobjects):
                 self.window.swap_buffers()
 
     def update_frame(self, scene):
-        window_background_color = color_to_rgba(config["background_color"])
-        self.frame_buffer_object.clear(*window_background_color)
+        self.frame_buffer_object.clear(*self.background_color)
         self.refresh_perspective_uniforms(scene.camera)
 
         for mobject in scene.mobjects:
@@ -530,3 +530,11 @@ def pixel_coords_to_space_coords(self, px, py, relative=False):
             # Only scale wrt one axis
             scale = fh / ph
             return fc + scale * np.array([(px - pw / 2), (py - ph / 2), 0])
+
+    @property
+    def background_color(self):
+        return self._background_color
+
+    @background_color.setter
+    def background_color(self, value):
+        self._background_color = color_to_rgba(value, 1.0)
diff --git a/tests/test_color_opengl.py b/tests/test_color_opengl.py
new file mode 100644
index 0000000000..470cc502f3
--- /dev/null
+++ b/tests/test_color_opengl.py
@@ -0,0 +1,95 @@
+import numpy as np
+
+from manim import BLACK, PURE_GREEN, Scene
+from manim.mobject.opengl_mobject import OpenGLMobject
+from manim.mobject.types.opengl_vectorized_mobject import OpenGLVMobject
+
+
+def test_import_color(using_opengl_renderer):
+    import manim.utils.color as C
+
+    C.WHITE
+
+
+def test_background_color(using_opengl_renderer):
+    S = Scene()
+    S.renderer.background_color = "#ff0000"
+    S.renderer.update_frame(S)
+    assert np.all(S.renderer.get_frame()[0, 0] == np.array([255, 0, 0, 255]))
+
+    S.renderer.background_color = "#436f80"
+    S.renderer.update_frame(S)
+    assert np.all(S.renderer.get_frame()[0, 0] == np.array([67, 111, 128, 255]))
+
+    S.renderer.background_color = "#fff"
+    S.renderer.update_frame(S)
+    assert np.all(S.renderer.get_frame()[0, 0] == np.array([255, 255, 255, 255]))
+
+
+def test_set_color(using_opengl_renderer):
+    m = OpenGLMobject()
+    assert m.color.hex == "#fff"
+    np.alltrue(m.rgbas == np.array((0.0, 0.0, 0.0, 1.0)))
+
+    m.set_color(BLACK)
+    assert m.color.hex == "#000"
+    np.alltrue(m.rgbas == np.array((1.0, 1.0, 1.0, 1.0)))
+
+    m.set_color(PURE_GREEN, opacity=0.5)
+    assert m.color.hex == "#0f0"
+    np.alltrue(m.rgbas == np.array((0.0, 1.0, 0.0, 0.5)))
+
+    m = OpenGLVMobject()
+    assert m.color.hex == "#fff"
+    np.alltrue(m.fill_rgba == np.array((0.0, 0.0, 0.0, 1.0)))
+    np.alltrue(m.stroke_rgba == np.array((0.0, 0.0, 0.0, 1.0)))
+
+    m.set_color(BLACK)
+    assert m.color.hex == "#000"
+    np.alltrue(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0)))
+    np.alltrue(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0)))
+
+    m.set_color(PURE_GREEN, opacity=0.5)
+    assert m.color.hex == "#0f0"
+    np.alltrue(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5)))
+    np.alltrue(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5)))
+
+
+def test_set_fill_color(using_opengl_renderer):
+    m = OpenGLVMobject()
+    assert m.fill_color.hex == "#fff"
+    np.alltrue(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5)))
+
+    m.set_fill(BLACK)
+    assert m.fill_color.hex == "#000"
+    np.alltrue(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0)))
+
+    m.set_fill(PURE_GREEN, opacity=0.5)
+    assert m.fill_color.hex == "#0f0"
+    np.alltrue(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5)))
+
+
+def test_set_stroke_color(using_opengl_renderer):
+    m = OpenGLVMobject()
+    assert m.stroke_color.hex == "#fff"
+    np.alltrue(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5)))
+
+    m.set_stroke(BLACK)
+    assert m.stroke_color.hex == "#000"
+    np.alltrue(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0)))
+
+    m.set_stroke(PURE_GREEN, opacity=0.5)
+    assert m.stroke_color.hex == "#0f0"
+    np.alltrue(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5)))
+
+
+def test_set_fill(using_opengl_renderer):
+    m = OpenGLMobject()
+    assert m.color.hex == "#fff"
+    m.set_color(BLACK)
+    assert m.color.hex == "#000"
+
+    m = OpenGLVMobject()
+    assert m.color.hex == "#fff"
+    m.set_color(BLACK)
+    assert m.color.hex == "#000"


Code review comments:
diff_hunk: @@ -163,6 +172,14 @@ def set_stroke(
         background=None,
         recurse=True,
     ):
+        if color is not None:
+            self.stroke_color = Color(color)
+        if opacity is not None:
+            self.stroke_opacity = opacity
+        if recurse:
+            for submobject in self.submobjects:
+                submobject.set_stroke(color, width, opacity, background, recurse)
path: manim/mobject/types/opengl_vectorized_mobject.py
text: @user1:
Can you please pass these as keyword arguments?

@author:
> Can you please pass these as keyword arguments?

Done and I forgot to move the test file into the opengl directory so did that too