ABU 1-oct-2011
This is a small experiment trying to add a kinetic behavior to some dragged objects.
You can click and drag one of the boxes in the main window and then, when you release the mouse button, the dragged object will continue its path with a progressively decreasing speed.
You can experiment this kinetic scrolling by changing the friction coefficient.
Note that, though it's trivial to get the mouse position, it's very hard to get the instantaneous mouse speed (this is a key factor for setting a 'good' motion equation). For this reason, there are two sliders controlling the min and max speeed.
When you release the mouse button after dragging, the instantaneous mouse speed is recorded and used for setting the kinetic effect.
No scrolling is performed if speed is too low (it will be unnoticeable) and, since mouse movements can be very fast (>4000pixel/sec), speed is limited to a speed-limit.
Code is really simplified in order to highlight the basic principle. Objects are here implemented as rectangle-items in a canvas widget but it's not difficult to abstract and implement them with other kinds of widgets. Note that the scrolling is performed as a chain of async callbacks, without blocking the main processing thread
For those interested with math, when you release the mouse button after dragging an object, an opposite force proportional to the speed is applied.
a(t) = -k*v(t)
a(t) acceleration (t=0 is when you relase the mouse-button)
k is a friction coefficient (k=0 means no friction at all)By integrating this equation you can get a formula for the speed v(t) and for the position p(t) ( or better the displacement after t=0 )
v(t) = v(0)*exp(-k*t) p(t)=v(0)/k*(1-exp(k*t)) ( we assume p(0)=0 )
Note that speed never reaches zero and position (displacement) tends to v(0)/k.
here is the code:
# Inertial Scrolling
# A small experiment on inertial movement
# Drag the box and let it scroll ..
# --------------------------------------------------------------------------
# parameters; fell free to change them (or use slide controls)
# WARNING: must be real numbers, not integers
# --------------------------------------------------------------------------
set ANIM(K) 4.5 ; # friction
set ANIM(VMIN) 50.0 ; # Velocity min - (pixel/sec)
set ANIM(VMAX) 1000.0 ; # Velocity max - (pixel/sec)
# --------------------------------------------------------------------------
# prepare a canvas with two boxes
# plus some parameters' controls
# --------------------------------------------------------------------------
label .msg -text "Drag the boxes and let them scroll...\nClick on canvas to stop them"
pack .msg
canvas .c -bg gray -bd 2 -relief sunken
pack .c -expand 1 -fill both -padx 10 -pady 10
# a probe for dragging speed
scale .speed -orient horizontal -variable V -label "Detected Speed" \
-from 0 -to 4000 -state disabled -relief flat
pack .speed -fill x
# note max and min values are just a suggestion
frame .s
scale .s.s1 -orient horizontal -variable ANIM(K) -label "Friction" \
-from 1.0 -to 12.0 -resolution 0.1
scale .s.s2 -orient horizontal -variable ANIM(VMIN) -label "Min speed" \
-from 10 -to 100 -resolution 0.1
scale .s.s3 -orient horizontal -variable ANIM(VMAX) -label "Max speed" \
-from 100 -to 2000 -resolution 0.1
pack {*}[winfo children .s] -side left -fill x -expand 1
pack .s -fill x
.c create rectangle 0 0 200 150 -fill orange -tags slidingBox
.c create rectangle 130 100 500 300 -fill yellow -tags slidingBox
# =============================================================================
# return current time in *seconds* as a decimal number
# with the highest precision (microseconds)
proc now {} { expr [clock microseconds]/1E6 }
.c bind slidingBox <ButtonPress-1> {
global MOUSE
set w %W
set MOUSE(X) %x
set MOUSE(Y) %y
set MOUSE(T) [now]
set MOUSE(VX) 0.0
set MOUSE(VY) 0.0
$w raise current
}
.c bind slidingBox <B1-Motion> {
global MOUSE
set w %W
set x %x
set y %y
set t [now]
set dx [expr {$x-$MOUSE(X)}]
set dy [expr {$y-$MOUSE(Y)}]
set dt [expr {$t-$MOUSE(T)}]
set MOUSE(X) $x
set MOUSE(Y) $y
set MOUSE(T) $t
set MOUSE(VX) [expr double($dx)/$dt]
set MOUSE(VY) [expr double($dy)/$dt]
set ::V [expr {hypot($MOUSE(VX),$MOUSE(VY))}] ; # just a probe for display
$w move current $dx $dy
}
# when you release mouse button, target should keep on moving,
# with an initial speed depending on the 'last' speed and the time spent
# from the last movement
.c bind slidingBox <ButtonRelease-1> {
global MOUSE
global ANIM
set w %W
set dt [expr {[now]-$MOUSE(T)}]
set DECEL [expr {exp(-$ANIM(K)*$dt)}]
set vx [expr {$MOUSE(VX) * $DECEL}]
set vy [expr {$MOUSE(VY) * $DECEL}]
# if speed magnitude is too low, it's useless to continue !
set vLen [expr {hypot($vx,$vy)}]
set ::V $vLen ; # just a probe for display
if { $vLen < $ANIM(VMIN) } return
# if speed is too high, limit it
if { $vLen > $ANIM(VMAX) } {
# reduce vLen to ANIM(VMAX)
set a [expr {$vLen / $ANIM(VMAX)}]
set vx [expr {$vx/$a}]
set vy [expr {$vy/$a}]
set vLen [expr double($ANIM(VMAX))]
}
# since speed zero won't be never reached,
# compute time to reach speed VMIN
set a [expr {$ANIM(VMIN) / $vLen}]
set duration [expr {log(1/$a)/$ANIM(K)}]
# then compute the position (displacement) we will reach in $duration
set DX [expr {$vx/$ANIM(K)*( 1-$a )}]
set DY [expr {$vy/$ANIM(K)*( 1-$a )}]
set id [$w find withtag current]
lassign [$w coords $id] X0 Y0
# move object from (X0,Y0) to (X0+DX,Y0+DY) within time-interval (now, now+duration)
_KEEPONMOVING [list $w $id] $X0 $Y0 $DX $DY [now] $duration
}
# when you click, stop all moving objects
bind .c <ButtonPress-1> {
global ANIM
foreach {key val} [array get ANIM ID,*] {
after cancel $val
array unset ANIM $key
}
}
proc _KEEPONMOVING {objID X0 Y0 DX DY T0 DT} {
global ANIM
lassign $objID w id
set t [expr ([now]-$T0)/$DT]
if { $t > 1.0 } { set t 1.0 }
# ---- easing transformation -----
# set tt [expr {1-exp(-$ANIM(K)*$t)}]
set tt [expr {1-exp(-$ANIM(K)*$t*$DT)}]
# - - - - - -
set px [expr {$X0+$tt*$DX}]
set py [expr {$Y0+$tt*$DY}]
_MOVE_TO $w $id $px $py
if { $t < 1.0 } {
set ANIM(ID,$objID) [after 40 [list _KEEPONMOVING $objID $X0 $Y0 $DX $DY $T0 $DT]]
} else {
# end of animation, unset unused memory
array unset ANIM ID,$objID
}
}
proc _MOVE_TO {w id x y} {
lassign [$w coords $id] x0 y0
$w move $id [expr $x-$x0] [expr $y-$y0]
}Ro: Very cool. Enjoyable!
APE : inserted in a namespace, and modified binding function so it can be used in an application using the "_moveTo" function
namespace eval kinetic {
variable param
set param(K) 2.0 ; # friction
set param(VMIN) 50.0 ; # Velocity min - (pixel/sec)
set param(VMAX) 4000.0 ; # Velocity max - (pixel/sec)
}
# return current time in *seconds* as a decimal number
# with the highest precision (microseconds)
proc kinetic::now {} { expr [clock microseconds]/1E6 }
proc kinetic::init {c tag} {
$c bind $tag <ButtonPress-1> {
global MOUSE
set w %W
set MOUSE(X) %x
set MOUSE(Y) %y
set MOUSE(T) [kinetic::now]
set MOUSE(VX) 0.0
set MOUSE(VY) 0.0
$w raise current
}
$c bind $tag <B1-Motion> {
global MOUSE
set w %W
set x %x
set y %y
set t [kinetic::now]
set dx [expr {$x-$MOUSE(X)}]
set dy [expr {$y-$MOUSE(Y)}]
set dt [expr {$t-$MOUSE(T)}]
set MOUSE(X) $x
set MOUSE(Y) $y
set MOUSE(T) $t
set MOUSE(VX) [expr double($dx)/$dt]
set MOUSE(VY) [expr double($dy)/$dt]
set ::V [expr {hypot($MOUSE(VX),$MOUSE(VY))}] ; # just a probe for display
# $w move current $dx $dy
}
# when you release mouse button, target should keep on moving,
# with an initial speed depending on the 'last' speed and the time spent
# from the last movement
$c bind $tag <ButtonRelease-1> {
global MOUSE
upvar #0 kinetic::param ANIM
set w %W
set dt [expr {[kinetic::now]-$MOUSE(T)}]
set DECEL [expr {exp(-$ANIM(K)*$dt)}]
set vx [expr {$MOUSE(VX) * $DECEL}]
set vy [expr {$MOUSE(VY) * $DECEL}]
# if speed magnitude is too low, it's useless to continue !
set vLen [expr {hypot($vx,$vy)}]
set ::V $vLen ; # just a probe for display
if { $vLen < $ANIM(VMIN) } return
# if speed is too high, limit it
if { $vLen > $ANIM(VMAX) } {
# reduce vLen to ANIM(VMAX)
set a [expr {$vLen / $ANIM(VMAX)}]
set vx [expr {$vx/$a}]
set vy [expr {$vy/$a}]
set vLen [expr double($ANIM(VMAX))]
}
# since speed zero won't be never reached,
# compute time to reach speed VMIN
set a [expr {$ANIM(VMIN) / $vLen}]
set duration [expr {log(1/$a)/$ANIM(K)}]
# then compute the position (displacement) we will reach in $duration
set DX [expr {$vx/$ANIM(K)*( 1-$a )}]
set DY [expr {$vy/$ANIM(K)*( 1-$a )}]
set id [$w find withtag current]
lassign [$w coords $id] X0 Y0
# move object from (X0,Y0) to (X0+DX,Y0+DY) within time-interval (now, now+duration)
kinetic::_keepMoving [list $w $id] $X0 $Y0 $DX $DY [kinetic::now] $duration
}
# when you click, stop all moving objects
bind $c <ButtonPress-1> "+ kinetic::but1"
}
proc kinetic::but1 {} {
upvar #0 ::kinetic::param ANIM
foreach {key val} [array get ANIM ID,*] {
after cancel $val
array unset ANIM $key
}
}
proc kinetic::_keepMoving {objID X0 Y0 DX DY T0 DT} {
variable param
lassign $objID w id
set t [expr ([kinetic::now]-$T0)/$DT]
if { $t > 1.0 } { set t 1.0 }
# ---- easing transformation -----
# set tt [expr {1-exp(-$ANIM(K)*$t)}]
set tt [expr {1-exp(-$param(K)*$t*$DT)}]
# - - - - - -
set px [expr {$X0+$tt*$DX}]
set py [expr {$Y0+$tt*$DY}]
_moveTo $w $id $px $py
if { $t < 1.0 } {
set param(ID,$objID) [after 40 [list kinetic::_keepMoving $objID $X0 $Y0 $DX $DY $T0 $DT]]
} else {
# end of animation, unset unused memory
array unset param ID,$objID
}
}
proc kinetic::_moveTo {w id x y} {
lassign [$w coords $id] x0 y0
# $w move $id [expr $x-$x0] [expr $y-$y0]
# move script here ...
}