| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307 |
- 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 great advantage with making figures this way through programming,
- rather than using interactive drawing programs, is that the
- accuracy of mathematical drawings is easily reflected, distances
- can be parameterized through variables such that the
- proportions of parts of the figure can be quickly changed,
- groups of objects can easily be manipulated (translated, rotated,
- scaled), objects can be left out while experimenting with the
- figure, to mention some important features. However, the single
- most important feature is the ability to make animations governed
- by mathematical formulas, maybe coming from simulations
- based on solving differential equations reflecting the physics
- of the problem.
- ===== 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.
|