## Working with SVG Files – manim Series: Part 12

The post is part of a series on learning how to use manim.  You can find the previous tutorial post in this series here and the overview of the entire series here.

# 12.0 Working with SVG Files

The PiCreatures in 3B1B are scalable vector graphics (svg) files. manim has an SVGMobject class that can import svg files. To play around with using svg images in manim, I’ve created a couple of figures using (Inkscape)[https://inkscape.org/en/], an open source vector graphics package. I wanted to try to make a stick figure wave in manim so I created two figures, one normal and one with the hand waving. You can get the svg files I’ve used at the end of this post. Place them in the \design\svg_images\ folder inside your media directory (the top level directory where all the animation subfolders get created).

The code I used to import the stick figure was based on the PiCreature code located in \for_3b1b_videos\pi_creatures.py. My code looks like:

class StickMan(SVGMobject):
CONFIG = {
"color" : BLUE_E,
"stroke_width" : 2,
"stroke_color" : WHITE,
"fill_opacity" : 1.0,
"propagate_style_to_family" : True,
"height" : 3,
"corner_scale_factor" : 0.75,
"flip_at_start" : False,
"is_looking_direction_purposeful" : False,
"start_corner" : None,
#Range of proportions along body where arms are
"right_arm_range" : [0.55, 0.7],
"left_arm_range" : [.34, .462],
}
def __init__(self, mode = "plain", **kwargs):
self.parts_named = False
try:
svg_file = os.path.join(
SVG_IMAGE_DIR,
"stick_man_%s.svg"%mode
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)
except:
warnings.warn("No StickMan design with mode %s"%mode)
svg_file = os.path.join(
SVG_IMAGE_DIR,
"stick_man.svg"
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)

if self.flip_at_start:
self.flip()
if self.start_corner is not None:
self.to_corner(self.start_corner)

def name_parts(self):
#self.mouth = self.submobjects[MOUTH_INDEX]
self.body = self.submobjects[BODY_INDEX]
self.arms = self.submobjects[ARMS_INDEX]
self.legs = self.submobjects[LEGS_INDEX]
self.parts_named = True

def init_colors(self):
SVGMobject.init_colors(self)
if not self.parts_named:
self.name_parts()
self.body.set_fill(self.color, opacity = 1)
self.arms.set_fill(YELLOW, opacity = 0)
self.legs.set_fill(GREEN, opacity = 0)
return self



I’m sure I could trim the code more but I just wanted something that would work without too much debugging. To animate the waving, I’ve used get_all_pi_creature_modes to load the file with the waving hand. To do this, the svg file with the wave must have a similar file name. In particular, if the original file is named stick_man.svg, the other file name needs to start with the same stick_man followed by an underscore and the name of the particular mode. Thus the figure with hand extended in a wave is called stick_man_wave.svg. I can create an instance of the stick man using the waving figure using StickMan("wave"). Below is the code for the scene to make the stick man wave.

class SVGStickMan(Scene):
def construct(self):
start_man = StickMan()
plain_man = StickMan()
waving_man = StickMan("wave")

self.wait()
self.play(Transform(start_man,waving_man))
self.play(Transform(start_man,plain_man))


The reason I create two instances of the StickMan() is because I am transforming start_man but want the image to end up back looking like the original figure.

Two things to note. (1) The stroke_width and stroke_color for PiCreatures are set to not draw the outline of objects. If you want to see lines or the outlines of shapes you will need to set these values to something visible (i.e. non-zero stroke_width and a stroke_color that is different than the background). (2) Lines in svg are labeled as paths. The way manim deals with paths it to treat them as closed shapes. That means that if I don’t set the opacity to zero for a line, I will see an enclosed shape. See the video below where I’ve set the fill_opacity in the init_colors method for everything to 1.

Although I haven’t delved into the manim code, I think all manim looks at is the outlines of the shapes and not the filling.

I created a second scene just to make sure I had a handle on the scalable vector graphics import. When creating your own images, you will need to open the .svg file in a text editor to determine the indices for each submobject. manim imports each svg entity (e.g. a path, ellipse, box, or other shape) as a single submobject, and you will need to determine the ordering of those items in the parent SVGMobject class. I created a couple of shapes ( a circle connected by lines to a pair of squares). The relevant part of the svg file for this is shown here (there is a lot more metadata in the file I left out):

  <g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
id="path10"
cx="90.714287"
cy="84.577377"
r="6.8035712"
style="stroke-width:0.26458332" />
<path
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 83.987573,84.783006 73.856309,84.723056 V 84.66311"
id="path12"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 97.535888,85.022797 10.550902,-7.0739 0.1199,-0.359687 v 0 l 2.27803,-5.155555"
id="path14"
inkscape:connector-curvature="0" />
<rect
id="rect16"
width="6.0547786"
height="4.3762259"
x="67.861481"
y="82.564919"
style="stroke-width:0.26458332" />
<rect
id="rect18"
width="3.7167947"
height="3.5968981"
x="108.62632"
y="69.016602"
style="stroke-width:0.26458332" />
</g>


The file contains a circle, two paths, and two rectangles. Thus, when imported into manim, the circle will be the first submobject (index of 0), the two paths or lines will be the second and third submobject (indices 1 and 2) and the two squares will be the fourth and fifth submobject (indices 3 and 4). The class I used for this circle and square drawing is

class CirclesAndSquares(SVGMobject):
CONFIG = {
"color" : BLUE_E,
"stroke_width" : 2,
"stroke_color" : WHITE,
"fill_opacity" : 1.0,
"propagate_style_to_family" : True,
"height" : 3,
"corner_scale_factor" : 0.75,
"flip_at_start" : False,
"is_looking_direction_purposeful" : False,
"start_corner" : None,
"circle_index" : 0,
"line1_index" :1,
"line2_index" : 2,
"square1_index" : 3,
"square2_index" : 4,
}
def __init__(self, mode = "plain", **kwargs):
try:
svg_file = os.path.join(
SVG_IMAGE_DIR,
"circles_and_squares_%s.svg"%mode
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)
except:
warnings.warn("No other mode design with mode %s"%mode)
svg_file = os.path.join(
SVG_IMAGE_DIR,
"circles_and_squares.svg"
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)

def name_parts(self):
self.circle = self.submobjects[self.circle_index]
self.line1 = self.submobjects[self.line1_index]
self.line2 = self.submobjects[self.line2_index]
self.square1 = self.submobjects[self.square1_index]
self.square2 = self.submobjects[self.square2_index]
self.parts_named = True

def init_colors(self):
SVGMobject.init_colors(self)
self.name_parts()
self.circle.set_fill(RED, opacity = 1)
self.line1.set_fill(self.color, opacity = 0)
self.line2.set_fill(self.color, opacity = 0)
self.square1.set_fill(GREEN, opacity = 1)
self.square2.set_fill(BLUE, opacity = 1)
return self


I’ve used the order of the different elements in the svg file to label the indices in my CONFIG dictionary at the start of the class. The code to display this on the screen is

class SVGCircleAndSquare(Scene):
def construct(self):
thingy = CirclesAndSquares()

self.wait()


I know I can trim the code down for the svg class but I’ll save that for another day.

## SVG Files

For some reason I’m not allowed to upload svg files to WordPress so I’ve included the full files here. Copy and paste each chunk of code into a separate text file and save it with a .svg extension). You can also find the files at https://github.com/zimmermant/manim_tutorial

