97 Commits

Author SHA1 Message Date
Devin Neal
e83c69d968 udpate documentation link 2019-07-07 21:37:24 -07:00
Devin Neal
dfdceb1908 allow pycairo>=1.18.0 for windows 2019-07-07 10:11:33 -07:00
Devin Neal
41792fdb5f Merge pull request #607 from 3b1b/video_dir
Add --video_output_dir flag
2019-06-21 23:18:23 -07:00
Devin Neal
3ec231f0ca add video_output_dir flag 2019-06-21 22:52:16 -07:00
Grant Sanderson
7d596d0840 Merge pull request #606 from 3b1b/diffyq
Diffyq
2019-06-19 16:14:56 -07:00
Grant Sanderson
2e0e5cfb5e Rename ode folder to diffyq 2019-06-19 16:11:46 -07:00
Grant Sanderson
96e34b969c Changed ode folder to be named diffyq, since many of these are pde videos 2019-06-19 16:10:21 -07:00
Grant Sanderson
2f6be9a389 Some of the first scenes for diffyq part4 2019-06-19 16:08:46 -07:00
Grant Sanderson
2294cdea4f Allow Bubble to flip over any axis, which was unnecessarily restricted before 2019-06-19 16:08:14 -07:00
Grant Sanderson
8ce13875a3 Added Line.set_length 2019-06-19 16:07:22 -07:00
Grant Sanderson
7ac990119a frame_center was getting double-added, messing up MovingCameraScene 2019-06-19 16:07:07 -07:00
Devin Neal
43b643db6c Merge pull request #586 from Kolloom/install_doc
Install doc
2019-06-19 14:46:34 -07:00
Grant Sanderson
9da74cb657 Beginning chapter 4 animations 2019-06-18 20:40:41 -07:00
Grant Sanderson
2680a9c373 Change Fourier scene to center on vectors, not circles 2019-06-18 20:40:29 -07:00
Devin Neal
f908b68bed Merge pull request #597 from cclauss/patch-1
Travis CI: The sudo tag is now deprecated
2019-06-18 00:19:41 -07:00
Devin Neal
71f79530be Merge pull request #600 from red5h4d0w/master
Update tex_file_writing.py
2019-06-17 13:01:47 -07:00
red5h4d0w
e72390bfc4 Update tex_file_writing.py 2019-06-17 15:40:03 -04:00
cclauss
dc83cac5b4 Travis CI: The sudo tag is now deprecated
__sudo: required__ no longer is...  [Travis are now recommending removing the __sudo__ tag](https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration).

"_If you currently specify __sudo:__ in your __.travis.yml__, we recommend removing that configuration_"
2019-06-16 22:48:58 +02:00
Grant Sanderson
5a86ba08f4 Merge pull request #596 from 3b1b/diffyq
Diffyq
2019-06-16 12:53:02 -07:00
Grant Sanderson
e421d3689a Merge branch 'master' of github.com:3b1b/manim into diffyq 2019-06-16 12:51:46 -07:00
Grant Sanderson
dfeb11a2ab Final animations for diffyq chapter 3 2019-06-16 12:51:34 -07:00
Devin Neal
66f9ff29e4 update windows docs 2019-06-15 21:42:05 -07:00
Grant Sanderson
62c2772982 Nearly the final animations for diffyq part 3 2019-06-15 20:52:38 -07:00
Grant Sanderson
9e7619844f Latest animations for diffyq part 3 2019-06-15 12:22:37 -07:00
Grant Sanderson
6c2e7f4c2c interpolate_mobject should return self 2019-06-15 12:22:21 -07:00
Grant Sanderson
08eee147b3 Added TangentLine 2019-06-15 12:21:29 -07:00
Kolloom
43b4508b2c Fix wording. 2019-06-15 14:03:04 -05:00
Devin Neal
dade021eaa Merge pull request #592 from Grae-Drake/feature/docs-edits
Update link to Todd Zimmerman tutorial
2019-06-15 00:09:33 -07:00
Devin Neal
0041e3fd45 Merge pull request #585 from Kolloom/doc
Add documentation for the coordinate system
2019-06-14 23:15:01 -07:00
Grae Drake
d031e5735a Update tutorial description 2019-06-14 23:06:06 -07:00
Kolloom
e64a25bd40 Add a docker diagram to README.md 2019-06-15 00:51:22 -05:00
Kolloom
d7054e61f0 Change docker-compose to reflect README.md changes 2019-06-15 00:45:14 -05:00
Kolloom
6c1fb7ddca Change docker instructions on README.md
Change all refernces of INPUT_DIR to INPUT_PATH
change all references of OUTPUT_DIR to OUTPUT_PATH

change a paragraph
2019-06-15 00:43:32 -05:00
Grae Drake
7fab15abd4 Update link to Todd Zimmerman tutorial 2019-06-14 21:38:37 -07:00
Kolloom
6571b0d88e Mention VMobject for geometry 2019-06-14 23:34:56 -05:00
Kolloom
ee83d3bcec Update assets with medium preset 2019-06-14 23:26:40 -05:00
Kolloom
542ea68824 Fix wording 2019-06-14 23:08:03 -05:00
Kolloom
2a930ff702 Use monospace for asset annotation 2019-06-14 23:06:03 -05:00
Grant Sanderson
6f7e123b1a Discrete case scenes for diffyq part 3 2019-06-14 10:07:06 -07:00
Grant Sanderson
4196bb627e New scenes for diffyq part3 2019-06-13 16:16:42 -07:00
Grant Sanderson
2cbe19af7c Bug fix for the case when the media_dir has spaces 2019-06-13 16:15:43 -07:00
Grant Sanderson
2ccf83c0aa Change output folder heirarchy back to grouping files of similar types, rather grouping them as parts of the same scene 2019-06-13 16:15:02 -07:00
Grant Sanderson
cfbbff1bdf Default color of Vector should be white 2019-06-13 16:14:03 -07:00
Grant Sanderson
f10baa3fcd Change default media output directory to from 'video' to 'videos' 2019-06-13 16:13:42 -07:00
Grant Sanderson
34d1d27c56 Merge pull request #588 from 3b1b/diffyq
Diffyq
2019-06-13 09:34:25 -07:00
Grant Sanderson
cc08c090d1 Merge branch 'master' of github.com:3b1b/manim into diffyq 2019-06-13 09:29:57 -07:00
Grant Sanderson
caa4efdfa5 Latest scenes for diffyq part 3 2019-06-13 09:27:35 -07:00
Grant Sanderson
503b2bc896 For some reason stage_scenes still has a hardcoded input directory... 2019-06-13 09:27:19 -07:00
Grant Sanderson
542ddb9afd Make sure set_opacity changes background stroke as well 2019-06-13 09:26:34 -07:00
Grant Sanderson
6214ea7a01 Added TracedPath 2019-06-13 09:26:03 -07:00
Grant Sanderson
3b42f1f709 Added int_func to SHowIncreasingSubsets 2019-06-13 09:25:14 -07:00
Devin Neal
ab75015099 Merge pull request #582 from stephenwild/master
Documentation for TipableVMobject
2019-06-13 00:48:15 -07:00
Kolloom
e2eac0bd96 Minor edit 2019-06-11 00:59:36 -05:00
Kolloom
65c23d9e9f Fix docker install instruction
Env Var used in the compose.yml is INPUT_DIR and OUTPUT_DIR

Clarify the meaning of both variables and output message.
Note on -p not working.
2019-06-11 00:49:31 -05:00
Devin Neal
1322152391 Update README.md 2019-06-10 20:09:42 -07:00
Kolloom
b71e63a540 Add documentation for the coordinate system
Update index
Add 4 assets, one of which is an image

Image uses width 700
Videos uses 700x394
Those dimension are similar to how content is laid out
in sphinx
2019-06-10 16:23:49 -05:00
Stephen Wild
41539387a5 Documentation for TipableVMobject
Add an overview of TipableVMobject's functionality and a few comments, as well as grouping some methods by functionality (roughly).
2019-06-10 03:36:16 +08:00
Devin Neal
22b4c3578b Merge pull request #580 from Kolloom/pull_jarwin_patch-3
Update learning_by_example.rst
2019-06-08 16:37:54 -07:00
Kolloom
5535997356 Simple wording change 2019-06-08 18:32:42 -05:00
Devin Neal
3e2a312f51 Merge pull request #581 from Kolloom/link_fix
fix broken link due to misnamed file
2019-06-08 15:33:13 -07:00
Devin Neal
aad68a5503 Update docker install link 2019-06-08 15:28:25 -07:00
Kolloom
7f54ac9b3a fix broken link due to misnamed file 2019-06-08 14:17:59 -05:00
Kolloom
fb9cf93470 Minor changes 2019-06-08 14:00:59 -05:00
Kolloom
d1ada7a8aa Update mac install doc to include manim
also caution on python2 incompatibility
2019-06-08 13:44:10 -05:00
Kolloom
9ffe4a6839 Rewrite learning_by_example.py
Move embedded video to below example code.

Delete ``manim -h`` output. If user is able to run manim,
they should be able to use the help flag too.

Change wording on "instance" when it should be "classes"

Move info on flags to a note block

Add a note on transform only change apparances.

Move explanations to after code block

...
2019-06-08 13:32:27 -05:00
Devin Neal
f6dc1cb50f updated Compose file and update docker section of README.md 2019-06-08 00:40:23 -07:00
Devin Neal
a024916e66 Apply link fix from master 2019-06-08 00:32:43 -05:00
Kolloom
73a1d37785 Merging mac install doc from jarwin/patch-3 2019-06-08 00:19:31 -05:00
Purusharth Saxena
62206bc0e7 Update learning_by_example.rst
Updated getting started to accomodate manimlib (pip installation)
2019-06-08 00:19:31 -05:00
Devin Neal
57b96fdd07 fix documentation links 2019-06-06 20:40:11 -07:00
Devin Neal
de0d7c907d Merge branch 'master' of github.com:3b1b/manim 2019-06-04 21:10:37 -07:00
Devin Neal
8449bc3418 update documentation 2019-06-04 21:10:12 -07:00
Devin Neal
f81c275631 make media, tex, and video directories configurable via flags 2019-06-04 20:51:18 -07:00
Devin Neal
949bfaa0b4 Merge pull request #577 from Kolloom/master
Add animation documentation and asset folder
2019-06-04 20:38:15 -07:00
Kolloom
e1fb9f480b Add animation documentation and asset folder
Animation documentation is not yet complete.
Asset folder contains renders from code snipples.

