123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- # pendulum.tcl --
- #
- # This demonstration illustrates how Tcl/Tk can be used to construct
- # simulations of physical systems.
- if {![info exists widgetDemo]} {
- error "This script should be run from the \"widget\" demo."
- }
- package require Tk
- set w .pendulum
- catch {destroy $w}
- toplevel $w
- wm title $w "Pendulum Animation Demonstration"
- wm iconname $w "pendulum"
- positionWindow $w
- 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."
- pack $w.msg
- ## See Code / Dismiss buttons
- set btns [addSeeDismiss $w.buttons $w]
- pack $btns -side bottom -fill x
- # Create some structural widgets
- pack [panedwindow $w.p] -fill both -expand 1
- $w.p add [labelframe $w.p.l1 -text "Pendulum Simulation"]
- $w.p add [labelframe $w.p.l2 -text "Phase Space"]
- # Create the canvas containing the graphical representation of the
- # simulated system.
- canvas $w.c -width 320 -height 200 -background white -bd 2 -relief sunken
- $w.c create text 5 5 -anchor nw -text "Click to Adjust Bob Start Position"
- # Coordinates of these items don't matter; they will be set properly below
- $w.c create line 0 25 320 25 -tags plate -fill grey50 -width 2
- $w.c create oval 155 20 165 30 -tags pivot -fill grey50 -outline {}
- $w.c create line 1 1 1 1 -tags rod -fill black -width 3
- $w.c create oval 1 1 2 2 -tags bob -fill yellow -outline black
- pack $w.c -in $w.p.l1 -fill both -expand true
- # Create the canvas containing the phase space graph; this consists of
- # a line that gets gradually paler as it ages, which is an extremely
- # effective visual trick.
- canvas $w.k -width 320 -height 200 -background white -bd 2 -relief sunken
- $w.k create line 160 200 160 0 -fill grey75 -arrow last -tags y_axis
- $w.k create line 0 100 320 100 -fill grey75 -arrow last -tags x_axis
- for {set i 90} {$i>=0} {incr i -10} {
- # Coordinates of these items don't matter; they will be set properly below
- $w.k create line 0 0 1 1 -smooth true -tags graph$i -fill grey$i
- }
- $w.k create text 0 0 -anchor ne -text "\u03b8" -tags label_theta
- $w.k create text 0 0 -anchor ne -text "\u03b4\u03b8" -tags label_dtheta
- pack $w.k -in $w.p.l2 -fill both -expand true
- # Initialize some variables
- set points {}
- set Theta 45.0
- set dTheta 0.0
- set pi 3.1415926535897933
- set length 150
- set home 160
- # This procedure makes the pendulum appear at the correct place on the
- # canvas. If the additional arguments "at $x $y" are passed (the 'at'
- # is really just syntactic sugar) instead of computing the position of
- # the pendulum from the length of the pendulum rod and its angle, the
- # length and angle are computed in reverse from the given location
- # (which is taken to be the centre of the pendulum bob.)
- proc showPendulum {canvas {at {}} {x {}} {y {}}} {
- global Theta dTheta pi length home
- if {$at eq "at" && ($x!=$home || $y!=25)} {
- set dTheta 0.0
- set x2 [expr {$x - $home}]
- set y2 [expr {$y - 25}]
- set length [expr {hypot($x2, $y2)}]
- set Theta [expr {atan2($x2, $y2) * 180/$pi}]
- } else {
- set angle [expr {$Theta * $pi/180}]
- set x [expr {$home + $length*sin($angle)}]
- set y [expr {25 + $length*cos($angle)}]
- }
- $canvas coords rod $home 25 $x $y
- $canvas coords bob \
- [expr {$x-15}] [expr {$y-15}] [expr {$x+15}] [expr {$y+15}]
- }
- showPendulum $w.c
- # Update the phase-space graph according to the current angle and the
- # rate at which the angle is changing (the first derivative with
- # respect to time.)
- proc showPhase {canvas} {
- global Theta dTheta points psw psh
- lappend points [expr {$Theta+$psw}] [expr {-20*$dTheta+$psh}]
- if {[llength $points] > 100} {
- set points [lrange $points end-99 end]
- }
- for {set i 0} {$i<100} {incr i 10} {
- set list [lrange $points end-[expr {$i-1}] end-[expr {$i-12}]]
- if {[llength $list] >= 4} {
- $canvas coords graph$i $list
- }
- }
- }
- # Set up some bindings on the canvases. Note that when the user
- # clicks we stop the animation until they release the mouse
- # button. Also note that both canvases are sensitive to <Configure>
- # events, which allows them to find out when they have been resized by
- # the user.
- bind $w.c <Destroy> {
- after cancel $animationCallbacks(pendulum)
- unset animationCallbacks(pendulum)
- }
- bind $w.c <Button-1> {
- after cancel $animationCallbacks(pendulum)
- showPendulum %W at %x %y
- }
- bind $w.c <B1-Motion> {
- showPendulum %W at %x %y
- }
- bind $w.c <ButtonRelease-1> {
- showPendulum %W at %x %y
- set animationCallbacks(pendulum) [after 15 repeat [winfo toplevel %W]]
- }
- bind $w.c <Configure> {
- %W coords plate 0 25 %w 25
- set home [expr {%w/2}]
- %W coords pivot [expr {$home-5}] 20 [expr {$home+5}] 30
- }
- bind $w.k <Configure> {
- set psh [expr {%h/2}]
- set psw [expr {%w/2}]
- %W coords x_axis 2 $psh [expr {%w-2}] $psh
- %W coords y_axis $psw [expr {%h-2}] $psw 2
- %W coords label_dtheta [expr {$psw-4}] 6
- %W coords label_theta [expr {%w-6}] [expr {$psh+4}]
- }
- # This procedure is the "business" part of the simulation that does
- # simple numerical integration of the formula for a simple rotational
- # pendulum.
- proc recomputeAngle {} {
- global Theta dTheta pi length
- set scaling [expr {3000.0/$length/$length}]
- # To estimate the integration accurately, we really need to
- # compute the end-point of our time-step. But to do *that*, we
- # need to estimate the integration accurately! So we try this
- # technique, which is inaccurate, but better than doing it in a
- # single step. What we really want is bound up in the
- # differential equation:
- # .. - sin theta
- # theta + theta = -----------
- # length
- # But my math skills are not good enough to solve this!
- # first estimate
- set firstDDTheta [expr {-sin($Theta * $pi/180)*$scaling}]
- set midDTheta [expr {$dTheta + $firstDDTheta}]
- set midTheta [expr {$Theta + ($dTheta + $midDTheta)/2}]
- # second estimate
- set midDDTheta [expr {-sin($midTheta * $pi/180)*$scaling}]
- set midDTheta [expr {$dTheta + ($firstDDTheta + $midDDTheta)/2}]
- set midTheta [expr {$Theta + ($dTheta + $midDTheta)/2}]
- # Now we do a double-estimate approach for getting the final value
- # first estimate
- set midDDTheta [expr {-sin($midTheta * $pi/180)*$scaling}]
- set lastDTheta [expr {$midDTheta + $midDDTheta}]
- set lastTheta [expr {$midTheta + ($midDTheta + $lastDTheta)/2}]
- # second estimate
- set lastDDTheta [expr {-sin($lastTheta * $pi/180)*$scaling}]
- set lastDTheta [expr {$midDTheta + ($midDDTheta + $lastDDTheta)/2}]
- set lastTheta [expr {$midTheta + ($midDTheta + $lastDTheta)/2}]
- # Now put the values back in our globals
- set dTheta $lastDTheta
- set Theta $lastTheta
- }
- # This method ties together the simulation engine and the graphical
- # display code that visualizes it.
- proc repeat w {
- global animationCallbacks
- # Simulate
- recomputeAngle
- # Update the display
- showPendulum $w.c
- showPhase $w.k
- # Reschedule ourselves
- set animationCallbacks(pendulum) [after 15 [list repeat $w]]
- }
- # Start the simulation after a short pause
- set animationCallbacks(pendulum) [after 500 [list repeat $w]]
|