Interpolator — wiki
-- posted 23 April 2007 by Duoas
Description
A simple class that produces vectors interpolating between two given vectors in a given amount of time. The vectors may be any dimention, so long as they can be parsed as a sequence. The interpolation defaults to a linear interpolation, but two parameters are provided to modify it into various smooth interpolations.
Background
I had been playing around with the LinearInterpolator and SmoothInterpolator functions here in the cookbook, and after fixing a couple (serious) bugs, I made some other heavy modifications to suit my needs. Finally, I decided to post my updates.
I don't have Numeric, and as it is getting rather old anyway I have puttered along without it. You might find some speed improvements by changing the list comprehensions into Numeric.arrays. The following apply:
- self.diff = Numeric.array( stop, Numeric.float ) -Numeric.array( start, Numeric.float )
- self.step = Numeric.array( self.diff[:] ) *self.inc /seconds
- self.maxs ...
- self.mins ...
Well, you get the idea.
interpolator.py
This is the module. You can run it through pydoc to get nice HTML documentation.
R"""
interpolator.py
Perform a simple interpolation between two points of any dimention, without
the use of Numeric.
2007 Michael Thomas Greer
Released to the Public Domain
"""
class Interpolator( object ):
R"""
The line actor. Returns successive points along a line until completely
traversed. Once traversed this class can do nothing more.
Care has been taken to fix errors due to floating-point arithmetic.
implementation notes
Following the docs, some simple math must be done to get the movement
desired. The first thing to remember is that we cannot change the FPS,
which is to say, we cannot change the speed of the program. So all our
manipulations work by modifying the step size between the start and
stop vectors (which, in other words, is the apparent speed of line
traversal).
For the default linear interpolation, the step size remains constant:
step = dx *( 1.0 /FPS ) /seconds
where dx represents the total distance to traverse for each dimention.
For non-linear interpolations, we must factor in our shape, which is
specified relative to the linear interpolation speed. The shape is a
simple power function which gives us a nice, spikey curve with a
vertical asymptote at the midpoint. The function is:
factor = shape *(closeness_to_asymptote **(shape -1.0))
where closeness_to_asymptote is a number in the range [0.0, 1.0]; 0.0
being at either end of the line and 1.0 being at the asymptote. (The
location of the asymptote, or "middle", is modifiable.)
The final calculation is to modify each vector position by (step *factor).
"""
#---------------------------------------------------------------------------
def __init__(
self,
start = None,
stop = None,
seconds = None,
fps = None,
shape = 1.0,
middle = 0.5
):
R"""
Create a new interpolator to produce timed vectors along a line.
arguments
start - The initial vector. If no vectors are specified the object is
treated as a placeholder object and does absolutely nothing
but return the two-dimentional vector (0, 0).
stop - The final vector. If no final vector is specified the object
is treated as a placeholder object and does absolutely nothing
but return the 'start' vector.
seconds - The number of seconds you wish the interpolation to take.
fps - The current number of frames per second. If you specify
seconds you must specify the FPS, otherwise a ValueError
exception is raised with the message "You must specify both
'seconds' and 'fps'."
shape - Modify the interpolation as non-linear.
First some quick information to explain:
- The total time to traverse the line is constant (in other
words, this function cannot take more or less 'seconds'
worth of vectors than you specify).
- For the same reason, the number of vectors produced by this
function (for a given value of 'seconds') is constant.
- Hence, the -speed- of a vector is defined wholly by the
distance between it the previously produced vector.
- In a linear interpolation, the distance between vectors (or
again, the speed of each vector) is constant; every vector
has the same speed.
- In a shaped interpolation, the speed of individual vectors
is modified: some are faster and some are slower.
The shape is the number of times greater than linear speed the
middle vector travels.
A shape of 1.0 is a linear interpolation. A shape of 2.0 has
slower vectors at the end and a vector in the middle traveling
twice as fast as it would in a linear interpolation. A shape
of 0.5 travels half as fast. There really isn't any upper-
limit, but zero is the lower. You'll get a ValueError if you
try any value less-than or equal-to zero.
middle - The location of the "middle vector" along the line, expressed
as a value from 0.0 (at 'start') to 1.0 (at 'stop').
"""
self._sec = -1
self._length = 0
if start is None: start = (0, 0)
if stop is None: self.stop = start
else:
if (seconds is None) or (fps is None):
raise ValueError( "You must specify both 'seconds' and 'fps'" )
if shape <= 0.0:
raise ValueError( "The 'shape' argument must have value > 0.0" )
if not (0.0 <= middle <= 1.0):
raise ValueError( "The 'middle' argument must be in range [0.0, 1.0]" )
self.stop = stop
self.diff = [ b -a for a, b in zip( start, stop ) ]
self.inc = 1.0 / fps
self.step = [ a *self.inc /seconds for a in self.diff ]
self._pos = start
self._sec = seconds
self.seconds = seconds
self.shape = shape
self.mid = middle
self.maxs = [ max( a, b ) for a, b in zip( start, stop ) ]
self.mins = [ min( a, b ) for a, b in zip( start, stop ) ]
self._length = None
#---------------------------------------------------------------------------
def next( self ):
R"""
Calculate the location of the next vector in the line.
The 'start' vector cannot be a "next" vector. (This is actually rather
convenient if you think about it.) That said, if your interpolation is set
up right, the first "next" vector might actually be in the same location
as the 'start' vector...
Care is taken that the 'stop' vector is always the final vector.
returns
The next vector or None if all done.
"""
def d( a, b, c ):
if b == 0.0: return c
else: return a /b
if self._sec >= 0.0:
if self.shape == 1.0: factor = 1.0
else:
percent = 1.0 -(self._sec /self.seconds) # percent complete
if percent < 0.95:
if percent > self.mid: k = d( (1.0 -percent), (1.0 -self.mid), 1.0 )
else: k = d( percent, self.mid, 0.0 )
if k in [0.0, 1.0]: factor = k *self.shape
else: factor = pow( k, self.shape -1.0 ) *self.shape
else:
# The final 5% of the line is calculated linearly to avoid any
# 'jump' or 'snap' artifacts caused by FPU arithmetic errors.
if self.mid is not None:
self.diff = [ b -a for a, b in zip( self._pos, self.stop ) ]
self.step = [ a *self.inc /self._sec for a in self.diff ]
self.mid = None
factor = 1.0
self._pos = tuple(
[ min( max(
a +(step *factor),
mina ), maxa )
for a, step, mina, maxa in
zip( self._pos, self.step, self.mins, self.maxs )
]
)
self._sec -= self.inc
return self._pos
else:
self._pos = self.stop
return None
#---------------------------------------------------------------------------
def _get_pos( self ):
R"""Return the current vector's location."""
return self._pos
#---------------------------------------------------------------------------
def _get_length( self ):
R"""
Returns the length of the line. The line's length is not calculated
until first required.
"""
from math import sqrt
if self._length is None:
sum = 0
for a in self.diff: sum += a *a
self._length = sqrt( sum )
return self._length
#---------------------------------------------------------------------------
pos = property( _get_pos, doc='The location of the current vector. Read-only.' )
length = property( _get_length, doc='The length of the line. Read-only.' )
# end interpolator
test.py
Here is a simple test program you can use to play around with it.
Click anywhere on the display to cause the colored squares to re-center themselves around the spot where you clicked, using a one-and-a-half-second interpolation.
- The blue square is interpolated linearly.
- The orange square has a smooth shape of 2.0.
- The purple square also has a smooth shape of 2.0, but the fastest part is at the end instead of the middle of the line.
- The red square has a smooth shape of 4.0, and the fastest part is at the beginning instead of the middle of the line.
- The yellow square has a smooth shape of 0.5, meaning that it is half-speed in the middle and fast at the ends.
Enjoy playing with it!
import pygame
from interpolator import *
RESOLUTION = (800, 600) # (adjust for your resolution)
SECONDS = 1.5 # slow enough to see how it goes, but not too slow...
class Sprite( pygame.sprite.Sprite ):
def __init__( self, color, pos ):
pygame.sprite.Sprite.__init__( self )
self.image = pygame.Surface( (30, 30) )
self.image.fill( color )
self.rect = self.image.get_rect()
self.rect.center = pos
self.line = Interpolator( pos )
def update( self, screen ):
screen.fill( (0, 0, 0), self.rect )
self.line.next()
self.rect.center = self.line.pos
screen.blit( self.image, self.rect )
def main():
pygame.init()
screen = pygame.display.set_mode( RESOLUTION )
x, y = RESOLUTION
x //= 2
y //= 2
shapes = [1.0, 2.0, 2.0, 4.0, 0.5]
middles = [0.5, 0.5, 1.0, 0.0, 0.5]
xoffsets = [0, -30, 30, -30, 30]
yoffsets = [0, -30, -30, 30, 30]
colors = [
( 52, 98, 166),
(244, 127, 48),
(176, 84, 175),
(229, 35, 58),
(255, 255, 162)
]
all = pygame.sprite.OrderedUpdates()
for xoffset, yoffset, color in zip( xoffsets, yoffsets, colors ):
all.add( Sprite( color, (x +xoffset, y +yoffset) ) )
clock = pygame.time.Clock()
all.update( screen )
pygame.display.update()
while True:
for event in pygame.event.get():
if event.type in [pygame.QUIT, pygame.KEYDOWN]:
return
elif event.type == pygame.MOUSEBUTTONDOWN:
fps = clock.get_fps()
x, y = event.pos
for sprite, shape, middle, xoffset, yoffset in zip(
all.sprites(), shapes, middles, xoffsets, yoffsets
):
sprite.line = Interpolator(
sprite.rect.center,
(x +xoffset, y +yoffset),
SECONDS,
fps,
shape,
middle
)
all.update( screen )
pygame.display.update()
clock.tick( 200 )
main()