Skip to content

Commit 68858c3

Browse files
authored
Merge pull request #1 from jeertmans/ThreeDSlide
Add ThreeDSlides support
2 parents b3210ec + 1720a7d commit 68858c3

File tree

7 files changed

+114
-68
lines changed

7 files changed

+114
-68
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Here are a few things that I implemented (or that I'm planning to implement) on
123123
- [x] User can easily generate dummy config file
124124
- [x] Config file path can be manually set
125125
- [ ] Play animation in reverse [#9](https://github.com/galatolofederico/manim-presentation/issues/9)
126-
- [ ] Handle 3D scenes out of the box
126+
- [x] Handle 3D scenes out of the box
127127
- [ ] Can work with both community and 3b1b versions (not tested)
128128
- [ ] Generate docs online
129129
- [ ] Fix the quality problem on Windows platforms with `fullscreen` flag

example.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,38 @@ def construct(self):
2828
# Each slide MUST end with an animation (a self.wait is considered an animation)
2929
self.play(dot.animate.move_to(ORIGIN))
3030

31+
3132
class ThreeDExample(ThreeDSlide):
3233
def construct(self):
3334
axes = ThreeDAxes()
34-
circle=Circle()
35+
circle = Circle(radius=3, color=BLUE)
36+
dot = Dot(color=RED)
37+
38+
self.add(axes)
3539

3640
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
37-
self.add(circle,axes)
3841

39-
self.begin_ambient_camera_rotation(rate=0.1)
42+
self.play(GrowFromCenter(circle))
43+
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
44+
4045
self.pause()
4146

47+
self.start_loop()
48+
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
49+
self.end_loop()
50+
4251
self.stop_ambient_camera_rotation()
4352
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
44-
self.wait()
53+
54+
self.play(dot.animate.move_to(ORIGIN))
55+
self.pause()
56+
57+
self.play(dot.animate.move_to(RIGHT * 3))
58+
self.pause()
4559

4660
self.start_loop()
47-
self.play(circle.animate.move_to(ORIGIN + UP))
61+
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
4862
self.end_loop()
4963

50-
self.play(circle.animate.move_to(ORIGIN))
51-
52-
# TODO: fixit
64+
# Each slide MUST end with an animation (a self.wait is considered an animation)
65+
self.play(dot.animate.move_to(ORIGIN))

manim_slides/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
def cli():
1313
pass
1414

15+
1516
cli.add_command(present)
1617
cli.add_command(wizard)
1718
cli.add_command(init)

manim_slides/present.py

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,25 @@ class State(Enum):
2121
END = 3
2222

2323
def __str__(self):
24-
if self.value == 0: return "Playing"
25-
if self.value == 1: return "Paused"
26-
if self.value == 2: return "Wait"
27-
if self.value == 3: return "End"
24+
if self.value == 0:
25+
return "Playing"
26+
if self.value == 1:
27+
return "Paused"
28+
if self.value == 2:
29+
return "Wait"
30+
if self.value == 3:
31+
return "End"
2832
return "..."
2933

34+
3035
def now():
3136
return round(time.time() * 1000)
3237

38+
3339
def fix_time(x):
3440
return x if x > 0 else 1
3541

42+
3643
class Presentation:
3744
def __init__(self, config, last_frame_next: bool = False):
3845
self.last_frame_next = last_frame_next
@@ -48,14 +55,15 @@ def __init__(self, config, last_frame_next: bool = False):
4855
def add_last_slide(self):
4956
last_slide_end = self.slides[-1]["end_animation"]
5057
last_animation = len(self.files)
51-
self.slides.append(dict(
52-
start_animation = last_slide_end,
53-
end_animation = last_animation,
54-
type = "last",
55-
number = len(self.slides) + 1,
56-
terminated = False
57-
))
58-
58+
self.slides.append(
59+
dict(
60+
start_animation=last_slide_end,
61+
end_animation=last_animation,
62+
type="last",
63+
number=len(self.slides) + 1,
64+
terminated=False,
65+
)
66+
)
5967

6068
def reset(self):
6169
self.current_animation = 0
@@ -78,7 +86,7 @@ def rewind_slide(self):
7886
self.current_animation = self.current_slide["start_animation"]
7987
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
8088

81-
def load_this_cap(self,cap_number):
89+
def load_this_cap(self, cap_number):
8290
if self.caps[cap_number] == None:
8391
# unload other caps
8492
for i in range(len(self.caps)):
@@ -135,7 +143,10 @@ def update_state(self, state):
135143
self.rewind_slide()
136144
elif self.current_slide["type"] == "last":
137145
self.current_slide["terminated"] = True
138-
elif self.current_slide["type"] == "last" and self.current_slide["end_animation"] == self.current_animation:
146+
elif (
147+
self.current_slide["type"] == "last"
148+
and self.current_slide["end_animation"] == self.current_animation
149+
):
139150
state = State.WAIT
140151
else:
141152
# Play next video!
@@ -162,15 +173,19 @@ def __init__(self, presentations, config, start_paused=False, fullscreen=False):
162173

163174
if fullscreen:
164175
cv2.namedWindow("Video", cv2.WND_PROP_FULLSCREEN)
165-
cv2.setWindowProperty("Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
176+
cv2.setWindowProperty(
177+
"Video", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN
178+
)
166179

167180
@property
168181
def current_presentation(self):
169182
return self.presentations[self.current_presentation_i]
170183

171184
def run(self):
172185
while True:
173-
self.lastframe, self.state = self.current_presentation.update_state(self.state)
186+
self.lastframe, self.state = self.current_presentation.update_state(
187+
self.state
188+
)
174189
if self.state == State.PLAYING or self.state == State.PAUSED:
175190
if self.start_paused:
176191
self.state = State.PAUSED
@@ -200,39 +215,34 @@ def show_info(self):
200215
info,
201216
f"Animation: {self.current_presentation.current_animation}",
202217
(grid_x[0], grid_y[0]),
203-
*font_args
204-
)
205-
cv2.putText(
206-
info,
207-
f"State: {self.state}",
208-
(grid_x[1], grid_y[0]),
209-
*font_args
218+
*font_args,
210219
)
220+
cv2.putText(info, f"State: {self.state}", (grid_x[1], grid_y[0]), *font_args)
211221

212222
cv2.putText(
213223
info,
214224
f"Slide {self.current_presentation.current_slide['number']}/{len(self.current_presentation.slides)}",
215225
(grid_x[0], grid_y[1]),
216-
*font_args
226+
*font_args,
217227
)
218228
cv2.putText(
219229
info,
220230
f"Slide Type: {self.current_presentation.current_slide['type']}",
221231
(grid_x[1], grid_y[1]),
222-
*font_args
232+
*font_args,
223233
)
224234

225235
cv2.putText(
226236
info,
227237
f"Scene {self.current_presentation_i + 1}/{len(self.presentations)}",
228-
((grid_x[0]+grid_x[1])//2, grid_y[2]),
229-
*font_args
238+
((grid_x[0] + grid_x[1]) // 2, grid_y[2]),
239+
*font_args,
230240
)
231241

232242
cv2.imshow("Info", info)
233243

234244
def handle_key(self):
235-
sleep_time = math.ceil(1000/self.current_presentation.fps)
245+
sleep_time = math.ceil(1000 / self.current_presentation.fps)
236246
key = cv2.waitKeyEx(fix_time(sleep_time - self.lag))
237247

238248
if self.config.QUIT.match(key):
@@ -241,7 +251,9 @@ def handle_key(self):
241251
self.state = State.PAUSED
242252
elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key):
243253
self.state = State.PLAYING
244-
elif self.state == State.WAIT and (self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)):
254+
elif self.state == State.WAIT and (
255+
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
256+
):
245257
self.current_presentation.next()
246258
self.state = State.PLAYING
247259
elif self.state == State.PLAYING and self.config.CONTINUE.match(key):
@@ -258,7 +270,6 @@ def handle_key(self):
258270
self.current_presentation.rewind_slide()
259271
self.state = State.PLAYING
260272

261-
262273
def quit(self):
263274
cv2.destroyAllWindows()
264275
sys.exit()
@@ -267,10 +278,19 @@ def quit(self):
267278
@click.command()
268279
@click.argument("scenes", nargs=-1)
269280
@config_path_option
270-
@click.option("--folder", default=FOLDER_PATH, type=click.Path(exists=True, file_okay=False), help="Set slides folder.")
281+
@click.option(
282+
"--folder",
283+
default=FOLDER_PATH,
284+
type=click.Path(exists=True, file_okay=False),
285+
help="Set slides folder.",
286+
)
271287
@click.option("--start-paused", is_flag=True, help="Start paused.")
272288
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
273-
@click.option("--last-frame-next", is_flag=True, help="Show the next animation first frame as last frame (hack).")
289+
@click.option(
290+
"--last-frame-next",
291+
is_flag=True,
292+
help="Show the next animation first frame as last frame (hack).",
293+
)
274294
@click.help_option("-h", "--help")
275295
def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next):
276296
"""Present the different scenes"""
@@ -279,7 +299,9 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
279299
for scene in scenes:
280300
config_file = os.path.join(folder, f"{scene}.json")
281301
if not os.path.exists(config_file):
282-
raise Exception(f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class")
302+
raise Exception(
303+
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
304+
)
283305
config = json.load(open(config_file))
284306
presentations.append(Presentation(config, last_frame_next=last_frame_next))
285307

@@ -288,5 +310,7 @@ def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_ne
288310
else:
289311
config = Config()
290312

291-
display = Display(presentations, config=config, start_paused=start_paused, fullscreen=fullscreen)
313+
display = Display(
314+
presentations, config=config, start_paused=start_paused, fullscreen=fullscreen
315+
)
292316
display.run()

manim_slides/slide.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
class Slide(Scene):
1111
def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
12-
super(Slide, self).__init__(*args, **kwargs)
12+
super().__init__(*args, **kwargs)
1313
self.output_folder = output_folder
1414
self.slides = list()
1515
self.current_slide = 1
@@ -18,16 +18,18 @@ def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs):
1818
self.pause_start_animation = 0
1919

2020
def play(self, *args, **kwargs):
21-
super(Slide, self).play(*args, **kwargs)
21+
super().play(*args, **kwargs)
2222
self.current_animation += 1
2323

2424
def pause(self):
25-
self.slides.append(dict(
26-
type="slide",
27-
start_animation=self.pause_start_animation,
28-
end_animation=self.current_animation,
29-
number=self.current_slide
30-
))
25+
self.slides.append(
26+
dict(
27+
type="slide",
28+
start_animation=self.pause_start_animation,
29+
end_animation=self.current_animation,
30+
number=self.current_slide,
31+
)
32+
)
3133
self.current_slide += 1
3234
self.pause_start_animation = self.current_animation
3335

@@ -36,13 +38,17 @@ def start_loop(self):
3638
self.loop_start_animation = self.current_animation
3739

3840
def end_loop(self):
39-
assert self.loop_start_animation is not None, "You have to start a loop before ending it"
40-
self.slides.append(dict(
41-
type="loop",
42-
start_animation=self.loop_start_animation,
43-
end_animation=self.current_animation,
44-
number=self.current_slide
45-
))
41+
assert (
42+
self.loop_start_animation is not None
43+
), "You have to start a loop before ending it"
44+
self.slides.append(
45+
dict(
46+
type="loop",
47+
start_animation=self.loop_start_animation,
48+
end_animation=self.current_animation,
49+
number=self.current_slide,
50+
)
51+
)
4652
self.current_slide += 1
4753
self.loop_start_animation = None
4854
self.pause_start_animation = self.current_animation
@@ -52,7 +58,7 @@ def render(self, *args, **kwargs):
5258
max_files_cached = config["max_files_cached"]
5359
config["max_files_cached"] = float("inf")
5460

55-
super(Slide, self).render(*args, **kwargs)
61+
super().render(*args, **kwargs)
5662

5763
config["max_files_cached"] = max_files_cached
5864

@@ -78,12 +84,10 @@ def render(self, *args, **kwargs):
7884
shutil.copyfile(src_file, dst_file)
7985
files.append(dst_file)
8086

81-
f = open(os.path.join(self.output_folder, "%s.json" % (scene_name, )), "w")
82-
json.dump(dict(
83-
slides=self.slides,
84-
files=files
85-
), f)
87+
f = open(os.path.join(self.output_folder, "%s.json" % (scene_name,)), "w")
88+
json.dump(dict(slides=self.slides, files=files), f)
8689
f.close()
8790

88-
class ThreeDSlide(ThreeDScene, Slide):
91+
92+
class ThreeDSlide(Slide, ThreeDScene):
8993
pass

manim_slides/wizard.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,24 @@ def wizard(config_path, force, merge):
2727
"""Launch configuration wizard."""
2828
return _init(config_path, force, merge, skip_interactive=False)
2929

30+
3031
@click.command()
3132
@config_options
3233
def init(config_path, force, merge, skip_interactive=False):
3334
"""Initialize a new default configuration file."""
3435
return _init(config_path, force, merge, skip_interactive=True)
3536

37+
3638
def _init(config_path, force, merge, skip_interactive=False):
3739

3840
if os.path.exists(config_path):
3941
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
4042

4143
if not force and not merge:
42-
choice = click.prompt("Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?", type=click.Choice(["o", "m", "q"], case_sensitive=False))
44+
choice = click.prompt(
45+
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
46+
type=click.Choice(["o", "m", "q"], case_sensitive=False),
47+
)
4348

4449
force = choice == "o"
4550
merge = choice == "m"
@@ -52,7 +57,6 @@ def _init(config_path, force, merge, skip_interactive=False):
5257
click.secho("Exiting.")
5358
sys.exit(0)
5459

55-
5660
config = Config()
5761

5862
if not skip_interactive:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
python_requires=">=3.7",
3131
install_requires=[
3232
"click>=8.0",
33-
"click-default-group>=1.2"
33+
"click-default-group>=1.2",
3434
"numpy>=1.19.3",
3535
"pydantic>=1.9.1",
3636
"opencv-python>=4.6",

0 commit comments

Comments
 (0)