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.
- janim/__init__.py +3 -0
- janim/__main__.py +270 -0
- janim/anims/animation.py +119 -0
- janim/anims/composition.py +167 -0
- janim/anims/creation.py +188 -0
- janim/anims/display.py +29 -0
- janim/anims/fading.py +118 -0
- janim/anims/rotation.py +45 -0
- janim/anims/timeline.py +489 -0
- janim/anims/transform.py +228 -0
- janim/anims/updater.py +131 -0
- janim/camera/camera.py +145 -0
- janim/camera/camera_info.py +79 -0
- janim/components/component.py +266 -0
- janim/components/depth.py +87 -0
- janim/components/image.py +45 -0
- janim/components/points.py +1006 -0
- janim/components/radius.py +105 -0
- janim/components/rgbas.py +254 -0
- janim/components/vpoints.py +510 -0
- janim/constants/__init__.py +5 -0
- janim/constants/alignment.py +7 -0
- janim/constants/colors.py +66 -0
- janim/constants/coord.py +20 -0
- janim/constants/degrees.py +8 -0
- janim/examples.py +32 -0
- janim/gui/anim_viewer.py +746 -0
- janim/gui/application.py +11 -0
- janim/gui/export.png +0 -0
- janim/gui/fixed_ratio_widget.py +36 -0
- janim/gui/glwidget.py +33 -0
- janim/gui/selector.py +222 -0
- janim/imports.py +34 -0
- janim/items/boolean_ops.py +149 -0
- janim/items/geometry/arc.py +313 -0
- janim/items/geometry/arrow.py +210 -0
- janim/items/geometry/line.py +207 -0
- janim/items/geometry/polygon.py +161 -0
- janim/items/image_item.py +171 -0
- janim/items/item.py +540 -0
- janim/items/points.py +110 -0
- janim/items/relation.py +199 -0
- janim/items/svg/svg_item.py +118 -0
- janim/items/svg/typst.py +67 -0
- janim/items/svg/typst_template.typ +3 -0
- janim/items/text/text.py +397 -0
- janim/items/vitem.py +154 -0
- janim/logger.py +13 -0
- janim/render/base.py +103 -0
- janim/render/file_writer.py +122 -0
- janim/render/impl.py +235 -0
- janim/render/shaders/dotcloud.frag.glsl +21 -0
- janim/render/shaders/dotcloud.geom.glsl +50 -0
- janim/render/shaders/dotcloud.vert.glsl +19 -0
- janim/render/shaders/image.frag.glsl +13 -0
- janim/render/shaders/image.vert.glsl +18 -0
- janim/render/shaders/vitem.frag.glsl +254 -0
- janim/render/shaders/vitem.vert.glsl +14 -0
- janim/render/texture.py +32 -0
- janim/typing.py +31 -0
- janim/utils/bezier.py +405 -0
- janim/utils/config.py +156 -0
- janim/utils/data.py +11 -0
- janim/utils/file_ops.py +29 -0
- janim/utils/font.py +79 -0
- janim/utils/font_manager.py +177 -0
- janim/utils/iterables.py +206 -0
- janim/utils/paths.py +60 -0
- janim/utils/rate_functions.py +111 -0
- janim/utils/refresh.py +64 -0
- janim/utils/signal.py +280 -0
- janim/utils/simple_functions.py +81 -0
- janim/utils/space_ops.py +360 -0
- janim/utils/unique_nparray.py +37 -0
- janim-0.1.dist-info/LICENSE +21 -0
- janim-0.1.dist-info/METADATA +51 -0
- janim-0.1.dist-info/RECORD +78 -0
- janim-0.1.dist-info/WHEEL +4 -0
janim/__init__.py
ADDED
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()
|
janim/anims/animation.py
ADDED
|
@@ -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)
|