-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathAnimation.html
More file actions
141 lines (104 loc) · 10.7 KB
/
Copy pathAnimation.html
File metadata and controls
141 lines (104 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html><head>
<title></title>
</head><body>
<div class="article">
<p>Your PlotDevice scripts can generate not only static images, but animations as well. Animations can be previewed on-screen then exported as QuickTime movies, animated GIFs or sequentially named image files. With the same syntax used for drawing, your script can become an animation doing all kinds of wonderful, lively things.</p>
<h1>Basic Animations</h1>
<p>For PlotDevice to recognize your script as an animation, simply <b>define a function</b> called <i>draw()</i> somewhere in your code. This function will be called repeatedly; once for each frame in the animation.</p>
<p>You can optionally call the <a href="../ref/Canvas.html#speed()">speed()</a> command with a frames-per-second value. This sets a target playback speed for showing your animation in the app's output pane. But depending on the speed of your system and the complexity of your code, the actual frame rate may well be lower.</p>
<p> To track the passage of time, read from the global <code class="kw">FRAME</code> variable. It starts at 1 and increments with each call to your <i>draw()</i> function. You'll likely want to keep more local state than this (and you can; see below), but it's surprising how much variety you can pull out of just the frame-number. In particular, try using the ever increasing <code class="kw">FRAME</code> with <a href="Geometry.html#trig">trigonometry</a> functions and watch how the oscillations evolve over time: <div class="media" style="margin-bottom:-20px;"><img height="391" src="../etc/tut/animation-example.png" width="721"/></div></p>
<p>The script begins normally, with a call to <i>size()</i>. After setting the animation <i>speed()</i> to 50 FPS we define a <i>draw()</i> function that does basically four things:</p>
<ol>
<li>Set the background color
</li><li>Set the fill, stroke, and pen styles
</li><li>Draw thirty rotated curves
</li><li>Use the math module's sine function to distort the curves fluidly
</li></ol>
<h2>Fluid movement</h2>
<p>Our <i>draw()</i> method makes use of the <i>sin()</i> function from the Python <code>math</code> module. A sine is an <i>oscillating</i> function that yields values from -1.0 to 1.0 (then back down to -1.0), but not in a linear fashion. When near to -1 or 1 the value changes slowly, then goes faster as it crosses 0. This is useful for animation that "eases" in or out.</p>
<p>To use the <i>sin()</i> function, import it from the Python math package along with the <i>radians()</i> function (sines work with multiples of <span style="font-style:italic; font-family:Georgia;">π</span>). Our <i>draw()</i> function passes the <code class="kw">FRAME</code> variable (as radians) to the <i>sin()</i> function then multiplies the sine by a differnt number for each curve. This produces a fluid distortion vector that eases each curve back and forth.</p>
<p>A sine function typically looks like this:</p>
<pre>from math import sin, radians
ease = sin(radians(x)) * y
</pre>
<h1>The Animation Lifecycle</h1>
<p>You'll notice that the TrigToy script calls <a href="../ref/Transform.html#rotate()">rotate()</a> repeatedly in the for loop and yet this doesn't carry over to subsequent frames. This is because after each of the calls to <i>draw()</i>, PlotDevice automatically clears the canvas and resets the color, transform, font, and compositing state (though the canvas size & background color will be preserved).</p>
<p>In addition to this housekeeping between frames, PlotDevice also provides hooks to allow your script to execute code once at the beginning of a ‘run’ and once at the end. You can opt-into this behavior by defining a <i>setup()</i> function to handle initialization and/or a <i>stop()</i> function to clean up after the user hits <span class="message">⌘.</span> to halt the animation.</p>
<p>Altogether, the sequence of a run looks something like:</p>
<ol>
<li>Execute all code at the script's top indentation-level</li>
<li>Call the <i>setup()</i> function (if defined)</li>
<li>Clear the canvas and reset the graphics state to defaults</li>
<li>Call the <i>draw()</i> function and plot its output to the screen</li>
<li>If user didn't press <span class="message">⌘.</span> goto #3</li>
<li>Call the <i>stop()</i> function (if defined)</li>
</ol>
<h2>Global state</h2>
<p>For all but the simplest animations you'll want a way to keep track of values that persist between calls to your <i>draw()</i> function. The most obvious solution to this problem would be to define global variables at the top of your script, then refer to them from your animation functions. If you do this, don't forget to use the Python <a href="https://docs.python.org/3/reference/simple_stmts.html#the-global-statement"><code>global</code></a> statement inside any of the functions that need to modify a variable. Otherwise you'll end up creating a local variable of the same name rather than updating the global:</p>
<pre>limit = 0
def setup():
global limit
limit = 100 # global is required for assignment to work
def draw():
global limit
limit -= 1 # likewise for value overwriting
def stop():
print(limit) # no need to use global here since we're only reading
</pre>
<h2>Animation state</h2>
<p>If the oddities of Python's global scope start getting you down, PlotDevice also offers an alternate way to share data between invocations of <i>setup()</i>, <i>draw()</i>, and <i>stop()</i>. If your function definitions include an argument, they will be passed a <a href="../ref/Misc.html#adict()">dictionary-like</a> object that persists throughout the run. You can name this object anything you like and access its fields using either dot-notation or traditional dictionary indexing.</p>
<p>This allows for a program-flow in which:</p>
<ol>
<li><i>setup()</i> populates the dictionary</li>
<li><i>draw()</i> uses those initial values and potentially updates them</li>
<li><i>stop()</i> can print out summary information based on the final values</li>
</ol>
<p>Here's an example that draws random dots for a fixed duration of frames each. It keeps track of the current "population" of dots in a persistent dictionary called <code>anim</code>, though of course we could give this any name we like.</p>
<p>With each call to <i>draw()</i> a new random dot location and color are added to a persistent list called <code>anim.dots</code>, then a circle is drawn for each dot in the list. Dots will gradually fade over the course of their lifetimes as we incrementally turn down the <i>alpha</i> in the saved <a href="Color.html">Color</a> objects.</p>
<p>
If <i>draw()</i> has been called enough times that the list is longer than the <code>anim.limit</code> chosen in <i>setup()</i>, elements are dropped off the front until it's the right length.</p>
<pre>speed(30)
def setup(anim):
anim.dots = []
anim.limit = 100
def draw(anim):
x, y = random(WIDTH), random(HEIGHT)
dotcolor = color(HSV, random(), .6, .9)
anim.dots.append([x, y, dotcolor])
for x, y, clr in anim.dots:
clr.alpha -= 1.0/anim.limit
fill(clr)
arc(x, y, 20)
anim.dots = anim.dots[-anim.limit:]
def stop(anim):
print("Final population:", len(anim.dots))
</pre>
<div class="media"><img height="516" src="../etc/tut/animation-state.png" width="716"/></div>
<p>Since this run was halted after 86 frames, the <i>stop()</i> function reports having an identical number of items in its <code>dots</code> list. What would you expect it to print out if the animation ran for 100 frames? What about 101 or 1,000,000?</p>
<h2>Bailing out</h2>
<p>PlotDevice animations typically run in an open-ended fashion – new frames will be generated until you hit <span class="message">⌘.</span> to end the run. You'll occassionally want more direct control over this, particularly when debugging. There are a couple of ways to prevent an animation from galloping away from you.</p>
<p>The first is a special case of the <i>speed()</i> command. If you call it with an FPS of zero, only a single frame will be drawn before the animation halts itself:</p>
<pre>speed(0)</pre>
<p>The second way to cut off an animation is to call the <a href="../ref/Canvas.html#halt()">halt()</a> command from within your <i>draw()</i> method. For instance, here's how you'd limit an animation to a dozen frames:</p>
<pre>def draw():
... # normal drawing code
if FRAME==12:
halt()</pre>
<h2>Tips for speed optimization</h2>
<ul>
<li>If you plan to use a lot of lines in your animation, group them into a single path by using the <a href="../ref/Drawing.html#bezier()">bezier()</a> command as part of a <code>with</code> statement and calling <a href="../ref/Primitives.html#line()">line()</a> or <a href="../ref/Drawing.html#lineto()">lineto()</a> & <a href="../ref/Drawing.html#moveto()">moveto()</a> within the block. A single big path renders faster than many small
paths. But keep in mind that all the lines in the path will have the same stroke color and width.
</li><li>If you plan to use a lot of text in your animation, create cached <a href="../ref/Typography.html#textpath()">textpath()</a> versions in your <i>setup()</i> routine and
<a href="../ref/Transform.html#translate()">translate()</a> those when drawing instead of
repeatedly calling <a href="../ref/Primitives.html#text()">text()</a> in the <i>draw()</i> command.
</li><li>Don't get too irrationally exuberant with the <i>speed()</i> command. Cranking it up is often counterproductive since PlotDevice will attempt to update the screen faster than the hardware can support. In particular, if you're not using a CRT there's basically no reason to go above 60 Hz.
</li><li>Tune the <a href="../ref/Canvas.html#size()">size()</a> of the canvas to exactly what you
need – a smaller canvas means less screen to refresh.<br/>
</li></ul>
<h1>Exporting an Animation</h1>
<p>In addition to exporting frames as static images, you can export an animation as an H.264-encoded Quicktime movie or an animated GIF. Select the <span class="message">File → Export as Movie...</span> menu or press <span class="message">⇧⌘P</span>. You can specify a number of frames to export and
a frame rate (which will override whatever you set with <i>speed()</i>). Video exports also allow you to set a target bitrate to control compression quality. Animated GIFs will loop if the checkbox is selected.
<div class="media" style="margin:0"><img height="435" src="../etc/tut/animation-export.png" width="578"/></div></p>
</div>
</body></html>