72 Commits

Author SHA1 Message Date
TonyCrane
1ec00629a5 release: ready to release v1.4.0 2022-01-30 13:06:22 +08:00
TonyCrane
aa6335cd90 docs: update changelog for #1719 #1720 #1721 and #1723 2022-01-30 13:00:57 +08:00
YishiMichael
7093f7d02d Some small refactors to MTex (#1723) 2022-01-30 12:42:35 +08:00
Grant Sanderson
fad9ed2df7 Merge pull request #1720 from YishiMichael/master
Handle explicit color-related commands
2022-01-29 08:01:48 -08:00
YishiMichael
725155409b Some small refactors 2022-01-29 21:06:54 +08:00
YishiMichael
a6675eb043 Some small refactors 2022-01-29 14:35:27 +08:00
YishiMichael
5d2dcec307 Fix color-related bugs 2022-01-29 14:05:52 +08:00
Grant Sanderson
f60dc7cd07 Merge pull request #1719 from 3b1b/parse-style
Parse style from <style> tag and Add support to <line> tag
2022-01-27 13:33:14 -08:00
YishiMichael
6c39cac62b Remove redundant attribute 2022-01-28 01:19:02 +08:00
鹤翔万里
2bd25a55fa add back override parameter to init_colors 2022-01-28 00:20:13 +08:00
鹤翔万里
0e4edfdd79 improve config helper (#1721) 2022-01-28 00:16:19 +08:00
YishiMichael
277256a407 Merge branch '3b1b:master' into master 2022-01-27 23:11:19 +08:00
YishiMichael
831b7d455c Handle explicit color-related commands 2022-01-27 23:09:05 +08:00
TonyCrane
1d14a23af9 docs: update changelog for #1712 #1717 and #1716 2022-01-27 22:21:40 +08:00
TonyCrane
dffa70ea15 docs: update changelog for #1704 and #1709 2022-01-27 22:09:57 +08:00
TonyCrane
31976063df add dependency cssselect2 2022-01-27 17:31:14 +08:00
TonyCrane
aa135280ac support <line> tag in SVG 2022-01-27 17:23:58 +08:00
TonyCrane
f0160822ba fix bug of ref map 2022-01-27 17:17:19 +08:00
TonyCrane
48e07d1817 parse style attribute using tinycss 2022-01-27 17:16:52 +08:00
TonyCrane
3ef5899a24 some cleanups 2022-01-27 16:43:45 +08:00
TonyCrane
f895455264 add parser for <style> tag of SVG 2022-01-27 16:37:51 +08:00
Grant Sanderson
3baa14103e Merge pull request #1716 from YishiMichael/master
Refactor MTex and some clean-ups
2022-01-26 08:56:44 -08:00
Grant Sanderson
c315300ff1 Merge branch 'master' into master 2022-01-26 08:54:18 -08:00
Grant Sanderson
3b17d6d0eb Merge pull request #1718 from 3b1b/text-fix
Text fix
2022-01-26 08:21:36 -08:00
Grant Sanderson
8a29de5ef0 Override style for Text 2022-01-26 08:21:00 -08:00
Grant Sanderson
ecb729850a Small style fixes 2022-01-26 08:20:45 -08:00
Grant Sanderson
a770291053 Include style in MTex.get_mobjects_from 2022-01-26 08:20:38 -08:00
Grant Sanderson
27c666fab5 Merge pull request #1717 from 3b1b/svg-style
Parse and generate style for SVG
2022-01-26 07:56:03 -08:00
YishiMichael
942a7e71b8 Update MTex 2022-01-26 23:46:13 +08:00
TonyCrane
ebb75d1235 cached SVGMobject in SingleStringTex with default color 2022-01-26 20:37:44 +08:00
TonyCrane
9af23415a2 synchronize SingleStringTex's color to SVGMobject 2022-01-26 20:20:48 +08:00
TonyCrane
19778e405a some cleanups 2022-01-26 19:55:47 +08:00
TonyCrane
833e40c2d4 fix default style 2022-01-26 19:50:27 +08:00
TonyCrane
9df53b8a18 fix the bug of M command with more than 2 args 2022-01-26 14:05:01 +08:00
TonyCrane
ff86b0e378 fix the bug of outdated relative_point after command Z 2022-01-26 13:56:42 +08:00
TonyCrane
92adcd75d4 add style support to svg 2022-01-26 13:53:53 +08:00
YishiMichael
240f5020b4 Add back default_config.yml 2022-01-26 13:21:27 +08:00
YishiMichael
e8205a5049 Some refactors for MTex 2022-01-26 13:03:14 +08:00
TonyCrane
6c8dd14adc some clean 2022-01-26 11:00:57 +08:00
Grant Sanderson
07f84e2676 Merge pull request #1712 from 3b1b/fix-svg
Improve handling of SVG transform and Some refactors
2022-01-25 13:26:40 -08:00
TonyCrane
8db1164ece some refactors 2022-01-25 21:48:04 +08:00
TonyCrane
790bf0a104 fix typo 2022-01-25 20:25:30 +08:00
TonyCrane
8205edcc4c fix a small bug 2022-01-25 20:13:20 +08:00
TonyCrane
05b3c9852e fix add_smooth_cubic_curve_to when have only one point 2022-01-25 20:06:00 +08:00
TonyCrane
925f2e123f add comments 2022-01-25 19:54:19 +08:00
TonyCrane
565763a2ff reconstruct path parser 2022-01-25 19:44:42 +08:00
TonyCrane
6a74c241b8 fix bug of node which is not an element 2022-01-25 16:28:23 +08:00
TonyCrane
416cc8e6d5 add warning for unsupported element type 2022-01-25 14:41:11 +08:00
TonyCrane
d694aed452 add support for skewX and skewY transform 2022-01-25 14:40:02 +08:00
TonyCrane
11379283aa add support for rotate transform 2022-01-25 14:29:47 +08:00
TonyCrane
dd13559b11 replace warnings.warn with log.warning 2022-01-25 14:09:05 +08:00
TonyCrane
1658438fef allow Mobject.scale receive iterable scale_factor 2022-01-25 14:05:32 +08:00
TonyCrane
f4eb2724c5 refactor SVGMobject.handle_transforms 2022-01-25 14:04:35 +08:00
TonyCrane
33f720c73a fix typo 2022-01-25 13:15:53 +08:00
TonyCrane
bbb4fa155c fix the depth of svg tag 2022-01-25 13:14:19 +08:00
Grant Sanderson
2318c9e716 Merge pull request #1709 from TurkeyBilly/patch-4
Fix "Code is unreachable Pylance" warning for NumberPlane
2022-01-17 08:56:08 -08:00
Bill Xi
e80dd243f1 Added abstract method decorator and override 2022-01-17 20:27:34 +08:00
Grant Sanderson
3ffe300f96 Merge pull request #1704 from TurkeyBilly/patch-2
Adding "label_buff" config parameter for Brace
2022-01-03 08:53:34 -08:00
Bill Xi
24e3caa072 fix no "import copy" bug
added import copy
2022-01-03 16:49:00 +08:00
Bill Xi
9efd02c500 Remove spelling mistake
I misspelled "label"
2022-01-03 16:37:26 +08:00
Bill Xi
0a318486c5 Adding "lable_buff" config parameter for Brace 2022-01-03 14:57:16 +08:00
鹤翔万里
919133c6bf Merge pull request #1702 from Suji04/patch-2
removed extra 'all' from comments
2021-12-31 18:18:50 +08:00
Sujan Dutta
066a2ed5dc removed extra 'all' from comments 2021-12-31 00:10:57 -05:00
TonyCrane
09ced7ce9a docs: update changelog for #1694 and #1697 2021-12-23 10:34:15 +08:00
Grant Sanderson
505b229117 Merge pull request #1697 from 3b1b/video-work
Video work
2021-12-21 10:59:50 -08:00
Grant Sanderson
5aa8d15d85 Use FFMPEG_BIN instead of "ffmpeg" for sound incorporation 2021-12-21 10:58:58 -08:00
Grant Sanderson
7aa05572ab Remove unnecessary import 2021-12-21 10:58:41 -08:00
Grant Sanderson
f1996f8479 Small hack for the lightbulb, needs to be fixed properly later 2021-12-21 10:58:33 -08:00
Grant Sanderson
37b63ca956 Merge pull request #1694 from DangGiaChi/BarChart_modified
Add option to add ticks on x-axis in BarChart()
2021-12-17 09:30:53 -08:00
DangGiaChi
84fd657d9b Change variables names: x_tick, x_ticks, y_tick, y_ticks 2021-12-17 15:02:10 +07:00
DangGiaChi
b489490f41 Fixed things as suggestions 2021-12-17 07:14:37 +07:00
DangGiaChi
bbf45f95c6 Add option to add ticks on x-axis in BarChart() 2021-12-16 22:03:29 +07:00
23 changed files with 919 additions and 523 deletions

View File

@@ -1,6 +1,40 @@
Changelog
=========
v1.4.0
------
Fixed bugs
^^^^^^^^^^
- `f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__: Temporarily fixed ``Lightbulb``
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Fixed some bugs of ``SVGMobject``
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Fixed some bugs of SVG path string parser
- `#1720 <https://github.com/3b1b/manim/pull/1720>`__: Fixed some bugs of ``MTex``
New Features
^^^^^^^^^^^^
- `#1694 <https://github.com/3b1b/manim/pull/1694>`__: Added option to add ticks on x-axis in ``BarChart``
- `#1704 <https://github.com/3b1b/manim/pull/1704>`__: Added ``lable_buff`` config parameter for ``Brace``
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Added support for ``rotate skewX skewY`` transform in SVG
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Added style support to ``SVGMobject``
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added parser to <style> element of SVG
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added support for <line> element in ``SVGMobject``
Refactor
^^^^^^^^
- `5aa8d15 <https://github.com/3b1b/manim/pull/1697/commits/5aa8d15d85797f68a8f169ca69fd90d441a3abbe>`__: Used ``FFMPEG_BIN`` instead of ``"ffmpeg"`` for sound incorporation
- `#1709 <https://github.com/3b1b/manim/pull/1709>`__: Decorated ``CoordinateSystem.get_axes`` and ``.get_all_ranges`` as abstract method
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Refactored SVG path string parser
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Allowed ``Mobject.scale`` to receive iterable ``scale_factor``
- `#1716 <https://github.com/3b1b/manim/pull/1716>`__: Refactored ``MTex``
- `#1721 <https://github.com/3b1b/manim/pull/1721>`__: Improved config helper (``manimgl --config``)
- `#1723 <https://github.com/3b1b/manim/pull/1723>`__: Refactored ``MTex``
Dependencies
^^^^^^^^^^^^
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__
v1.3.0
------
@@ -63,7 +97,7 @@ Refactor
Dependencies
^^^^^^^^^^^^
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python packages `skia-pathops <https://github.com/fonttools/skia-pathops>`__
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__
v1.2.0
------

View File

@@ -274,7 +274,7 @@ class UpdatersExample(Scene):
square = Square()
square.set_fill(BLUE_E, 1)
# On all all frames, the constructor Brace(square, UP) will
# On all frames, the constructor Brace(square, UP) will
# be called, and the mobject brace will set its data to match
# that of the newly constructed object
brace = always_redraw(Brace, square, UP)

View File

@@ -68,4 +68,3 @@ from manimlib.utils.rate_functions import *
from manimlib.utils.simple_functions import *
from manimlib.utils.sounds import *
from manimlib.utils.space_ops import *
from manimlib.utils.strings import *

View File

@@ -1,3 +1,4 @@
from abc import abstractmethod
import numpy as np
import numbers
@@ -55,9 +56,11 @@ class CoordinateSystem():
def get_origin(self):
return self.c2p(*[0] * self.dimension)
@abstractmethod
def get_axes(self):
raise Exception("Not implemented")
@abstractmethod
def get_all_ranges(self):
raise Exception("Not implemented")

View File

@@ -608,8 +608,8 @@ class Arrow(Line):
self.insert_tip_anchor()
return self
def init_colors(self):
super().init_colors()
def init_colors(self, override=True):
super().init_colors(override)
self.create_tip_with_stroke_width()
def get_arc_length(self):

View File

@@ -4,6 +4,7 @@ import random
import sys
import moderngl
from functools import wraps
from collections import Iterable
import numpy as np
@@ -108,8 +109,8 @@ class Mobject(object):
"reflectiveness": self.reflectiveness,
}
def init_colors(self):
self.set_color(self.color, self.opacity)
def init_colors(self, override=True):
self.set_color(self.color, self.opacity, override)
def init_points(self):
# Typically implemented in subclass, unlpess purposefully left blank
@@ -596,7 +597,10 @@ class Mobject(object):
Otherwise, if about_point is given a value, scaling is done with
respect to that point.
"""
scale_factor = max(scale_factor, min_scale_factor)
if isinstance(scale_factor, Iterable):
scale_factor = np.array(scale_factor).clip(min=min_scale_factor)
else:
scale_factor = max(scale_factor, min_scale_factor)
self.apply_points_function(
lambda points: scale_factor * points,
about_point=about_point,

View File

@@ -149,7 +149,9 @@ class BarChart(VGroup):
"height": 4,
"width": 6,
"n_ticks": 4,
"include_x_ticks": False,
"tick_width": 0.2,
"tick_height": 0.15,
"label_y_axis": True,
"y_axis_label_height": 0.25,
"max_value": 1,
@@ -165,6 +167,7 @@ class BarChart(VGroup):
if self.max_value is None:
self.max_value = max(values)
self.n_ticks_x = len(values)
self.add_axes()
self.add_bars(values)
self.center()
@@ -172,31 +175,42 @@ class BarChart(VGroup):
def add_axes(self):
x_axis = Line(self.tick_width * LEFT / 2, self.width * RIGHT)
y_axis = Line(MED_LARGE_BUFF * DOWN, self.height * UP)
ticks = VGroup()
y_ticks = VGroup()
heights = np.linspace(0, self.height, self.n_ticks + 1)
values = np.linspace(0, self.max_value, self.n_ticks + 1)
for y, value in zip(heights, values):
tick = Line(LEFT, RIGHT)
tick.set_width(self.tick_width)
tick.move_to(y * UP)
ticks.add(tick)
y_axis.add(ticks)
y_tick = Line(LEFT, RIGHT)
y_tick.set_width(self.tick_width)
y_tick.move_to(y * UP)
y_ticks.add(y_tick)
y_axis.add(y_ticks)
if self.include_x_ticks == True:
x_ticks = VGroup()
widths = np.linspace(0, self.width, self.n_ticks_x + 1)
label_values = np.linspace(0, len(self.bar_names), self.n_ticks_x + 1)
for x, value in zip(widths, label_values):
x_tick = Line(UP, DOWN)
x_tick.set_height(self.tick_height)
x_tick.move_to(x * RIGHT)
x_ticks.add(x_tick)
x_axis.add(x_ticks)
self.add(x_axis, y_axis)
self.x_axis, self.y_axis = x_axis, y_axis
if self.label_y_axis:
labels = VGroup()
for tick, value in zip(ticks, values):
for y_tick, value in zip(y_ticks, values):
label = Tex(str(np.round(value, 2)))
label.set_height(self.y_axis_label_height)
label.next_to(tick, LEFT, SMALL_BUFF)
label.next_to(y_tick, LEFT, SMALL_BUFF)
labels.add(label)
self.y_axis_labels = labels
self.add(labels)
def add_bars(self, values):
buff = float(self.width) / (2 * len(values) + 1)
buff = float(self.width) / (2 * len(values))
bars = VGroup()
for i, value in enumerate(values):
bar = Rectangle(
@@ -205,7 +219,7 @@ class BarChart(VGroup):
stroke_width=self.bar_stroke_width,
fill_opacity=self.bar_fill_opacity,
)
bar.move_to((2 * i + 1) * buff * RIGHT, DOWN + LEFT)
bar.move_to((2 * i + 0.5) * buff * RIGHT, DOWN + LEFT * 5)
bars.add(bar)
bars.set_color_by_gradient(*self.bar_colors)

View File

@@ -1,5 +1,6 @@
import numpy as np
import math
import copy
from manimlib.animation.composition import AnimationGroup
from manimlib.constants import *
@@ -88,6 +89,7 @@ class BraceLabel(VMobject):
CONFIG = {
"label_constructor": Tex,
"label_scale": 1,
"label_buff": DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
}
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
@@ -104,7 +106,7 @@ class BraceLabel(VMobject):
if self.label_scale != 1:
self.label.scale(self.label_scale)
self.brace.put_at_tip(self.label)
self.brace.put_at_tip(self.label, buff=self.label_buff)
self.set_submobjects([self.brace, self.label])
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):

View File

@@ -50,6 +50,7 @@ class Lightbulb(SVGMobject):
def __init__(self, **kwargs):
super().__init__("lightbulb", **kwargs)
self.insert_n_curves(25)
class Speedometer(VMobject):

View File

@@ -1,29 +1,38 @@
import itertools as it
import re
import sys
from types import MethodType
from manimlib.constants import BLACK
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.color import color_to_int_rgb
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import display_during_execution
from manimlib.logger import log
SCALE_FACTOR_PER_FONT_POINT = 0.001
tex_hash_to_mob_map = {}
TEX_HASH_TO_MOB_MAP = {}
def _contains(span_0, span_1):
return span_0[0] <= span_1[0] and span_1[1] <= span_0[1]
def _get_neighbouring_pairs(iterable):
return list(adjacent_pairs(iterable))[:-1]
class _LabelledTex(SVGMobject):
class _TexSVG(SVGMobject):
CONFIG = {
"color": BLACK,
"height": None,
"path_string_config": {
"should_subdivide_sharp_curves": True,
@@ -32,31 +41,20 @@ class _LabelledTex(SVGMobject):
}
@staticmethod
def color_str_to_label(color_str):
if len(color_str) == 4:
# "#RGB" => "#RRGGBB"
color_str = "#" + "".join([c * 2 for c in color_str[1:]])
return int(color_str[1:], 16) - 1
def color_to_label(fill_color):
r, g, b = color_to_int_rgb(fill_color)
rg = r * 256 + g
return rg * 256 + b
def get_mobjects_from(self, element):
result = super().get_mobjects_from(element)
for mob in result:
if not hasattr(mob, "glyph_label"):
mob.glyph_label = -1
try:
color_str = element.getAttribute("fill")
if color_str:
glyph_label = _LabelledTex.color_str_to_label(color_str)
for mob in result:
mob.glyph_label = glyph_label
except:
pass
return result
def parse_labels(self):
for glyph in self:
glyph.glyph_label = _TexSVG.color_to_label(glyph.fill_color)
return self
class _TexSpan(object):
def __init__(self, script_type, label):
# script_type: 0 for normal, 1 for subscript, 2 for superscript.
# `script_type`: 0 for normal, 1 for subscript, 2 for superscript.
# Only those spans with `script_type == 0` will be colored.
self.script_type = script_type
self.label = label
@@ -70,50 +68,48 @@ class _TexSpan(object):
class _TexParser(object):
def __init__(self, mtex):
self.tex_string = mtex.tex_string
strings_to_break_up = remove_list_redundancies([
*mtex.isolate, *mtex.tex_to_color_map.keys(), mtex.tex_string
])
if "" in strings_to_break_up:
strings_to_break_up.remove("")
unbreakable_commands = mtex.unbreakable_commands
def __init__(self, tex_string, additional_substrings):
self.tex_string = tex_string
self.tex_spans_dict = {}
self.current_label = 0
self.break_up_by_braces()
self.current_label = -1
self.brace_index_pairs = self.get_brace_index_pairs()
self.existing_color_command_spans = self.get_existing_color_command_spans()
self.has_existing_color_commands = any(self.existing_color_command_spans.values())
self.specified_substring_spans = []
self.add_tex_span((0, len(tex_string)))
self.break_up_by_double_braces()
self.break_up_by_scripts()
self.break_up_by_additional_strings(strings_to_break_up)
self.merge_unbreakable_commands(unbreakable_commands)
self.break_up_by_additional_substrings(additional_substrings)
self.specified_substrings = remove_list_redundancies([
tex_string[slice(*span_tuple)]
for span_tuple in self.specified_substring_spans
])
self.check_if_overlap()
self.analyse_containing_labels()
@staticmethod
def label_to_color_tuple(n):
# Get a unique color different from black,
# or the svg file will not include the color information.
rgb = n + 1
def label_to_color_tuple(rgb):
# Get a unique color different from black.
rg, b = divmod(rgb, 256)
r, g = divmod(rg, 256)
return r, g, b
@staticmethod
def contains(span_0, span_1):
return span_0[0] <= span_1[0] and span_1[1] <= span_0[1]
def add_tex_span(self, span_tuple, script_type=0, label=-1):
if span_tuple in self.tex_spans_dict:
return
if script_type == 0:
# Should be additionally labelled.
label = self.current_label
self.current_label += 1
label = self.current_label
tex_span = _TexSpan(script_type, label)
self.tex_spans_dict[span_tuple] = tex_span
def break_up_by_braces(self):
tex_string = self.tex_string
span_tuples = []
def get_brace_index_pairs(self):
result = []
left_brace_indices = []
for match_obj in re.finditer(r"(\\*)(\{|\})", tex_string):
for match_obj in re.finditer(r"(\\*)(\{|\})", self.tex_string):
# Braces following even numbers of backslashes are counted.
if len(match_obj.group(1)) % 2 == 1:
continue
@@ -123,17 +119,60 @@ class _TexParser(object):
else:
left_brace_index = left_brace_indices.pop()
right_brace_index = match_obj.span(2)[1]
span_tuples.append((left_brace_index, right_brace_index))
result.append((left_brace_index, right_brace_index))
if left_brace_indices:
self.raise_tex_parsing_error()
self.raise_tex_parsing_error("unmatched braces")
return result
self.paired_braces_tuples = span_tuples
for span_tuple in span_tuples:
self.add_tex_span(span_tuple)
def get_existing_color_command_spans(self):
tex_string = self.tex_string
color_related_commands_dict = _TexParser.get_color_related_commands_dict()
commands = color_related_commands_dict.keys()
result = {
command_name: []
for command_name in commands
}
brace_index_pairs = self.brace_index_pairs
pattern = "|".join([
re.escape(command_name)
for command_name in commands
])
for match_obj in re.finditer(pattern, tex_string):
span_tuple = match_obj.span()
command_begin_index = span_tuple[0]
command_name = match_obj.group()
n_braces = color_related_commands_dict[command_name]
for _ in range(n_braces):
span_tuple = min(filter(
lambda t: t[0] >= span_tuple[1],
brace_index_pairs
))
result[command_name].append(
(command_begin_index, span_tuple[1])
)
return result
def break_up_by_double_braces(self):
# Match paired double braces (`{{...}}`).
skip_pair = False
for prev_span_tuple, span_tuple in _get_neighbouring_pairs(
self.brace_index_pairs
):
if skip_pair:
skip_pair = False
continue
if all([
span_tuple[0] == prev_span_tuple[0] - 1,
span_tuple[1] == prev_span_tuple[1] + 1
]):
self.add_tex_span(span_tuple)
self.specified_substring_spans.append(span_tuple)
skip_pair = True
def break_up_by_scripts(self):
# Match subscripts & superscripts.
tex_string = self.tex_string
brace_indices_dict = dict(self.tex_spans_dict.keys())
brace_indices_dict = dict(self.brace_index_pairs)
for match_obj in re.finditer(r"((?<!\\)(_|\^)\s*)|(\s+(_|\^)\s*)", tex_string):
script_type = 1 if "_" in match_obj.group() else 2
token_begin, token_end = match_obj.span()
@@ -142,11 +181,11 @@ class _TexParser(object):
else:
content_match_obj = re.match(r"\w|\\[a-zA-Z]+", tex_string[token_end:])
if not content_match_obj:
self.raise_tex_parsing_error()
self.raise_tex_parsing_error("unclear subscript/superscript")
content_span = tuple([
index + token_end for index in content_match_obj.span()
])
self.add_tex_span(content_span)
self.add_tex_span(content_span)
label = self.tex_spans_dict[content_span].label
self.add_tex_span(
(token_begin, content_span[1]),
@@ -154,11 +193,11 @@ class _TexParser(object):
label=label
)
def break_up_by_additional_strings(self, strings_to_break_up):
def break_up_by_additional_substrings(self, additional_substrings):
tex_string = self.tex_string
all_span_tuples = []
for string in strings_to_break_up:
# Only matches non-crossing strings.
for string in additional_substrings:
# Only match non-crossing strings.
for match_obj in re.finditer(re.escape(string), tex_string):
all_span_tuples.append(match_obj.span())
@@ -175,58 +214,59 @@ class _TexParser(object):
if span_begin >= span_end:
continue
span_tuple = (span_begin, span_end)
if span_tuple not in self.tex_spans_dict:
self.add_tex_span(span_tuple)
self.add_tex_span(span_tuple)
self.specified_substring_spans.append(span_tuple)
def merge_unbreakable_commands(self, unbreakable_commands):
tex_string = self.tex_string
command_merge_spans = []
brace_indices_dict = dict(self.paired_braces_tuples)
# Braces leading by `unbreakable_commands` shouldn't be marked.
for command in unbreakable_commands:
for match_obj in re.finditer(re.escape(command), tex_string):
merge_begin_index = match_obj.span()[1]
merge_end_index = merge_begin_index
if merge_end_index not in brace_indices_dict:
def check_if_overlap(self):
span_tuples = sorted(
self.tex_spans_dict.keys(),
key=lambda t: (t[0], -t[1])
)
overlapping_span_pairs = []
for i, span_0 in enumerate(span_tuples):
for span_1 in span_tuples[i + 1 :]:
if span_0[1] <= span_1[0]:
continue
while merge_end_index in brace_indices_dict:
merge_end_index = brace_indices_dict[merge_end_index]
command_merge_spans.append((merge_begin_index, merge_end_index))
self.tex_spans_dict = {
span_tuple: tex_span
for span_tuple, tex_span in self.tex_spans_dict.items()
if all([
not _TexParser.contains(merge_span, span_tuple)
for merge_span in command_merge_spans
])
}
if span_0[1] < span_1[1]:
overlapping_span_pairs.append((span_0, span_1))
if overlapping_span_pairs:
tex_string = self.tex_string
log.error("Overlapping substring pairs occur in MTex:")
for span_tuple_pair in overlapping_span_pairs:
log.error(", ".join(
f"\"{tex_string[slice(*span_tuple)]}\""
for span_tuple in span_tuple_pair
))
raise ValueError
def analyse_containing_labels(self):
for span_0, tex_span_0 in self.tex_spans_dict.items():
if tex_span_0.script_type != 0:
continue
for span_1, tex_span_1 in self.tex_spans_dict.items():
if _TexParser.contains(span_1, span_0):
if _contains(span_1, span_0):
tex_span_1.containing_labels.append(tex_span_0.label)
def get_labelled_expression(self):
def get_labelled_tex_string(self):
tex_string = self.tex_string
if not self.tex_spans_dict:
if self.current_label == 0 and not self.has_existing_color_commands:
return tex_string
# Remove the span of extire tex string.
indices_with_labels = sorted([
(span_tuple[i], i, span_tuple[1 - i], tex_span.label)
for span_tuple, tex_span in self.tex_spans_dict.items()
if tex_span.script_type == 0
for i in range(2)
], key=lambda t: (t[0], -t[1], -t[2]))
# Add one more item to ensure all the substrings are joined.
indices_with_labels.append((len(tex_string), 0, 0, 0))
], key=lambda t: (t[0], -t[1], -t[2]))[1:]
# Prevent from "\\color[RGB]" being replaced.
# Hopefully tex string doesn't contain such a substring...
color_command_placeholder = "{{\\iffalse \\fi}}"
result = tex_string[: indices_with_labels[0][0]]
index_with_label_pairs = _get_neighbouring_pairs(indices_with_labels)
for index_with_label, next_index_with_label in index_with_label_pairs:
for index_with_label, next_index_with_label in _get_neighbouring_pairs(
indices_with_labels
):
index, flag, _, label = index_with_label
next_index, *_ = next_index_with_label
# Adding one more pair of braces will help maintain the glyghs of tex file...
@@ -234,7 +274,7 @@ class _TexParser(object):
color_tuple = _TexParser.label_to_color_tuple(label)
result += "".join([
"{{",
"\\color[RGB]",
color_command_placeholder,
"{",
",".join(map(str, color_tuple)),
"}"
@@ -242,65 +282,126 @@ class _TexParser(object):
else:
result += "}}"
result += tex_string[index : next_index]
return result
def raise_tex_parsing_error(self):
raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"")
color_related_commands_dict = _TexParser.get_color_related_commands_dict()
for command_name, command_spans in self.existing_color_command_spans.items():
if not command_spans:
continue
n_braces = color_related_commands_dict[command_name]
command_to_replace = command_name + n_braces * "{black}"
commands = {
tex_string[slice(*span_tuple)]
for span_tuple in command_spans
}
for command in commands:
result = result.replace(command, command_to_replace)
return result.replace(color_command_placeholder, "\\color[RGB]")
def raise_tex_parsing_error(self, message):
raise ValueError(f"Failed to parse tex ({message}): \"{self.tex_string}\"")
@staticmethod
def get_color_related_commands_dict():
# Only list a few commands that are commonly used.
return {
"\\color": 1,
"\\textcolor": 1,
"\\pagecolor": 1,
"\\colorbox": 1,
"\\fcolorbox": 2,
}
class MTex(VMobject):
CONFIG = {
"fill_opacity": 1.0,
"stroke_width": 0,
"should_center": True,
"font_size": 48,
"height": None,
"organize_left_to_right": False,
"alignment": "\\centering",
"tex_environment": "align*",
"isolate": [],
"unbreakable_commands": ["\\begin", "\\end"],
"tex_to_color_map": {},
"use_plain_tex_file": False,
}
def __init__(self, tex_string, **kwargs):
super().__init__(**kwargs)
self.tex_string = MTex.modify_tex_string(tex_string)
tex_parser = _TexParser(self)
self.tex_spans_dict = tex_parser.tex_spans_dict
new_tex = tex_parser.get_labelled_expression()
full_tex = self.get_tex_file_body(new_tex)
hash_val = hash(full_tex)
if hash_val not in tex_hash_to_mob_map:
with display_during_execution(f"Writing \"{tex_string}\""):
filename = tex_to_svg_file(full_tex)
svg_mob = _LabelledTex(filename)
tex_hash_to_mob_map[hash_val] = svg_mob
self.add(*[
submob.copy()
for submob in tex_hash_to_mob_map[hash_val]
])
self.build_submobjects()
self.init_colors()
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
@staticmethod
def modify_tex_string(tex_string):
result = tex_string.strip("\n")
tex_string = tex_string.strip()
# Prevent from passing an empty string.
if not result:
result = "\\quad"
if not tex_string:
tex_string = "\\quad"
self.tex_string = tex_string
self.generate_mobject()
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
def get_additional_substrings_to_break_up(self):
result = remove_list_redundancies([
*self.tex_to_color_map.keys(), *self.isolate
])
if "" in result:
result.remove("")
return result
def get_tex_file_body(self, new_tex):
def get_parser(self):
return _TexParser(self.tex_string, self.get_additional_substrings_to_break_up())
def generate_mobject(self):
tex_string = self.tex_string
tex_parser = self.get_parser()
self.tex_spans_dict = tex_parser.tex_spans_dict
self.specified_substrings = tex_parser.specified_substrings
fill_color = self.get_fill_color()
# Cannot simultaneously be false, so at least one file is generated.
require_labelled_tex_file = tex_parser.current_label != 0
require_plain_tex_file = any([
self.use_plain_tex_file,
tex_parser.has_existing_color_commands,
tex_parser.current_label == 0
])
if require_labelled_tex_file:
labelled_full_tex = self.get_tex_file_body(tex_parser.get_labelled_tex_string())
labelled_hash_val = hash(labelled_full_tex)
if labelled_hash_val in TEX_HASH_TO_MOB_MAP:
self.add(*TEX_HASH_TO_MOB_MAP[labelled_hash_val].copy())
else:
with display_during_execution(f"Writing \"{tex_string}\""):
labelled_svg_glyphs = MTex.get_svg_glyphs(labelled_full_tex)
labelled_svg_glyphs.parse_labels()
self.add(*labelled_svg_glyphs)
self.build_submobjects()
TEX_HASH_TO_MOB_MAP[labelled_hash_val] = self.copy()
if not require_plain_tex_file:
self.set_fill(color=fill_color)
return self
# require_plain_tex_file == True
self.set_submobjects([])
full_tex = self.get_tex_file_body(tex_string, fill_color=fill_color)
hash_val = hash(full_tex)
if hash_val in TEX_HASH_TO_MOB_MAP:
self.add(*TEX_HASH_TO_MOB_MAP[hash_val].copy())
else:
with display_during_execution(f"Writing \"{tex_string}\""):
svg_glyphs = MTex.get_svg_glyphs(full_tex)
if require_labelled_tex_file:
labelled_svg_mob = TEX_HASH_TO_MOB_MAP[labelled_hash_val]
for glyph, labelled_glyph in zip(svg_glyphs, it.chain(*labelled_svg_mob)):
glyph.glyph_label = labelled_glyph.glyph_label
else:
for glyph in svg_glyphs:
glyph.glyph_label = 0
self.add(*svg_glyphs)
self.build_submobjects()
TEX_HASH_TO_MOB_MAP[hash_val] = self.copy()
return self
def get_tex_file_body(self, new_tex, fill_color=None):
if self.tex_environment:
new_tex = "\n".join([
f"\\begin{{{self.tex_environment}}}",
@@ -309,6 +410,17 @@ class MTex(VMobject):
])
if self.alignment:
new_tex = "\n".join([self.alignment, new_tex])
if fill_color:
int_rgb = color_to_int_rgb(fill_color)
color_command = "".join([
"\\color[RGB]",
"{",
",".join(map(str, int_rgb)),
"}"
])
new_tex = "\n".join(
[color_command, new_tex]
)
tex_config = get_tex_config()
return tex_config["tex_body"].replace(
@@ -316,9 +428,17 @@ class MTex(VMobject):
new_tex
)
@staticmethod
def get_svg_glyphs(full_tex):
filename = tex_to_svg_file(full_tex)
return _TexSVG(filename)
def build_submobjects(self):
if not self.submobjects:
return
self.init_colors()
for glyph in self.submobjects:
glyph.set_fill(glyph.fill_color)
self.group_submobjects()
self.sort_scripts_in_tex_order()
self.assign_submob_tex_strings()
@@ -333,59 +453,71 @@ class MTex(VMobject):
new_submobjects.append(submobject)
new_glyphs = []
current_glyph_label = -1
for submob in self.submobjects:
if submob.glyph_label == current_glyph_label:
new_glyphs.append(submob)
current_glyph_label = 0
for glyph in self.submobjects:
if glyph.glyph_label == current_glyph_label:
new_glyphs.append(glyph)
else:
append_new_submobject(new_glyphs)
new_glyphs = [submob]
current_glyph_label = submob.glyph_label
new_glyphs = [glyph]
current_glyph_label = glyph.glyph_label
append_new_submobject(new_glyphs)
self.set_submobjects(new_submobjects)
def sort_scripts_in_tex_order(self):
# LaTeX always puts superscripts before subscripts.
# This function sorts the submobjects of scripts in the order of tex given.
tex_spans_dict = self.tex_spans_dict
index_and_span_list = sorted([
(index, span_tuple)
for span_tuple, tex_span in self.tex_spans_dict.items()
for span_tuple, tex_span in tex_spans_dict.items()
if tex_span.script_type != 0
for index in span_tuple
])
index_and_span_pair = _get_neighbouring_pairs(index_and_span_list)
for index_and_span_0, index_and_span_1 in index_and_span_pair:
switch_range_pairs = []
for index_and_span_0, index_and_span_1 in _get_neighbouring_pairs(
index_and_span_list
):
index_0, span_tuple_0 = index_and_span_0
index_1, span_tuple_1 = index_and_span_1
if index_0 != index_1:
continue
if not all([
self.tex_spans_dict[span_tuple_0].script_type == 1,
self.tex_spans_dict[span_tuple_1].script_type == 2
tex_spans_dict[span_tuple_0].script_type == 1,
tex_spans_dict[span_tuple_1].script_type == 2
]):
continue
submob_slice_0 = self.slice_of_part(
submob_range_0 = self.range_of_part(
self.get_part_by_span_tuples([span_tuple_0])
)
submob_slice_1 = self.slice_of_part(
submob_range_1 = self.range_of_part(
self.get_part_by_span_tuples([span_tuple_1])
)
submobs = self.submobjects
self.set_submobjects([
*submobs[: submob_slice_1.start],
*submobs[submob_slice_0],
*submobs[submob_slice_1.stop : submob_slice_0.start],
*submobs[submob_slice_1],
*submobs[submob_slice_0.stop :]
])
switch_range_pairs.append((submob_range_0, submob_range_1))
switch_range_pairs.sort(key=lambda t: (t[0].stop, -t[0].start))
indices = list(range(len(self.submobjects)))
for submob_range_0, submob_range_1 in switch_range_pairs:
indices = [
*indices[: submob_range_1.start],
*indices[submob_range_0.start : submob_range_0.stop],
*indices[submob_range_1.stop : submob_range_0.start],
*indices[submob_range_1.start : submob_range_1.stop],
*indices[submob_range_0.stop :]
]
submobs = self.submobjects
self.set_submobjects([submobs[i] for i in indices])
def assign_submob_tex_strings(self):
# Not sure whether this is the best practice...
# Just a temporary hack for supporting `TransformMatchingTex`.
# This temporarily supports `TransformMatchingTex`.
tex_string = self.tex_string
tex_spans_dict = self.tex_spans_dict
# Use tex strings including "_", "^".
label_dict = {}
for span_tuple, tex_span in self.tex_spans_dict.items():
for span_tuple, tex_span in tex_spans_dict.items():
if tex_span.script_type != 0:
label_dict[tex_span.label] = span_tuple
else:
@@ -402,7 +534,7 @@ class MTex(VMobject):
curr_span_tuple = label_dict[curr_label]
prev_span_tuple = label_dict[prev_label]
next_span_tuple = label_dict[next_label]
containing_labels = self.tex_spans_dict[curr_span_tuple].containing_labels
containing_labels = tex_spans_dict[curr_span_tuple].containing_labels
tex_string_spans.append([
prev_span_tuple[1] if prev_label in containing_labels else curr_span_tuple[0],
next_span_tuple[0] if next_label in containing_labels else curr_span_tuple[1]
@@ -415,31 +547,38 @@ class MTex(VMobject):
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
def get_part_by_span_tuples(self, span_tuples):
labels = remove_list_redundancies(list(it.chain(*[
self.tex_spans_dict[span_tuple].containing_labels
tex_spans_dict = self.tex_spans_dict
labels = set(it.chain(*[
tex_spans_dict[span_tuple].containing_labels
for span_tuple in span_tuples
])))
]))
return VGroup(*filter(
lambda submob: submob.submob_label in labels,
self.submobjects
))
def find_span_components_of_custom_span(self, custom_span_tuple, partial_result=[]):
def find_span_components_of_custom_span(self, custom_span_tuple):
tex_string = self.tex_string
span_choices = sorted(filter(
lambda t: _contains(custom_span_tuple, t),
self.tex_spans_dict.keys()
))
# Filter out spans that reach the farthest.
span_choices_dict = dict(span_choices)
span_begin, span_end = custom_span_tuple
if span_begin == span_end:
return partial_result
next_begin_choices = sorted([
span_tuple[1]
for span_tuple in self.tex_spans_dict.keys()
if span_tuple[0] == span_begin and span_tuple[1] <= span_end
], reverse=True)
for next_begin in next_begin_choices:
result = self.find_span_components_of_custom_span(
(next_begin, span_end), [*partial_result, (span_begin, next_begin)]
)
if result is not None:
return result
return None
result = []
while span_begin != span_end:
if span_begin not in span_choices_dict:
if tex_string[span_begin].strip():
return None
# Whitespaces may occur between spans.
span_begin += 1
continue
next_begin = span_choices_dict[span_begin]
result.append((span_begin, next_begin))
span_begin = next_begin
return result
def get_part_by_custom_span_tuple(self, custom_span_tuple):
span_tuples = self.find_span_components_of_custom_span(custom_span_tuple)
@@ -464,7 +603,10 @@ class MTex(VMobject):
def set_color_by_tex_to_color_map(self, tex_to_color_map):
for tex, color in list(tex_to_color_map.items()):
self.set_color_by_tex(tex, color)
try:
self.set_color_by_tex(tex, color)
except:
pass
return self
def indices_of_part(self, part):
@@ -480,13 +622,13 @@ class MTex(VMobject):
part = self.get_part_by_tex(tex, index=index)
return self.indices_of_part(part)
def slice_of_part(self, part):
def range_of_part(self, part):
indices = self.indices_of_part(part)
return slice(indices[0], indices[-1] + 1)
return range(indices[0], indices[-1] + 1)
def slice_of_part_by_tex(self, tex, index=0):
def range_of_part_by_tex(self, tex, index=0):
part = self.get_part_by_tex(tex, index=index)
return self.slice_of_part(part)
return self.range_of_part(part)
def index_of_part(self, part):
return self.indices_of_part(part)[0]
@@ -505,14 +647,8 @@ class MTex(VMobject):
for span_tuple in self.tex_spans_dict.keys()
])
def print_tex_strings_of_submobjects(self):
# For debugging
# Work with `index_labels()`
print("\n")
print(f"Submobjects of \"{self.get_tex()}\":")
for i, submob in enumerate(self.submobjects):
print(f"{i}: \"{submob.get_tex()}\"")
print("\n")
def get_specified_substrings(self):
return self.specified_substrings
class MTexText(MTex):

View File

@@ -1,18 +1,19 @@
import itertools as it
import re
import string
import warnings
import os
import hashlib
from xml.dom import minidom
import cssselect2
from colour import web2hex
from xml.etree import ElementTree
from tinycss2 import serialize as css_serialize
from tinycss2 import parse_stylesheet, parse_declaration_list
from manimlib.constants import DEFAULT_STROKE_WIDTH
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
from manimlib.constants import BLACK
from manimlib.constants import WHITE
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT, IN
from manimlib.constants import DEGREES, PI
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import RoundedRectangle
@@ -23,16 +24,77 @@ from manimlib.utils.config_ops import digest_config
from manimlib.utils.directories import get_mobject_data_dir
from manimlib.utils.images import get_full_vector_image_path
from manimlib.utils.simple_functions import clip
from manimlib.logger import log
def string_to_numbers(num_string):
num_string = num_string.replace("-", ",-")
num_string = num_string.replace("e,-", "e-")
return [
float(s)
for s in re.split("[ ,]", num_string)
if s != ""
]
DEFAULT_STYLE = {
"fill": "black",
"stroke": "none",
"fill-opacity": "1",
"stroke-opacity": "1",
"stroke-width": 0,
}
def cascade_element_style(element, inherited):
style = inherited.copy()
for attr in DEFAULT_STYLE:
if element.get(attr):
style[attr] = element.get(attr)
if element.get("style"):
declarations = parse_declaration_list(element.get("style"))
for declaration in declarations:
style[declaration.name] = css_serialize(declaration.value)
return style
def parse_color(color):
color = color.strip()
if color[0:3] == "rgb":
splits = color[4:-1].strip().split(",")
if splits[0].strip()[-1] == "%":
parsed_rgbs = [float(i.strip()[:-1]) / 100.0 for i in splits]
else:
parsed_rgbs = [int(i) / 255.0 for i in splits]
return rgb_to_hex(parsed_rgbs)
else:
return web2hex(color)
def fill_default_values(style, default_style):
default = DEFAULT_STYLE.copy()
default.update(default_style)
for attr in default:
if attr not in style:
style[attr] = default[attr]
def parse_style(style, default_style):
manim_style = {}
fill_default_values(style, default_style)
manim_style["fill_opacity"] = float(style["fill-opacity"])
manim_style["stroke_opacity"] = float(style["stroke-opacity"])
manim_style["stroke_width"] = float(style["stroke-width"])
if style["fill"] == "none":
manim_style["fill_opacity"] = 0
else:
manim_style["fill_color"] = parse_color(style["fill"])
if style["stroke"] == "none":
manim_style["stroke_width"] = 0
if "fill_color" in manim_style:
manim_style["stroke_color"] = manim_style["fill_color"]
else:
manim_style["stroke_color"] = parse_color(style["stroke"])
return manim_style
class SVGMobject(VMobject):
@@ -43,8 +105,7 @@ class SVGMobject(VMobject):
# Must be filled in in a subclass, or when called
"file_name": None,
"unpack_groups": True, # if False, creates a hierarchy of VGroups
# TODO, style components should be read in, not defaulted
"stroke_width": DEFAULT_STROKE_WIDTH,
"stroke_width": 0.0,
"fill_opacity": 1.0,
"path_string_config": {}
}
@@ -67,48 +128,68 @@ class SVGMobject(VMobject):
if self.width is not None:
self.set_width(self.width)
def init_colors(self, override=False):
super().init_colors(override=override)
def init_points(self):
doc = minidom.parse(self.file_path)
etree = ElementTree.parse(self.file_path)
wrapper = cssselect2.ElementWrapper.from_xml_root(etree)
svg = etree.getroot()
namespace = svg.tag.split("}")[0][1:]
self.ref_to_element = {}
self.css_matcher = cssselect2.Matcher()
for svg in doc.getElementsByTagName("svg"):
mobjects = self.get_mobjects_from(svg)
if self.unpack_groups:
self.add(*mobjects)
else:
self.add(*mobjects[0].submobjects)
doc.unlink()
for style in etree.findall(f"{{{namespace}}}style"):
self.parse_css_style(style.text)
def get_mobjects_from(self, element):
result = []
if not isinstance(element, minidom.Element):
return result
if element.tagName == 'defs':
self.update_ref_to_element(element)
elif element.tagName == 'style':
pass # TODO, handle style
elif element.tagName in ['g', 'svg', 'symbol']:
result += it.chain(*(
self.get_mobjects_from(child)
for child in element.childNodes
))
elif element.tagName == 'path':
result.append(self.path_string_to_mobject(
element.getAttribute('d')
))
elif element.tagName == 'use':
result += self.use_to_mobjects(element)
elif element.tagName == 'rect':
result.append(self.rect_to_mobject(element))
elif element.tagName == 'circle':
result.append(self.circle_to_mobject(element))
elif element.tagName == 'ellipse':
result.append(self.ellipse_to_mobject(element))
elif element.tagName in ['polygon', 'polyline']:
result.append(self.polygon_to_mobject(element))
mobjects = self.get_mobjects_from(wrapper, dict())
if self.unpack_groups:
self.add(*mobjects)
else:
pass # TODO
# warnings.warn("Unknown element type: " + element.tagName)
self.add(*mobjects[0].submobjects)
def get_mobjects_from(self, wrapper, style):
result = []
element = wrapper.etree_element
if not isinstance(element, ElementTree.Element):
return result
matches = self.css_matcher.match(wrapper)
if matches:
for match in matches:
_, _, _, css_style = match
style.update(css_style)
style = cascade_element_style(element, style)
tag = element.tag.split("}")[-1]
if tag == 'defs':
self.update_ref_to_element(wrapper, style)
elif tag in ['g', 'svg', 'symbol']:
result += it.chain(*(
self.get_mobjects_from(child, style)
for child in wrapper.iter_children()
))
elif tag == 'path':
result.append(self.path_string_to_mobject(
element.get('d'), style
))
elif tag == 'use':
result += self.use_to_mobjects(element, style)
elif tag == 'line':
result.append(self.line_to_mobject(element, style))
elif tag == 'rect':
result.append(self.rect_to_mobject(element, style))
elif tag == 'circle':
result.append(self.circle_to_mobject(element, style))
elif tag == 'ellipse':
result.append(self.ellipse_to_mobject(element, style))
elif tag in ['polygon', 'polyline']:
result.append(self.polygon_to_mobject(element, style))
elif tag == 'style':
pass
else:
log.warning(f"Unsupported element type: {tag}")
pass # TODO, support <text> tag
result = [m for m in result if m is not None]
self.handle_transforms(element, VGroup(*result))
if len(result) > 1 and not self.unpack_groups:
@@ -116,26 +197,51 @@ class SVGMobject(VMobject):
return result
def g_to_mobjects(self, g_element):
mob = VGroup(*self.get_mobjects_from(g_element))
self.handle_transforms(g_element, mob)
return mob.submobjects
def generate_default_style(self):
style = {
"fill-opacity": self.fill_opacity,
"stroke-width": self.stroke_width,
"stroke-opacity": self.stroke_opacity,
}
if self.color:
style["fill"] = style["stroke"] = self.color
if self.fill_color:
style["fill"] = self.fill_color
if self.stroke_color:
style["stroke"] = self.stroke_color
return style
def parse_css_style(self, css):
rules = parse_stylesheet(css, True, True)
for rule in rules:
selectors = cssselect2.compile_selector_list(rule.prelude)
declarations = parse_declaration_list(rule.content)
style = {
declaration.name: css_serialize(declaration.value)
for declaration in declarations
if declaration.name in DEFAULT_STYLE
}
payload = style
for selector in selectors:
self.css_matcher.add_selector(selector, payload)
def path_string_to_mobject(self, path_string):
def path_string_to_mobject(self, path_string, style):
return VMobjectFromSVGPathstring(
path_string,
**self.path_string_config,
**parse_style(style, self.generate_default_style()),
)
def use_to_mobjects(self, use_element):
def use_to_mobjects(self, use_element, local_style):
# Remove initial "#" character
ref = use_element.getAttribute("xlink:href")[1:]
ref = use_element.get(r"{http://www.w3.org/1999/xlink}href")[1:]
if ref not in self.ref_to_element:
warnings.warn(f"{ref} not recognized")
log.warning(f"{ref} not recognized")
return VGroup()
return self.get_mobjects_from(
self.ref_to_element[ref]
)
def_element, def_style = self.ref_to_element[ref]
style = local_style.copy()
style.update(def_style)
return self.get_mobjects_from(def_element, style)
def attribute_to_float(self, attr):
stripped_attr = "".join([
@@ -144,57 +250,38 @@ class SVGMobject(VMobject):
])
return float(stripped_attr)
def polygon_to_mobject(self, polygon_element):
path_string = polygon_element.getAttribute("points")
def polygon_to_mobject(self, polygon_element, style):
path_string = polygon_element.get("points")
for digit in string.digits:
path_string = path_string.replace(f" {digit}", f"L {digit}")
path_string = path_string.replace("L", "M", 1)
return self.path_string_to_mobject(path_string)
return self.path_string_to_mobject(path_string, style)
def circle_to_mobject(self, circle_element):
x, y, r = [
self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key)
else 0.0
def circle_to_mobject(self, circle_element, style):
x, y, r = (
self.attribute_to_float(circle_element.get(key, "0.0"))
for key in ("cx", "cy", "r")
]
return Circle(radius=r).shift(x * RIGHT + y * DOWN)
)
return Circle(
radius=r,
**parse_style(style, self.generate_default_style())
).shift(x * RIGHT + y * DOWN)
def ellipse_to_mobject(self, circle_element):
x, y, rx, ry = [
self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key)
else 0.0
def ellipse_to_mobject(self, circle_element, style):
x, y, rx, ry = (
self.attribute_to_float(circle_element.get(key, "0.0"))
for key in ("cx", "cy", "rx", "ry")
]
result = Circle()
)
result = Circle(**parse_style(style, self.generate_default_style()))
result.stretch(rx, 0)
result.stretch(ry, 1)
result.shift(x * RIGHT + y * DOWN)
return result
def rect_to_mobject(self, rect_element):
fill_color = rect_element.getAttribute("fill")
stroke_color = rect_element.getAttribute("stroke")
stroke_width = rect_element.getAttribute("stroke-width")
corner_radius = rect_element.getAttribute("rx")
def rect_to_mobject(self, rect_element, style):
stroke_width = rect_element.get("stroke-width", "")
corner_radius = rect_element.get("rx", "")
# input preprocessing
fill_opacity = 1
if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE):
fill_opacity = 0
fill_color = BLACK # shdn't be necessary but avoids error msgs
if fill_color in ["#000", "#000000"]:
fill_color = WHITE
if stroke_color in ["", "none", "#FFF", "#FFFFFF"] or Color(stroke_color) == Color(WHITE):
stroke_width = 0
stroke_color = BLACK
if stroke_color in ["#000", "#000000"]:
stroke_color = WHITE
if stroke_width in ["", "none", "0"]:
stroke_width = 0
@@ -203,98 +290,131 @@ class SVGMobject(VMobject):
corner_radius = float(corner_radius)
parsed_style = parse_style(style, self.generate_default_style())
parsed_style["stroke_width"] = stroke_width
if corner_radius == 0:
mob = Rectangle(
width=self.attribute_to_float(
rect_element.getAttribute("width")
rect_element.get("width", "")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
rect_element.get("height", "")
),
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_color=fill_color,
fill_opacity=fill_opacity
**parsed_style,
)
else:
mob = RoundedRectangle(
width=self.attribute_to_float(
rect_element.getAttribute("width")
rect_element.get("width", "")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
rect_element.get("height", "")
),
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_color=fill_color,
fill_opacity=opacity,
corner_radius=corner_radius
corner_radius=corner_radius,
**parsed_style
)
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
return mob
def line_to_mobject(self, line_element, style):
x1, y1, x2, y2 = (
self.attribute_to_float(line_element.get(key, "0.0"))
for key in ("x1", "y1", "x2", "y2")
)
return Line(
[x1, -y1, 0], [x2, -y2, 0],
**parse_style(style, self.generate_default_style())
)
def handle_transforms(self, element, mobject):
# TODO, this could use some cleaning...
x, y = 0, 0
try:
x = self.attribute_to_float(element.getAttribute('x'))
# Flip y
y = -self.attribute_to_float(element.getAttribute('y'))
mobject.shift([x, y, 0])
except Exception:
pass
x, y = (
self.attribute_to_float(element.get(key, "0.0"))
for key in ("x", "y")
)
mobject.shift(x * RIGHT + y * DOWN)
transform = element.getAttribute('transform')
transform_names = [
"matrix",
"translate", "translateX", "translateY",
"scale", "scaleX", "scaleY",
"rotate",
"skewX", "skewY"
]
transform_pattern = re.compile("|".join([x + r"[^)]*\)" for x in transform_names]))
number_pattern = re.compile(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?")
transforms = transform_pattern.findall(element.get("transform", ""))[::-1]
try: # transform matrix
prefix = "matrix("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
transform = string_to_numbers(transform)
transform = np.array(transform).reshape([3, 2])
x = transform[2][0]
y = -transform[2][1]
matrix = np.identity(self.dim)
matrix[:2, :2] = transform[:2, :]
matrix[1] *= -1
matrix[:, 1] *= -1
for transform in transforms:
op_name, op_args = transform.split("(")
op_name = op_name.strip()
op_args = [float(x) for x in number_pattern.findall(op_args)]
for mob in mobject.family_members_with_points():
mob.apply_matrix(matrix.T)
mobject.shift(x * RIGHT + y * UP)
except:
pass
if op_name == "matrix":
self._handle_matrix_transform(mobject, op_name, op_args)
elif op_name.startswith("translate"):
self._handle_translate_transform(mobject, op_name, op_args)
elif op_name.startswith("scale"):
self._handle_scale_transform(mobject, op_name, op_args)
elif op_name == "rotate":
self._handle_rotate_transform(mobject, op_name, op_args)
elif op_name.startswith("skew"):
self._handle_skew_transform(mobject, op_name, op_args)
try: # transform scale
prefix = "scale("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
scale_values = string_to_numbers(transform)
if len(scale_values) == 2:
scale_x, scale_y = scale_values
mobject.scale(np.array([scale_x, scale_y, 1]), about_point=ORIGIN)
elif len(scale_values) == 1:
scale = scale_values[0]
mobject.scale(np.array([scale, scale, 1]), about_point=ORIGIN)
except:
pass
def _handle_matrix_transform(self, mobject, op_name, op_args):
transform = np.array(op_args).reshape([3, 2])
x = transform[2][0]
y = -transform[2][1]
matrix = np.identity(self.dim)
matrix[:2, :2] = transform[:2, :]
matrix[1] *= -1
matrix[:, 1] *= -1
for mob in mobject.family_members_with_points():
mob.apply_matrix(matrix.T)
mobject.shift(x * RIGHT + y * UP)
try: # transform translate
prefix = "translate("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
x, y = string_to_numbers(transform)
mobject.shift(x * RIGHT + y * DOWN)
except:
pass
# TODO, ...
def _handle_translate_transform(self, mobject, op_name, op_args):
if op_name.endswith("X"):
x, y = op_args[0], 0
elif op_name.endswith("Y"):
x, y = 0, op_args[0]
else:
x, y = op_args
mobject.shift(x * RIGHT + y * DOWN)
def _handle_scale_transform(self, mobject, op_name, op_args):
if op_name.endswith("X"):
sx, sy = op_args[0], 1
elif op_name.endswith("Y"):
sx, sy = 1, op_args[0]
elif len(op_args) == 2:
sx, sy = op_args
else:
sx = sy = op_args[0]
if sx < 0:
mobject.flip(UP)
sx = -sx
if sy < 0:
mobject.flip(RIGHT)
sy = -sy
mobject.scale(np.array([sx, sy, 1]), about_point=ORIGIN)
def _handle_rotate_transform(self, mobject, op_name, op_args):
if len(op_args) == 1:
mobject.rotate(op_args[0] * DEGREES, axis=IN, about_point=ORIGIN)
else:
deg, x, y = op_args
mobject.rotate(deg * DEGREES, axis=IN, about_point=np.array([x, y, 0]))
def _handle_skew_transform(self, mobject, op_name, op_args):
rad = op_args[0] * DEGREES
if op_name == "skewX":
tana = np.tan(rad)
self._handle_matrix_transform(mobject, None, [1., 0., tana, 1., 0., 0.])
elif op_name == "skewY":
tana = np.tan(rad)
self._handle_matrix_transform(mobject, None, [1., tana, 0., 1., 0., 0.])
def flatten(self, input_list):
output_list = []
@@ -305,18 +425,22 @@ class SVGMobject(VMobject):
output_list.append(i)
return output_list
def get_all_childNodes_have_id(self, element):
all_childNodes_have_id = []
if not isinstance(element, minidom.Element):
def get_all_childWrappers_have_id(self, wrapper):
all_childWrappers_have_id = []
element = wrapper.etree_element
if not isinstance(element, ElementTree.Element):
return
if element.hasAttribute('id'):
return [element]
for e in element.childNodes:
all_childNodes_have_id.append(self.get_all_childNodes_have_id(e))
return self.flatten([e for e in all_childNodes_have_id if e])
if element.get('id'):
return [wrapper]
for e in wrapper.iter_children():
all_childWrappers_have_id.append(self.get_all_childWrappers_have_id(e))
return self.flatten([e for e in all_childWrappers_have_id if e])
def update_ref_to_element(self, defs):
new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)])
def update_ref_to_element(self, wrapper, style):
new_refs = {
e.etree_element.get('id', ''): (e, style)
for e in self.get_all_childWrappers_have_id(wrapper)
}
self.ref_to_element.update(new_refs)
@@ -374,13 +498,15 @@ class VMobjectFromSVGPathstring(VMobject):
upper_command = command.upper()
if upper_command == "Z":
func() # `close_path` takes no arguments
relative_point = self.get_last_point()
continue
number_types = np.array(list(number_types_str))
n_numbers = len(number_types_str)
number_groups = np.array(string_to_numbers(coord_string)).reshape((-1, n_numbers))
number_list = _PathStringParser(coord_string, number_types_str).args
number_groups = np.array(number_list).reshape((-1, n_numbers))
for numbers in number_groups:
for ind, numbers in enumerate(number_groups):
if command.islower():
# Treat it as a relative command
numbers[number_types == "x"] += relative_point[0]
@@ -396,10 +522,12 @@ class VMobjectFromSVGPathstring(VMobject):
args = list(np.hstack((
numbers.reshape((-1, 2)), np.zeros((n_numbers // 2, 1))
)))
if upper_command == "M" and ind != 0:
# M x1 y1 x2 y2 is equal to M x1 y1 L x2 y2
func, _ = self.command_to_function("L")
func(*args)
relative_point = self.get_last_point()
def add_elliptical_arc_to(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, point):
def close_to_zero(a, threshold=1e-5):
return abs(a) < threshold
@@ -520,9 +648,67 @@ class VMobjectFromSVGPathstring(VMobject):
"S": (self.add_smooth_cubic_curve_to, "xyxy"),
"Q": (self.add_quadratic_bezier_curve_to, "xyxy"),
"T": (self.add_smooth_curve_to, "xy"),
"A": (self.add_elliptical_arc_to, "-----xy"),
"A": (self.add_elliptical_arc_to, "uuaffxy"),
"Z": (self.close_path, ""),
}
def get_original_path_string(self):
return self.path_string
class InvalidPathError(ValueError):
pass
class _PathStringParser:
# modified from https://github.com/regebro/svg.path/
def __init__(self, arguments, rules):
self.args = []
arguments = bytearray(arguments, "ascii")
self._strip_array(arguments)
while arguments:
for rule in rules:
self._rule_to_function_map[rule](arguments)
@property
def _rule_to_function_map(self):
return {
"x": self._get_number,
"y": self._get_number,
"a": self._get_number,
"u": self._get_unsigned_number,
"f": self._get_flag,
}
def _strip_array(self, arg_array):
# wsp: (0x9, 0x20, 0xA, 0xC, 0xD) with comma 0x2C
# https://www.w3.org/TR/SVG/paths.html#PathDataBNF
while arg_array and arg_array[0] in [0x9, 0x20, 0xA, 0xC, 0xD, 0x2C]:
arg_array[0:1] = b""
def _get_number(self, arg_array):
pattern = re.compile(rb"^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?")
res = pattern.search(arg_array)
if not res:
raise InvalidPathError(f"Expected a number, got '{arg_array}'")
number = float(res.group())
self.args.append(number)
arg_array[res.start():res.end()] = b""
self._strip_array(arg_array)
return number
def _get_unsigned_number(self, arg_array):
number = self._get_number(arg_array)
if number < 0:
raise InvalidPathError(f"Expected an unsigned number, got '{number}'")
return number
def _get_flag(self, arg_array):
flag = arg_array[0]
if flag != 48 and flag != 49:
raise InvalidPathError(f"Expected a flag (0/1), got '{chr(flag)}'")
flag -= 48
self.args.append(flag)
arg_array[0:1] = b""
self._strip_array(arg_array)
return flag

View File

@@ -16,7 +16,7 @@ from manimlib.utils.tex_file_writing import display_during_execution
SCALE_FACTOR_PER_FONT_POINT = 0.001
tex_string_to_mob_map = {}
tex_string_with_color_to_mob_map = {}
class SingleStringTex(VMobject):
@@ -35,24 +35,26 @@ class SingleStringTex(VMobject):
super().__init__(**kwargs)
assert(isinstance(tex_string, str))
self.tex_string = tex_string
if tex_string not in tex_string_to_mob_map:
if tex_string not in tex_string_with_color_to_mob_map:
with display_during_execution(f" Writing \"{tex_string}\""):
full_tex = self.get_tex_file_body(tex_string)
filename = tex_to_svg_file(full_tex)
svg_mob = SVGMobject(
filename,
height=None,
color=self.color,
stroke_width=self.stroke_width,
path_string_config={
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
}
)
tex_string_to_mob_map[tex_string] = svg_mob
tex_string_with_color_to_mob_map[(self.color, tex_string)] = svg_mob
self.add(*(
sm.copy()
for sm in tex_string_to_mob_map[tex_string]
for sm in tex_string_with_color_to_mob_map[(self.color, tex_string)]
))
self.init_colors()
self.init_colors(override=False)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)

View File

@@ -3,7 +3,6 @@ import os
import re
import io
import typing
import warnings
import xml.etree.ElementTree as ET
import functools
import pygments
@@ -14,6 +13,7 @@ from contextlib import contextmanager
from pathlib import Path
import manimpango
from manimlib.logger import log
from manimlib.constants import *
from manimlib.mobject.geometry import Dot
from manimlib.mobject.svg.svg_mobject import SVGMobject
@@ -54,10 +54,9 @@ class Text(SVGMobject):
self.full2short(kwargs)
digest_config(self, kwargs)
if self.size:
warnings.warn(
"self.size has been deprecated and will "
log.warning(
"`self.size` has been deprecated and will "
"be removed in future.",
DeprecationWarning
)
self.font_size = self.size
if self.lsh == -1:
@@ -86,6 +85,9 @@ class Text(SVGMobject):
if self.height is None:
self.scale(TEXT_MOB_SCALE_FACTOR)
def init_colors(self, override=True):
super().init_colors(override=override)
def remove_empty_path(self, file_name):
with open(file_name, 'r') as fpr:
content = fpr.read()

View File

@@ -7,7 +7,6 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Line
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import get_norm

View File

@@ -260,7 +260,7 @@ class TexturedSurface(Surface):
super().init_uniforms()
self.uniforms["num_textures"] = self.num_textures
def init_colors(self):
def init_colors(self, override=True):
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
def set_opacity(self, opacity, recurse=True):

View File

@@ -90,7 +90,7 @@ class VMobject(Mobject):
})
# Colors
def init_colors(self):
def init_colors(self, override=True):
self.set_fill(
color=self.fill_color or self.color,
opacity=self.fill_opacity,
@@ -103,6 +103,9 @@ class VMobject(Mobject):
)
self.set_gloss(self.gloss)
self.set_flat_stroke(self.flat_stroke)
if not override:
for submobjects in self.submobjects:
submobjects.init_colors(override=False)
return self
def set_rgba_array(self, rgba_array, name=None, recurse=False):
@@ -382,7 +385,10 @@ class VMobject(Mobject):
def add_smooth_cubic_curve_to(self, handle, point):
self.throw_error_if_no_points()
new_handle = self.get_reflection_of_last_handle()
if self.get_num_points() == 1:
new_handle = self.get_points()[-1]
else:
new_handle = self.get_reflection_of_last_handle()
self.add_cubic_bezier_curve_to(new_handle, handle, point)
def has_new_path_started(self):

View File

@@ -308,7 +308,7 @@ class SceneFileWriter(object):
)
temp_file_path = stem + "_temp" + ext
commands = [
"ffmpeg",
FFMPEG_BIN,
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists

View File

@@ -7,8 +7,6 @@ from manimlib.constants import WHITE
from manimlib.constants import COLORMAP_3B1B
from manimlib.utils.bezier import interpolate
from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.simple_functions import clip_in_place
from manimlib.utils.space_ops import normalize
def color_to_rgb(color):
@@ -105,16 +103,6 @@ def random_color():
return Color(rgb=(random.random() for i in range(3)))
def get_shaded_rgb(rgb, point, unit_normal_vect, light_source):
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
def get_colormap_list(map_name="viridis", n_colors=9):
"""
Options for map_name:

View File

@@ -1,5 +1,28 @@
import yaml
import os
import yaml
import inspect
import importlib
from rich import box
from rich.rule import Rule
from rich.table import Table
from rich.console import Console
from rich.prompt import Prompt, Confirm
def get_manim_dir():
manimlib_module = importlib.import_module("manimlib")
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
return os.path.abspath(os.path.join(manimlib_dir, ".."))
def remove_empty_value(dictionary):
for key in list(dictionary.keys()):
if dictionary[key] == "":
dictionary.pop(key)
elif isinstance(dictionary[key], dict):
remove_empty_value(dictionary[key])
def init_customization():
configuration = {
@@ -24,6 +47,7 @@ def init_customization():
},
"window_position": "UR",
"window_monitor": 0,
"full_screen": False,
"break_into_partial_movies": False,
"camera_qualities": {
"low": {
@@ -46,41 +70,103 @@ def init_customization():
}
}
print("Initialize configuration")
scope = input(" Please select the scope of the configuration [global/local]: ")
if scope == "global":
from manimlib.config import get_manim_dir
file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
else:
file_name = os.path.join(os.getcwd(), "custom_config.yml")
console = Console()
console.print(Rule("[bold]Configuration Guide[/bold]"))
# print("Initialize configuration")
try:
scope = Prompt.ask(
" Select the scope of the configuration",
choices=["global", "local"],
default="local"
)
print("\n directories:")
configuration["directories"]["output"] = input(" [1/8] Where should manim output video and image files place: ")
configuration["directories"]["raster_images"] = input(" [2/8] Which folder should manim find raster images (.jpg .png .gif) in (optional): ")
configuration["directories"]["vector_images"] = input(" [3/8] Which folder should manim find vector images (.svg .xdv) in (optional): ")
configuration["directories"]["sounds"] = input(" [4/8] Which folder should manim find sound files (.mp3 .wav) in (optional): ")
configuration["directories"]["temporary_storage"] = input(" [5/8] Which folder should manim storage temporary files: ")
console.print("[bold]Directories:[/bold]")
dir_config = configuration["directories"]
dir_config["output"] = Prompt.ask(
" Where should manim [bold]output[/bold] video and image files place [prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["raster_images"] = Prompt.ask(
" Which folder should manim find [bold]raster images[/bold] (.jpg .png .gif) in "
"[prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["vector_images"] = Prompt.ask(
" Which folder should manim find [bold]vector images[/bold] (.svg .xdv) in "
"[prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["sounds"] = Prompt.ask(
" Which folder should manim find [bold]sound files[/bold] (.mp3 .wav) in "
"[prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["temporary_storage"] = Prompt.ask(
" Which folder should manim storage [bold]temporary files[/bold] "
"[prompt.default](recommended, use system temporary folder by default)",
default="",
show_default=False
)
print("\n tex:")
tex = input(" [6/8] Which executable file to use to compile [latex/xelatex]: ")
if tex == "latex":
configuration["tex"]["executable"] = "latex"
configuration["tex"]["template_file"] = "tex_template.tex"
configuration["tex"]["intermediate_filetype"] = "dvi"
else:
configuration["tex"]["executable"] = "xelatex -no-pdf"
configuration["tex"]["template_file"] = "ctex_template.tex"
configuration["tex"]["intermediate_filetype"] = "xdv"
console.print("[bold]LaTeX:[/bold]")
tex_config = configuration["tex"]
tex = Prompt.ask(
" Select an executable program to use to compile a LaTeX source file",
choices=["latex", "xelatex"],
default="latex"
)
if tex == "latex":
tex_config["executable"] = "latex"
tex_config["template_file"] = "tex_template.tex"
tex_config["intermediate_filetype"] = "dvi"
else:
tex_config["executable"] = "xelatex -no-pdf"
tex_config["template_file"] = "ctex_template.tex"
tex_config["intermediate_filetype"] = "xdv"
console.print("[bold]Styles:[/bold]")
configuration["style"]["background_color"] = Prompt.ask(
" Which [bold]background color[/bold] do you want [italic](hex code)",
default="#333333"
)
print("\n style:")
configuration["style"]["background_color"] = input(" [7/8] Which background color do you want (hex code): ")
console.print("[bold]Camera qualities:[/bold]")
table = Table(
"low", "medium", "high", "ultra_high",
title="Four defined qualities",
box=box.ROUNDED
)
table.add_row("480p15", "720p30", "1080p60", "2160p60")
console.print(table)
configuration["camera_qualities"]["default_quality"] = Prompt.ask(
" Which one to choose as the default rendering quality",
choices=["low", "medium", "high", "ultra_high"],
default="high"
)
print("\n camera_qualities:")
print(" Four defined qualities: low: 480p15 medium: 720p30 high: 1080p60 ultra_high: 2160p60")
configuration["camera_qualities"]["default_quality"] = input(" [8/8] Which one to choose as the default rendering quality [low/medium/high/ultra_high]: ")
write_to_file = Confirm.ask(
"\n[bold]Are you sure to write these configs to file?[/bold]",
default=True
)
if not write_to_file:
raise KeyboardInterrupt
with open(file_name, 'w', encoding="utf_8") as file:
yaml.dump(configuration, file)
global_file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
if scope == "global":
file_name = global_file_name
else:
if os.path.exists(global_file_name):
remove_empty_value(configuration)
file_name = os.path.join(os.getcwd(), "custom_config.yml")
with open(file_name, "w", encoding="utf-8") as f:
yaml.dump(configuration, f)
console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!")
console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]")
print(f"\nYou have set up a {scope} configuration file")
print(f"You can manually modify it again in: {file_name}\n")
except KeyboardInterrupt:
console.print("\n[green]Exit configuration guide[/green]")

