diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..631a5ae8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +source = janim +omit = + janim/gui/* + janim/locale/* diff --git a/janim/render/writer.py b/janim/render/writer.py index e33f44e0..1fd3d590 100644 --- a/janim/render/writer.py +++ b/janim/render/writer.py @@ -51,6 +51,10 @@ def __init__(self, anim: TimelineAnim): ) ) + @staticmethod + def writes(anim: TimelineAnim, file_path: str, *, quiet=False) -> None: + VideoWriter(anim).write_all(file_path, quiet=quiet) + def write_all(self, file_path: str, *, quiet=False) -> None: '''将时间轴动画输出到文件中 @@ -137,10 +141,6 @@ def close_video_pipe(self) -> None: self.writing_process.terminate() shutil.move(self.temp_file_path, self.final_file_path) - @staticmethod - def writes(anim: TimelineAnim, file_path: str, *, quiet=False) -> None: - VideoWriter(anim).write_all(file_path, quiet=quiet) - class AudioWriter: def __init__(self, anim: TimelineAnim): diff --git a/pyproject.toml b/pyproject.toml index c0560637..7e9aa4d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,8 @@ doc = [ exclude = [ ".vscode/", ".github/", "assets/", "doc/", "test/", ".pypirc", + ".coveragerc", + "janim/gui/*.ui", "janim/locale/source", "janim/locale/compile.py", "janim/locale/gettext.py", diff --git a/test/__main__.py b/test/__main__.py new file mode 100644 index 00000000..b516aac5 --- /dev/null +++ b/test/__main__.py @@ -0,0 +1,13 @@ +import unittest + + +def main() -> None: + test_loader = unittest.TestLoader() + test_suite = test_loader.discover('test', pattern='test_*.py') + + test_runner = unittest.TextTestRunner(verbosity=2, buffer=True) + test_runner.run(test_suite) + + +if __name__ == '__main__': + main() diff --git a/test/examples/__init__.py b/test/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/examples/examples_of_animations.py b/test/examples/examples_of_animations.py new file mode 100644 index 00000000..8a9ca8cf --- /dev/null +++ b/test/examples/examples_of_animations.py @@ -0,0 +1,660 @@ +# flake8: noqa +from janim.imports import * + + +class AnimGroupExample(Timeline): + def construct(self) -> None: + group = Group( + Circle(fill_alpha=0.5), + Square(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.forward() + self.play( + FadeIn(group[0]), + AnimGroup( + FadeIn(group[1]), + FadeIn(group[2]), + duration=2 + ) + ) + self.forward() + + self.hide(group) + self.play( + FadeIn(group[0], duration=2), + AnimGroup( + FadeIn(group[1]), + FadeIn(group[2]), + at=1, + duration=2 + ) + ) + self.forward() + + +class SuccessionExample(Timeline): + def construct(self) -> None: + group = Group( + Circle(fill_alpha=0.5), + Square(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.forward() + self.play( + Succession( + *map(FadeIn, group) + ) + ) + self.forward() + + self.hide(group) + self.play( + Succession( + *map(FadeIn, group), + offset=1 + ) + ) + self.forward() + + self.hide(group) + self.play( + Succession( + *map(FadeIn, group), + offset=-0.7 + ) + ) + self.forward() + + +class AlignedExample(Timeline): + def construct(self) -> None: + group = Group( + Circle(fill_alpha=0.5), + Square(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.forward() + self.play( + Aligned( + FadeIn(group[0], duration=2), + FadeIn(group[1], duration=3), + FadeIn(group[2], at=0.5, duration=0.5) + ) + ) + self.forward() + + +class ShowPartialExample(Timeline): + def construct(self) -> None: + group = Group( + Line(path_arc=PI), + Line(path_arc=PI), + Circle(), + Circle() + ) + group.points.arrange(aligned_edge=DOWN) + + func1 = lambda p: (0, p.alpha) + func2 = lambda p: (.5 - .5 * p.alpha, .5 + .5 * p.alpha) + + self.play( + ShowPartial(group[0], func1), + ShowPartial(group[1], func2), + ShowPartial(group[2], func1), + ShowPartial(group[3], func2), + duration=3 + ) + + +class CreateExample(Timeline): + def construct(self) -> None: + group = Group( + Square(), + Square(), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.play( + Create(group[0], auto_close_path=False), + Create(group[1:]), + duration=3 + ) + + +class UncreateExample(Timeline): + def construct(self) -> None: + group = Group( + Square(), + Square(), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.play( + Uncreate(group[0], auto_close_path=False), + Uncreate(group[1:]), + duration=3 + ) + + +class DrawBorderThenFillExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.play( + DrawBorderThenFill(group), + duration=3 + ) + self.forward() + + self.play( + DrawBorderThenFill(group, stroke_radius=0.02), + duration=3 + ) + self.forward() + + +class WriteExample(Timeline): + def construct(self) -> None: + dots = Dot(color=BLUE) * 10 + dots.points.arrange().shift(UP) + + txt = Text('Text text Text text') + txt.points.shift(DOWN) + + self.play( + Write(dots, duration=2), + Write(txt, duration=2), + ) + + +class FadeInExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.play( + FadeIn(group), + duration=2 + ) + + +class FadeOutExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.play( + FadeOut(group), + duration=2 + ) + + +class GrowFromPointExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + directions=[UP,LEFT,DOWN,RIGHT] + + for direction in directions: + self.play( + *[ + GrowFromPoint(item, item.points.box.center + direction * 3) + for item in group + ] + ) + + self.forward() + + +class GrowFromCenterExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.play(*map(GrowFromCenter, group)) + + self.forward() + + +class GrowFromEdgeExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + directions=[UP,LEFT,DOWN,RIGHT] + + for direction in directions: + self.play( + *[ + GrowFromEdge(item, direction) + for item in group + ] + ) + + self.forward() + + +class SpinInFromNothingExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.arrange(buff=LARGE_BUFF) + + self.play( + *map(SpinInFromNothing, group), + duration=2 + ) + self.forward() + + +class GrowArrowExample(Timeline): + def construct(self) -> None: + group = Group( + Arrow(ORIGIN, RIGHT * 6), + Vector(RIGHT * 6, color=YELLOW) + ) + group.points.arrange(DOWN, buff=2) + + self.play( + *map(GrowArrow, group), + duration=2 + ) + + self.forward() + + +class GrowDoubleArrowExample(Timeline): + def construct(self) -> None: + group = DoubleArrow(ORIGIN, RIGHT * 7) * 3 + group.points.arrange(DOWN, buff=LARGE_BUFF) + + self.play( + GrowDoubleArrow(group[0], start_ratio=0.2), + GrowDoubleArrow(group[1]), + GrowDoubleArrow(group[2], start_ratio=0.8), + duration=2 + ) + self.forward() + + +class FocusOnExample(Timeline): + def construct(self) -> None: + group = Group( + Dot(), + Typst('x') + ).show() + group.points.scale(2).arrange(RIGHT, buff=2) + + item_or_coord = [ + *group, # Items: Dot and "x" + group.points.box.right + RIGHT * 2 # Coord + ] + + colors=[GREY, RED, BLUE] + + for obj, color in zip(item_or_coord, colors): + self.play(FocusOn(obj, color=color)) + + self.forward(0.3) + + +class IndicateExample(Timeline): + def construct(self) -> None: + formula = Typst('f(x)') + dot = Dot() + + group = Group(formula, dot).show() + group.points.scale(3).arrange(DOWN, buff=3) + + for mob in [formula[2], dot]: + self.play(Indicate(mob)) + + self.forward(0.3) + + +class CircleIndicateExample(Timeline): + def construct(self): + group = Group( + Dot(), + Typst('x') + ).show() + group.points.scale(2).arrange(RIGHT, buff=2) + + self.forward(0.2) + + for obj in group: + self.play(CircleIndicate(obj)) + + self.forward(0.2) + + for obj in group: + self.play(CircleIndicate(obj, scale=1.5)) + + +class ShowCreationThenDestructionExample(Timeline): + def construct(self): + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.scale(1.5).arrange(RIGHT, buff=2) + + self.play( + *[ + ShowCreationThenDestruction(item, auto_close_path=True) + for item in group + ], + duration=2 + ) + self.forward() + + +class ShowCreationThenFadeOutExample(Timeline): + def construct(self): + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ) + group.points.scale(1.5).arrange(RIGHT, buff=2) + + self.play( + *map(ShowCreationThenFadeOut, group) + ) + self.forward() + + +class ShowPassingFlashAroundExample(Timeline): + def construct(self): + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ).show() + group.points.scale(1.5).arrange(RIGHT, buff=2) + + self.play( + *map(ShowPassingFlashAround, group) + ) + self.forward() + + +class ShowCreationThenDestructionAroundExample(Timeline): + def construct(self): + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ).show() + group.points.scale(1.5).arrange(RIGHT, buff=2) + + self.play( + *map(ShowCreationThenDestructionAround, group) + ) + self.forward() + + +class ShowCreationThenFadeAroundExample(Timeline): + def construct(self): + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ).show() + group.points.scale(1.5).arrange(RIGHT, buff=2) + + self.play( + *map(ShowCreationThenFadeAround, group) + ) + self.forward() + + +class FlashExample(Timeline): + def construct(self): + group = Group( + Dot(), + Typst('x') + ).show() + group.points.scale(2).arrange(RIGHT, buff=2) + + item_or_coord = [ + *group, # Items: Dot and "x" + group.points.box.right + RIGHT * 2 # Coord + ] + + colors = [GREY, RED, BLUE] + + self.forward(0.3) + + for obj, color in zip(item_or_coord, colors): + self.play(Flash(obj, color=color, flash_radius=0.5)) + + self.forward(0.3) + + +class ApplyWaveExample(Timeline): + def construct(self): + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ).show() + group.points.scale(1.5).arrange(RIGHT, buff=2) + + self.play(*map(ApplyWave, group)) + self.forward() + + +class WiggleOutThenInExample(Timeline): + def construct(self) -> None: + group = Group( + Square(fill_alpha=0.5), + Circle(fill_alpha=0.5), + Text('Text', font_size=48), + color=BLUE + ).show() + group.points.scale(1.5).arrange(RIGHT, buff=2) + + self.play(*map(WiggleOutThenIn, group)) + self.forward() + + +class HomotopyExample(Timeline): + def construct(self): + def homotopy_func(x, y, z, t): + return [x * t, y * t, z] + + square = Square() + self.play(Homotopy(square, homotopy_func)) + self.forward(0.3) + + +class ComplexHomotopyExample(Timeline): + def construct(self): + def complex_func(z: complex, t: float) -> complex: + return interpolate(z, z**3, t) + + group = Group( + Text('Text'), + Square(side_length=1), + ) + group.points.arrange(RIGHT, buff=2) + + self.play( + *[ComplexHomotopy( + item, + complex_func + ) for item in group] + ) + self.forward(0.3) + + +class MoveAlongPathExample(Timeline): + def construct(self) -> None: + line = Line(ORIGIN, RIGHT * Config.get.frame_width, buff=1) + dot1 = Dot(color=YELLOW) + + curve = ParametricCurve( + lambda t: [math.cos(t) * t * 0.2, math.sin(t) * t * 0.2, 0], + (0, 10, 0.1) + ) + dot2 = Dot(color=YELLOW) + + group = Group(line, curve).show() + group.points.arrange(DOWN) + + self.play( + MoveAlongPath(dot1, line), + MoveAlongPath(dot2, curve), + duration=2 + ) + self.forward(0.3) + + +class RotateExample(Timeline): + def construct(self): + square = Square(side_length=4).show() + + self.play( + Rotate( + square, + PI / 4, + duration=2 + ) + ) + self.forward(0.3) + self.play( + Rotate( + square, + PI, + axis=RIGHT, + duration=2, + ) + ) + self.forward(0.3) + + +class RotatingExample(Timeline): + def construct(self): + square = Square(side_length=4).show() + + self.play( + Rotating( + square, + PI / 4, + duration=2 + ) + ) + self.forward(0.3) + self.play( + Rotating( + square, + PI, + axis=RIGHT, + duration=2, + ) + ) + self.forward(0.3) + + +class TransformExample(Timeline): + def construct(self): + A = Text('Text-A', font_size=72) + B = Text('Text-B', font_size=72) + C = Text('C-Text', font_size=72) + + A.show() + self.forward() + self.play(Transform(A, B)) + self.forward() + self.play(Transform(B, C)) + self.forward() + + +class TransformInSegmentsExample(Timeline): + def construct(self): + typ1 = Typst('sin x + cos x') + typ2 = Typst('cos y + sin y') + Group(typ1, typ2).points.scale(3) + + self.show(typ1) + self.forward(0.5) + self.play(TransformInSegments(typ1, [[0,3,4], [5,8,9]], + typ2, ..., + lag_ratio=0.5)) + self.forward(0.5) + + +class MethodTransformExample(Timeline): + def construct(self): + A = Text("Text-A") + A.points.to_border(LEFT) + + A.show() + self.forward() + self.play( + A.anim.points.scale(3).shift(RIGHT * 7 + UP * 2) + ) + self.play( + A.anim.color.set(BLUE) + ) + self.forward() diff --git a/test/examples/examples_of_bugs.py b/test/examples/examples_of_bugs.py new file mode 100644 index 00000000..5b693525 --- /dev/null +++ b/test/examples/examples_of_bugs.py @@ -0,0 +1,25 @@ +# flake8: noqa +import random + +from janim.imports import * + + +class TestSuccessionFadeOutBug(Timeline): + def construct(self) -> None: + circle = Circle() + + random.seed(114514) + + for i in range(10): + self.prepare(FadeIn(circle), duration=0.32) + self.schedule(self.current_time + 0.32, circle.hide) + self.forward(0.5) + + self.play( + Succession( + Create(circle), + FadeOut(circle, duration=0.9 + 0.2 * random.random()) + ), + duration=0.9 + 0.2 * random.random() + ) + self.forward(0.3) diff --git a/test/examples/ref/AlignedExample.mov b/test/examples/ref/AlignedExample.mov new file mode 100644 index 00000000..d1b2aa61 Binary files /dev/null and b/test/examples/ref/AlignedExample.mov differ diff --git a/test/examples/ref/AnimGroupExample.mov b/test/examples/ref/AnimGroupExample.mov new file mode 100644 index 00000000..aa6deeaa Binary files /dev/null and b/test/examples/ref/AnimGroupExample.mov differ diff --git a/test/examples/ref/AnimatingPiExample.mov b/test/examples/ref/AnimatingPiExample.mov new file mode 100644 index 00000000..6ef49d95 Binary files /dev/null and b/test/examples/ref/AnimatingPiExample.mov differ diff --git a/test/examples/ref/ApplyWaveExample.mov b/test/examples/ref/ApplyWaveExample.mov new file mode 100644 index 00000000..c9658d9a Binary files /dev/null and b/test/examples/ref/ApplyWaveExample.mov differ diff --git a/test/examples/ref/CircleIndicateExample.mov b/test/examples/ref/CircleIndicateExample.mov new file mode 100644 index 00000000..85f767c1 Binary files /dev/null and b/test/examples/ref/CircleIndicateExample.mov differ diff --git a/test/examples/ref/ComplexHomotopyExample.mov b/test/examples/ref/ComplexHomotopyExample.mov new file mode 100644 index 00000000..d326c68a Binary files /dev/null and b/test/examples/ref/ComplexHomotopyExample.mov differ diff --git a/test/examples/ref/CreateExample.mov b/test/examples/ref/CreateExample.mov new file mode 100644 index 00000000..204c0ceb Binary files /dev/null and b/test/examples/ref/CreateExample.mov differ diff --git a/test/examples/ref/DrawBorderThenFillExample.mov b/test/examples/ref/DrawBorderThenFillExample.mov new file mode 100644 index 00000000..9d1d4749 Binary files /dev/null and b/test/examples/ref/DrawBorderThenFillExample.mov differ diff --git a/test/examples/ref/FadeInExample.mov b/test/examples/ref/FadeInExample.mov new file mode 100644 index 00000000..45632c12 Binary files /dev/null and b/test/examples/ref/FadeInExample.mov differ diff --git a/test/examples/ref/FadeOutExample.mov b/test/examples/ref/FadeOutExample.mov new file mode 100644 index 00000000..3b7dad3c Binary files /dev/null and b/test/examples/ref/FadeOutExample.mov differ diff --git a/test/examples/ref/FlashExample.mov b/test/examples/ref/FlashExample.mov new file mode 100644 index 00000000..aadfdb00 Binary files /dev/null and b/test/examples/ref/FlashExample.mov differ diff --git a/test/examples/ref/FocusOnExample.mov b/test/examples/ref/FocusOnExample.mov new file mode 100644 index 00000000..d64405a0 Binary files /dev/null and b/test/examples/ref/FocusOnExample.mov differ diff --git a/test/examples/ref/GrowArrowExample.mov b/test/examples/ref/GrowArrowExample.mov new file mode 100644 index 00000000..74f31d20 Binary files /dev/null and b/test/examples/ref/GrowArrowExample.mov differ diff --git a/test/examples/ref/GrowDoubleArrowExample.mov b/test/examples/ref/GrowDoubleArrowExample.mov new file mode 100644 index 00000000..318e6c51 Binary files /dev/null and b/test/examples/ref/GrowDoubleArrowExample.mov differ diff --git a/test/examples/ref/GrowFromCenterExample.mov b/test/examples/ref/GrowFromCenterExample.mov new file mode 100644 index 00000000..a3ac8c99 Binary files /dev/null and b/test/examples/ref/GrowFromCenterExample.mov differ diff --git a/test/examples/ref/GrowFromEdgeExample.mov b/test/examples/ref/GrowFromEdgeExample.mov new file mode 100644 index 00000000..89319ae0 Binary files /dev/null and b/test/examples/ref/GrowFromEdgeExample.mov differ diff --git a/test/examples/ref/GrowFromPointExample.mov b/test/examples/ref/GrowFromPointExample.mov new file mode 100644 index 00000000..fd8803f6 Binary files /dev/null and b/test/examples/ref/GrowFromPointExample.mov differ diff --git a/test/examples/ref/HelloJAnimExample.mov b/test/examples/ref/HelloJAnimExample.mov new file mode 100644 index 00000000..22431453 Binary files /dev/null and b/test/examples/ref/HelloJAnimExample.mov differ diff --git a/test/examples/ref/HomotopyExample.mov b/test/examples/ref/HomotopyExample.mov new file mode 100644 index 00000000..499287cb Binary files /dev/null and b/test/examples/ref/HomotopyExample.mov differ diff --git a/test/examples/ref/IndicateExample.mov b/test/examples/ref/IndicateExample.mov new file mode 100644 index 00000000..e4041582 Binary files /dev/null and b/test/examples/ref/IndicateExample.mov differ diff --git a/test/examples/ref/MethodTransformExample.mov b/test/examples/ref/MethodTransformExample.mov new file mode 100644 index 00000000..93a028e4 Binary files /dev/null and b/test/examples/ref/MethodTransformExample.mov differ diff --git a/test/examples/ref/MoveAlongPathExample.mov b/test/examples/ref/MoveAlongPathExample.mov new file mode 100644 index 00000000..4fb8fa0a Binary files /dev/null and b/test/examples/ref/MoveAlongPathExample.mov differ diff --git a/test/examples/ref/NumberPlaneExample.mov b/test/examples/ref/NumberPlaneExample.mov new file mode 100644 index 00000000..4a24304f Binary files /dev/null and b/test/examples/ref/NumberPlaneExample.mov differ diff --git a/test/examples/ref/RotateExample.mov b/test/examples/ref/RotateExample.mov new file mode 100644 index 00000000..81190c73 Binary files /dev/null and b/test/examples/ref/RotateExample.mov differ diff --git a/test/examples/ref/RotatingExample.mov b/test/examples/ref/RotatingExample.mov new file mode 100644 index 00000000..0156e6d5 Binary files /dev/null and b/test/examples/ref/RotatingExample.mov differ diff --git a/test/examples/ref/ShowCreationThenDestructionAroundExample.mov b/test/examples/ref/ShowCreationThenDestructionAroundExample.mov new file mode 100644 index 00000000..cf15dbcf Binary files /dev/null and b/test/examples/ref/ShowCreationThenDestructionAroundExample.mov differ diff --git a/test/examples/ref/ShowCreationThenDestructionExample.mov b/test/examples/ref/ShowCreationThenDestructionExample.mov new file mode 100644 index 00000000..20b2ef46 Binary files /dev/null and b/test/examples/ref/ShowCreationThenDestructionExample.mov differ diff --git a/test/examples/ref/ShowCreationThenFadeAroundExample.mov b/test/examples/ref/ShowCreationThenFadeAroundExample.mov new file mode 100644 index 00000000..094c596c Binary files /dev/null and b/test/examples/ref/ShowCreationThenFadeAroundExample.mov differ diff --git a/test/examples/ref/ShowCreationThenFadeOutExample.mov b/test/examples/ref/ShowCreationThenFadeOutExample.mov new file mode 100644 index 00000000..2d329d6f Binary files /dev/null and b/test/examples/ref/ShowCreationThenFadeOutExample.mov differ diff --git a/test/examples/ref/ShowPartialExample.mov b/test/examples/ref/ShowPartialExample.mov new file mode 100644 index 00000000..7c810f02 Binary files /dev/null and b/test/examples/ref/ShowPartialExample.mov differ diff --git a/test/examples/ref/ShowPassingFlashAroundExample.mov b/test/examples/ref/ShowPassingFlashAroundExample.mov new file mode 100644 index 00000000..4bc45e82 Binary files /dev/null and b/test/examples/ref/ShowPassingFlashAroundExample.mov differ diff --git a/test/examples/ref/SimpleCurveExample.mov b/test/examples/ref/SimpleCurveExample.mov new file mode 100644 index 00000000..90bb5477 Binary files /dev/null and b/test/examples/ref/SimpleCurveExample.mov differ diff --git a/test/examples/ref/SpinInFromNothingExample.mov b/test/examples/ref/SpinInFromNothingExample.mov new file mode 100644 index 00000000..559c0e37 Binary files /dev/null and b/test/examples/ref/SpinInFromNothingExample.mov differ diff --git a/test/examples/ref/SuccessionExample.mov b/test/examples/ref/SuccessionExample.mov new file mode 100644 index 00000000..b1909a01 Binary files /dev/null and b/test/examples/ref/SuccessionExample.mov differ diff --git a/test/examples/ref/TestSuccessionFadeOutBug.mov b/test/examples/ref/TestSuccessionFadeOutBug.mov new file mode 100644 index 00000000..3eba9910 Binary files /dev/null and b/test/examples/ref/TestSuccessionFadeOutBug.mov differ diff --git a/test/examples/ref/TextExample.mov b/test/examples/ref/TextExample.mov new file mode 100644 index 00000000..15eed411 Binary files /dev/null and b/test/examples/ref/TextExample.mov differ diff --git a/test/examples/ref/TransformExample.mov b/test/examples/ref/TransformExample.mov new file mode 100644 index 00000000..cf5daace Binary files /dev/null and b/test/examples/ref/TransformExample.mov differ diff --git a/test/examples/ref/TransformInSegmentsExample.mov b/test/examples/ref/TransformInSegmentsExample.mov new file mode 100644 index 00000000..2e114555 Binary files /dev/null and b/test/examples/ref/TransformInSegmentsExample.mov differ diff --git a/test/examples/ref/TypstExample.mov b/test/examples/ref/TypstExample.mov new file mode 100644 index 00000000..e310e903 Binary files /dev/null and b/test/examples/ref/TypstExample.mov differ diff --git a/test/examples/ref/UncreateExample.mov b/test/examples/ref/UncreateExample.mov new file mode 100644 index 00000000..cfbc30cf Binary files /dev/null and b/test/examples/ref/UncreateExample.mov differ diff --git a/test/examples/ref/UpdaterExample.mov b/test/examples/ref/UpdaterExample.mov new file mode 100644 index 00000000..55ddcd88 Binary files /dev/null and b/test/examples/ref/UpdaterExample.mov differ diff --git a/test/examples/ref/WiggleOutThenInExample.mov b/test/examples/ref/WiggleOutThenInExample.mov new file mode 100644 index 00000000..4ec97de9 Binary files /dev/null and b/test/examples/ref/WiggleOutThenInExample.mov differ diff --git a/test/examples/ref/WriteExample.mov b/test/examples/ref/WriteExample.mov new file mode 100644 index 00000000..5870a11d Binary files /dev/null and b/test/examples/ref/WriteExample.mov differ diff --git a/test/examples/test_examples.py b/test/examples/test_examples.py new file mode 100644 index 00000000..e5a42f26 --- /dev/null +++ b/test/examples/test_examples.py @@ -0,0 +1,111 @@ +import os +import subprocess as sp +import sys +import unittest +from typing import Generator + +import numpy as np + +import janim.examples as examples +from janim.anims.timeline import Timeline +from janim.cli import get_all_timelines_from_module +from janim.render.writer import VideoWriter +from janim.utils.config import Config +from janim.utils.file_ops import guarantee_existence + +sys.path.append(os.path.dirname(__file__)) + +import examples_of_animations as anim_examples +import examples_of_bugs as bug_examples + +WIDTH = 192 * 2 +HEIGHT = 108 * 2 + + +def get_ref_dir() -> str: + return os.path.join(os.path.dirname(__file__), 'ref') + + +def get_timelines_for_test() -> list[type[Timeline]]: + timelines: list[type[Timeline]] = [] + timelines += get_all_timelines_from_module(examples) + timelines += get_all_timelines_from_module(anim_examples) + timelines += get_all_timelines_from_module(bug_examples) + return timelines + + +def load_tests(loader, standard_tests, pattern) -> unittest.TestSuite: + class ExampleTester(unittest.TestCase): + def __init__(self, timeline_cls: type[Timeline], ref_path: str): + super().__init__('test') + self.timeline_cls = timeline_cls + self.ref_path = ref_path + + def __str__(self) -> str: + return f"{self.timeline_cls.__name__} ({self.__class__.__name__})" + + def test(self) -> None: + temp_file = os.path.join(Config.get.temp_dir, 'test_example.mov') + + with Config(pixel_width=WIDTH, + pixel_height=HEIGHT, + fps=5): + anim = self.timeline_cls().build(quiet=True) + VideoWriter.writes(anim, temp_file, quiet=True) + + render_frames = self.get_frames(temp_file) + ref_frames = self.get_frames(self.ref_path) + + for buf1, buf2 in zip(render_frames, ref_frames, strict=True): + render_data = np.frombuffer(buf1, dtype=np.uint8).astype(np.int16) + ref_data = np.frombuffer(buf2, dtype=np.uint8).astype(np.int16) + delta = np.abs(render_data - ref_data) + error = delta.sum() + assert error == 0 + + @staticmethod + def get_frames(video_path: str) -> Generator[bytes, None, None]: + ffmpeg_command = [ + 'ffmpeg', + '-i', video_path, + '-f', 'rawvideo', + '-pix_fmt', 'rgba', + '-' + ] + process = sp.Popen(ffmpeg_command, stdout=sp.PIPE, stderr=sp.PIPE) + + while raw_frame := process.stdout.read(HEIGHT * WIDTH * 4): + yield raw_frame + + path = get_ref_dir() + timelines = get_timelines_for_test() + suite = unittest.TestSuite() + suite.addTests([ + ExampleTester(timeline, os.path.join(path, f'{timeline.__name__}.mov')) + for timeline in timelines + ]) + return suite + + +def generate_ref() -> None: + includes = sys.argv[1:] + output_dir = guarantee_existence(get_ref_dir()) + + with Config(pixel_width=WIDTH, + pixel_height=HEIGHT, + fps=5, + output_dir=output_dir): + for timeline in get_timelines_for_test(): + if includes and timeline.__name__ not in includes: + continue + VideoWriter.writes(timeline().build(), + os.path.join(output_dir, f'{timeline.__name__}.mov')) + + +if __name__ == '__main__': + print('Warning: 直接执行该文件会进行样例视频输出,作为单元测试样例,确认继续执行吗?') + print('Warning: Running this file will generate sample video output for unit testing purposes. ' + 'Are you sure you want to proceed?') + ret = input('(y/N): ') + if ret.lower() == 'y': + generate_ref() diff --git a/test/items/test_relation.py b/test/items/test_relation.py index 40970b03..66b683e1 100644 --- a/test/items/test_relation.py +++ b/test/items/test_relation.py @@ -49,6 +49,9 @@ def test_relation(self) -> None: self.assertListEqual(root.descendants(), [m1, m2, m9]) def test_relation_family(self) -> None: + R = Item + class C1(R): ... + class C2(R): ... r''' m0(C1) | \____ @@ -64,9 +67,6 @@ def test_relation_family(self) -> None: | / m8(R) ''' - R = Item - class C1(R): ... - class C2(R): ... m: list[Item] = [C1(), C1(), R(), C2(), C2(), C1(), R(), C2(), R()] diff --git a/test/test.py b/test/test.py deleted file mode 100644 index b3de2b79..00000000 --- a/test/test.py +++ /dev/null @@ -1,6 +0,0 @@ -from janim.gui.anim_viewer import * -from janim.imports import * -from janim.logger import log -from janim.render.writer import * - -log.setLevel('NOTSET') diff --git a/test/test_examples.py b/test/test_examples.py deleted file mode 100644 index fe8d93da..00000000 --- a/test/test_examples.py +++ /dev/null @@ -1,23 +0,0 @@ -import inspect -import unittest - -import numpy as np - -import janim.examples as examples -from janim.anims.timeline import Timeline - - -class TestExamples(unittest.TestCase): - def test_examples(self) -> None: - classes = [ - value - for value in examples.__dict__.values() - if isinstance(value, type) and issubclass(value, Timeline) and value.__module__ == examples.__name__ - ] - classes.sort(key=lambda x: inspect.getsourcelines(x)[1]) - - for timeline_cls in classes: - anim = timeline_cls().build(quiet=True) - for t in np.arange(0, anim.global_range.duration, 0.5): - anim.anim_on(t) - anim.anim_on(anim.global_range.duration)