| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306 |
- 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:fig1}.
- This sketch is made up many individual elements....
- FIGURE: [figs-sketcher/...png, width=500]
- 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, and enhanced functionality for all the classes is also easy to
- implement in common, generic code that can be shared by all classes.
- ===== Using the Object Collection =====
- Before we dive into implementation details, let us first decide upon
- the interface we want to take advantage of to make sketches of the type in
- Figure ref{sketcher:fig1}. We 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.
- The drawing package is named `pysketcher` so it is natural that we
- must import tools from `pysketcher`. 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:
- !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
- 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 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 vehicle `body` group. The whole vehicle is a composition
- of the `wheels` and `body` groups. The codes goes like
- !bc pycod
- wheels = Compose({'wheel1': wheel1, 'wheel2': wheel2})
- body = Compose({'under': under, 'over': over})
- vehicle = Compose({'wheels': wheels, 'body': body})
- !ec
- The ground is illustrated by an object of type `Wall`,
- mostly used to indicate walls in sketches of physical 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 simple:
- !bc pycod
- ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
- !ec
- We may collect all the objects in a "top" object that contains
- the whole figure:
- !bc pycod
- fig = Compose({'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 filled, either with a color or with a
- special visual pattern:
- !bc
- # Set filling of all curves
- drawing_tool.set_filled_curves(color='blue', hatch='/')
- # 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' fillhatch='/'
- vehicle (Compose):
- body (Compose):
- over (Rectangle):
- rectangle (Curve): 5 coords
- under (Rectangle):
- rectangle (Curve): 5 coords
- wheels (Compose):
- wheel1 (Circle):
- arc (Curve): 181 coords
- wheel2 (Circle):
- arc (Curve): 181 coords
- !ec
- Here we can see the class type each object, how many
- coordinates that are involved in basic figures, 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.
- 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)
- !ec
- 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 $4$ 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, user_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, e.g.,
- !bc pycod
- from scitools.std import movie
- movie(files, encoder='html', output_file='anim')
- !ec
- This command makes a movie that is actually an HTML file `anim.html`,
- which can be loaded into a web browser.
- You can try this by running 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", or view a ready-made "movie": "http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/animation_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 = Compose({'wheel':
- Circle(center=(w_1, R), radius=R),
- 'cross':
- Compose({'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 need to displace all the objects in the
- entire vehicle and also rotate the crosses in the 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/src-sketcher/animation_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 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 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.
- ===== 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 sample classes so far has managed to define the geometric shape
- through just one `Curve` object.[[[
- Some objects in a figure will be associated with a point and not
- a curve. Therefore, it is natural to introduce a `Point` class
- as superclass for such objects:
- !bc pycod
- class Point(Shape):
- def __init__(self, x, y):
- self.x, self.y = x, y
- !ec
- A candidate for subclass is a text located at a given point:
- !bc pycod
- class Text(Point):
- def __init__(self, text, position, alignment='center', fontsize=18):
- self.text = text
- self.alignment, self.fontsize = alignment, fontsize
- is_sequence(position, length=2, can_be_None=True)
- Point.__init__(self, position[0], position[1])
- #no need for self.shapes here
- !ec
- [[[[[[[[[[[
- Class `Line` is a subclass of `Shape` and
- represents the simplest shape: a stright line between two points.
- Class `Rectangle` is another subclass of `Shape`, implementing the
- functionality needed to specify the four lines of a rectangle.
- Class `Circle` can be yet another subclass of `Shape`, or
- we may have a class `Arc` and let `Circle` be a subclass
- of `Arc` since a circle is an arc of 360 degrees.
- Class `Wheel`
- is also subclass of `Shape`, but it contains
- naturally two `Circle` instances for the inner and outer circles,
- plus a set of `Line` instances
- going from the inner to the outer circles.
- The discussion in the previous paragraph shows that a subclass in
- the `Shape` hierarchy typically contains a list of
- other subclass instances, *or* the shape is a primitive, such as a line,
- circle, or rectangle, where the geometry is defined through a set of
- $(x,y)$ coordinates rather than through other `Shape` instances.
- It turns out that the implementation is simplest if we introduce
- a class `Curve` for holding a primitive shape defined by
- $(x,y)$ coordinates. Then all other subclasses of `Shape` can
- have a list `shapes` holding the various instances of subclasses of
- `Shape` needed to
- build up the geometric object. The `shapes`
- attribute in class `Circle` will contain
- one `Curve` instance for holding the coordinates along the circle,
- while the `shapes` attribute in class `Wheel` contains
- two `Circle` instances and a number of `Line` instances.
- Figures ref{fig:oo:Rectangle:fig} and ref{fig:oo:Wheel:fig}
- display two UML drawings of the `shapes` class hierarchy where we
- can get a view of how `Rectangle` and `Wheel` relate to other classes:
- the darkest arrows represent is-a relationship while the lighter arrows
- represent has-a relationship.
- All instances in the `Shape` hierarchy must have a `draw` method.
- The `draw` method in class `Curve` plots the $(x,y)$ coordinates
- as a curve, while the `draw` method in all other classes simply
- draws all the shapes that make up the particular figure of the class:
- !bc cod
- for shape in self.shapes:
- shape.draw()
- !ec
- \begin{figure}
- \centerline{\psfig{figure=figs/lumpy_Rectangle_shapes_hier.ps,width=0.5\linewidth}}
- \caption{ label{fig:oo:Rectangle:fig}
- UML diagram of parts of the `shapes` hierarchy. Classes `Rectangle`
- and `Curve` are subclasses of `Shape`. The darkest arrow with
- the biggest arrowhead indicates inheritance and is-a
- relationship: `Rectangle` and `Curve` are both also `Shape`.
- The lighter arrow
- indicates {has-a} relationship:
- `Rectangle` has a `Curve`, and a `Curve` has a
- `NumPyArray`.
- }
- \end{figure}
- \begin{figure}
- \centerline{\psfig{figure=figs/lumpy_Wheel_shapes_hier.ps,width=0.7\linewidth}}
- \caption{ label{fig:oo:Wheel:fig}
- This is a variant of Figure ref{fig:oo:Rectangle:fig} where we
- display how class `Wheel` relates to other classes in the
- `shapes` hierarchy.
- `Wheel` is a `Shape`, like `Arc`, `Line`, and `Curve`,
- but `Wheel` contains `Circle` and `Line` objects,
- while the `Circle` and `Line` objects have a `Curve`, which has a
- `NumPyArray`. We also see that `Circle` is a subclass of `Arc`.
- }
- \end{figure}
- ===== The Drawing Tool =====
- We have in Chapter ref{ch:plot} introduced the Easyviz tool for
- plotting graphs. This tool is quite well suited for drawing
- geometric shapes defined in terms of curves, but when drawing shapes
- we often want to skip ticmarks on the axis, labeling of the curves and
- axis, and perform other adjustments. Instead of using Easyviz, which
- aims at function plotting, we
- have decided to use a plotting tool directly and fine-tune the few
- commands we need for drawing shapes.
- A simple plotting tool for shapes is based on Gnuplot and implemented in
- class `GnuplotDraw` in the file `GnuplotDraw.py`.
- This class has the following user interface:
- !bc cod
- class GnuplotDraw:
- def __init__(self, xmin, xmax, ymin, ymax):
- """Define the drawing area [xmin,xmax]x[ymin,ymax]."""
- def define_curve(self, x, y):
- """Define a curve with coordinates x and y (arrays)."""
- def erase(self):
- """Erase the current figure."""
- def display(self):
- """Display the figure."""
- def hardcopy(self, name):
- """Save figure in PNG file name.png."""
- def set_linecolor(self, color):
- """Change the color of lines."""
- def set_linewidth(self, width):
- """Change the line width (int, starts at 1)."""
- def filled_curves(self, on=True):
- """Fill area inside curves with current line color."""
- !ec
- One can easily make a similar class with an identical interface that
- applies another plotting package than Gnuplot to create the drawings.
- In particular, encapsulating the drawing actions in such a class makes
- it trivial to change the drawing program in the future.
- The program pieces that apply a drawing tool like `GnuplotDraw`
- remain the same. This is an important strategy to follow, especially
- when developing larger software systems.
- ===== Implementation of Shape Classes =====
- label{sec:oo:shape:impl}
- Our superclass `Shape` can naturally hold a coordinate system
- specification, i.e., the rectangle in which other shapes can be drawn.
- This area is fixed for all shapes, so the associated variables should
- be static and the method for setting them should also be static
- (see Chapter ref{sec:class:static} for static attributes and methods).
- It is also natural that class `Shape` holds access to a drawing
- tool, in our case a `GnuplotDraw` instance. This object
- is also static.
- However, it can be an advantage to mirror the static attributes and methods
- as global variables and functions in the `shapes` modules.
- Users not familiar with static class items can drop the `Shape`
- prefix and just use plain module variables and functions. This is what
- we do in the application examples.
- Class `Shape` defines an imporant method, `draw`, which
- just calls the `draw` method for all subshapes that build up the
- current shape.
- Here is a brief view of class `Shape`\footnote{We have
- for simplicity omitted
- the static attributes
- and methods. These can be viewed in the `shapes.py` file.}:
- !bc cod
- class Shape:
- def __init__(self):
- self.shapes = self.subshapes()
- if isinstance(self.shapes, Shape):
- self.shapes = [self.shapes] # turn to list
- def subshapes(self):
- """Define self.shapes as list of Shape instances."""
- raise NotImplementedError(self.__class__.__name__)
- def draw(self):
- for shape in self.shapes:
- shape.draw()
- !ec
- In class `Shape` we require the `shapes` attribute to be a
- list, but if the `subshape` method in subclasses returns just one
- instance, this is automatically wrapped in a list in the constructor.
- First we implement the special case class `Curve`, which does not
- have subshapes but instead $(x,y)$ coordinates for a curve:
- !bc cod
- class Curve(Shape):
- """General (x,y) curve with coordintes."""
- def __init__(self, x, y):
- self.x, self.y = x, y
- # Turn to Numerical Python arrays
- self.x = asarray(self.x, float)
- self.y = asarray(self.y, float)
- Shape.__init__(self)
- def subshapes(self):
- pass # geometry defined in constructor
- !ec
- # In Python, `Curve` does not need to be a subclass of `Shape`.
- # It could in fact be natural to remove the `subshapes` method and
- # the inheritance from `Shape`. In other languages where all
- # elements in `self.shapes` need to be instances of classes in the
- # `Shape` hierarchy, because all list elements must have a fixed
- # and specified type, `Curve` must be a subclass of `Shape`.
- The simplest ordinary `Shape` class is `Line`:
- !bc cod
- class Line(Shape):
- def __init__(self, start, stop):
- self.start, self.stop = start, stop
- Shape.__init__(self)
- def subshapes(self):
- x = [self.start[0], self.stop[0]]
- y = [self.start[1], self.stop[1]]
- return Curve(x,y)
- !ec
- The code in this class works with `start` and `stop` as
- tuples, lists, or arrays of length
- two, holding the end points of the line.
- The underlying `Curve` object needs only these two end points.
- A rectangle is represented by a slightly more complicated class, having the
- lower left corner, the width, and the height of the rectangle as
- attributes:
- !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.
|