View File

@@ -1,34 +1,16 @@
from functools import reduce
import inspect
import numpy as np
import operator as op
from scipy import special
from functools import lru_cache
def sigmoid(x):
return 1.0 / (1 + np.exp(-x))
CHOOSE_CACHE = {}
def choose_using_cache(n, r):
if n not in CHOOSE_CACHE:
CHOOSE_CACHE[n] = {}
if r not in CHOOSE_CACHE[n]:
CHOOSE_CACHE[n][r] = choose(n, r, use_cache=False)
return CHOOSE_CACHE[n][r]
def choose(n, r, use_cache=True):
if use_cache:
return choose_using_cache(n, r)
if n < r:
return 0
if r == 0:
return 1
denom = reduce(op.mul, range(1, r + 1), 1)
numer = reduce(op.mul, range(n, n - r, -1), 1)
return numer // denom
@lru_cache(maxsize=10)
def choose(n, k):
return special.comb(n, k, exact=True)
def get_num_args(function):
@@ -53,14 +35,6 @@ def clip(a, min_a, max_a):
return a
def clip_in_place(array, min_val=None, max_val=None):
if max_val is not None:
array[array > max_val] = max_val
if min_val is not None:
array[array < min_val] = min_val
return array
def fdiv(a, b, zero_over_zero_value=None):
if zero_over_zero_value is not None:
out = np.full_like(a, zero_over_zero_value)

