pendulum.tcl 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. # pendulum.tcl --
  2. #
  3. # This demonstration illustrates how Tcl/Tk can be used to construct
  4. # simulations of physical systems.
  5. if {![info exists widgetDemo]} {
  6. error "This script should be run from the \"widget\" demo."
  7. }
  8. package require Tk
  9. set w .pendulum
  10. catch {destroy $w}
  11. toplevel $w
  12. wm title $w "Pendulum Animation Demonstration"
  13. wm iconname $w "pendulum"
  14. positionWindow $w
  15. label $w.msg -font $font -wraplength 4i -justify left -text "This demonstration shows how Tcl/Tk can be used to carry out animations that are linked to simulations of physical systems. In the left canvas is a graphical representation of the physical system itself, a simple pendulum, and in the right canvas is a graph of the phase space of the system, which is a plot of the angle (relative to the vertical) against the angular velocity. The pendulum bob may be repositioned by clicking and dragging anywhere on the left canvas."
  16. pack $w.msg
  17. ## See Code / Dismiss buttons
  18. set btns [addSeeDismiss $w.buttons $w]
  19. pack $btns -side bottom -fill x
  20. # Create some structural widgets
  21. pack [panedwindow $w.p] -fill both -expand 1
  22. $w.p add [labelframe $w.p.l1 -text "Pendulum Simulation"]
  23. $w.p add [labelframe $w.p.l2 -text "Phase Space"]
  24. # Create the canvas containing the graphical representation of the
  25. # simulated system.
  26. canvas $w.c -width 320 -height 200 -background white -bd 2 -relief sunken
  27. $w.c create text 5 5 -anchor nw -text "Click to Adjust Bob Start Position"
  28. # Coordinates of these items don't matter; they will be set properly below
  29. $w.c create line 0 25 320 25 -tags plate -fill grey50 -width 2
  30. $w.c create oval 155 20 165 30 -tags pivot -fill grey50 -outline {}
  31. $w.c create line 1 1 1 1 -tags rod -fill black -width 3
  32. $w.c create oval 1 1 2 2 -tags bob -fill yellow -outline black
  33. pack $w.c -in $w.p.l1 -fill both -expand true
  34. # Create the canvas containing the phase space graph; this consists of
  35. # a line that gets gradually paler as it ages, which is an extremely
  36. # effective visual trick.
  37. canvas $w.k -width 320 -height 200 -background white -bd 2 -relief sunken
  38. $w.k create line 160 200 160 0 -fill grey75 -arrow last -tags y_axis
  39. $w.k create line 0 100 320 100 -fill grey75 -arrow last -tags x_axis
  40. for {set i 90} {$i>=0} {incr i -10} {
  41. # Coordinates of these items don't matter; they will be set properly below
  42. $w.k create line 0 0 1 1 -smooth true -tags graph$i -fill grey$i
  43. }
  44. # FIXME: UNICODE labels
  45. $w.k create text 0 0 -anchor ne -text "q" -font {Symbol 8} -tags label_theta
  46. $w.k create text 0 0 -anchor ne -text "dq" -font {Symbol 8} -tags label_dtheta
  47. pack $w.k -in $w.p.l2 -fill both -expand true
  48. # Initialize some variables
  49. set points {}
  50. set Theta 45.0
  51. set dTheta 0.0
  52. set pi 3.1415926535897933
  53. set length 150
  54. set home 160
  55. # This procedure makes the pendulum appear at the correct place on the
  56. # canvas. If the additional arguments "at $x $y" are passed (the 'at'
  57. # is really just syntactic sugar) instead of computing the position of
  58. # the pendulum from the length of the pendulum rod and its angle, the
  59. # length and angle are computed in reverse from the given location
  60. # (which is taken to be the centre of the pendulum bob.)
  61. proc showPendulum {canvas {at {}} {x {}} {y {}}} {
  62. global Theta dTheta pi length home
  63. if {$at eq "at" && ($x!=$home || $y!=25)} {
  64. set dTheta 0.0
  65. set x2 [expr {$x - $home}]
  66. set y2 [expr {$y - 25}]
  67. set length [expr {hypot($x2, $y2)}]
  68. set Theta [expr {atan2($x2, $y2) * 180/$pi}]
  69. } else {
  70. set angle [expr {$Theta * $pi/180}]
  71. set x [expr {$home + $length*sin($angle)}]
  72. set y [expr {25 + $length*cos($angle)}]
  73. }
  74. $canvas coords rod $home 25 $x $y
  75. $canvas coords bob \
  76. [expr {$x-15}] [expr {$y-15}] [expr {$x+15}] [expr {$y+15}]
  77. }
  78. showPendulum $w.c
  79. # Update the phase-space graph according to the current angle and the
  80. # rate at which the angle is changing (the first derivative with
  81. # respect to time.)
  82. proc showPhase {canvas} {
  83. global Theta dTheta points psw psh
  84. lappend points [expr {$Theta+$psw}] [expr {-20*$dTheta+$psh}]
  85. if {[llength $points] > 100} {
  86. set points [lrange $points end-99 end]
  87. }
  88. for {set i 0} {$i<100} {incr i 10} {
  89. set list [lrange $points end-[expr {$i-1}] end-[expr {$i-12}]]
  90. if {[llength $list] >= 4} {
  91. $canvas coords graph$i $list
  92. }
  93. }
  94. }
  95. # Set up some bindings on the canvases. Note that when the user
  96. # clicks we stop the animation until they release the mouse
  97. # button. Also note that both canvases are sensitive to <Configure>
  98. # events, which allows them to find out when they have been resized by
  99. # the user.
  100. bind $w.c <Destroy> {
  101. after cancel $animationCallbacks(pendulum)
  102. unset animationCallbacks(pendulum)
  103. }
  104. bind $w.c <1> {
  105. after cancel $animationCallbacks(pendulum)
  106. showPendulum %W at %x %y
  107. }
  108. bind $w.c <B1-Motion> {
  109. showPendulum %W at %x %y
  110. }
  111. bind $w.c <ButtonRelease-1> {
  112. showPendulum %W at %x %y
  113. set animationCallbacks(pendulum) [after 15 repeat [winfo toplevel %W]]
  114. }
  115. bind $w.c <Configure> {
  116. %W coords plate 0 25 %w 25
  117. set home [expr %w/2]
  118. %W coords pivot [expr $home-5] 20 [expr $home+5] 30
  119. }
  120. bind $w.k <Configure> {
  121. set psh [expr %h/2]
  122. set psw [expr %w/2]
  123. %W coords x_axis 2 $psh [expr %w-2] $psh
  124. %W coords y_axis $psw [expr %h-2] $psw 2
  125. %W coords label_dtheta [expr $psw-4] 6
  126. %W coords label_theta [expr %w-6] [expr $psh+4]
  127. }
  128. # This procedure is the "business" part of the simulation that does
  129. # simple numerical integration of the formula for a simple rotational
  130. # pendulum.
  131. proc recomputeAngle {} {
  132. global Theta dTheta pi length
  133. set scaling [expr {3000.0/$length/$length}]
  134. # To estimate the integration accurately, we really need to
  135. # compute the end-point of our time-step. But to do *that*, we
  136. # need to estimate the integration accurately! So we try this
  137. # technique, which is inaccurate, but better than doing it in a
  138. # single step. What we really want is bound up in the
  139. # differential equation:
  140. # .. - sin theta
  141. # theta + theta = -----------
  142. # length
  143. # But my math skills are not good enough to solve this!
  144. # first estimate
  145. set firstDDTheta [expr {-sin($Theta * $pi/180)*$scaling}]
  146. set midDTheta [expr {$dTheta + $firstDDTheta}]
  147. set midTheta [expr {$Theta + ($dTheta + $midDTheta)/2}]
  148. # second estimate
  149. set midDDTheta [expr {-sin($midTheta * $pi/180)*$scaling}]
  150. set midDTheta [expr {$dTheta + ($firstDDTheta + $midDDTheta)/2}]
  151. set midTheta [expr {$Theta + ($dTheta + $midDTheta)/2}]
  152. # Now we do a double-estimate approach for getting the final value
  153. # first estimate
  154. set midDDTheta [expr {-sin($midTheta * $pi/180)*$scaling}]
  155. set lastDTheta [expr {$midDTheta + $midDDTheta}]
  156. set lastTheta [expr {$midTheta + ($midDTheta + $lastDTheta)/2}]
  157. # second estimate
  158. set lastDDTheta [expr {-sin($lastTheta * $pi/180)*$scaling}]
  159. set lastDTheta [expr {$midDTheta + ($midDDTheta + $lastDDTheta)/2}]
  160. set lastTheta [expr {$midTheta + ($midDTheta + $lastDTheta)/2}]
  161. # Now put the values back in our globals
  162. set dTheta $lastDTheta
  163. set Theta $lastTheta
  164. }
  165. # This method ties together the simulation engine and the graphical
  166. # display code that visualizes it.
  167. proc repeat w {
  168. global animationCallbacks
  169. # Simulate
  170. recomputeAngle
  171. # Update the display
  172. showPendulum $w.c
  173. showPhase $w.k
  174. # Reschedule ourselves
  175. set animationCallbacks(pendulum) [after 15 [list repeat $w]]
  176. }
  177. # Start the simulation after a short pause
  178. set animationCallbacks(pendulum) [after 500 [list repeat $w]]