Update constants.rst to include a ref tag
Update index to include the animation doc
2019-06-04 13:15:35 -05:00
Devin Neal
caa4577cd1 reorganize media file tree 2019-06-03 23:41:05 -07:00
Grant Sanderson
43b82f2c53 Scenes up to AnalyzeSineCurve for diffyq part 3 2019-06-03 11:33:39 -07:00
Devin Neal
c203f8e8c0 remove repeated latex package 2019-06-02 21:13:41 -07:00
Devin Neal
75996a618c install less latex 2019-06-02 21:10:45 -07:00
Devin Neal
ed24541de6 update dockerfile, add flag for high quality rendering (#573)
Allow rendering in 1080p by passing `--high_quality` and update the Dockerfile and Compose file to install Manim directly into the container.
2019-06-02 18:58:33 -07:00
Kolloom
22c8aa0eb8 Add documentation for constants.py (#572)
* Add documentation for constants.py

* Update documentation for constants.py

With suggestions from eulertour
2019-06-02 18:21:11 -07:00
Kierán Meinhardt
0590c153ef remove typographically incorrect punctuation from documentation headings (#567) 2019-06-02 13:54:03 -07:00
Charly
c42ae8a55b add option -i to save as gif (#529)
* add option -i to save as gif

* re-add -c command

* No longer needs to save mp4 first
2019-06-02 12:13:22 -07:00
Devin Neal
3632d97140 install pycairo conditionally based on platform identifier (#571) 2019-05-31 10:44:51 -07:00
quark67
e9fc2aa46c Correction of a bad alt description (#566) 2019-05-30 13:17:45 -07:00
Yoshi Askharoun
20590e6823 Remove duplicate usepackage (#554)
The textcomp package was imported twice in the ctex template
2019-05-29 20:25:51 -07:00
Grant Sanderson
8bb0b4f010 Merge branch 'master' of github.com:3b1b/manim into diffyq 2019-05-29 18:29:11 -07:00
Grant Sanderson
ab2318ff9d New scenes for diffyq part 3 2019-05-29 18:28:40 -07:00
Grant Sanderson
5dcb113996 Moving the frame_center of camera was not working. This fixes it, but I suspect there is a need for a deeper fix where everything is handled in transform_points_pre_display properly for the various camera classes 2019-05-29 18:28:28 -07:00
Devin Neal
68fb61a5b2 add information to docs (#563) 2019-05-28 21:28:01 -07:00
Devin Neal
2f37e3abf2 Merge pull request #562 from eulertour/master
add master_doc to conf.py
2019-05-28 21:07:40 -07:00
Devin Neal
aecf5b0b18 add master_doc to conf.py 2019-05-28 21:03:20 -07:00
Devin Neal
224d389522 Merge pull request #561 from Kolloom/master
Add Windows installation documentation
2019-05-28 19:08:34 -07:00
Grant Sanderson
52054571ab Merge branch 'master' of github.com:3b1b/manim into diffyq 2019-05-28 13:38:50 -07:00
Grant Sanderson
ddd7ce2f12 Animations up to the preview for the breakdown into sine curves for diffyq chapter 3 2019-05-28 13:38:45 -07:00
Kolloom
0b35d8e2ec Add Windows installation documentation
The installation is done with a Windows8 machine,
further testing are needed for windows10.
2019-05-28 15:22:20 -05:00
Grant Sanderson
4096fc28b8 Merge branch 'master' of github.com:3b1b/manim into diffyq 2019-05-27 19:49:00 -07:00
82 changed files with 7303 additions and 1069 deletions

3
.gitignore vendored
View File

@@ -12,6 +12,7 @@ ben_cairo_test.py
.floo
.flooignore
.vscode
.vs
*.xml
*.iml
media
@@ -23,4 +24,4 @@ dist/
manim.egg-info/
primes.py
/media_dir.txt
/media_dir.txt

View File

@@ -1,5 +1,4 @@
language: python
sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069)
dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
python: "3.7"
cache: pip

View File

@@ -3,12 +3,17 @@ RUN apt-get update \
&& apt-get install -qqy --no-install-recommends \
apt-utils \
ffmpeg \
texlive-latex-base \
texlive-full \
texlive-fonts-extra \
sox \
libcairo2-dev \
texlive \
texlive-fonts-extra \
texlive-latex-extra \
texlive-latex-recommended \
texlive-science \
tipa \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt requirements.txt
RUN python -m pip install -r requirements.txt && rm requirements.txt
COPY . /manim
RUN cd /manim \
&& python setup.py sdist \
&& python -m pip install dist/manimlib*
ENTRYPOINT ["/bin/bash"]

View File

@@ -4,7 +4,7 @@
[![Documentation](https://img.shields.io/badge/docs-EulerTour-blue.svg)](https://www.eulertour.com/learn/manim/)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://choosealicense.com/licenses/mit/)
[![Manim Subreddit](https://img.shields.io/reddit/subreddit-subscribers/manim.svg?color=ff4301&label=reddit)](https://www.reddit.com/r/manim/)
[![Manim Subreddit](https://img.shields.io/discord/581738731934056449.svg?label=discord)](https://discord.gg/mMRrZQW)
[![Manim Discord](https://img.shields.io/discord/581738731934056449.svg?label=discord)](https://discord.gg/mMRrZQW)
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as seen in the videos at [3Blue1Brown](https://www.3blue1brown.com/).
@@ -34,7 +34,7 @@ If you want to hack on manimlib itself, clone this repository and in that direct
python3 -m pip install -r requirements.txt
# Try it out
python3 -m manim example_scenes.py SquareToCircle -pl
python3 ./manim.py example_scenes.py SquareToCircle -pl
```
### Directly (Windows)
@@ -74,14 +74,29 @@ python3 -m manim example_scenes.py SquareToCircle -pl
### Using Docker
Since it's a bit tricky to get all the dependencies set up just right, there is a Dockerfile and Compose file provided in this repo as well as [a premade image on Docker Hub](https://hub.docker.com/r/eulertour/manim/tags/). The Dockerfile contains instructions on how to build a manim image, while the Compose file contains instructions on how to run the image.
The image does not contain a copy of the repo. This is intentional, as it allows you to either bind mount a repo that you've cloned locally or clone any fork/branch you want. In order to do this with the Compose file, you must set the `MANIM_PATH` environment variable to the absolute path to the manim repo.
The prebuilt container image has manin repository included.
`INPUT_PATH` is where the container looks for scene files. You must set the `INPUT_PATH`
environment variable to the absolute path containing your scene file and the
`OUTPUT_PATH` environment variable to the directory where you want media to be written.
1. [Install Docker](https://www.docker.com/products/overview)
1. [Install Docker](https://docs.docker.com)
2. [Install Docker Compose](https://docs.docker.com/compose/install/)
3. Render an animation
```sh
MANIM_PATH=/absolute/path/to/manim/repo docker-compose run manim example_scenes.py SquareToCircle -l
INPUT_PATH=/path/to/dir/containing/source/code \
OUTPUT_PATH=/path/to/output/ \
docker-compose run manim example_scenes.py SquareToCircle -l
```
The command needs to be run as root if your username is not in the docker group.
You can replace `example.scenes.py` with any relative path from your `INPUT_PATH`.
<img src=./manim_docker_diagram.png/>
After running the output will say files ready at `/tmp/output/`, which refers to path inside the container. Your OUTPUT_PATH is bind mounted to this `/tmp/output` so any changes made by the container to `/tmp/output` will be mirrored on your OUTPUT_PATH. `/media/` will be created in `OUTPUT_PATH`.
`-p` won't work as manim would look for video player in the container system, which it does not have.
The first time you execute the above command, Docker will pull the image from Docker Hub and cache it. Any subsequent runs until the image is evicted will use the cached image.
Note that the image doesn't have any development tools installed and can't preview animations. Its purpose is building and testing only.

View File

@@ -0,0 +1,70 @@
from active_projects.ode.part3.staging import *
from active_projects.ode.part3.temperature_graphs import *
from active_projects.ode.part3.pi_creature_scenes import *
from active_projects.ode.part3.wordy_scenes import *
from active_projects.ode.part3.discrete_case import *
OUTPUT_DIRECTORY = "ode/part3"
SCENES_IN_ORDER = [
LastChapterWrapper,
ThreeConstraints,
OceanOfPossibilities,
ThreeMainObservations,
SimpleCosExpGraph,
AddMultipleSolutions,
FourierSeriesIllustraiton,
BreakDownAFunction,
SineCurveIsUnrealistic,
AnalyzeSineCurve,
EquationAboveSineAnalysis,
ExponentialDecay,
InvestmentGrowth,
GrowingPileOfMoney,
CarbonDecayCurve,
CarbonDecayingInMammoth,
SineWaveScaledByExp,
ShowSinExpDerivatives,
IfOnly,
BoundaryConditionInterlude,
BoundaryConditionReference,
GiantCross,
SimulateRealSineCurve,
DerivativesOfLinearFunction,
StraightLine3DGraph,
SimulateLinearGraph,
EmphasizeBoundaryPoints,
ShowNewRuleAtDiscreteBoundary,
DiscreteEvolutionPoint25,
DiscreteEvolutionPoint1,
FlatEdgesForDiscreteEvolution,
FlatEdgesForDiscreteEvolutionTinySteps,
FlatEdgesContinuousEvolution,
FlatAtBoundaryWords,
SlopeToHeatFlow,
CloserLookAtStraightLine,
WriteOutBoundaryCondition,
SoWeGotNowhere,
ManipulateSinExpSurface,
HeatEquationFrame,
ShowFreq1CosExpDecay,
ShowFreq2CosExpDecay,
ShowFreq4CosExpDecay,
CompareFreqDecays1to2,
CompareFreqDecays1to4,
CompareFreqDecays2to4,
ShowHarmonics,
ShowHarmonicSurfaces,
# SimpleCosExpGraph,
# AddMultipleSolutions,
# IveHeardOfThis,
# FourierSeriesOfLineIllustration,
# InFouriersShoes,
]
PART_4_SCENES = [
FourierSeriesIllustraiton,
FourierNameIntro,
CircleAnimationOfF,
]

View File

@@ -0,0 +1,18 @@
from active_projects.ode.part4.staging import *
from active_projects.ode.part4.fourier_series_scenes import *
from active_projects.ode.part4.pi_creature_scenes import *
OUTPUT_DIRECTORY = "ode/part4"
SCENES_IN_ORDER = [
ComplexFourierSeriesExample,
ComplexFourierSeriesExampleEnd,
FourierSeriesExampleWithRectForZoom,
ZoomedInFourierSeriesExample,
RelationToOtherVideos,
WhyWouldYouCare,
# Oldies
# FourierSeriesIllustraiton,
# FourierNameIntro,
# CircleAnimationOfF,
]

View File

@@ -4,7 +4,7 @@ from manimlib.imports import *
class FourierCirclesScene(Scene):
CONFIG = {
"n_circles": 10,
"n_vectors": 10,
"big_radius": 2,
"colors": [
BLUE_D,
@@ -15,14 +15,16 @@ class FourierCirclesScene(Scene):
"circle_style": {
"stroke_width": 2,
},
"arrow_config": {
"vector_config": {
"buff": 0,
"max_tip_length_to_length_ratio": 0.35,
"tip_length": 0.15,
"max_stroke_width_to_length_ratio": 10,
"stroke_width": 2,
},
"use_vectors": True,
"circle_config": {
"stroke_width": 1,
},
"base_frequency": 1,
"slow_factor": 0.25,
"center_point": ORIGIN,
@@ -33,26 +35,35 @@ class FourierCirclesScene(Scene):
self.slow_factor_tracker = ValueTracker(
self.slow_factor
)
self.vector_clock = ValueTracker(0)
self.vector_clock.add_updater(
lambda m, dt: m.increment_value(
self.get_slow_factor() * dt
)
)
self.add(self.vector_clock)
def get_slow_factor(self):
return self.slow_factor_tracker.get_value()
def get_vector_time(self):
return self.vector_clock.get_value()
#
def get_freqs(self):
n = self.n_circles
n = self.n_vectors
all_freqs = list(range(n // 2, -n // 2, -1))
all_freqs.sort(key=abs)
return all_freqs
def get_coefficients(self):
return [complex(0) for x in range(self.n_circles)]
return [complex(0) for x in range(self.n_vectors)]
def get_color_iterator(self):
return it.cycle(self.colors)
def get_circles(self, freqs=None, coefficients=None):
circles = VGroup()
color_iterator = self.get_color_iterator()
def get_rotating_vectors(self, freqs=None, coefficients=None):
vectors = VGroup()
self.center_tracker = VectorizedPoint(self.center_point)
if freqs is None:
@@ -60,80 +71,74 @@ class FourierCirclesScene(Scene):
if coefficients is None:
coefficients = self.get_coefficients()
last_circle = None
last_vector = None
for freq, coefficient in zip(freqs, coefficients):
if last_circle:
center_func = last_circle.get_start
if last_vector:
center_func = last_vector.get_end
else:
center_func = self.center_tracker.get_location
circle = self.get_circle(
vector = self.get_rotating_vector(
coefficient=coefficient,
freq=freq,
color=next(color_iterator),
center_func=center_func,
)
circles.add(circle)
last_circle = circle
return circles
vectors.add(vector)
last_vector = vector
return vectors
def get_circle(self, coefficient, freq, color, center_func):
radius = abs(coefficient)
phase = np.log(coefficient).imag
circle = Circle(
radius=radius,
color=color,
**self.circle_style,
)
line_points = (
circle.get_center(),
circle.get_start(),
)
if self.use_vectors:
circle.radial_line = Arrow(
*line_points,
**self.arrow_config,
)
def get_rotating_vector(self, coefficient, freq, center_func):
vector = Vector(RIGHT, **self.vector_config)
vector.scale(abs(coefficient))
if abs(coefficient) == 0:
phase = 0
else:
circle.radial_line = Line(
*line_points,
color=WHITE,
**self.circle_style,
phase = np.log(coefficient).imag
vector.rotate(phase, about_point=ORIGIN)
vector.freq = freq
vector.phase = phase
vector.coefficient = coefficient
vector.center_func = center_func
vector.add_updater(self.update_vector)
return vector
def update_vector(self, vector, dt):
time = self.get_vector_time()
vector.set_angle(
vector.phase + time * vector.freq * TAU
)
vector.shift(
vector.center_func() - vector.get_start()
)
return vector
def get_circles(self, vectors):
return VGroup(*[
self.get_circle(
vector,
color=color
)
circle.add(circle.radial_line)
circle.freq = freq
circle.phase = phase
circle.rotate(phase)
circle.coefficient = coefficient
circle.center_func = center_func
for vector, color in zip(
vectors,
self.get_color_iterator()
)
])
def get_circle(self, vector, color=BLUE):
circle = Circle(color=color, **self.circle_config)
circle.center_func = vector.get_start
circle.radius_func = vector.get_length
circle.add_updater(self.update_circle)
return circle
def update_circle(self, circle, dt):
circle.rotate(
self.get_slow_factor() * circle.freq * dt * TAU
)
def update_circle(self, circle):
circle.set_width(2 * circle.radius_func())
circle.move_to(circle.center_func())
return circle
def get_rotating_vectors(self, circles):
return VGroup(*[
self.get_rotating_vector(circle)
for circle in circles
])
def get_rotating_vector(self, circle):
vector = Vector(RIGHT, color=WHITE)
vector.add_updater(lambda v, dt: v.put_start_and_end_on(
circle.get_center(),
circle.get_start(),
))
circle.vector = vector
return vector
def get_circle_end_path(self, circles, color=YELLOW):
coefs = [c.coefficient for c in circles]
freqs = [c.freq for c in circles]
center = circles[0].get_center()
def get_vector_sum_path(self, vectors, color=YELLOW):
coefs = [v.coefficient for v in vectors]
freqs = [v.freq for v in vectors]
center = vectors[0].get_start()
path = ParametricFunction(
lambda t: center + reduce(op.add, [
@@ -150,13 +155,14 @@ class FourierCirclesScene(Scene):
return path
# TODO, this should be a general animated mobect
def get_drawn_path(self, circles, stroke_width=2, **kwargs):
path = self.get_circle_end_path(circles, **kwargs)
def get_drawn_path(self, vectors, stroke_width=2, **kwargs):
path = self.get_vector_sum_path(vectors, **kwargs)
broken_path = CurvesAsSubmobjects(path)
broken_path.curr_time = 0
def update_path(path, dt):
alpha = path.curr_time * self.get_slow_factor()
# alpha = path.curr_time * self.get_slow_factor()
alpha = self.get_vector_time()
n_curves = len(path)
for a, sp in zip(np.linspace(0, 1, n_curves), path):
b = alpha - a
@@ -173,12 +179,12 @@ class FourierCirclesScene(Scene):
return broken_path
def get_y_component_wave(self,
circles,
vectors,
left_x=1,
color=PINK,
n_copies=2,
right_shift_rate=5):
path = self.get_circle_end_path(circles)
path = self.get_vector_sum_path(vectors)
wave = ParametricFunction(
lambda t: op.add(
right_shift_rate * t * LEFT,
@@ -216,15 +222,16 @@ class FourierCirclesScene(Scene):
return VGroup(wave, wave_copies)
def get_wave_y_line(self, circles, wave):
def get_wave_y_line(self, vectors, wave):
return DashedLine(
circles[-1].get_start(),
vectors[-1].get_end(),
wave[0].get_end(),
stroke_width=1,
dash_length=DEFAULT_DASH_LENGTH * 0.5,
)
# Computing Fourier series
# i.e. where all the math happens
def get_coefficients_of_path(self, path, n_samples=10000, freqs=None):
if freqs is None:
freqs = self.get_freqs()
@@ -250,7 +257,7 @@ class FourierCirclesScene(Scene):
class FourierSeriesIntroBackground4(FourierCirclesScene):
CONFIG = {
"n_circles": 4,
"n_vectors": 4,
"center_point": 4 * LEFT,
"run_time": 30,
"big_radius": 1.5,
@@ -271,7 +278,7 @@ class FourierSeriesIntroBackground4(FourierCirclesScene):
self.wait(self.run_time)
def get_ks(self):
return np.arange(1, 2 * self.n_circles + 1, 2)
return np.arange(1, 2 * self.n_vectors + 1, 2)
def get_freqs(self):
return self.base_frequency * self.get_ks()
@@ -282,53 +289,70 @@ class FourierSeriesIntroBackground4(FourierCirclesScene):
class FourierSeriesIntroBackground8(FourierSeriesIntroBackground4):
CONFIG = {
"n_circles": 8,
"n_vectors": 8,
}
class FourierSeriesIntroBackground12(FourierSeriesIntroBackground4):
CONFIG = {
"n_circles": 12,
"n_vectors": 12,
}
class FourierSeriesIntroBackground20(FourierSeriesIntroBackground4):
CONFIG = {
"n_circles": 20,
"n_vectors": 20,
}
class FourierOfPiSymbol(FourierCirclesScene):
CONFIG = {
"n_circles": 50,
"n_vectors": 51,
"center_point": ORIGIN,
"slow_factor": 0.1,
"run_time": 30,
"n_cycles": 1,
"tex": "\\pi",
"start_drawn": False,
"max_circle_stroke_width": 1,
}
def construct(self):
self.add_vectors_circles_path()
for n in range(self.n_cycles):
self.run_one_cycle()
def add_vectors_circles_path(self):
path = self.get_path()
coefs = self.get_coefficients_of_path(path)
circles = self.get_circles(coefficients=coefs)
vectors = self.get_rotating_vectors(coefficients=coefs)
circles = self.get_circles(vectors)
self.set_decreasing_stroke_widths(circles)
# approx_path = self.get_circle_end_path(circles)
drawn_path = self.get_drawn_path(circles)
# approx_path = self.get_vector_sum_path(circles)
drawn_path = self.get_drawn_path(vectors)
if self.start_drawn:
drawn_path.curr_time = 1 / self.slow_factor
self.vector_clock.increment_value(1)
self.add(path)
self.add(vectors)
self.add(circles)
self.add(drawn_path)
self.wait(self.run_time)
self.vectors = vectors
self.circles = circles
self.path = path
self.drawn_path = drawn_path
def run_one_cycle(self):
time = 1 / self.slow_factor
self.wait(time)
def set_decreasing_stroke_widths(self, circles):
mcsw = self.max_circle_stroke_width
for k, circle in zip(it.count(1), circles):
circle.set_stroke(width=max(
1 / np.sqrt(k),
1,
# mcsw / np.sqrt(k),
mcsw / k,
mcsw,
))
return circles
@@ -343,7 +367,7 @@ class FourierOfPiSymbol(FourierCirclesScene):
class FourierOfName(FourierOfPiSymbol):
CONFIG = {
"n_circles": 100,
"n_vectors": 100,
"name_color": WHITE,
"name_text": "Abc",
"time_per_symbol": 5,
@@ -388,14 +412,14 @@ class FourierOfName(FourierOfPiSymbol):
class FourierOfPiSymbol5(FourierOfPiSymbol):
CONFIG = {
"n_circles": 5,
"n_vectors": 5,
"run_time": 10,
}
class FourierOfTrebleClef(FourierOfPiSymbol):
CONFIG = {
"n_circles": 100,
"n_vectors": 101,
"run_time": 10,
"start_drawn": True,
"file_name": "TrebleClef",
@@ -419,7 +443,7 @@ class FourierOfIP(FourierOfTrebleClef):
CONFIG = {
"file_name": "IP_logo2",
"height": 6,
"n_circles": 100,
"n_vectors": 100,
}
# def construct(self):
@@ -451,7 +475,7 @@ class FourierOfEighthNote(FourierOfTrebleClef):
class FourierOfN(FourierOfTrebleClef):
CONFIG = {
"height": 6,
"n_circles": 1000,
"n_vectors": 1000,
}
def get_shape(self):
@@ -461,7 +485,7 @@ class FourierOfN(FourierOfTrebleClef):
class FourierNailAndGear(FourierOfTrebleClef):
CONFIG = {
"height": 6,
"n_circles": 200,
"n_vectors": 200,
"run_time": 100,
"slow_factor": 0.01,
"parametric_function_step_size": 0.0001,
@@ -479,7 +503,7 @@ class FourierNailAndGear(FourierOfTrebleClef):
class FourierBatman(FourierOfTrebleClef):
CONFIG = {
"height": 4,
"n_circles": 100,
"n_vectors": 100,
"run_time": 10,
"arrow_config": {
"tip_length": 0.1,
@@ -495,7 +519,7 @@ class FourierBatman(FourierOfTrebleClef):
class FourierHeart(FourierOfTrebleClef):
CONFIG = {
"height": 4,
"n_circles": 100,
"n_vectors": 100,
"run_time": 10,
"arrow_config": {
"tip_length": 0.1,
@@ -517,7 +541,7 @@ class FourierHeart(FourierOfTrebleClef):
class FourierNDQ(FourierOfTrebleClef):
CONFIG = {
"height": 4,
"n_circles": 1000,
"n_vectors": 1000,
"run_time": 10,
"arrow_config": {
"tip_length": 0.1,
@@ -535,7 +559,7 @@ class FourierNDQ(FourierOfTrebleClef):
class FourierGoogleG(FourierOfTrebleClef):
CONFIG = {
"n_circles": 10,
"n_vectors": 10,
"height": 5,
"g_colors": [
"#4285F4",
@@ -570,7 +594,7 @@ class FourierGoogleG(FourierOfTrebleClef):
class ExplainCircleAnimations(FourierCirclesScene):
CONFIG = {
"n_circles": 100,
"n_vectors": 100,
"center_point": 2 * DOWN,
"n_top_circles": 9,
"path_height": 3,

View File

@@ -185,6 +185,7 @@ class BringTwoRodsTogether(Scene):
y_label = axes.get_y_axis_label("\\text{Temperature}")
y_label.to_edge(UP)
axes.y_axis.label = y_label
axes.y_axis.add(y_label)
axes.y_axis.add_numbers(
*range(20, 100, 20)
@@ -329,7 +330,7 @@ class BringTwoRodsTogether(Scene):
else:
return 10
def update_graph(self, graph, dt, alpha=None, n_mini_steps=100):
def update_graph(self, graph, dt, alpha=None, n_mini_steps=500):
if alpha is None:
alpha = self.alpha
points = np.append(
@@ -349,16 +350,16 @@ class BringTwoRodsTogether(Scene):
if (0 < i < len(points) - 1):
second_deriv = d2y / (dx**2)
else:
second_deriv = 0.5 * d2y / dx
second_deriv = 0
second_deriv = 2 * d2y / (dx**2)
# second_deriv = 0
y_change[i] = alpha * second_deriv * dt / n_mini_steps
# y_change[0] = y_change[1]
# y_change[-1] = y_change[-2]
y_change[0] = 0
y_change[-1] = 0
y_change -= np.mean(y_change)
# y_change[0] = 0
# y_change[-1] = 0
# y_change -= np.mean(y_change)
points[:, 1] += y_change
graph.set_points_smoothly(points)
return graph
@@ -374,8 +375,17 @@ class BringTwoRodsTogether(Scene):
)[1]
for alt_x in (x - dx, x, x + dx)
]
d2y = ry - 2 * y + ly
return d2y / (dx**2)
# At the boundary, don't return the second deriv,
# but instead something matching the Neumann
# boundary condition.
if x == x_max:
return (ly - y) / dx
elif x == x_min:
return (ry - y) / dx
else:
d2y = ry - 2 * y + ly
return d2y / (dx**2)
def get_rod(self, x_min, x_max, n_pieces=None):
if n_pieces is None:
@@ -407,7 +417,7 @@ class BringTwoRodsTogether(Scene):
self.rod_point_to_color(piece.get_right()),
])
def rod_point_to_color(self, point):
def rod_point_to_graph_y(self, point):
axes = self.axes
x = axes.x_axis.p2n(point)
@@ -417,11 +427,16 @@ class BringTwoRodsTogether(Scene):
self.graph_x_max,
x,
)
y = axes.y_axis.p2n(
return axes.y_axis.p2n(
graph.point_from_proportion(alpha)
)
return temperature_to_color(
(y - 45) / 45
def y_to_color(self, y):
return temperature_to_color((y - 45) / 45)
def rod_point_to_color(self, point):
return self.y_to_color(
self.rod_point_to_graph_y(point)
)
@@ -450,6 +465,7 @@ class ShowEvolvingTempGraphWithArrows(BringTwoRodsTogether):
self.add_clock()
self.add_rod()
self.add_arrows()
self.initialize_updaters()
self.let_play()
def add_axes(self):
@@ -467,7 +483,10 @@ class ShowEvolvingTempGraphWithArrows(BringTwoRodsTogether):
self.time_label.next_to(self.clock, DOWN)
def add_rod(self):
rod = self.rod = self.get_rod(0, 10)
rod = self.rod = self.get_rod(
self.graph_x_min,
self.graph_x_max,
)
self.add(rod)
def add_arrows(self):
@@ -504,24 +523,25 @@ class ShowEvolvingTempGraphWithArrows(BringTwoRodsTogether):
self.add(arrows)
self.arrows = arrows
def initialize_updaters(self):
if hasattr(self, "graph"):
self.graph.add_updater(self.update_graph)
if hasattr(self, "rod"):
self.rod.add_updater(self.color_rod_by_graph)
if hasattr(self, "time_label"):
self.time_label.add_updater(
lambda d, dt: d.increment_value(dt)
)
def let_play(self):
graph = self.graph
rod = self.rod
clock = self.clock
time_label = self.time_label
self.run_clock(self.wait_time)
graph.add_updater(self.update_graph)
time_label.add_updater(
lambda d, dt: d.increment_value(dt)
)
rod.add_updater(self.color_rod_by_graph)
# return
def run_clock(self, time):
self.play(
ClockPassesTime(
clock,
run_time=self.wait_time,
hours_passed=self.wait_time,
self.clock,
run_time=time,
hours_passed=time,
),
)

View File

@@ -0,0 +1,303 @@
from manimlib.imports import *
from active_projects.ode.part2.heat_equation import *
class ShowNewRuleAtDiscreteBoundary(DiscreteSetup):
CONFIG = {
"axes_config": {
"x_min": 0,
"stroke_width": 1,
"x_axis_config": {
"include_tip": False,
},
},
"freq_amplitude_pairs": [
(1, 0.5),
(2, 1),
(3, 0.5),
(4, 0.3),
],
"v_line_class": DashedLine,
"v_line_config": {
},
"step_size": 1,
"wait_time": 15,
"alpha": 0.25,
}
def construct(self):
self.add_axes()
self.set_points()
self.show_boundary_point_influenced_by_neighbor()
self.add_clock()
self.let_evolve()
def set_points(self):
axes = self.axes
for mob in axes.family_members_with_points():
if isinstance(mob, Line):
mob.set_stroke(width=1)
step_size = self.step_size
xs = np.arange(
axes.x_min,
axes.x_max + step_size,
step_size
)
dots = self.dots = self.get_dots(axes, xs)
self.v_lines = self.get_v_lines(dots)
self.rod_pieces = self.get_rod_pieces(dots)
# rod_pieces
self.add(self.dots)
self.add(self.v_lines)
self.add(self.rod_pieces)
def show_boundary_point_influenced_by_neighbor(self):
dots = self.dots
ld = dots[0]
ld_in = dots[1]
rd = dots[-1]
rd_in = dots[-2]
v_len = 0.75
l_arrow = Vector(v_len * LEFT)
l_arrow.move_to(ld.get_left(), RIGHT)
r_arrow = Vector(v_len * RIGHT)
r_arrow.move_to(rd.get_right(), LEFT)
arrows = VGroup(l_arrow, r_arrow)
q_marks = VGroup(*[
TexMobject("?").scale(1.5).next_to(
arrow, arrow.get_vector()
)
for arrow in arrows
])
arrows.set_color(YELLOW)
q_marks.set_color(YELLOW)
blocking_rects = VGroup(*[
BackgroundRectangle(VGroup(
*dots[i:-i],
*self.rod_pieces[i:-i]
))
for i in [1, 2]
])
for rect in blocking_rects:
rect.stretch(1.1, dim=1, about_edge=UP)
self.play(FadeIn(blocking_rects[0]))
self.play(
LaggedStartMap(ShowCreation, arrows),
LaggedStart(*[
FadeInFrom(q_mark, -arrow.get_vector())
for q_mark, arrow in zip(q_marks, arrows)
]),
run_time=1.5
)
self.wait()
# Point to inward neighbor
new_arrows = VGroup(*[
Arrow(
d1.get_center(),
VGroup(d1, d2).get_center(),
buff=0,
).match_style(l_arrow)
for d1, d2 in [(ld, ld_in), (rd, rd_in)]
])
new_arrows.match_style(arrows)
l_brace = Brace(VGroup(ld, ld_in), DOWN)
r_brace = Brace(VGroup(rd, rd_in), DOWN)
braces = VGroup(l_brace, r_brace)
for brace in braces:
brace.align_to(
self.axes.x_axis.get_center(), UP
)
brace.shift(SMALL_BUFF * DOWN)
brace.add(brace.get_tex("\\Delta x"))
self.play(
ReplacementTransform(arrows, new_arrows),
FadeOut(q_marks),
ReplacementTransform(*blocking_rects)
)
self.wait()
self.play(FadeInFrom(braces, UP))
self.wait()
self.play(
FadeOut(new_arrows),
FadeOut(blocking_rects[1]),
FadeOut(braces),
)
def add_clock(self):
super().add_clock()
self.time_label.add_updater(
lambda d, dt: d.increment_value(dt)
)
VGroup(
self.clock,
self.time_label
).shift(2 * LEFT)
def let_evolve(self):
dots = self.dots
dots.add_updater(self.update_dots)
wait_time = self.wait_time
self.play(
ClockPassesTime(
self.clock,
run_time=wait_time,
hours_passed=wait_time,
),
)
#
def get_dots(self, axes, xs):
dots = VGroup(*[
Dot(axes.c2p(x, self.temp_func(x, 0)))
for x in xs
])
max_width = 0.8 * self.step_size
for dot in dots:
dot.add_updater(self.update_dot_color)
if dot.get_width() > max_width:
dot.set_width(max_width)
return dots
def get_v_lines(self, dots):
return always_redraw(lambda: VGroup(*[
self.get_v_line(dot)
for dot in dots
]))
def get_v_line(self, dot):
x_axis = self.axes.x_axis
bottom = dot.get_bottom()
x = x_axis.p2n(bottom)
proj_point = x_axis.n2p(x)
return self.v_line_class(
proj_point, bottom,
**self.v_line_config,
)
def get_rod_pieces(self, dots):
axis = self.axes.x_axis
factor = 1 - np.exp(-(0.8 / self.step_size)**2)
width = factor * self.step_size
pieces = VGroup()
for dot in dots:
piece = Line(ORIGIN, width * RIGHT)
piece.set_stroke(width=5)
piece.move_to(dot)
piece.set_y(axis.get_center()[1])
piece.dot = dot
piece.add_updater(
lambda p: p.match_color(p.dot)
)
pieces.add(piece)
return pieces
def update_dot_color(self, dot):
y = self.axes.y_axis.p2n(dot.get_center())
dot.set_color(self.y_to_color(y))
def update_dots(self, dots, dt):
for ds in zip(dots, dots[1:], dots[2:]):
points = [d.get_center() for d in ds]
x0, x1, x2 = [p[0] for p in points]
dx = x1 - x0
y0, y1, y2 = [p[1] for p in points]
self.update_dot(
dot=ds[1],
dt=dt,
mean_diff=0.5 * (y2 - 2 * y1 + y0) / dx
)
if ds[0] is dots[0]:
self.update_dot(
dot=ds[0],
dt=dt,
mean_diff=(y1 - y0) / dx
)
elif ds[-1] is dots[-1]:
self.update_dot(
dot=ds[-1],
dt=dt,
mean_diff=(y1 - y2) / dx
)
def update_dot(self, dot, dt, mean_diff):
dot.shift(mean_diff * self.alpha * dt * UP)
class DiscreteEvolutionPoint25(ShowNewRuleAtDiscreteBoundary):
CONFIG = {
"step_size": 0.25,
"alpha": 0.5,
"wait_time": 30,
}
def construct(self):
self.add_axes()
self.set_points()
self.add_clock()
self.let_evolve()
class DiscreteEvolutionPoint1(DiscreteEvolutionPoint25):
CONFIG = {
"step_size": 0.1,
"v_line_config": {
"stroke_width": 1,
},
"wait_time": 30,
}
class FlatEdgesForDiscreteEvolution(DiscreteEvolutionPoint1):
CONFIG = {
"wait_time": 20,
"step_size": 0.1,
}
def let_evolve(self):
lines = VGroup(*[
Line(LEFT, RIGHT)
for x in range(2)
])
lines.set_width(1.5)
lines.set_stroke(WHITE, 5, opacity=0.5)
lines.add_updater(self.update_lines)
turn_animation_into_updater(
ShowCreation(lines, run_time=2)
)
self.add(lines)
super().let_evolve()
def update_lines(self, lines):
dots = self.dots
for line, dot in zip(lines, [dots[0], dots[-1]]):
line.move_to(dot)
class FlatEdgesForDiscreteEvolutionTinySteps(FlatEdgesForDiscreteEvolution):
CONFIG = {
"step_size": 0.025,
"wait_time": 10,
"v_line_class": Line,
"v_line_config": {
"stroke_opacity": 0.5,
}
}

View File

@@ -0,0 +1,175 @@
from manimlib.imports import *
from active_projects.ode.part2.wordy_scenes import *
class IveHeardOfThis(TeacherStudentsScene):
def construct(self):
point = VectorizedPoint()
point.move_to(3 * RIGHT + 2 * UP)
self.student_says(
"I've heard\\\\", "of this!",
student_index=1,
target_mode="hooray",
bubble_kwargs={
"height": 3,
"width": 3,
"direction": RIGHT,
},
run_time=1,
)
self.change_student_modes(
"thinking", "hooray", "thinking",
look_at_arg=point,
added_anims=[self.teacher.change, "happy"]
)
self.wait(3)
self.student_says(
"But who\\\\", "cares?",
student_index=1,
target_mode="maybe",
bubble_kwargs={
"direction": RIGHT,
"width": 3,
"height": 3,
},
run_time=1,
)
self.change_student_modes(
"pondering", "maybe", "pondering",
look_at_arg=point,
added_anims=[self.teacher.change, "guilty"]
)
self.wait(5)
class InFouriersShoes(PiCreatureScene, WriteHeatEquationTemplate):
def construct(self):
randy = self.pi_creature
fourier = ImageMobject("Joseph Fourier")
fourier.set_height(4)
fourier.next_to(randy, RIGHT, LARGE_BUFF)
fourier.align_to(randy, DOWN)
equation = self.get_d1_equation()
equation.next_to(fourier, UP, MED_LARGE_BUFF)
decades = list(range(1740, 2040, 20))
time_line = NumberLine(
x_min=decades[0],
x_max=decades[-1],
tick_frequency=1,
tick_size=0.05,
longer_tick_multiple=4,
unit_size=0.2,
numbers_with_elongated_ticks=decades,
numbers_to_show=decades,
decimal_number_config={
"group_with_commas": False,
},
stroke_width=2,
)
time_line.add_numbers()
time_line.move_to(ORIGIN, RIGHT)
time_line.to_edge(UP)
triangle = ArrowTip(start_angle=-90 * DEGREES)
triangle.set_height(0.25)
triangle.move_to(time_line.n2p(2019), DOWN)
triangle.set_color(WHITE)
self.play(FadeInFrom(fourier, 2 * LEFT))
self.play(randy.change, "pondering")
self.wait()
self.play(
DrawBorderThenFill(triangle, run_time=1),
FadeInFromDown(equation),
FadeIn(time_line),
)
self.play(
Animation(triangle),
ApplyMethod(
time_line.shift,
time_line.n2p(2019) - time_line.n2p(1822),
run_time=5
),
)
self.wait()
class SineCurveIsUnrealistic(TeacherStudentsScene):
def construct(self):
self.student_says(
"But that would\\\\never happen!",
student_index=1,
bubble_kwargs={
"direction": RIGHT,
"height": 3,
"width": 4,
},
target_mode="angry"
)
self.change_student_modes(
"guilty", "angry", "hesitant",
added_anims=[
self.teacher.change, "tease"
]
)
self.wait(3)
self.play(
RemovePiCreatureBubble(self.students[1]),
self.teacher.change, "raise_right_hand"
)
self.change_all_student_modes(
"pondering",
look_at_arg=3 * UP,
)
self.wait(5)
class IfOnly(TeacherStudentsScene):
def construct(self):
self.teacher_says(
"If only!",
target_mode="angry"
)
self.change_all_student_modes(
"confused",
look_at_arg=self.screen
)
self.wait(3)
class SoWeGotNowhere(TeacherStudentsScene):
def construct(self):
self.student_says(
"So we've gotten\\\\nowhere!",
target_mode="angry",
added_anims=[
self.teacher.change, "guilty"
]
)
self.change_all_student_modes("angry")
self.wait()
text = TexMobject(
"&\\text{Actually,}\\\\",
"&\\sin\\left({x}\\right)"
"e^{-\\alpha {t}}\\\\",
"&\\text{isn't far off.}",
tex_to_color_map={
"{x}": GREEN,
"{t}": YELLOW,
}
)
text.scale(0.8)
self.teacher_says(
text,
content_introduction_class=FadeIn,
bubble_kwargs={
"width": 4,
"height": 3.5,
}
)
self.change_all_student_modes(
"pondering",
look_at_arg=self.screen
)
self.wait(3)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,919 @@
from manimlib.imports import *
from active_projects.ode.part2.wordy_scenes import *
class ThreeMainObservations(Scene):
def construct(self):
fourier = ImageMobject("Joseph Fourier")
name = TextMobject("Joseph Fourier")
name.match_width(fourier)
name.next_to(fourier, DOWN, SMALL_BUFF)
fourier.add(name)
fourier.set_height(5)
fourier.to_corner(DR)
fourier.shift(LEFT)
bubble = ThoughtBubble(
direction=RIGHT,
height=3,
width=4,
)
bubble.move_tip_to(fourier.get_corner(UL) + 0.5 * DR)
observations = VGroup(
TextMobject(
"1)",
"Sine = Nice",
),
TextMobject(
"2)",
"Linearity"
),
TextMobject(
"3)",
"Fourier series"
),
)
# heart = SuitSymbol("hearts")
# heart.replace(observations[0][2])
# observations[0][2].become(heart)
# observations[0][1].add(happiness)
# observations[2][2].align_to(
# observations[2][1], LEFT,
# )
observations.arrange(
DOWN,
aligned_edge=LEFT,
buff=2 * LARGE_BUFF,
)
observations.set_height(FRAME_HEIGHT - 2)
observations.to_corner(UL, buff=LARGE_BUFF)
self.add(fourier)
self.play(ShowCreation(bubble))
self.wait()
self.play(LaggedStart(*[
TransformFromCopy(bubble, observation[0])
for observation in observations
], lag_ratio=0.2))
self.play(
FadeOut(fourier),
FadeOut(bubble),
)
self.wait()
for obs in observations:
self.play(FadeInFrom(obs[1], LEFT))
self.wait()
class LastChapterWrapper(Scene):
def construct(self):
full_rect = FullScreenFadeRectangle(
fill_color=DARK_GREY,
fill_opacity=1,
)
rect = ScreenRectangle(height=6)
rect.set_stroke(WHITE, 2)
rect.set_fill(BLACK, 1)
title = TextMobject("Last chapter")
title.scale(2)
title.to_edge(UP)
rect.next_to(title, DOWN)
self.add(full_rect)
self.play(
FadeIn(rect),
Write(title, run_time=2),
)
self.wait()
class ThreeConstraints(WriteHeatEquationTemplate):
def construct(self):
self.cross_out_solving()
self.show_three_conditions()
def cross_out_solving(self):
equation = self.get_d1_equation()
words = TextMobject("Solve this equation")
words.to_edge(UP)
equation.next_to(words, DOWN)
cross = Cross(words)
self.add(words, equation)
self.wait()
self.play(ShowCreation(cross))
self.wait()
self.equation = equation
self.to_remove = VGroup(words, cross)
def show_three_conditions(self):
equation = self.equation
to_remove = self.to_remove
title = TexMobject(
"\\text{Constraints }"
"T({x}, {t})"
"\\text{ must satisfy:}",
**self.tex_mobject_config
)
title.to_edge(UP)
items = VGroup(
TextMobject("1)", "The PDE"),
TextMobject("2)", "Boundary condition"),
TextMobject("3)", "Initial condition"),
)
items.scale(0.7)
items.arrange(RIGHT, buff=LARGE_BUFF)
items.set_width(FRAME_WIDTH - 2)
items.next_to(title, DOWN, LARGE_BUFF)
items[1].set_color(MAROON_B)
items[2].set_color(RED)
bc_paren = TextMobject("(Explained soon)")
bc_paren.scale(0.7)
bc_paren.next_to(items[1], DOWN)
self.play(
FadeInFromDown(title),
FadeOutAndShift(to_remove, UP),
equation.scale, 0.6,
equation.next_to, items[0], DOWN,
equation.shift_onto_screen,
LaggedStartMap(FadeIn, [
items[0],
items[1][0],
items[2][0],
])
)
self.wait()
self.play(Write(items[1][1]))
bc_paren.match_y(equation)
self.play(FadeInFrom(bc_paren, UP))
self.wait(2)
self.play(Write(items[2][1]))
self.wait(2)
self.title = title
self.items = items
self.pde = equation
self.bc_paren = bc_paren
class RectAroundEquation(WriteHeatEquationTemplate):
def construct(self):
eq = self.get_d1_equation()
self.play(ShowCreationThenFadeAround(eq))
class BorderRect(Scene):
def construct(self):
rect = FullScreenFadeRectangle()
rect.set_stroke(WHITE, 3)
rect.set_fill(opacity=0)
self.add(rect)
class SeekIdealized(Scene):
def construct(self):
phrases = VGroup(*[
TextMobject(
"Seek", text, "problems",
tex_to_color_map={
"realistic": GREEN,
"{idealized}": YELLOW,
"over-idealized": YELLOW,
"general": BLUE,
}
)
for text in [
"realistic",
"{idealized}",
"over-idealized",
"general",
]
])
phrases.scale(2)
words = VGroup()
for phrase in phrases:
phrase.center()
word = phrase[1]
words.add(word)
phrase.remove(word)
arrow = Vector(DOWN)
arrow.set_stroke(WHITE, 6)
arrow.next_to(words[3], UP)
low_arrow = arrow.copy()
low_arrow.next_to(words[3], DOWN)
solutions = TextMobject("solutions")
solutions.scale(2)
solutions.move_to(phrases[3][1], UL)
models = TextMobject("models")
models.scale(2)
models.next_to(
words[0], RIGHT, buff=0.35,
aligned_edge=DOWN
)
phrases.center()
phrase = phrases[0]
self.add(phrase)
self.add(words[0])
self.wait()
words[0].save_state()
self.play(
words[0].to_edge, DOWN,
words[0].set_opacity, 0.5,
Transform(phrase, phrases[1]),
FadeInFrom(words[1], UP)
)
self.wait()
# self.play(
# words[1].move_to, words[2], RIGHT,
# FadeIn(words[2]),
# Transform(phrase, phrases[2])
# )
# self.wait()
self.play(
words[1].next_to, arrow, UP,
ShowCreation(arrow),
MaintainPositionRelativeTo(
phrase, words[1]
),
FadeInFrom(solutions, LEFT),
FadeIn(words[3]),
)
self.wait()
words[0].generate_target()
words[0].target.next_to(low_arrow, DOWN)
words[0].target.set_opacity(1)
models.shift(
words[0].target.get_center() -
words[0].saved_state.get_center()
)
self.play(
MoveToTarget(words[0]),
ShowCreation(low_arrow),
FadeInFrom(models, LEFT)
)
self.wait()
class SecondDerivativeOfSine(Scene):
def construct(self):
equation = TexMobject(
"{d^2 \\over d{x}^2}",
"\\cos\\left({x}\\right) =",
"-\\cos\\left({x}\\right)",
tex_to_color_map={
"{x}": GREEN,
}
)
self.add(equation)
class EquationAboveSineAnalysis(WriteHeatEquationTemplate):
def construct(self):
equation = self.get_d1_equation()
equation.to_edge(UP)
equation.shift(2 * LEFT)
eq_index = equation.index_of_part_by_tex("=")
lhs = equation[:eq_index]
eq = equation[eq_index]
rhs = equation[eq_index + 1:]
t_terms = equation.get_parts_by_tex("{t}")[1:]
t_terms.save_state()
zeros = VGroup(*[
TexMobject("0").replace(t, dim_to_match=1)
for t in t_terms
])
zeros.align_to(t_terms, DOWN)
new_rhs = TexMobject(
"=", "-\\alpha \\cdot {T}", "({x}, 0)",
**self.tex_mobject_config
)
# new_rhs.move_to(equation.get_right())
# new_rhs.next_to(equation, DOWN, MED_LARGE_BUFF)
# new_rhs.align_to(eq, LEFT)
new_rhs.next_to(equation, RIGHT)
new_rhs.shift(SMALL_BUFF * DOWN)
self.add(equation)
self.play(ShowCreationThenFadeAround(rhs))
self.wait()
self.play(
FadeOutAndShift(t_terms, UP),
FadeInFrom(zeros, DOWN),
)
t_terms.fade(1)
self.wait()
self.play(
# VGroup(equation, zeros).next_to,
# new_rhs, LEFT,
FadeIn(new_rhs),
)
self.wait()
self.play(
VGroup(
lhs[6:],
eq,
rhs,
new_rhs[0],
new_rhs[-3:],
zeros,
).fade, 0.5,
)
self.play(ShowCreationThenFadeAround(lhs[:6]))
self.play(ShowCreationThenFadeAround(new_rhs[1:-3]))
self.wait()
class ExpVideoWrapper(Scene):
def construct(self):
self.add(FullScreenFadeRectangle(
fill_color=DARKER_GREY,
fill_opacity=1,
))
screen = ImageMobject("eoc_chapter5_thumbnail")
screen.set_height(6)
rect = SurroundingRectangle(screen, buff=0)
rect.set_stroke(WHITE, 2)
screen.add(rect)
title = TextMobject("Need a refresher?")
title.scale(1.5)
title.to_edge(UP)
screen.next_to(title, DOWN)
screen.center()
self.play(
# FadeInFrom(title, LEFT),
FadeInFrom(screen, DOWN),
)
self.wait()
class ShowSinExpDerivatives(WriteHeatEquationTemplate):
CONFIG = {
"tex_mobject_config": {
"tex_to_color_map": {
"{0}": WHITE,
"\\partial": WHITE,
"=": WHITE,
}
}
}
def construct(self):
pde = self.get_d1_equation_without_inputs()
pde.to_edge(UP)
pde.generate_target()
new_rhs = TexMobject(
"=- \\alpha \\cdot T",
**self.tex_mobject_config,
)
new_rhs.next_to(pde, RIGHT)
new_rhs.align_to(pde.get_part_by_tex("alpha"), DOWN)
equation1 = TexMobject(
"T({x}, {0}) = \\sin\\left({x}\\right)",
**self.tex_mobject_config
)
equation2 = TexMobject(
"T({x}, {t}) = \\sin\\left({x}\\right)",
"e^{-\\alpha{t}}",
**self.tex_mobject_config
)
for eq in equation1, equation2:
eq.next_to(pde, DOWN, MED_LARGE_BUFF)
eq2_part1 = equation2[:len(equation1)]
eq2_part2 = equation2[len(equation1):]
# Rectangles
exp_rect = SurroundingRectangle(eq2_part2)
exp_rect.set_stroke(RED, 3)
sin_rect = SurroundingRectangle(
eq2_part1[-3:]
)
sin_rect.set_color(BLUE)
VGroup(pde.target, new_rhs).center().to_edge(UP)
# Show proposed solution
self.add(pde)
self.add(equation1)
self.wait()
self.play(
MoveToTarget(pde),
FadeInFrom(new_rhs, LEFT)
)
self.wait()
self.play(
ReplacementTransform(equation1, eq2_part1),
FadeIn(eq2_part2),
)
self.play(ShowCreation(exp_rect))
self.wait()
self.play(FadeOut(exp_rect))
# Take partial derivatives wrt x
q_mark = TexMobject("?")
q_mark.next_to(pde.get_part_by_tex("="), UP)
q_mark.set_color(RED)
arrow1 = Vector(3 * DOWN + 1 * RIGHT, color=WHITE)
arrow1.scale(1.2 / arrow1.get_length())
arrow1.next_to(
eq2_part2.get_corner(DL),
DOWN, MED_LARGE_BUFF
)
ddx_label1 = TexMobject(
"\\partial \\over \\partial {x}",
**self.tex_mobject_config,
)
ddx_label1.scale(0.7)
ddx_label1.next_to(
arrow1.point_from_proportion(0.8),
UR, SMALL_BUFF
)
pde_ddx = VGroup(
*pde.get_parts_by_tex("\\partial")[2:],
pde.get_parts_by_tex("\\over")[1],
pde.get_part_by_tex("{x}"),
)
pde_ddx_rect = SurroundingRectangle(pde_ddx)
pde_ddx_rect.set_color(GREEN)
eq2_part2_rect = SurroundingRectangle(eq2_part2)
dx = TexMobject(
"\\cos\\left({x}\\right)", "e^{-\\alpha {t}}",
**self.tex_mobject_config
)
ddx = TexMobject(
"-\\sin\\left({x}\\right)", "e^{-\\alpha {t}}",
**self.tex_mobject_config
)
dx.next_to(arrow1, DOWN)
dx.align_to(eq2_part2, RIGHT)
x_shift = arrow1.get_end()[0] - arrow1.get_start()[0]
x_shift *= 2
dx.shift(x_shift * RIGHT)
arrow2 = arrow1.copy()
arrow2.next_to(dx, DOWN)
arrow2.shift(MED_SMALL_BUFF * RIGHT)
dx_arrows = VGroup(arrow1, arrow2)
ddx_label2 = ddx_label1.copy()
ddx_label2.shift(
arrow2.get_center() - arrow1.get_center()
)
ddx.next_to(arrow2, DOWN)
ddx.align_to(eq2_part2, RIGHT)
ddx.shift(2 * x_shift * RIGHT)
rhs = equation2[-6:]
self.play(
FadeInFromDown(q_mark)
)
self.play(
ShowCreation(pde_ddx_rect)
)
self.wait()
self.play(
LaggedStart(
GrowArrow(arrow1),
GrowArrow(arrow2),
),
TransformFromCopy(
pde_ddx[0], ddx_label1
),
TransformFromCopy(
pde_ddx[0], ddx_label2
),
)
self.wait()
self.play(
TransformFromCopy(rhs, dx)
)
self.wait()
self.play(
FadeIn(eq2_part2_rect)
)
self.play(
Transform(
eq2_part2_rect,
SurroundingRectangle(dx[-3:])
)
)
self.play(
FadeOut(eq2_part2_rect)
)
self.wait()
self.play(
TransformFromCopy(dx, ddx)
)
self.play(
FadeIn(
SurroundingRectangle(ddx).match_style(
pde_ddx_rect
)
)
)
self.wait()
# Take partial derivative wrt t
pde_ddt = pde[:pde.index_of_part_by_tex("=") - 1]
pde_ddt_rect = SurroundingRectangle(pde_ddt)
dt_arrow = Arrow(
arrow1.get_start(),
arrow2.get_end() + RIGHT,
buff=0
)
dt_arrow.flip(UP)
dt_arrow.next_to(dx_arrows, LEFT, MED_LARGE_BUFF)
dt_label = TexMobject(
"\\partial \\over \\partial {t}",
**self.tex_mobject_config,
)
dt_label.scale(1)
dt_label.next_to(
dt_arrow.get_center(), UL,
SMALL_BUFF,
)
rhs_copy = rhs.copy()
rhs_copy.next_to(dt_arrow.get_end(), DOWN)
rhs_copy.shift(MED_LARGE_BUFF * LEFT)
rhs_copy.match_y(ddx)
minus_alpha_in_exp = rhs_copy[-3][1:].copy()
minus_alpha_in_exp.set_color(RED)
minus_alpha = TexMobject("-\\alpha")
minus_alpha.next_to(rhs_copy, LEFT)
minus_alpha.align_to(rhs_copy[0][0], DOWN)
dot = TexMobject("\\cdot")
dot.move_to(midpoint(
minus_alpha.get_right(),
rhs_copy.get_left(),
))
self.play(
TransformFromCopy(
pde_ddx_rect,
pde_ddt_rect,
)
)
self.play(
GrowArrow(dt_arrow),
TransformFromCopy(
pde_ddt,
dt_label,
)
)
self.wait()
self.play(TransformFromCopy(rhs, rhs_copy))
self.play(FadeIn(minus_alpha_in_exp))
self.play(
ApplyMethod(
minus_alpha_in_exp.replace, minus_alpha,
path_arc=TAU / 4
),
FadeIn(dot),
)
self.play(
FadeIn(minus_alpha),
FadeOut(minus_alpha_in_exp),
)
self.wait()
rhs_copy.add(minus_alpha, dot)
self.play(
FadeIn(SurroundingRectangle(rhs_copy))
)
self.wait()
#
checkmark = TexMobject("\\checkmark")
checkmark.set_color(GREEN)
checkmark.move_to(q_mark, DOWN)
self.play(
FadeInFromDown(checkmark),
FadeOutAndShift(q_mark, UP)
)
self.wait()
class DerivativesOfLinearFunction(WriteHeatEquationTemplate):
CONFIG = {
"tex_mobject_config": {
"tex_to_color_map": {
"{c}": WHITE,
}
}
}
def construct(self):
func = TexMobject(
"T({x}, {t}) = {c} \\cdot {x}",
**self.tex_mobject_config
)
dx_T = TexMobject("{c}", **self.tex_mobject_config)
ddx_T = TexMobject("0")
dt_T = TexMobject("0")
for mob in func, dx_T, ddx_T, dt_T:
mob.scale(1.5)
func.generate_target()
arrows = VGroup(*[
Vector(1.5 * RIGHT, color=WHITE)
for x in range(3)
])
dx_arrows = arrows[:2]
dt_arrow = arrows[2]
dt_arrow.rotate(-TAU / 4)
dx_group = VGroup(
func.target,
dx_arrows[0],
dx_T,
dx_arrows[1],
ddx_T,
)
dx_group.arrange(RIGHT)
for arrow, char, vect in zip(arrows, "xxt", [UP, UP, RIGHT]):
label = TexMobject(
"\\partial \\over \\partial {%s}" % char,
**self.tex_mobject_config
)
label.scale(0.7)
label.next_to(arrow.get_center(), vect)
arrow.add(label)
dt_arrow.shift(
func.target[-3:].get_bottom() + MED_SMALL_BUFF * DOWN -
dt_arrow.get_start(),
)
dt_T.next_to(dt_arrow.get_end(), DOWN)
self.play(FadeInFromDown(func))
self.wait()
self.play(
MoveToTarget(func),
LaggedStartMap(Write, dx_arrows),
run_time=1,
)
self.play(
TransformFromCopy(func[-3:], dx_T),
path_arc=-TAU / 4,
)
self.play(
TransformFromCopy(dx_T, ddx_T),
path_arc=-TAU / 4,
)
self.wait()
# dt
self.play(Write(dt_arrow))
self.play(
TransformFromCopy(func[-3:], dt_T)
)
self.wait()
class FlatAtBoundaryWords(Scene):
def construct(self):
words = self.get_bc_words()
self.play(Write(words))
self.wait()
def get_bc_words(self):
return TextMobject(
"Flat at boundary\\\\"
"for all", "${t}$", "$> 0$",
)
class WriteOutBoundaryCondition(FlatAtBoundaryWords, ThreeConstraints, MovingCameraScene):
def construct(self):
self.force_skipping()
ThreeConstraints.construct(self)
self.revert_to_original_skipping_status()
self.add_ic()
self.write_bc_words()
self.write_bc_equation()
def add_ic(self):
image = ImageMobject("temp_initial_condition_example")
image.set_width(3)
border = SurroundingRectangle(image, buff=SMALL_BUFF)
border.shift(SMALL_BUFF * UP)
border.set_stroke(WHITE, 2)
group = Group(image, border)
group.next_to(self.items[2], DOWN)
self.add(group)
def write_bc_words(self):
bc_paren = self.bc_paren
bc_words = self.get_bc_words()
bc_words.match_width(self.items[1][1])
bc_words.move_to(bc_paren, UP)
bc_words.set_color_by_tex("{t}", YELLOW)
self.play(ShowCreationThenFadeAround(
VGroup(self.items[0], self.pde)
))
self.play(
FadeOutAndShift(bc_paren, UP),
FadeInFrom(bc_words, DOWN),
)
self.wait()
self.bc_words = bc_words
def write_bc_equation(self):
bc_words = self.bc_words
equation = TexMobject(
"{\\partial {T} \\over \\partial {x}}(0, {t}) = ",
"{\\partial {T} \\over \\partial {x}}(L, {t}) = ",
"0",
**self.tex_mobject_config,
)
equation.next_to(bc_words, DOWN, MED_LARGE_BUFF)
self.play(
self.camera_frame.shift, 0.8 * DOWN,
)
self.play(FadeInFrom(equation, UP))
self.wait()
class HeatEquationFrame(WriteHeatEquationTemplate):
def construct(self):
equation = self.get_d1_equation()
equation.to_edge(UP, buff=MED_SMALL_BUFF)
ddx = equation[-11:]
dt = equation[:11]
full_rect = FullScreenFadeRectangle(
fill_color=DARK_GREY,
fill_opacity=1,
)
smaller_rect = ScreenRectangle(
height=6,
fill_color=BLACK,
fill_opacity=1,
stroke_color=WHITE,
stroke_width=2,
)
smaller_rect.next_to(equation, DOWN)
self.add(full_rect)
self.add(smaller_rect)
self.add(equation)
self.wait()
self.play(ShowCreationThenFadeAround(
ddx,
surrounding_rectangle_config={
"stroke_color": GREEN,
}
))
self.wait()
self.play(ShowCreationThenFadeAround(dt))
self.wait()
class CompareFreqDecays1to2(Scene):
CONFIG = {
"freqs": [1, 2]
}
def construct(self):
background = FullScreenFadeRectangle(
fill_color=DARKER_GREY,
fill_opacity=1,
)
screens = VGroup(*[
ScreenRectangle(
height=4,
fill_color=BLACK,
fill_opacity=1,
stroke_width=1,
stroke_color=WHITE,
)
for x in range(2)
])
screens.arrange(RIGHT)
screens.set_width(FRAME_WIDTH - 1)
formulas = VGroup(*[
self.get_formula(freq)
for freq in self.freqs
])
for formula, screen in zip(formulas, screens):
formula.next_to(screen, UP)
self.add(background)
self.add(screens)
self.add(formulas)
self.wait()
def get_formula(self, freq):
f_str = str(freq)
return TexMobject(
"\\cos\\left(%s \\cdot {x}\\right)" % f_str,
"e^{-\\alpha \\cdot %s^2 \\cdot {t}}" % f_str,
tex_to_color_map={
"{x}": GREEN,
"{t}": YELLOW,
f_str: MAROON_B,
}
)
class CompareFreqDecays1to4(CompareFreqDecays1to2):
CONFIG = {
"freqs": [1, 4],
}
class CompareFreqDecays2to4(CompareFreqDecays1to2):
CONFIG = {
"freqs": [2, 4],
}
class WorryAboutGenerality(TeacherStudentsScene, WriteHeatEquationTemplate):
def construct(self):
eq = self.get_d1_equation()
diffyq = self.get_diffyq_set()
is_in = TexMobject("\\in")
is_in.scale(2)
group = VGroup(eq, is_in, diffyq)
group.arrange(RIGHT, buff=MED_LARGE_BUFF)
group.to_edge(UP)
arrow = Vector(DOWN)
arrow.set_stroke(WHITE, 5)
arrow.next_to(eq, DOWN)
themes = TextMobject("Frequent themes")
themes.scale(1.5)
themes.next_to(arrow, DOWN)
self.play(
self.get_student_changes(
"sad", "tired", "pleading"
),
self.teacher.change, "raise_right_hand",
FadeInFromDown(eq)
)
self.play(Write(group[1:]))
self.wait(2)
self.play(
ShowCreation(arrow),
self.get_student_changes(*3 * ["pondering"]),
)
self.play(
FadeInFrom(themes, UP),
self.get_student_changes(*3 * ["thinking"]),
self.teacher.change, "happy"
)
self.wait(4)
# def get_d1_equation(self):
# result = super().get_d1_equation()
# lp, rp = parens = TexMobject("(", ")")
# parens.match_height(result)
# lp.next_to(result, LEFT, SMALL_BUFF)
# rp.next_to(result, RIGHT, SMALL_BUFF)
# result.add_to_back(lp)
# result.add(rp)
# return result
def get_diffyq_set(self):
words = TextMobject(
"Differential\\\\equations"
)
words.scale(1.5)
words.set_color(BLUE)
lb = Brace(words, LEFT)
rb = Brace(words, RIGHT)
return VGroup(lb, words, rb)

View File

@@ -0,0 +1,242 @@
from manimlib.imports import *
from active_projects.ode.part2.fourier_series import FourierOfTrebleClef
class ComplexFourierSeriesExample(FourierOfTrebleClef):
CONFIG = {
"file_name": "EighthNote",
"run_time": 10,
"n_vectors": 200,
"n_cycles": 2,
"max_circle_stroke_width": 0.75,
"drawing_height": 5,
"center_point": DOWN,
"top_row_y": 3,
"top_row_label_y": 2,
"top_row_x_spacing": 1.75,
"top_row_copy_scale_factor": 0.9,
"start_drawn": False,
}
def construct(self):
self.add_vectors_circles_path()
self.add_top_row(self.vectors, self.circles)
self.write_title()
self.highlight_vectors_one_by_one()
self.change_shape()
def write_title(self):
title = TextMobject("Complex\\\\Fourier series")
title.scale(1.5)
title.to_edge(LEFT)
title.match_y(self.path)
self.wait(5)
self.play(FadeInFromDown(title))
self.wait(2)
self.title = title
def highlight_vectors_one_by_one(self):
# Don't know why these vectors can't get copied.
# That seems like a problem that will come up again.
labels = self.top_row[-1]
next_anims = []
for vector, circle, label in zip(self.vectors, self.circles, labels):
# v_color = vector.get_color()
c_color = circle.get_color()
c_stroke_width = circle.get_stroke_width()
rect = SurroundingRectangle(label, color=PINK)
self.play(
# vector.set_color, PINK,
circle.set_stroke, RED, 3,
FadeIn(rect),
*next_anims
)
self.wait()
next_anims = [
# vector.set_color, v_color,
circle.set_stroke, c_color, c_stroke_width,
FadeOut(rect),
]
self.play(*next_anims)
def change_shape(self):
# path_mob = TexMobject("\\pi")
path_mob = SVGMobject("Nail_And_Gear")
new_path = path_mob.family_members_with_points()[0]
new_path.set_height(4)
new_path.move_to(self.path, DOWN)
new_path.shift(0.5 * UP)
new_coefs = self.get_coefficients_of_path(new_path)
new_vectors = self.get_rotating_vectors(
coefficients=new_coefs
)
new_drawn_path = self.get_drawn_path(new_vectors)
self.vector_clock.set_value(0)
self.vector_clock.suspend_updating(0)
vectors = self.vectors
anims = []
for vect, new_vect in zip(vectors, new_vectors):
new_vect.update()
new_vect.clear_updaters()
line = Line(stroke_width=0)
line.put_start_and_end_on(*vect.get_start_and_end())
anims.append(ApplyMethod(
line.put_start_and_end_on,
*new_vect.get_start_and_end()
))
vect.freq = new_vect.freq
vect.phase = new_vect.phase
vect.coefficient = new_vect.coefficient
vect.line = line
vect.add_updater(
lambda v: v.put_start_and_end_on(
*v.line.get_start_and_end()
)
)
anims += [
FadeOut(self.drawn_path)
]
self.play(*anims, run_time=3)
self.vector_clock.resume_updating()
for vect in self.vectors:
vect.remove_updater(vect.updaters[-1])
self.add(new_drawn_path)
for n in range(self.n_cycles):
self.run_one_cycle()
#
def get_path(self):
path = super().get_path()
path.set_height(self.drawing_height)
path.to_edge(DOWN)
return path
def add_top_row(self, vectors, circles, max_freq=3):
self.top_row = self.get_top_row(
vectors, circles, max_freq
)
self.add(self.top_row)
def get_top_row(self, vectors, circles, max_freq=3):
vector_copies = VGroup()
circle_copies = VGroup()
for vector, circle in zip(vectors, circles):
if vector.freq > max_freq:
break
vcopy = vector.copy()
vcopy.clear_updaters()
ccopy = circle.copy()
ccopy.clear_updaters()
ccopy.original = circle
vcopy.original = vector
vcopy.center_point = np.array([
vector.freq * self.top_row_x_spacing,
self.top_row_y,
0
])
ccopy.center_point = vcopy.center_point
vcopy.add_updater(self.update_top_row_vector_copy)
ccopy.add_updater(self.update_top_row_circle_copy)
vector_copies.add(vcopy)
circle_copies.add(ccopy)
dots = VGroup(*[
TexMobject("\\dots").next_to(
circle_copies, direction,
MED_LARGE_BUFF,
)
for direction in [LEFT, RIGHT]
])
labels = self.get_top_row_labels(vector_copies)
return VGroup(
vector_copies,
circle_copies,
dots,
labels,
)
def update_top_row_vector_copy(self, vcopy):
vcopy.become(vcopy.original)
vcopy.scale(self.top_row_copy_scale_factor)
vcopy.shift(vcopy.center_point - vcopy.get_start())
return vcopy
def update_top_row_circle_copy(self, ccopy):
ccopy.become(ccopy.original)
ccopy.scale(self.top_row_copy_scale_factor)
ccopy.move_to(ccopy.center_point)
return ccopy
def get_top_row_labels(self, vector_copies):
labels = VGroup()
for vector_copy in vector_copies:
freq = vector_copy.freq
label = Integer(freq)
label.move_to(np.array([
freq * self.top_row_x_spacing,
self.top_row_label_y,
0
]))
labels.add(label)
return labels
class ComplexFourierSeriesExampleEnd(ExternallyAnimatedScene):
pass
class FourierSeriesExampleWithRectForZoom(ComplexFourierSeriesExample):
CONFIG = {
"n_vectors": 100,
"slow_factor": 0.01,
"rect_scale_factor": 0.15,
"parametric_function_step_size": 0.0001,
"start_drawn": True,
}
def construct(self):
self.add_vectors_circles_path()
self.circles.set_stroke(opacity=0.5)
rect = self.get_rect()
rect.set_height(self.rect_scale_factor * FRAME_HEIGHT)
rect.add_updater(lambda m: m.move_to(
center_of_mass([
v.get_end()
for v in self.vectors
])
))
self.add(rect)
self.run_one_cycle()
def get_rect(self):
return ScreenRectangle(
color=WHITE,
stroke_width=2,
)
class ZoomedInFourierSeriesExample(FourierSeriesExampleWithRectForZoom, MovingCameraScene):
CONFIG = {
"vector_config": {
"max_tip_length_to_length_ratio": 0.15,
"tip_length": 0.05,
}
}
def setup(self):
ComplexFourierSeriesExample.setup(self)
MovingCameraScene.setup(self)
def get_rect(self):
return self.camera_frame

View File

@@ -0,0 +1,22 @@
from manimlib.imports import *
class WhyWouldYouCare(TeacherStudentsScene):
def construct(self):
self.student_says(
"Who cares!",
target_mode="sassy",
student_index=2,
added_anims=[self.teacher.change, "guilty"],
)
self.wait()
self.play(
RemovePiCreatureBubble(self.students[2]),
self.teacher.change, "raise_right_hand",
self.get_student_changes(
"pondering", "erm", "thinking",
look_at_arg=self.screen,
)
)
self.look_at(self.screen)
self.wait(5)

View File

@@ -0,0 +1,345 @@
from manimlib.imports import *
from active_projects.ode.part3.temperature_graphs import TemperatureGraphScene
from active_projects.ode.part2.wordy_scenes import WriteHeatEquationTemplate
class RelationToOtherVideos(Scene):
CONFIG = {
"camera_config": {
"background_color": DARK_GREY,
},
}
def construct(self):
# Show three videos
videos = self.get_video_thumbnails()
brace = Brace(videos, UP)
text = TextMobject("Heat equation")
text.scale(2)
text.next_to(brace, UP)
self.play(
LaggedStartMap(
FadeInFrom, videos,
lambda m: (m, LEFT),
lag_ratio=0.4,
run_time=2,
),
GrowFromCenter(brace),
FadeInFromDown(text),
)
self.wait()
group = Group(text, brace, videos)
# Show Fourier thinking
fourier = ImageMobject("Joseph Fourier")
fourier.set_height(4)
fourier.to_edge(RIGHT)
group.generate_target()
group.target.to_edge(DOWN)
fourier.align_to(group.target[0], DOWN)
bubble = ThoughtBubble(
direction=RIGHT,
width=3,
height=2,
fill_opacity=0.5,
stroke_color=WHITE,
)
bubble[-1].shift(0.25 * DOWN + 0.5 * LEFT)
bubble[:-1].rotate(20 * DEGREES)
for mob in bubble[:-1]:
mob.rotate(-20 * DEGREES)
bubble.move_tip_to(
fourier.get_corner(UL) + DOWN
)
bubble.to_edge(UP, buff=SMALL_BUFF)
self.play(
MoveToTarget(group),
FadeInFrom(fourier, LEFT)
)
self.play(Write(bubble, run_time=1))
self.wait()
# Discount first two
first_two = videos[:2]
first_two.generate_target()
first_two.target.scale(0.5)
first_two.target.to_corner(DL)
new_brace = Brace(first_two.target, UP)
self.play(
# fourier.scale, 0.8,
fourier.match_x, new_brace,
fourier.to_edge, UP,
MoveToTarget(first_two),
Transform(brace, new_brace),
text.scale, 0.7,
text.next_to, new_brace, UP,
FadeOutAndShift(bubble, LEFT),
)
self.play(
videos[2].scale, 1.7,
videos[2].to_corner, UR,
)
self.wait()
#
def get_video_thumbnails(self):
thumbnails = Group(
ImageMobject("diffyq_part2_thumbnail"),
ImageMobject("diffyq_part3_thumbnail"),
ImageMobject("diffyq_part4_thumbnail"),
)
for thumbnail in thumbnails:
thumbnail.set_height(4)
thumbnail.add(SurroundingRectangle(
thumbnail,
color=WHITE,
stroke_width=2,
buff=0
))
thumbnails.arrange(RIGHT, buff=LARGE_BUFF)
thumbnails.set_width(FRAME_WIDTH - 1)
return thumbnails
class ShowLinearity(WriteHeatEquationTemplate, TemperatureGraphScene):
CONFIG = {
"temp_text": "Temp",
"alpha": 0.1,
"axes_config": {
"z_max": 2,
"z_min": -2,
"z_axis_config": {
"tick_frequency": 0.5,
"unit_size": 1.5,
},
},
"default_surface_config": {
"resolution": (16, 16)
# "resolution": (4, 4)
},
"freqs": [2, 5],
}
def setup(self):
TemperatureGraphScene.setup(self)
WriteHeatEquationTemplate.setup(self)
def construct(self):
self.init_camera()
self.add_three_graphs()
self.show_words()
self.add_function_labels()
self.change_scalars()
def init_camera(self):
self.camera.set_distance(1000)
def add_three_graphs(self):
axes_group = self.get_axes_group()
axes0, axes1, axes2 = axes_group
freqs = self.freqs
scalar_trackers = Group(
ValueTracker(1),
ValueTracker(1),
)
graphs = VGroup(
self.get_graph(axes0, [freqs[0]], [scalar_trackers[0]]),
self.get_graph(axes1, [freqs[1]], [scalar_trackers[1]]),
self.get_graph(axes2, freqs, scalar_trackers),
)
plus = TexMobject("+").scale(2)
equals = TexMobject("=").scale(2)
plus.move_to(midpoint(
axes0.get_right(),
axes1.get_left(),
))
equals.move_to(midpoint(
axes1.get_right(),
axes2.get_left(),
))
self.add(axes_group)
self.add(graphs)
self.add(plus)
self.add(equals)
self.axes_group = axes_group
self.graphs = graphs
self.scalar_trackers = scalar_trackers
self.plus = plus
self.equals = equals
def show_words(self):
equation = self.get_d1_equation()
name = TextMobject("Heat equation")
name.next_to(equation, DOWN)
name.set_color_by_gradient(RED, YELLOW)
group = VGroup(equation, name)
group.to_edge(UP)
shift_val = 0.5 * RIGHT
arrow = Vector(1.5 * RIGHT)
arrow.move_to(group)
arrow.shift(shift_val)
linear_word = TextMobject("``Linear''")
linear_word.scale(2)
linear_word.next_to(arrow, RIGHT)
self.add(group)
self.wait()
self.play(
ShowCreation(arrow),
group.next_to, arrow, LEFT
)
self.play(FadeInFrom(linear_word, LEFT))
self.wait()
def add_function_labels(self):
axes_group = self.axes_group
graphs = self.graphs
solution_labels = VGroup()
for axes in axes_group:
label = TextMobject("Solution", "$\\checkmark$")
label.set_color_by_tex("checkmark", GREEN)
label.next_to(axes, DOWN)
solution_labels.add(label)
kw = {
"tex_to_color_map": {
"T_1": BLUE,
"T_2": GREEN,
}
}
T1 = TexMobject("a", "T_1", **kw)
T2 = TexMobject("b", "T_2", **kw)
T_sum = TexMobject("T_1", "+", "T_2", **kw)
T_sum_with_scalars = TexMobject(
"a", "T_1", "+", "b", "T_2", **kw
)
T1.next_to(graphs[0], UP)
T2.next_to(graphs[1], UP)
T_sum.next_to(graphs[2], UP)
T_sum.shift(SMALL_BUFF * DOWN)
T_sum_with_scalars.move_to(T_sum)
a_brace = Brace(T1[0], UP, buff=SMALL_BUFF)
b_brace = Brace(T2[0], UP, buff=SMALL_BUFF)
s1_decimal = DecimalNumber()
s1_decimal.match_color(T1[1])
s1_decimal.next_to(a_brace, UP, SMALL_BUFF)
s1_decimal.add_updater(lambda m: m.set_value(
self.scalar_trackers[0].get_value()
))
s2_decimal = DecimalNumber()
s2_decimal.match_color(T2[1])
s2_decimal.next_to(b_brace, UP, SMALL_BUFF)
s2_decimal.add_updater(lambda m: m.set_value(
self.scalar_trackers[1].get_value()
))
self.play(
FadeInFrom(T1[1], DOWN),
FadeInFrom(solution_labels[0], UP),
)
self.play(
FadeInFrom(T2[1], DOWN),
FadeInFrom(solution_labels[1], UP),
)
self.wait()
self.play(
TransformFromCopy(T1[1], T_sum[0]),
TransformFromCopy(T2[1], T_sum[2]),
TransformFromCopy(self.plus, T_sum[1]),
*[
Transform(
graph.copy().set_fill(opacity=0),
graphs[2].copy().set_fill(opacity=0),
remover=True
)
for graph in graphs[:2]
]
)
self.wait()
self.play(FadeInFrom(solution_labels[2], UP))
self.wait()
# Show constants
self.play(
FadeIn(T1[0]),
FadeIn(T2[0]),
FadeIn(a_brace),
FadeIn(b_brace),
FadeIn(s1_decimal),
FadeIn(s2_decimal),
FadeOut(T_sum),
FadeIn(T_sum_with_scalars),
)
def change_scalars(self):
s1, s2 = self.scalar_trackers
kw = {
"run_time": 2,
}
for graph in self.graphs:
graph.resume_updating()
self.play(s2.set_value, -0.5, **kw)
self.play(s1.set_value, -0.2, **kw)
self.play(s2.set_value, 1.5, **kw)
self.play(s1.set_value, 1.2)
self.play(s2.set_value, 0.3)
self.wait()
#
def get_axes_group(self):
axes_group = VGroup(*[
self.get_axes()
for x in range(3)
])
axes_group.arrange(RIGHT, buff=2)
axes_group.set_width(FRAME_WIDTH - 1)
axes_group.to_edge(DOWN, buff=1)
return axes_group
def get_axes(self):
axes = self.get_three_d_axes()
# axes.input_plane.set_fill(opacity=0)
# axes.input_plane.set_stroke(width=0.5)
# axes.add(axes.input_plane)
self.orient_three_d_mobject(axes)
axes.rotate(-5 * DEGREES, UP)
axes.set_width(4)
axes.x_axis.label.next_to(
axes.x_axis.get_end(), DOWN,
buff=2 * SMALL_BUFF
)
return axes
def get_graph(self, axes, freqs, scalar_trackers):
L = axes.x_max
a = self.alpha
def func(x, t):
scalars = [st.get_value() for st in scalar_trackers]
return np.sum([
s * np.cos(k * x) * np.exp(-a * (k**2) * t)
for freq, s in zip(freqs, scalars)
for k in [freq * PI / L]
])
def get_surface_graph_group():
return VGroup(
self.get_surface(axes, func),
self.get_time_slice_graph(axes, func, t=0),
)
result = always_redraw(get_surface_graph_group)
result.suspend_updating()
return result

View File

@@ -1,13 +0,0 @@
from active_projects.ode.part3.staging import *
from active_projects.ode.part3.temperature_graphs import *
OUTPUT_DIRECTORY = "ode/part3"
SCENES_IN_ORDER = [
FourierSeriesIllustraiton,
FourierNameIntro,
CircleAnimationOfF,
LastChapterWrapper,
ThreeMainObservations,
SimpleSinExpGraph,
]

View File

@@ -1,427 +0,0 @@
from manimlib.imports import *
from active_projects.ode.part2.fourier_series import FourierOfTrebleClef
class FourierNameIntro(Scene):
def construct(self):
self.show_two_titles()
self.transition_to_image()
self.show_paper()
def show_two_titles(self):
lt = TextMobject("Fourier", "Series")
rt = TextMobject("Fourier", "Transform")
lt_variants = VGroup(
TextMobject("Complex", "Fourier Series"),
TextMobject("Discrete", "Fourier Series"),
)
rt_variants = VGroup(
TextMobject("Discrete", "Fourier Transform"),
TextMobject("Fast", "Fourier Transform"),
TextMobject("Quantum", "Fourier Transform"),
)
titles = VGroup(lt, rt)
titles.scale(1.5)
for title, vect in (lt, LEFT), (rt, RIGHT):
title.move_to(vect * FRAME_WIDTH / 4)
title.to_edge(UP)
for title, variants in (lt, lt_variants), (rt, rt_variants):
title.save_state()
title.target = title.copy()
title.target.scale(1 / 1.5, about_edge=RIGHT)
for variant in variants:
variant.move_to(title.target, UR)
variant[0].set_color(YELLOW)
v_line = Line(UP, DOWN)
v_line.set_height(FRAME_HEIGHT)
v_line.set_stroke(WHITE, 2)
self.play(
FadeInFrom(lt, RIGHT),
ShowCreation(v_line)
)
self.play(
FadeInFrom(rt, LEFT),
)
# Edit in images of circle animations
# and clips from FT video
# for title, variants in (rt, rt_variants), (lt, lt_variants):
for title, variants in [(rt, rt_variants)]:
# Maybe do it for left variant, maybe not...
self.play(
MoveToTarget(title),
FadeInFrom(variants[0][0], LEFT)
)
for v1, v2 in zip(variants, variants[1:]):
self.play(
FadeOutAndShift(v1[0], UP),
FadeInFrom(v2[0], DOWN),
run_time=0.5,
)
self.wait(0.5)
self.play(
Restore(title),
FadeOut(variants[-1][0])
)
self.wait()
self.titles = titles
self.v_line = v_line
def transition_to_image(self):
titles = self.titles
v_line = self.v_line
image = ImageMobject("Joseph Fourier")
image.set_height(5)
image.to_edge(LEFT)
frame = Rectangle()
frame.replace(image, stretch=True)
name = TextMobject("Joseph", "Fourier")
fourier_part = name.get_part_by_tex("Fourier")
fourier_part.set_color(YELLOW)
F_sym = fourier_part[0]
name.match_width(image)
name.next_to(image, DOWN)
self.play(
ReplacementTransform(v_line, frame),
FadeIn(image),
FadeIn(name[0]),
*[
ReplacementTransform(
title[0].deepcopy(),
name[1]
)
for title in titles
],
titles.scale, 0.65,
titles.arrange, DOWN,
titles.next_to, image, UP,
)
self.wait()
big_F = F_sym.copy()
big_F.set_fill(opacity=0)
big_F.set_stroke(WHITE, 2)
big_F.set_height(3)
big_F.move_to(midpoint(
image.get_right(),
RIGHT_SIDE,
))
big_F.shift(DOWN)
equivalence = VGroup(
fourier_part.copy().scale(1.25),
TexMobject("\\Leftrightarrow").scale(1.5),
TextMobject("Break down into\\\\pure frequencies"),
)
equivalence.arrange(RIGHT)
equivalence.move_to(big_F)
equivalence.to_edge(UP)
self.play(
FadeIn(big_F),
TransformFromCopy(fourier_part, equivalence[0]),
Write(equivalence[1:]),
)
self.wait(3)
self.play(FadeOut(VGroup(big_F, equivalence)))
self.image = image
self.name = name
def show_paper(self):
image = self.image
paper = ImageMobject("Fourier paper")
paper.match_height(image)
paper.next_to(image, RIGHT, MED_LARGE_BUFF)
date = TexMobject("1822")
date.next_to(paper, DOWN)
date_rect = SurroundingRectangle(date)
date_rect.scale(0.3)
date_rect.set_color(RED)
date_rect.shift(1.37 * UP + 0.08 * LEFT)
date_arrow = Arrow(
date_rect.get_bottom(),
date.get_top(),
buff=SMALL_BUFF,
color=date_rect.get_color(),
)
heat_rect = SurroundingRectangle(
TextMobject("CHALEUR")
)
heat_rect.set_color(RED)
heat_rect.scale(0.6)
heat_rect.move_to(
paper.get_top() +
1.22 * DOWN + 0.37 * RIGHT
)
heat_word = TextMobject("Heat")
heat_word.scale(1.5)
heat_word.next_to(paper, UP)
heat_word.shift(paper.get_width() * RIGHT)
heat_arrow = Arrow(
heat_rect.get_top(),
heat_word.get_left(),
buff=0.1,
path_arc=-60 * DEGREES,
color=heat_rect.get_color(),
)
self.play(FadeInFrom(paper, LEFT))
self.play(
ShowCreation(date_rect),
)
self.play(
GrowFromPoint(date, date_arrow.get_start()),
ShowCreation(date_arrow),
)
self.wait(3)
# Insert animation of circles/sine waves
# approximating a square wave
self.play(
ShowCreation(heat_rect),
)
self.play(
GrowFromPoint(heat_word, heat_arrow.get_start()),
ShowCreation(heat_arrow),
)
self.wait(3)
class FourierSeriesIllustraiton(Scene):
CONFIG = {
"n_range": range(1, 31, 2),
}
def construct(self):
n_range = self.n_range
axes1 = Axes(
number_line_config={
"include_tip": False,
},
x_axis_config={
"tick_frequency": 1 / 4,
"unit_size": 4,
},
x_min=0,
x_max=1,
y_min=-1,
y_max=1,
)
axes2 = axes1.copy()
step_func = axes2.get_graph(
lambda x: (1 if x < 0.5 else -1),
discontinuities=[0.5],
color=YELLOW,
stroke_width=3,
)
dot = Dot(axes2.c2p(0.5, 0), color=step_func.get_color())
dot.scale(0.5)
step_func.add(dot)
axes2.add(step_func)
arrow = Arrow(LEFT, RIGHT, color=WHITE)
VGroup(axes1, arrow, axes2).arrange(RIGHT).shift(UP)
def generate_nth_func(n):
return lambda x: (4 / n / PI) * np.sin(TAU * n * x)
def generate_kth_partial_sum_func(k):
return lambda x: np.sum([
generate_nth_func(n)(x)
for n in n_range[:k]
])
sine_graphs = VGroup(*[
axes1.get_graph(generate_nth_func(n))
for n in n_range
])
sine_graphs.set_stroke(width=3)
sine_graphs.set_color_by_gradient(
BLUE, GREEN, RED, YELLOW, PINK,
BLUE, GREEN, RED, YELLOW, PINK,
)
partial_sums = VGroup(*[
axes1.get_graph(generate_kth_partial_sum_func(k + 1))
for k in range(len(n_range))
])
partial_sums.match_style(sine_graphs)
sum_tex = TexMobject(
"\\frac{4}{\\pi}"
"\\sum_{1, 3, 5, \\dots}"
"\\frac{1}{n} \\sin(2\\pi \\cdot n \\cdot x)"
)
sum_tex.next_to(partial_sums, DOWN, buff=0.7)
eq = TexMobject("=")
step_tex = TexMobject(
"""
1 \\quad \\text{if $x < 0.5$} \\\\
0 \\quad \\text{if $x = 0.5$} \\\\
-1 \\quad \\text{if $x > 0.5$} \\\\
"""
)
lb = Brace(step_tex, LEFT, buff=SMALL_BUFF)
step_tex.add(lb)
step_tex.next_to(axes2, DOWN, buff=MED_LARGE_BUFF)
eq.move_to(midpoint(
step_tex.get_left(),
sum_tex.get_right()
))
rects = it.chain(
[
SurroundingRectangle(sum_tex[0][i])
for i in [4, 6, 8]
],
it.cycle([None])
)
self.add(axes1, arrow, axes2)
self.add(step_func)
self.add(sum_tex, eq, step_tex)
curr_partial_sum = axes1.get_graph(lambda x: 0)
curr_partial_sum.set_stroke(width=1)
for sine_graph, partial_sum, rect in zip(sine_graphs, partial_sums, rects):
anims1 = [
ShowCreation(sine_graph)
]
partial_sum.set_stroke(BLACK, 4, background=True)
anims2 = [
curr_partial_sum.set_stroke,
{"width": 1, "opacity": 0.5},
curr_partial_sum.set_stroke,
{"width": 0, "background": True},
ReplacementTransform(
sine_graph, partial_sum,
remover=True
),
]
if rect:
rect.match_style(sine_graph)
anims1.append(ShowCreation(rect))
anims2.append(FadeOut(rect))
self.play(*anims1)
self.play(*anims2)
curr_partial_sum = partial_sum
class CircleAnimationOfF(FourierOfTrebleClef):
CONFIG = {
"height": 3,
"n_circles": 200,
"run_time": 10,
"arrow_config": {
"tip_length": 0.1,
"stroke_width": 2,
}
}
def get_shape(self):
path = VMobject()
shape = TexMobject("F")
for sp in shape.family_members_with_points():
path.append_points(sp.points)
return path
class LastChapterWrapper(Scene):
def construct(self):
full_rect = FullScreenFadeRectangle(
fill_color=DARK_GREY,
fill_opacity=1,
)
rect = ScreenRectangle(height=6)
rect.set_stroke(WHITE, 2)
rect.set_fill(BLACK, 1)
title = TextMobject("Last chapter")
title.scale(2)
title.to_edge(UP)
rect.next_to(title, DOWN)
self.add(full_rect)
self.play(
FadeIn(rect),
Write(title, run_time=2),
)
self.wait()
class ThreeMainObservations(Scene):
def construct(self):
fourier = ImageMobject("Joseph Fourier")
fourier.set_height(5)
fourier.to_corner(DR)
fourier.shift(LEFT)
bubble = ThoughtBubble(
direction=RIGHT,
height=3,
width=4,
)
bubble.move_tip_to(fourier.get_corner(UL) + 0.5 * DR)
observations = VGroup(
TextMobject(
"1)",
# "Sine waves",
# "H",
# "Heat equation",
),
TextMobject(
"2)",
# "Linearity"
),
TextMobject(
"3)",
# "Any$^{*}$ function is\\\\",
# "a sum of sine waves",
),
)
# heart = SuitSymbol("hearts")
# heart.replace(observations[0][2])
# observations[0][2].become(heart)
# observations[0][1].add(happiness)
# observations[2][2].align_to(
# observations[2][1], LEFT,
# )
observations.arrange(
DOWN,
aligned_edge=LEFT,
buff=LARGE_BUFF,
)
observations.set_height(FRAME_HEIGHT - 2)
observations.to_corner(UL, buff=LARGE_BUFF)
self.add(fourier)
self.play(ShowCreation(bubble))
self.wait()
self.play(LaggedStart(*[
TransformFromCopy(bubble, observation)
for observation in observations
], lag_ratio=0.2))
self.play(
FadeOut(fourier),
FadeOut(bubble),
)
self.wait()
class NewSceneName(Scene):
def construct(self):
pass

View File

@@ -1,261 +0,0 @@
from manimlib.imports import *
class TemperatureGraphScene(SpecialThreeDScene):
CONFIG = {
"axes_config": {
"x_min": 0,
"x_max": TAU,
"y_min": 0,
"y_max": 10,
"z_min": -3,
"z_max": 3,
"x_axis_config": {
"tick_frequency": TAU / 8,
"include_tip": False,
},
"num_axis_pieces": 1,
},
"default_graph_style": {
"stroke_width": 2,
"stroke_color": WHITE,
"background_image_file": "VerticalTempGradient",
},
"default_surface_style": {
"fill_opacity": 0.1,
"checkerboard_colors": [LIGHT_GREY],
"stroke_width": 0.5,
"stroke_color": WHITE,
"stroke_opacity": 0.5,
},
}
def get_three_d_axes(self, include_labels=True):
axes = ThreeDAxes(**self.axes_config)
axes.set_stroke(width=2)
# Add number labels
# TODO?
# Add axis labels
if include_labels:
x_label = TexMobject("x")
x_label.next_to(axes.x_axis.get_right(), DOWN)
axes.x_axis.add(x_label)
t_label = TextMobject("Time")
t_label.rotate(90 * DEGREES, OUT)
t_label.next_to(axes.y_axis.get_top(), DL)
axes.y_axis.add(t_label)
temp_label = TextMobject("Temperature")
temp_label.rotate(90 * DEGREES, RIGHT)
temp_label.next_to(axes.z_axis.get_zenith(), RIGHT)
axes.z_axis.add(temp_label)
# Adjust axis orinetations
axes.x_axis.rotate(
90 * DEGREES, RIGHT,
about_point=axes.c2p(0, 0, 0),
)
axes.y_axis.rotate(
90 * DEGREES, UP,
about_point=axes.c2p(0, 0, 0),
)
# Add xy-plane
surface_config = {
"u_min": 0,
"u_max": axes.x_max,
"v_min": 0,
"v_max": axes.y_max,
"resolution": (16, 10),
}
axes.surface_config = surface_config
input_plane = ParametricSurface(
lambda x, t: axes.c2p(x, t, 0),
# lambda x, t: np.array([x, t, 0]),
**surface_config,
)
input_plane.set_style(
fill_opacity=0.5,
fill_color=BLUE_B,
stroke_width=0.5,
stroke_color=WHITE,
)
axes.input_plane = input_plane
return axes
def get_initial_state_graph(self, axes, func, **kwargs):
config = dict()
config.update(self.default_graph_style)
config.update(kwargs)
return ParametricFunction(
lambda x: axes.c2p(
x, 0, func(x)
),
t_min=axes.x_min,
t_max=axes.x_max,
**config,
)
def get_surface(self, axes, func, **kwargs):
config = dict()
config.update(axes.surface_config)
config.update(self.default_surface_style)
config.update(kwargs)
return ParametricSurface(
lambda x, t: axes.c2p(
x, t, func(x, t)
),
**config
)
def orient_three_d_mobject(self, mobject,
phi=85 * DEGREES,
theta=-80 * DEGREES):
mobject.rotate(-90 * DEGREES - theta, OUT)
mobject.rotate(phi, LEFT)
return mobject
class SimpleSinExpGraph(TemperatureGraphScene):
def construct(self):
axes = self.get_three_d_axes()
sine_graph = self.get_sine_graph(axes)
sine_exp_surface = self.get_sine_exp_surface(axes)
self.set_camera_orientation(
phi=80 * DEGREES,
theta=-80 * DEGREES,
)
self.camera.frame_center.shift(3 * RIGHT)
self.begin_ambient_camera_rotation(rate=0.01)
self.add(axes)
self.play(ShowCreation(sine_graph))
self.play(UpdateFromAlphaFunc(
sine_exp_surface,
lambda m, a: m.become(
self.get_sine_exp_surface(axes, v_max=a * 10)
),
run_time=3
))
self.wait(20)
#
def sin_exp(self, x, t, A=2, omega=1, k=0.25):
return A * np.sin(omega * x) * np.exp(-k * (omega**2) * t)
def get_sine_graph(self, axes, **config):
return self.get_initial_state_graph(
axes,
lambda x: self.sin_exp(x, 0),
**config
)
def get_sine_exp_surface(self, axes, **config):
return self.get_surface(
axes,
lambda x, t: self.sin_exp(x, t),
**config
)
class AddMultipleSolutions(SimpleSinExpGraph):
CONFIG = {
"axes_config": {
"x_axis_config": {
"unit_size": 0.7,
},
}
}
def construct(self):
axes1, axes2, axes3 = all_axes = VGroup(*[
self.get_three_d_axes(
include_labels=False,
)
for x in range(3)
])
all_axes.scale(0.5)
self.orient_three_d_mobject(all_axes)
As = [1.5, 1.5]
omegas = [1, 2]
ks = [0.25, 0.01]
quads = [
(axes1, [As[0]], [omegas[0]], [ks[0]]),
(axes2, [As[1]], [omegas[1]], [ks[1]]),
(axes3, As, omegas, ks),
]
for axes, As, omegas, ks in quads:
graph = self.get_initial_state_graph(
axes,
lambda x: np.sum([
self.sin_exp(x, 0, A, omega, k)
for A, omega, k in zip(As, omegas, ks)
])
)
surface = self.get_surface(
axes,
lambda x, t: np.sum([
self.sin_exp(x, t, A, omega)
for A, omega in zip(As, omegas)
])
)
surface.sort(lambda p: -p[2])
axes.add(surface, graph)
axes.graph = graph
axes.surface = surface
self.set_camera_orientation(distance=100)
plus = TexMobject("+").scale(2)
equals = TexMobject("=").scale(2)
group = VGroup(
axes1, plus, axes2, equals, axes3,
)
group.arrange(RIGHT, buff=SMALL_BUFF)
for axes in all_axes:
checkmark = TexMobject("\\checkmark")
checkmark.set_color(GREEN)
checkmark.scale(2)
checkmark.next_to(axes, UP)
checkmark.shift(0.7 * DOWN)
axes.checkmark = checkmark
self.add(axes1, axes2)
self.play(
LaggedStart(
Write(axes1.surface),
Write(axes2.surface),
),
LaggedStart(
FadeInFrom(axes1.checkmark, DOWN),
FadeInFrom(axes2.checkmark, DOWN),
),
lag_ratio=0.2,
run_time=1,
)
self.wait()
self.play(Write(plus))
self.play(
Transform(
axes1.copy().set_fill(opacity=0),
axes3
),
Transform(
axes2.copy().set_fill(opacity=0),
axes3
),
FadeInFrom(equals, LEFT)
)
self.play(
FadeInFrom(axes3.checkmark, DOWN),
)
self.wait()

View File

@@ -6,13 +6,11 @@ services:
image: eulertour/manim:latest
# uncomment this line to build rather than pull the image
# build: .
volumes:
- ${MANIM_PATH:?MANIM_PATH environment variable isn't set}:/opt/manim
environment:
- PYTHONPATH=/opt/manim
working_dir: /opt/manim
entrypoint:
- python
- -m
- manim
- --media_dir=/tmp/output
volumes:
- ${INPUT_PATH:?INPUT_PATH environment variable isn't set}:/tmp/input
- ${OUTPUT_PATH:?OUTPUT_PATH environment variable isn't set}:/tmp/output
working_dir: /tmp/input
network_mode: "none"

210
docs/source/animation.rst Normal file
View File

@@ -0,0 +1,210 @@
Animation
=========
The simplest of which is ``Scene.add``. The object appears on the first frame
without any animation::
class NoAnimation(Scene):
def construct(self):
square = Square()
self.add(square))
Animation are used in conjunction with ``scene.Play``
Fade
----
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationFadeIn.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationFadeIn(Scene):
def construct(self):
square = Square()
anno = TextMobject("Fade In")
anno.shift(2 * DOWN)
self.add(anno)
self.play(FadeIn(square))
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationFadeOut.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationFadeOut(Scene):
def construct(self):
square = Square()
anno = TextMobject("Fade Out")
anno.shift(2 * DOWN)
self.add(anno)
self.add(square)
self.play(FadeOut(square))
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationFadeInFrom.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationFadeInFrom(Scene):
def construct(self):
square = Square()
for label, edge in zip(
["LEFT", "RIGHT", "UP", "DOWN"], [LEFT, RIGHT, UP, DOWN]
):
anno = TextMobject(f"Fade In from {label}")
anno.shift(2 * DOWN)
self.add(anno)
self.play(FadeInFrom(square, edge))
self.remove(anno, square)
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationFadeOutAndShift.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationFadeOutAndShift(Scene):
def construct(self):
square = Square()
for label, edge in zip(
["LEFT", "RIGHT", "UP", "DOWN"], [LEFT, RIGHT, UP, DOWN]
):
anno = TextMobject(f"Fade Out and shift {label}")
anno.shift(2 * DOWN)
self.add(anno)
self.play(FadeOutAndShift(square, edge))
self.remove(anno, square)
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationFadeInFromLarge.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationFadeInFromLarge(Scene):
def construct(self):
square = Square()
for factor in [0.1, 0.5, 0.8, 1, 2, 5]:
anno = TextMobject(f"Fade In from large scale\_factor={factor}")
anno.shift(2 * DOWN)
self.add(anno)
self.play(FadeInFromLarge(square, scale_factor=factor))
self.remove(anno, square)
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationFadeInFromPoint.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationFadeInFromPoint(Scene):
def construct(self):
square = Square()
for i in range(-6, 7, 2):
anno = TextMobject(f"Fade In from point {i}")
anno.shift(2 * DOWN)
self.add(anno)
self.play(FadeInFromPoint(square, point=i))
self.remove(anno, square)
Grow
----
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationGrowFromEdge.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationGrowFromEdge(Scene):
def construct(self):
for label, edge in zip(
["LEFT", "RIGHT", "UP", "DOWN"], [LEFT, RIGHT, UP, DOWN]
):
anno = TextMobject(f"Grow from {label} edge")
anno.shift(2 * DOWN)
self.add(anno)
square = Square()
self.play(GrowFromEdge(square, edge))
self.remove(anno, square)
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationGrowFromCenter.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationGrowFromCenter(Scene):
def construct(self):
square = Square()
anno = TextMobject("Grow from center")
anno.shift(2 * DOWN)
self.add(anno)
self.play(GrowFromCenter(square))
Diagonal Directions
-------------------
You can combine cardinal directions to form diagonal animations
.. raw:: html
<video width="560" height="315" controls>
<source src="_static/AnimationFadeInFromDiagonal.mp4" type="video/mp4">
</video>
.. code-block:: python
class AnimationFadeInFromDiagonal(Scene):
def construct(self):
square = Square()
for diag in [UP + LEFT, UP + RIGHT, DOWN + LEFT, DOWN + RIGHT]:
self.play(FadeInFrom(square, diag))
.. note::
You can also use the abbreviated forms like ``UL, UR, DL, DR``.
See :ref:`ref-directions`.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

View File

@@ -23,6 +23,7 @@ author = 'EulerTour'
# -- General configuration ---------------------------------------------------
master_doc = 'index'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@@ -49,4 +50,4 @@ html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ['assets']

94
docs/source/constants.rst Normal file
View File

@@ -0,0 +1,94 @@
Manim Constants
===============
The ``constants.py`` under ``manimlib/`` contains variables that are used
during setup and running manim. Some variables are not documented here as they are
only used internally by manim.
Directories
-----------
MEDIA_DIR
The directory where ``VIDEO_DIR`` and ``TEX_DIR`` will be created,
if they aren't specified via flags.
VIDEO_DIR
Used to store the scenes rendered by Manim. When a scene is
finished rendering, it will be stored under
``VIDEO_DIR/module_name/scene_name/quality/scene_name.mp4``.
Created under ``MEDIA_DIR`` by default.
TEX_DIR
Files written by Latex are stored here. It also acts as a cache
so that the files aren't rewritten each Latex is needed.
Those directories are created if they don't exist.
Tex
---
TEX_USE_CTEX
A boolean value. Change it to True if you need to use Chinese typesetting.
TEX_TEXT_TO_REPLACE
Placeholder text used by manim when generating tex files
TEMPLATE_TEX_FILE
By default ``manimlib/tex_template.tex`` is used. If ``TEX_USE_CTEX``
is set to True then ``manimlib/ctex_template.tex`` is used.
Numerical Constants
-------------------
PI
alias to ``numpy.pi``
TAU
PI * 2
DEGREES
TAU / 360
Camera Configuration
--------------------
Render setting presets
PRODUCTION_QUALITY_CAMERA_CONFIG
2560x1440 @ 60fps # This is the default when rendering a scene
HIGH_QUALITY_CAMERA_CONFIG
1920x1080 @ 60fps. # Used when the ``-h`` or ``--high_quality`` flag
is passed.
MEDIUM_QUALITY_CAMERA_CONFIG
1280x720 @ 30fps. # Used when the ``-m`` or ``--medium_quality``
flag is passed.
LOW_QUALITY_CAMERA_CONFIG
854x480 @ 15fps. # Used when the ``-l`` or ``--low_quality`` flag is
passed.
.. _ref-directions:
Coordinates
-----------
Used for 2d/3d animations and placements::
ORIGIN
UP
DOWN
RIGHT
LEFT
IN # 3d camera only, away from camera
OUT # 3d camera only, close to camera
UL = UP + LEFT # diagonal abbreviations. You can use either one
UR = UP + RIGHT
DL = DOWN + LEFT
DR = DOWN + RIGHT
TOP
BOTTOM
LEFT_SIDE
RIGHT_SIDE``
Colors
------
COLOR_MAP
A predefined color maps
PALETTE
A list of color hex strings, derived from COLOR_MAP

178
docs/source/coordinate.rst Normal file
View File

@@ -0,0 +1,178 @@
Coordinate
==========
By default, the scene in manim is made up by 8 x 14 grid. The grid is addressed using a numpy
array in the form of [x, y, z]. For 2D animations only the x and y axes are used.
.. code-block:: python
class DotMap(Scene):
def construct(self):
dots = dict()
annos = dict()
var_index = 0
for x in range(-7, 8):
for y in range(-4, 5):
annos[f"{x}{y}"] = TexMobject(f"({x}, {y})")
dots[f"{var_index}"] = Dot(np.array([x, y, 0]))
var_index = var_index + 1
for anno, dot in zip(annos.values(), dots.values()):
self.add(anno)
self.add(dot)
self.wait(0.2)
self.remove(anno)
.. raw:: html
<video width="700" height="394" controls>
<source src="_static/coordinate/DotMap.mp4" type="video/mp4">
</video>
.. note::
You can place objects outside this boundary, but it won't show up in the render.
Using Coordinates
-----------------
Coordinates are used for creating geometries (`VMobject` in manim) and animations.
Here coordinates are used to create this Polygon
.. code-block:: python
class CoorPolygon(Scene):
def construct(self):
for x in range(-7, 8):
for y in range(-4, 5):
self.add(Dot(np.array([x, y, 0]), color=DARK_GREY))
polygon = Polygon(
np.array([3, 2, 0]),
np.array([1, -1, 0]),
np.array([-5, -4, 0]),
np.array([-4, 4, 0]))
self.add(polygon)
.. Image:: assets/coordinate/CoorPolygon.png
:width: 700px
Coordinate Aliasing
-------------------
From some animations typing a ``np.array`` everytime you need a coordinate can be tedious.
Manim provides aliases to the most common coordinates::
UP == np.array([0, 1, 0])
DOWN == np.array([0, -1, 0])
LEFT == np.array([-1, 0, 0])
RIGHT == np.array([1, 0, 0])
UL == np.array([-1, 1, 0])
DL == np.array([-1, -1, 0])
UR == np.array([1, 1, 0])
DR == np.array([1, -1, 0])
Here coordinates are used for animations
.. code-block:: python
class CoorAlias(Scene):
def construct(self):
for x in range(-7, 8):
for y in range(-4, 5):
self.add(Dot(np.array([x, y, 0]), color=DARK_GREY))
aliases = {
"UP": UP,
"np.array([0,1,0])": np.array([0, 1, 0]),
"DOWN": DOWN,
"np.array([0,-1,0])": np.array([0, -1, 0]),
"LEFT": LEFT,
"np.array([-1,0,0])": np.array([-1, 0, 0]),
"RIGHT": RIGHT,
"np.array([1,0,0])": np.array([1, 0, 0]),
"UL": UL,
"np.array([-1,1,0])": np.array([-1, 1, 0]),
"DL": DL,
"np.array([-1,-1,0])": np.array([-1, -1, 0]),
"UR": UR,
"np.array([1,1,0])": np.array([1, 1, 0]),
"DR": DR,
"np.array([1,-1,0])": np.array([1, -1, 0])}
circle = Circle(color=RED, radius=0.5)
self.add(circle)
self.wait(0.5)
for text, aliase in aliases.items():
anno = TexMobject(f"\\texttt{{{text}}}")
self.play(Write(anno, run_time=0.2))
self.play(ApplyMethod(circle.shift, aliase))
self.wait(0.2)
self.play(FadeOut(anno, run_time=0.2))
.. raw:: html
<video width="700" height="394" controls>
<source src="_static/coordinate/CoorAlias.mp4" type="video/mp4">
</video>
Coordinate Arithmetic
---------------------
Numpy array allows arithmetic operations::
>>> numpy.array([2,2,0]) + 4
array([6, 6, 4])
>>> np.array([1, -3, 0]) + np.array([-4, 2, 0])
array([-3, -1, 0])
>>> np.array([2, 2, 0]) - np.array([3,6, 0])
array([-1, -4, 0])
>>> numpy.array([2,2,0]) - 3
array([-1, -1, -3])
>>> np.array([1, -3, 0]) * 3
array([ 3, -9, 0])
>>> numpy.array([2,2,0]) / 2
array([1., 1., 0.])
>>> numpy.array([2,2,0]) / numpy.array([1, 4, 0])
__main__:1: RuntimeWarning: invalid value encountered in true_divide
array([2. , 0.5, nan])
.. code-block:: python
class CoorArithmetic(Scene):
def construct(self):
for x in range(-7, 8):
for y in range(-4, 5):
self.add(Dot(np.array([x, y, 0]), color=DARK_GREY))
circle = Circle(color=RED, radius=0.5)
self.add(circle)
self.wait(0.5)
aliases = {
"LEFT * 3": LEFT * 3,
"UP + RIGHT / 2": UP + RIGHT / 2,
"DOWN + LEFT * 2": DOWN + LEFT * 2,
"RIGHT * 3.75 * DOWN": RIGHT * 3.75 * DOWN,
# certain arithmetic won't work as you expected
# In [4]: RIGHT * 3.75 * DOWN
# Out[4]: array([ 0., -0., 0.])
"RIGHT * 3.75 + DOWN": RIGHT * 3.75 + DOWN}
for text, aliase in aliases.items():
anno = TexMobject(f"\\texttt{{{text}}}")
self.play(Write(anno, run_time=0.2))
self.play(ApplyMethod(circle.shift, aliase))
self.wait(0.2)
self.play(FadeOut(anno, run_time=0.2))
.. raw:: html
<video width="700" height="394" controls>
<source src="_static/coordinate/CoorArithmetic.mp4" type="video/mp4">
</video>

View File

@@ -2,14 +2,14 @@ Getting Started
===============
Todd Zimmerman put together `a very nice tutorial`_ on getting started with
``manim``, but is unfortunately outdated. It's still useful for understanding
how ``manim`` is used, but the examples won't run on the latest version of
``manim``.
``manim``, which has been updated to run on python 3.7. Note that you'll want
to change `from big_ol_pile_of_manim_imports import *` to `from
manimlib.imports import *` to work with the current codebase.
.. _a very nice tutorial: https://talkingphysics.wordpress.com/2018/06/11/learning-how-to-animate-videos-using-``manim``-series-a-journey/
.. _a very nice tutorial: https://talkingphysics.wordpress.com/2019/01/08/getting-started-animating-with-manim-and-python-3-7/
.. toctree::
:caption: Contents:
:caption: Contents
:maxdepth: 2
learning_by_example

View File

@@ -1,13 +1,19 @@
Learning by Example
===================
You create videos in manim by writing :class:`~scene.scene.Scene` instances.
``example_scenes.py`` contains a few simple ones that we can use to learn about
manim. For instance, take ``SquareToCircle``.
SquareToCircle
--------------
``example_scenes.py`` contains simple examples that we can use to learn about manim.
Go ahead and try out the ``SquareToCircle`` scene by running it with ``$ manim example_scenes.py SquareToCircle -p``
in manim directory.
.. code-block:: python
:linenos:
from manimlib.imports import *
class SquareToCircle(Scene):
def construct(self):
circle = Circle()
@@ -20,113 +26,106 @@ manim. For instance, take ``SquareToCircle``.
self.play(Transform(square, circle))
self.play(FadeOut(square))
:meth:`~scene.scene.Scene.construct` specifies what is displayed on the screen
when the :class:`~scene.scene.Scene` is rendered to video. You can render a
:class:`~scene.scene.Scene` by running ``extract_scene.py``. Run ``python
extract_scene.py -h`` to see how it's used.
.. code-block:: none
> python extract_scene.py -h
usage: extract_scene.py [-h] [-p] [-w] [-s] [-l] [-m] [-g] [-f] [-t] [-q] [-a]
[-o OUTPUT_NAME] [-n START_AT_ANIMATION_NUMBER]
[-r RESOLUTION] [-c COLOR] [-d OUTPUT_DIRECTORY]
file [scene_name]
positional arguments:
file path to file holding the python code for the scene
scene_name Name of the Scene class you want to see
optional arguments:
-h, --help show this help message and exit
-p, --preview
-w, --write_to_movie
-s, --show_last_frame
-l, --low_quality
-m, --medium_quality
-g, --save_pngs
-f, --show_file_in_finder
-t, --transparent
-q, --quiet
-a, --write_all
-o OUTPUT_NAME, --output_name OUTPUT_NAME
-n START_AT_ANIMATION_NUMBER, --start_at_animation_number START_AT_ANIMATION_NUMBER
-r RESOLUTION, --resolution RESOLUTION
-c COLOR, --color COLOR
-d OUTPUT_DIRECTORY, --output_directory OUTPUT_DIRECTORY
The most common flags are ``-p``, to automatically play the generated video,
``-l``, to render in lower quality in favor of speed, and ``-s``, to show the
last frame of the :class:`~scene.scene.Scene` for faster development. Run
``python extract_scene.py example_scenes.py SquareToCircle -pl`` to produce a
file called SquareToCircle.mp4 in the media directory that you have configured,
and automatically play it.
.. raw:: html
<iframe width="560" height="315" src="https://www.youtube.com/embed/8tvYDIGLJJA?ecver=1" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<video width="560" height="315" controls>
<source src="../_static/SquareToCircle.mp4" type="video/mp4">
</video>
Let's step through each line of the :class:`~scene.scene.Scene`. Lines 3 and 4
instantiate a :class:`~mobject.geometry.Circle` and
:class:`~mobject.geometry.Square`, respectively. Both of these subclass
:class:`~mobject.mobject.Mobject`, the base class for objects in manim. Note
.. note::
The flag ``-p`` plays the rendered video with default video player.
Other frequently used flags are:
* ``-l`` for rendering video in lower resolution (which renders faster)
* ``-s`` to show the last frame of the video.
Run ``manim -h`` all the available flags (``python -m manim -h`` if you installed it to a venv)
Let's step through each line of ``SquareToCircle``
.. code-block:: python
:lineno-start: 3
class SquareToCircle(Scene):
You create videos in manim by writing :class:`~scene.scene.Scene` classes.
Each :class:`~scene.scene.Scene` in manim is self-contained. That means everything
you created under this scene does not exist outside the class.
.. code-block:: python
:lineno-start: 4
def construct(self):
:meth:`~scene.scene.Scene.construct` specifies what is displayed on the screen
when the :class:`~scene.scene.Scene` is rendered to video.
.. code-block:: python
:lineno-start: 5
circle = Circle()
square = Square()
``Circle()`` and ``Square()`` create :class:`~mobject.geometry.Circle` and :class:`~mobject.geometry.Square`.
Both of these are instances of :class:`~mobject.mobject.Mobject` subclasses, the base class for objects in manim. Note
that instantiating a :class:`~mobject.mobject.Mobject` does not add it to the
:class:`~scene.scene.Scene`, so you wouldn't see anything if you were to render
the :class:`~scene.scene.Scene` at this point.
.. code-block:: python
:linenos:
:lineno-start: 3
circle = Circle()
square = Square()
Lines 5, 6, and 7 apply various modifications to the mobjects before animating
them. The call to :meth:`~mobject.mobject.Mobject.flip` on line 5 flips the
:class:`~mobject.geometry.Square` across the RIGHT vector. This is equivalent
to a refection across the x-axis. Then the call to
:meth:`~mobject.mobject.Mobject.rotate` on line 6 rotates the
:class:`~mobject.geometry.Square` 3/8ths of a full rotation counterclockwise.
Finally, the call to :meth:`~mobject.mobject.Mobject.set_fill` on line 7 sets
the fill color for the :class:`~mobject.geometry.Circle` to pink, and its
opacity to 0.5.
.. code-block:: python
:linenos:
:lineno-start: 5
:lineno-start: 7
square.flip(RIGHT)
square.rotate(-3 * TAU / 8)
circle.set_fill(PINK, opacity=0.5)
Line 9 is the first to generate video.
:class:`~animation.creation.ShowCreation`,
:class:`~animation.transform.Transform`, and
:class:`~animation.creation.FadeOut` are
:class:`~animation.animation.Animation` instances. Each
:class:`~animation.animation.Animation` takes one or more
:class:`~mobject.mobject.Mobject` instances as arguments, which it animates
when passed to :meth:`~scene.scene.Scene.play`. This is how video is typically
created in manim. :class:`~mobject.mobject.Mobject` instances are automatically
added to the :class:`~scene.scene.Scene` when they are animated. You can add a
:class:`~mobject.mobject.Mobject` to the :class:`~scene.scene.Scene` manually
by passing it as an argument to :meth:`~scene.scene.Scene.add`.
``flip()`` ``rotate()`` ``set_fill()`` apply various modifications to the mobjects before animating
them. The call to :meth:`~mobject.mobject.Mobject.flip` flips the
:class:`~mobject.geometry.Square` across the RIGHT vector. This is equivalent
to a refection across the x-axis.
The call to :meth:`~mobject.mobject.Mobject.rotate` rotates the
:class:`~mobject.geometry.Square` 3/8ths of a full rotation counterclockwise.
The call to :meth:`~mobject.mobject.Mobject.set_fill` sets
the fill color for the :class:`~mobject.geometry.Circle` to pink, and its opacity to 0.5.
.. code-block:: python
:linenos:
:lineno-start: 9
:lineno-start: 11
self.play(ShowCreation(square))
self.play(Transform(square, circle))
self.play(FadeOut(square))
:class:`~animation.creation.ShowCreation` draws a
:class:`~mobject.mobject.Mobject` to the screen,
:class:`~animation.transform.Transform` morphs one
:class:`~mobject.mobject.Mobject` into another, and
:class:`~animation.creation.FadeOut` fades a
:class:`~mobject.mobject.Mobject` out of the :class:`~scene.scene.Scene`. Note
that only the first argument to :class:`~animation.transform.Transform` is
modified, and the second is not added to the :class:`~scene.scene.Scene`. After
line 10 is executed ``square`` is a :class:`~mobject.geometry.Square` instance
with the shape of a :class:`~mobject.geometry.Circle`.
To generated animation, :class:`~animation.animation.Animation` classes are used.
Each :class:`~animation.animation.Animation` takes one or more :class:`~mobject.mobject.Mobject` instances as arguments, which it animates
when passed to :meth:`~scene.scene.Scene.play`. This is how video is typically
created in manim.
:class:`~mobject.mobject.Mobject` instances are automatically
added to the :class:`~scene.scene.Scene` when they are animated. You can add a
:class:`~mobject.mobject.Mobject` to the :class:`~scene.scene.Scene` manually
by passing it as an argument to :meth:`~scene.scene.Scene.add`.
:class:`~animation.creation.ShowCreation` draws a :class:`~mobject.mobject.Mobject` to the screen.
:class:`~animation.transform.Transform` morphs one :class:`~mobject.mobject.Mobject` into another.
:class:`~animation.creation.FadeOut` fades a :class:`~mobject.mobject.Mobject` out of the :class:`~scene.scene.Scene`.
.. note::
Only the first argument to :class:`~animation.transform.Transform` is modified,
the second is not added to the :class:`~scene.scene.Scene`. :class:`~animation.tranform.Transform`
only changes the appearance but not the underlying properties.
After the call to ``transform()`` ``square`` is still a :class:`~mobject.geometry.Square` instance
but with the shape of :class:`~mobject.geometry.Circle`.

View File

@@ -6,13 +6,20 @@
Welcome to Manim's documentation!
=================================
These docs are generated from the master branch of the
`Manim repo <https://github.com/3b1b/manim>`_. You can contribute by submitting
a pull request there.
.. toctree::
:maxdepth: 2
:caption: Contents:
:caption: Contents
about
installation/index
getting_started/index
coordinate
animation
constants
Indices and tables

View File

@@ -5,7 +5,7 @@ Instructions on installing Manim
.. toctree::
:maxdepth: 2
:caption: Contents:
:caption: Contents
linux
mac

View File

@@ -1,4 +1,12 @@
Mac
===
A stub for mac installation
The simplest way to install the system dependencies on Mac OS X is with Homebrew.
Mac come preinstalled with python2, but to use manim, python3 is required
1. Install python3 https://docs.python.org/3/using/mac.html
2. Install Cairo: ``brew install cairo``
3. Install Sox: ``brew install sox``
4. Install ffmpeg: ``brew install ffmpeg``
5. Install latex (MiKTeX): https://miktex.org/howto/install-miktex-mac
6. Install manimlib ``pip install manimlib`` (or ``pip install --user manimlib`` to just yourself)

View File

@@ -1,4 +1,60 @@
Windows
=======
A stub for windows installation
Install System Libraries
------------------------
Make sure you have *Python 3* for Windows installed first:
https://www.python.org/downloads/windows/
Install ffmpeg:
https://ffmpeg.org/download.html#build-windows
Install sox:
http://sox.sourceforge.net/Main/HomePage
Install a latex distribution. On Windows MikTex is commonly used:
https://miktex.org/howto/install-miktex
Path configuration
------------------
To invoke commandline without supplying path to the binary
the PATH environment needs to be configured. Below are template examples, please change
the path according to your username and specific python version. Assuming all the
softwares are installed with no alteration to the installation paths::
C:\Users\$username\AppData\local\Programs\Python\Python$version\
C:\Users\$username\AppData\local\Programs\Python\Python$version\Scripts\
C:\MikTex\miktex\bin\x64\
C:\ffmpeg\bin\
The path entries should be separated by semicolon.
Installing python packages and manim
------------------------------------
Make sure you can start pip using ``pip`` in your commandline. Then do
``pip install pyreadline`` for the ``readline`` package.
Grab the pycairo wheel binary ``pycairo1.18.0cp37cp37mwin32.whl`` from https://www.lfd.uci.edu/~gohlke/pythonlibs/#pycairo
and install it via ``python -m pip install C:\absolute\path\to\the\whl\file``
clone the manim repository if you have git ``git clone https://github.com/3b1b/manim`` or download the zip file from
the repository page with ``Clone or download`` button and unzip it.
Open the commandline within the manim directory with ``Shift + Right click`` on an empty space in the folder and select ``open command window here``
Install manim python dependencies with ``pip install -r requirement.txt``
Test the installation
---------------------
Type in ``python -m manim -h`` and if nothing went wrong during the installtion process you should see the help text.
Use ``python -m manim example_scenes.py SquareToCircle -pl`` to render the example scene and the file should play after rendering. The movie file should be
in ``media/videos/example_scenes/480p15``

0
manim.py Normal file → Executable file
View File

BIN
manim_docker_diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python
import manimlib.config
import manimlib.constants
import manimlib.extract_scene
import manimlib.stream_starter
@@ -8,6 +9,7 @@ def main():
args = manimlib.config.parse_cli()
if not args.livestream:
config = manimlib.config.get_configuration(args)
manimlib.constants.initialize_directories(config)
manimlib.extract_scene.main(config)
else:
manimlib.stream_starter.start_livestream(

View File

@@ -6,6 +6,8 @@ from manimlib.utils.rate_functions import linear
from manimlib.utils.rate_functions import double_smooth
from manimlib.utils.rate_functions import smooth
import numpy as np
class ShowPartial(Animation):
"""
@@ -120,6 +122,7 @@ class Write(DrawBorderThenFill):
class ShowIncreasingSubsets(Animation):
CONFIG = {
"suspend_mobject_updating": False,
"int_func": np.floor,
}
def __init__(self, group, **kwargs):
@@ -128,5 +131,5 @@ class ShowIncreasingSubsets(Animation):
def interpolate_mobject(self, alpha):
n_submobs = len(self.all_submobs)
index = int(alpha * n_submobs)
index = int(self.int_func(alpha * n_submobs))
self.mobject.submobjects = self.all_submobs[:index]

View File

@@ -332,6 +332,8 @@ class Camera(object):
points = self.transform_points_pre_display(
vmobject, vmobject.points
)
# TODO, shouldn't this be handled in transform_points_pre_display?
# points = points - self.get_frame_center()
if len(points) == 0:
return

View File

@@ -47,11 +47,21 @@ def parse_cli():
action="store_true",
help="Render at a medium quality",
),
parser.add_argument(
"--high_quality",
action="store_true",
help="Render at a high quality",
),
parser.add_argument(
"-g", "--save_pngs",
action="store_true",
help="Save each frame as a png",
),
parser.add_argument(
"-i", "--save_as_gif",
action="store_true",
help="Save the video as gif",
),
parser.add_argument(
"-f", "--show_file_in_finder",
action="store_true",
@@ -102,6 +112,23 @@ def parse_cli():
action="store_true",
help="Leave progress bars displayed in terminal",
)
parser.add_argument(
"--media_dir",
help="directory to write media",
)
video_group = parser.add_mutually_exclusive_group()
video_group.add_argument(
"--video_dir",
help="directory to write file tree for video",
)
video_group.add_argument(
"--video_output_dir",
help="directory to write video",
)
parser.add_argument(
"--tex_dir",
help="directory to write tex",
)
# For live streaming
module_location.add_argument(
@@ -161,6 +188,7 @@ def get_configuration(args):
"write_to_movie": args.write_to_movie or not args.save_last_frame,
"save_last_frame": args.save_last_frame,
"save_pngs": args.save_pngs,
"save_as_gif": args.save_as_gif,
# If -t is passed in (for transparent), this will be RGBA
"png_mode": "RGBA" if args.transparent else "RGB",
"movie_file_extension": ".mov" if args.transparent else ".mp4",
@@ -181,7 +209,11 @@ def get_configuration(args):
"start_at_animation_number": args.start_at_animation_number,
"end_at_animation_number": None,
"sound": args.sound,
"leave_progress_bars": args.leave_progress_bars
"leave_progress_bars": args.leave_progress_bars,
"media_dir": args.media_dir,
"video_dir": args.video_dir,
"video_output_dir": args.video_output_dir,
"tex_dir": args.tex_dir,
}
# Camera configuration
@@ -210,6 +242,8 @@ def get_camera_configuration(args):
camera_config.update(manimlib.constants.LOW_QUALITY_CAMERA_CONFIG)
elif args.medium_quality:
camera_config.update(manimlib.constants.MEDIUM_QUALITY_CAMERA_CONFIG)
elif args.high_quality:
camera_config.update(manimlib.constants.HIGH_QUALITY_CAMERA_CONFIG)
else:
camera_config.update(manimlib.constants.PRODUCTION_QUALITY_CAMERA_CONFIG)

View File

@@ -1,47 +1,57 @@
import numpy as np
import os
# Initialize directories
env_MEDIA_DIR = os.getenv("MEDIA_DIR")
if env_MEDIA_DIR:
MEDIA_DIR = env_MEDIA_DIR
elif os.path.isfile("media_dir.txt"):
with open("media_dir.txt", 'rU') as media_file:
MEDIA_DIR = media_file.readline().strip()
else:
MEDIA_DIR = os.path.join(
os.path.expanduser('~'),
"Dropbox (3Blue1Brown)/3Blue1Brown Team Folder"
)
if not os.path.isdir(MEDIA_DIR):
MEDIA_DIR = "./media"
print(
f"Media will be stored in {MEDIA_DIR + os.sep}. You can change "
"this behavior by writing a different directory to media_dir.txt."
)
MEDIA_DIR = ""
VIDEO_DIR = ""
VIDEO_OUTPUT_DIR = ""
TEX_DIR = ""
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
SOUND_DIR = os.path.join(MEDIA_DIR, "designs", "sounds")
###
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
FILE_DIR = os.path.join(os.getenv("FILE_DIR", default="."), "files")
TEX_DIR = os.path.join(FILE_DIR, "Tex")
# These two may be depricated now.
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
def initialize_directories(config):
global MEDIA_DIR
global VIDEO_DIR
global VIDEO_OUTPUT_DIR
global TEX_DIR
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR,
TEX_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR]:
if not os.path.exists(folder):
os.makedirs(folder)
video_path_specified = config["video_dir"] or config["video_output_dir"]
if not video_path_specified:
VIDEO_DIR = os.path.join(MEDIA_DIR, "videos")
elif config["video_output_dir"]:
VIDEO_OUTPUT_DIR = config["video_output_dir"]
else:
VIDEO_DIR = config["video_dir"]
TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex")
if not (video_path_specified and config["tex_dir"]):
if config["media_dir"]:
MEDIA_DIR = config["media_dir"]
else:
MEDIA_DIR = os.path.join(
os.path.expanduser('~'),
"Dropbox (3Blue1Brown)/3Blue1Brown Team Folder"
)
if not os.path.isdir(MEDIA_DIR):
MEDIA_DIR = "./media"
print(
f"Media will be written to {MEDIA_DIR + os.sep}. You can change "
"this behavior with the --media_dir flag."
)
else:
if config["media_dir"]:
print(
"Ignoring --media_dir, since both --tex_dir and a video "
"directory were both passed"
)
for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR]:
if folder != "" and not os.path.exists(folder):
os.makedirs(folder)
TEX_USE_CTEX = False
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(
THIS_DIR, "tex_template.tex" if not TEX_USE_CTEX
else "ctex_template.tex"
os.path.dirname(os.path.realpath(__file__)),
"tex_template.tex" if not TEX_USE_CTEX else "ctex_template.tex"
)
with open(TEMPLATE_TEX_FILE, "r") as infile:
TEMPLATE_TEXT_FILE_BODY = infile.read()
@@ -90,7 +100,7 @@ PRODUCTION_QUALITY_CAMERA_CONFIG = {
HIGH_QUALITY_CAMERA_CONFIG = {
"pixel_height": 1080,
"pixel_width": 1920,
"frame_rate": 30,
"frame_rate": 60,
}
MEDIUM_QUALITY_CAMERA_CONFIG = {

View File

@@ -14,7 +14,6 @@
\usepackage{ragged2e}
\usepackage{physics}
\usepackage{xcolor}
\usepackage{textcomp}
\usepackage{microtype}
%\DisableLigatures{encoding = *, family = * }
\usepackage[UTF8]{ctex}
@@ -24,4 +23,4 @@
YourTextHere
\end{document}
\end{document}

View File

@@ -15,11 +15,11 @@ from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import normalize
pi_creature_dir_maybe = os.path.join(MEDIA_DIR, "designs", "PiCreature")
pi_creature_dir_maybe = os.path.join(MEDIA_DIR, "assets", "PiCreature")
if os.path.exists(pi_creature_dir_maybe):
PI_CREATURE_DIR = pi_creature_dir_maybe
else:
PI_CREATURE_DIR = os.path.join(FILE_DIR)
PI_CREATURE_DIR = os.path.join("assets")
PI_CREATURE_SCALE_FACTOR = 0.5

View File

@@ -1,6 +1,8 @@
from manimlib.constants import *
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.rate_functions import smooth
from manimlib.utils.space_ops import get_norm
class AnimatedBoundary(VGroup):
@@ -66,3 +68,31 @@ class AnimatedBoundary(VGroup):
for sm1, sm2 in zip(family1, family2):
sm1.pointwise_become_partial(sm2, a, b)
return self
class TracedPath(VMobject):
CONFIG = {
"stroke_width": 2,
"stroke_color": WHITE,
"min_distance_to_new_point": 0.1,
}
def __init__(self, traced_point_func, **kwargs):
super().__init__(**kwargs)
self.traced_point_func = traced_point_func
self.add_updater(lambda m: m.update_path())
def update_path(self):
new_point = self.traced_point_func()
if self.has_no_points():
self.start_new_path(new_point)
self.add_line_to(new_point)
else:
# Set the end to be the new point
self.points[-1] = new_point
# Second to last point
nppcc = self.n_points_per_cubic_curve
dist = get_norm(new_point - self.points[-nppcc])
if dist >= self.min_distance_to_new_point:
self.add_line_to(new_point)

View File

@@ -26,6 +26,24 @@ DEFAULT_ARROW_TIP_LENGTH = 0.35
class TipableVMobject(VMobject):
"""
Meant for shared functionality between Arc and Line.
Functionality can be classified broadly into these groups:
* Adding, Creating, Modifying tips
- add_tip calls create_tip, before pushing the new tip
into the TipableVMobject's list of submobjects
- stylistic and positional configuration
* Checking for tips
- Boolean checks for whether the TipableVMobject has a tip
and a starting tip
* Getters
- Straightforward accessors, returning information pertaining
to the TipableVMobject instance's tip(s), its length etc
"""
CONFIG = {
"tip_length": DEFAULT_ARROW_TIP_LENGTH,
# TODO
@@ -35,12 +53,15 @@ class TipableVMobject(VMobject):
"stroke_width": 0,
}
}
"""
Meant simply for shard functionality between
Arc and Line
"""
# Adding, Creating, Modifying tips
def add_tip(self, tip_length=None, at_start=False):
"""
Adds a tip to the TipableVMobject instance, recognising
that the endpoints might need to be switched if it's
a 'starting tip' or not.
"""
tip = self.create_tip(tip_length, at_start)
self.reset_endpoints_based_on_tip(tip, at_start)
self.asign_tip_attr(tip, at_start)
@@ -48,11 +69,19 @@ class TipableVMobject(VMobject):
return self
def create_tip(self, tip_length=None, at_start=False):
"""
Stylises the tip, positions it spacially, and returns
the newly instantiated tip to the caller.
"""
tip = self.get_unpositioned_tip(tip_length)
self.position_tip(tip, at_start)
return tip
def get_unpositioned_tip(self, tip_length=None):
"""
Returns a tip that has been stylistically configured,
but has not yet been given a position in space.
"""
if tip_length is None:
tip_length = self.get_default_tip_length()
color = self.get_color()
@@ -85,6 +114,7 @@ class TipableVMobject(VMobject):
# Zero length, put_start_and_end_on wouldn't
# work
return self
if at_start:
self.put_start_and_end_on(
tip.get_base(), self.get_end()
@@ -102,7 +132,34 @@ class TipableVMobject(VMobject):
self.tip = tip
return self
# Checking for tips
def has_tip(self):
return hasattr(self, "tip") and self.tip in self
def has_start_tip(self):
return hasattr(self, "start_tip") and self.start_tip in self
# Getters
def pop_tips(self):
start, end = self.get_start_and_end()
result = VGroup()
if self.has_tip():
result.add(self.tip)
self.remove(self.tip)
if self.has_start_tip():
result.add(self.start_tip)
self.remove(self.start_tip)
self.put_start_and_end_on(start, end)
return result
def get_tips(self):
"""
Returns a VGroup (collection of VMobjects) containing
the TipableVMObject instance's tips.
"""
result = VGroup()
if hasattr(self, "tip"):
result.add(self.tip)
@@ -111,6 +168,8 @@ class TipableVMobject(VMobject):
return result
def get_tip(self):
"""Returns the TipableVMobject instance's (first) tip,
otherwise throws an exception."""
tips = self.get_tips()
if len(tips) == 0:
raise Exception("tip not found")
@@ -142,23 +201,7 @@ class TipableVMobject(VMobject):
start, end = self.get_start_and_end()
return get_norm(start - end)
def has_tip(self):
return hasattr(self, "tip") and self.tip in self
def has_start_tip(self):
return hasattr(self, "start_tip") and self.start_tip in self
def pop_tips(self):
start, end = self.get_start_and_end()
result = VGroup()
if self.has_tip():
result.add(self.tip)
self.remove(self.tip)
if self.has_start_tip():
result.add(self.start_tip)
self.remove(self.start_tip)
self.put_start_and_end_on(start, end)
return result
class Arc(TipableVMobject):
@@ -471,6 +514,9 @@ class Line(TipableVMobject):
about_point=self.get_start(),
)
def set_length(self, length):
self.scale(length / self.get_length())
def set_opacity(self, opacity, family=True):
# Overwrite default, which would set
# the fill opacity
@@ -534,6 +580,25 @@ class DashedLine(Line):
return self.submobjects[-1].points[-2]
class TangentLine(Line):
CONFIG = {
"length": 1,
"d_alpha": 1e-6
}
def __init__(self, vmob, alpha, **kwargs):
digest_config(self, kwargs)
da = self.d_alpha
a1 = np.clip(alpha - da, 0, 1)
a2 = np.clip(alpha + da, 0, 1)
super().__init__(
vmob.point_from_proportion(a1),
vmob.point_from_proportion(a2),
**kwargs
)
self.scale(self.length / self.get_length())
class Elbow(VMobject):
CONFIG = {
"width": 0.2,
@@ -625,7 +690,6 @@ class Arrow(Line):
class Vector(Arrow):
CONFIG = {
"color": YELLOW,
"buff": 0,
}

View File

@@ -9,6 +9,7 @@ import sys
from colour import Color
import numpy as np
import manimlib.constants as consts
from manimlib.constants import *
from manimlib.container.container import Container
from manimlib.utils.color import color_gradient
@@ -109,7 +110,7 @@ class Mobject(Container):
def save_image(self, name=None):
self.get_image().save(
os.path.join(VIDEO_DIR, (name or str(self)) + ".png")
os.path.join(consts.VIDEO_DIR, (name or str(self)) + ".png")
)
def copy(self):
@@ -1087,6 +1088,7 @@ class Mobject(Container):
mobject1.points, mobject2.points, alpha
)
self.interpolate_color(mobject1, mobject2, alpha)
return self
def interpolate_color(self, mobject1, mobject2, alpha):
pass # To implement in subclass

View File

@@ -455,9 +455,10 @@ class Bubble(SVGMobject):
mover.shift(point - self.get_tip())
return self
def flip(self):
Mobject.flip(self)
self.direction = -np.array(self.direction)
def flip(self, axis=UP):
Mobject.flip(self, axis=axis)
if abs(axis[0]) > 0:
self.direction = -np.array(self.direction)
return self
def pin_to(self, mobject):

View File

@@ -50,9 +50,9 @@ class SVGMobject(VMobject):
if self.file_name is None:
raise Exception("Must specify file for SVGMobject")
possible_paths = [
os.path.join(SVG_IMAGE_DIR, self.file_name),
os.path.join(SVG_IMAGE_DIR, self.file_name + ".svg"),
os.path.join(SVG_IMAGE_DIR, self.file_name + ".xdv"),
os.path.join(os.path.join("assets", "svg_images"), self.file_name),
os.path.join(os.path.join("assets", "svg_images"), self.file_name + ".svg"),
os.path.join(os.path.join("assets", "svg_images"), self.file_name + ".xdv"),
self.file_name,
]
for path in possible_paths:

View File

@@ -242,6 +242,7 @@ class VMobject(Mobject):
def set_opacity(self, opacity, family=True):
self.set_fill(opacity=opacity, family=family)
self.set_stroke(opacity=opacity, family=family)
self.set_stroke(opacity=opacity, family=family, background=True)
return self
def fade(self, darkness=0.5, family=True):

View File

@@ -7,13 +7,13 @@ import _thread as thread
from time import sleep
import datetime
import manimlib.constants as consts
from manimlib.constants import FFMPEG_BIN
from manimlib.constants import STREAMING_IP
from manimlib.constants import STREAMING_PORT
from manimlib.constants import STREAMING_PROTOCOL
from manimlib.constants import VIDEO_DIR
from manimlib.utils.config_ops import digest_config
from manimlib.utils.file_ops import guarantee_existance
from manimlib.utils.file_ops import guarantee_existence
from manimlib.utils.file_ops import add_extension_if_not_present
from manimlib.utils.file_ops import get_sorted_integer_files
from manimlib.utils.sounds import get_full_sound_file_path
@@ -27,6 +27,7 @@ class SceneFileWriter(object):
"png_mode": "RGBA",
"save_last_frame": False,
"movie_file_extension": ".mp4",
"gif_file_extension": ".gif",
"livestreaming": False,
"to_twitch": False,
"twitch_key": None,
@@ -34,7 +35,6 @@ class SceneFileWriter(object):
# TODO, address this in extract_scene et. al.
"file_name": None,
"output_directory": None,
"file_name": None,
}
def __init__(self, scene, **kwargs):
@@ -46,60 +46,69 @@ class SceneFileWriter(object):
# Output directories and files
def init_output_directories(self):
output_directory = self.output_directory or self.get_default_output_directory()
file_name = self.file_name or self.get_default_file_name()
module_directory = self.output_directory or self.get_default_module_directory()
scene_name = self.file_name or self.get_default_scene_name()
if self.save_last_frame:
image_dir = guarantee_existance(os.path.join(
VIDEO_DIR,
output_directory,
self.get_image_directory(),
))
if consts.VIDEO_DIR != "":
image_dir = guarantee_existence(os.path.join(
consts.VIDEO_DIR,
module_directory,
"images",
))
else:
image_dir = guarantee_existence(os.path.join(
consts.VIDEO_OUTPUT_DIR,
"images",
))
self.image_file_path = os.path.join(
image_dir,
add_extension_if_not_present(file_name, ".png")
add_extension_if_not_present(scene_name, ".png")
)
if self.write_to_movie:
movie_dir = guarantee_existance(os.path.join(
VIDEO_DIR,
output_directory,
self.get_movie_directory(),
))
if consts.VIDEO_DIR != "":
movie_dir = guarantee_existence(os.path.join(
consts.VIDEO_DIR,
module_directory,
self.get_resolution_directory(),
))
else:
movie_dir = guarantee_existence(consts.VIDEO_OUTPUT_DIR)
self.movie_file_path = os.path.join(
movie_dir,
add_extension_if_not_present(
file_name, self.movie_file_extension
scene_name, self.movie_file_extension
)
)
self.partial_movie_directory = guarantee_existance(os.path.join(
self.gif_file_path = os.path.join(
movie_dir,
self.get_partial_movie_directory(),
file_name,
add_extension_if_not_present(
scene_name, self.gif_file_extension
)
)
self.partial_movie_directory = guarantee_existence(os.path.join(
movie_dir,
"partial_movie_files",
scene_name,
))
def get_default_output_directory(self):
def get_default_module_directory(self):
filename = os.path.basename(self.input_file_path)
root, ext = os.path.splitext(filename)
return root if root else ext[1:]
root, _ = os.path.splitext(filename)
return root
def get_default_file_name(self):
def get_default_scene_name(self):
if self.file_name is None:
return self.scene.__class__.__name__
else:
return self.file_name
def get_movie_directory(self):
def get_resolution_directory(self):
pixel_height = self.scene.camera.pixel_height
frame_rate = self.scene.camera.frame_rate
return "{}p{}".format(
pixel_height, frame_rate
)
def get_image_directory(self):
return "images"
def get_partial_movie_directory(self):
return "partial_movie_files"
# Directory getters
def get_image_file_path(self):
return self.image_file_path
@@ -303,10 +312,19 @@ class SceneFileWriter(object):
'-f', 'concat',
'-safe', '0',
'-i', file_list,
'-c', 'copy',
'-loglevel', 'error',
movie_file_path
]
if not self.save_as_gif:
commands +=[
'-c', 'copy',
movie_file_path
]
if self.save_as_gif:
movie_file_path=self.gif_file_path
commands +=[
movie_file_path,
]
if not self.includes_sound:
commands.insert(-1, '-an')

View File

@@ -10,7 +10,7 @@ def add_extension_if_not_present(file_name, extension):
return file_name
def guarantee_existance(path):
def guarantee_existence(path):
if not os.path.exists(path):
os.makedirs(path)
return os.path.abspath(path)

View File

@@ -3,14 +3,13 @@ import os
from PIL import Image
from manimlib.constants import RASTER_IMAGE_DIR
from manimlib.utils.file_ops import seek_full_path_from_defaults
def get_full_raster_image_path(image_file_name):
return seek_full_path_from_defaults(
image_file_name,
default_dir=RASTER_IMAGE_DIR,
default_dir=os.path.join("assets", "raster_images"),
extensions=[".jpg", ".png", ".gif"]
)

View File

@@ -1,5 +1,4 @@
import os
from manimlib.constants import SOUND_DIR
from manimlib.utils.file_ops import seek_full_path_from_defaults
@@ -35,6 +34,6 @@ def play_finish_sound():
def get_full_sound_file_path(sound_file_name):
return seek_full_path_from_defaults(
sound_file_name,
default_dir=SOUND_DIR,
default_dir=os.path.join("assets", "sounds"),
extensions=[".wav", ".mp3"]
)

View File

@@ -1,9 +1,9 @@
import os
import hashlib
from manimlib.constants import TEX_DIR
from manimlib.constants import TEX_TEXT_TO_REPLACE
from manimlib.constants import TEX_USE_CTEX
import manimlib.constants as consts
def tex_hash(expression, template_tex_file_body):
@@ -22,7 +22,7 @@ def tex_to_svg_file(expression, template_tex_file_body):
def generate_tex_file(expression, template_tex_file_body):
result = os.path.join(
TEX_DIR,
consts.TEX_DIR,
tex_hash(expression, template_tex_file_body)
) + ".tex"
if not os.path.exists(result):
@@ -44,8 +44,8 @@ def tex_to_dvi(tex_file):
"latex",
"-interaction=batchmode",
"-halt-on-error",
"-output-directory=" + TEX_DIR,
tex_file,
"-output-directory=\"{}\"".format(consts.TEX_DIR),
"\"{}\"".format(tex_file),
">",
os.devnull
] if not TEX_USE_CTEX else [
@@ -53,8 +53,8 @@ def tex_to_dvi(tex_file):
"-no-pdf",
"-interaction=batchmode",
"-halt-on-error",
"-output-directory=" + TEX_DIR,
tex_file,
"-output-directory=\"{}\"".format(consts.TEX_DIR),
"\"{}\"".format(tex_file),
">",
os.devnull
]
@@ -79,12 +79,12 @@ def dvi_to_svg(dvi_file, regen_if_exists=False):
if not os.path.exists(result):
commands = [
"dvisvgm",
dvi_file,
"\"{}\"".format(dvi_file),
"-n",
"-v",
"0",
"-o",
result,
"\"{}\"".format(result),
">",
os.devnull
]

View File

@@ -6,5 +6,6 @@ progressbar==2.5
scipy==1.1.0
tqdm==4.24.0
opencv-python==3.4.2.17
pycairo>=1.17.1
pycairo==1.17.1; sys_platform == 'linux'
pycairo>=1.18.0; sys_platform == 'win32'
pydub==0.23.0

View File

@@ -11,10 +11,11 @@ project_urls =
Documentation = https://github.com/3b1b/manim
Source Code = https://github.com/3b1b/manim
license = MIT
url = https://eulertour.com/learn/manim
[files]
packages = manimlib
[entry_points]
console_scripts =
manim = manimlib:main
manim = manimlib:main

View File

@@ -4,8 +4,8 @@ import os
import sys
import importlib
import manimlib.constants as consts
from manimlib.constants import PRODUCTION_QUALITY_CAMERA_CONFIG
from manimlib.constants import VIDEO_DIR
from manimlib.config import get_module
from manimlib.extract_scene import is_child_scene
@@ -43,7 +43,7 @@ def stage_scenes(module_name):
# }
# TODO, fix this
animation_dir = os.path.join(
VIDEO_DIR, "ode", "part2", "1440p60"
consts.VIDEO_DIR, "ode", "part3", "1440p60"
)
#
files = os.listdir(animation_dir)