Add Code mobject and rewrite Text.text2settings()

This commit is contained in:
Michael W 2021-09-15 15:55:19 +08:00 committed by GitHub
parent da909c0df8
commit 9e563ae3b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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.