manim-mcp 0.1.0

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 (64) hide show
  1. package/README.md +104 -0
  2. package/dist/demo.mp4 +0 -0
  3. package/dist/index.js +65 -0
  4. package/dist/mcp-app.html +142 -0
  5. package/dist/server.js +1492 -0
  6. package/package.json +67 -0
  7. package/references/composer/SKILL.md +154 -0
  8. package/references/composer/references/3b1b-series-patterns.md +217 -0
  9. package/references/composer/references/domain-planning-guides/calculus-planning.md +188 -0
  10. package/references/composer/references/domain-planning-guides/linear-algebra-planning.md +169 -0
  11. package/references/composer/references/domain-planning-guides/ml-planning.md +286 -0
  12. package/references/composer/references/domain-planning-guides/number-theory-planning.md +187 -0
  13. package/references/composer/references/domain-planning-guides/physics-planning.md +249 -0
  14. package/references/composer/references/domain-planning-guides/probability-planning.md +200 -0
  15. package/references/composer/references/mathematical-storytelling.md +359 -0
  16. package/references/composer/references/narrative-patterns.md +221 -0
  17. package/references/composer/references/opening-patterns.md +284 -0
  18. package/references/composer/references/pacing-guide.md +289 -0
  19. package/references/composer/references/scene-archetypes.md +534 -0
  20. package/references/composer/references/scene-examples.md +379 -0
  21. package/references/composer/references/visual-techniques.md +480 -0
  22. package/references/composer/templates/scenes-template.md +147 -0
  23. package/references/manimce/SKILL.md +166 -0
  24. package/references/manimce/examples/3d_visualization.py +373 -0
  25. package/references/manimce/examples/basic_animations.py +212 -0
  26. package/references/manimce/examples/graph_plotting.py +401 -0
  27. package/references/manimce/examples/lorenz_attractor.py +172 -0
  28. package/references/manimce/examples/math_visualization.py +315 -0
  29. package/references/manimce/examples/updater_patterns.py +369 -0
  30. package/references/manimce/rules/3b1b-translation.md +594 -0
  31. package/references/manimce/rules/3d.md +254 -0
  32. package/references/manimce/rules/advanced-animations.md +594 -0
  33. package/references/manimce/rules/animation-groups.md +212 -0
  34. package/references/manimce/rules/animations.md +128 -0
  35. package/references/manimce/rules/api-pitfalls.md +89 -0
  36. package/references/manimce/rules/axes.md +214 -0
  37. package/references/manimce/rules/camera.md +208 -0
  38. package/references/manimce/rules/cli.md +232 -0
  39. package/references/manimce/rules/color-conventions.md +444 -0
  40. package/references/manimce/rules/colors.md +199 -0
  41. package/references/manimce/rules/config.md +264 -0
  42. package/references/manimce/rules/creation-animations.md +158 -0
  43. package/references/manimce/rules/graphing.md +233 -0
  44. package/references/manimce/rules/grouping.md +220 -0
  45. package/references/manimce/rules/latex.md +202 -0
  46. package/references/manimce/rules/lines.md +241 -0
  47. package/references/manimce/rules/long-form-video.md +552 -0
  48. package/references/manimce/rules/mathematical-domains.md +689 -0
  49. package/references/manimce/rules/mobjects.md +116 -0
  50. package/references/manimce/rules/multi-scene-composition.md +112 -0
  51. package/references/manimce/rules/pedagogy.md +532 -0
  52. package/references/manimce/rules/physics-simulations.md +610 -0
  53. package/references/manimce/rules/positioning.md +211 -0
  54. package/references/manimce/rules/scenes.md +121 -0
  55. package/references/manimce/rules/shapes.md +300 -0
  56. package/references/manimce/rules/styling.md +177 -0
  57. package/references/manimce/rules/text-animations.md +222 -0
  58. package/references/manimce/rules/text.md +189 -0
  59. package/references/manimce/rules/timing.md +227 -0
  60. package/references/manimce/rules/transform-animations.md +157 -0
  61. package/references/manimce/rules/updaters.md +226 -0
  62. package/references/manimce/templates/basic_scene.py +64 -0
  63. package/references/manimce/templates/camera_scene.py +100 -0
  64. package/references/manimce/templates/threed_scene.py +138 -0