## stick_man.svg

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="stick_man.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:zoom="0.98994949"
inkscape:cx="219.18269"
inkscape:cy="560"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="855"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<rdf:RDF>
<cc:Work
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
id="path10"
cx="109.1796"
cy="135.43558"
rx="10.289877"
ry="10.824416"
style="stroke-width:1;fill:none;stroke:#000000;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 109.5805,146.25998 c -0.26726,46.77217 0,47.03944 0,47.03944"
id="path817"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 81.784475,211.74102 c 0,0 0,-0.26727 27.796025,-18.4416 21.64884,15.23437 21.64884,15.23437 21.64884,15.23437"
id="path819"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="m 137.91107,154.81261 v 0 0 c 0,0 0,0 -28.06329,5.07812 -26.459687,-5.07812 -26.459687,-4.81085 -26.459687,-4.81085"
id="path821"
inkscape:connector-curvature="0" />
</g>
</svg>



## stick_man_wave.svg

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="stick_man_wave1.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:zoom="2.8"
inkscape:cx="392.29509"
inkscape:cy="542.37889"
inkscape:document-units="mm"
inkscape:current-layer="g846"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="855"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<rdf:RDF>
<cc:Work
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g846">
<ellipse
style="stroke-width:1;fill:none;stroke:#000000;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
ry="10.824416"
rx="10.289877"
cy="135.43558"
cx="109.1796"
id="path10" />
<path
inkscape:connector-curvature="0"
id="path817"
d="m 109.5805,146.25998 c -0.26726,46.77217 0,47.03944 0,47.03944"
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="path819"
d="m 81.784475,211.74102 c 0,0 0,-0.26727 27.796025,-18.4416 21.64884,15.23437 21.64884,15.23437 21.64884,15.23437"
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" />
<path
inkscape:connector-curvature="0"
id="path821"
d="m 140.46241,139.59907 c 0,0 -4.4095,8.39959 -8.03199,11.24479 -6.37726,5.00885 -22.58264,9.04687 -22.58264,9.04687 -26.459687,-5.07812 -26.459687,-4.81085 -26.459687,-4.81085"
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:nodetypes="cscc" />
</g>
</g>
</svg>



## circles_and_squares.svg

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="circles_and_squares.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:zoom="4.413525"
inkscape:cx="342.85715"
inkscape:cy="802.85716"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="855"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<rdf:RDF>
<cc:Work
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
id="path10"
cx="90.714287"
cy="84.577377"
r="6.8035712"
style="stroke-width:0.26458332" />
<path
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 83.987573,84.783006 73.856309,84.723056 V 84.66311"
id="path12"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 97.535888,85.022797 10.550902,-7.0739 0.1199,-0.359687 v 0 l 2.27803,-5.155555"
id="path14"
inkscape:connector-curvature="0" />
<rect
id="rect16"
width="6.0547786"
height="4.3762259"
x="67.861481"
y="82.564919"
style="stroke-width:0.26458332" />
<rect
id="rect18"
width="3.7167947"
height="3.5968981"
x="108.62632"
y="69.016602"
style="stroke-width:0.26458332" />
</g>
</svg>


Posted in Just for Fun, Programming | Tagged , , , | 2 Comments

## Fields of a Moving Charge – manim Series: Part 11

The post is part of a series on learning how to use manim.  You can find the previous tutorial post in this series here and the overview of the entire series here.

# 11.0 Field of a Moving Charge

There was a question over on Reddit about how to create the electric field of a moving charge. Since that is something I will want to do at some point I figured it would be fun to give it a try.

Before creating a changing field, I thought I’d start with moving charges around. I know I saw this in one of the videos so I can start with working code and modify it to my needs. Here is what I came up with:

class MovingCharges(Scene):
CONFIG = {
"plane_kwargs" : {
"color" : RED_B
},
"point_charge_loc" : 0.5*RIGHT-1.5*UP,
}
def construct(self):
plane = NumberPlane(**self.plane_kwargs)

field = VGroup(*[self.calc_field(x*RIGHT+y*UP)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
])
self.field=field
source_charge = self.Positron().move_to(self.point_charge_loc)
self.play(ShowCreation(field))
self.moving_charge()

def calc_field(self,point):
x,y = point[:2]
Rx,Ry = self.point_charge_loc[:2]
r = math.sqrt((x-Rx)**2 + (y-Ry)**2)
efield = (point - self.point_charge_loc)/r**3
return Vector(efield).shift(point)

def moving_charge(self):
numb_charges = 1
possible_points = [v.get_start() for v in self.field]
points = random.sample(possible_points, numb_charges)
particles = VGroup(*[
self.Positron().move_to(point)
for point in points
])
for particle in particles:
particle.velocity = np.array((0,0,0))

self.moving_particles = particles
self.always_continually_update = True
self.wait(10)

def field_at_point(self,point):
x,y = point[:2]
Rx,Ry = self.point_charge_loc[:2]
r = math.sqrt((x-Rx)**2 + (y-Ry)**2)
efield = (point - self.point_charge_loc)/r**3
return efield

def continual_update(self, *args, **kwargs):
if hasattr(self, "moving_particles"):
dt = self.frame_duration
for p in self.moving_particles:
accel = self.field_at_point(p.get_center())
p.velocity = p.velocity + accel*dt
p.shift(p.velocity*dt)

class Positron(Circle):
CONFIG = {
"stroke_width" : 3,
"color" : RED,
"fill_color" : RED,
"fill_opacity" : 0.5,
}
def __init__(self, **kwargs):
Circle.__init__(self, **kwargs)
plus = TexMobject("+")
plus.scale(0.7)
plus.move_to(self)


The most important method here is continual_update(). This method updates the screen for each frame during the entire scene. This differs from the various transformations that rely on the play() method in that the transformations occur over a short time interval, usually on the order of a few seconds while the continual methods continue to run for the entire scene. If we want a particle to move across the screen we might be tempted to use something like self.play(ApplyMethod(particle1.shift,5*LEFT)) but it would be challenging to control the timing of other transformations going on at the same time. The continual_update() allows you to animate things in the background while still controlling the timing of other transformations.

Since I know I will be using charged particles in my videos I’ve written a Positron class to create positively charged particles. The positron is the positive antiparticle of the electron. Why didn’t I make it a proton? Because the proton is roughly 2000 times more massive and I want similarly sized particles for what I want to do.

We’ve reused the code from a previous post about electric fields but we’ve added methods to create the charged particles and move them around. moving_charge() is what creates positrons by randomly selecting a field point (possible_points = [v.get_start() for v in self.field] is a list of the locations of the tails of all field vectors) and then selects numb_charges points to create particles at. Note that the randomly generated charges don’t react to one another, which I find disturbing to watch because it isn’t physical.

