diff --git a/manimlib/constants.py b/manimlib/constants.py index 7cd99e26..d4f3ac06 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"] @@ -36,7 +38,8 @@ def initialize_directories(config): "directory were both passed" ) - TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex") + TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "texs") + 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,23 @@ 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) +DEFAULT_FONT = '' +DEFAULT_LSH = 1 +DEFAULT_SIZE = 1 +NOT_SETTING_FONT_MSG=''' +Warning: +You haven't set DEFAULT_FONT. +If you are not using English, this may cause text rendering problem. +You can change the DEFAULT_FONT in manimlib\\constans.py or Text('your text', font='your font'). +''' +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..219134f9 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,33 @@ class SVGMobject(VMobject): pass # TODO, ... + def flatten(self, input_list): + output_list = [] + while True: + if input_list == []: + break + for index, i in enumerate(input_list): + if type(i)== list: + input_list = i + input_list[index+1:] + break + else: + output_list.append(i) + input_list.pop(index) + break + 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..31c443c0 --- /dev/null +++ b/manimlib/mobject/svg/text_mobject.py @@ -0,0 +1,324 @@ +import cairo +import copy +import hashlib +import re +import os +from manimlib.constants import * +from manimlib.mobject.svg.svg_mobject import SVGMobject +import manimlib.constants as consts + + +class TextStyle(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): + ''' + Params: + ------- + text :: + a str, the space(' ') in front or back and '\\n' and '\\t' will be ignored + + Params(optional): + ----------------- + color :: + color defined in constants.py or a str like '#FFFFFF', default is WHITE + + font :: + a str, the name of font like 'Source Han Sans', default is DEFAULT_FONT, defined in constants.py + + lsh (line_spacing_height) :: + a number, better larger than 0.1(due to anti-aliasing), irrelevant with MUnit, default is DEFAULT_LSH + + size :: + a number, better larger than 0.1(due to anti-aliasing), irrelevant with MUnit, default is DEFAULT_SIZE + + slant :: + NORMAL or ITALIC, default is NORMAL, defined in constants.py(a str actually) + + weight :: + NORMAL or BOLD, default is NORMAL, defined in constants.py(a str actually) + + fill_color :: + the same as color + + fill_opacity :: + a float, default is 1 + + stroke_color :: + the same as color + + storke_opacity :: + a float + + t2c (text2color) :: + a dict like {'text':color} or Accurate mode + + t2f (text2font) :: + a dict like {'text':font} or Accurate mode + + t2s (text2slant) :: + a dict like {'text':slant} or Accurate mode + + t2w (text2weight) :: + a dict like {'text':weight} or Accurate mode + + Functions : + ----------- + set_color(mobject function) :: + param color, this will set the color of the whole text + + set_text_color :: + param t2c, the same as the t2c mentioned above(require a dict!) + + Accurate mode: + -------------- + This will help you to choose a specific text just like slicing, e.g. :: + text = Text('ooo', t2c={'[:1]':RED, '[1:2]':GREEN, '[2:]':BLUE}) + + btw, you can use '[[:]]' to represent the text '[:]' + ''' + CONFIG = { + "color": WHITE, + "fill_opacity": 1, + "height": None, + } + + def __init__(self, text, **kwargs): + self.text = text + + kwargs = self.full2short(**kwargs) + file_name = self.text2svg(text, **kwargs) + SVGMobject.__init__(self, file_name=file_name, **kwargs) + if kwargs.__contains__('t2c'): + self.text2color(text, **kwargs) + #anti-aliasing + self.scale(0.1) + + def full2short(self, **kwargs): + 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__('text2slant'): + kwargs['t2s'] = kwargs.pop('text2slant') + if kwargs.__contains__('text2weight'): + kwargs['t2w'] = kwargs.pop('text2weight') + return kwargs + + def find_indexes(self, text, word): + indexes = [] + if re.match(r'\[\[[0-9\-]{0,}:[0-9\-]{0,}\]\]', word): + word = word[1:-1] + index = text.find(word) + while index != -1: + indexes.append((index, index+len(word))) + index = text.find(word, index+len(word)) + return indexes + + def find_strat_and_end(self, text, word): + m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word) + start = int(m.group(1)) if m.group(1) != '' else 0 + end = int(m.group(2)) if m.group(2) != '' else len(text) + return (start, end) + + def is_slicing(self, word): + m = re.match(r'\[[0-9\-]{0,}:[0-9\-]{0,}\]', word) + return True if m else False + + def get_t2c_indexes(self, t2c): + text = self.text + length = len(text) + t2c_indexes = [] + for word, color in list(t2c.items()): + # accurate mode + if self.is_slicing(word): + start, end = self.find_strat_and_end(text, word) + start = length + start if start < 0 else start + end = length + end if end < 0 else end + t2c_indexes.append((start, end, color)) + continue + for start, end in self.find_indexes(text, word): + t2c_indexes.append((start, end, color)) + return sorted(t2c_indexes, key=lambda i: i[1]) + + def getfsw(self, **kwargs): + font = kwargs['font'] if kwargs.__contains__('font') else DEFAULT_FONT + slant = kwargs['slant'] if kwargs.__contains__('slant') else NORMAL + weight = kwargs['weight'] if kwargs.__contains__('weight') else NORMAL + return (font, slant, weight) + + def getxywh(self, text, font, slant, weight, size): + dir_name = consts.TEXT_DIR + file_name = os.path.join(dir_name, 'temp')+'.svg' + + temp_surface = cairo.SVGSurface(file_name, 1, 1) + temp_context = cairo.Context(temp_surface) + temp_context.set_font_size(size) + if font != '': + fs = self.str2slant(slant) + fw = self.str2weight(weight) + temp_context.select_font_face(font, fs, fw) + x, y, w, h, dx, dy = temp_context.text_extents(text) + return (x, y, w, h) + + def get_space_w(self, font, size): + x1, y1, w1, h1 = self.getxywh('a', font, NORMAL, NORMAL, size) + x2, y2, w2, h2, = self.getxywh('aa', font, NORMAL, NORMAL, size) + return w2 - w1*2 + + def has_multi_line(self, text): + return True if re.search(r'\n', text) else False + + def set_text_color(self, t2c): + self.text2color(self.text, t2c=t2c) + + 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 text2color(self, text, **kwargs): + for word, color in list(kwargs['t2c'].items()): + # accurate mode + if self.is_slicing(word): + start, end = self.find_strat_and_end(text, word) + self[start:end].set_color(color) + continue + for start, end in self.find_indexes(text, word): + self[start:end].set_color(color) + + def text2hash(self, text, **kwargs): + ignores = [ + 'color', 't2c', + 'fill_color', 'fill_opacity', + 'stroke_color', 'storke_opacity' + ] + for ignore in ignores: + if kwargs.__contains__(ignore): + kwargs.pop(ignore) + + id_str = text+str(kwargs) + hasher = hashlib.sha256() + hasher.update(id_str.encode()) + return hasher.hexdigest()[:16] + + def text2styles(self, text, **kwargs): + styles = [] + f0, s0, w0 = self.getfsw(**kwargs) + + if kwargs.__contains__('t2f'): + for word, font in list(kwargs['t2f'].items()): + if self.is_slicing(word): + start, end = self.find_strat_and_end(text, word) + styles.append(TextStyle(start, end, font, s0, w0)) + for start, end in self.find_indexes(text, word): + styles.append(TextStyle(start, end, font, s0, w0)) + + if kwargs.__contains__('t2s'): + for word, slant in list(kwargs['t2s'].items()): + if self.is_slicing(word): + start, end = self.find_strat_and_end(text, word) + styles.append(TextStyle(start, end, f0, slant, w0)) + for start, end in self.find_indexes(text, word): + styles.append(TextStyle(start, end, f0, slant, w0)) + + if kwargs.__contains__('t2w'): + for word, weight in list(kwargs['t2w'].items()): + if self.is_slicing(word): + start, end = self.find_strat_and_end(text, word) + styles.append(TextStyle(start, end, f0, s0, weight)) + for start, end in self.find_indexes(text, word): + styles.append(TextStyle(start, end, f0, s0, weight)) + + #Set All text styles(default font slant weight) + styles = sorted(styles, key=lambda s: s.start) + temp_styles = styles.copy() + start = 0 + for style in styles: + if style.start != start: + temp_styles.append(TextStyle(start, style.start, f0, s0, w0)) + start = style.end + if start != len(text): + temp_styles.append(TextStyle(start, len(text), f0, s0, w0)) + styles = sorted(temp_styles, key=lambda s: s.start) + + if self.has_multi_line(text): + line_num = 0 + for start, end in self.find_indexes(text, '\n'): + for style in styles: + if style.line_num == -1: + style.line_num = line_num + if start < style.end: + line_num += 1 + new_style = copy.copy(style) + style.end = end + new_style.start = end + new_style.line_num = line_num + styles.append(new_style) + styles = sorted(styles, key=lambda s: s.start) + break + + return styles + + def text2svg(self, text, **kwargs): + font, slant, weight = self.getfsw(**kwargs) + size = kwargs['size'] if kwargs.__contains__('size') else DEFAULT_SIZE + lsh = kwargs['lsh'] if kwargs.__contains__('lsh') else DEFAULT_LSH + #anti-aliasing + size *= 10 + lsh *= 10 + + if font == '': + print(NOT_SETTING_FONT_MSG) + + dir_name = consts.TEXT_DIR + hash_name = self.text2hash(text, **kwargs) + file_name = os.path.join(dir_name, hash_name)+'.svg' + if os.path.exists(file_name): + return file_name + + text_surface = cairo.SVGSurface(file_name, 300, 200) + text_context = cairo.Context(text_surface) + text_context.set_font_size(size) + + styles = self.text2styles(text, **kwargs) + last_width = 0 + last_ln = 0 + for style in styles: + temp_text = text[style.start:style.end] + sf = style.font + ss = style.slant + sw = style.weight + sln = style.line_num if style.line_num != -1 else 0 + x1, y1, w1, h1 = self.getxywh(temp_text, sf, ss, sw, size) + csf = self.str2slant(ss) + csw = self.str2weight(sw) + + text_context.select_font_face(sf, csf, csw) + if sln != last_ln: + last_width = 0 + last_ln = sln + text_context.move_to(last_width-x1, lsh*sln) + text_context.show_text(temp_text) + + last_width += w1 + self.get_space_w(sf, size) + + return file_name