Skip to content

Attempt to implement a theming concept #2015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from
Closed
5 changes: 4 additions & 1 deletion manim/_config/default.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ output_file =
log_to_file = False

# -c, --background_color
background_color = BLACK
background_color = None

# theme
theme = dark_mode

# --background_opacity
background_opacity = 1
Expand Down
76 changes: 75 additions & 1 deletion manim/_config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,43 @@ class MyScene(Scene):
the background color will be set to RED, regardless of the contents of
``manim.cfg`` or the CLI arguments used when invoking manim.

Themes
------
There are options to set manim to follow a specific color scheme. You need to
make a ``.cfg`` file with which contains the lines:

.. code-block:: cfg

[CLI]

theme = light_mode

The options of the themes include:

* dark_mode (default)
* light_mode (white background and black mobjects)
* sepia (brown-esque)
* seagreen

.. important::
The theme cannot be stated from within your manim script.

You could also pass your own theme:

.. code-block:: cfg

[CLI]

theme = {"mobject_color": "#000", "background_color": "#555"}

.. note::
The theme changes the definition of some colors, namely the greyscale
family. ``WHITE`` will refer to the newly set "mobject_color" and
``BLACK`` will refer to the new "background_color". Every grey in between
(``GREY_A``, ``GREY_B`` and so on) will be a gradient between the two
``WHITE`` and ``BLACK``. If you wish to refer to the pure colors,
you may pass their hex codes instead.