particles is a vectorized mobject group that contains all of the moving charges with initial velocities set to zero (particle.velocity = np.array((0,0,0))). We could have simplified the code by only using one particle at a set location, but we’ll need multiple charges later on. The charges are then added to the screen (self.play(FadeIn(particles)) and assigned to a class variable that is needed in continual_update (self.moving_particles = particles). Mobjects are drawn in the order they are added to the screen but you can place certain mobjects in the foreground to insure they always remain drawn on top of other objects by using add_foreground_mobjects(). It is kind of like layers in Photoshop or similar software except each mobject is in its own layer. This has to do with the fact that manim keeps all mobjects drawn on the screen in a list and draws them in the order they are listed. There is no equivalent background mobject method, but you can send mobjects to the front or back layers with bring_to_front() and bring_to_back().

Next we tell manim to continually update things in the background (self.always_continually_update = True) and then wait ten seconds. It is important to set the wait() command because the continual update only runs as long as their are animation elements (play() commands) or wait() commands in the animation queue.

The field_at_point() method duplicates some of the earlier code but is used to return a numerical vector (a numpy 3-element array) rather than a mobject Vector, which is what calc_field() returns. It took me an embarassing amount of time to figure out why I couldn’t just use calc_field() to find the force vector.

The continual_update() method is called each frame when the scene is being composed. The first line, if hasattr(self, "moving_particles"): prevents the rest of the code running and throwing and error if you haven’t created self.moving_particles. The frame duration is either 1/15, 1/30, or 1/60 of a second, depending on whether your video is low, medium, or production quality (i.e. whether you include -l, -m, or no command line argument when extracting the scene). We run through the list of all moving particles (for p in self.moving_particles:) and then calculate the acceleration due to the electric field at the location of each particle (vect = self.field_at_point(p.get_center())). p.get_center() returns the vector location of each particle p. The velocity is updated using $\vec{v}_f = \vec{v}_i + a \Delta t$ and then the particle is shifted over the distance $\vec{v}_f \Delta t$.

## 11.1 Updating the Electric Field of a Moving Charge

Now we’ve got some experience moving things around on the screen so we can move on to calculating the field due to the particle. We will reuse much of the code from our previous program, with a few changes.

class FieldOfMovingCharge(Scene):
CONFIG = {
"plane_kwargs" : {
"color" : RED_B
},
"point_charge_start_loc" : 5.5*LEFT-1.5*UP,
}
def construct(self):
plane = NumberPlane(**self.plane_kwargs)

field = VGroup(*[self.create_vect_field(self.point_charge_start_loc,x*RIGHT+y*UP)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
])
self.field=field
self.source_charge = self.Positron().move_to(self.point_charge_start_loc)
self.source_charge.velocity = np.array((1,0,0))
self.play(ShowCreation(field))
self.moving_charge()

def create_vect_field(self,source_charge,observation_point):
return Vector(self.calc_field(source_charge,observation_point)).shift(observation_point)

def calc_field(self,source_point,observation_point):
x,y,z = observation_point
Rx,Ry,Rz = source_point
r = math.sqrt((x-Rx)**2 + (y-Ry)**2 + (z-Rz)**2)
if r<0.0000001:   #Prevent divide by zero
efield = np.array((0,0,0))
else:
efield = (observation_point - source_point)/r**3
return efield

def moving_charge(self):
numb_charges=1
possible_points = [v.get_start() for v in self.field]
points = random.sample(possible_points, numb_charges)
particles = VGroup(self.source_charge, *[
self.Positron().move_to(point)
for point in points
])
for particle in particles[1:]:
particle.velocity = np.array((0,0,0))
self.moving_particles = particles
self.always_continually_update = True
self.wait(5)

def continual_update(self, *args, **kwargs):
Scene.continual_update(self, *args, **kwargs)
if hasattr(self, "moving_particles"):
dt = self.frame_duration

for v in self.field:
field_vect=np.zeros(3)
for p in self.moving_particles:
field_vect = field_vect + self.calc_field(p.get_center(), v.get_start())
v.put_start_and_end_on(v.get_start(), field_vect+v.get_start())

for p in self.moving_particles:
accel = np.zeros(3)
p.velocity = p.velocity + accel*dt
p.shift(p.velocity*dt)

class Positron(Circle):
CONFIG = {
"stroke_width" : 3,
"color" : RED,
"fill_color" : RED,
"fill_opacity" : 0.5,
}
def __init__(self, **kwargs):
Circle.__init__(self, **kwargs)
plus = TexMobject("+")
plus.scale(0.7)
plus.move_to(self)



One change we’ve made is to let calc_field() return a numpy vector rather than a mobject Vector. This does mean adding in create_vect_field() to create the mobjects from the numpy vectors.

Since we want our source charge to be able to move we have to add that source charge to the particles list. Thus the Vgroup we create includes that source charge plus the randomly generated charges using particles = VGroup(self.source_charge, *[self.Positron().move_to(point) for point in points]). Remember that the asterisk in front of the list lets Python know that each element in the list should be broken out and treated as a separate argument for the VGroup() class. To help make sense of this line of code we can break it out into a less elegant form:

list_of_random_charges=[]
for point in points:
new_charge = self.Positron().move_toe(point)
list_of_random_charges.append(new_charge)
particles = VGroup(self.source_charge, list_of_random_charges[0], list_of_random_charges[1], list_of_random_charges[2])


Thus one line of code replaces several lines. The reason we use particles[1:] in the code defining the velocity and fading in the particles is that the source charge already has a velocity and is on screen so we don’t want to redefine the velocity or have it fade in again (which makes it blink).

In continual_update() we now need to calculate the field at each grid point at each time step. First we cycle through each field point (for v in self.field:). Since we want to add up the fields from several charges, we set the field vector to zero (field_vect=np.zeros(3)) and then add up the fields at that point due to each charge (field_vect = field_vect + self.calc_field(p.get_center(), v.get_start())). We need to redraw the field vectors by specifying the start and end points of the vector. The start point is the initial grid point where the vector starts (v.get_start()) and the tip of the arrow is a distance equal to the field vector plus the starting point (field_vect+v.get_start()).

I don’t have the particles react to the fields of the other particles. This looks very unrealistic to me but it should be easy enough to implement. I just wanted to put out a post that lays out the basics of how to get the field lines working.

As usual, the code can be found at https://github.com/zimmermant/manim_tutorial.

Next time I’ll look at how to work with SVG files in manim.

Posted in Just for Fun, Programming | Tagged , , , | 3 Comments

## 3D Scenes – manim Series: Part 10

The post is part of a series on learning how to use manim.  You can find the previous tutorial post in this series here and the overview of the entire series here.

Note: When extracting these scenes I’d recommend including the low quality command line argument l. These scenes can take several minutes to extract at higher qualities.

# 10.0 3D Scenes

The 3D scenes really aren’t any different than 2D scenes except you can now move the camera around using self.move_camera(). Below is an example using the 2D vector field from my previous post.

class ExampleThreeD(ThreeDScene):
CONFIG = {
"plane_kwargs" : {
"color" : RED_B
},
"point_charge_loc" : 0.5*RIGHT-1.5*UP,
}
def construct(self):
self.set_camera_position(0, -np.pi/2)
plane = NumberPlane(**self.plane_kwargs)

field2D = VGroup(*[self.calc_field2D(x*RIGHT+y*UP)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
])

self.play(ShowCreation(field2D))
self.wait()
self.move_camera(0.8*np.pi/2, -0.45*np.pi)
self.begin_ambient_camera_rotation()
self.wait(6)

def calc_field2D(self,point):
x,y = point[:2]
Rx,Ry = self.point_charge_loc[:2]
r = math.sqrt((x-Rx)**2 + (y-Ry)**2)
efield = (point - self.point_charge_loc)/r**3
return Vector(efield).shift(point)


The differences between this code and the example from my previous post is (1) the parent class is ThreeDScene rather than Scene, (2) the self.set_camera_position() command, (3) the self.move_camera() command, and (4) the self.begin_ambient_camera_rotation() command.

The ThreeDScene() adds the ability to change the orientation of the camera. set_camera_position() moves the camera to the specified location and orientation. The camera will abruptly jump to the given location. If we want a smooth camera transition (panning the camera) we’d use move_camera(). The keyword arguments for set_camera_position() are phi, theta, distance, center_x, center_y, and center_z. The point that the camera is pointing towards is given by center_x, center_y, center_z. The other three arguments correspond to $\phi$, $\theta$, and $r$ in the figure below. Notice that the center point corresponds to the origin of the graph, with the camera located at the ‘x’, looking back towards the origin. This is why $\phi = 0$ and $\theta = -\pi/2$ corresponds to the normal 2D orientation with the x-axis pointing right and the y-axis pointing up on the screen. If you set $\theta = \pi/2$ we flip the screen over.

By DmcqOwn work, CC BY-SA 3.0, Link

## 10.1 3D Vector Field

Drawing a three-dimensional vector field requires only a couple of tweaks to our original code. We will copy the code for field2D and the method calc_field2D to create field3D and calc_field3D. With field3D we need to only add for z in np.arange(-5,5,1) and then send the vector x*RIGHT + y*UP + z*OUT to calc_field3D. Next we add the z-coordinates to calc_field3D. This means we don’t slice point or self.point_charge since we need all three components. We also need to add (z-Rz)**2 to the equation for r.

class EFieldInThreeD(ThreeDScene):
CONFIG = {
"plane_kwargs" : {
"color" : RED_B
},
"point_charge_loc" : 0.5*RIGHT-1.5*UP,
}
def construct(self):
self.set_camera_position(0.1, -np.pi/2)
plane = NumberPlane(**self.plane_kwargs)

field2D = VGroup(*[self.calc_field2D(x*RIGHT+y*UP)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
])

field3D = VGroup(*[self.calc_field3D(x*RIGHT+y*UP+z*OUT)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
for z in np.arange(-5,5,1)])

self.play(ShowCreation(field3D))
self.wait()
self.move_camera(0.8*np.pi/2, -0.45*np.pi)
self.begin_ambient_camera_rotation()
self.wait(6)

def calc_field2D(self,point):
x,y = point[:2]
Rx,Ry = self.point_charge_loc[:2]
r = math.sqrt((x-Rx)**2 + (y-Ry)**2)
efield = (point - self.point_charge_loc)/r**3
return Vector(efield).shift(point)

def calc_field3D(self,point):
x,y,z = point
Rx,Ry,Rz = self.point_charge_loc
r = math.sqrt((x-Rx)**2 + (y-Ry)**2+(z-Rz)**2)
efield = (point - self.point_charge_loc)/r**3
return Vector(efield).shift(point)


We don’t need field2D or calc_field2D anymore but I left them in for comparison to the updated code.

Things to try
– Rotate the camera so it is below the axes and to the left (or any other point you choose)
– Try plotting a different vector field
– Plot a constant field
– Try efield = np.array((-y,x,z))/math.sqrt(x2+y2+z**2)
– Come up with your own 3D vector field

Check out the next post in this series where we look at how to animate the electric field of a moving charge.

Posted in Just for Fun, Programming | Tagged , , , | 3 Comments

## Matter & Interactions Section Headings

I was looking for the section headings for volume 2 of the 4th edition of Matter & Interactions and couldn’t find them online. Since texts tend to be online these days page numbers don’t matter as much as section titles. I sat down and typed them all in and I thought I’d post this here in case anyone else needed them.

Section_Titles_M_and_I_4th

## Vector Fields – manim Series: Part 9

The post is part of a series on learning how to use manim.  You can find the previous tutorial post in this series here and the overview of the entire series here.

# 9.0 Vector Fields

Before diving into draw a vector field, we should set up a Cartesian axes using NumberPlane(). This gives you two axes and an underlying grid. The CONFIG{} for the NumberPlane() class is

class NumberPlane(VMobject):
CONFIG = {
"color": BLUE_D,
"secondary_color": BLUE_E,
"axes_color": WHITE,
"secondary_stroke_width": 1,
"x_unit_size": 1,
"y_unit_size": 1,
"center_point": ORIGIN,
"x_line_frequency": 1,
"y_line_frequency": 1,
"secondary_line_ratio": 1,
"written_coordinate_height": 0.2,
"propagate_style_to_family": False,
"make_smooth_after_applying_functions": True,
}


You can change any of these default values by passing a dictionary with new values as keyword arguments. For example, if you want to change the spacing of the grid lines you could change x_line_frequency and y_line_frequency by defining a dictionary with these variables and then passing the dictionary to NumberPlane(). If you want to see the x-axis and y-axis indicated you can use get_axis_labels() to draw an x and ay next to the appropriate axis. See the code below.

class DrawAnAxis(Scene):
CONFIG = { "plane_kwargs" : {
"x_line_frequency" : 2,
"y_line_frequency" :2
}
}

def construct(self):
my_plane = NumberPlane(**self.plane_kwargs)
self.wait()


The double asterisk in front of the argument self.plane_kwargs lets the class know that this is a dictionary that needs to be unpacked.

I recommend changing the various properties to see what affect they have on the axes and grid. This is the best way to learn what things do.

## 9.1 A Simple Vector Field

Let’s start with a simple vector field; a constant field. We first need to define a set of vector points for each grid point, define the field at each grid point, then create the Vector() for the field at each point. Finally we combine all the Vector() instances into a VGroup to allow us to draw all vector lines with a single command.

class SimpleField(Scene):
CONFIG = {
"plane_kwargs" : {
"color" : RED
},
}
def construct(self):
plane = NumberPlane(**self.plane_kwargs) #Create axes and grid

points = [x*RIGHT+y*UP
for x in np.arange(-5,5,1)
for y in np.arange(-5,5,1)
]     #List of vectors pointing to each grid point

vec_field = []  #Empty list to use in for loop
for point in points:
field = 0.5*RIGHT + 0.5*UP   #Constant field up and to right
result = Vector(field).shift(point)   #Create vector and shift it to grid point
vec_field.append(result)   #Append to list

draw_field = VGroup(*vec_field)   #Pass list of vectors to create a VGroup

self.play(ShowCreation(draw_field))   #Draw VGroup on screen


After creating the NumberPlane() we use a list comprehension to create a list of the location of all grid points. Remember that RIGHT=np.array(1,0,0) and UP=np.array(0,1,0) so this list comprehension covers all points from (5,5,0) down to (-5,-5,0) in unit step sizes. The last number in arange() specifies the step size. Next we create an empty list vec_field to hold all of the vectors we are going to create. The for loop goes through each grid location in points and creates a vector whose length and direction are defined by field. It is inefficient to keep defining field each time through the loop but we are setting things up for later. The shift(point) command moves the vector to the grid location defined by point. These results are then appended to a list. After going through the for loop. all of the vectors are grouped together in a single VGroup called draw_field. The only reason for doing this is that you can then add draw_field using a single add or play command. You could have included self.add(result) inside each iteration of the for loop instead of showing the creation of draw_field, but using the VGroup feels cleaner.

## 9.2 A Variable Vector Field

For a slightly more interesting field we will look at the electric field due to a postive point charge. The electric field is:

$\displaystyle \vec{E} = \frac{1}{4 \pi \epsilon_0} \frac{q}{r^3} \vec{r}$

where $q_1$ is the charge on the point charge, $\vec{r}$ is the distance vector between the charge and the observation point, and $r$ is the magnitude of that vector. The constant out front $\frac{1}{4 \pi \epsilon_0} = 9 \times 10^9 Nm^2/C^2$ is essentially a conversion factor. For our purposes we will set all constants equal to zero and just look at

$\displaystyle \vec{E} = \frac{1}{r^3} \vec{r}$.

class FieldWithAxes(Scene):
CONFIG = {
"plane_kwargs" : {
"color" : RED_B
},
"point_charge_loc" : 0.5*RIGHT-1.5*UP,
}
def construct(self):
plane = NumberPlane(**self.plane_kwargs)

field = VGroup(*[self.calc_field(x*RIGHT+y*UP)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
])

self.play(ShowCreation(field))

def calc_field(self,point):
x,y = point[:2]
Rx,Ry = self.point_charge_loc[:2]
r = math.sqrt((x-Rx)**2 + (y-Ry)**2)
efield = (point - self.point_charge_loc)/r**3


The location of the point charge is set in CONFIG{}. To create the vector field we’ve condensed the previous code. We use a list comprehension and the function calc_field() as the argument of VGroup(). The calc_field() function defines the field to calculate. To make the formulas a little easier to read we unpack the x- and y-coordinates from the point vector and the self.point_charge_loc vector. The code x,y=point[:2] is equivalent to x=point[0] and y=point[1].

The fade(0.9) method sets the opacity of the lines to be one minus the fade level (so in this case the opacity is set to 0.1). This was done to make it easier to see the tiny field arrows farther from the charge location.

Things to try:
– Change each of the elements in CONFIG{} for NumberPlane() to see what affect they have on the axes and grid lines.
– Calculate different fields
– Try efield = np.array((-y,x,0))/math.sqrt(x**2+y**2)
– Try efield = np.array(( -2*(y%2)+1 , -2*(x%2)+1 , 0 ))/3
– Come up with your own equation

Note: If you end up creating a video using manim and post it online, please link to it in the comments. I’m sure everyone would love to see what sort of fun things everyone is doing.

Next time we will look at plotting vector fields in three dimensions.

Posted in Just for Fun, Programming | Tagged , , , | 4 Comments

## More Graphing – manim Series: Part 8

The post is part of a series on learning how to use manim.  You can find the previous tutorial post in this series here and the overview of the entire series here.

# 8.0 More Graphing

In the previous post in this series we looked at how to graph functions in manim so go take a look at this if you haven’t already. I’ve been playing a bit more with graphing in manim and wanted to share some of what I’ve learned. You can copy and paste the code below or download the code from Github.

class ExampleApproximation(GraphScene):
CONFIG = {
"function" : lambda x : np.cos(x),
"function_color" : BLUE,
"taylor" : [lambda x: 1, lambda x: 1-x**2/2, lambda x: 1-x**2/math.factorial(2)+x**4/math.factorial(4), lambda x: 1-x**2/2+x**4/math.factorial(4)-x**6/math.factorial(6),
lambda x: 1-x**2/math.factorial(2)+x**4/math.factorial(4)-x**6/math.factorial(6)+x**8/math.factorial(8), lambda x: 1-x**2/math.factorial(2)+x**4/math.factorial(4)-x**6/math.factorial(6)+x**8/math.factorial(8) - x**10/math.factorial(10)],
"center_point" : 0,
"approximation_color" : GREEN,
"x_min" : -10,
"x_max" : 10,
"y_min" : -1,
"y_max" : 1,
"graph_origin" : ORIGIN ,
"x_labeled_nums" :range(-10,12,2),

}
def construct(self):
self.setup_axes(animate=True)
func_graph = self.get_graph(
self.function,
self.function_color,
)
approx_graphs = [
self.get_graph(
f,
self.approximation_color
)
for f in self.taylor
]

term_num = [
TexMobject("n = " + str(n),aligned_edge=TOP)
for n in range(0,8)]
[t.to_edge(BOTTOM,buff=SMALL_BUFF) for t in term_num]

term = TexMobject("")
term.to_edge(BOTTOM,buff=SMALL_BUFF)

approx_graph = VectorizedPoint(
self.input_to_graph_point(self.center_point, func_graph)
)

self.play(
ShowCreation(func_graph),
)
for n,graph in enumerate(approx_graphs):
self.play(
Transform(approx_graph, graph, run_time = 2),
Transform(term,term_num[n])
)
self.wait()


I wanted to demonstrate how adding higher terms in a Taylor expansion results in better and better agreement with a function. This is similar to what
shinigamiphoenix posted here.

The functions to plot are defined as lambda functions in the CONFIG{} dictionary. manim processes all elements in CONFIG{} and turns the dictionary entries into class variables with the key as the variable name. Thus "function" can be accessed within my class by calling self.function and "taylor" can be called with self.taylor. If you aren’t familiar with lambda functions, check out this post at Python Conquers the Universe.

We create a list of graphs using get_graph() and a list comprehension. You can find a nice tutorial on list comprehensions over at datacamp.com. It was only after reading this tutorial that I made the connection between list comprehensions and mathematical notation for definitions of sets (e.g. the set of positive real numbers is{$x | x \in$  R and $x > 0$} or the set of even numbers which is {$x | x \in$  I and $x\ mod(2) = 0$}), which made list comprehensions click for me. For each item in the list self.taylor, a graph is created with color self.approximation_color. We also created a list of TexMobjects to indicate which order of terms are included from the Taylor expansion using a list comprehension.

Since we are going to do successive transformations from a list, it helps to have a blank placeholder on the screen. term and approx_graph are VectorizedPoint instances, which are mobjects that don’t display anything on screen. This way we can put the placeholders on the screen without anything appearing, and then transform those mobjects into either the graph or the TexMobjects.

The enumerate() command is a useful tool that iterates over a list and also returns the index of the item returned. Thus for n,graph in enumerate(approx_graphs) returns the index between 0 and 4 as n, and the element within the list as graph. This is used to display the corresponding item from term_num with each graph.

Check out how to draw vector fields in the next post in this series.

Posted in Just for Fun, Programming | Tagged , , , | 3 Comments

## Graphing Functions – manim Series: Part 7

The post is part of a series on learning how to use manim.  You can find the previous tutorial post in this series here and the overview of the entire series here.

# 7.0 Graphing Functions

The easiest way to plot functions is to base your scene class on the GraphScene(). The scene creates a set of axes and has methods for creating graphs. One thing that confused me a little at first is that the axes belong to your scene class so you will need to use self to access the methods related to the axes. This caused me a few issues when I started out.

We will start off by looking at how to create the axes and graphs but we will come back to look at the CONFIG{} dictionary, which is used frequently in manim for initializing many of the class variables.

class PlotFunctions(GraphScene):
CONFIG = {
"x_min" : -10,
"x_max" : 10,
"y_min" : -1.5,
"y_max" : 1.5,
"graph_origin" : ORIGIN ,
"function_color" : RED ,
"axes_color" : GREEN,
"x_labeled_nums" :range(-10,12,2),

}
def construct(self):
self.setup_axes(animate=True)
func_graph=self.get_graph(self.func_to_graph,self.function_color)
func_graph2=self.get_graph(self.func_to_graph2)
vert_line = self.get_vertical_line_to_graph(TAU,func_graph,color=YELLOW)
graph_lab = self.get_graph_label(func_graph, label = "\\cos(x)")
graph_lab2=self.get_graph_label(func_graph2,label = "\\sin(x)", x_val=-10, direction=UP/2)
two_pi = TexMobject("x = 2 \\pi")
label_coord = self.input_to_graph_point(TAU,func_graph)
two_pi.next_to(label_coord,RIGHT+UP)

self.play(ShowCreation(func_graph),ShowCreation(func_graph2))
self.play(ShowCreation(vert_line), ShowCreation(graph_lab), ShowCreation(graph_lab2),ShowCreation(two_pi))

def func_to_graph(self,x):
return np.cos(x)

def func_to_graph2(self,x):
return np.sin(x)


self.setup_axes() will create a set of axes on screen. With the exception of whether the creation is animated or not, all other variables for the axes are set using CONFIG{}, which I’ll explain in a bit. The default values for the GraphScene() are shown below:

CONFIG = {
"x_min": -1,
"x_max": 10,
"x_axis_width": 9,
"x_tick_frequency": 1,
"x_leftmost_tick": None, # Change if different from x_min
"x_labeled_nums": None,
"x_axis_label": "$x$",
"y_min": -1,
"y_max": 10,
"y_axis_height": 6,
"y_tick_frequency": 1,
"y_bottom_tick": None, # Change if different from y_min
"y_labeled_nums": None,
"y_axis_label": "$y$",
"axes_color": GREY,
"graph_origin": 2.5 * DOWN + 4 * LEFT,
"exclude_zero_label": True,
"num_graph_anchor_points": 25,
"default_graph_colors": [BLUE, GREEN, YELLOW],
"default_derivative_color": GREEN,
"default_input_color": YELLOW,
"default_riemann_start_color": BLUE,
"default_riemann_end_color": GREEN,
"area_opacity": 0.8,
"num_rects": 50,
}


With our example we have changed x_min, x_max, y_min, y_max, graph_origin, axes_color, and x_labeled_num. The values assigned in our class take priority over values set by the parent class. Every value that we don’t change is automatically assigned the value defined in the parent class. The x_labeled_num property takes a list of numbers for labels along the x-axis. We’ve used range(-10,12,2) to generate a list of values from -10 to +10 in steps of 2. One issue I’ve noted with the y-axis is that setting the min and max values along either axis to numbers that are not integer multiples of 0.5 results in the tick marks along that axis not being symmetric about zero (e.g. try y_min = -1.2 and y_max = 1.2). I’m not sure what that is about but it isn’t a problem if you stick to integer multiples of 0.5 you don’t have any problems.

Once you have the axes set up you can use self.get_graph() to graph a function. The argument of get_graph() needs to be a pointer to a function, rather than a call to the function itself. In other words, since one of my functions is func_to_graph() I should use self.get_graph(func_to_graph) without any parenthese after func_to_graph.

Rather than defining separate functions for graphing we could use lambda functions. For example, if I define self.func = lambda x: np.cos(x) and then use self.get_graph(self.func) I will get the same result.

With get_graph() you do need to expicitly pass arguments rather than using CONFIG{}. The possible arguments, in addition to the function to graph, are color, x_min, and x_max. If you don’t specify a color GraphScene will cylce through BLUE, GREEN, and YELLOW for successive graphs. Since I didn’t specify a color for my second graph it was automatically assigned the first color, BLUE.

There is a handy method to draw a vertical line from the x-axis to the graph called get_vertical_line_to_graph(). I love that the method naming convention is descriptive enought that you can see what each method does at a glance. Good job, Grant! The arguments for get_vertical_line_to_graph() are the x-value where you want the line and the particular graph you want the line drawn to. Note that get_vertical_line_to_graph() is a method of the GraphScene and not the graph or axes so it is called with self.get_vertical_line_to_graph().

You can label graphs using get_graph_label() to set the text associated with the graph. This is similar to the get_text() method of the Braces() class in that it creates a texmobject at a specific location but does not draw it on the screen; you need to add or play to show the label. The arguments for get_graph_label() are the particular graph you want to add a label to and the text for the label. If you don’t specify an x-value and/or direction the label is placed at the end of the graph. The direction specifies where, relative to the x_value you want the label placed.

There are several other methods associated with the GraphScene() that are worth looking at, but I found the input_to_graph_point() helpful. By specifying an x-value on the graph, this method will return the coordinate on the screen where that graph point lies. This is handy if you want to place some text or other mobject to call out a particular point on a graph.

## 7.1 The CONFIG{} Dictionary

Whenever a scene or mobject are created a method called digest_config() gets called. This method starts with the class you defined and looks for a dictionary called self.CONFIG and compiles a list of all entries in the dictionary. It then goes to the parent class and looks for self.CONFIG there and adds those entries. If the method comes across keys that have already been found, it ignores the values from the parent class. digest_config() keeps traveling up the hierarchy to the top parent class, with is Container(). Each entry in this dictionary is then assigned a class variable based on the key and value. Thus the dictionary entry "x_min" : -1 becomes self.x_min = -1 and so on. Each dictionary entry becomes a class variable that can be accessed by the methods within the class. Understanding all of the CONFIG{} entries for a class is crucial to getting the most out of manim. For example, GraphScene() has the following CONFIG{} entries:

<br />class GraphScene(Scene):
CONFIG = {
"x_min": -1,
"x_max": 10,
"x_axis_width": 9,
"x_tick_frequency": 1,
"x_leftmost_tick": None, # Change if different from x_min
"x_labeled_nums": None,
"x_axis_label": "$x$",
"y_min": -1,
"y_max": 10,
"y_axis_height": 6,
"y_tick_frequency": 1,
"y_bottom_tick": None, # Change if different from y_min
"y_labeled_nums": None,
"y_axis_label": "$y$",
"axes_color": GREY,
"graph_origin": 2.5 * DOWN + 4 * LEFT,
"exclude_zero_label": True,
"num_graph_anchor_points": 25,
"default_graph_colors": [BLUE, GREEN, YELLOW],
"default_derivative_color": GREEN,
"default_input_color": YELLOW,
"default_riemann_start_color": BLUE,
"default_riemann_end_color": GREEN,
"area_opacity": 0.8,
"num_rects": 50,
}


The parent class for GraphScene() has the following dictionary:

class Scene(Container):
CONFIG = {
"camera_class": Camera,
"camera_config": {},
"frame_duration": LOW_QUALITY_FRAME_DURATION,
"construct_args": [],
"skip_animations": False,
"ignore_waits": False,
"write_to_movie": False,
"save_frames": False,
"save_pngs": False,
"pngs_mode": "RGBA",
"movie_file_extension": ".mp4",
"name": None,
"always_continually_update": False,
"random_seed": 0,
"start_at_animation_number": None,
"end_at_animation_number": None,
}


Container(), the parent to Scene as well as Mobject, has no CONFIG{} entries.

When talking about mobjects, the list of CONFIG{} entries can get a little long. I won’t go into those right now but it is worth you time to take a look at the hierarchy of some of the mobject subclasses to see what all the properties you can control are.

Next time we’ll take a look at more graphing.

Posted in Just for Fun, Programming | Tagged , , , | 3 Comments