From cf656e9c21ec7f23a71a9f93b503294ec74c1b66 Mon Sep 17 00:00:00 2001 From: NavpreetDevpuri <30471072+NavpreetDevpuri@users.noreply.github.com> Date: Thu, 14 May 2020 10:49:09 +0530 Subject: [PATCH] Code() in file Code_mobject.py to display code with color highlighted added Paragraph() and "exact_spaces" parameter to Text() (#1036) --- manimlib/constants.py | 104 +++++++++ manimlib/imports.py | 1 + manimlib/mobject/svg/code_mobject.py | 310 +++++++++++++++++++++++++++ manimlib/mobject/svg/text_mobject.py | 174 +++++++++++++-- requirements.txt | 1 + 5 files changed, 573 insertions(+), 17 deletions(-) create mode 100644 manimlib/mobject/svg/code_mobject.py diff --git a/manimlib/constants.py b/manimlib/constants.py index 82d2298d..817467e5 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -275,3 +275,107 @@ them to manim.play(), e.g. >>> c = Circle() >>> manim.play(ShowCreation(c)) """ +code_languages_list = {"abap": "abap", "as": "as", "as3": "as3", "ada": "ada", "antlr": "antlr", + "antlr_as": "antlr-as", + "antlr_csharp": "antlr-csharp", "antlr_cpp": "antlr-cpp", "antlr_java": "antlr-java", + "antlr_objc": "antlr-objc", "antlr_perl": "antlr-perl", "antlr_python": "antlr-python", + "antlr_ruby": "antlr-ruby", "apacheconf": "apacheconf", "applescript": "applescript", + "aspectj": "aspectj", + "aspx_cs": "aspx-cs", "aspx_vb": "aspx-vb", "asy": "asy", "ahk": "ahk", "autoit": "autoit", + "awk": "awk", + "basemake": "basemake", "bash": "bash", "console": "console", "bat": "bat", + "bbcode": "bbcode", + "befunge": "befunge", "blitzmax": "blitzmax", "boo": "boo", "brainfuck": "brainfuck", + "bro": "bro", + "bugs": "bugs", "c": "c", "csharp": "csharp", "cpp": "cpp", "c_objdump": "c-objdump", + "ca65": "ca65", + "cbmbas": "cbmbas", "ceylon": "ceylon", "cfengine3": "cfengine3", "cfs": "cfs", + "cheetah": "cheetah", + "clojure": "clojure", "cmake": "cmake", "cobol": "cobol", "cobolfree": "cobolfree", + "coffee_script": "coffee-script", "cfm": "cfm", "common_lisp": "common-lisp", "coq": "coq", + "cpp_objdump": "cpp-objdump", "croc": "croc", "css": "css", "css_django": "css+django", + "css_genshitext": "css+genshitext", "css_lasso": "css+lasso", "css_mako": "css+mako", + "css_myghty": "css+myghty", "css_php": "css+php", "css_erb": "css+erb", + "css_smarty": "css+smarty", + "cuda": "cuda", "cython": "cython", "d": "d", "d_objdump": "d-objdump", "dpatch": "dpatch", + "dart": "dart", + "control": "control", "sourceslist": "sourceslist", "delphi": "delphi", "dg": "dg", + "diff": "diff", + "django": "django", "dtd": "dtd", "duel": "duel", "dylan": "dylan", + "dylan_console": "dylan-console", + "dylan_lid": "dylan-lid", "ec": "ec", "ecl": "ecl", "elixir": "elixir", "iex": "iex", + "ragel_em": "ragel-em", + "erb": "erb", "erlang": "erlang", "erl": "erl", "evoque": "evoque", "factor": "factor", + "fancy": "fancy", + "fan": "fan", "felix": "felix", "fortran": "fortran", "Clipper": "Clipper", + "fsharp": "fsharp", "gas": "gas", + "genshi": "genshi", "genshitext": "genshitext", "pot": "pot", "Cucumber": "Cucumber", + "glsl": "glsl", + "gnuplot": "gnuplot", "go": "go", "gooddata_cl": "gooddata-cl", "gosu": "gosu", "gst": "gst", + "groff": "groff", + "groovy": "groovy", "haml": "haml", "haskell": "haskell", "hx": "hx", "html": "html", + "html_cheetah": "html+cheetah", "html_django": "html+django", "html_evoque": "html+evoque", + "html_genshi": "html+genshi", "html_lasso": "html+lasso", "html_mako": "html+mako", + "html_myghty": "html+myghty", "html_php": "html+php", "html_smarty": "html+smarty", + "html_velocity": "html+velocity", "http": "http", "haxeml": "haxeml", "hybris": "hybris", + "idl": "idl", + "ini": "ini", "io": "io", "ioke": "ioke", "irc": "irc", "jade": "jade", "jags": "jags", + "java": "java", + "jsp": "jsp", "js": "js", "js_cheetah": "js+cheetah", "js_django": "js+django", + "js_genshitext": "js+genshitext", "js_lasso": "js+lasso", "js_mako": "js+mako", + "js_myghty": "js+myghty", + "js_php": "js+php", "js_erb": "js+erb", "js_smarty": "js+smarty", "json": "json", + "julia": "julia", + "jlcon": "jlcon", "kconfig": "kconfig", "koka": "koka", "kotlin": "kotlin", "lasso": "lasso", + "lighty": "lighty", "lhs": "lhs", "live_script": "live-script", "llvm": "llvm", + "logos": "logos", + "logtalk": "logtalk", "lua": "lua", "make": "make", "mako": "mako", "maql": "maql", + "mason": "mason", + "matlab": "matlab", "matlabsession": "matlabsession", "minid": "minid", + "modelica": "modelica", + "modula2": "modula2", "trac_wiki": "trac-wiki", "monkey": "monkey", "moocode": "moocode", + "moon": "moon", + "mscgen": "mscgen", "mupad": "mupad", "mxml": "mxml", "myghty": "myghty", "mysql": "mysql", + "nasm": "nasm", + "nemerle": "nemerle", "newlisp": "newlisp", "newspeak": "newspeak", "nginx": "nginx", + "nimrod": "nimrod", + "nsis": "nsis", "numpy": "numpy", "objdump": "objdump", "objective_c": "objective-c", + "objective_c_+": "objective-c++", "objective_j": "objective-j", "ocaml": "ocaml", + "octave": "octave", + "ooc": "ooc", "opa": "opa", "openedge": "openedge", "perl": "perl", "php": "php", + "plpgsql": "plpgsql", + "psql": "psql", "postgresql": "postgresql", "postscript": "postscript", "pov": "pov", + "powershell": "powershell", "prolog": "prolog", "properties": "properties", + "protobuf": "protobuf", + "puppet": "puppet", "pypylog": "pypylog", "python": "python", "python3": "python3", + "py3tb": "py3tb", + "pycon": "pycon", "pytb": "pytb", "qml": "qml", "racket": "racket", "ragel": "ragel", + "ragel_c": "ragel-c", + "ragel_cpp": "ragel-cpp", "ragel_d": "ragel-d", "ragel_java": "ragel-java", + "ragel_objc": "ragel-objc", + "ragel_ruby": "ragel-ruby", "raw": "raw", "rconsole": "rconsole", "rd": "rd", + "rebol": "rebol", + "redcode": "redcode", "registry": "registry", "rst": "rst", "rhtml": "rhtml", + "RobotFramework": "RobotFramework", "spec": "spec", "rb": "rb", "rbcon": "rbcon", + "rust": "rust", + "splus": "splus", "sass": "sass", "scala": "scala", "ssp": "ssp", "scaml": "scaml", + "scheme": "scheme", + "scilab": "scilab", "scss": "scss", "shell_session": "shell-session", "smali": "smali", + "smalltalk": "smalltalk", "smarty": "smarty", "snobol": "snobol", "sp": "sp", "sql": "sql", + "sqlite3": "sqlite3", "squidconf": "squidconf", "stan": "stan", "sml": "sml", + "systemverilog": "systemverilog", + "tcl": "tcl", "tcsh": "tcsh", "tea": "tea", "tex": "tex", "text": "text", + "treetop": "treetop", "ts": "ts", + "urbiscript": "urbiscript", "vala": "vala", "vb.net": "vb.net", "velocity": "velocity", + "verilog": "verilog", + "vgl": "vgl", "vhdl": "vhdl", "vim": "vim", "xml": "xml", "xml_cheetah": "xml+cheetah", + "xml_django": "xml+django", "xml_evoque": "xml+evoque", "xml_lasso": "xml+lasso", + "xml_mako": "xml+mako", + "xml_myghty": "xml+myghty", "xml_php": "xml+php", "xml_erb": "xml+erb", + "xml_smarty": "xml+smarty", + "xml_velocity": "xml+velocity", "xquery": "xquery", "xslt": "xslt", "xtend": "xtend", + "yaml": "yaml"} + +code_styles_list = {0: "autumn", 1: "borland", 2: "bw", 3: "colorful", 4: "default", 5: "emacs", + 6: "friendly", 7: "fruity", 8: "manni", 9: "monokai", 10: "murphy", 11: "native", + 12: "pastie", 13: "perldoc", 14: "rrt", 15: "tango", 16: "trac", 17: "vim", 18: "vs"} diff --git a/manimlib/imports.py b/manimlib/imports.py index 23c1498e..1020f8ce 100644 --- a/manimlib/imports.py +++ b/manimlib/imports.py @@ -50,6 +50,7 @@ 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.svg.code_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/code_mobject.py b/manimlib/mobject/svg/code_mobject.py new file mode 100644 index 00000000..f41cc159 --- /dev/null +++ b/manimlib/mobject/svg/code_mobject.py @@ -0,0 +1,310 @@ +import html +from manimlib.constants import * +from manimlib.container.container import Container +from manimlib.mobject.geometry import Rectangle, Dot, RoundedRectangle +from manimlib.mobject.shape_matchers import SurroundingRectangle +from manimlib.mobject.svg.text_mobject import Paragraph +from manimlib.mobject.types.vectorized_mobject import VGroup + +import re +from pygments import highlight +from pygments.lexers import get_lexer_by_name +from pygments.formatters.html import HtmlFormatter + +''' +1) Code is VGroup() with three things + 1.1) Code[0] is Code.background_mobject + which can be a + 1.1.1) Rectangle() if background == "rectangle" + 1.1.2) VGroup() of Rectangle() and Dot() for three buttons if background == "window" + 1.2) Code[1] is Code.line_numbers Which is a Paragraph() object, this mean you can use + Code.line_numbers[0] or Code[1][0] to access first line number + 1.3) Code[2] is Code.code + 1.3.1) Which is a Paragraph() with color highlighted, this mean you can use + Code.code[1] or Code[2][1] + line number 1 + Code.code[1][0] or Code.code[1][0] + first character of line number 1 + Code.code[1][0:5] or Code.code[1][0:5] + first five characters of line number 1 +''' + + +class Code(VGroup): + CONFIG = { + "tab_width": 3, + "line_spacing": 0.1, + "scale_factor": 0.5, + "run_time": 1, + "font": 'Monospac821 BT', + 'stroke_width': 0, + 'margin': 0.3, + 'indentation_char': " ", + "background": "rectangle", # or window + "corner_radius": 0.2, + 'insert_line_no': True, + 'line_no_from': 1, + "line_no_buff": 0.4, + 'style': 'vim', + 'language': 'cpp', + 'generate_html_file': False + } + + def __init__(self, file_name=None, **kwargs): + Container.__init__(self, **kwargs) + self.file_name = file_name or self.file_name + self.ensure_valid_file() + self.style = self.style.lower() + self.gen_html_string() + strati = self.html_string.find("background:") + self.background_color = self.html_string[strati + 12:strati + 19] + self.gen_code_json() + + self.code = self.gen_colored_lines() + if self.insert_line_no: + self.line_numbers = self.gen_line_numbers() + self.line_numbers.next_to(self.code, direction=LEFT, buff=self.line_no_buff) + + if self.background == "rectangle": + if self.insert_line_no: + forground = VGroup(self.code, self.line_numbers) + else: + forground = self.code + self.background_mobject = SurroundingRectangle(forground, buff=self.margin, + color=self.background_color, + fill_color=self.background_color, + stroke_width=0, + fill_opacity=1, ) + self.background_mobject.round_corners(self.corner_radius) + else: + if self.insert_line_no: + forground = VGroup(self.code, self.line_numbers) + else: + forground = self.code + + height = forground.get_height() + 0.1 * 3 + 2 * self.margin + width = forground.get_width() + 0.1 * 3 + 2 * self.margin + + rrect = RoundedRectangle(corner_radius=self.corner_radius, height=height, width=width, + stroke_width=0, + color=self.background_color, fill_opacity=1) + red_button = Dot(radius=0.1, stroke_width=0, color='#ff5f56') + red_button.shift(LEFT * 0.1 * 3) + yellow_button = Dot(radius=0.1, stroke_width=0, color='#ffbd2e') + green_button = Dot(radius=0.1, stroke_width=0, color='#27c93f') + green_button.shift(RIGHT * 0.1 * 3) + buttons = VGroup(red_button, yellow_button, green_button) + buttons.shift( + UP * (height / 2 - 0.1 * 2 - 0.05) + LEFT * (width / 2 - 0.1 * 5 - self.corner_radius / 2 - 0.05)) + + self.background_mobject = VGroup(rrect, buttons) + x = (height - forground.get_height()) / 2 - 0.1 * 3 + self.background_mobject.shift(forground.get_center()) + self.background_mobject.shift(UP * x) + + if self.insert_line_no: + VGroup.__init__(self, self.background_mobject, self.line_numbers, *self.code, **kwargs) + else: + VGroup.__init__(self, self.background_mobject, Dot(fill_opacity=0, stroke_opacity=0), *self.code, **kwargs) + + self.move_to(np.array([0, 0, 0])) + + def apply_points_function_about_point(self, func, about_point=None, about_edge=None): + if about_point is None: + if about_edge is None: + about_edge = self.get_corner(UP + LEFT) + about_point = self.get_critical_point(about_edge) + for mob in self.family_members_with_points(): + mob.points -= about_point + mob.points = func(mob.points) + mob.points += about_point + return self + + def ensure_valid_file(self): + if self.file_name is None: + raise Exception("Must specify file for Code") + possible_paths = [ + os.path.join(os.path.join("assets", "codes"), self.file_name), + self.file_name, + ] + for path in possible_paths: + if os.path.exists(path): + self.file_path = path + return + raise IOError("No file matching %s in codes directory" % + self.file_name) + + def gen_line_numbers(self): + line_numbers_array = [] + for line_no in range(0, self.code_json.__len__()): + number = str(self.line_no_from + line_no) + line_numbers_array.append(number) + line_numbers = Paragraph(*[i for i in line_numbers_array], line_spacing=self.line_spacing, + alignment="right", font=self.font, stroke_width=self.stroke_width).scale(self.scale_factor) + return line_numbers + + def gen_colored_lines(self): + lines_text = [] + for line_no in range(0, self.code_json.__len__()): + line_str = "" + for word_index in range(self.code_json[line_no].__len__()): + line_str = line_str + self.code_json[line_no][word_index][0] + lines_text.append(self.tab_spaces[line_no] * "\t" + line_str) + code = Paragraph(*[i for i in lines_text], line_spacing=self.line_spacing, tab_width=self.tab_width, + alignment="left", font=self.font, stroke_width=self.stroke_width).scale(self.scale_factor) + for line_no in range(code.__len__()): + line = code[line_no] + line_char_index = self.tab_spaces[line_no] + for word_index in range(self.code_json[line_no].__len__()): + line[line_char_index:line_char_index + self.code_json[line_no][word_index][0].__len__()].set_color( + self.code_json[line_no][word_index][1]) + line_char_index += self.code_json[line_no][word_index][0].__len__() + return code + + def gen_html_string(self): + file = open(self.file_path, "r") + code_str = file.read() + file.close() + self.html_string = hilite_me(code_str, self.language, {}, self.style, self.insert_line_no, + "border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;") + if self.generate_html_file: + os.makedirs(os.path.join("assets", "codes", "generated_html_files"), exist_ok=True) + file = open(os.path.join("assets", "codes", "generated_html_files", self.file_name + ".html"), "w") + file.write(self.html_string) + file.close() + + def gen_code_json(self): + if self.background_color == "#111111" or \ + self.background_color == "#272822" or \ + self.background_color == "#202020" or \ + self.background_color == "#000000": + self.default_color = "#ffffff" + else: + self.default_color = "#000000" + for i in range(3, -1, -1): + self.html_string = self.html_string.replace("" + " " * i, "") + for i in range(10, -1, -1): + self.html_string = self.html_string.replace("" + " " * i, " " * i + "") + self.html_string = self.html_string.replace("background-color:", "background:") + + if self.insert_line_no: + start_point = self.html_string.find("
") + lines[0] = lines[0][start_point + 1:] + # print(lines) + self.code_json = [] + self.tab_spaces = [] + code_json_line_index = -1 + for line_index in range(0, lines.__len__()): + if lines[line_index].__len__() == 0: + continue + # print(lines[line_index]) + self.code_json.append([]) + code_json_line_index = code_json_line_index + 1 + if lines[line_index].startswith(self.indentation_char): + start_point = lines[line_index].find("<") + starting_string = lines[line_index][:start_point] + indentation_char_count = lines[line_index][:start_point].count(self.indentation_char) + if starting_string.__len__() != indentation_char_count * self.indentation_char.__len__(): + lines[line_index] = "\t" * indentation_char_count + starting_string[starting_string.rfind( + self.indentation_char) + self.indentation_char.__len__():] + \ + lines[line_index][start_point:] + else: + lines[line_index] = "\t" * indentation_char_count + lines[line_index][start_point:] + + indentation_char_count = 0 + while lines[line_index][indentation_char_count] == '\t': + indentation_char_count = indentation_char_count + 1 + self.tab_spaces.append(indentation_char_count) + # print(lines[line_index]) + lines[line_index] = self.correct_non_span(lines[line_index]) + # print(lines[line_index]) + words = lines[line_index].split("") + end_point = words[word_index].find("") + text = words[word_index][start_point + 1:end_point] + text = html.unescape(text) + if text != "": + # print(text, "'" + color + "'") + self.code_json[code_json_line_index].append([text, color]) + # print(self.code_json) + + def correct_non_span(self, line_str): + words = line_str.split("") + line_str = "" + for i in range(0, words.__len__()): + if i != words.__len__() - 1: + j = words[i].find("' + words[i][starti:j] + "" + else: + temp = '' + words[i][starti:j] + temp = temp + words[i][j:] + words[i] = temp + if words[i] != "": + line_str = line_str + words[i] + "" + return line_str + + +def hilite_me(code, lexer, options, style, linenos, divstyles): + lexer = lexer or 'python' + style = style or 'colorful' + defstyles = 'overflow:auto;width:auto;' + + formatter = HtmlFormatter(style=style, + linenos=False, + noclasses=True, + cssclass='', + cssstyles=defstyles + divstyles, + prestyles='margin: 0') + html = highlight(code, get_lexer_by_name(lexer, **options), formatter) + if linenos: + html = insert_line_numbers(html) + html = "" + html + return html + + +def get_default_style(): + return 'border:solid gray;border-width:.1em .1em .1em .8em;padding:.2em .6em;' + + +def insert_line_numbers(html): + match = re.search('(]*>)(.*)()', html, re.DOTALL) + if not match: return html + + pre_open = match.group(1) + pre = match.group(2) + pre_close = match.group(3) + + html = html.replace(pre_close, '
' + pre_open + lines + ' | ' + pre_open) + return html diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 5b260c10..6c766722 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -5,8 +5,10 @@ import hashlib import cairo import manimlib.constants as consts from manimlib.constants import * -from manimlib.mobject.geometry import Dot +from manimlib.container.container import Container +from manimlib.mobject.geometry import Dot, Rectangle from manimlib.mobject.svg.svg_mobject import SVGMobject +from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.config_ops import digest_config @@ -51,14 +53,17 @@ class Text(SVGMobject): def __init__(self, text, **config): self.full2short(config) digest_config(self, config) + text_without_tabs = text if text.find('\t') != -1: - text = text.replace('\t', ' '*self.tab_width) - self.text = text + text_without_tabs = text.replace('\t', ' '*self.tab_width) + self.text = text_without_tabs self.lsh = self.size if self.lsh == -1 else self.lsh file_name = self.text2svg() self.remove_last_M(file_name) SVGMobject.__init__(self, file_name, **config) + self.apply_front_and_end_spaces() + self.text = text self.apply_space_chars() nppc = self.n_points_per_cubic_curve @@ -86,20 +91,76 @@ class Text(SVGMobject): if self.height is None and self.width is None: self.scale(TEXT_MOB_SCALE_FACTOR) - def apply_space_chars(self): - indexes = self.find_indexes(' ') + self.find_indexes('\n') - indexes = sorted(indexes, key=lambda i: i[0]) - if len(self.text) == len(indexes): - space = Dot(fill_opacity=0, stroke_opacity=0) - self.submobjects = [space.copy() for _ in range(len(indexes))] - return - for start, _ in indexes: - space = Dot(fill_opacity=0, stroke_opacity=0) - if start == 0: - space.move_to(self.submobjects[0].get_center()) + def get_space_width(self): + size = self.size * 10 + + dir_name = consts.TEXT_DIR + file_name = os.path.join(dir_name, "space") + '.svg' + + surface = cairo.SVGSurface(file_name, 600, 400) + context = cairo.Context(surface) + context.set_font_size(size) + context.move_to(START_X, START_Y) + context.select_font_face(self.font, self.str2slant(self.slant), self.str2weight(self.weight)) + context.move_to(START_X, START_Y) + context.show_text("_") + surface.finish() + svg_with_space = SVGMobject(file_name, height=self.height, + width=self.width, + stroke_width=self.stroke_width, + should_center=self.should_center, + unpack_groups=self.unpack_groups, ) + space_width = svg_with_space.get_width() + return space_width + + def apply_front_and_end_spaces(self): + space_width = self.get_space_width() + max_height = self.get_height() + front_spaces_count = 0 + i = -1 + for i in range(self.text.__len__()): + if self.text[i] == " ": + front_spaces_count += 1 + continue else: - space.move_to(self.submobjects[start-1].get_center()) - self.submobjects.insert(start, space) + break + first_visible_char_index = i + if first_visible_char_index != 0: + space = Rectangle(width=space_width * front_spaces_count, height=max_height, fill_opacity=0, + stroke_opacity=0, + stroke_width=0) + text_width = self.get_width() + space.move_to(np.array([-text_width / 2, max_height / 2, 0])) + self.next_to(space, direction=RIGHT, buff=0) + self.submobjects.insert(0, space) + + i = -1 + last_spaces_count = 0 + for i in range(self.text.__len__() - 1, -1, -1): + if self.text[i] == " ": + last_spaces_count += 1 + continue + else: + break + last_visible_char_index = i + if last_visible_char_index != self.text.__len__() - 1: + space = Rectangle(width=space_width * last_spaces_count, height=max_height, fill_opacity=0, + stroke_opacity=0, + stroke_width=0) + text_width = self.get_width() + space.move_to(np.array([-text_width / 2, max_height / 2, 0])) + self.next_to(space, direction=LEFT, buff=0) + self.submobjects.append(space) + self.move_to(np.array([0,0,0])) + + def apply_space_chars(self): + char_index = 0 + while char_index < self.text.__len__() - 1: + char_index += 1 + if self.text[char_index] == " " or self.text[char_index] == "\t" or self.text[char_index] == "\n": + space = Dot(fill_opacity=0, stroke_opacity=0) + space.move_to(self.submobjects[char_index - 1].get_center()) + self.submobjects.insert(char_index, space) def remove_last_M(self, file_name): with open(file_name, 'r') as fpr: @@ -226,7 +287,8 @@ class Text(SVGMobject): lsh = self.lsh * 10 if self.font == '': - print(NOT_SETTING_FONT_MSG) + if NOT_SETTING_FONT_MSG != '': + print(NOT_SETTING_FONT_MSG) dir_name = consts.TEXT_DIR hash_name = self.text2hash() @@ -257,3 +319,81 @@ class Text(SVGMobject): offset_x += context.text_extents(text)[4] return file_name + +class TextWithFixHeight(Text): + def __init__(self, text, **kwargs): + Text.__init__(self, text, **kwargs) + max_height = Text("(gyt{[/QW", **kwargs).get_height() + rectangle = Rectangle(width=0, height=max_height, fill_opacity=0, + stroke_opacity=0, + stroke_width=0) + self.submobjects.append(rectangle) + +class Paragraph(VGroup): + CONFIG = { + "line_spacing": 0.1, + "alignment": "center", + } + + def __init__(self, *text, **config): + Container.__init__(self, **config) + self.lines_list = list(text) + self.lines = [] + self.lines.append([]) + for line_no in range(self.lines_list.__len__()): + if "\n" in self.lines_list[line_no]: + self.lines_list[line_no:line_no + 1] = self.lines_list[line_no].split("\n") + for line_no in range(self.lines_list.__len__()): + self.lines[0].append(TextWithFixHeight(self.lines_list[line_no], **config)) + self.char_height = TextWithFixHeight("(", **config).get_height() + self.lines.append([]) + self.lines[1].extend([self.alignment for _ in range(self.lines_list.__len__())]) + self.lines[0][0].move_to(np.array([0, 0, 0])) + self.align_lines() + VGroup.__init__(self, *[self.lines[0][i] for i in range(self.lines[0].__len__())], **config) + self.move_to(np.array([0, 0, 0])) + + def set_all_lines_alignment(self, alignment): + self.lines[1] = [alignment for _ in range(self.lines_list.__len__())] + for line_no in range(0, self.lines[0].__len__()): + self.change_alignment_for_a_line(alignment, line_no) + return self + + def set_alignment(self, alignment, line_no): + self.change_alignment_for_a_line(alignment, line_no) + return self + + def change_alignment_for_a_line(self, alignment, line_no): + self.lines[1][line_no] = alignment + if self.lines[1][line_no] == "center": + self[line_no].move_to(self.get_top() + + np.array([0, -self.char_height / 2, 0]) + + np.array([0, - line_no * (self.char_height + self.line_spacing), 0])) + elif self.lines[1][line_no] == "right": + self[line_no].move_to(self.get_top() + + np.array([0, -self.char_height / 2, 0]) + + np.array([self.get_width() / 2 - self.lines[0][line_no].get_width() / 2, + - line_no * (self.char_height + self.line_spacing), 0]) + ) + elif self.lines[1][line_no] == "left": + self[line_no].move_to(self.get_top() + + np.array([0, -self.char_height / 2, 0]) + + np.array([- self.get_width() / 2 + self.lines[0][line_no].get_width() / 2, + - line_no * (self.char_height + self.line_spacing), 0]) + ) + + def align_lines(self): + for line_no in range(0, self.lines[0].__len__()): + if self.lines[1][line_no] == "center": + self.lines[0][line_no].move_to( + np.array([0, 0, 0]) + np.array([0, - line_no * (self.char_height + self.line_spacing), 0])) + elif self.lines[1][line_no] == "left": + self.lines[0][line_no].move_to(np.array([0, 0, 0]) + + np.array([self.lines[0][line_no].get_width() / 2, + - line_no * (self.char_height + self.line_spacing), 0]) + ) + elif self.lines[1][line_no] == "right": + self.lines[0][line_no].move_to(np.array([0, 0, 0]) + + np.array([- self.lines[0][line_no].get_width() / 2, + - line_no * (self.char_height + self.line_spacing), 0]) + ) diff --git a/requirements.txt b/requirements.txt index 66c5fc19..9b54f76a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ tqdm opencv-python pycairo pydub +pygments pyreadline; sys_platform == 'win32' |