Animate a pendulum

From Rosetta Code
Revision as of 22:30, 7 August 2009 by rosettacode>Glennj (add Ruby)
Task
Animate a pendulum
You are encouraged to solve this task according to the task description, using any language you may know.

One good way of making an animation is by simulating a physical system and illustrating the variables in that system using a dynamically changing graphical display. The classic such physical system is a simple gravity pendulum.

For this task, create a simple physical model of a pendulum and animate it.

E

Works with: E-on-Java

(Uses Java Swing for GUI. The animation logic is independent, however.)

The angle of a pendulum with length and acceleration due to gravity with all its mass at the end and no friction/air resistance has an acceleration at any given moment of

Failed to parse (SVG (MathML can be enabled via browser plugin): Invalid response ("Math extension cannot connect to Restbase.") from server "https://wikimedia.org/api/rest_v1/":): {\displaystyle \frac{d^2θ}{dt^2}\theta = -\frac{g}{L} \sin \theta}

This simulation uses this formula directly, updating the velocity from the acceleration and the position from the velocity; inaccuracy results from the finite timestep.

The event flow works like this: The clock object created by the simulation steps the simulation on the specified in the interval. The simulation writes its output to angle, which is a Lamport slot which can notify of updates. The whenever set up by makeDisplayComponent listens for updates and triggers redrawing as long as interest has been expressed, which is done whenever the component actually redraws, which happens only if the component's window is still on screen. When the window is closed, additionally, the simulation itself is stopped and the application allowed to exit. (This logic is more general than necessary; it is designed to be suitable for a larger application as well.)

<lang e>#!/usr/bin/env rune pragma.syntax("0.9")

def pi := (-1.0).acos() def makeEPainter := <unsafe:com.zooko.tray.makeEPainter> def makeLamportSlot := <import:org.erights.e.elib.slot.makeLamportSlot> def whenever := <import:org.erights.e.elib.slot.whenever> def colors := <awt:makeColor>

  1. --------------------------------------------------------------
  2. --- Definitions

def makePendulumSim(length_m :float64,

                   gravity_mps2 :float64,
                   initialAngle_rad :float64,
                   timestep_ms :int) {
 var velocity := 0
 def &angle := makeLamportSlot(initialAngle_rad)
 def k := -gravity_mps2/length_m
 def timestep_s := timestep_ms / 1000
 def clock := timer.every(timestep_ms, fn _ {
   def acceleration := k * angle.sin()
   velocity += acceleration * timestep_s
   angle    += velocity     * timestep_s
 })
 return [clock, &angle]

}

def makeDisplayComponent(&angle) {

 def c
 def updater := whenever([&angle], fn { c.repaint() })
 
 bind c := makeEPainter(def paintCallback {
   to paintComponent(g) {
     try {
       def originX := c.getWidth() // 2
       def originY := c.getHeight() // 2
       def pendRadius := (originX.min(originY) * 0.95).round()
       def ballRadius := (originX.min(originY) * 0.04).round()
       def ballX := (originX + angle.sin() * pendRadius).round()
       def ballY := (originY + angle.cos() * pendRadius).round()
       g.setColor(colors.getWhite())
       g.fillRect(0, 0, c.getWidth(), c.getHeight())
       g.setColor(colors.getBlack())
       
       g.fillOval(originX - 2, originY - 2, 4, 4)
       g.drawLine(originX, originY, ballX, ballY)
       g.fillOval(ballX - ballRadius, ballY - ballRadius, ballRadius * 2, ballRadius * 2)
     
       updater[] # provoke interest provided that we did get drawn (window not closed)
     } catch p {
       stderr.println(`In paint callback: $p${p.eStack()}`)
     }
   }
 })
 
 c.setPreferredSize(<awt:makeDimension>(300, 300))
 return c

}

  1. --------------------------------------------------------------
  2. --- Application setup

def [clock, &angle] := makePendulumSim(1, 9.80665, pi*99/100, 10)

  1. Create the window

def frame := <unsafe:javax.swing.makeJFrame>("Pendulum") frame.setContentPane(def display := makeDisplayComponent(&angle)) frame.addWindowListener(def mainWindowListener {

 to windowClosing(_) {
   clock.stop()
   interp.continueAtTop()
 }
 match _ {}

}) frame.setLocation(50, 50) frame.pack()

frame.show() clock.start() interp.blockAtTop()</lang>

Works with: UCB Logo

<lang logo> make "angle 45 make "L 1 make "bob 10

to draw.pendulum

 clearscreen
 seth :angle+180		; down on screen is 180
 forward :L*100-:bob
 penup
 forward :bob
 pendown
 arc 360 :bob

end

make "G 9.80665 make "dt 1/30 make "acc 0 make "vel 0

to step.pendulum

 make "acc  -:G / :L * sin :angle
 make "vel   :vel   + :acc * :dt
 make "angle :angle + :vel * :dt
 wait :dt*60
 draw.pendulum

end

hideturtle until [key?] [step.pendulum] </lang>

Ruby

Library: RubyTk
Translation of: Tcl

This does not have the window resizing handling that Tcl does -- I did not spend enough time in the docs to figure out how to get the new window size out of the configuration event. Of interest when running this pendulum side-by-side with the Tcl one: the Tcl pendulum swings noticibly faster.

<lang ruby>require 'tk'

