janim 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. janim/__init__.py +3 -0
  2. janim/__main__.py +270 -0
  3. janim/anims/animation.py +119 -0
  4. janim/anims/composition.py +167 -0
  5. janim/anims/creation.py +188 -0
  6. janim/anims/display.py +29 -0
  7. janim/anims/fading.py +118 -0
  8. janim/anims/rotation.py +45 -0
  9. janim/anims/timeline.py +489 -0
  10. janim/anims/transform.py +228 -0
  11. janim/anims/updater.py +131 -0
  12. janim/camera/camera.py +145 -0
  13. janim/camera/camera_info.py +79 -0
  14. janim/components/component.py +266 -0
  15. janim/components/depth.py +87 -0
  16. janim/components/image.py +45 -0
  17. janim/components/points.py +1006 -0
  18. janim/components/radius.py +105 -0
  19. janim/components/rgbas.py +254 -0
  20. janim/components/vpoints.py +510 -0
  21. janim/constants/__init__.py +5 -0
  22. janim/constants/alignment.py +7 -0
  23. janim/constants/colors.py +66 -0
  24. janim/constants/coord.py +20 -0
  25. janim/constants/degrees.py +8 -0
  26. janim/examples.py +32 -0
  27. janim/gui/anim_viewer.py +746 -0
  28. janim/gui/application.py +11 -0
  29. janim/gui/export.png +0 -0
  30. janim/gui/fixed_ratio_widget.py +36 -0
  31. janim/gui/glwidget.py +33 -0
  32. janim/gui/selector.py +222 -0
  33. janim/imports.py +34 -0
  34. janim/items/boolean_ops.py +149 -0
  35. janim/items/geometry/arc.py +313 -0
  36. janim/items/geometry/arrow.py +210 -0
  37. janim/items/geometry/line.py +207 -0
  38. janim/items/geometry/polygon.py +161 -0
  39. janim/items/image_item.py +171 -0
  40. janim/items/item.py +540 -0
  41. janim/items/points.py +110 -0
  42. janim/items/relation.py +199 -0
  43. janim/items/svg/svg_item.py +118 -0
  44. janim/items/svg/typst.py +67 -0
  45. janim/items/svg/typst_template.typ +3 -0
  46. janim/items/text/text.py +397 -0
  47. janim/items/vitem.py +154 -0
  48. janim/logger.py +13 -0
  49. janim/render/base.py +103 -0
  50. janim/render/file_writer.py +122 -0
  51. janim/render/impl.py +235 -0
  52. janim/render/shaders/dotcloud.frag.glsl +21 -0
  53. janim/render/shaders/dotcloud.geom.glsl +50 -0
  54. janim/render/shaders/dotcloud.vert.glsl +19 -0
  55. janim/render/shaders/image.frag.glsl +13 -0
  56. janim/render/shaders/image.vert.glsl +18 -0
  57. janim/render/shaders/vitem.frag.glsl +254 -0
  58. janim/render/shaders/vitem.vert.glsl +14 -0
  59. janim/render/texture.py +32 -0
  60. janim/typing.py +31 -0
  61. janim/utils/bezier.py +405 -0
  62. janim/utils/config.py +156 -0
  63. janim/utils/data.py +11 -0
  64. janim/utils/file_ops.py +29 -0
  65. janim/utils/font.py +79 -0
  66. janim/utils/font_manager.py +177 -0
  67. janim/utils/iterables.py +206 -0
  68. janim/utils/paths.py +60 -0
  69. janim/utils/rate_functions.py +111 -0
  70. janim/utils/refresh.py +64 -0
  71. janim/utils/signal.py +280 -0
  72. janim/utils/simple_functions.py +81 -0
  73. janim/utils/space_ops.py +360 -0
  74. janim/utils/unique_nparray.py +37 -0
  75. janim-0.1.dist-info/LICENSE +21 -0
  76. janim-0.1.dist-info/METADATA +51 -0
  77. janim-0.1.dist-info/RECORD +78 -0
  78. janim-0.1.dist-info/WHEEL +4 -0