"""

_OPTS = {
Expand Down Expand Up @@ -279,6 +316,7 @@ class MyScene(Scene):
"tex_dir",
"tex_template_file",
"text_dir",
"theme",
"upto_animation_number",
"renderer",
"use_opengl_renderer",
Expand Down Expand Up @@ -569,6 +607,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> "ManimConfig":
"input_file",
"output_file",
"movie_file_extension",
"theme",
"background_color",
"renderer",
"webgl_renderer_path",
Expand Down Expand Up @@ -679,6 +718,7 @@ def digest_args(self, args: argparse.Namespace) -> "ManimConfig":
"scene_names",
"verbosity",
"renderer",
"theme",
"background_color",
"use_opengl_renderer",
"use_webgl_renderer",
Expand Down Expand Up @@ -1006,10 +1046,44 @@ def format(self, val: str) -> None:

background_color = property(
lambda self: self._d["background_color"],
lambda self, val: self._d.__setitem__("background_color", colour.Color(val)),
doc="Background color of the scene (-c).",
)

@background_color.setter
def background_color(self, val):
if val != "None":
self._d.__setitem__("background_color", val)
else:
pass

theme = property(
lambda self: self._d["theme"],
doc="Color theme of the scene (-c).",
)

@theme.setter
def theme(
self,
th: typing.Union[str, typing.Dict[str, str]],
) -> None:
if type(th) == str:
if th.startswith("{"): # a custom dictionary
try:
t = eval(th)
except:
raise
th = constants.THEMES["dark_mode"]
th.update(t)
else: # predefined theme
if th not in constants.THEMES:
raise KeyError(
f"themes must be one of {list(constants.THEMES.keys())}",
)
th = constants.THEMES[th]
self._d["theme"] = th
self.mobject_color = th["mobject_color"]
self.background_color = th["background_color"]

from_animation_number = property(
lambda self: self._d["from_animation_number"],
lambda self, val: self._d.__setitem__("from_animation_number", val),
Expand Down
19 changes: 19 additions & 0 deletions manim/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,22 @@
CONTEXT_SETTINGS = {"help_option_names": HELP_OPTIONS}
SHIFT_VALUE = 65505
CTRL_VALUE = 65507

THEMES: typing.Dict[str, typing.Dict[str, str]] = {
"dark_mode": {
"mobject_color": "#FFFFFF",
"background_color": "#000000",
},
"light_mode": {
"mobject_color": "#000000",
"background_color": "#FFFFFF",
},
"sepia": {
"mobject_color": "#BCAC80",
"background_color": "#704214",
},
"seagreen": {
"mobject_color": "#FFFFFF",
"background_color": "#0F7D63",
},
}
221 changes: 113 additions & 108 deletions manim/utils/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,113 @@
import numpy as np
from colour import Color

from .. import config, constants
from ..utils.bezier import interpolate
from ..utils.simple_functions import clip_in_place
from ..utils.space_ops import normalize


def color_to_rgb(color: Union[Color, str]) -> np.ndarray:
if isinstance(color, str):
return hex_to_rgb(color)
elif isinstance(color, Color):
return np.array(color.get_rgb())
else:
raise ValueError("Invalid color type: " + str(color))


def color_to_rgba(color: Union[Color, str], alpha: float = 1) -> np.ndarray:
return np.array([*color_to_rgb(color), alpha])


def rgb_to_color(rgb: Iterable[float]) -> Color:
return Color(rgb=rgb)


def rgba_to_color(rgba: Iterable[float]) -> Color:
return rgb_to_color(rgba[:3])


def rgb_to_hex(rgb: Iterable[float]) -> str:
return "#" + "".join("%02x" % int(255 * x) for x in rgb)


def hex_to_rgb(hex_code: str) -> np.ndarray:
hex_part = hex_code[1:]
if len(hex_part) == 3:
hex_part = "".join([2 * c for c in hex_part])
return np.array([int(hex_part[i : i + 2], 16) / 255 for i in range(0, 6, 2)])


def invert_color(color: Color) -> Color:
return rgb_to_color(1.0 - color_to_rgb(color))


def color_to_int_rgb(color: Color) -> np.ndarray:
return (255 * color_to_rgb(color)).astype("uint8")


def color_to_int_rgba(color: Color, opacity: float = 1.0) -> np.ndarray:
alpha = int(255 * opacity)
return np.append(color_to_int_rgb(color), alpha)


def color_gradient(
reference_colors: Iterable[Color],
length_of_output: int,
) -> List[Color]:
if length_of_output == 0:
return reference_colors[0]
rgbs = list(map(color_to_rgb, reference_colors))
alphas = np.linspace(0, (len(rgbs) - 1), length_of_output)
floors = alphas.astype("int")
alphas_mod1 = alphas % 1
# End edge case
alphas_mod1[-1] = 1
floors[-1] = len(rgbs) - 2
return [
rgb_to_color(interpolate(rgbs[i], rgbs[i + 1], alpha))
for i, alpha in zip(floors, alphas_mod1)
]


def interpolate_color(color1: Color, color2: Color, alpha: float) -> Color:
rgb = interpolate(color_to_rgb(color1), color_to_rgb(color2), alpha)
return rgb_to_color(rgb)


def average_color(*colors: Color) -> Color:
rgbs = np.array(list(map(color_to_rgb, colors)))
mean_rgb = np.apply_along_axis(np.mean, 0, rgbs)
return rgb_to_color(mean_rgb)


def random_bright_color() -> Color:
color = random_color()
curr_rgb = color_to_rgb(color)
new_rgb = interpolate(curr_rgb, np.ones(len(curr_rgb)), 0.5)
return Color(rgb=new_rgb)


def random_color() -> Color:
return random.choice([c.value for c in list(Colors)])


def get_shaded_rgb(
rgb: np.ndarray,
point: np.ndarray,
unit_normal_vect: np.ndarray,
light_source: np.ndarray,
) -> np.ndarray:
to_sun = normalize(light_source - point)
factor = 0.5 * np.dot(unit_normal_vect, to_sun) ** 3
if factor < 0:
factor *= 0.5
result = rgb + factor
clip_in_place(rgb + factor, 0, 1)
return result


class Colors(Enum):
"""A list of pre-defined colors.

Expand Down Expand Up @@ -178,13 +280,17 @@ def named_lines_group(length, colors, names, text_colors, align_to_block):

