sketcher.do.txt 46 KB

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