diff --git a/manimlib/constants.py b/manimlib/constants.py index 7cd99e26..82d2298d 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -5,6 +5,7 @@ MEDIA_DIR = "" VIDEO_DIR = "" VIDEO_OUTPUT_DIR = "" TEX_DIR = "" +TEXT_DIR = "" def initialize_directories(config): @@ -12,6 +13,7 @@ def initialize_directories(config): global VIDEO_DIR global VIDEO_OUTPUT_DIR global TEX_DIR + global TEXT_DIR video_path_specified = config["video_dir"] or config["video_output_dir"] @@ -37,6 +39,7 @@ def initialize_directories(config): ) TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex") + TEXT_DIR = os.path.join(MEDIA_DIR, "texts") if not video_path_specified: VIDEO_DIR = os.path.join(MEDIA_DIR, "videos") VIDEO_OUTPUT_DIR = os.path.join(MEDIA_DIR, "videos") @@ -45,10 +48,28 @@ def initialize_directories(config): else: VIDEO_DIR = config["video_dir"] - for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR]: + for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR, TEXT_DIR]: if folder != "" and not os.path.exists(folder): os.makedirs(folder) +NOT_SETTING_FONT_MSG=''' +Warning: +You haven't set font. +If you are not using English, this may cause text rendering problem. +You set font like: +text = Text('your text', font='your font') +or: +class MyText(Text): + CONFIG = { + 'font': 'My Font' + } +''' +START_X = 30 +START_Y = 20 +NORMAL = 'NORMAL' +ITALIC = 'ITALIC' +OBLIQUE = 'OBLIQUE' +BOLD = 'BOLD' TEX_USE_CTEX = False TEX_TEXT_TO_REPLACE = "YourTextHere" diff --git a/manimlib/imports.py b/manimlib/imports.py index eb089282..b6fd097b 100644 --- a/manimlib/imports.py +++ b/manimlib/imports.py @@ -49,6 +49,7 @@ from manimlib.mobject.svg.brace import * from manimlib.mobject.svg.drawings import * from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.tex_mobject import * +from manimlib.mobject.svg.text_mobject import * from manimlib.mobject.three_d_utils import * from manimlib.mobject.three_dimensions import * from manimlib.mobject.types.image_mobject import * diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index a5dc6bc3..4798003a 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -81,7 +81,7 @@ class SVGMobject(VMobject): self.update_ref_to_element(element) elif element.tagName == 'style': pass # TODO, handle style - elif element.tagName in ['g', 'svg']: + elif element.tagName in ['g', 'svg', 'symbol']: result += it.chain(*[ self.get_mobjects_from(child) for child in element.childNodes @@ -284,12 +284,27 @@ class SVGMobject(VMobject): pass # TODO, ... + def flatten(self, input_list): + output_list = [] + for i in input_list: + if isinstance(i, list): + output_list.extend(self.flatten(i)) + else: + output_list.append(i) + return output_list + + def get_all_childNodes_have_id(self, element): + all_childNodes_have_id = [] + if not isinstance(element, minidom.Element): + return + if element.hasAttribute('id'): + return element + for e in element.childNodes: + all_childNodes_have_id.append(self.get_all_childNodes_have_id(e)) + return self.flatten([e for e in all_childNodes_have_id if e]) + def update_ref_to_element(self, defs): - new_refs = dict([ - (element.getAttribute('id'), element) - for element in defs.childNodes - if isinstance(element, minidom.Element) and element.hasAttribute('id') - ]) + new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)]) self.ref_to_element.update(new_refs) def move_into_position(self): diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py new file mode 100644 index 00000000..2be9b758 --- /dev/null +++ b/manimlib/mobject/svg/text_mobject.py @@ -0,0 +1,208 @@ +import re +import os +import copy +import hashlib +import cairo +import manimlib.constants as consts +from manimlib.constants import * +from manimlib.mobject.svg.svg_mobject import SVGMobject +from manimlib.utils.config_ops import digest_config + + +class TextSetting(object): + def __init__(self, start, end, font, slant, weight, line_num=-1): + self.start = start + self.end = end + self.font = font + self.slant = slant + self.weight = weight + self.line_num = line_num + + +class Text(SVGMobject): + CONFIG = { + # Mobject + 'color': consts.WHITE, + 'height': None, + # Text + 'font': '', + 'gradient': None, + 'lsh': -1, + 'size': 1, + 'slant': NORMAL, + 'weight': NORMAL, + 't2c': {}, + 't2f': {}, + 't2g': {}, + 't2s': {}, + 't2w': {}, + } + + def __init__(self, text, **config): + self.text = text + self.full2short(config) + digest_config(self, config) + self.lsh = self.size if self.lsh == -1 else self.lsh + + file_name = self.text2svg() + SVGMobject.__init__(self, file_name, **config) + + if self.t2c: + self.set_color_by_t2c() + if self.gradient: + self.set_color_by_gradient(*self.gradient) + if self.t2g: + self.set_color_by_t2g() + + # anti-aliasing + self.scale(0.1) + + def find_indexes(self, word): + m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word) + if m: + start = int(m.group(1)) if m.group(1) != '' else 0 + end = int(m.group(2)) if m.group(2) != '' else len(self.text) + start = len(self.text) + start if start < 0 else start + end = len(self.text) + end if end < 0 else end + return [(start, end)] + + indexes = [] + index = self.text.find(word) + while index != -1: + indexes.append((index, index + len(word))) + index = self.text.find(word, index + len(word)) + return indexes + + def full2short(self, config): + for kwargs in [config, self.CONFIG]: + if kwargs.__contains__('line_spacing_height'): + kwargs['lsh'] = kwargs.pop('line_spacing_height') + if kwargs.__contains__('text2color'): + kwargs['t2c'] = kwargs.pop('text2color') + if kwargs.__contains__('text2font'): + kwargs['t2f'] = kwargs.pop('text2font') + if kwargs.__contains__('text2gradient'): + kwargs['t2g'] = kwargs.pop('text2gradient') + if kwargs.__contains__('text2slant'): + kwargs['t2s'] = kwargs.pop('text2slant') + if kwargs.__contains__('text2weight'): + kwargs['t2w'] = kwargs.pop('text2weight') + + def set_color_by_t2c(self, t2c=None): + t2c = t2c if t2c else self.t2c + for word, color in list(t2c.items()): + for start, end in self.find_indexes(word): + self[start:end].set_color(color) + + def set_color_by_t2g(self, t2g=None): + t2g = t2g if t2g else self.t2g + for word, gradient in list(t2g.items()): + for start, end in self.find_indexes(word): + self[start:end].set_color_by_gradient(*gradient) + + def str2slant(self, string): + if string == NORMAL: + return cairo.FontSlant.NORMAL + if string == ITALIC: + return cairo.FontSlant.ITALIC + if string == OBLIQUE: + return cairo.FontSlant.OBLIQUE + + def str2weight(self, string): + if string == NORMAL: + return cairo.FontWeight.NORMAL + if string == BOLD: + return cairo.FontWeight.BOLD + + def text2hash(self): + settings = self.font + self.slant + self.weight + settings += str(self.t2f) + str(self.t2s) + str(self.t2w) + settings += str(self.lsh) + str(self.size) + id_str = self.text+settings + hasher = hashlib.sha256() + hasher.update(id_str.encode()) + return hasher.hexdigest()[:16] + + def text2settings(self): + settings = [] + t2x = [self.t2f, self.t2s, self.t2w] + for i in range(len(t2x)): + fsw = [self.font, self.slant, self.weight] + if t2x[i]: + for word, x in list(t2x[i].items()): + for start, end in self.find_indexes(word): + fsw[i] = x + settings.append(TextSetting(start, end, *fsw)) + + # Set All text settings(default font slant weight) + fsw = [self.font, self.slant, self.weight] + settings.sort(key=lambda setting: setting.start) + temp_settings = settings.copy() + start = 0 + for setting in settings: + if setting.start != start: + temp_settings.append(TextSetting(start, setting.start, *fsw)) + start = setting.end + if start != len(self.text): + temp_settings.append(TextSetting(start, len(self.text), *fsw)) + settings = sorted(temp_settings, key=lambda setting: setting.start) + + if re.search(r'\n', self.text): + line_num = 0 + for start, end in self.find_indexes('\n'): + for setting in settings: + if setting.line_num == -1: + setting.line_num = line_num + if start < setting.end: + line_num += 1 + new_setting = copy.copy(setting) + setting.end = end + new_setting.start = end + new_setting.line_num = line_num + settings.append(new_setting) + settings.sort(key=lambda setting: setting.start) + break + + for setting in settings: + if setting.line_num == -1: + setting.line_num = 0 + + return settings + + def text2svg(self): + # anti-aliasing + size = self.size * 10 + lsh = self.lsh * 10 + + if self.font == '': + print(NOT_SETTING_FONT_MSG) + + dir_name = consts.TEXT_DIR + hash_name = self.text2hash() + file_name = os.path.join(dir_name, hash_name)+'.svg' + if os.path.exists(file_name): + return file_name + + surface = cairo.SVGSurface(file_name, 600, 400) + context = cairo.Context(surface) + context.set_font_size(size) + context.move_to(START_X, START_Y) + + settings = self.text2settings() + offset_x = 0 + last_line_num = 0 + for setting in settings: + font = setting.font + slant = self.str2slant(setting.slant) + weight = self.str2weight(setting.weight) + text = self.text[setting.start:setting.end].replace('\n', ' ') + + context.select_font_face(font, slant, weight) + if setting.line_num != last_line_num: + offset_x = 0 + last_line_num = setting.line_num + context.move_to(START_X + offset_x, START_Y + lsh*setting.line_num) + context.show_text(text) + offset_x += context.text_extents(text)[4] + + return file_name