View File

@@ -1,42 +0,0 @@
import re
import string
def to_camel_case(name):
return "".join([
[c for c in part if c not in string.punctuation + string.whitespace].capitalize()
for part in name.split("_")
])
def initials(name, sep_values=[" ", "_"]):
return "".join([
(s[0] if s else "")
for s in re.split("|".join(sep_values), name)
])
def camel_case_initials(name):
return [c for c in name if c.isupper()]
def complex_string(complex_num):
return [c for c in str(complex_num) if c not in "()"]
def split_string_to_isolate_substrings(full_string, *isolate):
"""
Given a string, and an arbitrary number of possible substrings,
to isolate, this returns a list of strings which would concatenate
to make the full string, and in which these special substrings
appear as their own elements.
For example,split_string_to_isolate_substrings("to be or not to be", "to", "be")
would return ["to", " ", "be", " or not ", "to", " ", "be"]
"""
pattern = "|".join(*(
"({})".format(re.escape(ss))
for ss in isolate
))
pieces = re.split(pattern, full_string)
return list(filter(lambda s: s, pieces))

View File

@@ -20,3 +20,4 @@ validators
ipython
PyOpenGL
manimpango>=0.2.0,<0.4.0
cssselect2

View File

@@ -1,6 +1,6 @@
[metadata]
name = manimgl
version = 1.3.0
version = 1.4.0
author = Grant Sanderson
author_email= grant@3blue1brown.com
description = Animation engine for explanatory math videos
@@ -39,6 +39,7 @@ install_requires =
ipython
PyOpenGL
manimpango>=0.2.0,<0.4.0
cssselect2
[options.entry_points]
console_scripts =