Tutorial: The AnAnimLib Logo
One of the best ways to learn a new library is to do a project. This tutorial is a trip through the way that I think about creating animations. As such, it is only one possible work flow and I am certain that this library could be twisted in a variety of vile and perverse ways that I am completely unaware of. If you know of any, please share them! At any rate, what follows is how I work.
Caution
Tutorials (this one included) are intened to introduce new users to a library by by leading them through a fully formed solution to a prototypical problem. This approach, while logical, unintentionally presents said solution as though it sprang into existance fully formed, a fact that anyone who builds things knows is absurd.
As my great grandaddy, a famed SuperWidget builder, would say to anyone who would listen: “The only way to master SuperWidget building is to fail at building a lot of SuperWidgets.” Then he’d brag about the number fingers he lost in various SuperWidget accidents. You know. To show how many times he failed.
Or, as my friends in the Fortnight forums say when asked how to win a Battle Royale: “Git gud!” which translates roughly to: “Play a bunch. Die a bunch. Then, figure out how to not die so much. Then… Die less.” You know. Lose a finger.
Grand Daddy and the Fortnight trolls are saying the same thing: The path to mastery is through failure. Along the way, you might lose a finger. You might might even get some bad feels because a mean troll keeps killing you. And that’s just fine. Every lost finger or death by obnoxious adolescent is a lesson learned. Or something. Great Grandad would know.
The point is, nobody writes about the failures because we would look foolish! Some brave soul should rise to the occasion and write an honest tutorial that highlights the lessons learned at the absolute lowest point in the project cycle, because that is where all of the learning happened. I am not that person. So, let us put on our rose colored glasses and have a look.
Part 1: SlideyBox
As a thought exercise, look at the AnAnimLib logo as if you were simply imagining it and wanted to build it. It certainly started life that way.
Looking at the animation now, the hypnotic motion of the little box as it rotates and rides the curve has my attention. The rotation itself is mundane, but the behavior of the two vectors (arrows) is interesting. The upwards pointing vector rotates with the box but the lower vector always points down. How can we make that happen?
Being a physicist, I tend to hit the problem very hard until it breaks into the smallest and simplest possible pieces. In this case, my smashing arrived at: “Can I draw a box on the canvas and control its color?” (It was originally just “Can I draw a box on the canvas?” but we coverd that in the Quickstart!)
1
2 import ananimlib as al
3
4 box = al.Rectangle(
5 size = [1.0,1.0],
6 pen = al.Pen(
7 stroke_color = "#FFFFFF",
8 stroke_opacity = 1.0,
9 stroke_width = 3.0,
10 fill_color = "#cda448",
11 fill_opacity = 1.0
12 )
13 )
14
15 al.Animate(
16 al.AddAnObject(box),
17 al.Wait(1.0)
18 )
Here, we create a Rectangle on Line 4 and pass it a Pen object, which allows us to adjust its fill color, opacity, and outline (stroke) width. Our call to Animate has only two instructions, AddAnObject to put the rectangle in the scene and Wait to display it for one second.
Great. Now, we make it rotate.
1
2 import math
3 import ananimlib as al
4
5 box = al.Rectangle(
6 size = [1.0,1.0],
7 pen = al.Pen(
8 stroke_color = "#FFFFFF",
9 stroke_opacity = 1.0,
10 stroke_width = 3.0,
11 fill_color = "#cda448",
12 fill_opacity = 1.0
13 )
14 )
15
16 wave_angle=60*math.pi/180
17
18 al.Animate(
19 al.AddAnObject(box),
20 al.Rotate(box, wave_angle/2, duration=0.25),
21 al.Rotate(box, -wave_angle, duration=0.5),
22 al.Rotate(box, wave_angle/2, duration=0.25),
23 al.Wait(1.0)
24 )
I wanted it to kind of wave at me, so there are three calls to Rotate, one for each time it switches directions.
Next, we add an upwards pointing arrow and a downwards pointing arrow. The upper arrow should rotate with the box, but the lower arrow should remain staionary. Here is our rough draft.
1
2 import math
3 import ananimlib as al
4
5
6
7 box = al.Rectangle(
8 size = [1.0,1.0],
9 pen = al.Pen(
10 stroke_color = "#FFFFFF",
11 stroke_opacity = 1.0,
12 stroke_width = 2.0,
13 fill_color = "#cda448",
14 fill_opacity = 1.0
15 )
16 )
17
18 down_arrow = al.Arrow(tail_pos = [0,-0.5],
19 head_pos = [0,-1.5],
20 head_size = 0.75,
21 pen = al.Pen(stroke_width=2.0))
22
23 up_arrow = al.Arrow(tail_pos = [0,0.5],
24 head_pos = [0,1.5],
25 head_size = 0.75,
26 pen = al.Pen(stroke_width=2.0))
27
28
29 wave_angle=60*math.pi/180
30
31
32 al.Animate(
33 al.AddAnObject(box),
34 al.AddAnObject(down_arrow),
35 al.AddAnObject(up_arrow),
36 al.RunParallel(
37 al.RunSequential(
38 al.Rotate(box, wave_angle/2, duration=0.25),
39 al.Rotate(box, -wave_angle, duration=0.5),
40 al.Rotate(box, wave_angle/2, duration=0.25),
41 ),
42 al.RunSequential(
43 al.Rotate(up_arrow, wave_angle/2, duration=0.25),
44 al.Rotate(up_arrow, -wave_angle, duration=0.5),
45 al.Rotate(up_arrow, wave_angle/2, duration=0.25),
46 )
47 ),
48 al.Wait(1.0)
49 )
The arrows are in the scene, but the rotation on the top arrow isn’t correct. Looking closely at the animation, the arrow is rotating about its own tail while the box is rotating about its center. To make our rotations look correct, we need to adjust the about point of the upper arrow so that it coincides with the center of the box. I’m also bugged by the tail of the lower arrow overlapping the box, it just doesn’t look clean. We can change which object appears on top by changing the order in which they are added to the scene.
Those are minor issues. What really bothers me about the code above is the repetition. The two blocks starting at lines 37 and 42 are nearly identical and need to be broken out of the main body and turned into separate instructions. To fix it, we simply define a function called wave that returns the results of the RunSequential call.
1
2 import math
3 import ananimlib as al
4
5 wave_angle=60 # Wave angle in degrees
6 wave_angle *= math.pi/180 # Convert to radians
7
8 # Custom instruction to "wave" an AnObject
9 def wave(key,wave_angle,duration):
10 return al.RunSequential(
11 al.Rotate(key, wave_angle/2, duration = duration/4),
12 al.Rotate(key, -wave_angle, duration = duration/2),
13 al.Rotate(key, wave_angle/2, duration = duration/4),
14 )
15
16 # The gold colored box
17 box = al.Rectangle(
18 size = [1.0,1.0],
19 pen = al.Pen(
20 stroke_color = "#FFFFFF",
21 stroke_opacity = 1.0,
22 stroke_width = 2.0,
23 fill_color = "#cda448",
24 fill_opacity = 1.0
25 )
26 )
27
28 # The arrows
29 down_arrow = al.Arrow(tail_pos = [0,-0.5],
30 head_pos = [0,-1.5],
31 head_size = 0.75,
32 pen = al.Pen(stroke_width=2.0))
33
34 up_arrow = al.Arrow(tail_pos = [0,0.5],
35 head_pos = [0,1.5],
36 head_size = 0.75,
37 pen = al.Pen(stroke_width=2.0))
38
39 # Fix the up-arrow about point so that it rotates with the box.
40 up_arrow.about_point -= [0.5,0.0,0.0]
41
42
43
44 al.Animate(
45 al.AddAnObject(down_arrow),
46 al.AddAnObject(up_arrow),
47 al.AddAnObject(box),
48 al.RunParallel(
49 wave(box , wave_angle, duration = 1.0),
50 wave(up_arrow, wave_angle, duration = 1.0)
51 ),
52 al.Wait(1.0)
53 )
54
The output looks pretty solid. The issue now (for me anyway) is that the main body of the script is over 50 lines and I don’t like thinking about that many lines of code at one time. Also, we want to make that little box with its attendant arrows slide down a hill while it rotates, which is going to be tough with the code written the way it is. Time for some restructing before things get out of hand. Here are my changes:
1import math
2import ananimlib as al
3
4def tutorial_snip5():
5
6 wave_angle=60 # Wave angle in degrees
7 wave_angle *= math.pi/180 # Convert to radians
8
9 box = SlideyBox()
10
11 al.Animate(
12 al.AddAnObject(box),
13 wave_attr(box, 'slope', math.pi/4,duration=1.0),
14 )
15
16
17class SlideyBox(al.CompositeAnObject):
18 """Creates a block with a gravitational vector and a normal vector
19 """
20
21 def __init__(self):
22 super().__init__()
23
24 # The gold colored box
25 box = al.Rectangle(
26 size = [1.0,1.0],
27 pen = al.Pen(
28 stroke_color = "#FFFFFF",
29 stroke_opacity = 1.0,
30 stroke_width = 2.0,
31 fill_color = "#cda448",
32 fill_opacity = 1.0
33 )
34 )
35
36 # The arrows
37 down_arrow = al.Arrow(tail_pos = [0,-0.5],
38 head_pos = [0,-1.5],
39 head_size = 0.75,
40 pen = al.Pen(stroke_width=2.0))
41
42 up_arrow = al.Arrow(tail_pos = [0,0.5],
43 head_pos = [0,1.5],
44 head_size = 0.75,
45 pen = al.Pen(stroke_width=2.0))
46
47 # Fix the up-arrow about point so that it rotates with the box.
48 up_arrow.about_point -= [0.5,0.0,0.0]
49
50 self.add_anobject(down_arrow)
51 self.add_anobject(up_arrow,'up_arrow')
52 self.add_anobject(box,'box')
53
54 self.slope=0
55
56 @property
57 def slope(self):
58 return self._slope
59
60
61 @slope.setter
62 def slope(self, new_slope):
63 self._slope = new_slope
64
65 # Change the rotation angle of the up arrow and the box
66 self.get_anobject('box').rotation_angle = new_slope
67 self.get_anobject('up_arrow').rotation_angle = new_slope+math.pi/2
68
69
70# Custom instruction to "wave" an attribute
71def wave_attr(key, attribute, wave_distance, duration):
72 """Waves an attribute back and forth """
73
74 return al.RunSequential(
75 al.SlideAttribute(key, attribute, wave_distance/2,
76 duration = duration/4, relative=True),
77 al.SlideAttribute(key, attribute, -wave_distance ,
78 duration = duration/2, relative=True),
79 al.SlideAttribute(key, attribute, wave_distance/2,
80 duration = duration/4, relative=True),
81 )
82
83def wave(key,wave_angle,duration):
84 return wave_attr(key,'rotation_angle',wave_angle,duration=duration)
85
This program (it has functions and classes and stuff so it’s not just a “script” anymore) has identical output, but we made a lot of structural changes.
On Lines 4-15 we have created a function called tutorial_snip5 as the entry point to our animation. It follows the familiar pattern of creating some assets and then animating them with a call to Animate, but it only creates one AnObject called SlideyBox and the call to Animate has only three instructions.
Starting on Line 18, we define the new SlideyBox class. It is a CompositeAnObject, which is essentially a container that holds a set of one or more other AnObjects, any of which could also be a CompositeAnobject. All of the setup code from our previous example is in SlideyBox.__init__. In Lines 51-53 the arrows and the box get added to the composite. Then, they automatically get added to the scene when the composite itself is added.
In tutorial_snip, we create only one AnObject called SlideyBox and the call to Animate has only three Instructions, two of which are the familiar AddAnObject and Wait instructions and a third called wave_attr.
The definition of SlideyBox begins on Line 18.
1
2import math
3import ananimlib as al
4
5def tutorial_snip6():
6
7 wave_angle=60 # Wave angle in degrees
8 wave_angle *= math.pi/180 # Convert to radians
9
10 box = SlideyBox()
11 box.position=[2,0]
12
13
14 al.Animate(
15 al.AddAnObject(box),
16 al.RunParallel(
17 wave_attr(box, 'slope', math.pi/4,duration=2.0),
18 al.RunSequential(
19 al.Move(box, [-4, 0],duration=1.0),
20 al.Move(box, [ 4, 0],duration=1.0)
21 )
22 )
23 )
24