#!/usr/bin/wish -f ## ## SCRIPT: tkSimulateBouncingBall.tk ## ## PURPOSE: This Tk GUI script solves a second-order differential ## equation for the vertical fall and bouncing of a ball by ## using numerical integration (the Runge-Kutta method) --- ## and an algebraic method to model the bouncing of the ball. ## ## The ODE (ordinary differential equation) for a single, ## falling mass (the ball) with no frictional forces ## (such as air resistance) is ## ## D(D(u)) = -g ## ## where D represents the time-derivative operator d/dt, ## and t represents the independent variable time, ## and u represents the height of the center of the ball above ## the surface off of which it is bouncing, ## and g is the acceleration due to gravity (near the surface ## of the Earth or the Moon or Mars or a mountain-top ## or whatever) --- assumed to be constant relative to ## the height-range of the bouncing ball. ## ## We assume that the ball is rigid and that when the ball ## hits the surface, the height u is the radius of the ball. ## ## We let the height u be increasing in the upward direction. ## Then the velocity D(u) of the falling mass is negative ## (downward). (You can see this if you think of the velocity ## as being delta-height over delta-time, and observe that ## for the falling mass, delta-height is negative as ## delta-time is positive.) ## ## Also note that because the velocity is getting ## 'more and more negative' as the mass falls, ## the acceleration is negative. (Delta-velocity ## over delta-time is a negative number over a positive.) ## ## Since we let g be a positive constant, we need the ## negative sign in front of g in the expression above for ## the instantaneous acceleration of the mass at any time t. ## ## Galileo showed that if one releases a mass and allows ## it to fall freely under the influence of a constant ## accelerative attraction toward the center of the earth (g), ## then the distance the mass falls in any time t from ## the time of release from rest (i.e. starting with ## zero velocity) is g*t^2/2 --- g times t squared over 2. ## ## Actually, the concept of a gravitational constant, g, came ## after his time. Galileo showed that the distance fallen is ## proportional to the time squared. ## ## In any case, ## note that the mass of the ball is not involved ## in this relationship. This corresponds to the fact ## that Galileo observed that different masses, ## dropped from the same height, hit the ground at ## the same time. (He actually used spherical masses ## rolling down a linear inclined path, so that he ## could get fairly accurate measurements of the time ## elapsed in travelling to various distances ## along the inclined path. He could adjust the incline ## so that the ball rolled slow enough to get the ## time measurements.) ## ## So, from what Galileo discovered, we learned that we do not ## need numerical integration to solve the simple ODE ## D(D(u) = -g. ## If the initial height at time zero is denoted u(0), ## and the ball is dropped (initial velocity is zero), ## the height at time t is given by ## u(0) - g*t^2/2 ## ## However, in spite of the fact that we have a simple ## algebraic expression for the solution of the ODE, ## we use numerical integration to do the solve. ## ## This makes it relatively easy to handle additional terms ## describing effects like air resistance (which is related ## to the downward or upward velocity of the ball). ## ## If air resistance is proportional to the magnitude ## of the velocity or to the velocity squared or some ## combination of the two, then we can use numerical ## integration to solve a differential equation that ## may look something like ## D(D(u)) = -g -k1*D(u) -k2*sign(D(u))*D(u)^2 ## where D(u) represents the signed velocity of the ball. ## ## (Although these equations still may not involve ## the mass of the ball, note that the constants ## of resistance, k1 and k2, will surely depend on ## factors like the profile of the ball, like its ## radius, and the density of the air.) ## ##+#################### ## MODELLING THE BOUNCE: ##+#################### ## (in an algebraic way, rather than using ## an extremely 'stiff' differential equation) ## ## We model the bounce by switching the velocity direction ## of the ball when it strikes the surface --- when height ## u is less than or equal to the radius of the rigid ball ## --- so the velocity D(u) is set to -D(u). ## ## To allow for modeling a ball that is losing height ## with each bounce because of a loss of some momentum ## with each impact with the surface, we introduce ## a positive constant k which is less than or equal to 1. ## ## When we switch the direction of the velocity, we also ## reduce the velocity magnitude by the factor k. ## ## Hence the velocity D(u) becomes -k*D(u) at each bounce. ## ##+############# ## GUI FEATURES: ## ## The GUI allows the user to enter various values for ## g (gravity) and k (velocity adjustment at each bounce). ## ## (Note that the differential equation D(D(u) = -g does ## not involve mass --- as mentioned above. So ## we do not have to prompt for mass on the GUI.) ## ## To be able to do the numerical integration of the ## 2nd order ODE, we need a couple of initial conditions ## for the displacement u and the velocity D(u). ## ## The GUI allows the user to enter four parameters ## for the solver process: ## - an initial vertical height of the ball ## - an initial vertical velocity (typically zero) ## - an end-time for the end of the solution process ## - a time-step, h, for the solution process. ## ## Following a solution run, the GUI also allows the ## user to start (and stop) an animation of the bouncing ## ball drawn on a Tk canvas. ## ## The animation is shown centered on a rectangular Tk 'canvas' ## widget by using 'create oval' and 'delete' commands on the canvas. ## ##+############################################## ## METHOD - MATH MODELLING OF THE BOUNCING BALL MOTION: ## ## We convert the single 'second order' differential equation ## ## D(D(u)) = -g ## ## to two 'first order' differential equations with ## u1 = u and u2 = D(u) as the functions of t ## to be generated by integrating 2 differential eqns: ## ## D(u1) = u2 ## D(u2) = -g ## ## for initial conditions u1=A and u2=B, where A is an ## initial vertical height and B is an initial vertical velocity. ## ## The common way of expressing these kinds of systems of ## first order differential equations in compact, general form is ## ## D(u) = f(t,u) ## ## where u and f are N-dimensional vectors. ## ## This is a compact way of expressing a system of scalar ## differential equations: ## ## D(u1) = f1(t,u1,...,uN) ## D(u2) = f2(t,u1,...,uN) ## ...... ## D(uN) = fN(t,u1,...,uN) ## ## In the case of these bouncing ball equations, N=2, and ## we can think of solving for the unknown function vector ## (u1(t),u2(t)) where the right-hand-side (RHS) of the ## two equations above can be thought of as a special ## case of a more general user-specified function vector ## (f1(t,u1,u2),f2(t,u1,u2)) where ## ## f1(t,u1,u2) = u2 ## f2(t,u1,u2) = -g ## ## We use the popular Runge-Kutta 4th order method (RK4) to ## perform the numerical integration for a user-specified ## time step, h. ## ## We basically use two procs to perform the integration ## steps: ## - a proc to perform the RK4 integration for N=2--- ## giving the values of (u1,u2) for each time step ## ## - a proc to evaluate the RHS function: (f1,f2) ## for specified values of t,u1,u2. ## ## The latter proc is called several times by the former proc ## for each time step. ## ##+################################################################ ## METHOD - PLOTTING THE ANIMATION ON THE TK CANVAS: ## ## After a solution, we have the solution functions ## u1 and u2 for a sequence of equally-spaced time values. ## ## We use function u1 to do the animation. ## ## For each time value, t(i), the bouncing ball is drawn ## as a simple color-filled circle representing the ball. ## ## The GUI provides 2 buttons by which the user can specify ## the 2 colors for: ## - the canvas background ## - the bouncing ball (the circle). ## ## An 'animate' proc performs the bouncing ball animation when ## the user clicks on the 'Start' radiobutton of the GUI. ## ## This animate proc uses the 'world-coordinates' --- the ## values of height u1 --- to draw the bouncing ball within ## a square area of about Hmax-by-Hmax in world coordinates, ## where Hmax is the maximum height, over the surface on which ## the ball bounces, reached by the ball during a solve run. ## ## We use u1 to represent the location of the center of the ball, ## so we need to adust the max height of the bounce by the ## radius of the ball to accomodate the top half of the ball. ## We think of Hmax, below, as representing that 'augmented' height. ## ## We think of the ball bouncing up and down in the y-direction, ## with no 'side' forces on the ball in the x-direction. Hence ## the x-coordinate of the location of the ball stays constant. ## ## We imagine the Hmax-by-Hmax square to have an xy-coordinate ## system overlaid on it such that the origin (0.0,0.0) is ## at the middle of the bottom of the square. In other words, ## the y-coordinate of the square goes from 0.0 at the bottom of ## the square to Hmax at the top of the square --- and the x-coordinate ## goes from -Hmax/2 at the left side of the square to +Hmax/2 ## at right side of the square. ## ## We think of the bottom of the path of the center of the rigid, ## extremely hard bouncing ball (non-squishable, like a golf ball) ## as being above the origin --- (0.0,ball-radius). We use the ## height function u1(t(i)) of the center of the ball to set ## the y coordinate of the location of the center of the ## bouncing ball, and we keep the x-coordinate of the (x,y) ## position of the center of the ball at 0.0 --- the x-mid-point ## of the square. ## ## Note that the x,y coordinates of the upper-left corner of the ## square is (-Hmax/2,+Hmax) and the lower-right corner of the ## square is (+Hmax/2, 0.0). ## ## The Hmax-by-Hmax area allows for the bouncing ball to bounce ## to extremes --- from height zero to height Hmax. ## ## A proc is provided which maps the plot area limits in ## world coordinates --- say ## UpperLeftCorner: (-Hmax/2,+Hmax) ## LowerRightCorner: (+Hmax/2,0.0) ## to the corners of the plot area in pixel coordinates: ## UpperLeftCorner: (0,0) ## LowerRightCorner: (ImageWidthPx,ImageHeightPx) ## ## We use 2 values a little larger than Hmax and a little smaller ## than 0.0 for the world coordinate limits --- to allow for a little ## margin at the top and bottom of the path of the bouncing ball. ## ## To get a square image area, we use ImageWidthPx=ImageHeightPx ## and we determine this number of pixels by allowing the user ## to specify the integer value in an entry widget on the GUI. ## ## The animate proc uses 2 procs --- Xwc2Xpx and Ywc2Ypx --- ## to convert the world coordinates of each point --- such as the ## location of the center of the bouncing ball --- to pixel coordinates. ## ## The pixel-coordinates are used in the 'create oval' command ## to redraw the bouncing ball on the Tk canvas, for each time step. ## ##+############## ## THE GUI LAYOUT: ## ## From the discussion above, we see that the Tk GUI should ## allow the user to specify ## g, k, initial-height, initial-velocity, ## time-step-size, end-time, ball radius, and image size. ## ## There is to be a 'Solve' button to perform a solution ## when the user is ready to use these parameters. ## ## There are also to be 2 buttons by which to call up an ## RGB-color-selector GUI by which to specify the 2 colors ## for the animation drawing on the canvas. ## ## A 'ShowList' button can be used to show the list of ## solution values --- triplets t(i), u1(t(i)), u2(t(i)) --- ## in a popup window. ## ## In addition, on the GUI, there are to be 'Start' and 'Stop' ## radiobuttons to start and stop an animation run. ## ## To allow the user to speed-up or slow-down the animation, ## there could be a Tk widget ('entry' or 'scale') by which to specify ## a wait-time (in millisecs) between computing and displaying ## each new bouncing ball position. This would be an alternative to ## using a wait-time value calculated from the user-selected ## time-step, h. ## ## For now, we simply calculate the animation wait-time ## based on the time-step, h. ## ## One way the user can specify these parameters is indicated by ## the following 'sketch' of the GUI: ## ## FRAMEnames ## VVVVVVVVVV ## ------------------------------------------------------------------------------------------ ## Simulate a Bouncing Ball --- an animation ## [window title] ## ------------------------------------------------------------------------------------------ ## ## .fRbuttons {Exit} {Help} {Solve} {Show {Reset Animate: O Start O Stop {Ball {Background ## List} Parms} Color} Color} ## ## .fRrhs [ .... A description of the equation(s) or solution technique goes here, in a label widget. ....... ] ## [This could be an entry widget, someday, to allow for changes in the math expression for acceleration.] ## ## .fRfactors g (distance-units/sec/sec): 9.8__ k (momentum loss factor at bounce): 0.95_ ## ## .fRinit Initial Height (distance-units): 2.0___ Initial Velocity (distance-units/sec): 0.0___ ## ## .fRball Radius of the ball (distance-units): 0.1___ ## ## .fRtime Solve End Time (secs): 10__ Solve Step Size (secs): 0.05___ Image square (pixels): 300__ ## ## .fRmsg [ .......... Messages go here, in a label widget .......................... ] ## ## .fRcanvas |------------------------------------------------------------------------| ## | | ## | [This non-scrollable canvas contains the square animation image | ## | area --- centered at the top of the canvas widget. | ## | | ## | | ## | | ## | | ## |------------------------------------------------------------------------| ## ## ------------------------------------------------------------------- ## ## In the above sketch of the GUI: ## ## SQUARE BRACKETS indicate a comment (not to be placed on the GUI). ## BRACES indicate a Tk 'button' widget. ## UNDERSCORES indicate a Tk 'entry' widget. ## A COLON indicates that the text before the colon is on a 'label' widget. ## CAPITAL-O indicates a Tk 'radiobutton' widget. ## CAPITAL-X indicates a Tk 'checkbutton' widget (if any). ## ## VERTICAL-BARS and horizontal HYPHENS are used to outline a 'canvas' or ## 'listbox' or 'text' widget. ## ## If there are scrollbars: ## Less-than and greater-than signs indicate the left and right ends of a horizontal 'scrollbar'. ## Capital-V and Capital-A letters indicate the bottom and top ends of a vertical 'scrollbar'. ## ##+############## ## GUI components: ## ## From the GUI 'sketch' above, it is seen that the GUI consists of about ## ## - 7 button widgets ## - 11 label widgets ## - 8 entry widgets ## - 1 canvas widget with no scrollbars ## - 2 radiobutton widgets in 1 group ## - 0 scale widgets (but may use scale widgets in place of some entry widgets) ## - 0 checkbutton widgets ## - 0 listbox widgets ## - 0 text widgets ## ##+######################################################################## ## 'CANONICAL' STRUCTURE OF THIS TK CODE: ## ## 0) Set general window & widget parms (win-name, win-position, ## win-color-scheme, fonts, widget-geometry-parms, win-size-control). ## ## 1a) Define ALL frames (and sub-frames, if any). ## 1b) Pack ALL frames and sub-frames. ## ## 2) Define all widgets in the frames. Pack them. ## ## 3) Define keyboard or mouse/touchpad/touch-sensitive-screen action ## BINDINGS, if needed. ## ## 4) Define PROCS, if needed. ## ## 5) Additional GUI INITIALIZATION (typically with one or two of ## the procs), if needed. ## ## ## Some detail about the code structure of this particular script: ## ## 1a) Define ALL frames: ## ## Top-level : '.fRbuttons' ## '.fRrhs' ## '.fRfactors' ## '.fRinit' ## '.fRball' ## '.fRtime' ## '.fRmsg' ## '.fRcanvas' ## No sub-frames. ## ## 1b) Pack ALL frames. ## ## 2) Define all widgets in the frames (and pack them): ## ## - In '.fRbuttons': ## 5 button widgets ('Exit','Help','Solve','Show','Reset') ## 1 label and 2 radiobuttons ('Animate:', 'Start', 'Stop') ## and ## 2 buttons (for setting 2 colors). ## ## - In '.fRrhs': ## 1 label widget (may add an entry widget someday) ## ## - In '.fRfactors': ## 3 pairs of 'label' and 'entry' widgets ## ## - In '.fRinit': ## 2 pairs of 'label' and 'entry' widgets ## ## - In '.fRball': ## 1 pair of 'label' and 'entry' widgets ## ## - In '.fRtime': ## 2 paris of 'label' and 'entry' widgets ## ## - In '.fRmsg': ## 1 label widget to display messages to the user, such as ## elapsed execution time, as well as a 'calculation in progress' ## msg or other msgs. ## ## - In '.fRcanvas': 1 'canvas' widget ## ## 3) Define BINDINGS: see the BINDINGS section for bindings, if any ## ## 4) Define procs: ## ## - 'solve' - called by the 'Solve' button ## ## - 'runge-kutta-4' - called by the 'solve' proc ## ## - 'deriv' - called by the 'runge-kutta-4' proc ## ## - 'show_list' - called by the 'ShowList' button ## ## - 'animate' - called by a click on the 'Start' animation radiobutton ## ## - 'setMappingVars_for_px2wc' - called by proc 'animate' ## ## - 'Xpx2wc' - called by proc 'animate' ## - 'Ypx2wc' - called by proc 'animate' ## ## - 'set_ball_color1' - called by the 'BallColor' button ## ## - 'set_background_color2' - called by the 'BackgroundColor' button ## ## - 'update_color_button' - sets background & foreground color of ## either of the 2 color buttons ## ## - 'advise_user' - called by the 'solve' and 'animate' procs ## ## - 'reset_parms' - called by the 'ResetParms' button ## ## - 'edit_inputs' - called by the'solve' and 'animate' procs ## ## - 'decimal_check' - called by the 'edit_inputs' proc ## ## - 'popup_msgVarWithScroll' - called by the 'Help' button ## ## 5) Additional GUI initialization: ## Set some inital values of parameters ## such as the 2 colors and use a call to ## proc 'reset_parms' to starting values ## initial height and initial velocity ## and end-time and time step-size. ## ##+######################################################################## ## DEVELOPED WITH: ## Tcl-Tk 8.5 on Ubuntu 9.10 (2009-october release, 'Karmic Koala'). ## ## $ wish ## % puts "$tcl_version $tk_version" ## showed 8.5 8.5 on Ubuntu 9.10 ## after Tcl-Tk 8.4 was replaced by 8.5 --- to get anti-aliased fonts. ##+####################################################################### ## MAINTENANCE HISTORY: ## Created by: Blaise Montandon 2016jul10 ## Changed by: Blaise Montandon 2016 ##+####################################################################### ##+####################################################################### ## Set general window parms (win-title,win-position). ##+####################################################################### wm title . "Simulate a Bouncing Ball --- an animation" wm iconname . "BouncingBall" wm geometry . +15+30 ##+###################################################### ## Set the color scheme for the window and set the ## background color for the 'trough' in some widgets. ##+###################################################### tk_setPalette "#e0e0e0" set entryBKGD "#fcfcfc" set radbuttBKGD "#f0f0f0" # set scaleBKGD "#f0f0f0" # set chkbuttBKGD "#f0f0f0" # set listboxBKGD "#f0f0f0" ##+######################################################## ## Use a VARIABLE-WIDTH FONT for label and button widgets. ## ## Use a FIXED-WIDTH FONT for listboxes (and ## entry fields, if any). ##+######################################################## font create fontTEMP_varwidth \ -family {comic sans ms} \ -size -14 \ -weight bold \ -slant roman font create fontTEMP_SMALL_varwidth \ -family {comic sans ms} \ -size -12 \ -weight bold \ -slant roman ## Some other possible (similar) variable width fonts: ## Arial ## Bitstream Vera Sans ## DejaVu Sans ## Droid Sans ## FreeSans ## Liberation Sans ## Nimbus Sans L ## Trebuchet MS ## Verdana font create fontTEMP_fixedwidth \ -family {liberation mono} \ -size -14 \ -weight bold \ -slant roman font create fontTEMP_SMALL_fixedwidth \ -family {liberation mono} \ -size -12 \ -weight bold \ -slant roman ## Some other possible fixed width fonts (esp. on Linux): ## Andale Mono ## Bitstream Vera Sans Mono ## Courier 10 Pitch ## DejaVu Sans Mono ## Droid Sans Mono ## FreeMono ## Nimbus Mono L ## TlwgMono ##+########################################################### ## SET GEOM VARS FOR THE VARIOUS WIDGET DEFINITIONS. ## (e.g. width and height of canvas, and padding for Buttons) ##+########################################################### ## BUTTON geom parameters: set PADXpx_button 0 set PADYpx_button 0 set BDwidthPx_button 2 set RELIEF_button "raised" ## LABEL geom parameters: set PADXpx_label 0 set PADYpx_label 0 set BDwidthPx_label 2 # set RELIEF_label "ridge" # set RELIEF_label "raised" set RELIEF_label "flat" ## RADIOBUTTON widget geom settings: set BDwidthPx_radbutt 2 # set RELIEF_radbutt "ridge" set RELIEF_radbutt "raised" ## ENTRY widget geom settings: set BDwidthPx_entry 2 set ParmEntryWidthChars 7 ## SCALE geom parameters: ## (de-activated, for now) if {0} { set BDwidthPx_scale 2 # set initScaleLengthPx 100 set scaleThicknessPx 10 set scaleRepeatDelayMillisecs 800 } ## CANVAS geom parameters: set initCanWidthPx 300 set initCanHeightPx 300 set minCanHeightPx 24 # set BDwidthPx_canvas 2 set BDwidthPx_canvas 0 ##+################################################################### ## Set a MINSIZE of the window. ## ## For width, allow for the minwidth of the '.fRbuttons' frame: ## about 6 buttons, 1 label, 2 radiobuttons ## (Exit,Help,Solve,Reset,Animate,Start,Stop,Color,Color). ## We want to at least be able to see the Exit button. ## ## For height, allow ## 2 chars high for the '.fRbuttons' frame, ## 1 char high for the '.fRrhs' frame, ## 1 char high for the '.fRfactors' frame, ## 1 char high for the '.fRinit' frame, ## 1 char high for the '.fRball' frame, ## 1 char high for the '.fRtime' frame, ## 2 chars high for the '.fRmsg' frame, ## 24 pixels high for the '.fRcanvas' frame. ##+################################################################### ## MIN WIDTH: set minWinWidthPx [font measure fontTEMP_varwidth \ "Exit Help Solve Show Reset Animate Start Stop bouncing ball Background"] ## Add some pixels to account for right-left-side window decoration ## (about 8 pixels), about 9 x 4 pixels/widget for borders/padding for ## 9 widgets. set minWinWidthPx [expr {44 + $minWinWidthPx}] ## MIN HEIGHT --- allow ## 2 chars high for '.fRbuttons' ## 1 char high for '.fRrhs' ## 1 char high for '.fRfactors' ## 1 char high for '.fRinit' ## 1 char high for '.fRball' ## 1 char high for '.fRtime' ## 2 chars high for '.fRmsg' ## 24 pixels high for '.fRcanvas' set CharHeightPx [font metrics fontTEMP_varwidth -linespace] set minWinHeightPx [expr {24 + (9 * $CharHeightPx)}] ## Add about 28 pixels for top-bottom window decoration, ## about 8x4 pixels for each of the 8 stacked frames and their ## widgets (their borders/padding). set minWinHeightPx [expr {$minWinHeightPx + 60}] ## FOR TESTING: # puts "minWinWidthPx = $minWinWidthPx" # puts "minWinHeightPx = $minWinHeightPx" wm minsize . $minWinWidthPx $minWinHeightPx ## We allow the window to be resizable and we pack the canvas with ## '-fill both -expand 1' so that the canvas can be enlarged by enlarging ## the window. ## If you want to make the window un-resizable, ## you can use the following statement. # wm resizable . 0 0 ##+#################################################################### ## Set a TEXT-ARRAY to hold text for buttons & labels on the GUI. ## NOTE: This can aid INTERNATIONALIZATION. This array can ## be set according to a nation/region parameter. ##+#################################################################### ## if { "$VARlocale" == "en"} ## For widgets in 'fRbuttons' frame: set aRtext(buttonEXIT) "Exit" set aRtext(buttonHELP) "Help" set aRtext(buttonSOLVE) "Solve" set aRtext(buttonSHOW) "ShowList" set aRtext(buttonRESET) "ResetParms" set aRtext(labelANIMATE) "Animate:" set aRtext(radbuttSTART) "Start" set aRtext(radbuttSTOP) "Stop" set aRtext(buttonCOLOR1) "Ball Color" set aRtext(buttonCOLOR2) "Background Color" ## For widgets in 'fRrhs' frame: set aRtext(labelRHS1) \ "Acceleration expression to be integrated:" set aRtext(labelRHS2) \ " See 'Help' for more info on solution method." ## For widgets in .fRfactors' frame: set aRtext(labelG) "g (distance-units/sec/sec):" set aRtext(labelK) " k (momentum loss factor at bounces):" set aRtext(labelIMGSIZE) " Image square (pixels):" ## For widgets in 'fRinit' frame: set aRtext(labelINIT1) "Initial Height (distance-units to ball-center):" set aRtext(labelINIT2) " Initial Velocity (distance-units/sec):" ## For widgets in 'fRball' frame: set aRtext(labelRADIUS) "Radius of the ball (distance-units):" ## For widgets in 'fRtime' frame: set aRtext(labelENDTIME) "Solve End Time (secs):" set aRtext(labelSTEPSIZE) " Solve Step Size (secs):" # set aRtext(labelWAITTIME) " AnimationSteps TimeControl (millisecs):" ## For some calls to the 'advise_user' proc: set aRtext(SOLVEmsg) "*** Click 'Solve' when ready to do the solve ***" set aRtext(STARTmsg) "*** Click 'Start' when ready to start the animation ***" ## END OF if { "$VARlocale" == "en"} ##+################################################################ ## DEFINE *ALL* THE FRAMES: ## ## Top-level : '.fRbuttons' '.fRrhs' '.fRfactors' ## '.fRinit' '.fRtime' '.fRmsg' '.fRcanvas' ##+################################################################ ## FOR TESTING change 0 to 1: ## (Example1: To see appearance of frames when borders are drawn.) ## (Example2: To see sizes of frames for various '-fill' options.) ## (Example3: To see how frames expand as window is resized.) if {0} { set RELIEF_frame raised set BDwidthPx_frame 2 } else { set RELIEF_frame flat set BDwidthPx_frame 0 } frame .fRbuttons -relief $RELIEF_frame -borderwidth $BDwidthPx_frame # frame .fRrhs -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRrhs -relief raised -borderwidth 2 frame .fRfactors -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRinit -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRball -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRtime -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRmsg -relief raised -borderwidth 2 # frame .fRcanvas -relief $RELIEF_frame -borderwidth $BDwidthPx_frame frame .fRcanvas -relief raised -borderwidth 2 ##+############################## ## PACK the top-level FRAMES. ##+############################## pack .fRbuttons \ .fRrhs \ .fRfactors \ .fRinit \ .fRball \ .fRtime \ .fRmsg \ -side top \ -anchor nw \ -fill x \ -expand 0 pack .fRcanvas \ -side top \ -anchor nw \ -fill both \ -expand 1 ##+######################################################### ## OK. Now we are ready to define the widgets in the frames. ##+######################################################### ##+##################################################################### ## In the '.fRbuttons' FRAME --- DEFINE ## - 'Exit','Help','Solve','ShowList','ResetParms' buttons ## - a label and 2 start/stop radiobuttons ## and ## - 2 buttons ( to specify colors). ## Then PACK all these widgets. ##+##################################################################### button .fRbuttons.buttEXIT \ -text "$aRtext(buttonEXIT)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {exit} button .fRbuttons.buttHELP \ -text "$aRtext(buttonHELP)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {popup_msgVarWithScroll .topHelp "$HELPtext" +10+10} button .fRbuttons.buttSOLVE \ -text "$aRtext(buttonSOLVE)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {solve} button .fRbuttons.buttSHOW \ -text "$aRtext(buttonSHOW)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -state disabled \ -command {show_list} button .fRbuttons.buttRESET \ -text "$aRtext(buttonRESET)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command {reset_parms} label .fRbuttons.labelANIMATE \ -text "$aRtext(labelANIMATE)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## 'VARanimate0or1' is the var for these 2 radiobuttons. set VARanimate0or1 0 radiobutton .fRbuttons.radbuttSTART \ -text "$aRtext(radbuttSTART)" \ -font fontTEMP_varwidth \ -anchor w \ -variable VARanimate0or1 \ -value 1 \ -selectcolor "$radbuttBKGD" \ -relief $RELIEF_radbutt \ -state disabled \ -bd $BDwidthPx_radbutt radiobutton .fRbuttons.radbuttSTOP \ -text "$aRtext(radbuttSTOP)" \ -font fontTEMP_varwidth \ -anchor w \ -variable VARanimate0or1 \ -value 0 \ -selectcolor "$radbuttBKGD" \ -relief $RELIEF_radbutt \ -bd $BDwidthPx_radbutt button .fRbuttons.buttCOLOR1 \ -text "$aRtext(buttonCOLOR1)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command "set_ball_color1" button .fRbuttons.buttCOLOR2 \ -text "$aRtext(buttonCOLOR2)" \ -font fontTEMP_varwidth \ -padx $PADXpx_button \ -pady $PADYpx_button \ -relief raised \ -bd $BDwidthPx_button \ -command "set_background_color2" ##+########################################### ## Pack the widgets in the 'fRbuttons' frame. ##+########################################### pack .fRbuttons.buttEXIT \ .fRbuttons.buttHELP \ .fRbuttons.buttSOLVE \ .fRbuttons.buttSHOW \ .fRbuttons.buttRESET \ .fRbuttons.labelANIMATE \ .fRbuttons.radbuttSTART \ .fRbuttons.radbuttSTOP \ .fRbuttons.buttCOLOR1 \ .fRbuttons.buttCOLOR2 \ -side left \ -anchor w \ -fill none \ -expand 0 ##+################################################################## ## In the '.fRrhs' FRAME ---- DEFINE 1 LABEL widget. ## Then PACK all these widgets. ##+################################################################## label .fRrhs.labelRHS1 \ -text "$aRtext(labelRHS1)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bg "#66ff66" \ -bd $BDwidthPx_label ## Someday we may initialize this entry widget variable ## (like some others) in the 'Additional GUI Initialization' ## section at the bottom of this script --- via the ## 'reset_parms' proc. set ENTRYexpression "-g" entry .fRrhs.entryEXPRESSION \ -textvariable ENTRYexpression \ -bg $entryBKGD \ -disabledbackground $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## We disable this entry widget for now. ## If we ever enhance this script to allow for ## handling more elaborate acceleration expressions ## (to model air resistance, for example) and allow ## the user to change the expression, ## we would enable this entry widget. .fRrhs.entryEXPRESSION configure -state disabled label .fRrhs.labelRHS2 \ -text "$aRtext(labelRHS2)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bg "#66ff66" \ -bd $BDwidthPx_label ##################################### ## Pack the widgets in frame '.fRrhs'. ##################################### pack .fRrhs.labelRHS1 \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRrhs.entryEXPRESSION \ -side left \ -anchor w \ -fill x \ -expand 1 pack .fRrhs.labelRHS2 \ -side left \ -anchor w \ -fill none \ -expand 0 ##+################################################################## ## In the '.fRfactors' FRAME ---- DEFINE 3 pairs of ## LABEL-and-ENTRY widgets. ## Then PACK all these widgets. ##+################################################################## label .fRfactors.labelG \ -text "$aRtext(labelG)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYg "9.8" entry .fRfactors.entryG \ -textvariable ENTRYg \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR MOMENTUM LOSS FACTOR AT BOUNCE, k: label .fRfactors.labelK \ -text "$aRtext(labelK)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYk "0.9" entry .fRfactors.entryK \ -textvariable ENTRYk \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ##+##################################### ## PACK the widgets in frame '.fRfactors'. ##+##################################### pack .fRfactors.labelG \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRfactors.entryG \ -side left \ -anchor w \ -fill x \ -expand 0 pack .fRfactors.labelK \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRfactors.entryK \ -side left \ -anchor w \ -fill x \ -expand 0 ##+################################################################## ## In the '.fRinit' FRAME ---- DEFINE 2 pairs of ## LABEL and ENTRY widgets. ## Then PACK all these widgets. ##+################################################################## ## FOR INITIAL HEIGHT: label .fRinit.labelINIT1 \ -text "$aRtext(labelINIT1)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYinit1 "8" entry .fRinit.entryINIT1 \ -textvariable ENTRYinit1 \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR INITIAL VELOCITY: label .fRinit.labelINIT2 \ -text "$aRtext(labelINIT2)" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYinit2 "0.0" entry .fRinit.entryINIT2 \ -textvariable ENTRYinit2 \ -bg $entryBKGD \ -font fontTEMP_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ##+##################################### ## PACK the widgets in frame '.fRinit'. ##+##################################### pack .fRinit.labelINIT1 \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRinit.entryINIT1 \ -side left \ -anchor w \ -fill x \ -expand 0 pack .fRinit.labelINIT2 \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRinit.entryINIT2 \ -side left \ -anchor w \ -fill x \ -expand 0 ##+################################################################## ## In the '.fRball' FRAME ---- DEFINE 1 pair of ## LABEL and ENTRY widgets. ## Then PACK all these widgets. ##+################################################################### label .fRball.labelRADIUS \ -text "$aRtext(labelRADIUS)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYradius "0.1" entry .fRball.entryRADIUS \ -textvariable ENTRYradius \ -bg $entryBKGD \ -font fontTEMP_SMALL_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ##+##################################### ## PACK the widgets in frame '.fRball'. ##+##################################### pack .fRball.labelRADIUS \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRball.entryRADIUS \ -side left \ -anchor w \ -fill x \ -expand 0 ##+################################################################## ## In the '.fRtime' FRAME ---- DEFINE 2 pairs of ## LABEL and ENTRY widgets. ## Then PACK all these widgets. ##+################################################################### ## FOR INTEGRATION (solver) END-TIME: label .fRtime.labelENDTIME \ -text "$aRtext(labelENDTIME)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYendtime "20.0" entry .fRtime.entryENDTIME \ -textvariable ENTRYendtime \ -bg $entryBKGD \ -font fontTEMP_SMALL_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR INTEGRATION (solver) STEPSIZE: label .fRtime.labelSTEPSIZE \ -text "$aRtext(labelSTEPSIZE)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYstepsize "0.0" entry .fRtime.entrySTEPSIZE \ -textvariable ENTRYstepsize \ -bg $entryBKGD \ -font fontTEMP_SMALL_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ## FOR ANIMATION-STEPS WAIT-TIME: ## (DE-ACTIVATED FOR NOW) if {0} { label .fRtime.labelWAITTIME \ -text "$aRtext(labelWAITTIME)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYmillisecs "0.0" entry .fRtime.entryWAITTIME \ -textvariable ENTRYmillisecs \ -bg $entryBKGD \ -font fontTEMP_SMALL_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry } ## END OF if {0} ## FOR IMAGE SIZE: label .fRtime.labelIMGSIZE \ -text "$aRtext(labelIMGSIZE)" \ -font fontTEMP_SMALL_varwidth \ -justify left \ -anchor w \ -relief $RELIEF_label \ -bd $BDwidthPx_label ## We initialize this widget var (and others) ## in the GUI initialization section at the ## bottom of this script. ## # set ENTRYimgsize "300" entry .fRtime.entryIMGSIZE \ -textvariable ENTRYimgsize \ -bg $entryBKGD \ -font fontTEMP_SMALL_fixedwidth \ -width $ParmEntryWidthChars \ -relief sunken \ -bd $BDwidthPx_entry ##+##################################### ## PACK the widgets in frame '.fRtime'. ##+##################################### pack .fRtime.labelENDTIME \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRtime.entryENDTIME \ -side left \ -anchor w \ -fill x \ -expand 0 pack .fRtime.labelSTEPSIZE \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRtime.entrySTEPSIZE \ -side left \ -anchor w \ -fill x \ -expand 0 ## We do not implement the wait-time entry field, for now. if {0} { pack .fRtime.labelWAITTIME \ -side left \ -anchor w \ -fill none \ -expand 0 pack .fRtime.entryWAITTIME \ -side left \ -anchor w \ -fill x \ -expand 0 } ## END OF if {0} ## Pack the image-size widget on the RIGHT of the GUI. pack .fRtime.entryIMGSIZE \ -side right \ -anchor e \ -fill x \ -expand 0 pack .fRtime.labelIMGSIZE \ -side right \ -anchor e \ -fill none \ -expand 0 ##+################################################################## ## In the '.fRmsg' FRAME ---- DEFINE-and-PACK 1 LABEL widget. ##+################################################################## label .fRmsg.labelINFO \ -text "" \ -font fontTEMP_varwidth \ -justify left \ -anchor w \ -relief flat \ -bg "#ff6666" \ -bd $BDwidthPx_button pack .fRmsg.labelINFO \ -side left \ -anchor w \ -fill x \ -expand 1 ##+###################################################### ## In the '.fRcanvas' FRAME - DEFINE the 'canvas' widget ## --- no scrollbars, for now. ## Then PACK the widget(s). ##+###################################################### ## We set highlightthickness & borderwidth of the canvas to ## zero, as suggested on page 558, Chapter 37, 'The Canvas ## Widget', in the 4th edition of the book 'Practical ## Programming in Tcl and Tk'. ##+###################################################### canvas .fRcanvas.can \ -width $initCanWidthPx \ -height $initCanHeightPx \ -relief flat \ -highlightthickness 0 \ -borderwidth 0 # -yscrollcommand ".fRcanvas.scrolly set" \ # -xscrollcommand ".fRcanvas.scrollx set" # scrollbar .fRcanvas.scrolly \ # -orient vertical \ # -command ".fRcanvas.can yview" # scrollbar .fRcanvas.scrollx \ # -orient horizontal \ # -command ".fRcanvas.can xview" ##+####################################################### ## PACK the widgets in frame '.fRcanvas'. ## (Skip the scrollbar pack statements, for now.) ##+####################################################### if {0} { ##+######################################################## ## NOTE: ## NEED TO PACK THE SCROLLBARS BEFORE THE CANVAS WIDGET. ## OTHERWISE THE CANVAS WIDGET TAKES ALL THE FRAME SPACE. ##+####################################################### pack .fRcanvas.scrolly \ -side right \ -anchor e \ -fill y \ -expand 0 pack .fRcanvas.scrollx \ -side bottom \ -anchor s \ -fill x \ -expand 0 ##+############################################################## ## !!!NEED TO USE '-expand 0' FOR THE X AND Y SCROLLBARS, so that ## the canvas is allowed to fill the remaining frame-space nicely ## --- without a gap between the canvas and its scrollbars. ##+############################################################## } ## END OF if {0} pack .fRcanvas.can \ -side top \ -anchor n \ -fill none \ -expand 0 ## We don't let the canvas expand, because we want ## to make it a square with colored background --- ## centered at the bottom of the GUI. # -fill both \ # -expand 1 ##+######################################## ## END OF the DEFINITION OF THE GUI WIDGETS ##+######################################## ##+############################### ## BINDINGS SECTION: ##+############################### ##+################################################################### ## Remind user to click 'Solve' after changing a math expression ## parameter or an initial condition or a time parameter. ##+################################################################### bind .fRfactors.entryG \ {advise_user "$aRtext(SOLVEmsg) with new 'g' value."} bind .fRfactors.entryK \ {advise_user "$aRtext(SOLVEmsg) with new momentum-loss factor."} bind .fRinit.entryINIT1 \ {advise_user "$aRtext(SOLVEmsg) with new initial height value."} bind .fRinit.entryINIT2 \ {advise_user "$aRtext(SOLVEmsg) with new initial velocity value."} bind .fRball.entryRADIUS \ {advise_user "$aRtext(SOLVEmsg) with new ball-radius value."} bind .fRtime.entryENDTIME \ {advise_user "$aRtext(SOLVEmsg) with new END-TIME."} bind .fRtime.entrySTEPSIZE \ {advise_user "$aRtext(SOLVEmsg) with new STEP-SIZE."} ##+################################################################ ## Remind user to click animate 'Start' button after changing ## an animation-related parameter. ##+################################################################ bind .fRtime.entryIMGSIZE \ {advise_user "$aRtext(STARTmsg) with new image size value."} bind .fRbuttons.buttCOLOR1 \ {advise_user "$aRtext(STARTmsg) with new bouncing ball color."} bind .fRbuttons.buttCOLOR2 \ {advise_user "$aRtext(STARTmsg) with new background color."} ##+####################################################### ## Start the animation with a button1-release on the ## 'Start' radiobutton. ##+####################################################### bind .fRbuttons.radbuttSTART {animate} ##+###################################################################### ## PROCS SECTION: ## ## - 'solve' - called by the 'Solve' button. ## ## - 'runge-kutta-4' - called by the 'solve' proc. ## ## - 'deriv' - called by the 'runge-kutta-4' proc. ## ## - 'show_list' - called by the 'ShowList' button ## ## - 'animate' - called by a click on the 'Start' animation radiobutton. ## ## - 'setMappingVars_for_px2wc' - called by proc 'animate'. ## ## - 'Xpx2wc' - called by proc 'animate'. ## - 'Ypx2wc' - called by proc 'animate'. ## ## - 'set_ball_color1' - called by the 'BallColor' button. ## ## - 'set_background_color2' - called by the 'BackgroundColor' button. ## ## - 'update_color_button' - sets background & foreground color of ## either of the 2 color buttons. ## ## - 'advise_user' - called by the 'solver' and 'animate' procs. ## ## - 'reset_parms' - called by the 'ResetParms' button and in the ## 'Additional GUI Initialization' section, ## to initialize the parms. ## ## - 'edit_inputs' - called by 'solve' and 'animate' procs ## ## - 'decimal_check' - called by 'edit_inputs' proc ## ## - 'popup_msgVarWithScroll' - called by the 'Help' button. ## ##+####################################################################### ##+##################################################################### ## PROC: solve ## ## PURPOSE: ## Peforms the integration of the pair of first order equations: ## D(u1) = f1(t,u1,u2) = u2 ## D(u2) = f2(t,u1,u2) = -g (This could eventually include ## some air-resistance terms.) ## ## Puts the integration results in global arrays: ## aRtime, aRu1, aRu2, aRhf1, aRhf2 ## ## Also sets the value of Hmax, the max height reached, from u1. ## ## METHOD: ## First do some intialization of variables. Then, in a loop, ## we use the 'runge-kutta-4' proc to get the output u1,u2 --- ## and hf1,hf2 --- for each time step. ## ## hf1,hf2 are essentially h times the weighted averages of ## f1 and f2 that are computed in the 'runge-kutta-4' proc. ## ## The 'deriv' proc provides several derivative (rate of change) ## evaluations ---- evaluations of f1 and f2 --- that are needed ## for the Runge-Kutta-order-4 numerical integration process. ## ## The output of this proc is saved in five global arrays --- ## aRtime, aRu1, aRu2, aRhf1, aRhf2 --- which are ## indexed by integers representing the time-step number. ## ## CALLED BY: By clicking on the 'Solve' button. ##+#################################################################### proc solve {} { ## FOR TESTING: (dummy out this proc) # return global ENTRYg ENTRYk ENTRYinit1 ENTRYinit2 ENTRYradius \ ENTRYendtime ENTRYstepsize EDITcode \ aRtime aRu1 aRu2 aRhf1 aRhf2 Nstep Hmax ################################################ ## Disable the 'ShowList' button during a solve. ## Also the Animate-Start radiobutton. ################################################ .fRbuttons.buttSHOW configure -state disabled .fRbuttons.radbuttSTART configure -state disabled ######################################################## ## Check the entry field inputs. ######################################################## edit_inputs if {$EDITcode > 0} {return} ################################################ ## Set the current time, for determining elapsed ## time for building each 'photo' image. ################################################ set t0 [clock milliseconds] ##################################################################### ## We use local variables time, u1, and u2 to hold the current ## values of the independent variable and 2 dependent variables. ## ## We initialize those 3 local variables here. ##################################################################### set time 0.0 set u1 $ENTRYinit1 set u2 $ENTRYinit2 #################################################################### ## Initialize the 3 arrays --- aRtime, aRu1, aRu2 --- in which ## we store the main solution results. ## Also initialize Nstep (the index of the arrays). #################################################################### set Nstep 0 set aRtime($Nstep) $time set aRu1($Nstep) $u1 set aRu2($Nstep) $u2 #################################################################### ## Initialize Hmax --- to store the highest point reached among ## all the bounces simulated. ## (Hmax is for use in the 'animate' proc, to map ## world-coordinates to pixel-coordinates and set the ## height of the image area.) #################################################################### set Hmax $u1 #################################################################### ## Initialize 2 arrays --- aRhf1 and aRhf2 --- in which we store ## the hf1,hf2 values that increment u1 and u2 at each time step. ## We list these values in the 'show_list' proc --- and these values ## may be used for some error/accuracy checks --- perhaps to ## determine when to stop this solve process. #################################################################### set h [expr {double($ENTRYstepsize)}] set LISTf1ANDf2 [deriv $time $u1 $u2] set f1 [lindex $LISTf1ANDf2 0] set f2 [lindex $LISTf1ANDf2 1] set aRhf1($Nstep) [expr {$h * $f1}] set aRhf2($Nstep) [expr {$h * $f2}] #################################################################### ## Initialize a variable, PREVmax, used to keep track of the highest ## point reached in the current bounce. ## ## Note that u1 is the height of the center of the (rigid) ball ## above the bounce surface. The lowest value of height of the ball ## above the surface will be the radius of the ball. ## ## PREVmax may be checked in the 'if' statement that checks when ## the ball collides with the bounce-surface. ## ## PREVmax may be used in the solve loop, at the 'bounce check-points', ## to set velocity u2 to zero (and height u1 to the ball-radius), ## if the bounces are getting very small. ## ## This is intended to eliminate a 'jitter' condition when ## the ball has lost so much velocity that it is hardly ## bouncing off the surface, but the numeric integration ## process keeps the ball bouncing to tiny heights, due to ## numeric errors or whatever. ## #################################################################### ## There may be other ways of eliminating 'jitter' --- such as ## monitoring the incrementation of u1 and u2 --- say with ## hf1 and hf2. At a bounce time step (that is, in the 'if' ## statement that checks for collision of ball with surface), ## the bouncing (or the solve) could be stopped, ## when the magnitude of hf1 is becoming a significant proportion ## of the magnitude of u1 --- or ## when the magnitude of hf2 is becoming a significant proportion ## of the magnitude of u2. #################################################################### set PREVmax $ENTRYradius ############################################################ ## Set a variable, TINYheight, which may be used in the loop ## at the bounce check-points to check when the bounces ## are tiny and creating a 'jitter' effect. ## ## We set TINYheight to be slightly above ENTRYradius, ## the lowest height that the center of the rigid ball ## can achieve. ############################################################ set TINYheight [expr {1.01 * $ENTRYradius}] # set TINYheight [expr {1.05 * $ENTRYradius}] #################################################################### ## Start the solution loop, which is to go no longer than ## the user-requested end-time. #################################################################### set ENDtime [expr {double($ENTRYendtime)}] while {$time <= $ENDtime} { ############################################################# ## Call the 'runge-kutta-4' proc to get new values of u1,u2 ## from the current values of time,u1,u2 and time-step h. ## Note that 'runge-kutta-4' also outputs values hf1,hf2 --- ## the increment by which u1 and u2 were adjusted --- for ## possible use in stopping 'jitter' bouncing or the solve. ############################################################# set LISTu1outANDu2out [runge-kutta-4 $time $h $u1 $u2] ###################################################### ## Increment the index for the arrays. ###################################################### incr Nstep ############################################################## ## Store the output of the 'runge-kutta-4' proc in the arrays. ############################################################## set aRu1($Nstep) [lindex $LISTu1outANDu2out 0] set aRu2($Nstep) [lindex $LISTu1outANDu2out 1] set aRhf1($Nstep) [lindex $LISTu1outANDu2out 2] set aRhf2($Nstep) [lindex $LISTu1outANDu2out 3] ##################################################### ## Advance the 'time' variable. ## This value becomes the input time for the next step. ## Also save that time as the time location of the ## two outputs aRu1($Nstep) and aRu2($Nstep). ##################################################### set time [expr {$time + $h}] set aRtime($Nstep) $time ######################################################## ## Set the u1 and u2 local variables for the next step. ######################################################## set u1 $aRu1($Nstep) set u2 $aRu2($Nstep) ######################################################## ## Set the hf1 and hf2 local variables for possible use ## in the 'bounce-check' 'if' statement below. ######################################################## set hf1 $aRhf1($Nstep) set hf2 $aRhf2($Nstep) ##################################### ## Reset PREVmax if u1 is larger. ##################################### if {$u1 > $PREVmax} {set PREVmax $u1} ########################################################## ## THE BOUNCE LOGIC: ## (at the end of a completed integration step) ## Here is where we 'flip' the velocity and apply the ## momentum-loss coefficient --- if the height of the ## center of the ball has dropped to be equal to or ## less than the ball-radius. ## ## If u1 'overshoots', below the height which is the radius ## of the ball, set u1 to that radius. ## ## (If we want to be obsessive, we could back off the ## time variable in proportion to the 'overshoot', ## to more accurately time the bounce. The integration ## would continue from the adjusted time, using the ## flipped-velocity for u2 and the radius of the ball ## for u1, i.e. for new initial conditions.) ## ## To avoid a 'jitter' condition when the ball is barely ## bouncing off the surface, we may set the velocity to zero ## when the PREVmax is less than TINYheight --- or when ## hf1 is getting large relative to PREVmax --- or when ## some error estimate is getting large. ########################################################## if {$u1 <= $ENTRYradius} { ## FOR TESTING: (change {0} to {1} to activate) if {0} { ## Write debug msg no more than every 10th time step. if {[expr {$Nstep % 10}] == 0} { puts "" puts "**************" puts "Bounce at time: $time (Step-number: $Nstep)" puts "" puts " TINYheight: $TINYheight ENTRYradius: $ENTRYradius" puts " PREVmax: $PREVmax" puts "" puts "IN: u1 (height): $u1 u2 (velocity): $u2" puts " hf1: $hf1 hf2: $hf2" } } ############################################### ## Assure that u1 does not go below ENTRYradius. ############################################### set u1 $ENTRYradius set aRu1($Nstep) $u1 ####################################################### ## 'Flip' the velocity. ####################################################### set u2 [expr {-($ENTRYk) * $u2}] set aRu2($Nstep) $u2 ####################################################### ## To eliminate a 'jitter' effect, set the velocity to ## zero when the most recent bounce becomes tiny. ####################################################### ## This check can be eliminate/commented if you want to ## let the 'jitter' to take its course, even though ## it may be the result of integration error interfering ## with simulation of accurate acceleration and velocity effects. ################################################################ if {$PREVmax < $TINYheight} { set u2 0.0 set aRu2($Nstep) $u2 } ###################################### ## Reset PREVmax for the next bounce. ###################################### set PREVmax $ENTRYradius ## FOR TESTING: (change {0} to {1} to activate) if {0} { ## Write debug msg no more than every 10th time step. if {[expr {$Nstep % 10}] == 0} { puts "OUT: u1 (height): $u1 u2 (velocity): $u2" } } } ## END OF if {$u1 <= $ENTRYradius} ############################################################# ## Reset Hmax if u1 is greater than the current value of Hmax. ## (Hmax is for use in the 'animate' proc, to map ## world-coordinates to pixel-coordinates.) ############################################################# if {$u1 > $Hmax} { set Hmax $u1 } } ## END OF solution loop ################################################################ ## Augment Hmax by the radius of the ball. Since Hmax is used in ## the 'animate' proc to set the image area in world coordinates, ## this augmentation assures that the top half of the ball ## will be showing at the top of its flight path. ################################################################ set Hmax [expr {$Hmax + $ENTRYradius}] ######################################################## ## Show the user the elapsed-time for this solution run. ######################################################## set solvetime [expr {[clock milliseconds] - $t0}] set simtime [format "%.4f" $time] advise_user "\ ** SOLVE DONE: $solvetime millisecs elapsed ; SIMULATED TIME: $simtime secs ; Steps: $Nstep **" ######################################################## ## Activate the disabled 'ShowList' button ## and the 'Start' radiobutton. ######################################################## .fRbuttons.buttSHOW configure -state normal .fRbuttons.radbuttSTART configure -state normal } ## END OF PROC 'solve' ##+##################################################################### ## PROC: show_list ## ## PURPOSE: ## Creates a columnar list of results of the form: ## t u1 u2 ## where ## t is the time in secs, from 0.0 to ENTRYendtime ## u1 the vertical displacement of the ball ## u2 the vertical velocity of the ball ## ## Puts the list in a text variable, VARlist, and ## shows the list in a popup window by using the ## 'popup_msgVarWithScroll' proc. ## ## METHOD: Uses the 3 arrays created by the 'solve' proc. ## ## CALLED BY: By clicking on the 'ShowList' button. ##+#################################################################### proc show_list {} { ## FOR TESTING: (dummy out this proc) # return global ENTRYg ENTRYk ENTRYinit1 ENTRYinit2 ENTRYradius \ ENTRYendtime ENTRYstepsize \ aRtime aRu1 aRu2 aRhf1 aRhf2 Nstep ################################## ## Create the heading for the list. ################################## set VARlist \ "Simulation of a Bouncing Ball g (gravitational acceleration) = $ENTRYg (distance-units/sec/sec) k (momentum loss factor at each bounce) = $ENTRYk (dimensionless) Initial height: $ENTRYinit1 (distance-units to center of ball) Initial velocity: $ENTRYinit2 (distance-units/sec) Radius of the ball: $ENTRYradius (distance-units) Start time (secs): 0.0 End time (secs): $ENTRYendtime Solver time step, h (secs): $ENTRYstepsize Number of time steps: $Nstep A presentation of the differential equations that were integrated is at the bottom of this list. 'f1' and 'f2' are presented there. Height of the Incremental Incremental Time Center of the Ball Ball Velocity Step Change of Height Change of Velocity (secs) (distance-units) (distance-units/sec) Number (h * averaged-f1) (h * averaged-f2) ---------- -------------------- -------------------- --------- -------------------- -------------------- " ######################################################## ## In a loop over the integer-index of the 3 'aR' arrays, ## put the 3 numbers in each row of the list. ######################################################## set idx 0 while {$idx <= $Nstep} { set timeFMTED [format "%10.4f" $aRtime($idx)] set u1FMTED [format "%20.4f" $aRu1($idx)] set u2FMTED [format "%20.4f" $aRu2($idx)] set nFMTED [format "%9.1d" $idx] set hf1FMTED [format "%20.4f" $aRhf1($idx)] set hf2FMTED [format "%20.4f" $aRhf2($idx)] append VARlist "$timeFMTED $u1FMTED $u2FMTED $nFMTED $hf1FMTED $hf2FMTED \n" incr idx } ## END OF while loop ####################################### ## Add usage info to bottom of the list. ####################################### append VARlist " ---------------------------------------------------------------- With a mouse, you can highlight the lines above and paste them into a text editor window and save that text into a file. Then you can use that text file in a plot utility, such as 'gnuplot', to plot the height and/or the velocity against time. ---------------------------------------------------------------------- DESCRIPTION OF THE DIFFERENTIAL EQUATIONS USED: (and 'f1','f2') This simulation is done by integrating the 'second-order' ODE (ordinary differential equation) for a single, falling mass (a rigid ball) with no frictional forces (such as air resistance), namely D(D(u)) = -g where D represents the time-derivative operator d/dt, and t represents the independent variable time, and u represents the height of the center of the ball above the surface off of which it is bouncing, and g is the acceleration due to gravity (near the surface of the Earth or the Moon or Mars or a mountain-top or whatever) --- assumed to be constant relative to the height-range of the bouncing ball. We convert the single 'second order' differential equation D(D(u)) = -g to two 'first order' differential equations with u1 = u and u2 = D(u) as two functions of t to be generated by integrating 2 'first-order' differential equations: D(u1) = u2 D(u2) = -g for initial conditions u1=A and u2=B, where A is an initial vertical height and B is an initial vertical velocity. The common way of expressing these kinds of systems of first-order differential equations in compact, general form is D(u) = f(t,u) where u and f are N-dimensional vectors. This is a compact way of expressing a system of scalar differential equations: D(u1) = f1(t,u1,...,uN) D(u2) = f2(t,u1,...,uN) ...... D(uN) = fN(t,u1,...,uN) In the case of these bouncing ball equations, N=2, and we can think of solving for the unknown function vector (u1(t),u2(t)) where the right-hand-side (RHS) of the two equations above can be thought of as a special case of a more general user-specified function vector (f1(t,u1,u2),f2(t,u1,u2)) where f1(t,u1,u2) = u2 f2(t,u1,u2) = -g A Runge-Kutta-order-4 integration routine is used to generate values of u1 and u2 at a next time step by equations of the form u1(t+h) = u1(t) + h * averaged-f1(t,u1,u2) u2(t+h) = u2(t) + h * averaged-f2(t,u1,u2) where the 'averaged-f1' and 'averaged-f2' are a weighted average of the functions evaluated at four slightly different values of (t,u1,u2). The increments h * averaged-f1(t,u1,u2) h * averaged-f2(t,u1,u2) are presented in the last 2 columns above. " ############################## ## Show the list. ############################## popup_msgVarWithScroll .topList "$VARlist" +20+30 } ## END OF PROC 'show_list' ##+############################################################## ## PROC: 'runge-kutta-4' ## ## PURPOSE: Let u(t) be a vector function of independent variable ## t (a scalar) --- in this case a 2-dimensional vector ## function composed of 2 scalar functions u1(t) and u2(t). ## ## Let f(t,u) be a vector function of t and vector u --- ## in this case a 2-dimensional vector function ## composed of 2 functions f1(t,u1,u2) and f2(t,u1,u2). ## ## For a given value of t and h (a step-size of t), ## this proc is TO RETURN a pair of floating-point values ## corresponding to using a Runge-Kutta 4th order ## method to evaluate u1(t+h) and u2(t+h). ## ##+###### ## METHOD: ## ## For a first-order differential equation of the form ## ## D(u) = f(t,u) where D is the d/dt derivative operator ## ## and where t is the independent variable and u is a vector ## function of t, ## ## the Runge-Kutta 4th order method (RK4) is often written in the form ## ## u(t+h) = u(t) + (Ka + 2 * Kb + 2 * Kc + Kd) / 6 ## where ## Ka = h * f (t , u(t) ) ## Kb = h * f (t + (h/2), u(t) + (Ka / 2) ) ## Kc = h * f (t + (h/2), u(t) + (Kb / 2) ) ## Kd = h * f (t + h, u(t) + Kc ) ## where ## f and u and the K's are vectors (2-dimensional in this ## oscillating single bouncing ball case). ## ##+########################################### ## An alternate way of writing these equations: ## (essentially factoring out the factor 'h' from the K eqns) ## ## u(t+h) = u(t) + (h/6) * (Da + 2*Db + 2*Dc + Dd) ## where ## Da = f (t , u(t) ) ## Db = f (t + (h/2), u(t) + (h/2)*Da ) ## Dc = f (t + (h/2), u(t) + (h/2)*Db ) ## Dd = f (t + h, u(t) + h*Dc ) ## ##+############### ## Scalar eqns instead of vector eqns: ## ## Let us rewrite these vector equations as twice as many ## scalar equations involving scalar functions --- u1,u2,f1,f2, ## Da1,Da2,Db1,Db2,Dc1,Dc2,Dd1,Dd2. ## ## Let us rewrite them in an order in which they would need ## to be computed. ## ## Da1 = f1 (t , u1(t), u2(t) ) ## Da2 = f2 (t , u1(t), u2(t) ) ## ## Db1 = f1 (t + (h/2), u1(t) + (h/2)*Da1, u2(t) + (h/2)*Da2 ) ## Db2 = f2 (t + (h/2), u1(t) + (h/2)*Da1, u2(t) + (h/2)*Da2 ) ## ## Dc1 = f1 (t + (h/2), u1(t) + (h/2)*Db1, u2(t) + (h/2)*Db2 ) ## Dc2 = f2 (t + (h/2), u1(t) + (h/2)*Db1, u2(t) + (h/2)*Db2 ) ## ## Dd1 = f1 (t + h, u1(t) + h*Dc1, u2(t) + h*Dc2) ## Dd2 = f2 (t + h, u1(t) + h*Dc1, u2(t) + h*Dc2) ## ## hf1 = (h/6) * (Da1 + 2*Db1 + 2*Dc1 + Dd1) ## hf2 = (h/6) * (Da2 + 2*Db2 + 2*Dc2 + Dd2) ## ## u1(t+h) = u1(t) + hf1 ## u2(t+h) = u2(t) + hf2 ## ## This proc returns u1(t+h) and u2(t+h) for given t and h. ## ## This proc also returns hf1 and hf2, in case the increments by which ## u1 and u2 are needed, for example, for error testing. ## ## The values of f1 and f2 for given u1 and u2 --- and t --- ## are given by calls to proc 'deriv' --- before each pair ## of D equations. ## ## So, essentially, the proc 'deriv' provides more input to ## this proc --- namely, the expressions for the derivatives ## that provide the RHS (right-hand-side) of the differential ## equations of motion. ## ## CALLED BY: the 'solve' proc. ##+############################################################## proc runge-kutta-4 {t h u1 u2} { ## FOR TESTING: (dummy out this proc) # return ###################################################### ## Use the 'deriv' proc to get deriv values f1 and f2 ## for the given t,u1,u2. ###################################################### set LISTf1ANDf2 [deriv $t $u1 $u2] set Da1 [lindex $LISTf1ANDf2 0] set Da2 [lindex $LISTf1ANDf2 1] set h2 [expr { $h / 2.0 }] set t_h2 [expr { $t + $h2 }] set u1_hD [expr {$u1 + ($h2 * $Da1)}] set u2_hD [expr {$u2 + ($h2 * $Da2)}] set LISTf1ANDf2 [deriv $t_h2 $u1_hD $u2_hD] set Db1 [lindex $LISTf1ANDf2 0] set Db2 [lindex $LISTf1ANDf2 1] set u1_hD [expr {$u1 + ($h2 * $Db1)}] set u2_hD [expr {$u2 + ($h2 * $Db2)}] set LISTf1ANDf2 [deriv $t_h2 $u1_hD $u2_hD] set Dc1 [lindex $LISTf1ANDf2 0] set Dc2 [lindex $LISTf1ANDf2 1] set t_h [expr { $t + $h}] set u1_hD [expr {$u1 + ($h * $Dc1)}] set u2_hD [expr {$u2 + ($h * $Dc2)}] set LISTf1ANDf2 [deriv $t_h $u1_hD $u2_hD] set Dd1 [lindex $LISTf1ANDf2 0] set Dd2 [lindex $LISTf1ANDf2 1] set h6 [expr { $h / 6.0 }] set hf1 [expr { $h6 * ($Da1 + (2.0 * ($Db1 + $Dc1)) + $Dd1) }] set hf2 [expr { $h6 * ($Da2 + (2.0 * ($Db2 + $Dc2)) + $Dd2) }] set u1out [expr {$u1 + $hf1}] set u2out [expr {$u2 + $hf2}] return "$u1out $u2out $hf1 $hf2" } ## END OF PROC 'runge-kutta-4' ##+############################################################## ## PROC: 'deriv' ## ## PURPOSE: For a given independent variable, t, and for a value ## of a vector u (in this case, a 2-dimensional vector ## with values u1 and u2), this proc is ## TO RETURN a pair of floating-point values for a ## vector function f(t,u) (in this case, for a ## 2-dimensional vector f). ## ## In other words, for 2 scalar functions ## f1(t,u1,u2) ## f2(t,u1,u2) ## this proc returns two floating point values ## determined by evaluating f1 and f2 for the ## given floating point values t, u1, and u2. ## ## METHOD: In this application, ## f1(t,u1,u2) = u2 ## and ## f2(t,u1,u2) = -g ## ## CALLED BY: the 'runge-kutta-4' proc. ##+############################################################## proc deriv {t u1 u2} { ## FOR TESTING: (dummy out this proc) # return global ENTRYg ENTRYk set f1 $u2 set f2 [expr { -$ENTRYg }] ## NOTE: ## t is not used in these functions, but we are ## prepared to allow adding a forcing function ## such as A * sin(k * t). ## ## More likely, we would add a term or two ## representing air resistance as a function ## of velocity u2. return "$f1 $f2" } ## END OF PROC 'deriv' ##+##################################################################### ## PROC: animate ## ## PURPOSE: ## Starts drawing and showing the sequence of bouncing ball positions ## --- with 'create oval' statements. Also use 'create line' to ## draw a horizontal line to represent the flat surface off of ## which the ball is bouncing. ## ## METHOD: ## The height results in the array 'aRu1' are used to ## specify the 'world-coordinates', (x,y), of the bouncing ball. ## ## Periodically checking the status of the 'Stop' radiobutton ## allows the user to stop the animation before the last ## time step is reached. ## ## CALLED BY: By clicking on the 'Start' animation radiobutton. ##+##################################################################### proc animate {} { ## FOR TESTING: (dummy out this proc) # return global VARanimate0or1 aRu1 Nstep Hmax ENTRYradius PXperWCx \ ENTRYimgsize ENTRYstepsize EDITcode ENTRYinit1 \ COLOR1r COLOR1g COLOR1b COLOR1hex \ COLOR2r COLOR2g COLOR2b COLOR2hex ################################################################## ## If the Nstep variable does not exist (i.e. no solver run yet), ## do not perform an animation. ################################################################## if {[info exists Nstep] == 0} {return} ######################################################## ## Check the entry field inputs. ######################################################## edit_inputs if {$EDITcode > 0} {return} ############################################################ ## Set WAITmillisecs variable from ENTRYstepsize seconds. ## (WAITmillisecs is used between each draw of a ## bouncing ball position.) ############################################################ set WAITmillisecs [expr {int(1000.0 * $ENTRYstepsize)}] ############################################################ ## Clear the message area --- replace with an animation msg. ############################################################ advise_user "* Animation IN PROGRESS. Click 'Stop' radiobutton to halt. *" ############################################################ ## Clear the canvas, including the ball and the line that ## represents the flat surface off of which the ball bounces. ############################################################ .fRcanvas.can delete all ############################################################## ## Reconfigure the canvas into a square image area that ## we are going to use --- from the user-specified image size ## in variable ENTRYimgsize. ## ## We want the canvas background color to delineate a square ## area on which the animated bouncing ball image will be drawn. ############################################################## .fRcanvas.can configure -width $ENTRYimgsize .fRcanvas.can configure -height $ENTRYimgsize ############################################################ ## Set the background color of the canvas area from the ## current background color variable. ############################################################ .fRcanvas.can config -bg $COLOR2hex ############################################################# ## We use a 'wm geometry' command to get the window (and the ## canvas widget) to resize appropriately --- even after the ## user has manually resized the top window. ## ## Reference: wiki.tcl.tk/10720 and wiki.tcl.tk/44 ## and page 237 of Ousterhout's book 'Tcl and the Tk Toolkit': ## "If you would like to restore a window to its natural ## size, you can invoke 'wm geometry' with an empty ## geometry string." ############################################################# wm geometry . {} ################################################################ ## Set the variables for converting pixels to world-coords. ## This is in case the user changed the image dimensions. ################################################################ ## Recall input vars for proc 'setMappingVars_for_px2wc' are: ## xORy,ULwcX,ULwcY,ULpxX,ULpxY,LRwcX,LRwcY,LRpxX,LRpxY ## where UL = upper-left and LR = lower-right. ## ## We map the upper-left of the image area: ## world-coordinates (-Hmax/2 , +Hmax) (or a little bigger) ## TO pixel-coordinated ( 0 , 0 ) ## ## AND ## ## We map the lower-left of the image area: ## world-coordinates ( +Hmax/2 , 0.0 ) (or a little bigger) ## TO pixel-coordinates (ENTRYimgsize , ENTRYimgsize) ## ## See code in 'setMappingVars_for_px2wc' for details. ######################################################################## set Hhalf [expr {$Hmax / 2.0}] ## Set a margin value in world-coordinates (relative to Hmax) ## to provide a margin in the image --- ## especially some margin below the plot of the line ## representing the height 0.0 and some margin above the ## ball (the circle) at the top of the flight. set MARGINwc [expr {0.1 * $Hmax}] set XwcUL [expr {-$Hhalf - $MARGINwc}] set YwcUL [expr {$Hmax + $MARGINwc}] set XwcLR [expr {$Hhalf + $MARGINwc}] set YwcLR [expr {0.0 - $MARGINwc}] setMappingVars_for_px2wc xy $XwcUL $YwcUL 0 0 $XwcLR $YwcLR $ENTRYimgsize $ENTRYimgsize ######################################################### ## Draw a horizontal line representing the flat surface ## off of which the ball bounces. Uses 'create line'. ######################################################### ## Set the pixel coordinates of the left and right ## end-points of the line. ######################################################### set surfX1 [expr {-$Hhalf}] set surfX1px [Xwc2px $surfX1] set surfY1 0.0 set surfY1px [Ywc2px $surfY1] set surfX2 $Hhalf set surfX2px [Xwc2px $surfX2] set surfY2px $surfY1px .fRcanvas.can create line $surfX1px $surfY1px $surfX2px $surfY2px \ -tags TAGline -fill "$COLOR1hex" ######################################################### ## Draw text (distances) at the left and right end-points ## of the horizontal line representing the flat surface. ## This gives the user an idea of the scale of the objects ## in the image. Uses 'create text'. ## (If too confusing, this can be de-activated ## by changing 'if {1}' to 'if {0}'.) ######################################################### if {1} { set HhalfFMTED [format "%6.2f" $Hhalf] .fRcanvas.can create text $surfX1px $surfY1px -fill "$COLOR1hex" \ -tags TAGtext -text "-Hmax/2\n-$HhalfFMTED" -anchor sw .fRcanvas.can create text $surfX2px $surfY2px -fill "$COLOR1hex" \ -tags TAGtext -text "+Hmax/2\n+$HhalfFMTED" -anchor se } ######################################################### ## Draw text near the top-middle of the image area that ## indicates the height at that level. ######################################################### set topX 0.0 set topXpx [Xwc2px $topX] set topY $Hmax set topYpx [Ywc2px $topY] set HmaxFMTED [format "%6.2f" $Hmax] .fRcanvas.can create text $topXpx $topYpx -fill "$COLOR1hex" \ -tags TAGtext -text "---- MaxHeight (Hmax) = $HmaxFMTED ----" \ -anchor center ################################################################# ## Set the (constant) x-coordinate of the bouncing ball location ## in world-coords and pixel-coordinates --- for use in the ## loop below over u1(t(i)). ## ## This avoids repeating this little bit of processing ## over and over again. ################################################################# set x 0.0 set xPx [Xwc2px $x] ################################################################# ## Set the ball radius in pixels --- for use in the loop below. ## (We use the global variable PXperWCx which was set in the ## ' setMappingVars_for_px2wc' proc.) ## ## This avoids repeating this little bit of processing ## over and over again. ################################################################# set radiusPx [expr {$PXperWCx * $ENTRYradius}] ############################################################### ## LOOP over the index of the array aRu1 (height of the center ## of the ball) to perform the animation of the bouncing ball ## --- where the index goes from 0 to Nstep. ############################################################### ## This while loop stops when VARanimate0or1 is set to 0 ## --- by the user clicking on the 'Stop' radiobutton. ## ## At the bottom of each pass through the loop, the results-array ## index is incremented. If that index exceeds Nstep, ## we 'break' out of this animation loop. ############################################################### set idx 0 while {$VARanimate0or1 == 1} { ####################################################### ## Delete the bouncing ball (circle) ## from a previous draw iteration. ####################################################### .fRcanvas.can delete -tags TAGcircle ###################################################################### ## Set the y-coordinate of the location of the center of the ## bouncing ball, in world-coords. And convert those world-coordinates ## to pixel coordinates, for use in the 'create oval' command below. ###################################################################### set y $aRu1($idx) set yPx [Ywc2px $y] ########################################################################## ## Draw the bouncing ball --- with 'create oval'. ## Set the upper-left and lower-right corners of the oval (a circle ## in this application). ########################################################################## set massX1Px [expr {$xPx - $radiusPx}] set massY1Px [expr {$yPx - $radiusPx}] set massX2Px [expr {$xPx + $radiusPx}] set massY2Px [expr {$yPx + $radiusPx}] .fRcanvas.can create oval $massX1Px $massY1Px $massX2Px $massY2Px \ -tags TAGcircle -fill "$COLOR1hex" ########################################################## ## Make sure the image is shown before going on to build ## the next image (the next bouncing ball position). ########################################################## update ########################################################## ## Wait a few millisecs before proceeding to erase the ## current bouncing ball drawing and before preparing to ## to draw the next bouncing ball position. ## (To get a better wait-time to make the oscillation ## times more accurate, we could get the draw time using ## a couple of calls to 'clock milliseconds' before and ## after each draw and subtract the indicated draw-time ## from WAITmillsecs.) ########################################################## after $WAITmillisecs incr idx ########################################################### ## If we exceed the number of elements in the array aRu1 ## (that is, the last of the time steps), break out of this ## animation loop. ########################################################### if {$idx > $Nstep} {break} } ## END OF the animation loop (over the index of array aRu1) ############################################################ ## Clear the message area --- replace with an animation msg. ############################################################ advise_user "\ ** ANIMATION ENDED. NumberOfTimeStepsAvailable: $Nstep **" set VARanimate0or1 0 } ## END OF PROC 'animate' ##+######################################################################## ## PROC: 'setMappingVars_for_px2wc' ##+######################################################################## ## PURPOSE: Sets up 'constants' to be used in converting between x,y ## 'world coordinates' and 'pixel coordinates' on a Tk canvas. ## ## Puts the constants in global variables: ## PXperWC BASEwcX BASEwcY BASEpxX BASEpxY ## ## These variables are for use by 'Xwc2px' and 'Ywc2px' procs ## and 'Xpx2wc' and 'Ypx2wc' procs. ## ## The 'BASE' variables are coordinates of the upper-left point ## of the 'plotting rectangle' --- in world coordinates and ## in pixel coordinates. ## ## METHOD: This proc takes the coordinates of an UpperLeft (UL) ## point and a LowerRight (LR) point --- in both ## 'world coordinates' and 'pixel coordinates' and ## sets some global variables to the used by the ## other drawing procs --- mainly the ratio: ## ## the number-of-pixels-per-world-coordinate-unit, ## in global variable 'PXperWC' ## ## (This will generally include a fractional amount, ## i.e. it is not necessarily an integer.) ## ## INPUTS: ## ## At least eight numbers are input to this proc, as indicated by: ## ## ULwcX ULwcY ULpxX ULpxY LRwcX LRwcY LRpxX LRpxY ## ## Generally, the 'wc' inputs may be floating point numbers, and the ## 'px' inputs will generally be (non-negative) integers. ## ## Example: (for a plot area with x between -1.2 and +1.2 ## and with y between -0.2 and +1.2) ## setMappingVars_for_px2wc xy -1.2 1.2 0 0 1.2 -0.2 $canvasWidthPx $canvasHeightPx ## ## The first argument can be either 'x' or 'y' or 'xy'. This determines whether ## global variable 'PXperWC' is detemined by just the X-numbers, just the Y-numbers, ## or both. In this script, we use 'xy' (both). ## ## An 'adjustYpx' global variable can be used to adjust if the pixels ## on a user's monitor are not square. ## ## OUTPUTS: global variables PXperWC BASEwcX BASEwcY BASEpxX BASEpxY ## ## CALLED BY: by the animate' proc. ##+######################################################################## proc setMappingVars_for_px2wc {xORy ULwcX ULwcY ULpxX ULpxY LRwcX LRwcY LRpxX LRpxY} { global PXperWCx PXperWCy BASEwcX BASEwcY BASEpxX BASEpxY adjustYpx ## FOR TESTING: (to dummy out this proc) # return ############################################################ ## Calculate PXperWCx and PXperWCy ## (pixels-per-world-coordinate-unit) --- the ratio ## of pixels-per-world-coordinate in the x and y directions, ## for the given UL and LR values. ############################################################ set PXperWCx [expr {abs(($LRpxX - $ULpxX) / ($LRwcX - $ULwcX))}] set PXperWCy [expr {abs(($LRpxY - $ULpxY) / ($LRwcY - $ULwcY))}] ## FOR TESTING: if {0} { puts "proc 'animate':" puts "LRwcY: $LRwcY ULwcY: $ULwcY LRwcX: $LRwcX ULwcX: $ULwcX" puts "PXperWCx: $PXperWCx" puts "LRpxY: $LRpxY ULpxY: $ULpxY LRpxX: $LRpxX ULpxX: $ULpxX" puts "PXperWCy: $PXperWCy" } ############################################################# ## Reset PXperWCx and PXperWCy according to whether input ## variable 'xORy' is 'x' or 'y' or 'min' or 'xy'. ## ## For 'x', we set PXperWCy equal to PCperWcx. ## ## For 'y', we set PXperWCx equal to PCperWcy. ## ## For 'min', we set PXperWCx and PXperWCy to the smaller ## of PXperWCx and PXperWCy. ## ## For 'xy', we will leave PXperWCx and PXperWCy unchanged. ############################################################# if {$xORy == "x"} { set PXperWCy $PXperWCx } elseif {$xORy == "y"} { set PXperWCx $PXperWCy } elseif {$xORy == "min"} { if {$PXperWCx > $PXperWCy} { set PXperWCx $PXperWCy } else { set PXperWCy $PXperWCx } } ## END OF if {$xORy == "x"} ############################################################ ## In case the pixels are not square, provide a factor ## that can be used to adjust in the Y direction. ############################################################ set adjustYpx 1.0 ############################################################ ## Set BASEwcX, BASEwcY, BASEpxX and BASEpxY. ############################################################ set BASEwcX $ULwcX set BASEwcY $ULwcY set BASEpxX $ULpxX set BASEpxY $ULpxY ## FOR TESTING: if {0} { puts "proc 'setMappingVars_for_px2wc':" puts "PXperWCx: $PXperWCx PXperWCy: $PXperWCy" puts "BASEwcX: $BASEwcX BASEwcY: $BASEwcY" puts "BASEpxX: $BASEpxX BASEpxY: $BASEpxY" } } ## END OF PROC 'setMappingVars_for_px2wc' ##+######################################################################## ## PROC: 'Xwc2px' ##+######################################################################## ## PURPOSE: Converts an x world-coordinate to pixel units. ## ## CALLED BY: the 'draw' procs ##+######################################################################## proc Xwc2px {x} { global PXperWCx BASEwcX BASEpxX set px [expr {($x - $BASEwcX) * $PXperWCx + $BASEpxX}] return $px } ## END OF PROC 'Xwc2px' ##+######################################################################## ## PROC: 'Xpx2wc' ##+######################################################################## ## PURPOSE: Converts an x-pixel unit to an x-world-coordinate value. ## ## CALLED BY: the 'StartAnimation' proc ##+######################################################################## proc Xpx2wc {px} { global PXperWCx BASEwcX BASEpxX set x [expr {( ($px - $BASEpxX) / $PXperWCx ) + $BASEwcX }] return $x } ## END OF PROC 'Xpx2wc' ##+######################################################################## ## PROC: 'Ywc2px' ##+######################################################################## ## PURPOSE: Converts an y world-coordinate to pixel units. ## ## CALLED BY: the 'draw' procs ##+######################################################################## proc Ywc2px {y} { global PXperWCy BASEwcY BASEpxY adjustYpx set px [expr {($BASEwcY - $y) * $PXperWCy * $adjustYpx + $BASEpxY}] return $px } ## END OF PROC 'Ywc2px' ##+######################################################################## ## PROC: 'Ypx2wc' ##+######################################################################## ## PURPOSE: Converts a y-pixel unit to a y-world-coordinate value. ## ## CALLED BY: the 'StartAnimation' proc ##+######################################################################## proc Ypx2wc {px} { global PXperWCy BASEwcY BASEpxY adjustYpx set y [expr { $BASEwcY - ( ($px - $BASEpxY) / ( $PXperWCy * $adjustYpx ) ) }] return $y } ## END OF PROC 'Ypx2wc' ##+##################################################################### ## PROC: 'set_ball_color1' ##+##################################################################### ## PURPOSE: ## ## This procedure is invoked to get an RGB triplet ## via 3 RGB slider bars on the FE Color Selector GUI. ## ## Uses that RGB value to set a 'bouncing ball' color. ## ## Arguments: global variables ## ## CALLED BY: .fRbuttons.buttCOLOR1 button ##+##################################################################### proc set_ball_color1 {} { global COLOR1r COLOR1g COLOR1b COLOR1hex ColorSelectorScript aRtext ## FOR TESTING: # puts "COLOR1r: $COLOR1r" # puts "COLOR1g: $COLOR1g" # puts "COLOR1b: $COLOR1b" set TEMPrgb [ exec $ColorSelectorScript $COLOR1r $COLOR1g $COLOR1b] ## FOR TESTING: # puts "TEMPrgb: $TEMPrgb" if { "$TEMPrgb" == "" } { return } scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB set COLOR1hex "#$hexRGB" set COLOR1r $r255 set COLOR1g $g255 set COLOR1b $b255 ## Set background-and-foreground colors of the indicated color button. update_color_button color1 advise_user "$aRtext(STARTmsg) to use a new color" } ## END OF proc 'set_ball_color1' ##+##################################################################### ## PROC: 'set_background_color2' ##+##################################################################### ## PURPOSE: ## ## This procedure is invoked to get an RGB triplet ## via 3 RGB slider bars on the FE Color Selector GUI. ## ## Uses that RGB value to set a 'background' color. ## ## Arguments: global variables ## ## CALLED BY: .fRbuttons.buttCOLOR2 button ##+##################################################################### proc set_background_color2 {} { global COLOR2r COLOR2g COLOR2b COLOR2hex ColorSelectorScript aRtext ## FOR TESTING: # puts "COLOR2r: $COLOR2r" # puts "COLOR2g: $COLOR2g" # puts "COLOR2b: $COLOR2b" set TEMPrgb [ exec $ColorSelectorScript $COLOR2r $COLOR2g $COLOR2b] ## FOR TESTING: # puts "TEMPrgb: $TEMPrgb" if { "$TEMPrgb" == "" } { return } scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB set COLOR2hex "#$hexRGB" set COLOR2r $r255 set COLOR2g $g255 set COLOR2b $b255 ## Set background-and-foreground colors of the indicated color button. update_color_button color2 advise_user "$aRtext(STARTmsg) to use new color." } ## END OF proc 'set_background_color2' ##+##################################################################### ## PROC: 'update_color_button' ##+##################################################################### ## PURPOSE: ## This procedure is invoked to set the background color of the ## color button, indicated by the 'colorID' string, ## to its currently set 'colorID' color --- and sets ## foreground color, for text on the button, to a suitable black or ## white color, so that the label text is readable. ## ## Arguments: global color vars ## ## CALLED BY: in two 'set_*_color' procs ## and in the additional-GUI-initialization section at ## the bottom of this script. ##+##################################################################### proc update_color_button {colorID} { global COLOR1r COLOR1g COLOR1b COLOR1hex global COLOR2r COLOR2g COLOR2b COLOR2hex # set colorBREAK 300 set colorBREAK 350 if {"$colorID" == "color1"} { .fRbuttons.buttCOLOR1 configure -bg $COLOR1hex set sumCOLOR1 [expr {$COLOR1r + $COLOR1g + $COLOR1b}] if {$sumCOLOR1 > $colorBREAK} { .fRbuttons.buttCOLOR1 configure -fg "#000000" } else { .fRbuttons.buttCOLOR1 configure -fg "#ffffff" } } elseif {"$colorID" == "color2"} { .fRbuttons.buttCOLOR2 configure -bg $COLOR2hex set sumCOLOR1 [expr {$COLOR2r + $COLOR2g + $COLOR2b}] if {$sumCOLOR1 > $colorBREAK} { .fRbuttons.buttCOLOR2 configure -fg "#000000" } else { .fRbuttons.buttCOLOR2 configure -fg "#ffffff" } } else { ## Seems to be an invalid colorID. return } } ## END OF PROC 'update_color_button' ##+##################################################################### ## PROC: 'advise_user' ##+##################################################################### ## PURPOSE: Puts a message to the user on the GUI. ## ## CALLED BY: in three 'set_*_color*' procs, ## in some 'bind' statements in the BIND section above, ## and in the additional-GUI-initialization section at ## the bottom of this script. ##+##################################################################### proc advise_user {text} { .fRmsg.labelINFO configure -text "$text" ## Make sure the text is displayed on the GUI. update ## Alternatively, we could put the message in the title-bar ## of the GUI window. (But it is easy for the user to ## fail to see the message there. Besides, we have more ## options in displaying the message by putting it on a ## Tk widget in the GUI.) ## # wm title . "$text" } ## END OF PROC 'advise_user' ##+######################################################################## ## PROC: 'reset_parms' ##+######################################################################## ## PURPOSE: To reset the 'entry' widgets on the GUI ## --- math expression parameters, initial value parameters, ## and two time parameters (endtime and stepsize). ## ## CALLED BY: 'ResetParms' button and in the 'Additional GUI Initialization' ## section at the bottom of this script. ##+######################################################################## proc reset_parms {} { global ENTRYg ENTRYk ENTRYexpression \ ENTRYinit1 ENTRYinit2 ENTRYradius \ ENTRYendtime ENTRYstepsize ####################################### ## Set parameters g and k. ####################################### set ENTRYg 9.8 # set ENTRYg 32.2 # set ENTRYk 0.92 set ENTRYk 0.84 ############################################# ## Set the expression (RHS) to be integrated. ############################################# ## The entry widget for this variable may ## someday be used to allow the user to enter ## (and change) more complex expressions than ## simply the constant, -g. For example, ## something like: ## -g -k1*D(u) -k2*sign(D(u))*D(u)^2 ## to model air resistance. ############################################# # set ENTRYexpression "-g ...." ################################## ## Set initial-height of the ball. ## (for example, in meters) ################################## # set ENTRYinit1 8.0 set ENTRYinit1 2.0 #################################### ## Set initial-velocity of the ball. #################################### set ENTRYinit2 0.0 ################################################# ## Set radius of the ball. ## (This should probably be set relative to ## the initial-height value, ENTRYinit1, above.) ################################################# # set ENTRYradius 0.05 set ENTRYradius 0.1 ########################################## ## Set time values for the solve (seconds). ########################################## # set ENTRYendtime 20.0 set ENTRYendtime 10.0 # set ENTRYstepsize 0.01 set ENTRYstepsize 0.005 } ## END OF PROC 'reset_parms' ##+#################################################################### ## PROC: 'edit_inputs' ##+##################################################################### ## PURPOSE: Checks entry widgets entries and pops up an error message ## if the data is invalid. ## ## CALLED BY: the 'solve' and 'animate' procs ##+##################################################################### proc edit_inputs {} { ## Decimal/floating-point variables. global ENTRYg ENTRYk ENTRYinit1 ENTRYinit2 ENTRYradius \ ENTRYendtime ENTRYstepsize ## Integer variables. global ENTRYimgsize ## Error indicator. global EDITcode ## We could do without the following EDITcode variable, by using ## a code with the 'return' statement herein. But using this ## code variable is a little more self-documenting. global EDITcode set EDITcode 0 ####################################################### ## Remove trailing and leading blanks (if any) from the ## user entries in the 'entry' widgets. ####################################################### set ENTRYg [string trim $ENTRYg] set ENTRYk [string trim $ENTRYk] set ENTRYinit1 [string trim $ENTRYinit1] set ENTRYinit2 [string trim $ENTRYinit2] set ENTRYradius [string trim $ENTRYradius] set ENTRYendtime [string trim $ENTRYendtime] set ENTRYstepsize [string trim $ENTRYstepsize] set ENTRYimgsize [string trim $ENTRYimgsize] ######################################################################### ## Check that these entry fields are NOT blank. ######################################################################### set MSGblank "is blank. Must NOT be blank." if {"$ENTRYg" == ""} { popup_msgVarWithScroll .topErr "The gravitation constant g $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYk" == ""} { popup_msgVarWithScroll .topErr "The momentum-loss-at-bounce factor $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYinit1" == ""} { popup_msgVarWithScroll .topErr "The initial height $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYinit2" == ""} { popup_msgVarWithScroll .topErr "The initial velocity $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYradius" == ""} { popup_msgVarWithScroll .topErr "The ball-radius $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYendtime" == ""} { popup_msgVarWithScroll .topErr "The end-time for the solve $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYstepsize" == ""} { popup_msgVarWithScroll .topErr "The solve step-size $MSGblank" +10+10 set EDITcode 1 return } if {"$ENTRYimgsize" == ""} { popup_msgVarWithScroll .topErr "The size of the image square (pixels) $MSGblank" +10+10 set EDITcode 1 return } ########################################################## ## Check that ENTRYimgsize is an integer. ########################################################## set MSGnotInteger " is NOT INTEGER." if {![string is integer -strict "$ENTRYimgsize"]} { popup_msgVarWithScroll .topErr "The size of the image square (pixels) $MSGnotInteger" +10+10 set EDITcode 1 return } ######################################################################### ## Check that ENTRYg ENTRYk ENTRYinit1 ENTRYinit2 ENTRYradius ## ENTRYendtime and ENTRYstepsize are decimal numbers (positive or negative) ## --- such as 1.234 or -3 or -3.0 or -.4 or .5 or 7 ######################################################################### ## Implemented using the 'decimal_check' proc below. ######################################################################### set NUMERICmsg "should be a decimal number. Examples: 1.234 or 0.56 or -.789" if {![decimal_check "$ENTRYg"]} { popup_msgVarWithScroll .topErr "The gravitational constant g $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYk"]} { popup_msgVarWithScroll .topErr "The momentum-loss-at-bounce factor $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYinit1"]} { popup_msgVarWithScroll .topErr "The initial height $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYinit2"]} { popup_msgVarWithScroll .topErr "The intial velocity $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYradius"]} { popup_msgVarWithScroll .topErr "The ball-radius $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYendtime"]} { popup_msgVarWithScroll .topErr "The solve end-time (seconds) $NUMERICmsg" +10+10 set EDITcode 1 return } if {![decimal_check "$ENTRYstepsize"]} { popup_msgVarWithScroll .topErr "The solve step-size (seconds) $NUMERICmsg" +10+10 set EDITcode 1 return } ####################################################################### ## Check that ENTRYg ENTRYk ENTRYradius ENTRYendtime ENTRYstepsize ## are not negative. ####################################################################### set POSITIVEmsg "should be a POSITIVE number. Examples: 1.234 or 0.56" if {$ENTRYg < 0.0} { popup_msgVarWithScroll .topErr "The gravitational constant g $POSITIVEmsg" +10+10 set EDITcode 1 return } if {$ENTRYk < 0.0} { popup_msgVarWithScroll .topErr "The momentum-loss-at-bounce factor $POSITIVEmsg" +10+10 set EDITcode 1 return } if {$ENTRYradius < 0.0} { popup_msgVarWithScroll .topErr "The ball-radius $POSITIVEmsg" +10+10 set EDITcode 1 return } if {$ENTRYendtime < 0.0} { popup_msgVarWithScroll .topErr "The solve end-time (seconds) $POSITIVEmsg" +10+10 set EDITcode 1 return } if {$ENTRYstepsize < 0.0} { popup_msgVarWithScroll .topErr "The solve step-size (seconds) $POSITIVEmsg" +10+10 set EDITcode 1 return } ########################################################################## ## Check that ENTRYk is less than or equal to 1.0 (not greater than 1.0). ## (For now, we de-activate this check, so that the user can model ## the freak situation of a ball that gains energy with each bounce.) ########################################################################## # if {$ENTRYk > 1.0} { # popup_msgVarWithScroll .topErr "The momentum-loss-at-bounce factor should not be greater than 1.0" +10+10 # set EDITcode 1 # return # } } ## END of proc 'edit_inputs' ##+######################################################################## ## PROC: 'decimal_check' ##+######################################################################## ## PURPOSE: Returns 1 or 0 if the input string looks like a decimal number ## --- positive or negative. Example numbers that are OK: ## 1.234 12.34 0.234 .234 6 ## -1.234 -12.34 -0.234 -.234 -6 ##+######################################################################## ## References (lots of one-liners): ## http://stackoverflow.com/questions/2072222/regular-expression-for-positive-and-a-negative-decimal-value-in-java ## http://stackoverflow.com/questions/308122/simple-regular-expression-for-a-decimal-with-a-precision-of-2 ## http://stackoverflow.com/questions/4246077/matching-numbers-with-regular-expressions-only-digits-and-commas/4247184#4247184 ## ## More specific to Tcl-Tk (including multi-liners): ## http://wiki.tcl.tk/989 'Regular Expression Examples' ## http://wiki.tcl.tk/768 'Entry Validation' - See "Integer forbidding leading zero:" ## http://wiki.tcl.tk/10166 'string is' ## http://wiki.tcl.tk/40710 'significant digits rounding' - uses regexp to split a number - ## Splits using: if {[regexp {^([+,-]?)([0-9]+)(\.?[0-9]*)?([eE][+-]?[0-9]+)?$} $num -> s i d e]} ## Removes leading zero with: regexp {^(0*)([1-9][0-9]*)$} $i -> NULL DIG ## http://wiki.tcl.tk/530 'Unit converter' has a regexp to parse numbers: ## set RE {(?ix) # Ignore case, extended syntax ## ([-+]?) # Optional leading sign ## ([0-9]*) # Integer part ## \.? # Optional decimal point ## ([0-9]*) # Fractional part ## (e?[0-9]*) # Optional exponent ## } ## ##+######################################################################## ## I do not mind incurring a little (minute amount of) processing ## with a multiple-line implementation. Probably easier to fix if ## a string gets through --- such as ".0.3" (two decimal points). ## ## CALLED BY: proc 'edit_inputs' ##+######################################################################## proc decimal_check {string} { set PosDecimalOK [regexp {^([0-9]*)\.?([0-9]*)$} "$string"] set NegDecimalOK [regexp {^\-([0-9]*)\.?([0-9]*)$} "$string"] set PosNakedDecimalOK [regexp {^\.?([0-9]*)$} "$string"] set NegNakedDecimalOK [regexp {^\-\.?([0-9]*)$} "$string"] set IntegerOK [string is integer $string] set retCODE [expr { \ $PosDecimalOK || $NegDecimalOK || \ $PosNakedDecimalOK || $NegNakedDecimalOK || \ $IntegerOK }] ## FOR TESTING: if {0} { puts "" puts "decimal_check:" puts "string: $string" puts "PosDecimalOK: $PosDecimalOK" puts "NegDecimalOK: $NegDecimalOK" puts "PosNakedDecimalOK: $PosNakedDecimalOK" puts "NegNakedDecimalOK: $NegNakedDecimalOK" puts "IntegerOK: $IntegerOK" puts "retCODE: $retCODE" } return $retCODE } ## END of proc 'decimal_check' ##+######################################################################## ## PROC: 'popup_msgVarWithScroll' ##+######################################################################## ## PURPOSE: Report help or error conditions to the user. ## ## We do not use focus,grab,tkwait in this proc, ## because we use it to show help when the GUI is idle, ## and we may want the user to be able to keep the Help ## window open while doing some other things with the GUI ## such as putting a filename in the filename entry field ## or clicking on a radiobutton. ## ## For a similar proc with focus-grab-tkwait added, ## see the proc 'popup_msgVarWithScroll_wait' in a ## 3DterrainGeneratorExaminer Tk script. ## ## REFERENCE: page 602 of 'Practical Programming in Tcl and Tk', ## 4th edition, by Welch, Jones, Hobbs. ## ## ARGUMENTS: A toplevel frame name (such as .fRhelp or .fRerrmsg) ## and a variable holding text (many lines, if needed). ## ## CALLED BY: 'help' button ##+######################################################################## ## To have more control over the formatting of the message (esp. ## words per line), we use this 'toplevel-text' method, ## rather than the 'tk_dialog' method -- like on page 574 of the book ## by Hattie Schroeder & Mike Doyel,'Interactive Web Applications ## with Tcl/Tk', Appendix A "ED, the Tcl Code Editor". ##+######################################################################## proc popup_msgVarWithScroll { toplevName VARtext ULloc} { ## global fontTEMP_varwidth #; Not needed. 'wish' makes this global. ## global env # bell # bell ################################################# ## Set VARwidth & VARheight from $VARtext. ################################################# ## To get VARheight, ## split at '\n' (newlines) and count 'lines'. ################################################# set VARlist [ split $VARtext "\n" ] ## For testing: # puts "VARlist: $VARlist" set VARheight [ llength $VARlist ] ## For testing: # puts "VARheight: $VARheight" ################################################# ## To get VARwidth, ## loop through the 'lines' getting length ## of each; save max. ################################################# set VARwidth 0 ############################################# ## LOOK AT EACH LINE IN THE LIST. ############################################# foreach line $VARlist { ############################################# ## Get the length of the line. ############################################# set LINEwidth [ string length $line ] if { $LINEwidth > $VARwidth } { set VARwidth $LINEwidth } } ## END OF foreach line $VARlist ## For testing: # puts "VARwidth: $VARwidth" ############################################################### ## NOTE: VARwidth works for a fixed-width font used for the ## text widget ... BUT the programmer may need to be ## careful that the contents of VARtext are all ## countable characters by the 'string length' command. ############################################################### ##################################### ## SETUP 'TOP LEVEL' HELP WINDOW. ##################################### catch {destroy $toplevName} toplevel $toplevName # wm geometry $toplevName 600x400+100+50 # wm geometry $toplevName +100+50 wm geometry $toplevName $ULloc wm title $toplevName "Note" # wm title $toplevName "Note to $env(USER)" wm iconname $toplevName "Note" ##################################### ## In the frame '$toplevName' - ## DEFINE THE TEXT WIDGET and ## its two scrollbars --- and ## DEFINE an OK BUTTON widget. ##################################### if {$VARheight > 10} { text $toplevName.text \ -wrap none \ -font fontTEMP_fixedwidth \ -width $VARwidth \ -height $VARheight \ -bg "#f0f0f0" \ -relief raised \ -bd 2 \ -yscrollcommand "$toplevName.scrolly set" \ -xscrollcommand "$toplevName.scrollx set" ## -font fontTEMP_varwidth scrollbar $toplevName.scrolly \ -orient vertical \ -command "$toplevName.text yview" scrollbar $toplevName.scrollx \ -orient horizontal \ -command "$toplevName.text xview" } else { text $toplevName.text \ -wrap none \ -font fontTEMP_fixedwidth \ -width $VARwidth \ -height $VARheight \ -bg "#f0f0f0" \ -relief raised \ -bd 2 ## -font fontTEMP_varwidth } button $toplevName.butt \ -text "OK" \ -font fontTEMP_varwidth \ -command "destroy $toplevName" ############################################### ## PACK *ALL* the widgets in frame '$toplevName'. ############################################### ## Pack the bottom button BEFORE the ## bottom x-scrollbar widget, pack $toplevName.butt \ -side bottom \ -anchor center \ -fill none \ -expand 0 if {$VARheight > 10} { ## Pack the scrollbars BEFORE the text widget, ## so that the text does not monopolize the space. pack $toplevName.scrolly \ -side right \ -anchor center \ -fill y \ -expand 0 ## DO NOT USE '-expand 1' HERE on the Y-scrollbar. ## THAT ALLOWS Y-SCROLLBAR TO EXPAND AND PUTS ## BLANK SPACE BETWEEN Y-SCROLLBAR & THE TEXT AREA. pack $toplevName.scrollx \ -side bottom \ -anchor center \ -fill x \ -expand 0 ## DO NOT USE '-expand 1' HERE on the X-scrollbar. ## THAT KEEPS THE TEXT AREA FROM EXPANDING. pack $toplevName.text \ -side top \ -anchor center \ -fill both \ -expand 1 } else { pack $toplevName.text \ -side top \ -anchor center \ -fill both \ -expand 1 } ################################################ ## Set some 'event' bindings to allow for ## easy scrolling through huge listings. ## is a press of the Page-Down key. ## is a press of the Page-Up key. ## is a press of the Home key ## to go to the top of the listing. ## is a press of the End key ## to go to the bottom of the listing. ## is a press of the Up-arrow key. ## is a press of the Down-arrow key. ################################################ bind $toplevName "$toplevName.text yview scroll +1 page" bind $toplevName "$toplevName.text yview scroll -1 page" bind $toplevName "$toplevName.text see 1.0" bind $toplevName "$toplevName.text see end" bind $toplevName "$toplevName.text yview scroll -1 unit" bind $toplevName "$toplevName.text yview scroll +1 unit" ##################################### ## LOAD MSG INTO TEXT WIDGET. ##################################### ## $toplevName.text delete 1.0 end $toplevName.text insert end $VARtext $toplevName.text configure -state disabled } ## END OF PROC 'popup_msgVarWithScroll' ##+######################################################## ## Set the 'HELPtext' var. ##+######################################################## set HELPtext \ " ** HELP for this 'Simulate a Bouncing Ball' ** * software application * (which includes an option to animate the bouncing ball on a 'canvas') This Tk GUI script solves a second-order differential equation for the vertical fall and bouncing of a ball. The script uses numerical integration (a Runge-Kutta method) --- combined with an algebraic method to model the bouncing of the ball. The ODE (ordinary differential equation) for a single, falling mass (the ball) with no frictional forces (such as air resistance) is D(D(u)) = -g where D represents the time-derivative operator d/dt, and t represents the independent variable time, and u represents the height of the ball above the surface off of which it is bouncing, and g is the acceleration due to gravity (near the surface of the Earth or the Moon or Mars or a mountain-top or whatever) --- assumed to be constant relative to the height-range of the bouncing ball. We assume that u is the position of the CENTER of the ball, and, when the rigid, unsquishable ball (like a golf ball) hits the solid, ungiving surface, the height u is the radius of the spherical ball. We consider the height u to be measured such that it is increasing in the upward direction. Then the velocity, D(u), of the mass when it is falling is negative (downward). (You can see this if you think of the velocity as being delta-height over delta-time, and observe that for the falling mass, delta-height is negative while delta-time is positive.) Also note that because the velocity is getting 'more and more negative' as the mass falls, the acceleration is negative. (Delta-velocity over delta-time is a negative number over a positive.) Hence, since we let g be a positive constant, we need the negative sign in front of g in the expression above for the instantaneous acceleration of the mass at any time t. Galileo showed that if one releases a mass and allows it to fall freely under the influence of the constant accelerative attraction toward the center of the earth (g), then the distance the mass falls in any time t from the time of release from rest (i.e. starting with zero velocity) is g*t^2/2 --- g times t squared over 2. Actually, the concept of a gravitational constant, g, came after his time. Galileo showed that the distance fallen is proportional to the time squared. In any case, note that the mass of the ball is not involved in this expression. This corresponds to the fact that Galileo observed that different masses, dropped from the same height, hit the ground at the same time. (He actually used spherical masses rolling down a linear inclined path, so that he could get fairly accurate measurements of the time elapsed in travelling to various distances along the inclined path. He could adjust the incline so that the ball rolled slow enough to get the time measurements.) So, from what Galileo discovered, we learned that we do not need numerical integration to solve the simple ODE D(D(u) = -g. If the initial height at time zero is denoted u(0), and the ball is dropped (initial velocity is zero), the height at time t is given by u(0) - g*t^2/2 However, in spite of the fact that we have a simple algebraic expression for the solution of the ODE, we use numerical integration to do the solve. This makes it relatively easy to handle additional terms describing effects like air resistance (which is related to the downward or upward velocity of the ball). If air resistance is proportional to the magnitude of the velocity or to the velocity squared or some combination of the two, then we can use numerical integration to solve a differential equation that may look something like D(D(u)) = -g -k1*D(u) -k2*sign(D(u))*D(u)^2 where D(u) represents the signed velocity of the mass. (Although these equations still may not involve the mass of the ball, note that the constants of resistance, k1 and k2, will surely depend on factors like the profile of the ball, like its radius, and the density of the air.) ********************* MODELLING THE BOUNCE: ********************* (in an algebraic way, rather than using an extremely 'stiff' differential equation) We model the bounce by switching the velocity direction of the ball when it strikes the surface (when u is less than or equal to the radius of the ball) --- so the velocity D(u) is set to -D(u). To allow for modeling a ball that is losing height with each bounce because of a loss of some momentum with each impact with the surface, we introduce a positive constant k which is less than or equal to 1. When we switch the direction of the velocity, we also reduce the velocity magnitude by the factor k. Hence the velocity D(u) becomes -k*D(u) at each bounce. ***************** SOME GUI FEATURES: ***************** The GUI allows the user to enter various values for g. To get meaningful results, the units for g should be compatible with the units that you want for the output --- the height and velocity of the ball. For example: g = 9.80665 meters-per-second-per-second will model the ball height in meters and the ball velocity in meters per second or g = 32.174 feet-per-second-per-second will model the ball height in feet and the ball velocity in feet per second (Note that the equation for acceleration of the ball does not involve mass. So the GUI does not need to prompt for mass of the ball.) The GUI allows the user to enter various values for k, the velocity-loss coefficient. This should generally be between 0.0 and 1.0. (Since the ball is not losing mass at the moment of impact, this coefficient could also be called a momentum-loss coefficient.) The GUI also allows the user to enter parameters for the solver process: - an initial height of the center of the ball - an initial (vertical) velocity of the ball (typically zero, to simulate dropping the ball rather than throwing it) - the radius of the ball - an end-time for the end of the solution process - a time-step, h, for the solution process. Reasonable starting values would be 9.8 (meters/sec/sec) for the gravitational acceleration at the surface of the Earth, and initial height of 2.0 (meters, for a ball drop from about the height of a human). A radius of 0.1 (meters) is 10 centimeters (about 4 inches) --- or a ball about 8 inches in diameter. Following a solution run, the GUI also allows the user to start (and stop) an animation of a bouncing ball drawn on a Tk canvas. The GUI allows the user to specify the size of a square image area for the animation, in pixels. The animation is shown on a square Tk 'canvas' widget by using 'create oval' and 'delete' commands on the canvas. ************** USE OF THE GUI: ************** The Tk GUI allows the user to change the solve parameters: g, k, initial-height, initial-vertical-velocity, ball-radius, time-step-size, end-time. Once these parameters are set as desired, the user can click on the 'Solve' button to perform a solve run. After a 'Solve' run, the user can use the 'ShowList' button to show a list of solution values --- triplets t(i), u1(t(i)), u2(t(i)) --- time, height, velocity --- in a popup window. In addition, on the GUI, the 'Start' and 'Stop' radiobuttons can be used to start and stop an animation run. To perform the animation, the values of u1 that were generated from the last click on the 'Solve' button are used to generate the animation. Before starting an animation, the user can use the 2 'Color' buttons to call up an RGB-color-selector GUI by which to specify the 2 colors for the animation drawing on the canvas. A wait-time (in millisecs) between computing and displaying each new bouncing ball position controls the speed of the animation. This wait-time value is calculated based on converting the user-selected time-step, h seconds, to milliseconds. (This seems to work nicely, so the GUI does not prompt the user for a parameter, such as a wait-time parameter, to control the speed of the animation.) ************************* EXPERIMENTING VIA THE GUI: ************************* It was indicated above that the acceleration of the bouncing ball (except at the moment of a bounce) is given by a constant 'g', a value for gravitational acceleration that is sufficiently accurate, if the bounces are not 'out of this world'. Then, to make the bouncing ball fall more quickly, the user can increase g. And to make the bouncing ball accelerate downward more slowly, the user can decrease g. One could experiment with g to compare how fast a bouncing ball would travel (and how high it would re-bound) on the surface of a planet like Jupiter or Mars or Saturn --- versus on Earth. Here is a table of approximate gravitational acceleration values on the surface of several large spherical celestial bodies: Planet g (meters/sec/sec) --------- ------------------ Mercury 3.8 Venus 8.8 Earth 9.8 Mars 3.8 Jupiter 25 Saturn 10.4 Uranus 10.4 Neptune 13.8 Moons g (meters/sec/sec) ---------------- ------------------ Earth's Moon 1.62 Jupiter's Callisto 1.23 Jupiter's Europa 1.31 Jupiter's Ganymede 1.43 Jupiter's Io 1.80 Saturn's Titan 1.35 One source of values like these is Wikipedia. The user can enter negative or positive values for the initial velocity --- to simulate throwing the ball down or up. The user can use a value of 1.0 for k, the loss-factor --- to see a simulation of a 'perfectly elastic' ball --- that is, no energy losses at each bounce. The user can use the 'ResetParms' button to return to the values that were initially placed in the entry widgets of the GUI. **************************************************** METHOD of MATH MODELLING OF THE BOUNCING BALL MOTION: **************************************************** We convert the single 'second order' differential equation D(D(u)) = -g to two 'first order' differential equations with u1 = u and u2 = D(u) as the functions of t to be generated by integrating 2 differential eqns: D(u1) = u2 D(u2) = -g for initial conditions u1=A and u2=B, where A is an initial vertical height and B is an initial vertical velocity. The common way of expressing these kinds of systems of first order differential equations in compact, general form is D(u) = f(t,u) where u and f are N-dimensional vectors. This is a compact way of expressing a system of scalar differential equations: D(u1) = f1(t,u1,...,uN) D(u2) = f2(t,u1,...,uN) ...... D(uN) = fN(t,u1,...,uN) In the case of these bouncing ball equations, N=2, and we can think of solving for the unknown function vector (u1(t),u2(t)) where the right-hand-side (RHS) of the two equations above can be thought of as a special case of a more general user-specified function vector (f1(t,u1,u2),f2(t,u1,u2)) where f1(t,u1,u2) = u2 f2(t,u1,u2) = -g We use the popular Runge-Kutta 4th order method (RK4) to perform the numerical integration for a user-specified time step, h. We basically use two procs to perform the integration steps: - a proc to perform the RK4 integration for N=2--- giving the values of (u1,u2) for each time step - a proc to evaluate the RHS function: (f1,f2) for specified values of t,u1,u2. The latter proc is called several times by the former proc for each time step. *************************************************** METHOD of PLOTTING THE ANIMATION on the TK CANVAS: *************************************************** After a solution, we have the solution functions u1 and u2 for a sequence of equally-spaced time values. We use function u1 to do the animation. For each time value, t(i), the bouncing ball is drawn as a simple color-filled circle representing the ball. The GUI provides 2 buttons by which the user can specify the 2 colors for: - the canvas background - the bouncing ball (the circle) and the bounce-surface (a line). An 'animate' proc performs the bouncing ball animation when the user clicks on the 'Start' radiobutton of the GUI. This animate proc uses the 'world-coordinates' --- the values of height u1 --- to draw the bouncing ball within a square area of about Hmax by Hmax in world coordinates, where Hmax is the maximum height, over the surface on which the ball bounces, reached by the ball during a solve run. We use u1 to represent the location of the center of the ball, so we need to adust the max height of the bounce by the radius of the ball to accomodate the top half of the ball. We think of Hmax, below, as representing that 'augmented' height. We think of the ball bouncing up and down in the y-direction, with no 'side' forces on the ball in the x-direction. Hence the x-coordinate of the location of the ball stays constant. We imagine the Hmax-by-Hmax square to have an xy-coordinate system overlaid on it such that the origin (0.0,0.0) is at the middle of the bottom of the square. In other words, the y-coordinate of the square goes from 0.0 at the bottom of the square to Hmax at the top of the square --- and the x-coordinate goes from -Hmax/2 at the left side of the square to +Hmax/2 at right side of the square. We think of the bottom of the path of the center of the rigid, extremely hard bouncing ball (non-squishable, like a golf ball) as being above the origin --- (0.0,ball-radius). We use the height function u1(t(i)) of the center of the ball to set the y coordinate of the location of the center of the bouncing ball, and we keep the x-coordinate of the (x,y) position of the center of the ball at 0.0 --- the x-mid-point of the square. Note that the x,y coordinates of the upper-left corner of the square is (-Hmax/2,+Hmax) and the lower-right corner of the square is (+Hmax/2, 0.0). The Hmax-by-Hmax area allows for the bouncing ball to bounce to extremes --- from height zero to height Hmax. A proc is provided which maps the plot area limits in world coordinates --- say UpperLeftCorner: (-Hmax/2,+Hmax) LowerRightCorner: (+Hmax/2,0.0) to the corners of the plot area in pixel coordinates: UpperLeftCorner: (0,0) LowerRightCorner: (ImageWidthPx,ImageHeightPx) Hmax is augmented by the radius of the ball, to accomodate the top-half of the ball in the image area. So Hmax is at least as high as the initial height of the ball plus its radius. Actually, we augment the world-coordinate limits by a value of about one-tenth of Hmax --- to allow for a little margin at the top and bottom of the path of the bouncing ball. To get a square image area, we use ImageWidthPx=ImageHeightPx and we determine this number of pixels by allowing the user to specify the integer value in an entry widget on the GUI. The 'animate' proc uses 2 procs --- Xwc2Xpx and Ywc2Ypx --- to convert the world coordinates of each point --- such as the location of the center of the bouncing ball --- and the radius of the ball --- to pixel coordinates. The pixel-coordinates are used in the 'create oval' command to redraw the bouncing ball for each time step. " ##+##################################################### ##+##################################################### ## ADDITIONAL GUI INITIALIZATION, if needed (or wanted). ##+##################################################### ##+##################################################### ##+##################################################### ## Set the full-name of the RGB color-selector Tk script ## that is used in several procs above. ##+##################################################### ## FOR TESTING: # puts "argv0: $argv0" set DIRthisScript "[file dirname $argv0]" ## For ease of testing in a Linux/Unix terminal and located at the ## directory containing this Tk script. Set the full directory name. if {"$DIRthisScript" == "."} { set DIRthisScript "[pwd]" } set DIRupOne "[file dirname "$DIRthisScript"]" set DIRupTwo "[file dirname "$DIRupOne"]" set ColorSelectorScript "$DIRupTwo/SELECTORtools/tkRGBselector/sho_colorvals_via_sliders3rgb.tk" ## Alternatively: Put the RGB color-selector Tk script in the ## same directory as this Tk script and uncomment the following. # set ColorSelectorScript "$DIRthisScript/sho_colorvals_via_sliders3rgb.tk" ##+############################################# ## Initialize the bouncing ball-color. ## ## (Change 'if {1}' to 'if {0}' to try an ## alternative bouncing ball-color.) ##+############################################# if {1} { ## Yellow: set COLOR1r 255 set COLOR1g 255 set COLOR1b 0 } else { ## White: set COLOR1r 255 set COLOR1g 255 set COLOR1b 255 } set COLOR1hex [format "#%02X%02X%02X" $COLOR1r $COLOR1g $COLOR1b] update_color_button "color1" ##+############################################# ## Initialize the background-color. ## ## (Change 'if {1}' to 'if {0}' to try an ## alternative min-color.) ##+############################################# if {1} { ## Dark Blue: set COLOR2r 0 set COLOR2g 0 set COLOR2b 170 } else { ## Red: set COLOR2r 255 set COLOR2g 0 set COLOR2b 0 } set COLOR2hex [format "#%02X%02X%02X" $COLOR2r $COLOR2g $COLOR2b] update_color_button "color2" #+####################################################### ## Initialize the entry widgets on the GUI. ##+###################################################### reset_parms ##+############################################################### ## Set the wait-millisecs between each draw of the bouncing ball ## in the 'animate' proc. ## (To be used if we activate the entry widget for ## ENTRYmillisecs.) ##+############################################################### # set ENTRYmillisecs 100 ##+############################################################### ## Initialize the image-size entry field. ##+############################################################### set ENTRYimgsize 300 ##+##################################################### ## Advise the user how to start. ##+##################################################### advise_user "$aRtext(SOLVEmsg)"