| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254 |
- Implementing a drawing program provides a very good example on the
- usefulness of object-oriented programming. In the following we shall
- develop the simpler parts of a relatively small and compact drawing
- program for making sketches of the type shown in
- Figure ref{sketcher:fig:inclinedplane}. This is a typical principal sketch of
- a physics problem, here involving a rolling wheel on an inclined
- plane. This sketch is made up many individual elements: a rectangle
- filled with a pattern (the inclined plane), a hollow circle with color
- (the wheel), arrows with label (the $N$ and $Mg$ forces, and the $x$
- axis), an angle with symbol $\theta$, and a dashed line indicating the
- starting location of the wheel. Drawing software and plotting
- programs can produce such figures quite easily in principle, but the
- amount of details the user needs to control with the mouse can be
- substantial. Software more tailored to producing sketches of this type
- would work with more convenient abstractions, such as circle, wall,
- angle, force arrow, axis, and so forth.
- FIGURE: [figs-sketcher/wheel_on_inclined_plane.png, width=500] Sketch of a physics problem. label{sketcher:fig:inclinedplane}
- Classes are very suitable for implementing the various components that
- build up a sketch and their functionality. In particular, we shall
- demonstrate that as soon some classes are established, more are easily
- added. Enhanced functionality for all the classes is also easy to
- implement in common, generic code that can immediately be shared by all
- present and future classes.
- ===== Using the Object Collection =====
- We start by demonstrating a convenient user interface for making
- sketches of the type in Figure ref{sketcher:fig1}. However, it is more
- appropriate to start with a significantly simpler example as depicted
- in Figure ref{sketcher:fig:vehicle0}. This toy sketch consists of
- several elements: two circles, two rectangles, and a "ground" element.
- FIGURE: [figs-sketcher/vehicle0_dim.png, width=400] Sketch of a simple figure. label{sketcher:fig:vehicle0}
- === Basic Drawing ===
- A typical program creating these five elements is shown next.
- After importing the `pysketcher` package, the first task is always to
- define a coordinate system. Some graphics operations are done with
- a helper object called `drawing_tool` (imported from `pysketcher`).
- With the drawing area in place we can make the first `Circle` object
- in an intuitive fashion:
- !bc pycod
- from pysketcher import *
- R = 1 # radius of wheel
- L = 4 # distance between wheels
- H = 2 # height of vehicle body
- w_1 = 5 # position of front wheel
- drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
- ymin=-1, ymax=2*R + 3*H)
- wheel1 = Circle(center=(w_1, R), radius=R)
- !ec
- By using symbols for characteristic lengths in the drawing, rather than
- absolute lengths, it is easier
- to change dimensions later.
- To translate the geometric information about the `wheel1` object to
- instructions for the plotting engine (in this case Matplotlib), one calls the
- `wheel1.draw()`. To display all drawn objects, one issues
- `drawing_tool.display()`. The typical steps are hence:
- !bc pycod
- wheel1 = Circle(center=(w_1, R), radius=R)
- wheel1.draw()
- # Define other objects and call their draw() methods
- drawing_tool.display()
- drawing_tool.savefig('tmp.png') # store picture
- !ec
- The next wheel can be made by taking a copy of `wheel1` and
- translating the object a distance (to the right) described by the
- vector $(4,0)$:
- !bc pycod
- wheel2 = wheel1.copy()
- wheel2.translate((L,0))
- !ec
- The two rectangles are made in an intuitive way:
- !bc pycod
- under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
- width=2*R + L + 2*R, height=H)
- over = Rectangle(lower_left_corner=(w_1, 2*R + H),
- width=2.5*R, height=1.25*H)
- !ec
- === Groups of Objects ===
- Instead of calling the `draw` method of every object, we can
- group objects and call `draw`, or perform other operations, for
- the whole group. For example, we may collect the two wheels
- in a `wheels` group and the `over` and `under` rectangles
- in a `body` group. The whole vehicle is a composition
- of its `wheels` and `body` groups. The codes goes like
- !bc pycod
- wheels = Composition({'wheel1': wheel1, 'wheel2': wheel2})
- body = Composition({'under': under, 'over': over})
- vehicle = Composition({'wheels': wheels, 'body': body})
- !ec
- The ground is illustrated by an object of type `Wall`,
- mostly used to indicate walls in sketches of mechanical systems.
- A `Wall` takes the `x` and `y` coordinates of some curve,
- and a `thickness` parameter, and creates a "thick" curve filled
- with a simple pattern. In this case the curve is just a flat
- line so the construction is made of two points on the
- ground line ($(w_1-L,0)$ and $(w_1+3L,0)$):
- !bc pycod
- ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
- !ec
- The negative thickness makes the pattern-filled rectangle appear below
- the defined line, otherwise it appears above.
- We may now collect all the objects in a "top" object that contains
- the whole figure:
- !bc pycod
- fig = Composition({'vehicle': vehicle, 'ground': ground})
- fig.draw() # send all figures to plotting backend
- drawing_tool.display()
- drawing_tool.savefig('tmp.png')
- !ec
- The `fig.draw()` call will visit
- all subgroups, their subgroups,
- and so in the herarchical tree structure that we have collected,
- and call `draw` for every object.
- === Changing Line Styles and Colors ===
- Controlling the line style, line color, and line width is
- fundamental when designing figures. The `pysketcher`
- package allows the user to control such properties in
- single objects, but also set global properties that are
- used if the object has no particular specification of
- the properties. Setting the global properties are done like
- !bc pycod
- drawing_tool.set_linestyle('dashed')
- drawing_tool.set_linecolor('black')
- drawing_tool.set_linewidth(4)
- !ec
- At the object level the properties are specified in a similar
- way:
- !bc pycod
- wheel1.set_linestyle('solid')
- wheel1.set_linecolor('red')
- !ec
- and so on.
- Geometric figures can be specified as *filled*, either with a color or with a
- special visual pattern:
- !bc
- # Set filling of all curves
- drawing_tool.set_filled_curves(color='blue', pattern='/')
- # Turn off filling of all curves
- drawing_tool.set_filled_curves(False)
- # Fill the wheel with red color
- wheel1.set_filled_curves('red')
- !ec
- # http://packages.python.org/ete2/ for visualizing tree structures!
- === The Figure Composition as an Object Hierarchy ===
- The composition of objects is hierarchical, as in a family, where
- each object has a parent and a number of children. Do a
- `print fig` to display the relations:
- !bc dat
- ground
- wall
- vehicle
- body
- over
- rectangle
- under
- rectangle
- wheels
- wheel1
- arc
- wheel2
- arc
- !ec
- The indentation reflects how deep down in the hierarchy (family)
- we are.
- This output is to be interpreted as follows:
- * `fig` contains two objects, `ground` and `vehicle`
- * `ground` contains an object `wall`
- * `vehicle` contains two objects, `body` and `wheels`
- * `body` contains two objects, `over` and `under`
- * `wheels` contains two objects, `wheel1` and `wheel2`
- More detailed information can be printed by
- !bc pycod
- print fig.show_hierarchy('std')
- !ec
- yielding the output
- !bc dat
- ground (Wall):
- wall (Curve): 4 coords fillcolor='white' fillpattern='/'
- vehicle (Composition):
- body (Composition):
- over (Rectangle):
- rectangle (Curve): 5 coords
- under (Rectangle):
- rectangle (Curve): 5 coords
- wheels (Composition):
- wheel1 (Circle):
- arc (Curve): 181 coords
- wheel2 (Circle):
- arc (Curve): 181 coords
- !ec
- Here we can see the class type for each figure object, how many
- coordinates that are involved in basic figures (`Curve` objects), and
- special settings of the basic figure (fillcolor, line types, etc.).
- For example, `wheel2` is a `Circle` object consisting of an `arc`,
- which is a `Curve` object consisting of 181 coordinates (the
- points needed to draw a smooth circle). The `Curve` objects are the
- only objects that really holds specific coordinates to be drawn.
- The other object types are just compositions used to group
- parts of the complete figure.
- One can also get a graphical overview of the hiearchy of figure objects
- that build up a particular figure `fig`.
- Just call `fig.graphviz_dot('fig')` to produce a file `fig.dot` in
- the *dot format*. This file contains relations between parent and
- child objects in the figure and can be turned into an image,
- as in Figure ref{sketcher:fig:vehicle0:hier1}, by
- running the `dot` program:
- !bc sys
- Terminal> dot -Tpng -o fig.png fig.dot
- !ec
- FIGURE: [figs-sketcher/vehicle0_hier1.png, width=400] Hierarchical relation between figure objects. label{sketcher:fig:vehicle0:hier1}
- The call `fig.graphviz_dot('fig', classname=True)` makes a `fig.dot` file
- where the class type of each object is also visible, see
- Figure ref{sketcher:fig:vehicle0:hier2}. The ability to write out the
- object hierarchy or view it graphically can be of great help when
- working with complex figures that involve layers of subfigures.
- FIGURE: [figs-sketcher/vehicle0_hier1.png, width=400] Hierarchical relation between figure objects, including their class names. label{sketcher:fig:vehicle0:hier2}
- Any of the objects can in the program be reached through their names, e.g.,
- !bc pycodc
- fig['vehicle']
- fig['vehicle']['wheels']
- fig['vehicle']['wheels']['wheel2']
- fig['vehicle']['wheels']['wheel2']['arc']
- fig['vehicle']['wheels']['wheel2']['arc'].x # x coords
- fig['vehicle']['wheels']['wheel2']['arc'].y # y coords
- fig['vehicle']['wheels']['wheel2']['arc'].linestyle
- fig['vehicle']['wheels']['wheel2']['arc'].linetype
- !ec
- Grabbing a part of the figure this way is very handy for
- changing properties of that part, for example, colors, line styles
- (see Figure ref{sketcher:fig:vehicle0:v2}):
- !bc pycod
- fig['vehicle']['wheels'].set_filled_curves('blue')
- fig['vehicle']['wheels'].set_linewidth(6)
- fig['vehicle']['wheels'].set_linecolor('black')
- fig['vehicle']['body']['under'].set_filled_curves('red')
- fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
- fig['vehicle']['body']['over'].set_linewidth(14)
- fig['vehicle']['body']['over']['rectangle'].linewidth = 4
- !ec
- The last line accesses the `Curve` object directly, while the line above,
- accesses the `Rectangle` object which will then set the linewidth of
- its `Curve` object, and other objects if it had any.
- The result of the actions above is shown in Figure ref{sketcher:fig:vehicle0:v2}.
- FIGURE: [figs-sketcher/vehicle0.png, width=700] Left: Basic line-based drawing. Right: Thicker lines and filled parts. label{sketcher:fig:vehicle0:v2}
- We can also change position of parts of the figure and thereby make
- animations, as shown next.
- === Animation: Translating the Vehicle ===
- Can we make our little vehicle roll? A first attempt will be to
- fake rolling by just displacing all parts of the vehicle.
- The relevant parts constitute the `fig['vehicle']` object.
- This part of the figure can be translated, rotated, and scaled.
- A translation along the ground means a translation in $x$ direction,
- say a length $L$ to the right:
- !bc pycod
- fig['vehicle'].translate((L,0))
- !ec
- You need to erase, draw, and display to see the movement:
- !bc pycod
- drawing_tool.erase()
- fig.draw()
- drawing_tool.display()
- !ec
- Without erasing the old position of the vehicle will remain in
- the figure so you get two vehicles. Without `fig.draw()` the
- new coordinates of the vehicle will not be communicated to
- the drawing tool, and without calling dislay the updated
- drawing will not be visible.
- Let us make a velocity function and move the object according
- to that velocity in small steps of time:
- !bc pydoc
- def v(t):
- return -8*R*t*(1 - t/(2*R))
- animate(fig, tp, action)
- !ec
- For small time steps `dt` the corresponding displacement is
- well approximated by `dt*v(t)` (we could integrate the velocity
- to obtain the exact position, but we would anyway need to
- calculate the displacement from time step to time step).
- The `animate` function takes as arguments some figure `fig`, a set of
- time points `tp`, and a user function `action`,
- and then a new figure is drawn for each time point and the user
- can through the provided `action` function modify desired parts
- of the figure. Here the `action` function will move the vehicle:
- !bc pycod
- def move_vehicle(t, fig):
- x_displacement = dt*v(t)
- fig['vehicle'].translate((x_displacement, 0))
- !ec
- Defining a set of time points for the frames in the animation
- and performing the animation is done by
- !bc pycod
- import numpy
- tp = numpy.linspace(0, 2*R, 25)
- dt = tp[1] - tp[0] # time step
- animate(fig, tp, move_vehicle, pause_per_frame=0.2)
- !ec
- The `pause_per_frame` adds a pause, here 0.2 seconds, between
- each frame.
- We can also make a movie file of the animation:
- !bc pycod
- files = animate(fig, tp, move_vehicle, moviefiles=True,
- pause_per_frame=0.2)
- !ec
- The `files` variable holds a string with the family of
- files constituting the frames in the movie, here
- `'tmp_frame*.png'`. Making a movie out of the individual
- frames can be done in many ways.
- A simple approach is to make an animated GIF file with help of
- `convert`, a program in the ImageMagick software suite:
- !bc sys
- Terminal> convert -delay 20 tmp_frame*.png anim.gif
- Terminal> animate anim.gif # play movie
- !ec
- The delay between frames governs the speed of the movie.
- The `anim.gif` file can be embedded in a web page and shown as
- a movie the page is loaded into a web browser (just insert
- `<img src="anim.gif">` in the HTML code to play the GIF animation).
- The tool `ffmpeg` can alternatively be used, e.g.,
- !bc sys
- Terminal> ffmpeg -i "tmp_frame_%04d.png" -b 800k -r 25 \
- -vcodec mpeg4 -y -qmin 2 -qmax 31 anim.mpeg
- !ec
- An easy-to-use interface to movie-making tools is provided by the
- SciTools package:
- !bc pycod
- from scitools.std import movie
- # HTML page showing individual frames
- movie(files, encoder='html', output_file='anim.html')
- # Standard GIF file
- movie(files, encoder='convert', output_file='anim.gif')
- # AVI format
- movie('tmp_*.png', encoder='ffmpeg', fps=4,
- output_file='anim.avi') # requires ffmpeg package
- # MPEG format
- movie('tmp_*.png', encoder='ffmpeg', fps=4,
- output_file='anim2.mpeg', vodec='mpeg2video')
- # or
- movie(files, encoder='ppmtompeg', fps=24,
- output_file='anim.mpeg') # requires the netpbm package
- !ec
- When difficulties with encoders and players arise, the simple
- web page for showing a movie, here `anim.html` (generated by the
- first `movie` command above), is a safe method that you always
- can rely on.
- You can try loading `anim.html` into a web browser, after first
- having run the present example in the file
- # #ifdef PRIMER_BOOK
- `vehicle0.py`.
- # #else
- "`vehicle0.py`": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle0.py". Alternatively, you can view a ready-made "movie": "http://hplgit.github.com/pysketcher/doc/src/sketcher/movies-sketcher/anim.html_vehicle0/anim.html".
- # #endif
- === Animation: Rolling the Wheels ===
- It is time to show rolling wheels. To this end, we make somewhat
- more complicated wheels with spokes as on a bicyle, formed by
- two crossing lines, see Figure ref{sketcher:fig:vehicle1}.
- The construction of the wheels will now involve a circle
- and two lines:
- !bc pycod
- wheel1 = Composition({
- 'wheel': Circle(center=(w_1, R), radius=R),
- 'cross': Composition({'cross1': Line((w_1,0), (w_1,2*R)),
- 'cross2': Line((w_1-R,R), (w_1+R,R))})})
- wheel2 = wheel1.copy()
- wheel2.translate((L,0))
- !ec
- Observe that `wheel1.copy()` copies all the objects that make
- up the first wheel, and `wheel2.translate` translates all
- the copied objects.
- FIGURE: [figs-sketcher/vehicle1.png, width=400] Wheels with spokes to show rotation. label{sketcher:fig:vehicle1}
- The `move_vehicle` function now needs to displace all the objects in the
- entire vehicle and also rotate the `cross1` and `cross2`
- objects in both wheels.
- The rotation angle follows from the fact that the arc length
- of a rolling wheel equals the displacement of the center of
- the wheel, leading to a rotation angle
- !bc pycod
- angle = - x_displacement/R
- !ec
- With `w_1` tracking the $x$ coordinate of the center
- of the front wheel, we can rotate that wheel by
- !bc pycod
- w1 = fig['vehicle']['wheels']['wheel1']
- from math import degrees
- w1.rotate(degrees(angle), center=(w_1, R))
- !ec
- The `rotate` function takes two parameters: the rotation angle
- (in degrees) and the center point of the rotation, which is the
- center of the wheel in this case. The other wheel is rotated by
- !bc pycod
- w2 = fig['vehicle']['wheels']['wheel2']
- w2.rotate(degrees(angle), center=(w_1 + L, R))
- !ec
- That is, the angle is the same, but the rotation point is different.
- The update of the center point is done by `w_1 += displacement[0]`.
- The complete `move_vehicle` function then becomes
- !bc pycod
- w_1 = w_1 + L # start position
- def move_vehicle(t, fig):
- x_displacement = dt*v(t)
- fig['vehicle'].translate((x_displacement, 0))
- # Rotate wheels
- global w_1
- w_1 += x_displacement
- # R*angle = -x_displacement
- angle = - x_displacement/R
- w1 = fig['vehicle']['wheels']['wheel1']
- w1.rotate(degrees(angle), center=(w_1, R))
- w2 = fig['vehicle']['wheels']['wheel2']
- w2.rotate(degrees(angle), center=(w_1 + L, R))
- !ec
- The complete example is found in the file
- # #ifdef PRIMER_BOOK
- `vehicle1.py`.
- # #else
- "`vehicle1.py`": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle1.py". You may run this file or watch a "ready-made movie": "http://hplgit.github.com/pysketcher/doc/src/sketcher/movies-sketcher/anim.html_vehicle1/anim.html".
- # #endif
- The advantages with making figures this way through programming,
- rather than using interactive drawing programs, are numerous. For
- example, the objects are parameterized by variables so that various
- dimensions can easily be changed. Subparts of the figure, possible
- involving a lot of figure objects, can change
- color, linetype, filling or other properties through a *single* function
- call. Subparts of the figure can be rotated, translated, or scaled.
- Subparts of the figure can also be copied and moved to other parts of the
- drawing area. However, the single most important feature is probably
- the ability to make animations governed by mathematical formulas or
- data coming from physics simulations of the problem sketched in
- the drawing, as very simplistically shown in the example above.
- ===== Example of Classes for Geometric Objects =====
- We shall now explain how we can, quite easily, realize software
- with the capabilities demonstrated above. Each object in the
- figure is represented as a class in a class hierarchy. Using
- inheritance, classes can inherit properties from parent classes
- and add new geometric features.
- We introduce class `Shape` as superclass for all specialized objects
- in a figure. This class does not store any data, but provides a
- series of functions that add functionality to all the subclasses.
- This will be shown later.
- === Simple Geometric Objects ===
- One simple subclass is `Rectangle`:
- !bc pycod
- class Rectangle(Shape):
- def __init__(self, lower_left_corner, width, height):
- p = lower_left_corner # short form
- x = [p[0], p[0] + width,
- p[0] + width, p[0], p[0]]
- y = [p[1], p[1], p[1] + height,
- p[1] + height, p[1]]
- self.shapes = {'rectangle': Curve(x,y)}
- !ec
- Any subclass of `Shape` will have a constructor which takes
- geometric information about the shape of the object and
- creates a dictionary `self.shapes` with the shape built of
- simpler shapes. The most fundamental shape is `Curve`, which is
- just a collection of $(x,y)$ coordinates in two arrays `x` and `y`.
- Drawing the `Curve` object is a matter of plotting `y` versus `x`.
- The `Rectangle` class illustrates how the constructor takes information
- about the lower left corner, the width and the height, and
- creates coordinate arrays `x` and `y` consisting of the four corners,
- plus the first one repeated such that plotting `x` and `y` will
- form a closed four-sided rectangle. This construction procedure
- demands that the rectangle will always be aligned with the $x$ and
- $y$ axis. However, we may easily rotate the rectangle about
- any point once the object is constructed.
- Class `Line` constitutes a similar example:
- !bc pycod
- class Line(Shape):
- def __init__(self, start, end):
- x = [start[0], end[0]]
- y = [start[1], end[1]]
- self.shapes = {'line': Curve(x, y)}
- !ec
- Here we only need two points, the start and end point on the line.
- However, we may add some useful functionality, e.g., the ability
- to give an $x$ coordinate and have the class calculate the
- corresponding $y$ coordinate:
- !bc pycod
- def __call__(self, x):
- """Given x, return y on the line."""
- x, y = self.shapes['line'].x, self.shapes['line'].y
- self.a = (y[1] - y[0])/(x[1] - x[0])
- self.b = y[0] - self.a*x[0]
- return self.a*x + self.b
- !ec
- Unfortunately, this is too simplistic because vertical lines cannot
- be handled (infinte `self.a`). The source code of `Line` therefore
- provides a more general solution at the cost of significantly
- longer code with more tests.
- A circle gives us somewhat increased complexity. Again we represent
- the geometic object by a `Curve` object, but this time the `Curve`
- object needs to store a large number of points on the curve such
- that a plotting program produces a visually smooth curve.
- The points on the circle must be calculated manually in the constructor
- of class `Circle`. The formulas for points $(x,y)$ on a curve with radius
- $R$ and center at $(x_0, y_0)$ are given by
- !bt
- \begin{align*}
- x &= x_0 + R\cos (t),\\
- y &= y_0 + R\sin (t),
- \end{align*}
- !et
- where $t\in [0, 2\pi]$. A discrete set of $t$ values in this
- interval gives the corresponding set of $(x,y)$ coordinates on
- the circle. The user must specify the resolution, i.e., the number
- of $t$ values, or equivalently, points on the circle. The circle's
- radius and center must of course also be specified.
- We can write the `Circle` class as
- !bc pycod
- class Circle(Shape):
- def __init__(self, center, radius, resolution=180):
- self.center, self.radius = center, radius
- self.resolution = resolution
- t = linspace(0, 2*pi, resolution+1)
- x0 = center[0]; y0 = center[1]
- R = radius
- x = x0 + R*cos(t)
- y = y0 + R*sin(t)
- self.shapes = {'circle': Curve(x, y)}
- !ec
- As in class `Line` we can offer the possibility to give an angle
- $\theta$ (equivalent to $t$ in the formulas above)
- and then get the corresponding $x$ and $y$ coordinates:
- !bc pycod
- def __call__(self, theta):
- """Return (x, y) point corresponding to angle theta."""
- return self.center[0] + self.radius*cos(theta), \
- self.center[1] + self.radius*sin(theta)
- !ec
- There is one flaw with this method: it yields illegal values after
- a translation, scaling, or rotation of the circle.
- A part of a circle, an arc, is a frequent geometric object when
- drawing mechanical systems. The arc is constructed much like
- a circle, but $t$ runs in $[\theta_0, \theta_1]$. Giving
- $\theta_1$ and $\theta_2$ the slightly more descriptive names
- `start_angle` and `arc_angle`, the code looks like this:
- !bc pycod
- class Arc(Shape):
- def __init__(self, center, radius,
- start_angle, arc_angle,
- resolution=180):
- self.center = center
- self.radius = radius
- self.start_angle = start_angle*pi/180 # radians
- self.arc_angle = arc_angle*pi/180
- self.resolution = resolution
- t = linspace(self.start_angle,
- self.start_angle + self.arc_angle,
- resolution+1)
- x0 = center[0]; y0 = center[1]
- R = radius
- x = x0 + R*cos(t)
- y = y0 + R*sin(t)
- self.shapes = {'arc': Curve(x, y)}
- !ec
- Having the `Arc` class, a `Circle` can alternatively befined as
- a subclass specializing the arc to a circle:
- !bc pycod
- class Circle(Arc):
- def __init__(self, center, radius, resolution=180):
- Arc.__init__(self, center, radius, 0, 360, resolution)
- !ec
- A wall is about drawing a curve, displacing the curve vertically by
- some thickness, and then filling the space between the curves
- by some pattern. The input is the `x` and `y` coordinate arrays
- of the curve and a thickness parameter. The computed coordinates
- will be a polygon: going along the originally curve and then back again
- along the vertically displaced curve. The relevant code becomes
- !bc pycod
- class CurveWall(Shape):
- def __init__(self, x, y, thickness):
- # User's curve
- x1 = asarray(x, float)
- y1 = asarray(y, float)
- # Displaced curve (according to thickness)
- x2 = x1
- y2 = y1 + thickness
- # Combine x1,y1 with x2,y2 reversed
- from numpy import concatenate
- x = concatenate((x1, x2[-1::-1]))
- y = concatenate((y1, y2[-1::-1]))
- wall = Curve(x, y)
- wall.set_filled_curves(color='white', pattern='/')
- self.shapes = {'wall': wall}
- !ec
- === Class Curve ===
- Class `Curve` sits on the coordinates to be drawn, but how is
- that done? The constructor just stores the coordinates, while
- a method `draw` sends the coordinates to the plotting program
- to make a graph.
- Or more precisely, to avoid a lot of (e.g.) Matplotlib-specific
- plotting commands we have created a small layer with a
- simple programming interface to plotting programs. This makes it
- straightforward to change from Matplotlib to another plotting
- program. The programming interface is represented by the `drawing_tool`
- object and has a few functions:
- * `plot_curve` for sending a curve in terms of $x$ and $y$ coordinates
- to the plotting program,
- * `set_coordinate_system` for specifying the graphics area,
- * `erase` for deleting all elements of the graph,
- * `set_grid` for turning on a grid (convenient while constructing the plot),
- * `set_instruction_file` for creating a separate file with all
- plotting commands (Matplotlib commands in our case),
- * a series of `set_X` functions where `X` is some property like
- `linecolor`, `linestyle`, `linewidth`, `filled_curves`.
- This is basically all we need to communicate to a plotting program.
- Any class in the `Shape` hierarchy inherits `set_X` functions for
- setting properties of curves. This information is propagated to
- all other shape objects that make up the figure. Class
- `Curve` stores the line properties together with the coordinates
- of its curve and propagates this information to the plotting program.
- When saying `vehicle.set_linewidth(10)`, all objects that make
- up the `vehicle` object will get a `set_linewidth(10)` call,
- but only the `Curve` object at the end of the chain will actually
- store the information and send it to the plotting program.
- A rough sketch of class `Curve` reads
- !bc pycod
- class Curve(Shape):
- """General curve as a sequence of (x,y) coordintes."""
- def __init__(self, x, y):
- self.x = asarray(x, dtype=float)
- self.y = asarray(y, dtype=float)
- self.linestyle = None
- self.linewidth = None
- self.linecolor = None
- self.fillcolor = None
- self.fillpattern = None
- self.arrow = None
- def draw(self):
- drawing_tool.plot_curve(
- self.x, self.y,
- self.linestyle, self.linewidth, self.linecolor,
- self.arrow, self.fillcolor, self.fillpattern)
- def set_linewidth(self, width):
- self.linewidth = width
- det set_linestyle(self, style):
- self.linestyle = style
- ...
- !ec
- === Compound Geometric Objects ===
- The simple classes `Line`, `Arc`, and `Circle` could define the geometric
- shape through just one `Curve` object. More complicated figure elements
- are built from instances of various subclasses of `Shape`. Classes used
- for professional drawings soon get quite complex in composition and
- have a lot of geometric details, so here we prefer to make a very simple
- composition: the already drawy vehicle from
- Figure refref{sketcher:fig:vehicle0}.
- That is, instead of composing the drawing in a Python code we make a class
- `Vehicle0` for doing the same thing, and derive it from `Shape`.
- The `Shape` hierarchy is found in the `pysketcher` package, so to use these
- classes or derive a new one, we need to import `pysketcher`. The constructor
- of clas `Vehicle0` performs approximately the same statements as
- in the example program we developed for making the drawing in
- Figure refref{sketcher:fig:vehicle0}.
- !bc pycod
- class Vehicle0(Shape):
- def __init__(self, w_1, R, L, H):
- wheel1 = Circle(center=(w_1, R), radius=R)
- wheel2 = wheel1.copy()
- wheel2.translate((L,0))
- under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
- width=2*R + L + 2*R, height=H)
- over = Rectangle(lower_left_corner=(w_1, 2*R + H),
- width=2.5*R, height=1.25*H)
- wheels = Composition(
- {'wheel1': wheel1, 'wheel2': wheel2})
- body = Composition(
- {'under': under, 'over': over})
- vehicle = Composition({'wheels': wheels, 'body': body})
- xmax = w_1 + 2*L + 3*R
- ground = Wall(x=[R, xmax], y=[0, 0], thickness=-0.3*R)
- self.shapes = {'vehicle': vehicle, 'ground': ground}
- !ec
- Any subclass of `Shape` *must* define the `shapes` attribute, otherwise
- the inherited `draw` method (and a lot of other methods too) will
- not work.
- The painting of the vehicle could be offered by a method:
- !bc pycod
- def colorful(self):
- wheels = self.shapes['vehicle']['wheels']
- wheels.set_filled_curves('blue')
- wheels.set_linewidth(6)
- wheels.set_linecolor('black')
- under = self.shapes['vehicle']['body']['under']
- under.set_filled_curves('red')
- over = self.shapes['vehicle']['body']['over']
- over.set_filled_curves(pattern='/')
- over.set_linewidth(14)
- !ec
- The usage of the class is simple: after having set up an appropriate
- coordinate system a s previously shown, we can do
- !bc pycod
- vehicle = Vehicle0(w_1, R, L, H)
- vehicle.draw()
- drawing_tool.display()
- !ec
- The color from Figure ref{sketcher:fig:vehicle0:v2} is realized by
- !bc pycod
- drawing_tool.erase()
- vehicle.colorful()
- vehicle.draw()
- drawing_tool.display()
- !ec
- A complete code defining and using class `Vehicle0` is found in the file
- # #ifdef PRIMER_BOOK
- `vehicle2.py`.
- # #else
- "`vehicle2.py`": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle2.py".
- # #endif
- The `pysketcher` package contains a wide range of classes for various
- geometrical objects, particularly those that are frequently used in
- drawings of mechanical systems.
- ======= Adding Functionality via Recursion =======
- The really powerful feature of our class hierarchy is that we can add
- much functionality to the superclass `Shape` and to the "bottom" classe
- `Curve`, and all other classes for all types of geometrical shapes
- immediately get the new functionality. To explain the idea we first have
- to look at the `draw` method, which all classes in the `Shape`
- hierarchy must have. The inner workings of the `draw` method explain
- the secrets of how a series of other useful operations on figures
- can be implemented.
- [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[
- !bc cod
- class Rectangle(Shape):
- def __init__(self, lower_left_corner, width, height):
- self.lower_left_corner = lower_left_corner # 2-tuple
- self.width, self.height = width, height
- Shape.__init__(self)
- def subshapes(self):
- ll = self.lower_left_corner # short form
- x = [ll[0], ll[0]+self.width,
- ll[0]+self.width, ll[0], ll[0]]
- y = [ll[1], ll[1], ll[1]+self.height,
- ll[1]+self.height, ll[1]]
- return Curve(x,y)
- !ec
- Class `Circle` needs many coordinates in its `Curve` object
- in order to display a smooth circle. We can provide the number of
- straight line segments along the circle as a parameter `resolution`.
- Using a default value of 180 means that each straight line segment
- approximates an arc of 2 degrees. This resolution should be sufficient
- for visual purposes. The set of coordinates along a circle with radius $R$
- and center $(x_0,y_0)$ is defined by
- !bt
- \begin{align}
- x &= x_0 + R\cos(t), label{sec:oo:circle:eq1}\\
- y &= y_0 + R\sin(t), label{sec:oo:circle:eq2}
- \end{align}
- !et
- for `resolution+1` $t$ values between $0$ and $2\pi$.
- The vectorized code for computing the coordinates becomes
- !bc cod
- t = linspace(0, 2*pi, self.resolution+1)
- x = x0 + R*cos(t)
- y = y0 + R*sin(t)
- !ec
- The complete `Circle` class is shown below:
- !bc cod
- class Circle(Shape):
- def __init__(self, center, radius, resolution=180):
- self.center, self.radius = center, radius
- self.resolution = resolution
- Shape.__init__(self)
- def subshapes(self):
- t = linspace(0, 2*pi, self.resolution+1)
- x0 = self.center[0]; y0 = self.center[1]
- R = self.radius
- x = x0 + R*cos(t)
- y = y0 + R*sin(t)
- return Curve(x,y)
- !ec
- We can also introduce class `Arc` for drawing the arc of a circle.
- Class `Arc` could be a subclass of `Circle`, extending the
- latter with two additional parameters: the opening of the arc
- (in degrees) and the starting $t$ value in
- (ref{sec:oo:circle:eq1})--(ref{sec:oo:circle:eq2}).
- The implementation of class `Arc` will then be almost a copy of
- the implementation of class `Circle`. The `subshapes` method
- will just define a different `t` array.
- Another view is to let class `Arc` be a subclass of `Shape`,
- and `Circle` a subclass of `Arc`, since a circle is an arc
- of 360 degrees. Let us employ this idea:
- !bc cod
- class Arc(Shape):
- def __init__(self, center, radius,
- start_degrees, opening_degrees, resolution=180):
- self.center = center
- self.radius = radius
- self.start_degrees = start_degrees*pi/180
- self.opening_degrees = opening_degrees*pi/180
- self.resolution = resolution
- Shape.__init__(self)
- def subshapes(self):
- t = linspace(self.start_degrees,
- self.start_degrees + self.opening_degrees,
- self.resolution+1)
- x0 = self.center[0]; y0 = self.center[1]
- R = self.radius
- x = x0 + R*cos(t)
- y = y0 + R*sin(t)
- return Curve(x,y)
- class Circle(Arc):
- def __init__(self, center, radius, resolution=180):
- Arc.__init__(self, center, radius, 0, 360, resolution)
- !ec
- In this latter implementation, we save a lot of code in class
- `Circle` since all of class `Arc` can be reused.
- Class `Wheel` may conceptually be a subclass of `Circle`.
- One circle, say the outer, is inherited and the subclass must have
- the inner circle as an attribute. Because of this "asymmetric"
- representation of the two circles in a wheel, we find it more natural
- to derive `Wheel` directly from `Shape`, and have the two
- circles as two attributes of type `Circle`:
- !bc cod
- class Wheel(Shape):
- def __init__(self, center, radius, inner_radius=None, nlines=10):
- self.center = center
- self.radius = radius
- if inner_radius is None:
- self.inner_radius = radius/5.0
- else:
- self.inner_radius = inner_radius
- self.nlines = nlines
- Shape.__init__(self)
- !ec
- If the radius of the inner circle
- is not defined (`None`)
- we take it as 1/5 of the radius of the outer circle.
- The wheel is naturally composed of two `Circle` instances and
- `nlines` `Line` instances:
- !bc cod
- def subshapes(self):
- outer = Circle(self.center, self.radius)
- inner = Circle(self.center, self.inner_radius)
- lines = []
- t = linspace(0, 2*pi, self.nlines)
- Ri = self.inner_radius; Ro = self.radius
- x0 = self.center[0]; y0 = self.center[1]
- xinner = x0 + Ri*cos(t)
- yinner = y0 + Ri*sin(t)
- xouter = x0 + Ro*cos(t)
- youter = y0 + Ro*sin(t)
- lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
- zip(xinner, yinner, xouter, youter)]
- return [outer, inner] + lines
- !ec
- For the fun of it, we can implement other shapes, say a sine wave
- !bt
- \begin{equation*}y = m + A\sin kx,\quad k=2\pi /\lambda,\end{equation*}
- !et
- where $\lambda$ is the wavelength
- of the sine waves, $A$ is the wave amplitude, and $m$ is the mean
- value of the wave. The class looks like
- !bc cod
- class Wave(Shape):
- def __init__(self, xstart, xstop,
- wavelength, amplitude, mean_level):
- self.xstart = xstart
- self.xstop = xstop
- self.wavelength = wavelength
- self.amplitude = amplitude
- self.mean_level = mean_level
- Shape.__init__(self)
- def subshapes(self):
- npoints = (self.xstop - self.xstart)/(self.wavelength/61.0)
- x = linspace(self.xstart, self.xstop, npoints)
- k = 2*pi/self.wavelength # frequency
- y = self.mean_level + self.amplitude*sin(k*x)
- return Curve(x,y)
- !ec
- With this and the previous example, you should be in a position to
- write your own subclasses. Exercises~ref{sec:oo:ex11}--ref{sec:oo:ex13b}
- suggest some smaller projects.
- # ===== A Class for Drawing Springs =====
- #
- # Give: bottom point $B$, number of spring tags $n$, length $L$. Assume that
- # $L/3$ is the bottom and top vertical line and that the tags
- # are in the middle $L/3$. The width of the tags, $w$, can be either fixed to
- # a number or relative to $L$ (say $L/10$ -- need two variables, one
- # fixed true/false and one value).
- #
- #
- # \notready
- __Functions for Controlling Lines, Colors, etc.__
- The `shapes` module containing class `Shape` and all subclasses
- mentioned above, also offers some additional functions that do not
- depend on any particular shape:
- * `display()` for displaying the defined figures so far (all figures whose `draw` method is called).
- * `erase()` for ereasing the current figure.
- * `hardcopy(name)` for saving the current figure to a PNG file `name.png`.
- * `set_linecolor(color)` for setting the color of lines, where
- `color` is a string like `'red'` (default), `'blue'`, `'green'`, `'aqua'`, `'purple'`, `'yellow'`, and `'black'`.
- * `set_linewidth(width)` for setting the width of a line, measured as an integer (default is 2).
- * `filled_curves(on)` for turrning on (`on=True`) or off (`on=False`) whether the area inside a shape should be filled with the current line color.
- Actually, the functions above are static methods in class `Shape`
- (cf.~Chapter ref{sec:class:static}),
- and they are
- just mirrored
- as global functions\footnote{You can look into `shapes.py` to see how
- we automate the duplication of static methods as global functions.}
- in the `shapes` module.
- Users without knowledge of static methods do not need to use the
- `Shape` prefix for reaching this functionality.
- ===== Scaling, Translating, and Rotating a Figure =====
- label{sec:oo:scaling}
- The real power of object-oriented programming will be obvious in a
- minute when we, with a few lines of code, suddenly can
- equip *all* shape objects
- with additional functionality for scaling, translating, and rotating the
- figure.
- __Scaling.__
- Let us first treat the simplest of the three cases: scaling.
- For a `Curve` instance containing a set of $n$ coordinates
- $(x_i,y_i)$ that make up a curve, scaling by
- a factor $a$ means that we multiply all the $x$ and $y$ coordinates
- by $a$:
- !bt
- \begin{equation*} x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,\quad i=0,\ldots,n-1\thinspace . \end{equation*}
- !et
- Here we apply the arrow as an assignment operator.
- The corresponding Python implementation in
- class `Curve` reads
- !bc cod
- class Curve:
- ...
- def scale(self, factor):
- self.x = factor*self.x
- self.y = factor*self.y
- !ec
- Note here that `self.x` and `self.y` are Numerical Python arrays,
- so that multiplication by a scalar number `factor` is
- a vectorized operation.
- In an instance of a subclass of `Shape`,
- the meaning of a method `scale` is
- to run through all objects in the list `self.shapes` and ask
- each object to scale itself. This is the same delegation of actions
- to subclass instances as we do in the `draw` method, and
- all objects, except `Curve` instances, can share the same
- implementation of the `scale` method. Therefore, we place
- the `scale` method in the superclass `Shape` such that all
- subclasses can inherit this method.
- Since `scale` and `draw` are so similar,
- we can easily implement the `scale` method in class `Shape` by
- copying and editing the `draw` method:
- !bc cod
- class Shape:
- ...
- def scale(self, factor):
- for shape in self.shapes:
- shape.scale(factor)
- !ec
- This is all we have to do in order to equip all subclasses of
- `Shape` with scaling functionality! But why is it so easy?
- All subclasses inherit `scale` from class `Shape`.
- Say we have a subclass instance `s` and that we call
- `s.scale(factor)`. This leads to calling the inherited `scale`
- method shown above, and in the `for` loop we call the
- `scale` method for each `shape` object in the `self.shapes` list.
- If `shape` is not a `Curve` object, this procedure repeats,
- until we hit a `shape` that is a `Curve`, and then
- the scaling on that set of coordinates is performed.
- __Translation.__
- A set of coordinates $(x_i, y_i)$ can be translated $x$ units in
- the $x$ direction and $y$ units in the $y$ direction using the formulas
- !bt
- \begin{equation*} x_i\leftarrow x+x_i,\quad y_i\leftarrow y + y_i,\quad i=0,\ldots,n-1\thinspace . \end{equation*}
- !et
- The corresponding Python implementation in class `Curve` becomes
- !bc cod
- class Curve:
- ...
- def translate(self, x, y):
- self.x = x + self.x
- self.y = y + self.y
- !ec
- The translation operation for a shape object is very similar to the
- scaling and drawing operations. This means that we can implement a
- common method `translate` in the superclass `Shape`. The code
- is parallel to the `scale` method:
- !bc cod
- class Shape:
- ....
- def translate(self, x, y):
- for shape in self.shapes:
- shape.translate(x, y)
- !ec
- __Rotation.__
- Rotating a figure is more complicated than scaling and translating.
- A counter clockwise rotation of $\theta$ degrees for a set of
- coordinates $(x_i,y_i)$ is given by
- !bt
- \begin{align*}
- \bar x_i &\leftarrow& x_i\cos\theta - y_i\sin\theta,\\
- \bar y_i &\leftarrow& x_i\sin\theta + y_i\cos\theta\thinspace .
- \end{align*}
- !et
- This rotation is performed around the origin. If we want the figure
- to be rotated with respect to a general point $(x,y)$, we need to
- extend the formulas above:
- !bt
- \begin{align*}
- \bar x_i &\leftarrow& x + (x_i -x)\cos\theta - (y_i -y)\sin\theta,\\
- \bar y_i &\leftarrow& y + (x_i -x)\sin\theta + (y_i -y)\cos\theta\thinspace .
- \end{align*}
- !et
- The Python implementation in class `Curve`, assuming that $\theta$
- is given in degrees and not in radians, becomes
- !bc cod
- def rotate(self, angle, x=0, y=0):
- angle = angle*pi/180
- c = cos(angle); s = sin(angle)
- xnew = x + (self.x - x)*c - (self.y - y)*s
- ynew = y + (self.x - x)*s + (self.y - y)*c
- self.x = xnew
- self.y = ynew
- !ec
- The `rotate` method in class `Shape` is identical to the
- `draw`, `scale`, and `translate` methods except that we
- have other arguments:
- !bc cod
- class Shape:
- ....
- def rotate(self, angle, x=0, y=0):
- for shape in self.shapes:
- shape.rotate(angle, x, y)
- !ec
- __Application: Rolling Wheel.__
- To demonstrate the effect of translation and rotation we can roll
- a wheel on the screen. First we draw the wheel and rotate it a bit
- to demonstrate the basic operations:
- !bc cod
- center = (6,2) # the wheel's center point
- w1 = Wheel(center=center, radius=2, inner_radius=0.5, nlines=7)
- # rorate the wheel 2 degrees around its center point:
- w1.rotate(angle=2, center[0], center[1])
- w1.draw()
- display()
- !ec
- Now we want to roll the wheel by making many such small rotations.
- At the same time we need to translate the wheel since rolling
- an arc length $L=R\theta$, where $\theta$ is the rotation angle
- (in radians) and $R$ is the outer radius of the wheel, implies that
- the center point moves a distance $L$ to the left ($\theta >0$
- means counter clockwise rotation).
- In code we must therefore combine rotation with translation:
- !bc cod
- L = radius*angle*pi/180 # translation = arc length
- w1.rotate(angle, center[0], center[1])
- w1.translate(-L, 0)
- center = (center[0] - L, center[1])
- !ec
- We are now in a position to put the rotation and translation
- operations in a `for` loop and make a complete function:
- !bc cod
- def rolling_wheel(total_rotation_angle):
- """Animation of a rotating wheel."""
- set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
- center = (6,2)
- radius = 2.0
- angle = 2.0
- w1 = Wheel(center=center, radius=radius,
- inner_radius=0.5, nlines=7)
- for i in range(int(total_rotation_angle/angle)):
- w1.draw()
- display()
- L = radius*angle*pi/180 # translation = arc length
- w1.rotate(angle, center[0], center[1])
- w1.translate(-L, 0)
- center = (center[0] - L, center[1])
- erase()
- !ec
- To control the visual "velocity" of the wheel, we can insert a pause
- between each frame in the `for` loop. A call to
- `time.sleep(s)`, where `s` is the length of
- the pause in seconds, can do this for us.
- Another convenient feature is to save each frame drawn in the `for`
- loop as a hardcopy in PNG format and then, after the loop,
- make an animated GIF file based on the individual PNG frames.
- The latter operation is performed either by the `movie` function
- from `scitools.std`
- or by the `convert` program from
- the ImageMagick suite. With the latter you write the following command
- in a terminal window:
- !bc ccq
- convert -delay 50 -loop 1000 xxx tmp_movie.gif
- !ec
- Here, `xxx` is a space-separated list of all the PNG files, and
- `tmp_movie.gif` is the name of the resulting animated GIF file.
- We can easily make `xxx` by collecting the names of the PNG files
- from the loop in a list object, and then join the names.
- The `convert` command can be run as an `os.system` call.
- The complete `rolling_wheel` function, incorporating the
- mentioned movie making, will then be
- !bc cod
- def rolling_wheel(total_rotation_angle):
- """Animation of a rotating wheel."""
- set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
- import time
- center = (6,2)
- radius = 2.0
- angle = 2.0
- pngfiles = []
- w1 = Wheel(center=center, radius=radius,
- inner_radius=0.5, nlines=7)
- for i in range(int(total_rotation_angle/angle)):
- w1.draw()
- display()
- filename = 'tmp_
- #03d' i
- pngfiles.append(filename + '.png')
- hardcopy(filename)
- time.sleep(0.3) # pause 0.3 sec
- L = radius*angle*pi/180 # translation = arc length
- w1.rotate(angle, center[0], center[1])
- w1.translate(-L, 0)
- center = (center[0] - L, center[1])
- erase() # erase the screen before new figure
- cmd = 'convert -delay 50 -loop 1000
- #s tmp_movie.gif' \
- # (' '.join(pngfiles))
- import commands
- failure, output = commands.getstatusoutput(cmd)
- if failure: print 'Could not run', cmd
- !ec
- The last two lines run a command, from Python, as we would run the
- command in a terminal window.
- The resulting animated GIF file can be viewed with
- `animate tmp_movie.gif` as a command in a terminal window.
|