$root = TkRoot.new("title" => "Pendulum Animation") $canvas = TkCanvas.new($root) do

 width 320
 height 200
 create TkcLine, 0,25,320,25,   'tags' => 'plate', 'width' => 2, 'fill' => 'grey50'
 create TkcOval, 155,20,165,30, 'tags' => 'pivot', 'outline' => "", 'fill' => 'grey50'
 create TkcLine, 1,1,1,1, 'tags' => 'rod', 'width' => 3, 'fill' => 'black'
 create TkcOval, 1,1,2,2, 'tags' => 'bob', 'outline' => 'black', 'fill' => 'yellow'

end $canvas.pack('fill' => 'both', 'expand' => true)

$Theta = 45.0 $dTheta = 0.0 $length = 150 $homeX = 160 $homeY = 25

def show_pendulum

 angle = $Theta * Math::PI / 180
 x = $homeX + $length * Math.sin(angle)
 y = $homeY + $length * Math.cos(angle)
 $canvas.coords('rod', $homeX, $homeY, x, y)
 $canvas.coords('bob', x-15, y-15, x+15, y+15)
 $canvas.raise('pivot')

end

def recompute_angle

 scaling = 3000.0 / ($length ** 2)
 # first estimate
 firstDDTheta = -Math.sin($Theta * Math::PI / 180) * scaling
 midDTheta = $dTheta + firstDDTheta
 midTheta = $Theta + ($dTheta + midDTheta)/2
 # second estimate
 midDDTheta = -Math.sin(midTheta * Math::PI / 180) * scaling
 midDTheta = $dTheta + (firstDDTheta + midDDTheta)/2
 midTheta = $Theta + ($dTheta + midDTheta)/2
 # again, first
 midDDTheta = -Math.sin(midTheta * Math::PI / 180) * scaling
 lastDTheta = midDTheta + midDDTheta
 lastTheta = midTheta + (midDTheta + lastDTheta)/2
 # again, second
 lastDDTheta = -Math.sin(lastTheta * Math::PI/180) * scaling
 lastDTheta = midDTheta + (midDDTheta + lastDDTheta)/2
 lastTheta = midTheta + (midDTheta + lastDTheta)/2
 # Now put the values back in our globals
 $dTheta  = lastDTheta
 $Theta = lastTheta

end

def animate

 recompute_angle
 show_pendulum
 $after_id = $root.after(15) {animate}

end

show_pendulum $after_id = $root.after(500) {animate}

$canvas.bind('<Destroy>') {$root.after_cancel($after_id)}

Tk.mainloop</lang>

Tcl

Works with: Tcl version 8.5

and

Library: Tk

<lang tcl>package require Tcl 8.5 package require Tk

  1. Make the graphical entities

pack [canvas .c -width 320 -height 200] -fill both -expand 1 .c create line 0 25 320 25 -width 2 -fill grey50 -tags plate .c create line 1 1 1 1 -tags rod -width 3 -fill black .c create oval 1 1 2 2 -tags bob -fill yellow -outline black .c create oval 155 20 165 30 -fill grey50 -outline {} -tags pivot

  1. Set some vars

set points {} set Theta 45.0 set dTheta 0.0 set pi 3.1415926535897933 set length 150 set homeX 160

  1. How to respond to a changing in size of the window

proc resized {width} {

   global homeX
   .c coords plate 0 25 $width 25
   set homeX [expr {$width / 2}]
   .c coords pivot [expr {$homeX-5}] 20 [expr {$homeX+5}] 30
   showPendulum

}

  1. How to actually arrange the pendulum, mapping the model to the display

proc showPendulum {} {

   global Theta dTheta pi length homeX
   set angle [expr {$Theta * $pi/180}]
   set x [expr {$homeX + $length*sin($angle)}]
   set y [expr {25 + $length*cos($angle)}]
   .c coords rod $homeX 25 $x $y
   .c coords bob [expr {$x-15}] [expr {$y-15}] [expr {$x+15}] [expr {$y+15}]

}

  1. The dynamic part of the display

proc recomputeAngle {} {

   global Theta dTheta pi length
   set scaling [expr {3000.0/$length**2}]
   # first estimate
   set firstDDTheta [expr {-sin($Theta * $pi/180)*$scaling}]
   set midDTheta [expr {$dTheta + $firstDDTheta}]
   set midTheta [expr {$Theta + ($dTheta + $midDTheta)/2}]
   # second estimate
   set midDDTheta [expr {-sin($midTheta * $pi/180)*$scaling}]
   set midDTheta [expr {$dTheta + ($firstDDTheta + $midDDTheta)/2}]
   set midTheta [expr {$Theta + ($dTheta + $midDTheta)/2}]
   # Now we do a double-estimate approach for getting the final value
   # first estimate
   set midDDTheta [expr {-sin($midTheta * $pi/180)*$scaling}]
   set lastDTheta [expr {$midDTheta + $midDDTheta}]
   set lastTheta [expr {$midTheta + ($midDTheta + $lastDTheta)/2}]
   # second estimate
   set lastDDTheta [expr {-sin($lastTheta * $pi/180)*$scaling}]
   set lastDTheta [expr {$midDTheta + ($midDDTheta + $lastDDTheta)/2}]
   set lastTheta [expr {$midTheta + ($midDTheta + $lastDTheta)/2}]
   # Now put the values back in our globals
   set dTheta $lastDTheta
   set Theta $lastTheta

}

  1. Run the animation by updating the physical model then the display

proc animate {} {

   global animation
   recomputeAngle
   showPendulum
   # Reschedule
   set animation [after 15 animate]

} set animation [after 500 animate]; # Extra initial delay is visually pleasing

  1. Callback to handle resizing of the canvas

bind .c <Configure> {resized %w}

  1. Callback to stop the animation cleanly when the GUI goes away

bind .c <Destroy> {after cancel $animation}</lang>