Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from collections.abc import Sequence | |
| from manim import ( | |
| BLUE, | |
| DOWN, | |
| LEFT, | |
| RIGHT, | |
| UP, | |
| WHITE, | |
| Arrow, | |
| Code, | |
| Line, | |
| NumberLine, | |
| NumberPlane, | |
| Text, | |
| Vector, | |
| VGroup, | |
| ) | |
| from manim.mobject.mobject import Mobject | |
| from primitives.theme import COLOR_3B1B_BLUE, COLOR_3B1B_YELLOW | |
| DEFAULT_FONT_SIZE = 40.0 | |
| def get_text_panel(text: str, color: str = BLUE, font_size: float = DEFAULT_FONT_SIZE) -> Text: | |
| """Readable title/body text with consistent defaults.""" | |
| return Text(str(text), font_size=font_size, color=color) | |
| def get_array_block( | |
| values: Sequence[str | float | int], | |
| highlight_index: int | None = 0, | |
| cell_font_size: float = 32.0, | |
| ) -> VGroup: | |
| """Horizontal row of cells; optional highlight index (negative disables).""" | |
| cells: list[Text] = [] | |
| for i, v in enumerate(values): | |
| t = Text(str(v), font_size=cell_font_size) | |
| if highlight_index is not None and i == highlight_index: | |
| t.set_color(BLUE) | |
| else: | |
| t.set_color(WHITE) | |
| cells.append(t) | |
| group = VGroup(*cells) | |
| group.arrange(RIGHT, buff=0.35) | |
| return group | |
| def get_code_box(code_string: str, language: str = "python") -> Mobject: | |
| """Syntax-highlighted code block with fallback for missing fonts/dependencies.""" | |
| from manim import BLACK, RoundedRectangle | |
| try: | |
| return Code( | |
| code_string=code_string, language=language, add_line_numbers=False, style="monokai" | |
| ) | |
| except Exception: | |
| # Fallback: Dark background + Text | |
| bg = RoundedRectangle(corner_radius=0.1, fill_color=BLACK, fill_opacity=0.8, stroke_width=1) | |
| txt = Text(code_string, font="Monospace", font_size=20) | |
| bg.stretch_to_fit_width(txt.width + 0.5) | |
| bg.stretch_to_fit_height(txt.height + 0.5) | |
| return VGroup(bg, txt) | |
| def get_title_card(title: str, subtitle: str | None = None) -> VGroup: | |
| """Stacked title + optional subtitle.""" | |
| title_m = Text(str(title), font_size=48, weight="BOLD") | |
| parts: list[Text] = [title_m] | |
| if subtitle: | |
| parts.append(Text(str(subtitle), font_size=28).next_to(title_m, DOWN, buff=0.35)) | |
| group = VGroup(*parts) | |
| group.arrange(DOWN, buff=0.25, aligned_edge=LEFT) | |
| return group | |
| def get_bulleted_list(items: Sequence[str]) -> VGroup: | |
| """Bullet list (text-based; avoids LaTeX in CI/dev).""" | |
| rows = [Text(f"• {str(x)}", font_size=30) for x in items] | |
| group = VGroup(*rows) | |
| group.arrange(DOWN, buff=0.25, aligned_edge=LEFT) | |
| return group | |
| def get_equation_block(latex: str) -> Mobject: | |
| """Equation rendering: `MathTex` when LaTeX is installed, otherwise `Text` fallback. | |
| NOTE: The AI system prompt currently specifies LaTeX is NOT installed. | |
| This primitive provides a safe fallback using Text mobjects. | |
| """ | |
| import shutil | |
| from manim import ITALIC, MathTex | |
| # We only use MathTex if explicitly available, otherwise fallback to Text | |
| if shutil.which("latex") is not None and shutil.which("dvips") is not None: | |
| try: | |
| return MathTex(str(latex)) | |
| except Exception: | |
| # Fallback on render error | |
| pass | |
| # Fallback: Use Text with a math-like slant | |
| return Text(str(latex), font_size=36, slant=ITALIC) | |
| def get_labeled_arrow(label: str, buff: float = 0.15) -> VGroup: | |
| """Arrow from LEFT to RIGHT with a text label above the shaft.""" | |
| arrow = Arrow(LEFT * 2.2, RIGHT * 2.2, buff=0.05) | |
| lbl = Text(str(label), font_size=28).next_to(arrow, UP, buff=buff) | |
| return VGroup(arrow, lbl) | |
| def get_number_line( | |
| x_range: Sequence[float], | |
| length: float = 8.0, | |
| include_numbers: bool = False, | |
| ) -> NumberLine: | |
| """Number line with tick spacing from `x_range` (start, end, step). | |
| Defaults to `include_numbers=False` as LaTeX is often unavailable in CI. | |
| """ | |
| import shutil | |
| if len(x_range) < 3: | |
| # Prevent crash on malformed input | |
| start, end, step = (x_range[0], x_range[1], 1.0) if len(x_range) == 2 else (0, 10, 1) | |
| else: | |
| start, end, step = x_range[:3] | |
| # Safe fallback: if LaTeX is missing, force include_numbers=False to prevent crash. | |
| safe_include = include_numbers and (shutil.which("latex") is not None) | |
| return NumberLine( | |
| x_range=(start, end, step), | |
| length=length, | |
| include_numbers=safe_include, | |
| ) | |
| def get_separator_line(width: float = 10.0, color: str = WHITE) -> Line: | |
| """Thin horizontal separator.""" | |
| left = LEFT * (width / 2) | |
| right = RIGHT * (width / 2) | |
| return Line(left, right, color=color, stroke_width=2) | |
| def get_data_chart( | |
| values: Sequence[float], | |
| labels: Sequence[str] | None = None, | |
| max_height: float = 4.0, | |
| width: float = 8.0, | |
| color: str = BLUE, | |
| ) -> VGroup: | |
| """Simple bar chart using Rectangles and Text labels.""" | |
| from manim import ORIGIN, Rectangle | |
| if not values: | |
| return VGroup() | |
| n = len(values) | |
| bar_width = (width / n) * 0.8 | |
| spacing = (width / n) * 0.2 | |
| max_val = max(values) or 1.0 | |
| bars = VGroup() | |
| label_objs = VGroup() | |
| for i, val in enumerate(values): | |
| h = (val / max_val) * max_height | |
| bar = Rectangle( | |
| width=bar_width, height=h, fill_opacity=0.8, fill_color=color, stroke_width=1 | |
| ) | |
| bar.move_to(ORIGIN + RIGHT * (i * (bar_width + spacing)) + UP * (h / 2)) | |
| bars.add(bar) | |
| if labels and i < len(labels): | |
| lbl = Text(labels[i], font_size=20).next_to(bar, DOWN, buff=0.2) | |
| label_objs.add(lbl) | |
| group = VGroup(bars, label_objs) | |
| group.center() | |
| return group | |
| def get_geometric_diagram( | |
| shape_type: str = "triangle", | |
| size: float = 2.0, | |
| color: str = WHITE, | |
| label: str | None = None, | |
| ) -> VGroup: | |
| """Basic geometric shapes with optional center label.""" | |
| from manim import Circle, Square, Triangle | |
| if shape_type.lower() == "circle": | |
| mobj = Circle(radius=size / 2, color=color) | |
| elif shape_type.lower() == "square": | |
| mobj = Square(side_length=size, color=color) | |
| else: | |
| mobj = Triangle(color=color).scale(size / 1.5) | |
| res = VGroup(mobj) | |
| if label: | |
| lbl = Text(label, font_size=24).move_to(mobj.get_center()) | |
| res.add(lbl) | |
| return res | |
| def dynamic_pointer( | |
| target: Mobject, label: str = "Note", direction: str | Sequence[float] | None = None | |
| ) -> VGroup: | |
| """Arrow pointing at a target mobject with a label. Handles string directions.""" | |
| import numpy as np | |
| # Map string directions to Manim vectors | |
| dir_map = {"UP": UP, "DOWN": DOWN, "LEFT": LEFT, "RIGHT": RIGHT} | |
| if isinstance(direction, str): | |
| dir_vec = dir_map.get(direction.upper(), UP) | |
| elif direction is not None: | |
| dir_vec = np.array(direction) | |
| else: | |
| dir_vec = UP | |
| arrow = Arrow(target.get_center() + dir_vec * 1.5, target.get_center() + dir_vec * 0.2, buff=0) | |
| lbl = Text(label, font_size=24).next_to(arrow.get_start(), dir_vec, buff=0.1) | |
| return VGroup(arrow, lbl) | |
| def get_math_grid( | |
| x_range: Sequence[float] = (-8, 8, 1), | |
| y_range: Sequence[float] = (-5, 5, 1), | |
| ) -> NumberPlane: | |
| """3B1B-style NumberPlane with subtle grid lines.""" | |
| return NumberPlane( | |
| x_range=x_range, | |
| y_range=y_range, | |
| background_line_style={ | |
| "stroke_color": COLOR_3B1B_BLUE, | |
| "stroke_width": 1, | |
| "stroke_opacity": 0.1, | |
| }, | |
| axis_config={"stroke_color": WHITE, "stroke_width": 2}, | |
| ) | |
| def get_vector_arrow( | |
| coords: Sequence[float], label: str | None = None, color: str = COLOR_3B1B_YELLOW | |
| ) -> VGroup: | |
| """Vector arrow with optional label at the tip.""" | |
| vec = Vector(coords, color=color) | |
| res = VGroup(vec) | |
| if label: | |
| lbl = Text( | |
| f"({coords[0]}, {coords[1]})" if label == "auto" else label, font_size=20, color=color | |
| ) | |
| lbl.next_to(vec.get_end(), vec.get_vector(), buff=0.2) | |
| res.add(lbl) | |
| return res | |
| def get_matrix_block( | |
| matrix_data: Sequence[Sequence[str | float | int]], | |
| color: str = WHITE, | |
| cell_font_size: float = 28.0, | |
| ) -> VGroup: | |
| """A 2D grid of elements with manually drawn square brackets (no-LaTeX fallback).""" | |
| rows = [] | |
| for r in matrix_data: | |
| # Create a row using get_array_block logic but simplified | |
| cells = [Text(str(v), font_size=cell_font_size, color=color) for v in r] | |
| row_group = VGroup(*cells).arrange(RIGHT, buff=0.6) | |
| rows.append(row_group) | |
| grid = VGroup(*rows).arrange(DOWN, buff=0.6) | |
| # Manual brackets | |
| h = grid.get_height() + 0.5 | |
| w = 0.2 | |
| l_bracket = VGroup( | |
| Line(UP * h / 2 + RIGHT * w, UP * h / 2, color=color), | |
| Line(UP * h / 2, DOWN * h / 2, color=color), | |
| Line(DOWN * h / 2, DOWN * h / 2 + RIGHT * w, color=color), | |
| ).next_to(grid, LEFT, buff=0.2) | |
| r_bracket = l_bracket.copy().flip(RIGHT).next_to(grid, RIGHT, buff=0.2) | |
| return VGroup(grid, l_bracket, r_bracket) | |
| def get_info_box(text: str, color: str = BLUE, width: float = 6.0) -> VGroup: | |
| """A styled information box with a border and background.""" | |
| from manim import BLACK, RoundedRectangle | |
| txt = Text(text, font_size=24, color=WHITE) | |
| box = RoundedRectangle( | |
| corner_radius=0.1, fill_color=BLACK, fill_opacity=0.7, stroke_color=color, stroke_width=2 | |
| ) | |
| box.stretch_to_fit_width(min(txt.width + 1.0, width)) | |
| box.stretch_to_fit_height(txt.height + 0.6) | |
| return VGroup(box, txt) | |
| def get_two_column(left_mobj: Mobject, right_mobj: Mobject, buff: float = 1.0) -> VGroup: | |
| """Arranges two mobjects in a side-by-side (two-column) layout.""" | |
| return VGroup(left_mobj, right_mobj).arrange(RIGHT, buff=buff) | |
| def get_table_block( | |
| headers: Sequence[str], rows: Sequence[Sequence[str]], cell_font_size: float = 24.0 | |
| ) -> VGroup: | |
| """A text-based table with headers and rows (no-LaTeX).""" | |
| from manim import ORIGIN, Line | |
| all_rows = [headers] + list(rows) | |
| row_groups = [] | |
| # Calculate column widths | |
| num_cols = len(headers) | |
| col_widths = [0.0] * num_cols | |
| for r in all_rows: | |
| for i, val in enumerate(r): | |
| txt = Text(str(val), font_size=cell_font_size) | |
| col_widths[i] = max(col_widths[i], txt.width + 0.5) | |
| for r_idx, r in enumerate(all_rows): | |
| cells = [] | |
| curr_x = 0.0 | |
| for i, val in enumerate(r): | |
| txt = Text(str(val), font_size=cell_font_size) | |
| if r_idx == 0: # Header | |
| txt.set_color(BLUE).set_weight("BOLD") | |
| txt.move_to(ORIGIN + RIGHT * (curr_x + col_widths[i] / 2)) | |
| cells.append(txt) | |
| curr_x += col_widths[i] | |
| row_groups.append(VGroup(*cells)) | |
| table = VGroup(*row_groups).arrange(DOWN, buff=0.4, aligned_edge=LEFT) | |
| # Add horizontal line after header | |
| if len(row_groups) > 1: | |
| h_line = Line( | |
| table[0].get_left() + LEFT * 0.1, table[0].get_right() + RIGHT * 0.1, stroke_width=1 | |
| ) | |
| h_line.next_to(table[0], DOWN, buff=0.15) | |
| table.add(h_line) | |
| return table | |
| def get_step_indicator(total: int, current: int, color: str = BLUE) -> VGroup: | |
| """A series of dots representing progress steps.""" | |
| from manim import GRAY, Dot | |
| dots = VGroup() | |
| for i in range(total): | |
| d = Dot(color=color if i < current else GRAY) | |
| if i == current - 1: | |
| d.scale(1.3) | |
| dots.add(d) | |
| return dots.arrange(RIGHT, buff=0.3) | |
| def get_key_value_panel(pairs: Sequence[tuple[str, str]], key_color: str = BLUE) -> VGroup: | |
| """A vertical list of Key: Value pairs.""" | |
| rows = [] | |
| for k, v in pairs: | |
| k_txt = Text(f"{k}:", font_size=24, color=key_color, weight="BOLD") | |
| v_txt = Text(str(v), font_size=24) | |
| row = VGroup(k_txt, v_txt).arrange(RIGHT, buff=0.2, aligned_edge=UP) | |
| rows.append(row) | |
| return VGroup(*rows).arrange(DOWN, buff=0.3, aligned_edge=LEFT) | |