mirror of
https://github.com/3b1b/manim.git
synced 2025-09-01 00:48:45 +00:00
Add Code mobject and rewrite Text.text2settings()
This commit is contained in:
parent
da909c0df8
commit
9e563ae3b4
1 changed files with 600 additions and 525 deletions
|
@ -7,6 +7,9 @@ import typing
|
||||||
import warnings
|
import warnings
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import functools
|
import functools
|
||||||
|
import pygments
|
||||||
|
import pygments.lexers
|
||||||
|
import pygments.styles
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -24,6 +27,7 @@ from manimpango import PangoUtils, TextSetting, MarkupUtils
|
||||||
TEXT_MOB_SCALE_FACTOR = 0.0076
|
TEXT_MOB_SCALE_FACTOR = 0.0076
|
||||||
DEFAULT_LINE_SPACING_SCALE = 0.3
|
DEFAULT_LINE_SPACING_SCALE = 0.3
|
||||||
|
|
||||||
|
|
||||||
class Text(SVGMobject):
|
class Text(SVGMobject):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
# Mobject
|
# Mobject
|
||||||
|
@ -47,9 +51,9 @@ class Text(SVGMobject):
|
||||||
"disable_ligatures": True,
|
"disable_ligatures": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, text, **config):
|
def __init__(self, text, **kwargs):
|
||||||
self.full2short(config)
|
self.full2short(kwargs)
|
||||||
digest_config(self, config)
|
digest_config(self, kwargs)
|
||||||
if self.size:
|
if self.size:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"self.size has been deprecated and will "
|
"self.size has been deprecated and will "
|
||||||
|
@ -68,7 +72,7 @@ class Text(SVGMobject):
|
||||||
file_name = self.text2svg()
|
file_name = self.text2svg()
|
||||||
PangoUtils.remove_last_M(file_name)
|
PangoUtils.remove_last_M(file_name)
|
||||||
self.remove_empty_path(file_name)
|
self.remove_empty_path(file_name)
|
||||||
SVGMobject.__init__(self, file_name, **config)
|
SVGMobject.__init__(self, file_name, **kwargs)
|
||||||
self.text = text
|
self.text = text
|
||||||
if self.disable_ligatures:
|
if self.disable_ligatures:
|
||||||
self.apply_space_chars()
|
self.apply_space_chars()
|
||||||
|
@ -145,13 +149,13 @@ class Text(SVGMobject):
|
||||||
|
|
||||||
def set_color_by_t2c(self, t2c=None):
|
def set_color_by_t2c(self, t2c=None):
|
||||||
t2c = t2c if t2c else self.t2c
|
t2c = t2c if t2c else self.t2c
|
||||||
for word, color in list(t2c.items()):
|
for word, color in t2c.items():
|
||||||
for start, end in self.find_indexes(word):
|
for start, end in self.find_indexes(word):
|
||||||
self[start:end].set_color(color)
|
self[start:end].set_color(color)
|
||||||
|
|
||||||
def set_color_by_t2g(self, t2g=None):
|
def set_color_by_t2g(self, t2g=None):
|
||||||
t2g = t2g if t2g else self.t2g
|
t2g = t2g if t2g else self.t2g
|
||||||
for word, gradient in list(t2g.items()):
|
for word, gradient in t2g.items():
|
||||||
for start, end in self.find_indexes(word):
|
for start, end in self.find_indexes(word):
|
||||||
self[start:end].set_color_by_gradient(*gradient)
|
self[start:end].set_color_by_gradient(*gradient)
|
||||||
|
|
||||||
|
@ -165,49 +169,62 @@ class Text(SVGMobject):
|
||||||
return hasher.hexdigest()[:16]
|
return hasher.hexdigest()[:16]
|
||||||
|
|
||||||
def text2settings(self):
|
def text2settings(self):
|
||||||
|
"""
|
||||||
|
Substrings specified in t2f, t2s, t2w can occupy each other.
|
||||||
|
For each category of style, a stack following first-in-last-out is constructed,
|
||||||
|
and the last value in each stack takes effect.
|
||||||
|
"""
|
||||||
settings = []
|
settings = []
|
||||||
t2x = [self.t2f, self.t2s, self.t2w]
|
self.line_num = 0
|
||||||
for i in range(len(t2x)):
|
def add_text_settings(start, end, style_stacks):
|
||||||
fsw = [self.font, self.slant, self.weight]
|
if start == end:
|
||||||
if t2x[i]:
|
return
|
||||||
for word, x in list(t2x[i].items()):
|
breakdown_indices = [start, *[
|
||||||
for start, end in self.find_indexes(word):
|
i + start + 1 for i, char in enumerate(self.text[start:end]) if char == "\n"
|
||||||
fsw[i] = x
|
], end]
|
||||||
settings.append(TextSetting(start, end, *fsw))
|
style = [stack[-1] for stack in style_stacks]
|
||||||
|
for atom_start, atom_end in zip(breakdown_indices[:-1], breakdown_indices[1:]):
|
||||||
|
if atom_start < atom_end:
|
||||||
|
settings.append(TextSetting(atom_start, atom_end, *style, self.line_num))
|
||||||
|
self.line_num += 1
|
||||||
|
self.line_num -= 1
|
||||||
|
|
||||||
# Set All text settings(default font slant weight)
|
# Set all the default and specified values.
|
||||||
fsw = [self.font, self.slant, self.weight]
|
len_text = len(self.text)
|
||||||
settings.sort(key=lambda setting: setting.start)
|
t2x_items = sorted([
|
||||||
temp_settings = settings.copy()
|
*[
|
||||||
start = 0
|
(0, len_text, t2x_index, value)
|
||||||
for setting in settings:
|
for t2x_index, value in enumerate([self.font, self.slant, self.weight])
|
||||||
if setting.start != start:
|
],
|
||||||
temp_settings.append(TextSetting(start, setting.start, *fsw))
|
*[
|
||||||
start = setting.end
|
(start, end, t2x_index, value)
|
||||||
if start != len(self.text):
|
for t2x_index, t2x in enumerate([self.t2f, self.t2s, self.t2w])
|
||||||
temp_settings.append(TextSetting(start, len(self.text), *fsw))
|
for word, value in t2x.items()
|
||||||
settings = sorted(temp_settings, key=lambda setting: setting.start)
|
for start, end in self.find_indexes(word)
|
||||||
|
]
|
||||||
|
], key=lambda item: item[0])
|
||||||
|
|
||||||
if re.search(r'\n', self.text):
|
# Break down ranges and construct settings separately.
|
||||||
line_num = 0
|
active_items = []
|
||||||
for start, end in self.find_indexes('\n'):
|
style_stacks = [[] for _ in range(3)]
|
||||||
for setting in settings:
|
for item, next_start in zip(t2x_items, [*[item[0] for item in t2x_items[1:]], len_text]):
|
||||||
if setting.line_num == -1:
|
active_items.append(item)
|
||||||
setting.line_num = line_num
|
start, end, t2x_index, value = item
|
||||||
if start < setting.end:
|
style_stacks[t2x_index].append(value)
|
||||||
line_num += 1
|
halting_items = sorted(filter(
|
||||||
new_setting = copy.copy(setting)
|
lambda item: item[1] <= next_start,
|
||||||
setting.end = end
|
active_items
|
||||||
new_setting.start = end
|
), key=lambda item: item[1])
|
||||||
new_setting.line_num = line_num
|
atom_start = start
|
||||||
settings.append(new_setting)
|
for halting_item in halting_items:
|
||||||
settings.sort(key=lambda setting: setting.start)
|
active_items.remove(halting_item)
|
||||||
break
|
_, atom_end, t2x_index, _ = halting_item
|
||||||
|
add_text_settings(atom_start, atom_end, style_stacks)
|
||||||
for setting in settings:
|
style_stacks[t2x_index].pop()
|
||||||
if setting.line_num == -1:
|
atom_start = atom_end
|
||||||
setting.line_num = 0
|
add_text_settings(atom_start, next_start, style_stacks)
|
||||||
|
|
||||||
|
del self.line_num
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
def text2svg(self):
|
def text2svg(self):
|
||||||
|
@ -257,6 +274,7 @@ class MarkupText(SVGMobject):
|
||||||
"gradient": None,
|
"gradient": None,
|
||||||
"disable_ligatures": True,
|
"disable_ligatures": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, text, **config):
|
def __init__(self, text, **config):
|
||||||
digest_config(self, config)
|
digest_config(self, config)
|
||||||
self.text = f'<span>{text}</span>'
|
self.text = f'<span>{text}</span>'
|
||||||
|
@ -303,6 +321,7 @@ class MarkupText(SVGMobject):
|
||||||
# anti-aliasing
|
# anti-aliasing
|
||||||
if self.height is None:
|
if self.height is None:
|
||||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||||
|
|
||||||
def text2hash(self):
|
def text2hash(self):
|
||||||
"""Generates ``sha256`` hash for file name."""
|
"""Generates ``sha256`` hash for file name."""
|
||||||
settings = (
|
settings = (
|
||||||
|
@ -349,7 +368,6 @@ class MarkupText(SVGMobject):
|
||||||
**extra_kwargs
|
**extra_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_color(self, col):
|
def _parse_color(self, col):
|
||||||
"""Parse color given in ``<color>`` or ``<gradient>`` tags."""
|
"""Parse color given in ``<color>`` or ``<gradient>`` tags."""
|
||||||
if re.match("#[0-9a-f]{6}", col):
|
if re.match("#[0-9a-f]{6}", col):
|
||||||
|
@ -472,6 +490,63 @@ class MarkupText(SVGMobject):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"MarkupText({repr(self.original_text)})"
|
return f"MarkupText({repr(self.original_text)})"
|
||||||
|
|
||||||
|
|
||||||
|
class Code(Text):
|
||||||
|
CONFIG = {
|
||||||
|
"font": "Consolas",
|
||||||
|
"font_size": 24,
|
||||||
|
"lsh": 1.0,
|
||||||
|
"language": "python",
|
||||||
|
# Visit https://pygments.org/demo/ to have a preview of more styles.
|
||||||
|
"code_style": "monokai",
|
||||||
|
# If not None, then each character will cover a space of equal width.
|
||||||
|
"char_width": None
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, code, **kwargs):
|
||||||
|
self.full2short(kwargs)
|
||||||
|
digest_config(self, kwargs)
|
||||||
|
code = code.lstrip("\n") # avoid mismatches of character indices
|
||||||
|
lexer = pygments.lexers.get_lexer_by_name(self.language)
|
||||||
|
tokens_generator = pygments.lex(code, lexer)
|
||||||
|
styles_dict = dict(pygments.styles.get_style_by_name(self.code_style))
|
||||||
|
default_color_hex = styles_dict[pygments.token.Text]["color"]
|
||||||
|
if not default_color_hex:
|
||||||
|
default_color_hex = self.color[1:]
|
||||||
|
start_index = 0
|
||||||
|
t2c = {}
|
||||||
|
t2s = {}
|
||||||
|
t2w = {}
|
||||||
|
for pair in tokens_generator:
|
||||||
|
ttype, token = pair
|
||||||
|
end_index = start_index + len(token)
|
||||||
|
range_str = f"[{start_index}:{end_index}]"
|
||||||
|
style_dict = styles_dict[ttype]
|
||||||
|
t2c[range_str] = "#" + (style_dict["color"] or default_color_hex)
|
||||||
|
t2s[range_str] = ITALIC if style_dict["italic"] else NORMAL
|
||||||
|
t2w[range_str] = BOLD if style_dict["bold"] else NORMAL
|
||||||
|
start_index = end_index
|
||||||
|
t2c.update(self.t2c)
|
||||||
|
t2s.update(self.t2s)
|
||||||
|
t2w.update(self.t2w)
|
||||||
|
kwargs["t2c"] = t2c
|
||||||
|
kwargs["t2s"] = t2s
|
||||||
|
kwargs["t2w"] = t2w
|
||||||
|
Text.__init__(self, code, **kwargs)
|
||||||
|
if self.char_width is not None:
|
||||||
|
self.set_monospace(self.char_width)
|
||||||
|
|
||||||
|
def set_monospace(self, char_width):
|
||||||
|
current_char_index = 0
|
||||||
|
for i, char in enumerate(self.text):
|
||||||
|
if char == "\n":
|
||||||
|
current_char_index = 0
|
||||||
|
continue
|
||||||
|
self[i].set_x(current_char_index * char_width)
|
||||||
|
current_char_index += 1
|
||||||
|
self.center()
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def register_font(font_file: typing.Union[str, Path]):
|
def register_font(font_file: typing.Union[str, Path]):
|
||||||
"""Temporarily add a font file to Pango's search path.
|
"""Temporarily add a font file to Pango's search path.
|
||||||
|
|
Loading…
Add table
Reference in a new issue