sketcher.do.txt 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307
  1. Implementing a drawing program provides a very good example on the usefulness
  2. of object-oriented programming. In the following we shall develop
  3. the simpler parts of a relatively small and compact drawing program
  4. for making sketches of the type shown in Figure ref{sketcher:fig1}.
  5. This sketch is made up many individual elements....
  6. FIGURE: [figs-sketcher/...png, width=500]
  7. Classes are very suitable for implementing the various components that
  8. build up a sketch and their functionality. In particular, we shall
  9. demonstrate that as soon some classes are established, more are easily
  10. added, and enhanced functionality for all the classes is also easy to
  11. implement in common, generic code that can be shared by all classes.
  12. ===== Using the Object Collection =====
  13. Before we dive into implementation details, let us first decide upon
  14. the interface we want to take advantage of to make sketches of the type in
  15. Figure ref{sketcher:fig1}. We start with a significantly simpler
  16. example as depicted in Figure ref{sketcher:fig:vehicle0}.
  17. This toy sketch consists of several elements: two circles, two
  18. rectangles, and a "ground" element.
  19. FIGURE: [figs-sketcher/vehicle0_dim.png, width=400] Sketch of a simple figure. label{sketcher:fig:vehicle0}
  20. === Basic Drawing ===
  21. A typical program creating these five elements is shown next.
  22. The drawing package is named `pysketcher` so it is natural that we
  23. must import tools from `pysketcher`. The first task is always to
  24. define a coordinate system. Some graphics operations are done with
  25. a helper object called `drawing_tool` (imported from `pysketcher`).
  26. With the drawing area in place we can make the first `Circle` object:
  27. !bc pycod
  28. from pysketcher import *
  29. R = 1 # radius of wheel
  30. L = 4 # distance between wheels
  31. H = 2 # height of vehicle body
  32. w_1 = 5 # position of front wheel
  33. drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
  34. ymin=-1, ymax=2*R + 3*H)
  35. wheel1 = Circle(center=(w_1, R), radius=R)
  36. !ec
  37. To translate the geometric information about the `wheel1` object to
  38. instructions for the plotting engine (in this case Matplotlib), one calls the
  39. `wheel1.draw()`. To display all drawn objects, one issues
  40. `drawing_tool.display()`. The steps are hence:
  41. !bc pycod
  42. wheel1 = Circle(center=(w_1, R), radius=R)
  43. wheel1.draw()
  44. # Define other objects and call their draw() methods
  45. drawing_tool.display()
  46. drawing_tool.savefig('tmp.png') # store picture
  47. !ec
  48. The next wheel can be made by taking a copy of `wheel1` and
  49. translating the object a distance (to the right) described by the
  50. vector $(4,0)$:
  51. !bc pycod
  52. wheel2 = wheel1.copy()
  53. wheel2.translate((L,0))
  54. !ec
  55. The two rectangles are made in an intuitive way:
  56. !bc pycod
  57. under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
  58. width=2*R + L + 2*R, height=H)
  59. over = Rectangle(lower_left_corner=(w_1, 2*R + H),
  60. width=2.5*R, height=1.25*H)
  61. !ec
  62. === Groups of Objects ===
  63. Instead of calling the `draw` method of every object, we can
  64. group objects and call `draw`, or perform other operations, for
  65. the whole group. For example, we may collect the two wheels
  66. in a `wheels` group and the `over` and `under` rectangles
  67. in a vehicle `body` group. The whole vehicle is a composition
  68. of the `wheels` and `body` groups. The codes goes like
  69. !bc pycod
  70. wheels = Compose({'wheel1': wheel1, 'wheel2': wheel2})
  71. body = Compose({'under': under, 'over': over})
  72. vehicle = Compose({'wheels': wheels, 'body': body})
  73. !ec
  74. The ground is illustrated by an object of type `Wall`,
  75. mostly used to indicate walls in sketches of physical systems.
  76. A `Wall` takes the `x` and `y` coordinates of some curve
  77. and a `thickness` parameter and creates a "thick" curve filled
  78. with a simple pattern. In this case the curve is just a flat
  79. line so the construction is simple:
  80. !bc pycod
  81. ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
  82. !ec
  83. We may collect all the objects in a "top" object that contains
  84. the whole figure:
  85. !bc pycod
  86. fig = Compose({'vehicle': vehicle, 'ground': ground})
  87. fig.draw() # send all figures to plotting backend
  88. drawing_tool.display()
  89. drawing_tool.savefig('tmp.png')
  90. !ec
  91. The `fig.draw()` call will visit
  92. all subgroups, their subgroups,
  93. and so in the herarchical tree structure that we have collected,
  94. and call `draw` for every object.
  95. === Changing Line Styles and Colors ===
  96. Controlling the line style, line color, and line width is
  97. fundamental when designing figures. The `pysketcher`
  98. package allows the user to control such properties in
  99. single objects, but also set global properties that are
  100. used if the object has no particular specification of
  101. the properties. Setting the global properties are done like
  102. !bc pycod
  103. drawing_tool.set_linestyle('dashed')
  104. drawing_tool.set_linecolor('black')
  105. drawing_tool.set_linewidth(4)
  106. !ec
  107. At the object level the properties are specified in a similar
  108. way:
  109. !bc pycod
  110. wheel1.set_linestyle('solid')
  111. wheel1.set_linecolor('red')
  112. !ec
  113. and so on.
  114. Geometric figures can be filled, either with a color or with a
  115. special visual pattern:
  116. !bc
  117. # Set filling of all curves
  118. drawing_tool.set_filled_curves(color='blue', hatch='/')
  119. # Turn off filling of all curves
  120. drawing_tool.set_filled_curves(False)
  121. # Fill the wheel with red color
  122. wheel1.set_filled_curves('red')
  123. !ec
  124. # http://packages.python.org/ete2/ for visualizing tree structures!
  125. === The Figure Composition as an Object Hierarchy ===
  126. The composition of objects is hierarchical, as in a family, where
  127. each object has a parent and a number of children. Do a
  128. `print fig` to display the relations:
  129. !bc dat
  130. ground
  131. wall
  132. vehicle
  133. body
  134. over
  135. rectangle
  136. under
  137. rectangle
  138. wheels
  139. wheel1
  140. arc
  141. wheel2
  142. arc
  143. !ec
  144. The indentation reflects how deep down in the hierarchy (family)
  145. we are.
  146. This output is to be interpreted as follows:
  147. * `fig` contains two objects, `ground` and `vehicle`
  148. * `ground` contains an object `wall`
  149. * `vehicle` contains two objects, `body` and `wheels`
  150. * `body` contains two objects, `over` and `under`
  151. * `wheels` contains two objects, `wheel1` and `wheel2`
  152. More detailed information can be printed by
  153. !bc pycod
  154. print fig.show_hierarchy('std')
  155. !ec
  156. yielding the output
  157. !bc dat
  158. ground (Wall):
  159. wall (Curve): 4 coords fillcolor='white' fillhatch='/'
  160. vehicle (Compose):
  161. body (Compose):
  162. over (Rectangle):
  163. rectangle (Curve): 5 coords
  164. under (Rectangle):
  165. rectangle (Curve): 5 coords
  166. wheels (Compose):
  167. wheel1 (Circle):
  168. arc (Curve): 181 coords
  169. wheel2 (Circle):
  170. arc (Curve): 181 coords
  171. !ec
  172. Here we can see the class type each object, how many
  173. coordinates that are involved in basic figures, and
  174. special settings of the basic figure (fillcolor, line types, etc.).
  175. For example, `wheel2` is a `Circle` object consisting of an `arc`,
  176. which is a `Curve` object consisting of 181 coordinates (the
  177. points needed to draw a smooth circle). The `Curve` objects are the
  178. only objects that really holds specific coordinates to be drawn.
  179. The other object types are just compositions used to group
  180. parts of the complete figure.
  181. Any of the objects can in the program be reached through their names, e.g.,
  182. !bc pycodc
  183. fig['vehicle']
  184. fig['vehicle']['wheels']
  185. fig['vehicle']['wheels']['wheel2']
  186. fig['vehicle']['wheels']['wheel2']['arc']
  187. fig['vehicle']['wheels']['wheel2']['arc'].x # x coords
  188. fig['vehicle']['wheels']['wheel2']['arc'].y # y coords
  189. fig['vehicle']['wheels']['wheel2']['arc'].linestyle
  190. fig['vehicle']['wheels']['wheel2']['arc'].linetype
  191. !ec
  192. Grabbing a part of the figure this way is very handy for
  193. changing properties of that part, for example, colors, line styles
  194. (see Figure ref{sketcher:fig:vehicle0:v2}):
  195. !bc pycod
  196. fig['vehicle']['wheels'].set_filled_curves('blue')
  197. fig['vehicle']['wheels'].set_linewidth(6)
  198. fig['vehicle']['wheels'].set_linecolor('black')
  199. fig['vehicle']['body']['under'].set_filled_curves('red')
  200. fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
  201. fig['vehicle']['body']['over'].set_linewidth(14)
  202. !ec
  203. FIGURE: [figs-sketcher/vehicle0.png, width=700] Left: Basic line-based drawing. Right: Thicker lines and filled parts. label{sketcher:fig:vehicle0:v2}
  204. We can also change position of parts of the figure and thereby make
  205. animations, as shown next.
  206. === Animation: Translating the Vehicle ===
  207. Can we make our little vehicle roll? A first attempt will be to
  208. fake rolling by just displacing all parts of the vehicle.
  209. The relevant parts constitute the `fig['vehicle']` object.
  210. This part of the figure can be translated, rotated, and scaled.
  211. A translation along the ground means a translation in $x$ direction,
  212. say a length $4$ to the right:
  213. !bc pycod
  214. fig['vehicle'].translate((L,0))
  215. !ec
  216. You need to erase, draw, and display to see the movement:
  217. !bc pycod
  218. drawing_tool.erase()
  219. fig.draw()
  220. drawing_tool.display()
  221. !ec
  222. Without erasing, the old position of the vehicle will remain in
  223. the figure so you get two vehicles. Without `fig.draw()` the
  224. new coordinates of the vehicle will not be communicated to
  225. the drawing tool, and without calling dislay the updated
  226. drawing will not be visible.
  227. Let us make a velocity function and move the object according
  228. to that velocity in small steps of time:
  229. !bc pydoc
  230. def v(t):
  231. return -8*R*t*(1 - t/(2*R))
  232. animate(fig, tp, user_action)
  233. !ec
  234. For small time steps `dt` the corresponding displacement is
  235. well approximated by `dt*v(t)` (we could integrate the velocity
  236. to obtain the exact position, but we would anyway need to
  237. calculate the displacement from time step to time step).
  238. The `animate` function takes as arguments some figure `fig`, a set of
  239. time points `tp`, and a user function `action`,
  240. and then a new figure is drawn for each time point and the user
  241. can through the provided `action` function modify desired parts
  242. of the figure. Here the `action` function will move the `vehicle`:
  243. !bc pycod
  244. def move_vehicle(t, fig):
  245. x_displacement = dt*v(t)
  246. fig['vehicle'].translate((x_displacement, 0))
  247. !ec
  248. Defining a set of time points for the frames in the animation
  249. and performing the animation is done by
  250. !bc pycod
  251. import numpy
  252. tp = numpy.linspace(0, 2*R, 25)
  253. dt = tp[1] - tp[0] # time step
  254. animate(fig, tp, move_vehicle, pause_per_frame=0.2)
  255. !ec
  256. The `pause_per_frame` adds a pause, here 0.2 seconds, between
  257. each frame.
  258. We can also make a movie file of the animation:
  259. !bc pycod
  260. files = animate(fig, tp, move_vehicle, moviefiles=True,
  261. pause_per_frame=0.2)
  262. !ec
  263. The `files` variable holds a string with the family of
  264. files constituting the frames in the movie, here
  265. `'tmp_frame*.png'`. Making a movie out of the individual
  266. frames can be done in many ways, e.g.,
  267. !bc pycod
  268. from scitools.std import movie
  269. movie(files, encoder='html', output_file='anim')
  270. !ec
  271. This command makes a movie that is actually an HTML file `anim.html`,
  272. which can be loaded into a web browser.
  273. You can try this by running the present example in the file
  274. # #ifdef PRIMER_BOOK
  275. `vehicle0.py`.
  276. # #else
  277. "`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".
  278. # #endif
  279. === Animation: Rolling the Wheels ===
  280. It is time to show rolling wheels. To this end, we make somewhat
  281. more complicated wheels with spokes as on a bicyle, formed by
  282. two crossing lines, see Figure ref{sketcher:fig:vehicle1}.
  283. The construction of the wheels will now involve a circle
  284. and two lines:
  285. !bc pycod
  286. wheel1 = Compose({'wheel':
  287. Circle(center=(w_1, R), radius=R),
  288. 'cross':
  289. Compose({'cross1': Line((w_1,0), (w_1,2*R)),
  290. 'cross2': Line((w_1-R,R), (w_1+R,R))})})
  291. wheel2 = wheel1.copy()
  292. wheel2.translate((L,0))
  293. !ec
  294. Observe that `wheel1.copy()` copies all the objects that make
  295. up the first wheel, and `wheel2.translate` translates all
  296. the copied objects.
  297. FIGURE: [figs-sketcher/vehicle1.png, width=400] Wheels with spokes to show rotation. label{sketcher:fig:vehicle1}
  298. The `move_vehicle` function need to displace all the objects in the
  299. entire vehicle and also rotate the crosses in the wheels.
  300. The rotation angle follows from the fact that the arc length
  301. of a rolling wheel equals the displacement of the center of
  302. the wheel, leading to a rotation angle
  303. !bc pycod
  304. angle = - x_displacement/R
  305. !ec
  306. With `w_1` tracking the $x$ coordinate of the center
  307. of the front wheel, we can rotate that wheel by
  308. !bc pycod
  309. w1 = fig['vehicle']['wheels']['wheel1']
  310. from math import degrees
  311. w1.rotate(degrees(angle), center=(w_1, R))
  312. !ec
  313. The `rotate` function takes two parameters: the rotation angle
  314. (in degrees) and the center point of the rotation, which is the
  315. center of the wheel in this case. The other wheel is rotated by
  316. !bc pycod
  317. w2 = fig['vehicle']['wheels']['wheel2']
  318. w2.rotate(degrees(angle), center=(w_1 + L, R))
  319. !ec
  320. That is, the angle is the same, but the rotation point is different.
  321. The update of the center point is done by `w_1 += displacement[0]`.
  322. The complete `move_vehicle` function then becomes
  323. !bc pycod
  324. w_1 = w_1 + L # start position
  325. def move_vehicle(t, fig):
  326. x_displacement = dt*v(t)
  327. fig['vehicle'].translate((x_displacement, 0))
  328. # Rotate wheels
  329. global w_1
  330. w_1 += x_displacement
  331. # R*angle = -x_displacement
  332. angle = - x_displacement/R
  333. w1 = fig['vehicle']['wheels']['wheel1']
  334. w1.rotate(degrees(angle), center=(w_1, R))
  335. w2 = fig['vehicle']['wheels']['wheel2']
  336. w2.rotate(degrees(angle), center=(w_1 + L, R))
  337. !ec
  338. The complete example is found in the file
  339. # #ifdef PRIMER_BOOK
  340. `vehicle1.py`.
  341. # #else
  342. "`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".
  343. # #endif
  344. The great advantage with making figures this way through programming,
  345. rather than using interactive drawing programs, is that the
  346. accuracy of mathematical drawings is easily reflected, distances
  347. can be parameterized through variables such that the
  348. proportions of parts of the figure can be quickly changed,
  349. groups of objects can easily be manipulated (translated, rotated,
  350. scaled), objects can be left out while experimenting with the
  351. figure, to mention some important features. However, the single
  352. most important feature is the ability to make animations governed
  353. by mathematical formulas, maybe coming from simulations
  354. based on solving differential equations reflecting the physics
  355. of the problem.
  356. ===== Example of Classes for Geometric Objects =====
  357. We shall now explain how we can, quite easily, realize software
  358. with the capabilities demonstrated above. Each object in the
  359. figure is represented as a class in a class hierarchy. Using
  360. inheritance, classes can inherit properties from parent classes
  361. and add new geometric features.
  362. We introduce class `Shape` as superclass for all specialized objects
  363. in a figure. This class does not store any data, but provides a
  364. series of functions that add functionality to all the subclasses.
  365. This will be shown later.
  366. === Simple Geometric Objects ===
  367. One simple subclass is `Rectangle`:
  368. !bc pycod
  369. class Rectangle(Shape):
  370. def __init__(self, lower_left_corner, width, height):
  371. p = lower_left_corner # short form
  372. x = [p[0], p[0] + width,
  373. p[0] + width, p[0], p[0]]
  374. y = [p[1], p[1], p[1] + height,
  375. p[1] + height, p[1]]
  376. self.shapes = {'rectangle': Curve(x,y)}
  377. !ec
  378. Any subclass of `Shape` will have a constructor which takes
  379. geometric information about the shape of the object and
  380. creates a dictionary `self.shapes` with the shape built of
  381. simpler shapes. The most fundamental shape is `Curve`, which is
  382. just a collection of $(x,y)$ coordinates in two arrays `x` and `y`.
  383. Drawing the `Curve` object is a matter of plotting `y` versus `x`.
  384. The `Rectangle` class illustrates how the constructor takes information
  385. about the lower left corner, the width and the height, and
  386. creates coordinate arrays `x` and `y` consisting of the four corners,
  387. plus the first one repeated such that plotting `x` and `y` will
  388. form a closed four-sided rectangle. This construction procedure
  389. demands that the rectangle will always be aligned with the $x$ and
  390. $y$ axis. However, we may easily rotate the rectangle about
  391. any point once the object is constructed.
  392. Class `Line` constitutes a similar example:
  393. !bc pycod
  394. class Line(Shape):
  395. def __init__(self, start, end):
  396. x = [start[0], end[0]]
  397. y = [start[1], end[1]]
  398. self.shapes = {'line': Curve(x, y)}
  399. !ec
  400. Here we only need two points, the start and end point on the line.
  401. However, we may add some useful functionality, e.g., the ability
  402. to give an $x$ coordinate and have the class calculate the
  403. corresponding $y$ coordinate:
  404. !bc pycod
  405. def __call__(self, x):
  406. """Given x, return y on the line."""
  407. x, y = self.shapes['line'].x, self.shapes['line'].y
  408. self.a = (y[1] - y[0])/(x[1] - x[0])
  409. self.b = y[0] - self.a*x[0]
  410. return self.a*x + self.b
  411. !ec
  412. Unfortunately, this is too simplistic because vertical lines cannot
  413. be handled (infinte `self.a`). The source code of `Line` therefore
  414. provides a more general solution at the cost of significantly
  415. longer code with more tests.
  416. A circle gives us somewhat increased complexity. Again we represent
  417. the geometic object by a `Curve` object, but this time the `Curve`
  418. object needs to store a large number of points on the curve such
  419. that a plotting program produces a visually smooth curve.
  420. The points on the circle must be calculated manually in the constructor
  421. of class `Circle`. The formulas for points $(x,y)$ on a curve with radius
  422. $R$ and center at $(x_0, y_0)$ are given by
  423. !bt
  424. \begin{align*}
  425. x &= x_0 + R\cos (t),\\
  426. y &= y_0 + R\sin (t),
  427. \end{align*}
  428. !et
  429. where $t\in [0, 2\pi]$. A discrete set of $t$ values in this
  430. interval gives the corresponding set of $(x,y)$ coordinates on
  431. the circle. The user must specify the resolution, i.e., the number
  432. of $t$ values, or equivalently, points on the circle. The circle's
  433. radius and center must of course also be specified.
  434. We can write the `Circle` class as
  435. !bc pycod
  436. class Circle(Shape):
  437. def __init__(self, center, radius, resolution=180):
  438. self.center, self.radius = center, radius
  439. self.resolution = resolution
  440. t = linspace(0, 2*pi, resolution+1)
  441. x0 = center[0]; y0 = center[1]
  442. R = radius
  443. x = x0 + R*cos(t)
  444. y = y0 + R*sin(t)
  445. self.shapes = {'circle': Curve(x, y)}
  446. !ec
  447. As in class `Line` we can offer the possibility to give an angle
  448. $\theta$ (equivalent to $t$ in the formulas above)
  449. and then get the corresponding $x$ and $y$ coordinates:
  450. !bc pycod
  451. def __call__(self, theta):
  452. """Return (x, y) point corresponding to angle theta."""
  453. return self.center[0] + self.radius*cos(theta), \
  454. self.center[1] + self.radius*sin(theta)
  455. !ec
  456. There is one flaw with this method: it yields illegal values after
  457. a translation, scaling, or rotation of the circle.
  458. A part of a circle, an arc, is a frequent geometric object when
  459. drawing mechanical systems. The arc is constructed much like
  460. a circle, but $t$ runs in $[\theta_0, \theta_1]$. Giving
  461. $\theta_1$ and $\theta_2$ the slightly more descriptive names
  462. `start_angle` and `arc_angle`, the code looks like this:
  463. !bc pycod
  464. class Arc(Shape):
  465. def __init__(self, center, radius,
  466. start_angle, arc_angle,
  467. resolution=180):
  468. self.center = center
  469. self.radius = radius
  470. self.start_angle = start_angle*pi/180 # radians
  471. self.arc_angle = arc_angle*pi/180
  472. self.resolution = resolution
  473. t = linspace(self.start_angle,
  474. self.start_angle + self.arc_angle,
  475. resolution+1)
  476. x0 = center[0]; y0 = center[1]
  477. R = radius
  478. x = x0 + R*cos(t)
  479. y = y0 + R*sin(t)
  480. self.shapes = {'arc': Curve(x, y)}
  481. !ec
  482. Having the `Arc` class, a `Circle` can alternatively befined as
  483. a subclass specializing the arc to a circle:
  484. !bc pycod
  485. class Circle(Arc):
  486. def __init__(self, center, radius, resolution=180):
  487. Arc.__init__(self, center, radius, 0, 360, resolution)
  488. !ec
  489. A wall is about drawing a curve, displacing the curve vertically by
  490. some thickness, and then filling the space between the curves
  491. by some pattern. The input is the `x` and `y` coordinate arrays
  492. of the curve and a thickness parameter. The computed coordinates
  493. will be a polygon: going along the originally curve and then back again
  494. along the vertically displaced curve. The relevant code becomes
  495. !bc pycod
  496. class CurveWall(Shape):
  497. def __init__(self, x, y, thickness):
  498. # User's curve
  499. x1 = asarray(x, float)
  500. y1 = asarray(y, float)
  501. # Displaced curve (according to thickness)
  502. x2 = x1
  503. y2 = y1 + thickness
  504. # Combine x1,y1 with x2,y2 reversed
  505. from numpy import concatenate
  506. x = concatenate((x1, x2[-1::-1]))
  507. y = concatenate((y1, y2[-1::-1]))
  508. wall = Curve(x, y)
  509. wall.set_filled_curves(color='white', pattern='/')
  510. self.shapes = {'wall': wall}
  511. !ec
  512. === Class Curve ===
  513. Class `Curve` sits on the coordinates to be drawn, but how is
  514. that done? The constructor just stores the coordinates, while
  515. a method `draw` sends the coordinates to the plotting program
  516. to make a graph.
  517. Or more precisely, to avoid a lot of (e.g.) Matplotlib-specific
  518. plotting commands we have created a small layer with a
  519. simple programming interface to plotting programs. This makes it
  520. straightforward to change from Matplotlib to another plotting
  521. program. The programming interface is represented by the `drawing_tool`
  522. object and has a few functions:
  523. * `plot_curve` for sending a curve in terms of $x$ and $y$ coordinates
  524. to the plotting program,
  525. * `set_coordinate_system` for specifying the graphics area,
  526. * `erase` for deleting all elements of the graph,
  527. * `set_grid` for turning on a grid (convenient while constructing the plot),
  528. * `set_instruction_file` for creating a separate file with all
  529. plotting commands (Matplotlib commands in our case),
  530. * a series of `set_X` functions where `X` is some property like
  531. `linecolor`, `linestyle`, `linewidth`, `filled_curves`.
  532. This is basically all we need to communicate to a plotting program.
  533. Any class in the `Shape` hierarchy inherits `set_X` functions for
  534. setting properties of curves. This information is propagated to
  535. all other shape objects that make up the figure. Class
  536. `Curve` stores the line properties together with the coordinates
  537. of its curve and propagates this information to the plotting program.
  538. When saying `vehicle.set_linewidth(10)`, all objects that make
  539. up the `vehicle` object will get a `set_linewidth(10)` call,
  540. but only the `Curve` object at the end of the chain will actually
  541. store the information and send it to the plotting program.
  542. A rough sketch of class `Curve` reads
  543. !bc pycod
  544. class Curve(Shape):
  545. """General curve as a sequence of (x,y) coordintes."""
  546. def __init__(self, x, y):
  547. self.x = asarray(x, dtype=float)
  548. self.y = asarray(y, dtype=float)
  549. self.linestyle = None
  550. self.linewidth = None
  551. self.linecolor = None
  552. self.fillcolor = None
  553. self.fillpattern = None
  554. self.arrow = None
  555. def draw(self):
  556. drawing_tool.plot_curve(
  557. self.x, self.y,
  558. self.linestyle, self.linewidth, self.linecolor,
  559. self.arrow, self.fillcolor, self.fillpattern)
  560. def set_linewidth(self, width):
  561. self.linewidth = width
  562. det set_linestyle(self, style):
  563. self.linestyle = style
  564. ...
  565. !ec
  566. === Compound Geometric Objects ===
  567. The sample classes so far has managed to define the geometric shape
  568. through just one `Curve` object.
  569. Some objects in a figure will be associated with a point and not
  570. a curve. Therefore, it is natural to introduce a `Point` class
  571. as superclass for such objects:
  572. !bc pycod
  573. class Point(Shape):
  574. def __init__(self, x, y):
  575. self.x, self.y = x, y
  576. !ec
  577. A candidate for subclass is a text located at a given point:
  578. !bc pycod
  579. class Text(Point):
  580. def __init__(self, text, position, alignment='center', fontsize=18):
  581. self.text = text
  582. self.alignment, self.fontsize = alignment, fontsize
  583. is_sequence(position, length=2, can_be_None=True)
  584. Point.__init__(self, position[0], position[1])
  585. #no need for self.shapes here
  586. !ec
  587. [[[[[[[[[[[
  588. Class `Line` is a subclass of `Shape` and
  589. represents the simplest shape: a stright line between two points.
  590. Class `Rectangle` is another subclass of `Shape`, implementing the
  591. functionality needed to specify the four lines of a rectangle.
  592. Class `Circle` can be yet another subclass of `Shape`, or
  593. we may have a class `Arc` and let `Circle` be a subclass
  594. of `Arc` since a circle is an arc of 360 degrees.
  595. Class `Wheel`
  596. is also subclass of `Shape`, but it contains
  597. naturally two `Circle` instances for the inner and outer circles,
  598. plus a set of `Line` instances
  599. going from the inner to the outer circles.
  600. The discussion in the previous paragraph shows that a subclass in
  601. the `Shape` hierarchy typically contains a list of
  602. other subclass instances, *or* the shape is a primitive, such as a line,
  603. circle, or rectangle, where the geometry is defined through a set of
  604. $(x,y)$ coordinates rather than through other `Shape` instances.
  605. It turns out that the implementation is simplest if we introduce
  606. a class `Curve` for holding a primitive shape defined by
  607. $(x,y)$ coordinates. Then all other subclasses of `Shape` can
  608. have a list `shapes` holding the various instances of subclasses of
  609. `Shape` needed to
  610. build up the geometric object. The `shapes`
  611. attribute in class `Circle` will contain
  612. one `Curve` instance for holding the coordinates along the circle,
  613. while the `shapes` attribute in class `Wheel` contains
  614. two `Circle` instances and a number of `Line` instances.
  615. Figures ref{fig:oo:Rectangle:fig} and ref{fig:oo:Wheel:fig}
  616. display two UML drawings of the `shapes` class hierarchy where we
  617. can get a view of how `Rectangle` and `Wheel` relate to other classes:
  618. the darkest arrows represent is-a relationship while the lighter arrows
  619. represent has-a relationship.
  620. All instances in the `Shape` hierarchy must have a `draw` method.
  621. The `draw` method in class `Curve` plots the $(x,y)$ coordinates
  622. as a curve, while the `draw` method in all other classes simply
  623. draws all the shapes that make up the particular figure of the class:
  624. !bc cod
  625. for shape in self.shapes:
  626. shape.draw()
  627. !ec
  628. \begin{figure}
  629. \centerline{\psfig{figure=figs/lumpy_Rectangle_shapes_hier.ps,width=0.5\linewidth}}
  630. \caption{ label{fig:oo:Rectangle:fig}
  631. UML diagram of parts of the `shapes` hierarchy. Classes `Rectangle`
  632. and `Curve` are subclasses of `Shape`. The darkest arrow with
  633. the biggest arrowhead indicates inheritance and is-a
  634. relationship: `Rectangle` and `Curve` are both also `Shape`.
  635. The lighter arrow
  636. indicates {has-a} relationship:
  637. `Rectangle` has a `Curve`, and a `Curve` has a
  638. `NumPyArray`.
  639. }
  640. \end{figure}
  641. \begin{figure}
  642. \centerline{\psfig{figure=figs/lumpy_Wheel_shapes_hier.ps,width=0.7\linewidth}}
  643. \caption{ label{fig:oo:Wheel:fig}
  644. This is a variant of Figure ref{fig:oo:Rectangle:fig} where we
  645. display how class `Wheel` relates to other classes in the
  646. `shapes` hierarchy.
  647. `Wheel` is a `Shape`, like `Arc`, `Line`, and `Curve`,
  648. but `Wheel` contains `Circle` and `Line` objects,
  649. while the `Circle` and `Line` objects have a `Curve`, which has a
  650. `NumPyArray`. We also see that `Circle` is a subclass of `Arc`.
  651. }
  652. \end{figure}
  653. ===== The Drawing Tool =====
  654. We have in Chapter ref{ch:plot} introduced the Easyviz tool for
  655. plotting graphs. This tool is quite well suited for drawing
  656. geometric shapes defined in terms of curves, but when drawing shapes
  657. we often want to skip ticmarks on the axis, labeling of the curves and
  658. axis, and perform other adjustments. Instead of using Easyviz, which
  659. aims at function plotting, we
  660. have decided to use a plotting tool directly and fine-tune the few
  661. commands we need for drawing shapes.
  662. A simple plotting tool for shapes is based on Gnuplot and implemented in
  663. class `GnuplotDraw` in the file `GnuplotDraw.py`.
  664. This class has the following user interface:
  665. !bc cod
  666. class GnuplotDraw:
  667. def __init__(self, xmin, xmax, ymin, ymax):
  668. """Define the drawing area [xmin,xmax]x[ymin,ymax]."""
  669. def define_curve(self, x, y):
  670. """Define a curve with coordinates x and y (arrays)."""
  671. def erase(self):
  672. """Erase the current figure."""
  673. def display(self):
  674. """Display the figure."""
  675. def hardcopy(self, name):
  676. """Save figure in PNG file name.png."""
  677. def set_linecolor(self, color):
  678. """Change the color of lines."""
  679. def set_linewidth(self, width):
  680. """Change the line width (int, starts at 1)."""
  681. def filled_curves(self, on=True):
  682. """Fill area inside curves with current line color."""
  683. !ec
  684. One can easily make a similar class with an identical interface that
  685. applies another plotting package than Gnuplot to create the drawings.
  686. In particular, encapsulating the drawing actions in such a class makes
  687. it trivial to change the drawing program in the future.
  688. The program pieces that apply a drawing tool like `GnuplotDraw`
  689. remain the same. This is an important strategy to follow, especially
  690. when developing larger software systems.
  691. ===== Implementation of Shape Classes =====
  692. label{sec:oo:shape:impl}
  693. Our superclass `Shape` can naturally hold a coordinate system
  694. specification, i.e., the rectangle in which other shapes can be drawn.
  695. This area is fixed for all shapes, so the associated variables should
  696. be static and the method for setting them should also be static
  697. (see Chapter ref{sec:class:static} for static attributes and methods).
  698. It is also natural that class `Shape` holds access to a drawing
  699. tool, in our case a `GnuplotDraw` instance. This object
  700. is also static.
  701. However, it can be an advantage to mirror the static attributes and methods
  702. as global variables and functions in the `shapes` modules.
  703. Users not familiar with static class items can drop the `Shape`
  704. prefix and just use plain module variables and functions. This is what
  705. we do in the application examples.
  706. Class `Shape` defines an imporant method, `draw`, which
  707. just calls the `draw` method for all subshapes that build up the
  708. current shape.
  709. Here is a brief view of class `Shape`\footnote{We have
  710. for simplicity omitted
  711. the static attributes
  712. and methods. These can be viewed in the `shapes.py` file.}:
  713. !bc cod
  714. class Shape:
  715. def __init__(self):
  716. self.shapes = self.subshapes()
  717. if isinstance(self.shapes, Shape):
  718. self.shapes = [self.shapes] # turn to list
  719. def subshapes(self):
  720. """Define self.shapes as list of Shape instances."""
  721. raise NotImplementedError(self.__class__.__name__)
  722. def draw(self):
  723. for shape in self.shapes:
  724. shape.draw()
  725. !ec
  726. In class `Shape` we require the `shapes` attribute to be a
  727. list, but if the `subshape` method in subclasses returns just one
  728. instance, this is automatically wrapped in a list in the constructor.
  729. First we implement the special case class `Curve`, which does not
  730. have subshapes but instead $(x,y)$ coordinates for a curve:
  731. !bc cod
  732. class Curve(Shape):
  733. """General (x,y) curve with coordintes."""
  734. def __init__(self, x, y):
  735. self.x, self.y = x, y
  736. # Turn to Numerical Python arrays
  737. self.x = asarray(self.x, float)
  738. self.y = asarray(self.y, float)
  739. Shape.__init__(self)
  740. def subshapes(self):
  741. pass # geometry defined in constructor
  742. !ec
  743. # In Python, `Curve` does not need to be a subclass of `Shape`.
  744. # It could in fact be natural to remove the `subshapes` method and
  745. # the inheritance from `Shape`. In other languages where all
  746. # elements in `self.shapes` need to be instances of classes in the
  747. # `Shape` hierarchy, because all list elements must have a fixed
  748. # and specified type, `Curve` must be a subclass of `Shape`.
  749. The simplest ordinary `Shape` class is `Line`:
  750. !bc cod
  751. class Line(Shape):
  752. def __init__(self, start, stop):
  753. self.start, self.stop = start, stop
  754. Shape.__init__(self)
  755. def subshapes(self):
  756. x = [self.start[0], self.stop[0]]
  757. y = [self.start[1], self.stop[1]]
  758. return Curve(x,y)
  759. !ec
  760. The code in this class works with `start` and `stop` as
  761. tuples, lists, or arrays of length
  762. two, holding the end points of the line.
  763. The underlying `Curve` object needs only these two end points.
  764. A rectangle is represented by a slightly more complicated class, having the
  765. lower left corner, the width, and the height of the rectangle as
  766. attributes:
  767. !bc cod
  768. class Rectangle(Shape):
  769. def __init__(self, lower_left_corner, width, height):
  770. self.lower_left_corner = lower_left_corner # 2-tuple
  771. self.width, self.height = width, height
  772. Shape.__init__(self)
  773. def subshapes(self):
  774. ll = self.lower_left_corner # short form
  775. x = [ll[0], ll[0]+self.width,
  776. ll[0]+self.width, ll[0], ll[0]]
  777. y = [ll[1], ll[1], ll[1]+self.height,
  778. ll[1]+self.height, ll[1]]
  779. return Curve(x,y)
  780. !ec
  781. Class `Circle` needs many coordinates in its `Curve` object
  782. in order to display a smooth circle. We can provide the number of
  783. straight line segments along the circle as a parameter `resolution`.
  784. Using a default value of 180 means that each straight line segment
  785. approximates an arc of 2 degrees. This resolution should be sufficient
  786. for visual purposes. The set of coordinates along a circle with radius $R$
  787. and center $(x_0,y_0)$ is defined by
  788. !bt
  789. \begin{align}
  790. x &= x_0 + R\cos(t), label{sec:oo:circle:eq1}\\
  791. y &= y_0 + R\sin(t), label{sec:oo:circle:eq2}
  792. \end{align}
  793. !et
  794. for `resolution+1` $t$ values between $0$ and $2\pi$.
  795. The vectorized code for computing the coordinates becomes
  796. !bc cod
  797. t = linspace(0, 2*pi, self.resolution+1)
  798. x = x0 + R*cos(t)
  799. y = y0 + R*sin(t)
  800. !ec
  801. The complete `Circle` class is shown below:
  802. !bc cod
  803. class Circle(Shape):
  804. def __init__(self, center, radius, resolution=180):
  805. self.center, self.radius = center, radius
  806. self.resolution = resolution
  807. Shape.__init__(self)
  808. def subshapes(self):
  809. t = linspace(0, 2*pi, self.resolution+1)
  810. x0 = self.center[0]; y0 = self.center[1]
  811. R = self.radius
  812. x = x0 + R*cos(t)
  813. y = y0 + R*sin(t)
  814. return Curve(x,y)
  815. !ec
  816. We can also introduce class `Arc` for drawing the arc of a circle.
  817. Class `Arc` could be a subclass of `Circle`, extending the
  818. latter with two additional parameters: the opening of the arc
  819. (in degrees) and the starting $t$ value in
  820. (ref{sec:oo:circle:eq1})--(ref{sec:oo:circle:eq2}).
  821. The implementation of class `Arc` will then be almost a copy of
  822. the implementation of class `Circle`. The `subshapes` method
  823. will just define a different `t` array.
  824. Another view is to let class `Arc` be a subclass of `Shape`,
  825. and `Circle` a subclass of `Arc`, since a circle is an arc
  826. of 360 degrees. Let us employ this idea:
  827. !bc cod
  828. class Arc(Shape):
  829. def __init__(self, center, radius,
  830. start_degrees, opening_degrees, resolution=180):
  831. self.center = center
  832. self.radius = radius
  833. self.start_degrees = start_degrees*pi/180
  834. self.opening_degrees = opening_degrees*pi/180
  835. self.resolution = resolution
  836. Shape.__init__(self)
  837. def subshapes(self):
  838. t = linspace(self.start_degrees,
  839. self.start_degrees + self.opening_degrees,
  840. self.resolution+1)
  841. x0 = self.center[0]; y0 = self.center[1]
  842. R = self.radius
  843. x = x0 + R*cos(t)
  844. y = y0 + R*sin(t)
  845. return Curve(x,y)
  846. class Circle(Arc):
  847. def __init__(self, center, radius, resolution=180):
  848. Arc.__init__(self, center, radius, 0, 360, resolution)
  849. !ec
  850. In this latter implementation, we save a lot of code in class
  851. `Circle` since all of class `Arc` can be reused.
  852. Class `Wheel` may conceptually be a subclass of `Circle`.
  853. One circle, say the outer, is inherited and the subclass must have
  854. the inner circle as an attribute. Because of this "asymmetric"
  855. representation of the two circles in a wheel, we find it more natural
  856. to derive `Wheel` directly from `Shape`, and have the two
  857. circles as two attributes of type `Circle`:
  858. !bc cod
  859. class Wheel(Shape):
  860. def __init__(self, center, radius, inner_radius=None, nlines=10):
  861. self.center = center
  862. self.radius = radius
  863. if inner_radius is None:
  864. self.inner_radius = radius/5.0
  865. else:
  866. self.inner_radius = inner_radius
  867. self.nlines = nlines
  868. Shape.__init__(self)
  869. !ec
  870. If the radius of the inner circle
  871. is not defined (`None`)
  872. we take it as 1/5 of the radius of the outer circle.
  873. The wheel is naturally composed of two `Circle` instances and
  874. `nlines` `Line` instances:
  875. !bc cod
  876. def subshapes(self):
  877. outer = Circle(self.center, self.radius)
  878. inner = Circle(self.center, self.inner_radius)
  879. lines = []
  880. t = linspace(0, 2*pi, self.nlines)
  881. Ri = self.inner_radius; Ro = self.radius
  882. x0 = self.center[0]; y0 = self.center[1]
  883. xinner = x0 + Ri*cos(t)
  884. yinner = y0 + Ri*sin(t)
  885. xouter = x0 + Ro*cos(t)
  886. youter = y0 + Ro*sin(t)
  887. lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
  888. zip(xinner, yinner, xouter, youter)]
  889. return [outer, inner] + lines
  890. !ec
  891. For the fun of it, we can implement other shapes, say a sine wave
  892. !bt
  893. \begin{equation*}y = m + A\sin kx,\quad k=2\pi /\lambda,\end{equation*}
  894. !et
  895. where $\lambda$ is the wavelength
  896. of the sine waves, $A$ is the wave amplitude, and $m$ is the mean
  897. value of the wave. The class looks like
  898. !bc cod
  899. class Wave(Shape):
  900. def __init__(self, xstart, xstop,
  901. wavelength, amplitude, mean_level):
  902. self.xstart = xstart
  903. self.xstop = xstop
  904. self.wavelength = wavelength
  905. self.amplitude = amplitude
  906. self.mean_level = mean_level
  907. Shape.__init__(self)
  908. def subshapes(self):
  909. npoints = (self.xstop - self.xstart)/(self.wavelength/61.0)
  910. x = linspace(self.xstart, self.xstop, npoints)
  911. k = 2*pi/self.wavelength # frequency
  912. y = self.mean_level + self.amplitude*sin(k*x)
  913. return Curve(x,y)
  914. !ec
  915. With this and the previous example, you should be in a position to
  916. write your own subclasses. Exercises~ref{sec:oo:ex11}--ref{sec:oo:ex13b}
  917. suggest some smaller projects.
  918. # ===== A Class for Drawing Springs =====
  919. #
  920. # Give: bottom point $B$, number of spring tags $n$, length $L$. Assume that
  921. # $L/3$ is the bottom and top vertical line and that the tags
  922. # are in the middle $L/3$. The width of the tags, $w$, can be either fixed to
  923. # a number or relative to $L$ (say $L/10$ -- need two variables, one
  924. # fixed true/false and one value).
  925. #
  926. #
  927. # \notready
  928. __Functions for Controlling Lines, Colors, etc.__
  929. The `shapes` module containing class `Shape` and all subclasses
  930. mentioned above, also offers some additional functions that do not
  931. depend on any particular shape:
  932. * `display()` for displaying the defined figures so far (all figures whose `draw` method is called).
  933. * `erase()` for ereasing the current figure.
  934. * `hardcopy(name)` for saving the current figure to a PNG file `name.png`.
  935. * `set_linecolor(color)` for setting the color of lines, where
  936. `color` is a string like `'red'` (default), `'blue'`, `'green'`, `'aqua'`, `'purple'`, `'yellow'`, and `'black'`.
  937. * `set_linewidth(width)` for setting the width of a line, measured as an integer (default is 2).
  938. * `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.
  939. Actually, the functions above are static methods in class `Shape`
  940. (cf.~Chapter ref{sec:class:static}),
  941. and they are
  942. just mirrored
  943. as global functions\footnote{You can look into `shapes.py` to see how
  944. we automate the duplication of static methods as global functions.}
  945. in the `shapes` module.
  946. Users without knowledge of static methods do not need to use the
  947. `Shape` prefix for reaching this functionality.
  948. ===== Scaling, Translating, and Rotating a Figure =====
  949. label{sec:oo:scaling}
  950. The real power of object-oriented programming will be obvious in a
  951. minute when we, with a few lines of code, suddenly can
  952. equip *all* shape objects
  953. with additional functionality for scaling, translating, and rotating the
  954. figure.
  955. __Scaling.__
  956. Let us first treat the simplest of the three cases: scaling.
  957. For a `Curve` instance containing a set of $n$ coordinates
  958. $(x_i,y_i)$ that make up a curve, scaling by
  959. a factor $a$ means that we multiply all the $x$ and $y$ coordinates
  960. by $a$:
  961. !bt
  962. \begin{equation*} x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,\quad i=0,\ldots,n-1\thinspace . \end{equation*}
  963. !et
  964. Here we apply the arrow as an assignment operator.
  965. The corresponding Python implementation in
  966. class `Curve` reads
  967. !bc cod
  968. class Curve:
  969. ...
  970. def scale(self, factor):
  971. self.x = factor*self.x
  972. self.y = factor*self.y
  973. !ec
  974. Note here that `self.x` and `self.y` are Numerical Python arrays,
  975. so that multiplication by a scalar number `factor` is
  976. a vectorized operation.
  977. In an instance of a subclass of `Shape`,
  978. the meaning of a method `scale` is
  979. to run through all objects in the list `self.shapes` and ask
  980. each object to scale itself. This is the same delegation of actions
  981. to subclass instances as we do in the `draw` method, and
  982. all objects, except `Curve` instances, can share the same
  983. implementation of the `scale` method. Therefore, we place
  984. the `scale` method in the superclass `Shape` such that all
  985. subclasses can inherit this method.
  986. Since `scale` and `draw` are so similar,
  987. we can easily implement the `scale` method in class `Shape` by
  988. copying and editing the `draw` method:
  989. !bc cod
  990. class Shape:
  991. ...
  992. def scale(self, factor):
  993. for shape in self.shapes:
  994. shape.scale(factor)
  995. !ec
  996. This is all we have to do in order to equip all subclasses of
  997. `Shape` with scaling functionality! But why is it so easy?
  998. All subclasses inherit `scale` from class `Shape`.
  999. Say we have a subclass instance `s` and that we call
  1000. `s.scale(factor)`. This leads to calling the inherited `scale`
  1001. method shown above, and in the `for` loop we call the
  1002. `scale` method for each `shape` object in the `self.shapes` list.
  1003. If `shape` is not a `Curve` object, this procedure repeats,
  1004. until we hit a `shape` that is a `Curve`, and then
  1005. the scaling on that set of coordinates is performed.
  1006. __Translation.__
  1007. A set of coordinates $(x_i, y_i)$ can be translated $x$ units in
  1008. the $x$ direction and $y$ units in the $y$ direction using the formulas
  1009. !bt
  1010. \begin{equation*} x_i\leftarrow x+x_i,\quad y_i\leftarrow y + y_i,\quad i=0,\ldots,n-1\thinspace . \end{equation*}
  1011. !et
  1012. The corresponding Python implementation in class `Curve` becomes
  1013. !bc cod
  1014. class Curve:
  1015. ...
  1016. def translate(self, x, y):
  1017. self.x = x + self.x
  1018. self.y = y + self.y
  1019. !ec
  1020. The translation operation for a shape object is very similar to the
  1021. scaling and drawing operations. This means that we can implement a
  1022. common method `translate` in the superclass `Shape`. The code
  1023. is parallel to the `scale` method:
  1024. !bc cod
  1025. class Shape:
  1026. ....
  1027. def translate(self, x, y):
  1028. for shape in self.shapes:
  1029. shape.translate(x, y)
  1030. !ec
  1031. __Rotation.__
  1032. Rotating a figure is more complicated than scaling and translating.
  1033. A counter clockwise rotation of $\theta$ degrees for a set of
  1034. coordinates $(x_i,y_i)$ is given by
  1035. !bt
  1036. \begin{align*}
  1037. \bar x_i &\leftarrow& x_i\cos\theta - y_i\sin\theta,\\
  1038. \bar y_i &\leftarrow& x_i\sin\theta + y_i\cos\theta\thinspace .
  1039. \end{align*}
  1040. !et
  1041. This rotation is performed around the origin. If we want the figure
  1042. to be rotated with respect to a general point $(x,y)$, we need to
  1043. extend the formulas above:
  1044. !bt
  1045. \begin{align*}
  1046. \bar x_i &\leftarrow& x + (x_i -x)\cos\theta - (y_i -y)\sin\theta,\\
  1047. \bar y_i &\leftarrow& y + (x_i -x)\sin\theta + (y_i -y)\cos\theta\thinspace .
  1048. \end{align*}
  1049. !et
  1050. The Python implementation in class `Curve`, assuming that $\theta$
  1051. is given in degrees and not in radians, becomes
  1052. !bc cod
  1053. def rotate(self, angle, x=0, y=0):
  1054. angle = angle*pi/180
  1055. c = cos(angle); s = sin(angle)
  1056. xnew = x + (self.x - x)*c - (self.y - y)*s
  1057. ynew = y + (self.x - x)*s + (self.y - y)*c
  1058. self.x = xnew
  1059. self.y = ynew
  1060. !ec
  1061. The `rotate` method in class `Shape` is identical to the
  1062. `draw`, `scale`, and `translate` methods except that we
  1063. have other arguments:
  1064. !bc cod
  1065. class Shape:
  1066. ....
  1067. def rotate(self, angle, x=0, y=0):
  1068. for shape in self.shapes:
  1069. shape.rotate(angle, x, y)
  1070. !ec
  1071. __Application: Rolling Wheel.__
  1072. To demonstrate the effect of translation and rotation we can roll
  1073. a wheel on the screen. First we draw the wheel and rotate it a bit
  1074. to demonstrate the basic operations:
  1075. !bc cod
  1076. center = (6,2) # the wheel's center point
  1077. w1 = Wheel(center=center, radius=2, inner_radius=0.5, nlines=7)
  1078. # rorate the wheel 2 degrees around its center point:
  1079. w1.rotate(angle=2, center[0], center[1])
  1080. w1.draw()
  1081. display()
  1082. !ec
  1083. Now we want to roll the wheel by making many such small rotations.
  1084. At the same time we need to translate the wheel since rolling
  1085. an arc length $L=R\theta$, where $\theta$ is the rotation angle
  1086. (in radians) and $R$ is the outer radius of the wheel, implies that
  1087. the center point moves a distance $L$ to the left ($\theta >0$
  1088. means counter clockwise rotation).
  1089. In code we must therefore combine rotation with translation:
  1090. !bc cod
  1091. L = radius*angle*pi/180 # translation = arc length
  1092. w1.rotate(angle, center[0], center[1])
  1093. w1.translate(-L, 0)
  1094. center = (center[0] - L, center[1])
  1095. !ec
  1096. We are now in a position to put the rotation and translation
  1097. operations in a `for` loop and make a complete function:
  1098. !bc cod
  1099. def rolling_wheel(total_rotation_angle):
  1100. """Animation of a rotating wheel."""
  1101. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1102. center = (6,2)
  1103. radius = 2.0
  1104. angle = 2.0
  1105. w1 = Wheel(center=center, radius=radius,
  1106. inner_radius=0.5, nlines=7)
  1107. for i in range(int(total_rotation_angle/angle)):
  1108. w1.draw()
  1109. display()
  1110. L = radius*angle*pi/180 # translation = arc length
  1111. w1.rotate(angle, center[0], center[1])
  1112. w1.translate(-L, 0)
  1113. center = (center[0] - L, center[1])
  1114. erase()
  1115. !ec
  1116. To control the visual "velocity" of the wheel, we can insert a pause
  1117. between each frame in the `for` loop. A call to
  1118. `time.sleep(s)`, where `s` is the length of
  1119. the pause in seconds, can do this for us.
  1120. Another convenient feature is to save each frame drawn in the `for`
  1121. loop as a hardcopy in PNG format and then, after the loop,
  1122. make an animated GIF file based on the individual PNG frames.
  1123. The latter operation is performed either by the `movie` function
  1124. from `scitools.std`
  1125. or by the `convert` program from
  1126. the ImageMagick suite. With the latter you write the following command
  1127. in a terminal window:
  1128. !bc ccq
  1129. convert -delay 50 -loop 1000 xxx tmp_movie.gif
  1130. !ec
  1131. Here, `xxx` is a space-separated list of all the PNG files, and
  1132. `tmp_movie.gif` is the name of the resulting animated GIF file.
  1133. We can easily make `xxx` by collecting the names of the PNG files
  1134. from the loop in a list object, and then join the names.
  1135. The `convert` command can be run as an `os.system` call.
  1136. The complete `rolling_wheel` function, incorporating the
  1137. mentioned movie making, will then be
  1138. !bc cod
  1139. def rolling_wheel(total_rotation_angle):
  1140. """Animation of a rotating wheel."""
  1141. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1142. import time
  1143. center = (6,2)
  1144. radius = 2.0
  1145. angle = 2.0
  1146. pngfiles = []
  1147. w1 = Wheel(center=center, radius=radius,
  1148. inner_radius=0.5, nlines=7)
  1149. for i in range(int(total_rotation_angle/angle)):
  1150. w1.draw()
  1151. display()
  1152. filename = 'tmp_
  1153. #03d' i
  1154. pngfiles.append(filename + '.png')
  1155. hardcopy(filename)
  1156. time.sleep(0.3) # pause 0.3 sec
  1157. L = radius*angle*pi/180 # translation = arc length
  1158. w1.rotate(angle, center[0], center[1])
  1159. w1.translate(-L, 0)
  1160. center = (center[0] - L, center[1])
  1161. erase() # erase the screen before new figure
  1162. cmd = 'convert -delay 50 -loop 1000
  1163. #s tmp_movie.gif' \
  1164. # (' '.join(pngfiles))
  1165. import commands
  1166. failure, output = commands.getstatusoutput(cmd)
  1167. if failure: print 'Could not run', cmd
  1168. !ec
  1169. The last two lines run a command, from Python, as we would run the
  1170. command in a terminal window.
  1171. The resulting animated GIF file can be viewed with
  1172. `animate tmp_movie.gif` as a command in a terminal window.