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.
- package/README.md +104 -0
- package/dist/demo.mp4 +0 -0
- package/dist/index.js +65 -0
- package/dist/mcp-app.html +142 -0
- package/dist/server.js +1492 -0
- package/package.json +67 -0
- package/references/composer/SKILL.md +154 -0
- package/references/composer/references/3b1b-series-patterns.md +217 -0
- package/references/composer/references/domain-planning-guides/calculus-planning.md +188 -0
- package/references/composer/references/domain-planning-guides/linear-algebra-planning.md +169 -0
- package/references/composer/references/domain-planning-guides/ml-planning.md +286 -0
- package/references/composer/references/domain-planning-guides/number-theory-planning.md +187 -0
- package/references/composer/references/domain-planning-guides/physics-planning.md +249 -0
- package/references/composer/references/domain-planning-guides/probability-planning.md +200 -0
- package/references/composer/references/mathematical-storytelling.md +359 -0
- package/references/composer/references/narrative-patterns.md +221 -0
- package/references/composer/references/opening-patterns.md +284 -0
- package/references/composer/references/pacing-guide.md +289 -0
- package/references/composer/references/scene-archetypes.md +534 -0
- package/references/composer/references/scene-examples.md +379 -0
- package/references/composer/references/visual-techniques.md +480 -0
- package/references/composer/templates/scenes-template.md +147 -0
- package/references/manimce/SKILL.md +166 -0
- package/references/manimce/examples/3d_visualization.py +373 -0
- package/references/manimce/examples/basic_animations.py +212 -0
- package/references/manimce/examples/graph_plotting.py +401 -0
- package/references/manimce/examples/lorenz_attractor.py +172 -0
- package/references/manimce/examples/math_visualization.py +315 -0
- package/references/manimce/examples/updater_patterns.py +369 -0
- package/references/manimce/rules/3b1b-translation.md +594 -0
- package/references/manimce/rules/3d.md +254 -0
- package/references/manimce/rules/advanced-animations.md +594 -0
- package/references/manimce/rules/animation-groups.md +212 -0
- package/references/manimce/rules/animations.md +128 -0
- package/references/manimce/rules/api-pitfalls.md +89 -0
- package/references/manimce/rules/axes.md +214 -0
- package/references/manimce/rules/camera.md +208 -0
- package/references/manimce/rules/cli.md +232 -0
- package/references/manimce/rules/color-conventions.md +444 -0
- package/references/manimce/rules/colors.md +199 -0
- package/references/manimce/rules/config.md +264 -0
- package/references/manimce/rules/creation-animations.md +158 -0
- package/references/manimce/rules/graphing.md +233 -0
- package/references/manimce/rules/grouping.md +220 -0
- package/references/manimce/rules/latex.md +202 -0
- package/references/manimce/rules/lines.md +241 -0
- package/references/manimce/rules/long-form-video.md +552 -0
- package/references/manimce/rules/mathematical-domains.md +689 -0
- package/references/manimce/rules/mobjects.md +116 -0
- package/references/manimce/rules/multi-scene-composition.md +112 -0
- package/references/manimce/rules/pedagogy.md +532 -0
- package/references/manimce/rules/physics-simulations.md +610 -0
- package/references/manimce/rules/positioning.md +211 -0
- package/references/manimce/rules/scenes.md +121 -0
- package/references/manimce/rules/shapes.md +300 -0
- package/references/manimce/rules/styling.md +177 -0
- package/references/manimce/rules/text-animations.md +222 -0
- package/references/manimce/rules/text.md +189 -0
- package/references/manimce/rules/timing.md +227 -0
- package/references/manimce/rules/transform-animations.md +157 -0
- package/references/manimce/rules/updaters.md +226 -0
- package/references/manimce/templates/basic_scene.py +64 -0
- package/references/manimce/templates/camera_scene.py +100 -0
- 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
|