@@ -0,0 +1,610 @@
1
+ ---
2
+ name: physics-simulations
3
+ description: Physics-based animation patterns from 3b1b including Euler integration, pendulums, particle systems, phase space, vector fields, and sound
4
+ metadata:
5
+ tags: physics, simulation, euler-integration, pendulum, particles, phase-space, vector-field, spring, SIR, sound
6
+ ---
7
+
8
+ # Physics Simulations in ManimCE
9
+
10
+ Patterns derived from 3b1b's differential equations series, SIR epidemic model, optics puzzles, Galton board, and other physics-heavy videos.
11
+
12
+ ## Euler Integration with Substeps (n_steps_per_frame)
13
+
14
+ The foundational pattern for smooth physics simulations. 3b1b's pendulum code uses `n_steps_per_frame = 100` for numerical stability at 30fps.
15
+
16
+ ```python
17
+ from manim import *
18
+ import numpy as np
19
+
20
+ class PendulumSimulation(Scene):
21
+ """Euler integration with substeps, directly from 3b1b's diffyq/pendulum.py"""
22
+
23
+ def construct(self):
24
+ pendulum = PendulumMobject(
25
+ length=3,
26
+ initial_theta=0.8,
27
+ damping=0.1,
28
+ gravity=9.8,
29
+ n_steps_per_frame=100,
30
+ )
31
+ pendulum.shift(UP)
32
+ self.add(pendulum)
33
+ self.wait(10)
34
+
35
+
36
+ class PendulumMobject(VGroup):
37
+ """A pendulum that simulates via Euler integration in an updater."""
38
+
39
+ def __init__(
40
+ self,
41
+ length=3,
42
+ initial_theta=0.3,
43
+ omega=0,
44
+ damping=0.1,
45
+ gravity=9.8,
46
+ top_point=2 * UP,
47
+ n_steps_per_frame=100,
48
+ **kwargs,
49
+ ):
50
+ super().__init__(**kwargs)
51
+ self.length = length
52
+ self.theta = initial_theta
53
+ self.omega = omega
54
+ self.damping = damping
55
+ self.gravity = gravity
56
+ self.top_point = top_point
57
+ self.n_steps_per_frame = n_steps_per_frame
58
+
59
+ # Visual elements
60
+ self.rod = Line(UP, DOWN, stroke_width=3, stroke_color=GREY_B)
61
+ self.rod.set_height(length)
62
+ self.weight = Circle(radius=0.25, fill_opacity=1, fill_color=GREY_BROWN)
63
+ self.weight.set_stroke(width=0)
64
+ self.pivot = Dot(top_point, color=WHITE, radius=0.07)
65
+
66
+ self.add(self.rod, self.weight, self.pivot)
67
+ self._update_visual()
68
+
69
+ # Physics updater
70
+ self.add_updater(self._physics_step)
71
+
72
+ def _physics_step(self, mob, dt):
73
+ """Euler integration with substeps for stability."""
74
+ if dt == 0:
75
+ return
76
+ n = self.n_steps_per_frame
77
+ sub_dt = dt / n
78
+ for _ in range(n):
79
+ # d(theta)/dt = omega
80
+ d_theta = self.omega * sub_dt
81
+ # d(omega)/dt = -damping * omega - (g/L) * sin(theta)
82
+ d_omega = (
83
+ -self.damping * self.omega
84
+ - (self.gravity / self.length) * np.sin(self.theta)
85
+ ) * sub_dt
86
+ self.theta += d_theta
87
+ self.omega += d_omega
88
+ self._update_visual()
89
+
90
+ def _update_visual(self):
91
+ """Position rod and weight based on current theta."""
92
+ direction = np.array([
93
+ np.sin(self.theta), -np.cos(self.theta), 0
94
+ ])
95
+ end_point = self.top_point + self.length * direction
96
+ self.rod.put_start_and_end_on(self.top_point, end_point)
97
+ self.weight.move_to(end_point)
98
+ ```
99
+
100
+ ## Spring-Mass System
101
+
102
+ From `_2019/diffyq/part1/pendulum.py` where a spring-mass analogy is shown alongside the pendulum.
103
+
104
+ ```python
105
+ class SpringMassSystem(VGroup):
106
+ """A horizontal spring-mass system with Euler integration."""
107
+
108
+ def __init__(
109
+ self,
110
+ mass=1.0,
111
+ k=4.0, # spring constant
112
+ damping=0.3,
113
+ initial_x=2.0,
114
+ initial_v=0.0,
115
+ n_steps_per_frame=100,
116
+ anchor_point=3 * LEFT,
117
+ **kwargs,
118
+ ):
119
+ super().__init__(**kwargs)
120
+ self.mass = mass
121
+ self.k = k
122
+ self.damping_coeff = damping
123
+ self.x = initial_x
124
+ self.v = initial_v
125
+ self.n_steps = n_steps_per_frame
126
+ self.anchor = anchor_point
127
+
128
+ # Visual elements
129
+ self.wall = Line(UP, DOWN, color=GREY_B).set_height(1.5)
130
+ self.wall.move_to(anchor_point)
131
+
132
+ self.block = Square(side_length=0.6, fill_opacity=1, fill_color=BLUE_D)
133
+ self.spring_line = VMobject(stroke_color=WHITE, stroke_width=2)
134
+
135
+ self.add(self.wall, self.spring_line, self.block)
136
+ self._update_visual()
137
+ self.add_updater(self._physics_step)
138
+
139
+ def _get_spring_points(self, start, end, n_coils=8, amplitude=0.2):
140
+ """Generate zigzag spring points between start and end."""
141
+ points = [start]
142
+ direction = end - start
143
+ length = np.linalg.norm(direction)
144
+ unit = direction / length if length > 0 else RIGHT
145
+ perp = np.array([-unit[1], unit[0], 0])
146
+
147
+ for i in range(1, n_coils * 2 + 1):
148
+ frac = i / (n_coils * 2 + 1)
149
+ sign = 1 if i % 2 == 0 else -1
150
+ point = start + frac * direction + sign * amplitude * perp
151
+ points.append(point)
152
+ points.append(end)
153
+ return points
154
+
155
+ def _physics_step(self, mob, dt):
156
+ if dt == 0:
157
+ return
158
+ sub_dt = dt / self.n_steps
159
+ for _ in range(self.n_steps):
160
+ force = -self.k * self.x - self.damping_coeff * self.v
161
+ acc = force / self.mass
162
+ self.v += acc * sub_dt
163
+ self.x += self.v * sub_dt
164
+ self._update_visual()
165
+
166
+ def _update_visual(self):
167
+ block_center = self.anchor + RIGHT * (2 + self.x)
168
+ self.block.move_to(block_center)
169
+ spring_points = self._get_spring_points(
170
+ self.anchor + 0.2 * RIGHT,
171
+ block_center + 0.3 * LEFT,
172
+ )
173
+ self.spring_line.set_points_as_corners(spring_points)
174
+
175
+
176
+ class SpringMassDemo(Scene):
177
+ def construct(self):
178
+ system = SpringMassSystem(
179
+ initial_x=1.5,
180
+ k=6.0,
181
+ damping=0.2,
182
+ )
183
+ self.add(system)
184
+ self.wait(8)
185
+ ```
186
+
187
+ ## Particle Systems (SIR Model Pattern)
188
+
189
+ From `_2020/sir.py` -- a particle-based epidemic simulation with states, color-coded by status.
190
+
191
+ ```python
192
+ class Particle(VGroup):
193
+ """A particle with position, velocity, and state -- from the SIR model pattern."""
194
+
195
+ def __init__(
196
+ self,
197
+ status="S",
198
+ radius=0.1,
199
+ max_speed=1.0,
200
+ color_map=None,
201
+ bounds=None,
202
+ **kwargs,
203
+ ):
204
+ super().__init__(**kwargs)
205
+ if color_map is None:
206
+ color_map = {"S": BLUE, "I": RED, "R": GREY_D}
207
+ if bounds is None:
208
+ bounds = {
209
+ "x_min": -FRAME_WIDTH / 2 + 0.5,
210
+ "x_max": FRAME_WIDTH / 2 - 0.5,
211
+ "y_min": -FRAME_HEIGHT / 2 + 0.5,
212
+ "y_max": FRAME_HEIGHT / 2 - 0.5,
213
+ }
214
+
215
+ self.status = status
216
+ self.max_speed = max_speed
217
+ self.color_map = color_map
218
+ self.bounds = bounds
219
+ self.velocity = max_speed * normalize(
220
+ np.array([np.random.randn(), np.random.randn(), 0])
221
+ )
222
+
223
+ self.body = Dot(radius=radius)
224
+ self.body.set_color(color_map[status])
225
+ self.add(self.body)
226
+
227
+ self.add_updater(self._move_and_bounce)
228
+
229
+ def _move_and_bounce(self, mob, dt):
230
+ """Random walk with wall bouncing."""
231
+ center = self.get_center()
232
+ new_pos = center + self.velocity * dt
233
+
234
+ b = self.bounds
235
+ if new_pos[0] < b["x_min"] or new_pos[0] > b["x_max"]:
236
+ self.velocity[0] *= -1
237
+ if new_pos[1] < b["y_min"] or new_pos[1] > b["y_max"]:
238
+ self.velocity[1] *= -1
239
+
240
+ self.shift(self.velocity * dt)
241
+
242
+ def set_status(self, new_status):
243
+ """Change status with color transition."""
244
+ self.status = new_status
245
+ self.body.set_color(self.color_map[new_status])
246
+
247
+
248
+ class SIRDemo(Scene):
249
+ """Simplified SIR epidemic model visualization."""
250
+
251
+ def construct(self):
252
+ # SIR colors -- standard 3b1b convention
253
+ color_map = {"S": BLUE, "I": RED, "R": GREY_D}
254
+
255
+ # Legend
256
+ legend = VGroup(*[
257
+ VGroup(
258
+ Dot(color=color_map[s]),
259
+ Text(label, font_size=20),
260
+ ).arrange(RIGHT, buff=0.2)
261
+ for s, label in [("S", "Susceptible"), ("I", "Infected"), ("R", "Recovered")]
262
+ ]).arrange(DOWN, aligned_edge=LEFT).to_corner(UL)
263
+ self.add(legend)
264
+
265
+ # Create particles
266
+ n_particles = 50
267
+ particles = VGroup(*[
268
+ Particle(
269
+ status="S",
270
+ max_speed=0.8 + 0.4 * np.random.random(),
271
+ color_map=color_map,
272
+ )
273
+ for _ in range(n_particles)
274
+ ])
275
+
276
+ # Scatter initial positions
277
+ for p in particles:
278
+ p.move_to([
279
+ np.random.uniform(-5, 5),
280
+ np.random.uniform(-3, 3),
281
+ 0
282
+ ])
283
+
284
+ # Patient zero
285
+ particles[0].set_status("I")
286
+
287
+ self.add(particles)
288
+ self.wait(15)
289
+ ```
290
+
291
+ ## Galton Board (Ball Drop Simulation)
292
+
293
+ From `_2023/clt/galton_board.py` -- demonstrates falling balls with sound cues.
294
+
295
+ ```python
296
+ class SimpleGaltonBoard(Scene):
297
+ """Simplified Galton board pattern from 3b1b's CLT video."""
298
+
299
+ n_rows = 5
300
+ peg_radius = 0.1
301
+ ball_radius = 0.08
302
+ spacing = 0.8
303
+
304
+ def construct(self):
305
+ pegs = self.create_pegs()
306
+ buckets = self.create_buckets(pegs)
307
+
308
+ self.play(
309
+ LaggedStartMap(Write, pegs, lag_ratio=0.02),
310
+ LaggedStartMap(Write, buckets, lag_ratio=0.05),
311
+ )
312
+ self.wait()
313
+
314
+ # Drop balls
315
+ for _ in range(15):
316
+ self.drop_ball(pegs, buckets)
317
+
318
+ self.wait(2)
319
+
320
+ def create_pegs(self):
321
+ pegs = VGroup()
322
+ for row in range(self.n_rows):
323
+ n_pegs = row + 1
324
+ row_group = VGroup()
325
+ for col in range(n_pegs):
326
+ x = (col - row / 2) * self.spacing
327
+ y = -row * self.spacing * 0.866 + 2 # Triangle layout
328
+ peg = Dot(
329
+ point=[x, y, 0],
330
+ radius=self.peg_radius,
331
+ color=WHITE,
332
+ )
333
+ row_group.add(peg)
334
+ pegs.add(row_group)
335
+ return pegs
336
+
337
+ def create_buckets(self, pegs):
338
+ buckets = VGroup()
339
+ bottom_y = pegs[-1][0].get_y() - self.spacing
340
+ for i in range(self.n_rows + 1):
341
+ x = (i - self.n_rows / 2) * self.spacing
342
+ bucket = Line(
343
+ [x, bottom_y - 0.5, 0],
344
+ [x, bottom_y + 0.5, 0],
345
+ stroke_color=GREY_B, stroke_width=2,
346
+ )
347
+ buckets.add(bucket)
348
+ return buckets
349
+
350
+ def drop_ball(self, pegs, buckets):
351
+ ball = Dot(radius=self.ball_radius, color=YELLOW)
352
+ ball.move_to(pegs[0][0].get_center() + 0.5 * UP)
353
+ self.add(ball)
354
+
355
+ # Bounce through pegs
356
+ col = 0
357
+ for row in range(self.n_rows):
358
+ direction = np.random.choice([-1, 1])
359
+ if direction == 1:
360
+ col += 1
361
+ target_peg = pegs[row][min(col, len(pegs[row]) - 1)]
362
+ offset = direction * self.spacing * 0.3
363
+ target = target_peg.get_center() + np.array([offset, -0.2, 0])
364
+
365
+ self.play(
366
+ ball.animate.move_to(target),
367
+ run_time=0.15,
368
+ rate_func=rate_functions.rush_into,
369
+ )
370
+
371
+ # Fall to bucket
372
+ final_x = ball.get_x()
373
+ bottom_y = pegs[-1][0].get_y() - self.spacing - 0.3
374
+ self.play(
375
+ ball.animate.move_to([final_x, bottom_y, 0]),
376
+ run_time=0.2,
377
+ )
378
+ ```
379
+
380
+ ## Phase Space Visualization
381
+
382
+ From `_2019/diffyq/` and `_2018/div_curl.py` -- showing trajectories in phase space.
383
+
384
+ ```python
385
+ class PhaseSpaceVisualization(Scene):
386
+ """Phase portrait for a damped pendulum -- theta vs omega."""
387
+
388
+ def construct(self):
389
+ # Phase plane
390
+ axes = Axes(
391
+ x_range=[-PI, PI, PI / 2],
392
+ y_range=[-4, 4, 1],
393
+ x_length=8,
394
+ y_length=5,
395
+ axis_config={"include_tip": True},
396
+ )
397
+ x_label = MathTex(r"\theta").next_to(axes.x_axis, RIGHT)
398
+ y_label = MathTex(r"\dot{\theta}").next_to(axes.y_axis, UP)
399
+ axes.add(x_label, y_label)
400
+
401
+ self.play(Create(axes))
402
+ self.wait()
403
+
404
+ # Trace a trajectory through phase space
405
+ gravity = 9.8
406
+ length = 2.0
407
+ damping = 0.3
408
+
409
+ def phase_trajectory(theta0, omega0, dt=0.01, steps=2000):
410
+ points = []
411
+ theta, omega = theta0, omega0
412
+ for _ in range(steps):
413
+ points.append(axes.c2p(theta, omega))
414
+ d_omega = -damping * omega - (gravity / length) * np.sin(theta)
415
+ omega += d_omega * dt
416
+ theta += omega * dt
417
+ # Wrap theta to [-pi, pi]
418
+ if abs(theta) > PI:
419
+ break
420
+ return points
421
+
422
+ # Draw multiple trajectories from different initial conditions
423
+ colors = [BLUE, GREEN, YELLOW, RED, PURPLE]
424
+ initial_conditions = [
425
+ (2.5, 0), (1.5, 2), (-1.0, 3), (0.5, -2), (-2.0, -1)
426
+ ]
427
+
428
+ for (theta0, omega0), color in zip(initial_conditions, colors):
429
+ points = phase_trajectory(theta0, omega0)
430
+ if len(points) > 2:
431
+ path = VMobject()
432
+ path.set_points_smoothly(points)
433
+ path.set_stroke(color, 2)
434
+
435
+ # Starting dot
436
+ start_dot = Dot(points[0], color=color, radius=0.06)
437
+ self.play(FadeIn(start_dot), run_time=0.3)
438
+ self.play(Create(path), run_time=2)
439
+
440
+ self.wait(2)
441
+ ```
442
+
443
+ ## Vector Fields and StreamLines
444
+
445
+ From `_2019/diffyq/part1/staging.py` and `_2018/div_curl.py`.
446
+
447
+ ```python
448
+ class VectorFieldDemo(Scene):
449
+ """Vector field visualization -- pendulum phase space."""
450
+
451
+ def construct(self):
452
+ plane = NumberPlane(
453
+ x_range=[-4, 4], y_range=[-3, 3],
454
+ background_line_style={"stroke_opacity": 0.3},
455
+ )
456
+ self.add(plane)
457
+
458
+ # Define the vector field function
459
+ gravity, length = 9.8, 2.0
460
+ damping = 0.3
461
+
462
+ def pendulum_field(pos):
463
+ theta, omega = pos[0], pos[1]
464
+ d_theta = omega
465
+ d_omega = -damping * omega - (gravity / length) * np.sin(theta)
466
+ return np.array([d_theta, d_omega, 0])
467
+
468
+ # Arrow-based vector field
469
+ field = ArrowVectorField(
470
+ pendulum_field,
471
+ x_range=[-4, 4, 0.5],
472
+ y_range=[-3, 3, 0.5],
473
+ length_func=lambda norm: 0.4 * sigmoid(norm),
474
+ )
475
+ field.set_opacity(0.7)
476
+
477
+ self.play(Create(field), run_time=2)
478
+ self.wait(2)
479
+
480
+ # Stream lines for flow visualization
481
+ stream_lines = StreamLines(
482
+ pendulum_field,
483
+ x_range=[-4, 4, 0.3],
484
+ y_range=[-3, 3, 0.3],
485
+ stroke_width=1.5,
486
+ max_anchors_per_line=30,
487
+ )
488
+ self.play(FadeOut(field))
489
+ self.play(stream_lines.create(), run_time=3)
490
+ self.wait(2)
491
+ ```
492
+
493
+ ## Sound Integration
494
+
495
+ From `_2019/windmill.py` (hit sounds), `_2023/clt/galton_board.py` (clink sounds). ManimCE supports `self.add_sound()`.
496
+
497
+ ```python
498
+ class SoundIntegration(Scene):
499
+ """Add sound effects synchronized with animations."""
500
+
501
+ def construct(self):
502
+ # Create a bouncing ball
503
+ ball = Dot(radius=0.2, color=YELLOW)
504
+ ball.move_to(3 * UP)
505
+ floor = Line(5 * LEFT, 5 * RIGHT, color=WHITE).shift(2 * DOWN)
506
+ self.add(floor)
507
+
508
+ # Drop and bounce with sound
509
+ self.play(FadeIn(ball))
510
+
511
+ for height in [3, 1.5, 0.75]:
512
+ # Fall
513
+ self.play(
514
+ ball.animate.move_to(2 * DOWN + 0.2 * UP),
515
+ run_time=0.3 * np.sqrt(height),
516
+ rate_func=rate_functions.rush_into,
517
+ )
518
+ # Sound on impact (requires .wav/.mp3 file in working directory)
519
+ # self.add_sound("bounce.wav")
520
+
521
+ # Bounce up
522
+ self.play(
523
+ ball.animate.shift(height * 0.5 * UP),
524
+ run_time=0.3 * np.sqrt(height * 0.5),
525
+ rate_func=rate_functions.rush_from,
526
+ )
527
+
528
+ self.wait()
529
+ ```
530
+
531
+ ## Wave Simulation
532
+
533
+ From `_2023/optics_puzzles/slowing_waves.py` -- oscillating waves with phase tracking.
534
+
535
+ ```python
536
+ class WaveSimulation(Scene):
537
+ """Oscillating wave with ValueTracker-driven phase."""
538
+
539
+ def construct(self):
540
+ axes = Axes(
541
+ x_range=[-8, 8, 1],
542
+ y_range=[-2, 2, 1],
543
+ x_length=12,
544
+ y_length=4,
545
+ )
546
+ self.add(axes)
547
+
548
+ # Wave parameters
549
+ wave_length = 2.0
550
+ amplitude = 1.0
551
+ speed = 1.5
552
+ time_tracker = ValueTracker(0)
553
+
554
+ def wave_func(x):
555
+ t = time_tracker.get_value()
556
+ return amplitude * np.sin(
557
+ TAU * x / wave_length - TAU * t * speed / wave_length
558
+ )
559
+
560
+ wave = always_redraw(
561
+ lambda: axes.plot(wave_func, color=YELLOW, stroke_width=3)
562
+ )
563
+ self.add(wave)
564
+
565
+ # Animate time passing
566
+ self.play(
567
+ time_tracker.animate.set_value(10),
568
+ run_time=10,
569
+ rate_func=linear,
570
+ )
571
+ ```
572
+
573
+ ## Combining Physics with Visualization
574
+
575
+ The 3b1b pattern: show the physical simulation AND the mathematical representation side by side.
576
+
577
+ ```python
578
+ class PhysicsAndMath(Scene):
579
+ """Show physical system alongside its phase portrait."""
580
+
581
+ def construct(self):
582
+ # Left: physical pendulum
583
+ physical_group = VGroup()
584
+ # ... pendulum visualization
585
+
586
+ # Right: phase space
587
+ phase_axes = Axes(
588
+ x_range=[-PI, PI], y_range=[-5, 5],
589
+ x_length=5, y_length=4,
590
+ ).shift(3 * RIGHT)
591
+ phase_label = Text("Phase Space", font_size=24)
592
+ phase_label.next_to(phase_axes, UP)
593
+
594
+ # Divider
595
+ divider = DashedLine(3 * UP, 3 * DOWN)
596
+
597
+ self.add(divider, phase_axes, phase_label)
598
+ self.wait(8)
599
+ ```
600
+
601
+ ## Best Practices for Physics Simulations
602
+
603
+ 1. **Use substeps** (`n_steps_per_frame`) -- at least 100 for pendulums, 10+ for simple systems
604
+ 2. **Separate physics from visuals** -- compute in the updater, update positions at the end
605
+ 3. **Color-code states** -- SIR: S=BLUE, I=RED, R=GREY_D
606
+ 4. **Show phase space** alongside the physical system when possible
607
+ 5. **Use `always_redraw`** for wave functions that depend on ValueTrackers
608
+ 6. **Wall bouncing** -- check bounds in the updater and reflect velocity
609
+ 7. **Sound cues** -- `self.add_sound()` on impacts for visceral feedback
610
+ 8. **Start simple** -- show one ball/particle first, then many