"""

white = "#FFFFFF"
gray_a = "#DDDDDD"
gray_b = "#BBBBBB"
gray_c = "#888888"
gray_d = "#444444"
gray_e = "#222222"
black = "#000000"
white = config["mobject_color"]
black = config["background_color"]

if [white, black] != ["#FFFFFF", "#000000"]:
gray_a, gray_b, gray_c, gray_d, gray_e = color_gradient([white, black], 7)[1:6]
else:
gray_a = "#DDDDDD"
gray_b = "#BBBBBB"
gray_c = "#888888"
gray_d = "#444444"
gray_e = "#222222"
lighter_gray = gray_a
light_gray = gray_b
gray = gray_c
Expand Down Expand Up @@ -361,104 +467,3 @@ def named_lines_group(length, colors, names, text_colors, align_to_block):
"GRAY_BROWN",
"GREY_BROWN",
]


def color_to_rgb(color: Union[Color, str]) -> np.ndarray:
if isinstance(color, str):
return hex_to_rgb(color)
elif isinstance(color, Color):
return np.array(color.get_rgb())
else:
raise ValueError("Invalid color type: " + str(color))


def color_to_rgba(color: Union[Color, str], alpha: float = 1) -> np.ndarray:
return np.array([*color_to_rgb(color), alpha])


def rgb_to_color(rgb: Iterable[float]) -> Color:
return Color(rgb=rgb)


def rgba_to_color(rgba: Iterable[float]) -> Color:
return rgb_to_color(rgba[:3])


def rgb_to_hex(rgb: Iterable[float]) -> str:
return "#" + "".join("%02x" % int(255 * x) for x in rgb)


def hex_to_rgb(hex_code: str) -> np.ndarray:
hex_part = hex_code[1:]
if len(hex_part) == 3:
hex_part = "".join([2 * c for c in hex_part])
return np.array([int(hex_part[i : i + 2], 16) / 255 for i in range(0, 6, 2)])


def invert_color(color: Color) -> Color:
return rgb_to_color(1.0 - color_to_rgb(color))


def color_to_int_rgb(color: Color) -> np.ndarray:
return (255 * color_to_rgb(color)).astype("uint8")


def color_to_int_rgba(color: Color, opacity: float = 1.0) -> np.ndarray:
alpha = int(255 * opacity)
return np.append(color_to_int_rgb(color), alpha)


def color_gradient(
reference_colors: Iterable[Color],
length_of_output: int,
) -> List[Color]:
if length_of_output == 0:
return reference_colors[0]
rgbs = list(map(color_to_rgb, reference_colors))
alphas = np.linspace(0, (len(rgbs) - 1), length_of_output)
floors = alphas.astype("int")
alphas_mod1 = alphas % 1
# End edge case
alphas_mod1[-1] = 1
floors[-1] = len(rgbs) - 2
return [
rgb_to_color(interpolate(rgbs[i], rgbs[i + 1], alpha))
for i, alpha in zip(floors, alphas_mod1)
]


def interpolate_color(color1: Color, color2: Color, alpha: float) -> Color:
rgb = interpolate(color_to_rgb(color1), color_to_rgb(color2), alpha)
return rgb_to_color(rgb)


def average_color(*colors: Color) -> Color:
rgbs = np.array(list(map(color_to_rgb, colors)))
mean_rgb = np.apply_along_axis(np.mean, 0, rgbs)
return rgb_to_color(mean_rgb)


def random_bright_color() -> Color:
color = random_color()
curr_rgb = color_to_rgb(color)
new_rgb = interpolate(curr_rgb, np.ones(len(curr_rgb)), 0.5)
return Color(rgb=new_rgb)


def random_color() -> Color:
return random.choice([c.value for c in list(Colors)])


def get_shaded_rgb(
rgb: np.ndarray,
point: np.ndarray,
unit_normal_vect: np.ndarray,
light_source: np.ndarray,
) -> np.ndarray:
to_sun = normalize(light_source - point)
factor = 0.5 * np.dot(unit_normal_vect, to_sun) ** 3
if factor < 0:
factor *= 0.5
result = rgb + factor
clip_in_place(rgb + factor, 0, 1)
return result
Loading