janim/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ '''a library for simple animation effects'''
2
+
3
+ __version__ = '0.1'
janim/__main__.py ADDED
@@ -0,0 +1,270 @@
1
+ import importlib
2
+ import time
3
+ import os
4
+ import platform
5
+ import subprocess as sp
6
+ from argparse import ArgumentParser, Namespace
7
+
8
+ from janim.anims.timeline import Timeline
9
+ from janim.logger import log
10
+ from janim.utils.config import Config, default_config
11
+
12
+
13
+ def main() -> None:
14
+ parser = ArgumentParser(description='A library for simple animation effects')
15
+ parser.set_defaults(func=None)
16
+
17
+ sp = parser.add_subparsers()
18
+ run_parser(sp.add_parser('run', help='Run timeline(s) from specific namespace'))
19
+ write_parser(sp.add_parser('write', help='Generate video file(s) of timeline(s) from specific namesapce'))
20
+ examples_parser(sp.add_parser('examples', help='Show examples of janim'))
21
+
22
+ args = parser.parse_args()
23
+ if args.func is None:
24
+ parser.print_help()
25
+ else:
26
+ args.func(args)
27
+
28
+
29
+ def render_args(parser: ArgumentParser) -> None:
30
+ parser.add_argument(
31
+ 'namespace',
32
+ help='Namespace to file holding the python code for the timeline'
33
+ )
34
+ parser.add_argument(
35
+ 'timeline_names',
36
+ nargs='*',
37
+ help='Name of the Timeline class you want to see'
38
+ )
39
+ parser.add_argument(
40
+ '-a', '--all',
41
+ action='store_true',
42
+ help='Render all timelines from a file'
43
+ )
44
+ parser.add_argument(
45
+ '-c', '--config',
46
+ nargs=2,
47
+ metavar=('key', 'value'),
48
+ action='append',
49
+ help='Override config'
50
+ )
51
+
52
+
53
+ def run_parser(parser: ArgumentParser) -> None:
54
+ render_args(parser)
55
+ parser.add_argument(
56
+ '-i', '--interact',
57
+ action='store_true',
58
+ help='Enable the network socket for interacting'
59
+ )
60
+ parser.set_defaults(func=run)
61
+
62
+
63
+ def write_parser(parser: ArgumentParser) -> None:
64
+ render_args(parser)
65
+ parser.add_argument(
66
+ '-o', '--open',
67
+ action='store_true',
68
+ help='Open the file after writing'
69
+ )
70
+ parser.add_argument(
71
+ '--format',
72
+ choices=['mp4', 'mov'],
73
+ default='mp4',
74
+ help='Format of the output file'
75
+ )
76
+ parser.set_defaults(func=write)
77
+
78
+
79
+ def examples_parser(parser: ArgumentParser) -> None:
80
+ parser.set_defaults(namespace='janim.examples')
81
+ parser.add_argument(
82
+ 'timeline_names',
83
+ nargs='*',
84
+ help='Name of the example you want to see'
85
+ )
86
+ parser.set_defaults(all=None)
87
+ parser.set_defaults(config=None)
88
+ parser.set_defaults(func=run)
89
+ parser.set_defaults(interact=False)
90
+
91
+
92
+ def run(args: Namespace) -> None:
93
+ module = get_module(args.namespace)
94
+ if module is None:
95
+ return
96
+ modify_default_config(args)
97
+
98
+ timelines = extract_timelines_from_module(args, module)
99
+ if not timelines:
100
+ return
101
+
102
+ auto_play = len(timelines) == 1
103
+
104
+ from janim.gui.anim_viewer import AnimViewer
105
+ from janim.gui.application import Application
106
+ from PySide6.QtCore import QPoint, QTimer
107
+
108
+ app = Application()
109
+
110
+ log.info('======')
111
+
112
+ widgets: list[AnimViewer] = []
113
+ for timeline in timelines:
114
+ viewer = AnimViewer(timeline().build(), auto_play, args.interact)
115
+ widgets.append(viewer)
116
+
117
+ log.info('======')
118
+ log.info('Constructing window')
119
+
120
+ t = time.time()
121
+
122
+ for i, widget in enumerate(widgets):
123
+ if i != 0:
124
+ widget.move(widgets[i - 1].pos() + QPoint(24, 24))
125
+ widget.show()
126
+
127
+ QTimer.singleShot(200, widgets[-1].activateWindow)
128
+
129
+ log.info(f'Finished constructing in {time.time() - t:.2f} s')
130
+ log.info('======')
131
+
132
+ app.exec()
133
+
134
+
135
+ def write(args: Namespace) -> None:
136
+ module = get_module(args.namespace)
137
+ if module is None:
138
+ return
139
+ modify_default_config(args)
140
+
141
+ timelines = extract_timelines_from_module(args, module)
142
+ if not timelines:
143
+ return
144
+
145
+ from janim.render.file_writer import FileWriter
146
+
147
+ log.info('======')
148
+
149
+ built = [timeline().build() for timeline in timelines]
150
+
151
+ log.info('======')
152
+
153
+ output_dir = os.path.normpath(Config.get.output_dir)
154
+ if not os.path.exists(output_dir):
155
+ os.makedirs(output_dir)
156
+
157
+ log.info(f'fps={Config.get.fps}')
158
+ log.info(f'resolution="{Config.get.pixel_width}x{Config.get.pixel_height}"')
159
+ log.info(f'output_dir="{output_dir}"')
160
+ log.info(f'format="{args.format}"')
161
+
162
+ log.info('======')
163
+
164
+ for anim in built:
165
+ writer = FileWriter(anim)
166
+ writer.write_all(
167
+ os.path.join(Config.get.output_dir,
168
+ f'{anim.timeline.__class__.__name__}.{args.format}')
169
+ )
170
+ if args.open and anim is built[-1]:
171
+ open_file(writer.final_file_path)
172
+
173
+ log.info('======')
174
+
175
+
176
+ def modify_default_config(args: Namespace) -> None:
177
+ if args.config:
178
+ for key, value in args.config:
179
+ dtype = type(getattr(default_config, key))
180
+ setattr(default_config, key, dtype(value))
181
+
182
+
183
+ def get_module(namespace: str):
184
+ try:
185
+ return importlib.import_module(namespace)
186
+ except ModuleNotFoundError:
187
+ log.error(f'No module named "{namespace}"')
188
+ return None
189
+
190
+
191
+ def extract_timelines_from_module(args: Namespace, module) -> list[type[Timeline]]:
192
+ timelines = []
193
+ err = False
194
+
195
+ if not args.all and args.timeline_names:
196
+ for name in args.timeline_names:
197
+ try:
198
+ timelines.append(module.__dict__[name])
199
+ except KeyError:
200
+ log.error(f'No timeline named "{name}"')
201
+ err = True
202
+ else:
203
+ import inspect
204
+
205
+ classes = [
206
+ value
207
+ for value in module.__dict__.values()
208
+ if isinstance(value, type) and issubclass(value, Timeline) and value.__module__ == module.__name__
209
+ ]
210
+ if len(classes) <= 1:
211
+ return classes
212
+ classes.sort(key=lambda x: inspect.getsourcelines(x)[1])
213
+ if args.all:
214
+ return classes
215
+
216
+ max_digits = len(str(len(classes)))
217
+
218
+ name_to_class = {}
219
+ for idx, timeline_class in enumerate(classes, start=1):
220
+ name = timeline_class.__name__
221
+ print(f"{str(idx).zfill(max_digits)}: {name}")
222
+ name_to_class[name] = timeline_class
223
+
224
+ user_input = input(
225
+ "\nThat module has multiple timelines, "
226
+ "which ones would you like to render?"
227
+ "\nTimeline Name or Number: "
228
+ )
229
+ for split_str in user_input.replace(' ', '').split(','):
230
+ if not split_str:
231
+ continue
232
+ if split_str.isnumeric():
233
+ idx = int(split_str) - 1
234
+ if 0 <= idx < len(classes):
235
+ timelines.append(classes[idx])
236
+ else:
237
+ log.error(f'Invaild number {idx + 1}')
238
+ err = True
239
+ else:
240
+ try:
241
+ timelines.append(name_to_class[split_str])
242
+ except KeyError:
243
+ log.error(f'No timeline named {split_str}')
244
+ err = True
245
+
246
+ return [] if err else timelines
247
+
248
+
249
+ def open_file(file_path: str) -> None:
250
+ current_os = platform.system()
251
+ if current_os == "Windows":
252
+ os.startfile(file_path)
253
+ else:
254
+ commands = []
255
+ if current_os == "Linux":
256
+ commands.append("xdg-open")
257
+ elif current_os.startswith("CYGWIN"):
258
+ commands.append("cygstart")
259
+ else: # Assume macOS
260
+ commands.append("open")
261
+
262
+ commands.append(file_path)
263
+
264
+ FNULL = open(os.devnull, 'w')
265
+ sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
266
+ FNULL.close()
267
+
268
+
269
+ if __name__ == '__main__':
270
+ main()
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Callable
6
+
7
+ from janim.components.depth import Cmpt_Depth
8
+ from janim.utils.rate_functions import RateFunc, smooth
9
+
10
+ if TYPE_CHECKING:
11
+ from janim.anims.composition import AnimGroup
12
+
13
+
14
+ @dataclass
15
+ class TimeRange:
16
+ at: float
17
+ duration: float
18
+
19
+ @property
20
+ def end(self) -> float:
21
+ return self.at + self.duration
22
+
23
+
24
+ @dataclass
25
+ class RenderCall:
26
+ '''
27
+ 绘制调用
28
+
29
+ - ``depth``: 该绘制的深度
30
+ - ``func``: 该绘制所调用的函数
31
+
32
+ 具体机制:
33
+
34
+ - 在每个动画对象中,都会使用 :meth:`~.Animation.set_render_call_list` 来设置该动画进行绘制时所执行的函数
35
+ - 在进行渲染(具体参考 :meth:`~.TimelineAnim.render_all` )时,会按照深度进行排序,依次对 ``func`` 进行调用,深度越高的越先调用
36
+
37
+ 例:
38
+
39
+ - 在 :class:`~.Display` 中,设置了单个 :class:`RenderCall` ,作用是绘制物件
40
+ - 在 :class:`~.Transform` 中,对于每个插值物件都设置了 :class:`RenderCall`,绘制所有的插值物件
41
+ '''
42
+ depth: Cmpt_Depth
43
+ func: Callable[[], None]
44
+
45
+
46
+ class Animation:
47
+ '''
48
+ 动画基类
49
+
50
+ - 创建一个从 ``at`` 持续至 ``at + duration`` 的动画
51
+ - 指定 ``rate_func`` 可以设定插值函数,默认为 :meth:`janim.utils.rate_functions.smooth` 即平滑插值
52
+ '''
53
+ label_color: tuple[float, float, float] = (128, 132, 137)
54
+
55
+ def __init__(
56
+ self,
57
+ *,
58
+ at: float = 0,
59
+ duration: float = 1.0,
60
+ rate_func: RateFunc = smooth,
61
+ ):
62
+ from janim.anims.timeline import Timeline
63
+ self.timeline = Timeline.get_context()
64
+ self.parent: AnimGroup = None
65
+ self.current_alpha = None
66
+
67
+ self.local_range = TimeRange(at, duration)
68
+ self.global_range = None
69
+ self.rate_func = rate_func
70
+
71
+ self.render_call_list: list[RenderCall] = []
72
+
73
+ def set_global_range(self, at: float, duration: float | None = None) -> None:
74
+ '''
75
+ 设置在 :class:`~.Timeline` 上的时间范围
76
+
77
+ 不需要手动设置,该方法是被 :meth:`~.AnimGroup.set_global_range` 调用以计算的
78
+ '''
79
+ if duration is None:
80
+ duration = self.local_range.duration
81
+ self.global_range = TimeRange(at, duration)
82
+
83
+ def set_render_call_list(self, lst: list[RenderCall]) -> None:
84
+ '''
85
+ 设置绘制调用,具体参考 :class:`RenderCall`
86
+ '''
87
+ self.render_call_list = sorted(lst, key=lambda x: x.depth, reverse=True)
88
+
89
+ def anim_pre_init(self) -> None: '''在 :meth:`~.Timeline.detect_changes_of_all` 执行之前调用的初始化方法'''
90
+
91
+ def anim_init(self) -> None: '''在 :meth:`~.Timeline.detect_changes_of_all` 执行之前调用的初始化方法'''
92
+
93
+ def anim_on(self, local_t: float) -> None:
94
+ '''
95
+ 将 ``local_t`` 换算为 ``alpha`` 并调用 :meth:`anim_on_alpha`
96
+ '''
97
+ alpha = self.rate_func(local_t / self.local_range.duration)
98
+ self.anim_on_alpha(alpha)
99
+
100
+ def get_alpha_on_global_t(self, global_t: float) -> float:
101
+ '''
102
+ 传入全局 ``global_t``,得到物件在该时刻应当处于哪个 ``alpha`` 的插值
103
+ '''
104
+ if self.parent is None:
105
+ return self.rate_func((global_t - self.global_range.at) / self.global_range.duration)
106
+
107
+ anim_t = self.parent.get_anim_t(self.parent.get_alpha_on_global_t(global_t), self)
108
+ return self.rate_func(anim_t / self.local_range.duration)
109
+
110
+ global_t_ctx: ContextVar[float] = ContextVar('Animation.global_t_ctx')
111
+ '''
112
+ 对该值进行设置,使得进行 :meth:`anim_on` 和 :meth:`render` 时不需要将 ``global_t`` 作为参数传递也能获取到
113
+ '''
114
+
115
+ def anim_on_alpha(self, alpha: float) -> None:
116
+ '''
117
+ 动画在 ``alpha`` 处的行为
118
+ '''
119
+ pass
@@ -0,0 +1,167 @@
1
+
2
+ from janim.anims.animation import Animation
3
+ from janim.utils.rate_functions import RateFunc, linear
4
+
5
+
6
+ class AnimGroup(Animation):
7
+ '''
8
+ 动画集合(并列执行)
9
+
10
+ - 若不传入 ``duration``,则将终止时间(子动画结束时间的最大值)作为该动画集合的 ``duration``
11
+ - 若传入 ``duration``,则会将子动画的时间进行拉伸,使得终止时间与 ``duration`` 一致
12
+ - 且可以使用 ``at`` 进行总体偏移(如 ``at=1`` 则是总体延后 1s)
13
+
14
+ 时间示例:
15
+
16
+ .. code-block:: python
17
+
18
+ AnimGroup(
19
+ Anim1(duration=3),
20
+ Anim2(duration=4)
21
+ ) # Anim1 在 0~3s 执行,Anim2 在 0~4s 执行
22
+
23
+ AnimGroup(
24
+ Anim1(duration=3),
25
+ Anim2(duration=4),
26
+ duration=6
27
+ ) # Anim1 在 0~4.5s 执行,Anim2 在 0~6s 执行
28
+
29
+ AnimGroup(
30
+ Anim1(duration=3),
31
+ Anim2(duration=4),
32
+ at=1,
33
+ duration=6
34
+ ) # Anim1 在 1~5.5s 执行,Anim2 在 1~7s 执行
35
+ '''
36
+ def __init__(
37
+ self,
38
+ *anims: Animation,
39
+ duration: float | None = None,
40
+ rate_func: RateFunc = linear,
41
+ **kwargs
42
+ ):
43
+ self.anims = anims
44
+ self.maxt = 0 if not anims else max(anim.local_range.end for anim in anims)
45
+ if duration is None:
46
+ duration = self.maxt
47
+
48
+ super().__init__(duration=duration, rate_func=rate_func, **kwargs)
49
+ for anim in self.anims:
50
+ anim.parent = self
51
+
52
+ def flatten(self) -> list[Animation]:
53
+ result = [self]
54
+ for anim in self.anims:
55
+ if isinstance(anim, AnimGroup):
56
+ result.extend(anim.flatten())
57
+ else:
58
+ result.append(anim)
59
+
60
+ return result
61
+
62
+ def set_global_range(self, at: float, duration: float | None = None) -> None:
63
+ '''
64
+ 设置并计算子动画的时间范围
65
+
66
+ 不需要手动设置,该方法是被 :meth:`~.Timeline.prepare` 调用以计算的
67
+ '''
68
+ super().set_global_range(at, duration)
69
+
70
+ if duration is None:
71
+ duration = self.local_range.duration
72
+
73
+ factor = duration / self.maxt
74
+
75
+ for anim in self.anims:
76
+ anim.set_global_range(
77
+ self.global_range.at + anim.local_range.at * factor,
78
+ anim.local_range.duration * factor
79
+ )
80
+
81
+ def anim_pre_init(self) -> None:
82
+ for anim in self.anims:
83
+ anim.anim_pre_init()
84
+
85
+ def anim_init(self) -> None:
86
+ for anim in self.anims:
87
+ anim.anim_init()
88
+
89
+ def get_anim_t(self, alpha: float, anim: Animation) -> float:
90
+ return alpha * self.maxt - anim.local_range.at
91
+
92
+ def anim_on_alpha(self, alpha: float) -> None:
93
+ '''
94
+ 在该方法中,:class:`AnimGroup` 通过 ``alpha``
95
+ 换算出子动画的 ``local_t`` 并调用子动画的 :meth:`~.Animation.anim_on` 方法
96
+ '''
97
+ for anim in self.anims:
98
+ anim_t = self.get_anim_t(alpha, anim)
99
+ if 0 <= anim_t < anim.local_range.duration:
100
+ anim.anim_on(anim_t)
101
+
102
+
103
+ class Succession(AnimGroup):
104
+ '''
105
+ 动画集合(顺序执行)
106
+
107
+ - 会将传入的动画依次执行,不并行
108
+ - 可以传入 `buff` 指定前后动画中间的空白时间
109
+ - 其余与 `AnimGroup` 相同
110
+
111
+ 时间示例:
112
+
113
+ .. code-block:: python
114
+
115
+ Succession(
116
+ Anim1(duration=3),
117
+ Anim2(duration=4)
118
+ ) # Anim1 在 0~3s 执行,Anim2 在 3~7s 执行
119
+
120
+ Succession(
121
+ Anim1(duration=2),
122
+ Anim2(at=1, duration=2),
123
+ Anim3(at=0.5, duration=2)
124
+ ) # Anim1 在 0~2s 执行,Anim2 在 3~5s 执行,Anim3 在 5.5~7.5s 执行
125
+
126
+ Succession(
127
+ Anim1(duration=2),
128
+ Anim2(duration=2),
129
+ Anim3(duration=2),
130
+ buff=0.5
131
+ ) # Anim1 在 0~2s 执行,Anim2 在 2.5~4.5s 执行,Anim3 在 5~7s 执行
132
+ '''
133
+ def __init__(self, *anims: Animation, buff: float = 0, **kwargs):
134
+ end_time = 0
135
+ for anim in anims:
136
+ anim.local_range.at += end_time
137
+ end_time = anim.local_range.end + buff
138
+ super().__init__(*anims, **kwargs)
139
+
140
+
141
+ class Aligned(AnimGroup):
142
+ '''
143
+ 动画集合(并列对齐执行)
144
+
145
+ 时间示例:
146
+
147
+ .. code-block:: python
148
+
149
+ Aligned(
150
+ Anim1(duration=1),
151
+ Anim2(duration=2)
152
+ ) # Anim1 和 Anim2 都在 0~2s 执行
153
+
154
+ Aligned(
155
+ Anim1(duration=1),
156
+ Anim2(duration=2),
157
+ duration=4
158
+ ) # Anim1 和 Anim2 都在 0~4s 执行
159
+ '''
160
+ def __init__(*anims: Animation, **kwargs):
161
+ maxt = max(anim.local_range.end for anim in anims)
162
+ for anim in anims:
163
+ factor = anim.local_range.end / maxt
164
+ anim.local_range.at *= factor
165
+ anim.local_range.duration *= factor
166
+
167
+ super().__init__(*anims, **kwargs)