Merge branch 'master' of github.com:3b1b/manim into video-work

This commit is contained in:
Grant Sanderson 2022-09-13 14:08:15 -07:00
commit 88f2ae6d0d
25 changed files with 1645 additions and 739 deletions

View file

@ -12,7 +12,7 @@
Manim is an engine for precise programmatic animations, designed for creating explanatory math videos.
Note, there are two versions of manim. This repository began as a personal project by the author of [3Blue1Brown](https://www.3blue1brown.com/) for the purpose of animating those videos, with video-specific code available [here](https://github.com/3b1b/videos). In 2020 a group of developers forked it into what is now the [community edition](https://github.com/ManimCommunity/manim/), with a goal of being more stable, better tested, quicker to respond to community contributions, and all around friendlier to get started with. See [this page](https://docs.manim.community/en/stable/installation/versions.html?highlight=OpenGL#which-version-to-use) for more details.
Note, there are two versions of manim. This repository began as a personal project by the author of [3Blue1Brown](https://www.3blue1brown.com/) for the purpose of animating those videos, with video-specific code available [here](https://github.com/3b1b/videos). In 2020 a group of developers forked it into what is now the [community edition](https://github.com/ManimCommunity/manim/), with a goal of being more stable, better tested, quicker to respond to community contributions, and all around friendlier to get started with. See [this page](https://docs.manim.community/en/stable/faq/installation.html#different-versions) for more details.
## Installation
> **WARNING:** These instructions are for ManimGL _only_. Trying to use these instructions to install [ManimCommunity/manim](https://github.com/ManimCommunity/manim) or instructions there to install this version will cause problems. You should first decide which version you wish to install, then only follow the instructions for your desired version.

View file

@ -1,6 +1,97 @@
Changelog
=========
Unreleased
----------
Breaking Changes
^^^^^^^^^^^^^^^^
- Added ``InteractiveScene`` (`#1794 <https://github.com/3b1b/manim/pull/1794>`__)
Fixed bugs
^^^^^^^^^^
- Fixed ``ImageMobject`` by overriding ``set_color`` method (`#1791 <https://github.com/3b1b/manim/pull/1791>`__)
- Fixed bug with trying to close window during embed (`#1796 <https://github.com/3b1b/manim/commit/e0f5686d667152582f052021cd62bd2ef8c6b470>`__)
- Fixed animating ``Mobject.restore`` bug (`#1796 <https://github.com/3b1b/manim/commit/62289045cc8e102121cfe4d7739f3c89102046fb>`__)
- Fixed ``InteractiveScene.refresh_selection_highlight`` (`#1802 <https://github.com/3b1b/manim/commit/205116b8cec964b5619416f6e8acf0d8ac7df828>`__)
- Fixed ``VMobject.match_style`` (`#1821 <https://github.com/3b1b/manim/commit/0060a4860c9d6b073a60cd839269c213446bba7b>`__)
New Features
^^^^^^^^^^^^
- Added specific euler angle getters (`#1794 <https://github.com/3b1b/manim/commit/df2d465140e25fee265f602608aebbbaa2898c7e>`__)
- Added start angle option to ``Circle`` (`#1794 <https://github.com/3b1b/manim/commit/217c1d7bb02f23a61722bf7275c40802be808563>`__)
- Added ``Mobject.is_touching`` (`#1794 <https://github.com/3b1b/manim/commit/c1716895c0d9f36e23487322a18963991100bb95>`__)
- Added ``Mobject.get_highlight`` (`#1794 <https://github.com/3b1b/manim/commit/29816fa74c7aa6ca060b63ab4165c89987e58d8b>`__)
- Allowed for saving and loading mobjects from file (`#1794 <https://github.com/3b1b/manim/commit/50f5d20cc379947d7253d841c060dd7c55fa7787>`__)
- Added ``Mobject.get_all_corners`` (`#1794 <https://github.com/3b1b/manim/commit/f636199d9a5d1e87ab861bcb6aebae6c9d96a133>`__)
- Added ``Scene.id_to_mobject`` and ``Scene.ids_to_group`` (`#1794 <https://github.com/3b1b/manim/commit/cb768c26a0bc63e02c3035b4af31ba5cbc2e9dda>`__)
- Added ``Scene.save_mobject`` and ``Scene.load_mobject`` to allow for saving and loading mobjects from file at the Scene level (`#1794 <https://github.com/3b1b/manim/commit/777b6d37783f8592df8a8abc3d62af972bc5a0c6>`__)
- Added ``InteractiveScene`` (`#1794 <https://github.com/3b1b/manim/commit/c3afc84bfeb3a76ea8ede4ec4d9f36df0d4d9a28>`__)
- Added ``VHighlight`` (`#1794 <https://github.com/3b1b/manim/commit/9d5e2b32fa9215219d11a601829126cea40410d1>`__)
- Allowed for sweeping selection (`#1796 <https://github.com/3b1b/manim/commit/4caa03332367631d2fff15afd7e56b15fe8701ee>`__)
- Allowed stretched-resizing (`#1796 <https://github.com/3b1b/manim/commit/b4b72d1b68d0993b96a6af76c4bb6816f77f0f12>`__)
- Added cursor location label (`#1796 <https://github.com/3b1b/manim/commit/b9751e9d06068f27a327b419c52fd3c9d68db2e6>`__)
- Added ``Mobject.deserialize`` (`#1796 <https://github.com/3b1b/manim/commit/4d8698a0e88333f6481c08d1b84b6e44f9dc4543>`__)
- Added undo and redo stacks for scene (`#1796 <https://github.com/3b1b/manim/commit/cf466006faa00fc12dc22f5732dc21ccedaa5a63>`__)
- Added ``Mobject.looks_identical`` (`#1802 <https://github.com/3b1b/manim/commit/c3c5717dde543b172b928b516d80a29bbd12651f>`__)
- Added equality for ``ShaderWrapper`` (`#1802 <https://github.com/3b1b/manim/commit/3ae0a4e81b7790194bcf27142a1deb29fa548b9d>`__)
- Added ``Mobject.get_ancestors`` (`#1802 <https://github.com/3b1b/manim/commit/db884b0a67fcee1ad7009f1869c475015fa886c7>`__)
- Added smarter default radius to ``Polygon.round_corners`` (`#1802 <https://github.com/3b1b/manim/commit/4c1210b3ab1bf66b161f3d00cb859d36068c2fbb>`__)
- Added checkpoints to ``Scene`` (`#1821 <https://github.com/3b1b/manim/commit/1b589e336f8151f2914ff00e8956baea8a95abc5>`__)
- Added ``crosshair`` to ``InteractiveScene`` (`#1821 <https://github.com/3b1b/manim/commit/33ffd4863aaa7ecf950b7044181a8e8e3c643698>`__)
- Added ``SceneState`` (`#1821 <https://github.com/3b1b/manim/commit/75e1cff5792065aa1c7fb3eb02e6ee0fa0e8e18d>`__)
- Added ``time_span`` option to ``Animation`` (`#1821 <https://github.com/3b1b/manim/commit/a6fcfa3b4053b7f68f7b029eae87dbd207d97ad2>`__)
- Added ``Mobject.arrange_to_fit_dim`` (`#1821 <https://github.com/3b1b/manim/commit/a87d3b5f59a64ce5a89ce6e17310bdbf62166157>`__)
- Added ``DecimalNumber.get_tex`` (`#1821 <https://github.com/3b1b/manim/commit/48689c8c7bc0029bf5c1b540c11f647e857d419b>`__)
Refactor
^^^^^^^^
- Updated parent updater status when adding updaters (`#1794 <https://github.com/3b1b/manim/commit/3b847da9eaad7391e779c5dbce63ad9257d8c773>`__)
- Added case for zero vectors on ``angle_between_vectors`` (`#1794 <https://github.com/3b1b/manim/commit/e8ac25903e19cbb2b2c2037c988baafce4ddcbbc>`__)
- Refactored ``Mobject.clear_updaters`` (`#1794 <https://github.com/3b1b/manim/commit/95f56f5e80106443d705c68fa220850ec38daee0>`__)
- Changed the way changing-vs-static mobjects are tracked (more details see `#1794 <https://github.com/3b1b/manim/commit/50565fcd7a43ed13dc532f17515208edf97f64d0>`__)
- Refactored ``Mobject.is_point_touching`` (`#1794 <https://github.com/3b1b/manim/commit/135f68de35712be266a1a85261d6d44234fc0056>`__)
- Refactored ``Mobject.make_movable`` and ``Mobject.set_animating_status`` to recurse over family (`#1794 <https://github.com/3b1b/manim/commit/48390375037f745c9cb82b03d1cb3a1de6c530f3>`__)
- Refactored ``AnimationGroup`` (`#1794 <https://github.com/3b1b/manim/commit/fdeab8ca953b46a902b531febcf132739ca194d4>`__)
- Refactored ``Scene.save_state`` and ``Scene.restore`` (`#1794 <https://github.com/3b1b/manim/commit/97400a5cf26f33ed507ddeeb9b9a7f1a558d4f17>`__)
- Added ``MANIM_COLORS`` (`#1794 <https://github.com/3b1b/manim/commit/5a34ca1fba8b4724eda0caa11b271d74e49f468c>`__)
- Changed default transparent background codec to be prores (`#1794 <https://github.com/3b1b/manim/commit/eae7dbbe6eaf4344374713052aae694e69b62c28>`__)
- Simplified ``Mobject.copy`` (`#1794 <https://github.com/3b1b/manim/commit/1b009a4b035244bd6a0b48bc4dc945fd3b4236ef>`__)
- Refactored ``StringMobject`` and relevant classes (`#1795 <https://github.com/3b1b/manim/pull/1795>`__)
- Updates to copying based on pickle serializing (`#1796 <https://github.com/3b1b/manim/commit/fe3e10acd29a3dd6f8b485c0e36ead819f2d937b>`)
- Removed ``refresh_shader_wrapper_id`` from ``Mobject.become`` (`#1796 <https://github.com/3b1b/manim/commit/1b2460f02a694314897437b9b8755443ed290cc1>`__)
- Refactored ``Scene.embed`` to play nicely with gui interactions (`#1796 <https://github.com/3b1b/manim/commit/c96bdc243e57c17bb75bf12d73ab5bf119cf1464>`__)
- Made ``BlankScene`` inherit from ``InteractiveScene`` (`#1796 <https://github.com/3b1b/manim/commit/2737d9a736885a594dd101ffe07bb82e00069333>`__)
- Updated behavior of -e flag to take in (optional) strings as inputs (`#1796 <https://github.com/3b1b/manim/commit/bb7fa2c8aa68d7c7992517cfde3c7d0e804e13e8>`__)
- Refactor -e flag (`#1796 <https://github.com/3b1b/manim/commit/71c14969dffc8762a43f9646a0c3dc024a51b8df>`__)
- Reverted to original copying scheme (`#1796 <https://github.com/3b1b/manim/commit/59506b89cc73fff3b3736245dd72e61dcebf9a2c>`__)
- Renamed ``Mobject.is_movable`` to ``Mobject.interaction_allowed`` (`#1796 <https://github.com/3b1b/manim/commit/3961005fd708333a3e77856d10e78451faa04075>`__)
- Refreshed static mobjects on undo's and redo's (`#1796 <https://github.com/3b1b/manim/commit/04bca6cafbb1482b8f25cfb34ce83316d8a095c9>`__)
- Factored out event handling (`#1796 <https://github.com/3b1b/manim/commit/754316bf586be5a59839f8bac6fb9fcc47da0efb>`__)
- Removed ``Mobject.interaction_allowed``, in favor of using ``_is_animating`` for multiple purposes (`#1796 <https://github.com/3b1b/manim/commit/f70e91348c8241bcb96470e7881dd92d9d3386d3>`__)
- Moved Command + z and Command + shift + z behavior to Scene (`#1797 <https://github.com/3b1b/manim/commit/0fd8491c515ad23ca308099abe0f39fc38e2dd0e>`__)
- Slight copy refactor (`#1797 <https://github.com/3b1b/manim/commit/902c2c002d6ca03c8080b2bd02ca36f2b8a748b6>`__)
- When scene saves state, have it only copy mobjects which have changed (`#1802 <https://github.com/3b1b/manim/commit/bd2dce08300e5b110c6668bd6763f3918fcdc65e>`__)
- Cleaned up ``Scene.remove`` function (`#1802 <https://github.com/3b1b/manim/commit/6310e2fb6414b01b3fe4be1d4d98525e34356b5e>`__)
- Speed-ups to ``Mobject.copy`` (`#1802 <https://github.com/3b1b/manim/commit/e49e4b8373c13c7a888193aaf61955470acbe5d6>`__)
- Slight speed-up to ``InteractiveScene.gather_selection`` (`#1802 <https://github.com/3b1b/manim/commit/f2b4245c134da577a2854732ec0331768d93ffbe>`__)
- Only leave wait notes in presenter mode (`#1802 <https://github.com/3b1b/manim/commit/42d1f48c60d11caa043d5458e64bfceb31ea203f>`__)
- Refactored ``remove_list_redundancies`` and ``list_update`` (`#1821 <https://github.com/3b1b/manim/commit/b920e7be7b85bc0bb0577e2f71c4320bb97b42d4>`__)
- Match updaters in ``Mobject.become`` (`#1821 <https://github.com/3b1b/manim/commit/0e45b41fea5f22d136f62f4af2e0d892e61a12ce>`__)
- Don't show animation progress bar by default (`#1821 <https://github.com/3b1b/manim/commit/52259af5df619d3f44fbaff4c43402b93d01be2f>`__)
- Handle quitting during scene more gracefully (`#1821 <https://github.com/3b1b/manim/commit/e83ad785caaa1a1456e07b23f207469d335bbc0d>`__)
- Made ``selection_highlight`` refresh with an updater (`#1821 <https://github.com/3b1b/manim/commit/ac08963feff24a1dd2e57f604b44ea0a18ab01f3>`__)
- Refactored ``anims_from_play_args`` to ``prepare_animations`` which deprecating old style ``self.play(mob.method, ...)`` (`#1821 <https://github.com/3b1b/manim/commit/feab79c260498fd7757a304e24c617a4e51ba1df>`__)
- Made presenter mode hold before first play call (`#1821 <https://github.com/3b1b/manim/commit/a9a151d4eff80cc37b9db0fe7117727aac45ba09>`__)
- Update frame on all play calls when skipping animations, so as to provide a rapid preview during scene loading (`#1821 <https://github.com/3b1b/manim/commit/41b811a5e7c03f528d41555217106e62b287ca3b>`__)
- Renamed frame_rate to fps (`#1821 <https://github.com/3b1b/manim/commit/6decb0c32aec21c09007f9a2b91aaa8e642ca848>`__)
- Let default text alignment be decided in default_config (`#1821 <https://github.com/3b1b/manim/commit/83b4aa6b88b6c3defb19f204189681f5afbb219e>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on ``pyperclip`` (`#1794 <https://github.com/3b1b/manim/commit/e579f4c955844fba415b976c313f64d1bb0376d0>`__)
v1.6.1
------

View file

@ -98,6 +98,9 @@ Import line that need to execute when entering interactive mode directly.
- ``font``
Default font of Text
- ``text_alignment``
Default text alignment for LaTeX
- ``background_color``
Default background color
@ -113,6 +116,11 @@ means left(L) / middle(O) / right(R)).
The number of the monitor you want the preview window to pop up on. (default is 0)
``full_screen``
---------------
Whether open the window in full screen. (default is false)
``break_into_partial_movies``
-----------------------------
@ -123,22 +131,27 @@ to form the full scene.
Sometimes video-editing is made easier when working with the broken up scene, which
effectively has cuts at all the places you might want.
``camera_qualities``
--------------------
``camera_resolutions``
----------------------
Export quality
Export resolutions
- ``low``
Low quality (default is 480p15)
Low resolutions (default is 480p)
- ``medium``
Medium quality (default is 720p30)
Medium resolutions (default is 720p)
- ``high``
High quality (default is 1080p30)
High resolutions (default is 1080p)
- ``ultra_high``
Ultra high quality (default is 4K60)
Ultra high resolutions (default is 4K)
- ``default_quality``
Default quality (one of the above four)
- ``default_resolutions``
Default resolutions (one of the above four, default is high)
``fps``
-------
Export frame rate. (default is 30)

View file

@ -32,10 +32,11 @@ Some useful flags
All supported flags
^^^^^^^^^^^^^^^^^^^
========================================================== ====== =================================================================================================================================================================================================
========================================================== ====== =====================================================================================================================================================================================================
flag abbr function
========================================================== ====== =================================================================================================================================================================================================
========================================================== ====== =====================================================================================================================================================================================================
``--help`` ``-h`` Show the help message and exit
``--version`` ``-v`` Display the version of manimgl
``--write_file`` ``-w`` Render the scene as a movie file
``--skip_animations`` ``-s`` Skip to the last frame
``--low_quality`` ``-l`` Render at a low quality (for faster rendering)
@ -45,7 +46,7 @@ flag abbr function
``--full_screen`` ``-f`` Show window in full screen
``--presenter_mode`` ``-p`` Scene will stay paused during wait calls until space bar or right arrow is hit, like a slide show
``--save_pngs`` ``-g`` Save each frame as a png
``--save_as_gif`` ``-i`` Save the video as gif
``--gif`` ``-i`` Save the video as gif
``--transparent`` ``-t`` Render to a movie file with an alpha channel
``--quiet`` ``-q``
``--write_all`` ``-a`` Write all the scenes from a file
@ -54,14 +55,15 @@ flag abbr function
``--config`` Guide for automatic configuration
``--file_name FILE_NAME`` Name for the movie or image file
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
``--embed [EMBED]`` ``-e`` Creates a new file where the line ``self.embed`` is inserted into the Scenes construct method. If a string is passed in, the line will be inserted below the last line of code including that string.
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
``--fps FPS`` Frame rate, as an integer
``--fps FPS`` Frame rate, as an integer
``--color COLOR`` ``-c`` Background color
``--leave_progress_bars`` Leave progress bars displayed in terminal
``--video_dir VIDEO_DIR`` Directory to write video
``--config_file CONFIG_FILE`` Path to the custom configuration file
========================================================== ====== =================================================================================================================================================================================================
``--log-level LOG_LEVEL`` Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL
========================================================== ====== =====================================================================================================================================================================================================
custom_config
--------------

View file

@ -12,6 +12,7 @@ from manimlib.animation.fading import FadeOut
from manimlib.animation.fading import FadeIn
from manimlib.animation.movement import Homotopy
from manimlib.animation.transform import Transform
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import ORIGIN, RIGHT, UP
from manimlib.constants import SMALL_BUFF
from manimlib.constants import TAU

View file

@ -176,9 +176,9 @@ class MoveToTarget(Transform):
class _MethodAnimation(MoveToTarget):
def __init__(self, mobject: Mobject, methods: Callable):
def __init__(self, mobject: Mobject, methods: list[Callable], **kwargs):
self.methods = methods
super().__init__(mobject)
super().__init__(mobject, **kwargs)
class ApplyMethod(Transform):

View file

@ -167,81 +167,95 @@ class TransformMatchingStrings(AnimationGroup):
digest_config(self, kwargs)
assert isinstance(source, StringMobject)
assert isinstance(target, StringMobject)
anims = []
source_indices = list(range(len(source.labels)))
target_indices = list(range(len(target.labels)))
def get_filtered_indices_lists(indices_lists, rest_indices):
def get_matched_indices_lists(*part_items_list):
part_items_list_len = len(part_items_list)
indexed_part_items = sorted(it.chain(*[
[
(substr, items_index, indices_list)
for substr, indices_list in part_items
]
for items_index, part_items in enumerate(part_items_list)
]))
grouped_part_items = [
(substr, [
[indices_lists for _, _, indices_lists in grouper_2]
for _, grouper_2 in it.groupby(
grouper_1, key=lambda t: t[1]
)
])
for substr, grouper_1 in it.groupby(
indexed_part_items, key=lambda t: t[0]
)
]
return [
tuple(indices_lists_list)
for _, indices_lists_list in sorted(filter(
lambda t: t[0] and len(t[1]) == part_items_list_len,
grouped_part_items
), key=lambda t: len(t[0]), reverse=True)
]
def get_filtered_indices_lists(indices_lists, used_indices):
result = []
used = []
for indices_list in indices_lists:
if not indices_list:
continue
if not all(index in rest_indices for index in indices_list):
if not all(
index not in used_indices and index not in used
for index in indices_list
):
continue
result.append(indices_list)
for index in indices_list:
rest_indices.remove(index)
return result
used.extend(indices_list)
return result, used
def add_anims(anim_class, indices_lists_pairs):
for source_indices_lists, target_indices_lists in indices_lists_pairs:
source_indices_lists = get_filtered_indices_lists(
source_indices_lists, source_indices
)
target_indices_lists = get_filtered_indices_lists(
target_indices_lists, target_indices
)
if not source_indices_lists or not target_indices_lists:
continue
anims.append(anim_class(
source.build_parts_from_indices_lists(source_indices_lists),
target.build_parts_from_indices_lists(target_indices_lists),
**kwargs
))
def get_substr_to_indices_lists_map(part_items):
result = {}
for substr, indices_list in part_items:
if substr not in result:
result[substr] = []
result[substr].append(indices_list)
return result
def add_anims_from(anim_class, func):
source_substr_map = get_substr_to_indices_lists_map(func(source))
target_substr_map = get_substr_to_indices_lists_map(func(target))
common_substrings = sorted([
s for s in source_substr_map if s and s in target_substr_map
], key=len, reverse=True)
add_anims(
anim_class,
[
(source_substr_map[substr], target_substr_map[substr])
for substr in common_substrings
]
)
add_anims(
ReplacementTransform,
[
anim_class_items = [
(ReplacementTransform, [
(
source.get_submob_indices_lists_by_selector(k),
target.get_submob_indices_lists_by_selector(v)
)
for k, v in self.key_map.items()
]
)
add_anims_from(
FadeTransformPieces,
StringMobject.get_specified_part_items
)
add_anims_from(
FadeTransformPieces,
StringMobject.get_group_part_items
)
]),
(FadeTransformPieces, get_matched_indices_lists(
source.get_specified_part_items(),
target.get_specified_part_items()
)),
(FadeTransformPieces, get_matched_indices_lists(
source.get_group_part_items(),
target.get_group_part_items()
))
]
rest_source = VGroup(*[source[index] for index in source_indices])
rest_target = VGroup(*[target[index] for index in target_indices])
anims = []
source_used_indices = []
target_used_indices = []
for anim_class, pairs in anim_class_items:
for source_indices_lists, target_indices_lists in pairs:
source_filtered, source_used = get_filtered_indices_lists(
source_indices_lists, source_used_indices
)
target_filtered, target_used = get_filtered_indices_lists(
target_indices_lists, target_used_indices
)
if not source_filtered or not target_filtered:
continue
anims.append(anim_class(
source.build_parts_from_indices_lists(source_filtered),
target.build_parts_from_indices_lists(target_filtered),
**kwargs
))
source_used_indices.extend(source_used)
target_used_indices.extend(target_used)
rest_source = VGroup(*[
submob for index, submob in enumerate(source.submobjects)
if index not in source_used_indices
])
rest_target = VGroup(*[
submob for index, submob in enumerate(target.submobjects)
if index not in target_used_indices
])
if self.transform_mismatches:
anims.append(
ReplacementTransform(rest_source, rest_target, **kwargs)

View file

@ -16,17 +16,9 @@ directories:
# return whatever is at to the TMPDIR environment variable. If you want to
# specify them elsewhere,
temporary_storage: ""
tex:
executable: "latex"
template_file: "tex_template.tex"
intermediate_filetype: "dvi"
text_to_replace: "[tex_expression]"
# For ctex, use the following configuration
# executable: "xelatex -no-pdf"
# template_file: "ctex_template.tex"
# intermediate_filetype: "xdv"
universal_import_line: "from manimlib import *"
style:
tex_template: "default"
font: "Consolas"
text_alignment: "LEFT"
background_color: "#333333"
@ -49,4 +41,4 @@ camera_resolutions:
high: "1920x1080"
4k: "3840x2160"
default_resolution: "high"
fps: 30
fps: 30

View file

@ -8,7 +8,7 @@ import numpy as np
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR
from manimlib.constants import GREY_A, RED, WHITE
from manimlib.constants import MED_SMALL_BUFF
from manimlib.constants import PI, TAU, DEGREES
from manimlib.constants import DEGREES, PI, TAU
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
@ -31,7 +31,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from colour import Color
from typing import Union
from typing import Iterable, Union
ManimColor = Union[str, Color]

View file

@ -2020,7 +2020,9 @@ class _AnimationBuilder:
self.overridden_animation = None
self.mobject.generate_target()
self.is_chaining = False
self.methods = []
self.methods: list[Callable] = []
self.anim_args = {}
self.can_pass_args = True
def __getattr__(self, method_name: str):
method = getattr(self.mobject.target, method_name)
@ -2045,13 +2047,40 @@ class _AnimationBuilder:
self.is_chaining = True
return update_target
def __call__(self, **kwargs):
return self.set_anim_args(**kwargs)
def set_anim_args(self, **kwargs):
'''
You can change the args of :class:`~manimlib.animation.transform.Transform`, such as
- ``run_time``
- ``time_span``
- ``rate_func``
- ``lag_ratio``
- ``path_arc``
- ``path_func``
and so on.
'''
if not self.can_pass_args:
raise ValueError(
"Animation arguments can only be passed by calling ``animate`` "
"or ``set_anim_args`` and can only be passed once",
)
self.anim_args = kwargs
self.can_pass_args = False
return self
def build(self):
from manimlib.animation.transform import _MethodAnimation
if self.overridden_animation:
return self.overridden_animation
return _MethodAnimation(self.mobject, self.methods)
return _MethodAnimation(self.mobject, self.methods, **self.anim_args)
def override_animate(method):

View file

@ -1,5 +1,7 @@
from __future__ import annotations
import numpy as np
from manimlib.constants import BLUE, BLUE_E, GREEN_E, GREY_B, GREY_D, MAROON_B, YELLOW
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF

View file

@ -1,9 +1,10 @@
from __future__ import annotations
import re
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.utils.tex_file_writing import display_during_execution
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from typing import TYPE_CHECKING
@ -37,6 +38,8 @@ class MTex(StringMobject):
"alignment": "\\centering",
"tex_environment": "align*",
"tex_to_color_map": {},
"template": "",
"additional_preamble": "",
}
def __init__(self, tex_string: str, **kwargs):
@ -57,77 +60,112 @@ class MTex(StringMobject):
self.path_string_config,
self.base_color,
self.isolate,
self.protect,
self.tex_string,
self.alignment,
self.tex_environment,
self.tex_to_color_map
self.tex_to_color_map,
self.template,
self.additional_preamble
)
def get_file_path_by_content(self, content: str) -> str:
tex_config = get_tex_config()
full_tex = tex_config["tex_body"].replace(
tex_config["text_to_replace"],
content
)
with display_during_execution(f"Writing \"{self.string}\""):
file_path = tex_to_svg_file(full_tex)
with display_during_execution(f"Writing \"{self.tex_string}\""):
file_path = tex_content_to_svg_file(
content, self.template, self.additional_preamble
)
return file_path
# Parsing
def get_cmd_spans(self) -> list[Span]:
return self.find_spans(r"\\(?:[a-zA-Z]+|\s|\S)|[_^{}]")
def get_substr_flag(self, substr: str) -> int:
return {"{": 1, "}": -1}.get(substr, 0)
def get_repl_substr_for_content(self, substr: str) -> str:
return substr
def get_repl_substr_for_matching(self, substr: str) -> str:
return substr if substr.startswith("\\") else ""
def get_specified_items(
self, cmd_span_pairs: list[tuple[Span, Span]]
) -> list[tuple[Span, dict[str, str]]]:
cmd_content_spans = [
(span_begin, span_end)
for (_, span_begin), (span_end, _) in cmd_span_pairs
]
specified_spans = [
*[
cmd_content_spans[range_begin]
for _, (range_begin, range_end) in self.compress_neighbours([
(span_begin + index, span_end - index)
for index, (span_begin, span_end) in enumerate(
cmd_content_spans
)
])
if range_end - range_begin >= 2
],
*[
span
for selector in self.tex_to_color_map
for span in self.find_spans_by_selector(selector)
],
*self.find_spans_by_selector(self.isolate)
]
return [(span, {}) for span in specified_spans]
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
# Lump together adjacent brace pairs
pattern = re.compile(r"""
(?P<command>\\(?:[a-zA-Z]+|.))
|(?P<open>{+)
|(?P<close>}+)
""", flags=re.X | re.S)
result = []
open_stack = []
for match_obj in pattern.finditer(string):
if match_obj.group("open"):
open_stack.append((match_obj.span(), len(result)))
elif match_obj.group("close"):
close_start, close_end = match_obj.span()
while True:
if not open_stack:
raise ValueError("Missing '{' inserted")
(open_start, open_end), index = open_stack.pop()
n = min(open_end - open_start, close_end - close_start)
result.insert(index, pattern.fullmatch(
string, pos=open_end - n, endpos=open_end
))
result.append(pattern.fullmatch(
string, pos=close_start, endpos=close_start + n
))
close_start += n
if close_start < close_end:
continue
open_end -= n
if open_start < open_end:
open_stack.append(((open_start, open_end), index))
break
else:
result.append(match_obj)
if open_stack:
raise ValueError("Missing '}' inserted")
return result
@staticmethod
def get_color_cmd_str(rgb_hex: str) -> str:
def get_command_flag(match_obj: re.Match) -> int:
if match_obj.group("open"):
return 1
if match_obj.group("close"):
return -1
return 0
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
return match_obj.group()
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("command"):
return match_obj.group()
return ""
@staticmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match
) -> dict[str, str] | None:
if len(open_command.group()) >= 2:
return {}
return None
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [
(span, {})
for selector in self.tex_to_color_map
for span in self.find_spans_by_selector(selector)
]
@staticmethod
def get_color_command(rgb_hex: str) -> str:
rgb = MTex.hex_to_int(rgb_hex)
rg, b = divmod(rgb, 256)
r, g = divmod(rg, 256)
return f"\\color[RGB]{{{r}, {g}, {b}}}"
@staticmethod
def get_cmd_str_pair(
attr_dict: dict[str, str], label_hex: str | None
) -> tuple[str, str]:
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
if label_hex is None:
return "", ""
return "{{" + MTex.get_color_cmd_str(label_hex), "}}"
return ""
if is_end:
return "}}"
return "{{" + MTex.get_color_command(label_hex)
def get_content_prefix_and_suffix(
self, is_labelled: bool
@ -135,17 +173,14 @@ class MTex(StringMobject):
prefix_lines = []
suffix_lines = []
if not is_labelled:
prefix_lines.append(self.get_color_cmd_str(self.base_color_hex))
prefix_lines.append(self.get_color_command(
self.color_to_hex(self.base_color)
))
if self.alignment:
prefix_lines.append(self.alignment)
if self.tex_environment:
if isinstance(self.tex_environment, str):
env_prefix = f"\\begin{{{self.tex_environment}}}"
env_suffix = f"\\end{{{self.tex_environment}}}"
else:
env_prefix, env_suffix = self.tex_environment
prefix_lines.append(env_prefix)
suffix_lines.append(env_suffix)
prefix_lines.append(f"\\begin{{{self.tex_environment}}}")
suffix_lines.append(f"\\end{{{self.tex_environment}}}")
return (
"".join([line + "\n" for line in prefix_lines]),
"".join(["\n" + line for line in suffix_lines])
@ -156,8 +191,8 @@ class MTex(StringMobject):
def get_parts_by_tex(self, selector: Selector) -> VGroup:
return self.select_parts(selector)
def get_part_by_tex(self, selector: Selector) -> VGroup:
return self.select_part(selector)
def get_part_by_tex(self, selector: Selector, **kwargs) -> VGroup:
return self.select_part(selector, **kwargs)
def set_color_by_tex(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color)

View file

@ -18,7 +18,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from colour import Color
from typing import Iterable, Sequence, TypeVar, Union
from typing import Callable, Iterable, Union
ManimColor = Union[str, Color]
Span = tuple[int, int]
@ -32,7 +32,6 @@ if TYPE_CHECKING:
tuple[Union[int, None], Union[int, None]]
]]
]
T = TypeVar("T")
class StringMobject(SVGMobject, ABC):
@ -47,7 +46,7 @@ class StringMobject(SVGMobject, ABC):
if they want to do anything with their corresponding submobjects.
`isolate` parameter can be either a string, a `re.Pattern` object,
or a 2-tuple containing integers or None, or a collection of the above.
Note, substrings specified cannot *partially* overlap with each other.
Note, substrings specified cannot *partly* overlap with each other.
Each instance of `StringMobject` generates 2 svg files.
The additional one is generated with some color commands inserted,
@ -64,6 +63,7 @@ class StringMobject(SVGMobject, ABC):
},
"base_color": WHITE,
"isolate": (),
"protect": (),
}
def __init__(self, string: str, **kwargs):
@ -71,9 +71,7 @@ class StringMobject(SVGMobject, ABC):
digest_config(self, kwargs)
if self.base_color is None:
self.base_color = WHITE
self.base_color_hex = self.color_to_hex(self.base_color)
self.full_span = (0, len(self.string))
self.parse()
super().__init__(**kwargs)
self.labels = [submob.label for submob in self.submobjects]
@ -90,9 +88,9 @@ class StringMobject(SVGMobject, ABC):
super().generate_mobject()
labels_count = len(self.labelled_spans)
if not labels_count:
if labels_count == 1:
for submob in self.submobjects:
submob.label = -1
submob.label = 0
return
labelled_content = self.get_content(is_labelled=True)
@ -104,7 +102,7 @@ class StringMobject(SVGMobject, ABC):
"to the original svg. Skip the labelling process."
)
for submob in self.submobjects:
submob.label = -1
submob.label = 0
return
self.rearrange_submobjects_by_positions(labelled_svg)
@ -112,18 +110,21 @@ class StringMobject(SVGMobject, ABC):
for submob, labelled_svg_submob in zip(
self.submobjects, labelled_svg.submobjects
):
color_int = self.hex_to_int(self.color_to_hex(
label = self.hex_to_int(self.color_to_hex(
labelled_svg_submob.get_fill_color()
))
if color_int > labels_count:
unrecognizable_colors.append(color_int)
color_int = 0
submob.label = color_int - 1
if label >= labels_count:
unrecognizable_colors.append(label)
label = 0
submob.label = label
if unrecognizable_colors:
log.warning(
"Unrecognizable color labels detected (%s, etc). "
"Unrecognizable color labels detected (%s). "
"The result could be unexpected.",
self.int_to_hex(unrecognizable_colors[0])
", ".join(
self.int_to_hex(color)
for color in unrecognizable_colors
)
)
def rearrange_submobjects_by_positions(
@ -153,30 +154,27 @@ class StringMobject(SVGMobject, ABC):
# Toolkits
def get_substr(self, span: Span) -> str:
return self.string[slice(*span)]
def find_spans(self, pattern: str | re.Pattern) -> list[Span]:
return [
match_obj.span()
for match_obj in re.finditer(pattern, self.string)
]
def find_spans_by_selector(self, selector: Selector) -> list[Span]:
def find_spans_by_single_selector(sel):
if isinstance(sel, str):
return self.find_spans(re.escape(sel))
return [
match_obj.span()
for match_obj in re.finditer(re.escape(sel), self.string)
]
if isinstance(sel, re.Pattern):
return self.find_spans(sel)
return [
match_obj.span()
for match_obj in sel.finditer(self.string)
]
if isinstance(sel, tuple) and len(sel) == 2 and all(
isinstance(index, int) or index is None
for index in sel
):
l = self.full_span[1]
l = len(self.string)
span = tuple(
default_index if index is None else
min(index, l) if index >= 0 else max(index + l, 0)
if index is not None else default_index
for index, default_index in zip(sel, self.full_span)
for index, default_index in zip(sel, (0, l))
)
return [span]
return None
@ -189,57 +187,12 @@ class StringMobject(SVGMobject, ABC):
if spans is None:
raise TypeError(f"Invalid selector: '{sel}'")
result.extend(spans)
return result
@staticmethod
def get_neighbouring_pairs(vals: Sequence[T]) -> list[tuple[T, T]]:
return list(zip(vals[:-1], vals[1:]))
@staticmethod
def compress_neighbours(vals: Sequence[T]) -> list[tuple[T, Span]]:
if not vals:
return []
unique_vals = [vals[0]]
indices = [0]
for index, val in enumerate(vals):
if val == unique_vals[-1]:
continue
unique_vals.append(val)
indices.append(index)
indices.append(len(vals))
val_ranges = StringMobject.get_neighbouring_pairs(indices)
return list(zip(unique_vals, val_ranges))
return list(filter(lambda span: span[0] <= span[1], result))
@staticmethod
def span_contains(span_0: Span, span_1: Span) -> bool:
return span_0[0] <= span_1[0] and span_0[1] >= span_1[1]
@staticmethod
def get_complement_spans(
universal_span: Span, interval_spans: list[Span]
) -> list[Span]:
if not interval_spans:
return [universal_span]
span_ends, span_begins = zip(*interval_spans)
return list(zip(
(universal_span[0], *span_begins),
(*span_ends, universal_span[1])
))
def replace_substr(self, span: Span, repl_items: list[Span, str]):
if not repl_items:
return self.get_substr(span)
repl_spans, repl_strs = zip(*sorted(repl_items, key=lambda t: t[0]))
pieces = [
self.get_substr(piece_span)
for piece_span in self.get_complement_spans(span, repl_spans)
]
repl_strs = [*repl_strs, ""]
return "".join(it.chain(*zip(pieces, repl_strs)))
@staticmethod
def color_to_hex(color: ManimColor) -> str:
return rgb_to_hex(color_to_rgb(color))
@ -255,131 +208,220 @@ class StringMobject(SVGMobject, ABC):
# Parsing
def parse(self) -> None:
cmd_spans = self.get_cmd_spans()
cmd_substrs = [self.get_substr(span) for span in cmd_spans]
flags = [self.get_substr_flag(substr) for substr in cmd_substrs]
specified_items = self.get_specified_items(
self.get_cmd_span_pairs(cmd_spans, flags)
)
split_items = [
(span, attr_dict)
for specified_span, attr_dict in specified_items
for span in self.split_span_by_levels(
specified_span, cmd_spans, flags
def get_substr(span: Span) -> str:
return self.string[slice(*span)]
configured_items = self.get_configured_items()
isolated_spans = self.find_spans_by_selector(self.isolate)
protected_spans = self.find_spans_by_selector(self.protect)
command_matches = self.get_command_matches(self.string)
def get_key(category, i, flag):
def get_span_by_category(category, i):
if category == 0:
return configured_items[i][0]
if category == 1:
return isolated_spans[i]
if category == 2:
return protected_spans[i]
return command_matches[i].span()
index, paired_index = get_span_by_category(category, i)[::flag]
return (
index,
flag * (2 if index != paired_index else -1),
-paired_index,
flag * category,
flag * i
)
]
self.specified_spans = [span for span, _ in specified_items]
self.split_items = split_items
self.labelled_spans = [span for span, _ in split_items]
self.cmd_repl_items_for_content = [
(span, self.get_repl_substr_for_content(substr))
for span, substr in zip(cmd_spans, cmd_substrs)
]
self.cmd_repl_items_for_matching = [
(span, self.get_repl_substr_for_matching(substr))
for span, substr in zip(cmd_spans, cmd_substrs)
]
self.check_overlapping()
index_items = sorted([
(category, i, flag)
for category, item_length in enumerate((
len(configured_items),
len(isolated_spans),
len(protected_spans),
len(command_matches)
))
for i in range(item_length)
for flag in (1, -1)
], key=lambda t: get_key(*t))
inserted_items = []
labelled_items = []
overlapping_spans = []
level_mismatched_spans = []
label = 1
protect_level = 0
bracket_stack = [0]
bracket_count = 0
open_command_stack = []
open_stack = []
for category, i, flag in index_items:
if category >= 2:
protect_level += flag
if flag == 1 or category == 2:
continue
inserted_items.append((i, 0))
command_match = command_matches[i]
command_flag = self.get_command_flag(command_match)
if command_flag == 1:
bracket_count += 1
bracket_stack.append(bracket_count)
open_command_stack.append((len(inserted_items), i))
continue
if command_flag == 0:
continue
pos, i_ = open_command_stack.pop()
bracket_stack.pop()
open_command_match = command_matches[i_]
attr_dict = self.get_attr_dict_from_command_pair(
open_command_match, command_match
)
if attr_dict is None:
continue
span = (open_command_match.end(), command_match.start())
labelled_items.append((span, attr_dict))
inserted_items.insert(pos, (label, 1))
inserted_items.insert(-1, (label, -1))
label += 1
continue
if flag == 1:
open_stack.append((
len(inserted_items), category, i,
protect_level, bracket_stack.copy()
))
continue
span, attr_dict = configured_items[i] \
if category == 0 else (isolated_spans[i], {})
pos, category_, i_, protect_level_, bracket_stack_ \
= open_stack.pop()
if category_ != category or i_ != i:
overlapping_spans.append(span)
continue
if protect_level_ or protect_level:
continue
if bracket_stack_ != bracket_stack:
level_mismatched_spans.append(span)
continue
labelled_items.append((span, attr_dict))
inserted_items.insert(pos, (label, 1))
inserted_items.append((label, -1))
label += 1
labelled_items.insert(0, ((0, len(self.string)), {}))
inserted_items.insert(0, (0, 1))
inserted_items.append((0, -1))
if overlapping_spans:
log.warning(
"Partly overlapping substrings detected: %s",
", ".join(
f"'{get_substr(span)}'"
for span in overlapping_spans
)
)
if level_mismatched_spans:
log.warning(
"Cannot handle substrings: %s",
", ".join(
f"'{get_substr(span)}'"
for span in level_mismatched_spans
)
)
def reconstruct_string(
start_item: tuple[int, int],
end_item: tuple[int, int],
command_replace_func: Callable[[re.Match], str],
command_insert_func: Callable[[int, int, dict[str, str]], str]
) -> str:
def get_edge_item(i: int, flag: int) -> tuple[Span, str]:
if flag == 0:
match_obj = command_matches[i]
return (
match_obj.span(),
command_replace_func(match_obj)
)
span, attr_dict = labelled_items[i]
index = span[flag < 0]
return (
(index, index),
command_insert_func(i, flag, attr_dict)
)
items = [
get_edge_item(i, flag)
for i, flag in inserted_items[slice(
inserted_items.index(start_item),
inserted_items.index(end_item) + 1
)]
]
pieces = [
get_substr((start, end))
for start, end in zip(
[interval_end for (_, interval_end), _ in items[:-1]],
[interval_start for (interval_start, _), _ in items[1:]]
)
]
interval_pieces = [piece for _, piece in items[1:-1]]
return "".join(it.chain(*zip(pieces, (*interval_pieces, ""))))
self.labelled_spans = [span for span, _ in labelled_items]
self.reconstruct_string = reconstruct_string
def get_content(self, is_labelled: bool) -> str:
content = self.reconstruct_string(
(0, 1), (0, -1),
self.replace_for_content,
lambda label, flag, attr_dict: self.get_command_string(
attr_dict,
is_end=flag < 0,
label_hex=self.int_to_hex(label) if is_labelled else None
)
)
prefix, suffix = self.get_content_prefix_and_suffix(
is_labelled=is_labelled
)
return "".join((prefix, content, suffix))
@staticmethod
@abstractmethod
def get_cmd_spans(self) -> list[Span]:
def get_command_matches(string: str) -> list[re.Match]:
return []
@staticmethod
@abstractmethod
def get_substr_flag(self, substr: str) -> int:
def get_command_flag(match_obj: re.Match) -> int:
return 0
@staticmethod
@abstractmethod
def get_repl_substr_for_content(self, substr: str) -> str:
return ""
@abstractmethod
def get_repl_substr_for_matching(self, substr: str) -> str:
def replace_for_content(match_obj: re.Match) -> str:
return ""
@staticmethod
def get_cmd_span_pairs(
cmd_spans: list[Span], flags: list[int]
) -> list[tuple[Span, Span]]:
result = []
begin_cmd_spans_stack = []
for cmd_span, flag in zip(cmd_spans, flags):
if flag == 1:
begin_cmd_spans_stack.append(cmd_span)
elif flag == -1:
if not begin_cmd_spans_stack:
raise ValueError("Missing open command")
begin_cmd_span = begin_cmd_spans_stack.pop()
result.append((begin_cmd_span, cmd_span))
if begin_cmd_spans_stack:
raise ValueError("Missing close command")
return result
@abstractmethod
def replace_for_matching(match_obj: re.Match) -> str:
return ""
@staticmethod
@abstractmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match,
) -> dict[str, str] | None:
return None
@abstractmethod
def get_specified_items(
self, cmd_span_pairs: list[tuple[Span, Span]]
) -> list[tuple[Span, dict[str, str]]]:
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return []
def split_span_by_levels(
self, arbitrary_span: Span, cmd_spans: list[Span], flags: list[int]
) -> list[Span]:
cmd_range = (
sum([
arbitrary_span[0] > interval_begin
for interval_begin, _ in cmd_spans
]),
sum([
arbitrary_span[1] >= interval_end
for _, interval_end in cmd_spans
])
)
complement_spans = self.get_complement_spans(
self.full_span, cmd_spans
)
adjusted_span = (
max(arbitrary_span[0], complement_spans[cmd_range[0]][0]),
min(arbitrary_span[1], complement_spans[cmd_range[1]][1])
)
if adjusted_span[0] > adjusted_span[1]:
return []
upward_cmd_spans = []
downward_cmd_spans = []
for cmd_span, flag in list(zip(cmd_spans, flags))[slice(*cmd_range)]:
if flag == 1:
upward_cmd_spans.append(cmd_span)
elif flag == -1:
if upward_cmd_spans:
upward_cmd_spans.pop()
else:
downward_cmd_spans.append(cmd_span)
return list(filter(
lambda span: self.get_substr(span).strip(),
self.get_complement_spans(
adjusted_span, downward_cmd_spans + upward_cmd_spans
)
))
def check_overlapping(self) -> None:
labelled_spans = self.labelled_spans
if len(labelled_spans) >= 16777216:
raise ValueError("Cannot handle that many substrings")
for span_0, span_1 in it.product(labelled_spans, repeat=2):
if not span_0[0] < span_1[0] < span_0[1] < span_1[1]:
continue
raise ValueError(
"Partially overlapping substrings detected: "
f"'{self.get_substr(span_0)}' and '{self.get_substr(span_1)}'"
)
@staticmethod
@abstractmethod
def get_cmd_str_pair(
attr_dict: dict[str, str], label_hex: str | None
) -> tuple[str, str]:
return "", ""
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
return ""
@abstractmethod
def get_content_prefix_and_suffix(
@ -387,38 +429,6 @@ class StringMobject(SVGMobject, ABC):
) -> tuple[str, str]:
return "", ""
def get_content(self, is_labelled: bool) -> str:
inserted_str_pairs = [
(span, self.get_cmd_str_pair(
attr_dict,
label_hex=self.int_to_hex(label + 1) if is_labelled else None
))
for label, (span, attr_dict) in enumerate(self.split_items)
]
inserted_str_items = sorted([
(index, s)
for (index, _), s in [
*sorted([
(span[::-1], end_str)
for span, (_, end_str) in reversed(inserted_str_pairs)
], key=lambda t: (t[0][0], -t[0][1])),
*sorted([
(span, begin_str)
for span, (begin_str, _) in inserted_str_pairs
], key=lambda t: (t[0][0], -t[0][1]))
]
], key=lambda t: t[0])
repl_items = self.cmd_repl_items_for_content + [
((index, index), inserted_str)
for index, inserted_str in inserted_str_items
]
prefix, suffix = self.get_content_prefix_and_suffix(is_labelled)
return "".join([
prefix,
self.replace_substr(self.full_span, repl_items),
suffix
])
# Selector
def get_submob_indices_list_by_span(
@ -427,59 +437,69 @@ class StringMobject(SVGMobject, ABC):
return [
submob_index
for submob_index, label in enumerate(self.labels)
if label != -1 and self.span_contains(
arbitrary_span, self.labelled_spans[label]
)
if self.span_contains(arbitrary_span, self.labelled_spans[label])
]
def get_specified_part_items(self) -> list[tuple[str, list[int]]]:
return [
(
self.get_substr(span),
self.string[slice(*span)],
self.get_submob_indices_list_by_span(span)
)
for span in self.specified_spans
for span in self.labelled_spans[1:]
]
def get_group_part_items(self) -> list[tuple[str, list[int]]]:
if not self.labels:
return []
group_labels, labelled_submob_ranges = zip(
*self.compress_neighbours(self.labels)
)
ordered_spans = [
self.labelled_spans[label] if label != -1 else self.full_span
for label in group_labels
]
interval_spans = [
(
next_span[0]
if self.span_contains(prev_span, next_span)
else prev_span[1],
prev_span[1]
if self.span_contains(next_span, prev_span)
else next_span[0]
)
for prev_span, next_span in self.get_neighbouring_pairs(
ordered_spans
)
]
group_substrs = [
re.sub(r"\s+", "", self.replace_substr(
span, [
(cmd_span, repl_str)
for cmd_span, repl_str in self.cmd_repl_items_for_matching
if self.span_contains(span, cmd_span)
]
))
for span in self.get_complement_spans(
(ordered_spans[0][0], ordered_spans[-1][1]), interval_spans
)
]
def get_neighbouring_pairs(vals):
return list(zip(vals[:-1], vals[1:]))
range_lens, group_labels = zip(*(
(len(list(grouper)), val)
for val, grouper in it.groupby(self.labels)
))
submob_indices_lists = [
list(range(*submob_range))
for submob_range in labelled_submob_ranges
for submob_range in get_neighbouring_pairs(
[0, *it.accumulate(range_lens)]
)
]
labelled_spans = self.labelled_spans
start_items = [
(group_labels[0], 1),
*(
(curr_label, 1)
if self.span_contains(
labelled_spans[prev_label], labelled_spans[curr_label]
)
else (prev_label, -1)
for prev_label, curr_label in get_neighbouring_pairs(
group_labels
)
)
]
end_items = [
*(
(curr_label, -1)
if self.span_contains(
labelled_spans[next_label], labelled_spans[curr_label]
)
else (next_label, 1)
for curr_label, next_label in get_neighbouring_pairs(
group_labels
)
),
(group_labels[-1], -1)
]
group_substrs = [
re.sub(r"\s+", "", self.reconstruct_string(
start_item, end_item,
self.replace_for_matching,
lambda label, flag, attr_dict: ""
))
for start_item, end_item in zip(start_items, end_items)
]
return list(zip(group_substrs, submob_indices_lists))
@ -497,13 +517,13 @@ class StringMobject(SVGMobject, ABC):
def build_parts_from_indices_lists(
self, indices_lists: list[list[int]]
) -> VGroup:
return VGroup(*[
VGroup(*[
return VGroup(*(
VGroup(*(
self.submobjects[submob_index]
for submob_index in indices_list
])
))
for indices_list in indices_lists
])
))
def build_groups(self) -> VGroup:
return self.build_parts_from_indices_lists([

View file

@ -1,6 +1,5 @@
from __future__ import annotations
import hashlib
import os
from xml.etree import ElementTree as ET
@ -19,6 +18,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.directories import get_mobject_data_dir
from manimlib.utils.images import get_full_vector_image_path
from manimlib.utils.iterables import hash_obj
from manimlib.utils.simple_functions import hash_string
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
@ -106,7 +106,7 @@ class SVGMobject(VMobject):
return get_full_vector_image_path(self.file_name)
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
config_style_dict = self.generate_config_style_dict()
config_style_attrs = self.generate_config_style_dict()
style_keys = (
"fill",
"fill-opacity",
@ -116,14 +116,17 @@ class SVGMobject(VMobject):
"style"
)
root = element_tree.getroot()
root_style_dict = {
k: v for k, v in root.attrib.items()
style_attrs = {
k: v
for k, v in root.attrib.items()
if k in style_keys
}
new_root = ET.Element("svg", {})
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
# Ignore other attributes in case that svgelements cannot parse them
SVG_XMLNS = "{http://www.w3.org/2000/svg}"
new_root = ET.Element("svg")
config_style_node = ET.SubElement(new_root, f"{SVG_XMLNS}g", config_style_attrs)
root_style_node = ET.SubElement(config_style_node, f"{SVG_XMLNS}g", style_attrs)
root_style_node.extend(root)
return ET.ElementTree(new_root)
@ -147,7 +150,7 @@ class SVGMobject(VMobject):
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
result = []
for shape in svg.elements():
if isinstance(shape, se.Group):
if isinstance(shape, (se.Group, se.Use)):
continue
elif isinstance(shape, se.Path):
mob = self.path_to_mobject(shape)
@ -155,9 +158,7 @@ class SVGMobject(VMobject):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, se.Circle):
mob = self.circle_to_mobject(shape)
elif isinstance(shape, se.Ellipse):
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
@ -168,11 +169,12 @@ class SVGMobject(VMobject):
elif type(shape) == se.SVGElement:
continue
else:
log.warning(f"Unsupported element type: {type(shape)}")
log.warning("Unsupported element type: %s", type(shape))
continue
if not mob.has_points():
continue
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.GraphicObject):
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
result.append(mob)
@ -203,21 +205,10 @@ class SVGMobject(VMobject):
)
return mob
@staticmethod
def handle_transform(mob, matrix):
mat = np.array([
[matrix.a, matrix.c],
[matrix.b, matrix.d]
])
vec = np.array([matrix.e, matrix.f, 0.0])
mob.apply_matrix(mat)
mob.shift(vec)
return mob
def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
return VMobjectFromSVGPath(path, **self.path_string_config)
def line_to_mobject(self, line: se.Line) -> Line:
def line_to_mobject(self, line: se.SimpleLine) -> Line:
return Line(
start=_convert_point_to_3d(line.x1, line.y1),
end=_convert_point_to_3d(line.x2, line.y2)
@ -242,15 +233,7 @@ class SVGMobject(VMobject):
))
return mob
def circle_to_mobject(self, circle: se.Circle) -> Circle:
# svgelements supports `rx` & `ry` but `r`
mob = Circle(radius=circle.rx)
mob.shift(_convert_point_to_3d(
circle.cx, circle.cy
))
return mob
def ellipse_to_mobject(self, ellipse: se.Ellipse) -> Circle:
def ellipse_to_mobject(self, ellipse: se.Circle | se.Ellipse) -> Circle:
mob = Circle(radius=ellipse.rx)
mob.stretch_to_fit_height(2 * ellipse.ry)
mob.shift(_convert_point_to_3d(
@ -302,8 +285,7 @@ class VMobjectFromSVGPath(VMobject):
# will be saved to a file so that future calls for the same path
# don't need to retrace the same computation.
path_string = self.path_obj.d()
hasher = hashlib.sha256(path_string.encode())
path_hash = hasher.hexdigest()[:16]
path_hash = hash_string(path_string)
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")

View file

@ -13,8 +13,7 @@ from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.config_ops import digest_config
from manimlib.utils.tex_file_writing import display_during_execution
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from typing import TYPE_CHECKING
@ -44,6 +43,8 @@ class SingleStringTex(SVGMobject):
"alignment": "\\centering",
"math_mode": True,
"organize_left_to_right": False,
"template": "",
"additional_preamble": "",
}
def __init__(self, tex_string: str, **kwargs):
@ -64,27 +65,24 @@ class SingleStringTex(SVGMobject):
self.path_string_config,
self.tex_string,
self.alignment,
self.math_mode
self.math_mode,
self.template,
self.additional_preamble
)
def get_file_path(self) -> str:
full_tex = self.get_tex_file_body(self.tex_string)
content = self.get_tex_file_body(self.tex_string)
with display_during_execution(f"Writing \"{self.tex_string}\""):
file_path = tex_to_svg_file(full_tex)
file_path = tex_content_to_svg_file(
content, self.template, self.additional_preamble
)
return file_path
def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string)
if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
new_tex = self.alignment + "\n" + new_tex
tex_config = get_tex_config()
return tex_config["tex_body"].replace(
tex_config["text_to_replace"],
new_tex
)
return self.alignment + "\n" + new_tex
def get_modified_expression(self, tex_string: str) -> str:
return self.modify_special_strings(tex_string.strip())

View file

@ -18,7 +18,7 @@ from manimlib.utils.config_ops import digest_config
from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_downloads_dir
from manimlib.utils.directories import get_text_dir
from manimlib.utils.tex_file_writing import tex_hash
from manimlib.utils.simple_functions import hash_string
from typing import TYPE_CHECKING
@ -63,7 +63,6 @@ class _Alignment:
class MarkupText(StringMobject):
CONFIG = {
"is_markup": True,
"font_size": 48,
"lsh": None,
"justify": False,
@ -81,21 +80,11 @@ class MarkupText(StringMobject):
"t2w": {},
"global_config": {},
"local_configs": {},
# For backward compatibility
"isolate": (re.compile(r"[a-zA-Z]+"), re.compile(r"\S+")),
"disable_ligatures": True,
"isolate": re.compile(r"\w+", re.U),
}
# See https://docs.gtk.org/Pango/pango_markup.html
MARKUP_COLOR_KEYS = {
"foreground": False,
"fgcolor": False,
"color": False,
"background": True,
"bgcolor": True,
"underline_color": True,
"overline_color": True,
"strikethrough_color": True,
}
MARKUP_TAGS = {
"b": {"font_weight": "bold"},
"big": {"font_size": "larger"},
@ -107,17 +96,24 @@ class MarkupText(StringMobject):
"tt": {"font_family": "monospace"},
"u": {"underline": "single"},
}
MARKUP_ENTITY_DICT = {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"\"": "&quot;",
"'": "&apos;"
}
def __init__(self, text: str, **kwargs):
self.full2short(kwargs)
digest_config(self, kwargs)
if not isinstance(self, Text):
self.validate_markup_string(text)
if not self.font:
self.font = get_customization()["style"]["font"]
if not self.alignment:
self.alignment = get_customization()["style"]["text_alignment"]
if self.is_markup:
self.validate_markup_string(text)
self.text = text
super().__init__(text, **kwargs)
@ -140,8 +136,8 @@ class MarkupText(StringMobject):
self.path_string_config,
self.base_color,
self.isolate,
self.protect,
self.text,
self.is_markup,
self.font_size,
self.lsh,
self.justify,
@ -156,7 +152,8 @@ class MarkupText(StringMobject):
self.t2s,
self.t2w,
self.global_config,
self.local_configs
self.local_configs,
self.disable_ligatures
)
def full2short(self, config: dict) -> None:
@ -182,7 +179,7 @@ class MarkupText(StringMobject):
self.line_width
))
svg_file = os.path.join(
get_text_dir(), tex_hash(hash_content) + ".svg"
get_text_dir(), hash_string(hash_content) + ".svg"
)
if not os.path.exists(svg_file):
self.markup_to_svg(content, svg_file)
@ -229,76 +226,92 @@ class MarkupText(StringMobject):
f"{validate_error}"
)
# Toolkits
@staticmethod
def escape_markup_char(substr: str) -> str:
return MarkupText.MARKUP_ENTITY_DICT.get(substr, substr)
@staticmethod
def unescape_markup_char(substr: str) -> str:
return {
v: k
for k, v in MarkupText.MARKUP_ENTITY_DICT.items()
}.get(substr, substr)
# Parsing
def get_cmd_spans(self) -> list[Span]:
if not self.is_markup:
return self.find_spans(r"""[<>&"']""")
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
pattern = re.compile(r"""
(?P<tag>
<
(?P<close_slash>/)?
(?P<tag_name>\w+)\s*
(?P<attr_list>(?:\w+\s*\=\s*(?P<quot>["']).*?(?P=quot)\s*)*)
(?P<elision_slash>/)?
>
)
|(?P<passthrough>
<\?.*?\?>|<!--.*?-->|<!\[CDATA\[.*?\]\]>|<!DOCTYPE.*?>
)
|(?P<entity>&(?P<unicode>\#(?P<hex>x)?)?(?P<content>.*?);)
|(?P<char>[>"'])
""", flags=re.X | re.S)
return list(pattern.finditer(string))
# Unsupported passthroughs:
# "<?...?>", "<!--...-->", "<![CDATA[...]]>", "<!DOCTYPE...>"
# See https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gmarkup.c
return self.find_spans(
r"""&[\s\S]*?;|[>"']|</?\w+(?:\s*\w+\s*\=\s*(["'])[\s\S]*?\1)*/?>"""
)
def get_substr_flag(self, substr: str) -> int:
if re.fullmatch(r"<\w[\s\S]*[^/]>", substr):
return 1
if substr.startswith("</"):
return -1
@staticmethod
def get_command_flag(match_obj: re.Match) -> int:
if match_obj.group("tag"):
if match_obj.group("close_slash"):
return -1
if not match_obj.group("elision_slash"):
return 1
return 0
def get_repl_substr_for_content(self, substr: str) -> str:
if substr.startswith("<") and substr.endswith(">"):
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
if match_obj.group("tag"):
return ""
return {
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"\"": "&quot;",
"'": "&apos;"
}.get(substr, substr)
if match_obj.group("char"):
return MarkupText.escape_markup_char(match_obj.group("char"))
return match_obj.group()
def get_repl_substr_for_matching(self, substr: str) -> str:
if substr.startswith("<") and substr.endswith(">"):
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("tag") or match_obj.group("passthrough"):
return ""
if substr.startswith("&#") and substr.endswith(";"):
if substr.startswith("&#x"):
char_reference = int(substr[3:-1], 16)
else:
char_reference = int(substr[2:-1], 10)
return chr(char_reference)
return {
"&lt;": "<",
"&gt;": ">",
"&amp;": "&",
"&quot;": "\"",
"&apos;": "'"
}.get(substr, substr)
if match_obj.group("entity"):
if match_obj.group("unicode"):
base = 10
if match_obj.group("hex"):
base = 16
return chr(int(match_obj.group("content"), base))
return MarkupText.unescape_markup_char(match_obj.group("entity"))
return match_obj.group()
def get_specified_items(
self, cmd_span_pairs: list[tuple[Span, Span]]
) -> list[tuple[Span, dict[str, str]]]:
attr_pattern = r"""(\w+)\s*\=\s*(["'])([\s\S]*?)\2"""
internal_items = []
for begin_cmd_span, end_cmd_span in cmd_span_pairs:
begin_tag = self.get_substr(begin_cmd_span)
tag_name = re.match(r"<(\w+)", begin_tag).group(1)
if tag_name == "span":
attr_dict = {
attr_match_obj.group(1): attr_match_obj.group(3)
for attr_match_obj in re.finditer(attr_pattern, begin_tag)
}
else:
attr_dict = MarkupText.MARKUP_TAGS.get(tag_name, {})
internal_items.append(
((begin_cmd_span[1], end_cmd_span[0]), attr_dict)
)
@staticmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match
) -> dict[str, str] | None:
pattern = r"""
(?P<attr_name>\w+)
\s*\=\s*
(?P<quot>["'])(?P<attr_val>.*?)(?P=quot)
"""
tag_name = open_command.group("tag_name")
if tag_name == "span":
return {
match_obj.group("attr_name"): match_obj.group("attr_val")
for match_obj in re.finditer(
pattern, open_command.group("attr_list"), re.S | re.X
)
}
return MarkupText.MARKUP_TAGS.get(tag_name, {})
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [
*internal_items,
*[
*(
(span, {key: val})
for t2x_dict, key in (
(self.t2c, "foreground"),
@ -308,49 +321,49 @@ class MarkupText(StringMobject):
)
for selector, val in t2x_dict.items()
for span in self.find_spans_by_selector(selector)
],
*[
),
*(
(span, local_config)
for selector, local_config in self.local_configs.items()
for span in self.find_spans_by_selector(selector)
],
*[
(span, {})
for span in self.find_spans_by_selector(self.isolate)
]
)
]
@staticmethod
def get_cmd_str_pair(
attr_dict: dict[str, str], label_hex: str | None
) -> tuple[str, str]:
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
if is_end:
return "</span>"
if label_hex is not None:
converted_attr_dict = {"foreground": label_hex}
for key, val in attr_dict.items():
substitute_key = MarkupText.MARKUP_COLOR_KEYS.get(key, None)
if substitute_key is None:
converted_attr_dict[key] = val
elif substitute_key:
if key in (
"background", "bgcolor",
"underline_color", "overline_color", "strikethrough_color"
):
converted_attr_dict[key] = "black"
elif key not in ("foreground", "fgcolor", "color"):
converted_attr_dict[key] = val
else:
converted_attr_dict = attr_dict.copy()
attrs_str = " ".join([
f"{key}='{val}'"
for key, val in converted_attr_dict.items()
])
return f"<span {attrs_str}>", "</span>"
return f"<span {attrs_str}>"
def get_content_prefix_and_suffix(
self, is_labelled: bool
) -> tuple[str, str]:
global_attr_dict = {
"foreground": self.base_color_hex,
"foreground": self.color_to_hex(self.base_color),
"font_family": self.font,
"font_style": self.slant,
"font_weight": self.weight,
"font_size": str(self.font_size * 1024),
"font_size": str(round(self.font_size * 1024)),
}
global_attr_dict.update(self.global_config)
# `line_height` attribute is supported since Pango 1.50.
pango_version = manimpango.pango_version()
if tuple(map(int, pango_version.split("."))) < (1, 50):
@ -365,10 +378,17 @@ class MarkupText(StringMobject):
global_attr_dict["line_height"] = str(
((line_spacing_scale) + 1) * 0.6
)
if self.disable_ligatures:
global_attr_dict["font_features"] = "liga=0,dlig=0,clig=0,hlig=0"
return self.get_cmd_str_pair(
global_attr_dict,
label_hex=self.int_to_hex(0) if is_labelled else None
global_attr_dict.update(self.global_config)
return tuple(
self.get_command_string(
global_attr_dict,
is_end=is_end,
label_hex=self.int_to_hex(0) if is_labelled else None
)
for is_end in (False, True)
)
# Method alias
@ -376,8 +396,8 @@ class MarkupText(StringMobject):
def get_parts_by_text(self, selector: Selector) -> VGroup:
return self.select_parts(selector)
def get_part_by_text(self, selector: Selector) -> VGroup:
return self.select_part(selector)
def get_part_by_text(self, selector: Selector, **kwargs) -> VGroup:
return self.select_part(selector, **kwargs)
def set_color_by_text(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color)
@ -393,9 +413,27 @@ class MarkupText(StringMobject):
class Text(MarkupText):
CONFIG = {
"is_markup": False,
# For backward compatibility
"isolate": (re.compile(r"\w+", re.U), re.compile(r"\S+", re.U)),
}
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
pattern = re.compile(r"""[<>&"']""")
return list(pattern.finditer(string))
@staticmethod
def get_command_flag(match_obj: re.Match) -> int:
return 0
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
return Text.escape_markup_char(match_obj.group())
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
return match_obj.group()
class Code(MarkupText):
CONFIG = {

View file

@ -1,5 +1,7 @@
from __future__ import annotations
import numpy as np
from manimlib.constants import BLACK
from manimlib.constants import ORIGIN
from manimlib.mobject.mobject import Mobject

732
manimlib/tex_templates.yml Normal file
View file

@ -0,0 +1,732 @@
# Classical TeX templates
default:
description: ""
compiler: latex
preamble: |-
\usepackage[english]{babel}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\usepackage{pifont}
\DisableLigatures{encoding = *, family = * }
\linespread{1}
ctex:
description: ""
compiler: xelatex
preamble: |-
\usepackage[UTF8]{ctex}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\linespread{1}
# Simplified TeX templates
basic:
description: ""
compiler: latex
preamble: |-
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
basic_ctex:
description: ""
compiler: xelatex
preamble: |-
\usepackage[UTF8]{ctex}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
empty:
description: ""
compiler: latex
preamble: ""
empty_ctex:
description: ""
compiler: xelatex
preamble: ""
# A collection of TeX templates for the fonts described at
# http://jf.burnol.free.fr/showcase.html
american_typewriter:
description: American Typewriter
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{American Typewriter}
\usepackage[defaultmathsizes]{mathastext}
antykwa:
description: Antykwa Poltawskiego (TX Fonts for Greek and math symbols)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[OT4,OT1]{fontenc}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\usepackage{antpolt}
\usepackage[defaultmathsizes,nolessnomore]{mathastext}
apple_chancery:
description: Apple Chancery
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Apple Chancery}
\usepackage[defaultmathsizes]{mathastext}
auriocus_kalligraphicus:
description: Auriocus Kalligraphicus (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{aurical}
\renewcommand{\rmdefault}{AuriocusKalligraphicus}
\usepackage[symbolgreek]{mathastext}
baskervald_adf_fourier:
description: Baskervald ADF with Fourier
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[upright]{fourier}
\usepackage{baskervald}
\usepackage[defaultmathsizes,noasterisk]{mathastext}
baskerville_it:
description: Baskerville (Italic)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Baskerville}
\usepackage[defaultmathsizes,italic]{mathastext}
biolinum:
description: Biolinum
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Minion Pro}
\setsansfont[Mapping=tex-text,Scale=MatchUppercase]{Myriad Pro}
\renewcommand\familydefault\sfdefault
\usepackage[defaultmathsizes]{mathastext}
\renewcommand\familydefault\rmdefault
brushscriptx:
description: BrushScriptX-Italic (PX math and Greek)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{pxfonts}
\renewcommand{\rmdefault}{pbsi}
\renewcommand{\mddefault}{xl}
\renewcommand{\bfdefault}{xl}
\usepackage[defaultmathsizes,noasterisk]{mathastext}
\boldmath
chalkboard_se:
description: Chalkboard SE
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Chalkboard SE}
\usepackage[defaultmathsizes]{mathastext}
chalkduster:
description: Chalkduster
compiler: lualatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Chalkduster}
\usepackage[defaultmathsizes]{mathastext}
comfortaa:
description: Comfortaa
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[default]{comfortaa}
\usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext}
\let\varphi\phi
\linespread{1.06}
comic_sans:
description: Comic Sans MS
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Comic Sans MS}
\usepackage[defaultmathsizes]{mathastext}
droid_sans:
description: Droid Sans
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidsans}
\usepackage[LGRgreek]{mathastext}
\let\varepsilon\epsilon
droid_sans_it:
description: Droid Sans (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidsans}
\usepackage[LGRgreek,defaultmathsizes,italic]{mathastext}
\let\varphi\phi
droid_serif:
description: Droid Serif
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidserif}
\usepackage[LGRgreek]{mathastext}
\let\varepsilon\epsilon
droid_serif_px_it:
description: Droid Serif (PX math symbols) (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{pxfonts}
\usepackage[default]{droidserif}
\usepackage[LGRgreek,defaultmathsizes,italic,basic]{mathastext}
\let\varphi\phi
ecf_augie:
description: ECF Augie (Euler Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\renewcommand\familydefault{fau}
\usepackage[defaultmathsizes,eulergreek]{mathastext}
ecf_jd:
description: ECF JD (with TX fonts)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\renewcommand\familydefault{fjd}
\usepackage{mathastext}
\mathversion{bold}
ecf_skeetch:
description: ECF Skeetch (CM Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\DeclareFontFamily{T1}{fsk}{}
\DeclareFontShape{T1}{fsk}{m}{n}{<->s*[1.315] fskmw8t}{}
\renewcommand\rmdefault{fsk}
\usepackage[noendash,defaultmathsizes,nohbar,defaultimath]{mathastext}
ecf_tall_paul:
description: ECF Tall Paul (with Symbol font)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\DeclareFontFamily{T1}{ftp}{}
\DeclareFontShape{T1}{ftp}{m}{n}{<->s*[1.4] ftpmw8t}{}
\renewcommand\familydefault{ftp}
\usepackage[symbol]{mathastext}
\let\infty\inftypsy
ecf_webster:
description: ECF Webster (with TX fonts)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\renewcommand\familydefault{fwb}
\usepackage{mathastext}
\renewcommand{\int}{\intop\limits}
\linespread{1.5}
\mathversion{bold}
electrum_adf:
description: Electrum ADF (CM Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[LGRgreek,basic,defaultmathsizes]{mathastext}
\usepackage[lf]{electrum}
\Mathastext
\let\varphi\phi
epigrafica:
description: Epigrafica
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[LGR,OT1]{fontenc}
\usepackage{epigrafica}
\usepackage[basic,LGRgreek,defaultmathsizes]{mathastext}
\let\varphi\phi
\linespread{1.2}
fourier_utopia:
description: Fourier Utopia (Fourier upright Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[upright]{fourier}
\usepackage{mathastext}
french_cursive:
description: French Cursive (Euler Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{frcursive}
\usepackage[eulergreek,noplusnominus,noequal,nohbar,nolessnomore,noasterisk]{mathastext}
gfs_bodoni:
description: GFS Bodoni
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\renewcommand{\rmdefault}{bodoni}
\usepackage[LGRgreek]{mathastext}
\let\varphi\phi
\linespread{1.06}
gfs_didot:
description: GFS Didot (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\renewcommand\rmdefault{udidot}
\usepackage[LGRgreek,defaultmathsizes,italic]{mathastext}
\let\varphi\phi
gfs_neohellenic:
description: GFS NeoHellenic
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\renewcommand{\rmdefault}{neohellenic}
\usepackage[LGRgreek]{mathastext}
\let\varphi\phi
\linespread{1.06}
gnu_freesans_tx:
description: GNU FreeSerif (and TX fonts symbols)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\usepackage{txfonts}
\setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif}
\usepackage[defaultmathsizes]{mathastext}
gnu_freeserif_freesans:
description: GNU FreeSerif and FreeSans
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSerifBold,ItalicFont=FreeSerifItalic,BoldItalicFont=FreeSerifBoldItalic]{FreeSerif}
\setsansfont[ExternalLocation,Mapping=tex-text,BoldFont=FreeSansBold,ItalicFont=FreeSansOblique,BoldItalicFont=FreeSansBoldOblique,Scale=MatchLowercase]{FreeSans}
\renewcommand{\familydefault}{lmss}
\usepackage[LGRgreek,defaultmathsizes,noasterisk]{mathastext}
\renewcommand{\familydefault}{\sfdefault}
\Mathastext
\let\varphi\phi
\renewcommand{\familydefault}{\rmdefault}
helvetica_fourier_it:
description: Helvetica with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[scaled]{helvet}
\usepackage{fourier}
\renewcommand{\rmdefault}{phv}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
latin_modern_tw:
description: Latin Modern Typewriter Proportional
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[variablett]{lmodern}
\renewcommand{\rmdefault}{\ttdefault}
\usepackage[LGRgreek]{mathastext}
\MTgreekfont{lmtt}
\Mathastext
\let\varepsilon\epsilon
latin_modern_tw_it:
description: Latin Modern Typewriter Proportional (CM Greek) (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[variablett,nomath]{lmodern}
\renewcommand{\familydefault}{\ttdefault}
\usepackage[frenchmath]{mathastext}
\linespread{1.08}
libertine:
description: Libertine
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{libertine}
\usepackage[greek=n]{libgreek}
\usepackage[noasterisk,defaultmathsizes]{mathastext}
libris_adf_fourier:
description: Libris ADF with Fourier
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[upright]{fourier}
\usepackage{libris}
\renewcommand{\familydefault}{\sfdefault}
\usepackage[noasterisk]{mathastext}
minion_pro_myriad_pro:
description: Minion Pro and Myriad Pro (and TX fonts symbols)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage[default]{droidserif}
\usepackage[LGRgreek]{mathastext}
\let\varepsilon\epsilon
minion_pro_tx:
description: Minion Pro (and TX fonts symbols)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{txfonts}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Minion Pro}
\usepackage[defaultmathsizes]{mathastext}
new_century_schoolbook:
description: New Century Schoolbook (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{newcent}
\usepackage[symbolgreek]{mathastext}
\linespread{1.1}
new_century_schoolbook_px:
description: New Century Schoolbook (Symbol Greek, PX math symbols)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{pxfonts}
\usepackage{newcent}
\usepackage[symbolgreek,defaultmathsizes]{mathastext}
\linespread{1.06}
noteworthy_light:
description: Noteworthy Light
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Noteworthy Light}
\usepackage[defaultmathsizes]{mathastext}
palatino:
description: Palatino (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{palatino}
\usepackage[symbolmax,defaultmathsizes]{mathastext}
papyrus:
description: Papyrus
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Papyrus}
\usepackage[defaultmathsizes]{mathastext}
romande_adf_fourier_it:
description: Romande ADF with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{fourier}
\usepackage{romande}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
\renewcommand{\itshape}{\swashstyle}
slitex:
description: SliTeX (Euler Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{tpslifonts}
\usepackage[eulergreek,defaultmathsizes]{mathastext}
\MTEulerScale{1.06}
\linespread{1.2}
times_fourier_it:
description: Times with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{fourier}
\renewcommand{\rmdefault}{ptm}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
urw_avant_garde:
description: URW Avant Garde (Symbol Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{avant}
\renewcommand{\familydefault}{\sfdefault}
\usepackage[symbolgreek,defaultmathsizes]{mathastext}
urw_zapf_chancery:
description: URW Zapf Chancery (CM Greek)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\DeclareFontFamily{T1}{pzc}{}
\DeclareFontShape{T1}{pzc}{mb}{it}{<->s*[1.2] pzcmi8t}{}
\DeclareFontShape{T1}{pzc}{m}{it}{<->ssub * pzc/mb/it}{}
\DeclareFontShape{T1}{pzc}{mb}{sl}{<->ssub * pzc/mb/it}{}
\DeclareFontShape{T1}{pzc}{m}{sl}{<->ssub * pzc/mb/sl}{}
\DeclareFontShape{T1}{pzc}{m}{n}{<->ssub * pzc/mb/it}{}
\usepackage{chancery}
\usepackage{mathastext}
\linespread{1.05}
\boldmath
venturis_adf_fourier_it:
description: Venturis ADF with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{fourier}
\usepackage[lf]{venturis}
\usepackage[italic,defaultmathsizes,noasterisk]{mathastext}
verdana_it:
description: Verdana (Italic)
compiler: xelatex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[no-math]{fontspec}
\setmainfont[Mapping=tex-text]{Verdana}
\usepackage[defaultmathsizes,italic]{mathastext}
vollkorn:
description: Vollkorn (TX fonts for Greek and math symbols)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage[T1]{fontenc}
\usepackage{txfonts}
\usepackage[upright]{txgreeks}
\usepackage{vollkorn}
\usepackage[defaultmathsizes]{mathastext}
vollkorn_fourier_it:
description: Vollkorn with Fourier (Italic)
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\usepackage{fourier}
\usepackage{vollkorn}
\usepackage[italic,nohbar]{mathastext}
zapf_chancery:
description: Zapf Chancery
compiler: latex
preamble: |-
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\DeclareFontFamily{T1}{pzc}{}
\DeclareFontShape{T1}{pzc}{mb}{it}{<->s*[1.2] pzcmi8t}{}
\DeclareFontShape{T1}{pzc}{m}{it}{<->ssub * pzc/mb/it}{}
\usepackage{chancery}
\renewcommand\shapedefault\itdefault
\renewcommand\bfdefault\mddefault
\usepackage[defaultmathsizes]{mathastext}
\linespread{1.05}

View file

@ -1,25 +0,0 @@
\documentclass[preview]{standalone}
\usepackage[UTF8]{ctex}
\usepackage[english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\linespread{1}
\begin{document}
[tex_expression]
\end{document}

View file

@ -1,28 +0,0 @@
\documentclass[preview]{standalone}
\usepackage[english]{babel}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{dsfont}
\usepackage{setspace}
\usepackage{tipa}
\usepackage{relsize}
\usepackage{textcomp}
\usepackage{mathrsfs}
\usepackage{calligra}
\usepackage{wasysym}
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{microtype}
\usepackage{pifont}
\DisableLigatures{encoding = *, family = * }
\linespread{1}
\begin{document}
[tex_expression]
\end{document}

View file

@ -42,14 +42,9 @@ def init_customization() -> None:
"sounds": "",
"temporary_storage": "",
},
"tex": {
"executable": "",
"template_file": "",
"intermediate_filetype": "",
"text_to_replace": "[tex_expression]",
},
"universal_import_line": "from manimlib import *",
"style": {
"tex_template": "",
"font": "Consolas",
"background_color": "",
},
@ -62,7 +57,7 @@ def init_customization() -> None:
"medium": "1280x720",
"high": "1920x1080",
"4k": "3840x2160",
"default_resolution": "high",
"default_resolution": "",
},
"fps": 30,
}
@ -109,24 +104,14 @@ def init_customization() -> None:
show_default=False
)
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(
style_config = configuration["style"]
tex_template = Prompt.ask(
" Select a TeX template to compile a LaTeX source file",
default="default"
)
style_config["tex_template"] = tex_template
style_config["background_color"] = Prompt.ask(
" Which [bold]background color[/bold] do you want [italic](hex code)",
default="#333333"
)
@ -139,7 +124,7 @@ def init_customization() -> None:
)
table.add_row("480p15", "720p30", "1080p60", "2160p60")
console.print(table)
configuration["camera_qualities"]["default_quality"] = Prompt.ask(
configuration["camera_resolutions"]["default_resolution"] = Prompt.ask(
" Which one to choose as the default rendering quality",
choices=["low", "medium", "high", "ultra_high"],
default="high"
@ -161,7 +146,7 @@ def init_customization() -> None:
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]")

View file

@ -1,4 +1,5 @@
from functools import lru_cache
import hashlib
import inspect
import math
@ -76,3 +77,9 @@ def binary_search(function,
else:
return None
return mh
def hash_string(string):
# Truncating at 16 bytes for cleanliness
hasher = hashlib.sha256(string.encode())
return hasher.hexdigest()[:16]

View file

@ -1,135 +1,152 @@
from __future__ import annotations
from contextlib import contextmanager
import hashlib
import os
import sys
import re
import yaml
from manimlib.config import get_custom_config
from manimlib.config import get_manim_dir
from manimlib.logger import log
from manimlib.utils.directories import get_tex_dir
from manimlib.utils.simple_functions import hash_string
SAVED_TEX_CONFIG = {}
def get_tex_template_config(template_name: str) -> dict[str, str]:
name = template_name.replace(" ", "_").lower()
with open(os.path.join(
get_manim_dir(), "manimlib", "tex_templates.yml"
), encoding="utf-8") as tex_templates_file:
templates_dict = yaml.safe_load(tex_templates_file)
if name not in templates_dict:
log.warning(
"Cannot recognize template '%s', falling back to 'default'.",
name
)
name = "default"
return templates_dict[name]
def get_tex_config() -> dict[str, str]:
"""
Returns a dict which should look something like this:
{
"executable": "latex",
"template_file": "tex_template.tex",
"intermediate_filetype": "dvi",
"text_to_replace": "YourTextHere",
"tex_body": "..."
"template": "default",
"compiler": "latex",
"preamble": "..."
}
"""
# Only load once, then save thereafter
if not SAVED_TEX_CONFIG:
custom_config = get_custom_config()
SAVED_TEX_CONFIG.update(custom_config["tex"])
# Read in template file
template_filename = os.path.join(
get_manim_dir(), "manimlib", "tex_templates",
SAVED_TEX_CONFIG["template_file"],
)
with open(template_filename, "r", encoding="utf-8") as file:
SAVED_TEX_CONFIG["tex_body"] = file.read()
template_name = get_custom_config()["style"]["tex_template"]
template_config = get_tex_template_config(template_name)
SAVED_TEX_CONFIG.update({
"template": template_name,
"compiler": template_config["compiler"],
"preamble": template_config["preamble"]
})
return SAVED_TEX_CONFIG
def tex_hash(tex_file_content: str) -> int:
# Truncating at 16 bytes for cleanliness
hasher = hashlib.sha256(tex_file_content.encode())
return hasher.hexdigest()[:16]
def tex_content_to_svg_file(
content: str, template: str, additional_preamble: str
) -> str:
tex_config = get_tex_config()
if not template or template == tex_config["template"]:
compiler = tex_config["compiler"]
preamble = tex_config["preamble"]
else:
config = get_tex_template_config(template)
compiler = config["compiler"]
preamble = config["preamble"]
if additional_preamble:
preamble += "\n" + additional_preamble
full_tex = "\n\n".join((
"\\documentclass[preview]{standalone}",
preamble,
"\\begin{document}",
content,
"\\end{document}"
)) + "\n"
def tex_to_svg_file(tex_file_content: str) -> str:
svg_file = os.path.join(
get_tex_dir(), tex_hash(tex_file_content) + ".svg"
get_tex_dir(), hash_string(full_tex) + ".svg"
)
if not os.path.exists(svg_file):
# If svg doesn't exist, create it
tex_to_svg(tex_file_content, svg_file)
create_tex_svg(full_tex, svg_file, compiler)
return svg_file
def tex_to_svg(tex_file_content: str, svg_file: str) -> str:
tex_file = svg_file.replace(".svg", ".tex")
with open(tex_file, "w", encoding="utf-8") as outfile:
outfile.write(tex_file_content)
svg_file = dvi_to_svg(tex_to_dvi(tex_file))
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
if compiler == "latex":
program = "latex"
dvi_ext = ".dvi"
elif compiler == "xelatex":
program = "xelatex -no-pdf"
dvi_ext = ".xdv"
else:
raise NotImplementedError(
f"Compiler '{compiler}' is not implemented"
)
# Write tex file
root, _ = os.path.splitext(svg_file)
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
tex_file.write(full_tex)
# tex to dvi
if os.system(" ".join((
program,
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory=\"{os.path.dirname(svg_file)}\"",
f"\"{root}.tex\"",
">",
os.devnull
))):
log.error(
"LaTeX Error! Not a worry, it happens to the best of us."
)
with open(root + ".log", "r", encoding="utf-8") as log_file:
error_match_obj = re.search(r"(?<=\n! ).*", log_file.read())
if error_match_obj:
log.debug(
"The error could be: `%s`",
error_match_obj.group()
)
raise LatexError()
# dvi to svg
os.system(" ".join((
"dvisvgm",
f"\"{root}{dvi_ext}\"",
"-n",
"-v",
"0",
"-o",
f"\"{svg_file}\"",
">",
os.devnull
)))
# Cleanup superfluous documents
tex_dir, name = os.path.split(svg_file)
stem, end = name.split(".")
for file in filter(lambda s: s.startswith(stem), os.listdir(tex_dir)):
if not file.endswith(end):
os.remove(os.path.join(tex_dir, file))
return svg_file
def tex_to_dvi(tex_file: str) -> str:
tex_config = get_tex_config()
program = tex_config["executable"]
file_type = tex_config["intermediate_filetype"]
result = tex_file.replace(".tex", "." + file_type)
if not os.path.exists(result):
commands = [
program,
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory=\"{os.path.dirname(tex_file)}\"",
f"\"{tex_file}\"",
">",
os.devnull
]
exit_code = os.system(" ".join(commands))
if exit_code != 0:
log_file = tex_file.replace(".tex", ".log")
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
error_str = ""
with open(log_file, "r", encoding="utf-8") as file:
for line in file.readlines():
if line.startswith("!"):
error_str = line[2:-1]
log.debug(f"The error could be: `{error_str}`")
raise LatexError(error_str)
return result
def dvi_to_svg(dvi_file: str) -> str:
"""
Converts a dvi, which potentially has multiple slides, into a
directory full of enumerated pngs corresponding with these slides.
Returns a list of PIL Image objects for these images sorted as they
where in the dvi
"""
file_type = get_tex_config()["intermediate_filetype"]
result = dvi_file.replace("." + file_type, ".svg")
if not os.path.exists(result):
commands = [
"dvisvgm",
"\"{}\"".format(dvi_file),
"-n",
"-v",
"0",
"-o",
"\"{}\"".format(result),
">",
os.devnull
]
os.system(" ".join(commands))
return result
for ext in (".tex", dvi_ext, ".log", ".aux"):
try:
os.remove(root + ext)
except FileNotFoundError:
pass
# TODO, perhaps this should live elsewhere
@contextmanager
def display_during_execution(message: str) -> None:
# Only show top line
to_print = message.split("\n")[0]
def display_during_execution(message: str):
# Merge into a single line
to_print = message.replace("\n", " ")
max_characters = os.get_terminal_size().columns - 1
if len(to_print) > max_characters:
to_print = to_print[:max_characters - 3] + "..."
@ -140,6 +157,5 @@ def display_during_execution(message: str) -> None:
print(" " * len(to_print), end="\r")
class LatexError(Exception):
pass

View file

@ -17,7 +17,7 @@ rich
scipy
screeninfo
skia-pathops
svgelements
svgelements>=1.8.1
sympy
tqdm
validators

View file

@ -48,7 +48,7 @@ install_requires =
scipy
screeninfo
skia-pathops
svgelements
svgelements>=1.8.1
sympy
tqdm
validators