sketcher.do.txt 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  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 advantages with making figures this way through programming,
  345. rather than using interactive drawing programs, are numerous. For
  346. example, the objects are parameterized by variables so that various
  347. dimensions can easily be changed. Subparts of the figure can change
  348. color, linetype, filling or other properties through a single function
  349. call. Subparts of the figure can be rotated, translated, or scaled.
  350. Subparts of the figure can be copied and moved to other parts of the
  351. drawing area. However, the single most important feature is probably
  352. the ability to make animations governed by mathematical formulas or
  353. data coming from physics simulations of the problem sketched in
  354. the drawing.
  355. ===== Example of Classes for Geometric Objects =====
  356. We shall now explain how we can, quite easily, realize software
  357. with the capabilities demonstrated above. Each object in the
  358. figure is represented as a class in a class hierarchy. Using
  359. inheritance, classes can inherit properties from parent classes
  360. and add new geometric features.
  361. We introduce class `Shape` as superclass for all specialized objects
  362. in a figure. This class does not store any data, but provides a
  363. series of functions that add functionality to all the subclasses.
  364. This will be shown later.
  365. === Simple Geometric Objects ===
  366. One simple subclass is `Rectangle`:
  367. !bc pycod
  368. class Rectangle(Shape):
  369. def __init__(self, lower_left_corner, width, height):
  370. p = lower_left_corner # short form
  371. x = [p[0], p[0] + width,
  372. p[0] + width, p[0], p[0]]
  373. y = [p[1], p[1], p[1] + height,
  374. p[1] + height, p[1]]
  375. self.shapes = {'rectangle': Curve(x,y)}
  376. !ec
  377. Any subclass of `Shape` will have a constructor which takes
  378. geometric information about the shape of the object and
  379. creates a dictionary `self.shapes` with the shape built of
  380. simpler shapes. The most fundamental shape is `Curve`, which is
  381. just a collection of $(x,y)$ coordinates in two arrays `x` and `y`.
  382. Drawing the `Curve` object is a matter of plotting `y` versus `x`.
  383. The `Rectangle` class illustrates how the constructor takes information
  384. about the lower left corner, the width and the height, and
  385. creates coordinate arrays `x` and `y` consisting of the four corners,
  386. plus the first one repeated such that plotting `x` and `y` will
  387. form a closed four-sided rectangle. This construction procedure
  388. demands that the rectangle will always be aligned with the $x$ and
  389. $y$ axis. However, we may easily rotate the rectangle about
  390. any point once the object is constructed.
  391. Class `Line` constitutes a similar example:
  392. !bc pycod
  393. class Line(Shape):
  394. def __init__(self, start, end):
  395. x = [start[0], end[0]]
  396. y = [start[1], end[1]]
  397. self.shapes = {'line': Curve(x, y)}
  398. !ec
  399. Here we only need two points, the start and end point on the line.
  400. However, we may add some useful functionality, e.g., the ability
  401. to give an $x$ coordinate and have the class calculate the
  402. corresponding $y$ coordinate:
  403. !bc pycod
  404. def __call__(self, x):
  405. """Given x, return y on the line."""
  406. x, y = self.shapes['line'].x, self.shapes['line'].y
  407. self.a = (y[1] - y[0])/(x[1] - x[0])
  408. self.b = y[0] - self.a*x[0]
  409. return self.a*x + self.b
  410. !ec
  411. Unfortunately, this is too simplistic because vertical lines cannot
  412. be handled (infinte `self.a`). The source code of `Line` therefore
  413. provides a more general solution at the cost of significantly
  414. longer code with more tests.
  415. A circle gives us somewhat increased complexity. Again we represent
  416. the geometic object by a `Curve` object, but this time the `Curve`
  417. object needs to store a large number of points on the curve such
  418. that a plotting program produces a visually smooth curve.
  419. The points on the circle must be calculated manually in the constructor
  420. of class `Circle`. The formulas for points $(x,y)$ on a curve with radius
  421. $R$ and center at $(x_0, y_0)$ are given by
  422. !bt
  423. \begin{align*}
  424. x &= x_0 + R\cos (t),\\
  425. y &= y_0 + R\sin (t),
  426. \end{align*}
  427. !et
  428. where $t\in [0, 2\pi]$. A discrete set of $t$ values in this
  429. interval gives the corresponding set of $(x,y)$ coordinates on
  430. the circle. The user must specify the resolution, i.e., the number
  431. of $t$ values, or equivalently, points on the circle. The circle's
  432. radius and center must of course also be specified.
  433. We can write the `Circle` class as
  434. !bc pycod
  435. class Circle(Shape):
  436. def __init__(self, center, radius, resolution=180):
  437. self.center, self.radius = center, radius
  438. self.resolution = resolution
  439. t = linspace(0, 2*pi, resolution+1)
  440. x0 = center[0]; y0 = center[1]
  441. R = radius
  442. x = x0 + R*cos(t)
  443. y = y0 + R*sin(t)
  444. self.shapes = {'circle': Curve(x, y)}
  445. !ec
  446. As in class `Line` we can offer the possibility to give an angle
  447. $\theta$ (equivalent to $t$ in the formulas above)
  448. and then get the corresponding $x$ and $y$ coordinates:
  449. !bc pycod
  450. def __call__(self, theta):
  451. """Return (x, y) point corresponding to angle theta."""
  452. return self.center[0] + self.radius*cos(theta), \
  453. self.center[1] + self.radius*sin(theta)
  454. !ec
  455. There is one flaw with this method: it yields illegal values after
  456. a translation, scaling, or rotation of the circle.
  457. A part of a circle, an arc, is a frequent geometric object when
  458. drawing mechanical systems. The arc is constructed much like
  459. a circle, but $t$ runs in $[\theta_0, \theta_1]$. Giving
  460. $\theta_1$ and $\theta_2$ the slightly more descriptive names
  461. `start_angle` and `arc_angle`, the code looks like this:
  462. !bc pycod
  463. class Arc(Shape):
  464. def __init__(self, center, radius,
  465. start_angle, arc_angle,
  466. resolution=180):
  467. self.center = center
  468. self.radius = radius
  469. self.start_angle = start_angle*pi/180 # radians
  470. self.arc_angle = arc_angle*pi/180
  471. self.resolution = resolution
  472. t = linspace(self.start_angle,
  473. self.start_angle + self.arc_angle,
  474. resolution+1)
  475. x0 = center[0]; y0 = center[1]
  476. R = radius
  477. x = x0 + R*cos(t)
  478. y = y0 + R*sin(t)
  479. self.shapes = {'arc': Curve(x, y)}
  480. !ec
  481. Having the `Arc` class, a `Circle` can alternatively befined as
  482. a subclass specializing the arc to a circle:
  483. !bc pycod
  484. class Circle(Arc):
  485. def __init__(self, center, radius, resolution=180):
  486. Arc.__init__(self, center, radius, 0, 360, resolution)
  487. !ec
  488. A wall is about drawing a curve, displacing the curve vertically by
  489. some thickness, and then filling the space between the curves
  490. by some pattern. The input is the `x` and `y` coordinate arrays
  491. of the curve and a thickness parameter. The computed coordinates
  492. will be a polygon: going along the originally curve and then back again
  493. along the vertically displaced curve. The relevant code becomes
  494. !bc pycod
  495. class CurveWall(Shape):
  496. def __init__(self, x, y, thickness):
  497. # User's curve
  498. x1 = asarray(x, float)
  499. y1 = asarray(y, float)
  500. # Displaced curve (according to thickness)
  501. x2 = x1
  502. y2 = y1 + thickness
  503. # Combine x1,y1 with x2,y2 reversed
  504. from numpy import concatenate
  505. x = concatenate((x1, x2[-1::-1]))
  506. y = concatenate((y1, y2[-1::-1]))
  507. wall = Curve(x, y)
  508. wall.set_filled_curves(color='white', pattern='/')
  509. self.shapes = {'wall': wall}
  510. !ec
  511. === Class Curve ===
  512. Class `Curve` sits on the coordinates to be drawn, but how is
  513. that done? The constructor just stores the coordinates, while
  514. a method `draw` sends the coordinates to the plotting program
  515. to make a graph.
  516. Or more precisely, to avoid a lot of (e.g.) Matplotlib-specific
  517. plotting commands we have created a small layer with a
  518. simple programming interface to plotting programs. This makes it
  519. straightforward to change from Matplotlib to another plotting
  520. program. The programming interface is represented by the `drawing_tool`
  521. object and has a few functions:
  522. * `plot_curve` for sending a curve in terms of $x$ and $y$ coordinates
  523. to the plotting program,
  524. * `set_coordinate_system` for specifying the graphics area,
  525. * `erase` for deleting all elements of the graph,
  526. * `set_grid` for turning on a grid (convenient while constructing the plot),
  527. * `set_instruction_file` for creating a separate file with all
  528. plotting commands (Matplotlib commands in our case),
  529. * a series of `set_X` functions where `X` is some property like
  530. `linecolor`, `linestyle`, `linewidth`, `filled_curves`.
  531. This is basically all we need to communicate to a plotting program.
  532. Any class in the `Shape` hierarchy inherits `set_X` functions for
  533. setting properties of curves. This information is propagated to
  534. all other shape objects that make up the figure. Class
  535. `Curve` stores the line properties together with the coordinates
  536. of its curve and propagates this information to the plotting program.
  537. When saying `vehicle.set_linewidth(10)`, all objects that make
  538. up the `vehicle` object will get a `set_linewidth(10)` call,
  539. but only the `Curve` object at the end of the chain will actually
  540. store the information and send it to the plotting program.
  541. A rough sketch of class `Curve` reads
  542. !bc pycod
  543. class Curve(Shape):
  544. """General curve as a sequence of (x,y) coordintes."""
  545. def __init__(self, x, y):
  546. self.x = asarray(x, dtype=float)
  547. self.y = asarray(y, dtype=float)
  548. self.linestyle = None
  549. self.linewidth = None
  550. self.linecolor = None
  551. self.fillcolor = None
  552. self.fillpattern = None
  553. self.arrow = None
  554. def draw(self):
  555. drawing_tool.plot_curve(
  556. self.x, self.y,
  557. self.linestyle, self.linewidth, self.linecolor,
  558. self.arrow, self.fillcolor, self.fillpattern)
  559. def set_linewidth(self, width):
  560. self.linewidth = width
  561. det set_linestyle(self, style):
  562. self.linestyle = style
  563. ...
  564. !ec
  565. === Compound Geometric Objects ===
  566. The sample classes so far has managed to define the geometric shape
  567. through just one `Curve` object.[[[
  568. Some objects in a figure will be associated with a point and not
  569. a curve. Therefore, it is natural to introduce a `Point` class
  570. as superclass for such objects:
  571. !bc pycod
  572. class Point(Shape):
  573. def __init__(self, x, y):
  574. self.x, self.y = x, y
  575. !ec
  576. A candidate for subclass is a text located at a given point:
  577. !bc pycod
  578. class Text(Point):
  579. def __init__(self, text, position, alignment='center', fontsize=18):
  580. self.text = text
  581. self.alignment, self.fontsize = alignment, fontsize
  582. is_sequence(position, length=2, can_be_None=True)
  583. Point.__init__(self, position[0], position[1])
  584. #no need for self.shapes here
  585. !ec
  586. [[[[[[[[[[[
  587. Class `Line` is a subclass of `Shape` and
  588. represents the simplest shape: a stright line between two points.
  589. Class `Rectangle` is another subclass of `Shape`, implementing the
  590. functionality needed to specify the four lines of a rectangle.
  591. Class `Circle` can be yet another subclass of `Shape`, or
  592. we may have a class `Arc` and let `Circle` be a subclass
  593. of `Arc` since a circle is an arc of 360 degrees.
  594. Class `Wheel`
  595. is also subclass of `Shape`, but it contains
  596. naturally two `Circle` instances for the inner and outer circles,
  597. plus a set of `Line` instances
  598. going from the inner to the outer circles.
  599. The discussion in the previous paragraph shows that a subclass in
  600. the `Shape` hierarchy typically contains a list of
  601. other subclass instances, *or* the shape is a primitive, such as a line,
  602. circle, or rectangle, where the geometry is defined through a set of
  603. $(x,y)$ coordinates rather than through other `Shape` instances.
  604. It turns out that the implementation is simplest if we introduce
  605. a class `Curve` for holding a primitive shape defined by
  606. $(x,y)$ coordinates. Then all other subclasses of `Shape` can
  607. have a list `shapes` holding the various instances of subclasses of
  608. `Shape` needed to
  609. build up the geometric object. The `shapes`
  610. attribute in class `Circle` will contain
  611. one `Curve` instance for holding the coordinates along the circle,
  612. while the `shapes` attribute in class `Wheel` contains
  613. two `Circle` instances and a number of `Line` instances.
  614. Figures ref{fig:oo:Rectangle:fig} and ref{fig:oo:Wheel:fig}
  615. display two UML drawings of the `shapes` class hierarchy where we
  616. can get a view of how `Rectangle` and `Wheel` relate to other classes:
  617. the darkest arrows represent is-a relationship while the lighter arrows
  618. represent has-a relationship.
  619. All instances in the `Shape` hierarchy must have a `draw` method.
  620. The `draw` method in class `Curve` plots the $(x,y)$ coordinates
  621. as a curve, while the `draw` method in all other classes simply
  622. draws all the shapes that make up the particular figure of the class:
  623. !bc cod
  624. for shape in self.shapes:
  625. shape.draw()
  626. !ec
  627. \begin{figure}
  628. \centerline{\psfig{figure=figs/lumpy_Rectangle_shapes_hier.ps,width=0.5\linewidth}}
  629. \caption{ label{fig:oo:Rectangle:fig}
  630. UML diagram of parts of the `shapes` hierarchy. Classes `Rectangle`
  631. and `Curve` are subclasses of `Shape`. The darkest arrow with
  632. the biggest arrowhead indicates inheritance and is-a
  633. relationship: `Rectangle` and `Curve` are both also `Shape`.
  634. The lighter arrow
  635. indicates {has-a} relationship:
  636. `Rectangle` has a `Curve`, and a `Curve` has a
  637. `NumPyArray`.
  638. }
  639. \end{figure}
  640. \begin{figure}
  641. \centerline{\psfig{figure=figs/lumpy_Wheel_shapes_hier.ps,width=0.7\linewidth}}
  642. \caption{ label{fig:oo:Wheel:fig}
  643. This is a variant of Figure ref{fig:oo:Rectangle:fig} where we
  644. display how class `Wheel` relates to other classes in the
  645. `shapes` hierarchy.
  646. `Wheel` is a `Shape`, like `Arc`, `Line`, and `Curve`,
  647. but `Wheel` contains `Circle` and `Line` objects,
  648. while the `Circle` and `Line` objects have a `Curve`, which has a
  649. `NumPyArray`. We also see that `Circle` is a subclass of `Arc`.
  650. }
  651. \end{figure}
  652. ===== The Drawing Tool =====
  653. We have in Chapter ref{ch:plot} introduced the Easyviz tool for
  654. plotting graphs. This tool is quite well suited for drawing
  655. geometric shapes defined in terms of curves, but when drawing shapes
  656. we often want to skip ticmarks on the axis, labeling of the curves and
  657. axis, and perform other adjustments. Instead of using Easyviz, which
  658. aims at function plotting, we
  659. have decided to use a plotting tool directly and fine-tune the few
  660. commands we need for drawing shapes.
  661. A simple plotting tool for shapes is based on Gnuplot and implemented in
  662. class `GnuplotDraw` in the file `GnuplotDraw.py`.
  663. This class has the following user interface:
  664. !bc cod
  665. class GnuplotDraw:
  666. def __init__(self, xmin, xmax, ymin, ymax):
  667. """Define the drawing area [xmin,xmax]x[ymin,ymax]."""
  668. def define_curve(self, x, y):
  669. """Define a curve with coordinates x and y (arrays)."""
  670. def erase(self):
  671. """Erase the current figure."""
  672. def display(self):
  673. """Display the figure."""
  674. def hardcopy(self, name):
  675. """Save figure in PNG file name.png."""
  676. def set_linecolor(self, color):
  677. """Change the color of lines."""
  678. def set_linewidth(self, width):
  679. """Change the line width (int, starts at 1)."""
  680. def filled_curves(self, on=True):
  681. """Fill area inside curves with current line color."""
  682. !ec
  683. One can easily make a similar class with an identical interface that
  684. applies another plotting package than Gnuplot to create the drawings.
  685. In particular, encapsulating the drawing actions in such a class makes
  686. it trivial to change the drawing program in the future.
  687. The program pieces that apply a drawing tool like `GnuplotDraw`
  688. remain the same. This is an important strategy to follow, especially
  689. when developing larger software systems.
  690. ===== Implementation of Shape Classes =====
  691. label{sec:oo:shape:impl}
  692. Our superclass `Shape` can naturally hold a coordinate system
  693. specification, i.e., the rectangle in which other shapes can be drawn.
  694. This area is fixed for all shapes, so the associated variables should
  695. be static and the method for setting them should also be static
  696. (see Chapter ref{sec:class:static} for static attributes and methods).
  697. It is also natural that class `Shape` holds access to a drawing
  698. tool, in our case a `GnuplotDraw` instance. This object
  699. is also static.
  700. However, it can be an advantage to mirror the static attributes and methods
  701. as global variables and functions in the `shapes` modules.
  702. Users not familiar with static class items can drop the `Shape`
  703. prefix and just use plain module variables and functions. This is what
  704. we do in the application examples.
  705. Class `Shape` defines an imporant method, `draw`, which
  706. just calls the `draw` method for all subshapes that build up the
  707. current shape.
  708. Here is a brief view of class `Shape`\footnote{We have
  709. for simplicity omitted
  710. the static attributes
  711. and methods. These can be viewed in the `shapes.py` file.}:
  712. !bc cod
  713. class Shape:
  714. def __init__(self):
  715. self.shapes = self.subshapes()
  716. if isinstance(self.shapes, Shape):
  717. self.shapes = [self.shapes] # turn to list
  718. def subshapes(self):
  719. """Define self.shapes as list of Shape instances."""
  720. raise NotImplementedError(self.__class__.__name__)
  721. def draw(self):
  722. for shape in self.shapes:
  723. shape.draw()
  724. !ec
  725. In class `Shape` we require the `shapes` attribute to be a
  726. list, but if the `subshape` method in subclasses returns just one
  727. instance, this is automatically wrapped in a list in the constructor.
  728. First we implement the special case class `Curve`, which does not
  729. have subshapes but instead $(x,y)$ coordinates for a curve:
  730. !bc cod
  731. class Curve(Shape):
  732. """General (x,y) curve with coordintes."""
  733. def __init__(self, x, y):
  734. self.x, self.y = x, y
  735. # Turn to Numerical Python arrays
  736. self.x = asarray(self.x, float)
  737. self.y = asarray(self.y, float)
  738. Shape.__init__(self)
  739. def subshapes(self):
  740. pass # geometry defined in constructor
  741. !ec
  742. # In Python, `Curve` does not need to be a subclass of `Shape`.
  743. # It could in fact be natural to remove the `subshapes` method and
  744. # the inheritance from `Shape`. In other languages where all
  745. # elements in `self.shapes` need to be instances of classes in the
  746. # `Shape` hierarchy, because all list elements must have a fixed
  747. # and specified type, `Curve` must be a subclass of `Shape`.
  748. The simplest ordinary `Shape` class is `Line`:
  749. !bc cod
  750. class Line(Shape):
  751. def __init__(self, start, stop):
  752. self.start, self.stop = start, stop
  753. Shape.__init__(self)
  754. def subshapes(self):
  755. x = [self.start[0], self.stop[0]]
  756. y = [self.start[1], self.stop[1]]
  757. return Curve(x,y)
  758. !ec
  759. The code in this class works with `start` and `stop` as
  760. tuples, lists, or arrays of length
  761. two, holding the end points of the line.
  762. The underlying `Curve` object needs only these two end points.
  763. A rectangle is represented by a slightly more complicated class, having the
  764. lower left corner, the width, and the height of the rectangle as
  765. attributes:
  766. !bc cod
  767. class Rectangle(Shape):
  768. def __init__(self, lower_left_corner, width, height):
  769. self.lower_left_corner = lower_left_corner # 2-tuple
  770. self.width, self.height = width, height
  771. Shape.__init__(self)
  772. def subshapes(self):
  773. ll = self.lower_left_corner # short form
  774. x = [ll[0], ll[0]+self.width,
  775. ll[0]+self.width, ll[0], ll[0]]
  776. y = [ll[1], ll[1], ll[1]+self.height,
  777. ll[1]+self.height, ll[1]]
  778. return Curve(x,y)
  779. !ec
  780. Class `Circle` needs many coordinates in its `Curve` object
  781. in order to display a smooth circle. We can provide the number of
  782. straight line segments along the circle as a parameter `resolution`.
  783. Using a default value of 180 means that each straight line segment
  784. approximates an arc of 2 degrees. This resolution should be sufficient
  785. for visual purposes. The set of coordinates along a circle with radius $R$
  786. and center $(x_0,y_0)$ is defined by
  787. !bt
  788. \begin{align}
  789. x &= x_0 + R\cos(t), label{sec:oo:circle:eq1}\\
  790. y &= y_0 + R\sin(t), label{sec:oo:circle:eq2}
  791. \end{align}
  792. !et
  793. for `resolution+1` $t$ values between $0$ and $2\pi$.
  794. The vectorized code for computing the coordinates becomes
  795. !bc cod
  796. t = linspace(0, 2*pi, self.resolution+1)
  797. x = x0 + R*cos(t)
  798. y = y0 + R*sin(t)
  799. !ec
  800. The complete `Circle` class is shown below:
  801. !bc cod
  802. class Circle(Shape):
  803. def __init__(self, center, radius, resolution=180):
  804. self.center, self.radius = center, radius
  805. self.resolution = resolution
  806. Shape.__init__(self)
  807. def subshapes(self):
  808. t = linspace(0, 2*pi, self.resolution+1)
  809. x0 = self.center[0]; y0 = self.center[1]
  810. R = self.radius
  811. x = x0 + R*cos(t)
  812. y = y0 + R*sin(t)
  813. return Curve(x,y)
  814. !ec
  815. We can also introduce class `Arc` for drawing the arc of a circle.
  816. Class `Arc` could be a subclass of `Circle`, extending the
  817. latter with two additional parameters: the opening of the arc
  818. (in degrees) and the starting $t$ value in
  819. (ref{sec:oo:circle:eq1})--(ref{sec:oo:circle:eq2}).
  820. The implementation of class `Arc` will then be almost a copy of
  821. the implementation of class `Circle`. The `subshapes` method
  822. will just define a different `t` array.
  823. Another view is to let class `Arc` be a subclass of `Shape`,
  824. and `Circle` a subclass of `Arc`, since a circle is an arc
  825. of 360 degrees. Let us employ this idea:
  826. !bc cod
  827. class Arc(Shape):
  828. def __init__(self, center, radius,
  829. start_degrees, opening_degrees, resolution=180):
  830. self.center = center
  831. self.radius = radius
  832. self.start_degrees = start_degrees*pi/180
  833. self.opening_degrees = opening_degrees*pi/180
  834. self.resolution = resolution
  835. Shape.__init__(self)
  836. def subshapes(self):
  837. t = linspace(self.start_degrees,
  838. self.start_degrees + self.opening_degrees,
  839. self.resolution+1)
  840. x0 = self.center[0]; y0 = self.center[1]
  841. R = self.radius
  842. x = x0 + R*cos(t)
  843. y = y0 + R*sin(t)
  844. return Curve(x,y)
  845. class Circle(Arc):
  846. def __init__(self, center, radius, resolution=180):
  847. Arc.__init__(self, center, radius, 0, 360, resolution)
  848. !ec
  849. In this latter implementation, we save a lot of code in class
  850. `Circle` since all of class `Arc` can be reused.
  851. Class `Wheel` may conceptually be a subclass of `Circle`.
  852. One circle, say the outer, is inherited and the subclass must have
  853. the inner circle as an attribute. Because of this "asymmetric"
  854. representation of the two circles in a wheel, we find it more natural
  855. to derive `Wheel` directly from `Shape`, and have the two
  856. circles as two attributes of type `Circle`:
  857. !bc cod
  858. class Wheel(Shape):
  859. def __init__(self, center, radius, inner_radius=None, nlines=10):
  860. self.center = center
  861. self.radius = radius
  862. if inner_radius is None:
  863. self.inner_radius = radius/5.0
  864. else:
  865. self.inner_radius = inner_radius
  866. self.nlines = nlines
  867. Shape.__init__(self)
  868. !ec
  869. If the radius of the inner circle
  870. is not defined (`None`)
  871. we take it as 1/5 of the radius of the outer circle.
  872. The wheel is naturally composed of two `Circle` instances and
  873. `nlines` `Line` instances:
  874. !bc cod
  875. def subshapes(self):
  876. outer = Circle(self.center, self.radius)
  877. inner = Circle(self.center, self.inner_radius)
  878. lines = []
  879. t = linspace(0, 2*pi, self.nlines)
  880. Ri = self.inner_radius; Ro = self.radius
  881. x0 = self.center[0]; y0 = self.center[1]
  882. xinner = x0 + Ri*cos(t)
  883. yinner = y0 + Ri*sin(t)
  884. xouter = x0 + Ro*cos(t)
  885. youter = y0 + Ro*sin(t)
  886. lines = [Line((xi,yi),(xo,yo)) for xi, yi, xo, yo in \
  887. zip(xinner, yinner, xouter, youter)]
  888. return [outer, inner] + lines
  889. !ec
  890. For the fun of it, we can implement other shapes, say a sine wave
  891. !bt
  892. \begin{equation*}y = m + A\sin kx,\quad k=2\pi /\lambda,\end{equation*}
  893. !et
  894. where $\lambda$ is the wavelength
  895. of the sine waves, $A$ is the wave amplitude, and $m$ is the mean
  896. value of the wave. The class looks like
  897. !bc cod
  898. class Wave(Shape):
  899. def __init__(self, xstart, xstop,
  900. wavelength, amplitude, mean_level):
  901. self.xstart = xstart
  902. self.xstop = xstop
  903. self.wavelength = wavelength
  904. self.amplitude = amplitude
  905. self.mean_level = mean_level
  906. Shape.__init__(self)
  907. def subshapes(self):
  908. npoints = (self.xstop - self.xstart)/(self.wavelength/61.0)
  909. x = linspace(self.xstart, self.xstop, npoints)
  910. k = 2*pi/self.wavelength # frequency
  911. y = self.mean_level + self.amplitude*sin(k*x)
  912. return Curve(x,y)
  913. !ec
  914. With this and the previous example, you should be in a position to
  915. write your own subclasses. Exercises~ref{sec:oo:ex11}--ref{sec:oo:ex13b}
  916. suggest some smaller projects.
  917. # ===== A Class for Drawing Springs =====
  918. #
  919. # Give: bottom point $B$, number of spring tags $n$, length $L$. Assume that
  920. # $L/3$ is the bottom and top vertical line and that the tags
  921. # are in the middle $L/3$. The width of the tags, $w$, can be either fixed to
  922. # a number or relative to $L$ (say $L/10$ -- need two variables, one
  923. # fixed true/false and one value).
  924. #
  925. #
  926. # \notready
  927. __Functions for Controlling Lines, Colors, etc.__
  928. The `shapes` module containing class `Shape` and all subclasses
  929. mentioned above, also offers some additional functions that do not
  930. depend on any particular shape:
  931. * `display()` for displaying the defined figures so far (all figures whose `draw` method is called).
  932. * `erase()` for ereasing the current figure.
  933. * `hardcopy(name)` for saving the current figure to a PNG file `name.png`.
  934. * `set_linecolor(color)` for setting the color of lines, where
  935. `color` is a string like `'red'` (default), `'blue'`, `'green'`, `'aqua'`, `'purple'`, `'yellow'`, and `'black'`.
  936. * `set_linewidth(width)` for setting the width of a line, measured as an integer (default is 2).
  937. * `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.
  938. Actually, the functions above are static methods in class `Shape`
  939. (cf.~Chapter ref{sec:class:static}),
  940. and they are
  941. just mirrored
  942. as global functions\footnote{You can look into `shapes.py` to see how
  943. we automate the duplication of static methods as global functions.}
  944. in the `shapes` module.
  945. Users without knowledge of static methods do not need to use the
  946. `Shape` prefix for reaching this functionality.
  947. ===== Scaling, Translating, and Rotating a Figure =====
  948. label{sec:oo:scaling}
  949. The real power of object-oriented programming will be obvious in a
  950. minute when we, with a few lines of code, suddenly can
  951. equip *all* shape objects
  952. with additional functionality for scaling, translating, and rotating the
  953. figure.
  954. __Scaling.__
  955. Let us first treat the simplest of the three cases: scaling.
  956. For a `Curve` instance containing a set of $n$ coordinates
  957. $(x_i,y_i)$ that make up a curve, scaling by
  958. a factor $a$ means that we multiply all the $x$ and $y$ coordinates
  959. by $a$:
  960. !bt
  961. \begin{equation*} x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,\quad i=0,\ldots,n-1\thinspace . \end{equation*}
  962. !et
  963. Here we apply the arrow as an assignment operator.
  964. The corresponding Python implementation in
  965. class `Curve` reads
  966. !bc cod
  967. class Curve:
  968. ...
  969. def scale(self, factor):
  970. self.x = factor*self.x
  971. self.y = factor*self.y
  972. !ec
  973. Note here that `self.x` and `self.y` are Numerical Python arrays,
  974. so that multiplication by a scalar number `factor` is
  975. a vectorized operation.
  976. In an instance of a subclass of `Shape`,
  977. the meaning of a method `scale` is
  978. to run through all objects in the list `self.shapes` and ask
  979. each object to scale itself. This is the same delegation of actions
  980. to subclass instances as we do in the `draw` method, and
  981. all objects, except `Curve` instances, can share the same
  982. implementation of the `scale` method. Therefore, we place
  983. the `scale` method in the superclass `Shape` such that all
  984. subclasses can inherit this method.
  985. Since `scale` and `draw` are so similar,
  986. we can easily implement the `scale` method in class `Shape` by
  987. copying and editing the `draw` method:
  988. !bc cod
  989. class Shape:
  990. ...
  991. def scale(self, factor):
  992. for shape in self.shapes:
  993. shape.scale(factor)
  994. !ec
  995. This is all we have to do in order to equip all subclasses of
  996. `Shape` with scaling functionality! But why is it so easy?
  997. All subclasses inherit `scale` from class `Shape`.
  998. Say we have a subclass instance `s` and that we call
  999. `s.scale(factor)`. This leads to calling the inherited `scale`
  1000. method shown above, and in the `for` loop we call the
  1001. `scale` method for each `shape` object in the `self.shapes` list.
  1002. If `shape` is not a `Curve` object, this procedure repeats,
  1003. until we hit a `shape` that is a `Curve`, and then
  1004. the scaling on that set of coordinates is performed.
  1005. __Translation.__
  1006. A set of coordinates $(x_i, y_i)$ can be translated $x$ units in
  1007. the $x$ direction and $y$ units in the $y$ direction using the formulas
  1008. !bt
  1009. \begin{equation*} x_i\leftarrow x+x_i,\quad y_i\leftarrow y + y_i,\quad i=0,\ldots,n-1\thinspace . \end{equation*}
  1010. !et
  1011. The corresponding Python implementation in class `Curve` becomes
  1012. !bc cod
  1013. class Curve:
  1014. ...
  1015. def translate(self, x, y):
  1016. self.x = x + self.x
  1017. self.y = y + self.y
  1018. !ec
  1019. The translation operation for a shape object is very similar to the
  1020. scaling and drawing operations. This means that we can implement a
  1021. common method `translate` in the superclass `Shape`. The code
  1022. is parallel to the `scale` method:
  1023. !bc cod
  1024. class Shape:
  1025. ....
  1026. def translate(self, x, y):
  1027. for shape in self.shapes:
  1028. shape.translate(x, y)
  1029. !ec
  1030. __Rotation.__
  1031. Rotating a figure is more complicated than scaling and translating.
  1032. A counter clockwise rotation of $\theta$ degrees for a set of
  1033. coordinates $(x_i,y_i)$ is given by
  1034. !bt
  1035. \begin{align*}
  1036. \bar x_i &\leftarrow& x_i\cos\theta - y_i\sin\theta,\\
  1037. \bar y_i &\leftarrow& x_i\sin\theta + y_i\cos\theta\thinspace .
  1038. \end{align*}
  1039. !et
  1040. This rotation is performed around the origin. If we want the figure
  1041. to be rotated with respect to a general point $(x,y)$, we need to
  1042. extend the formulas above:
  1043. !bt
  1044. \begin{align*}
  1045. \bar x_i &\leftarrow& x + (x_i -x)\cos\theta - (y_i -y)\sin\theta,\\
  1046. \bar y_i &\leftarrow& y + (x_i -x)\sin\theta + (y_i -y)\cos\theta\thinspace .
  1047. \end{align*}
  1048. !et
  1049. The Python implementation in class `Curve`, assuming that $\theta$
  1050. is given in degrees and not in radians, becomes
  1051. !bc cod
  1052. def rotate(self, angle, x=0, y=0):
  1053. angle = angle*pi/180
  1054. c = cos(angle); s = sin(angle)
  1055. xnew = x + (self.x - x)*c - (self.y - y)*s
  1056. ynew = y + (self.x - x)*s + (self.y - y)*c
  1057. self.x = xnew
  1058. self.y = ynew
  1059. !ec
  1060. The `rotate` method in class `Shape` is identical to the
  1061. `draw`, `scale`, and `translate` methods except that we
  1062. have other arguments:
  1063. !bc cod
  1064. class Shape:
  1065. ....
  1066. def rotate(self, angle, x=0, y=0):
  1067. for shape in self.shapes:
  1068. shape.rotate(angle, x, y)
  1069. !ec
  1070. __Application: Rolling Wheel.__
  1071. To demonstrate the effect of translation and rotation we can roll
  1072. a wheel on the screen. First we draw the wheel and rotate it a bit
  1073. to demonstrate the basic operations:
  1074. !bc cod
  1075. center = (6,2) # the wheel's center point
  1076. w1 = Wheel(center=center, radius=2, inner_radius=0.5, nlines=7)
  1077. # rorate the wheel 2 degrees around its center point:
  1078. w1.rotate(angle=2, center[0], center[1])
  1079. w1.draw()
  1080. display()
  1081. !ec
  1082. Now we want to roll the wheel by making many such small rotations.
  1083. At the same time we need to translate the wheel since rolling
  1084. an arc length $L=R\theta$, where $\theta$ is the rotation angle
  1085. (in radians) and $R$ is the outer radius of the wheel, implies that
  1086. the center point moves a distance $L$ to the left ($\theta >0$
  1087. means counter clockwise rotation).
  1088. In code we must therefore combine rotation with translation:
  1089. !bc cod
  1090. L = radius*angle*pi/180 # translation = arc length
  1091. w1.rotate(angle, center[0], center[1])
  1092. w1.translate(-L, 0)
  1093. center = (center[0] - L, center[1])
  1094. !ec
  1095. We are now in a position to put the rotation and translation
  1096. operations in a `for` loop and make a complete function:
  1097. !bc cod
  1098. def rolling_wheel(total_rotation_angle):
  1099. """Animation of a rotating wheel."""
  1100. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1101. center = (6,2)
  1102. radius = 2.0
  1103. angle = 2.0
  1104. w1 = Wheel(center=center, radius=radius,
  1105. inner_radius=0.5, nlines=7)
  1106. for i in range(int(total_rotation_angle/angle)):
  1107. w1.draw()
  1108. display()
  1109. L = radius*angle*pi/180 # translation = arc length
  1110. w1.rotate(angle, center[0], center[1])
  1111. w1.translate(-L, 0)
  1112. center = (center[0] - L, center[1])
  1113. erase()
  1114. !ec
  1115. To control the visual "velocity" of the wheel, we can insert a pause
  1116. between each frame in the `for` loop. A call to
  1117. `time.sleep(s)`, where `s` is the length of
  1118. the pause in seconds, can do this for us.
  1119. Another convenient feature is to save each frame drawn in the `for`
  1120. loop as a hardcopy in PNG format and then, after the loop,
  1121. make an animated GIF file based on the individual PNG frames.
  1122. The latter operation is performed either by the `movie` function
  1123. from `scitools.std`
  1124. or by the `convert` program from
  1125. the ImageMagick suite. With the latter you write the following command
  1126. in a terminal window:
  1127. !bc ccq
  1128. convert -delay 50 -loop 1000 xxx tmp_movie.gif
  1129. !ec
  1130. Here, `xxx` is a space-separated list of all the PNG files, and
  1131. `tmp_movie.gif` is the name of the resulting animated GIF file.
  1132. We can easily make `xxx` by collecting the names of the PNG files
  1133. from the loop in a list object, and then join the names.
  1134. The `convert` command can be run as an `os.system` call.
  1135. The complete `rolling_wheel` function, incorporating the
  1136. mentioned movie making, will then be
  1137. !bc cod
  1138. def rolling_wheel(total_rotation_angle):
  1139. """Animation of a rotating wheel."""
  1140. set_coordinate_system(xmin=0, xmax=10, ymin=0, ymax=10)
  1141. import time
  1142. center = (6,2)
  1143. radius = 2.0
  1144. angle = 2.0
  1145. pngfiles = []
  1146. w1 = Wheel(center=center, radius=radius,
  1147. inner_radius=0.5, nlines=7)
  1148. for i in range(int(total_rotation_angle/angle)):
  1149. w1.draw()
  1150. display()
  1151. filename = 'tmp_
  1152. #03d' i
  1153. pngfiles.append(filename + '.png')
  1154. hardcopy(filename)
  1155. time.sleep(0.3) # pause 0.3 sec
  1156. L = radius*angle*pi/180 # translation = arc length
  1157. w1.rotate(angle, center[0], center[1])
  1158. w1.translate(-L, 0)
  1159. center = (center[0] - L, center[1])
  1160. erase() # erase the screen before new figure
  1161. cmd = 'convert -delay 50 -loop 1000
  1162. #s tmp_movie.gif' \
  1163. # (' '.join(pngfiles))
  1164. import commands
  1165. failure, output = commands.getstatusoutput(cmd)
  1166. if failure: print 'Could not run', cmd
  1167. !ec
  1168. The last two lines run a command, from Python, as we would run the
  1169. command in a terminal window.
  1170. The resulting animated GIF file can be viewed with
  1171. `animate tmp_movie.gif` as a command in a terminal window.