Parabolic motion for Time Machine (translate + zoom environment)

When moving between two far-away, zoomed-in locations, zoom out first to a "context point" from which both the source and destination points can be seen.

``````

In [6]:

from scipy import *
from pylab import *
from pandas import *
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = [10,8]

``````
``````

In [128]:

def interpolate(a, b, frac):
return (b - a) * frac + a

class Point3:
"""3D point"""
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z

@staticmethod
def interpolate(a, b, frac):
return Point3(interpolate(a.x, b.x, frac),
interpolate(a.y, b.y, frac),
interpolate(a.z, b.z, frac))

def subtract(self, rhs):
return Point3(self.x - rhs.x,
self.y - rhs.y,
self.z - rhs.z)

def length(self):
return sqrt(self.x * self.x + self.y * self.y + self.z * self.z)

def eval(self, u):
return Point3(self.x.eval(u), self.y.eval(u), self.z.eval(u))

def __repr__(self):
return "(%g,%g,%g)" % (self.x, self.y, self.z)

class Line2:
"""y = ax + b"""
def __init__(self, a, b):
self.a = a
self.b = b

@staticmethod
def from_intercepts(x1, y1, x2, y2):
# y1 = a x1 + b
# y2 = a x2 + b
# y1 - a x1 = y2 - a x2
# a x1 - a x2 = y1 - y2
# a (x1 - x2) = y1 - y2
# a = (y1 - y2) / (x1 - x2)
a = (y1 - y2) / float(x1 - x2)
b = y1 - a * x1
return Line2(a, b)

def eval(self, x):
return self.a * x + self.b

def __repr__(self):
return "(%gx+%g)" % (self.a, self.b)

class Parabola2:
# Simplified parabola y = ax^2 + c
# Vertex is at (0, c)
# Intersects (+-1, c + a)

def __init__(self, a, c):
self.a = a
self.c = c

def eval(self, x):
return self.a * x ** 2 + self.c

def __repr__(self):
return "(%g(x^2)+%g)" % (self.a, self.c)

# Compute "zoom length" of segment from p1 to p1 + frac * p2 - p1
# Zoom length is path length scaled inversely with z,
# since apparent motion on screen also varies inversely with z

def fractionalZoomLength(p1, p2, frac):
length = p1.subtract(p2).length()
dz = p2.z - p1.z
if fabs(dz) < 1e-10:
return frac * length / p1.z;
else:
return length / dz * (log(dz * frac + p1.z) - log(p1.z))

# Compute "zoom length" of segment from p1 to p2.
# Zoom length is path length scaled inversely with z,
# since apparent motion on screen also varies inversely with z

def zoomLength(p1, p2):
return fractionalZoomLength(p1, p2, 1)

# Compute point that's dist zoom distance from p1 along the path to p2.

def zoomInterpolate(p1, p2, dist):
length = p1.subtract(p2).length()
dz = p2.z - p1.z
if fabs(dz) < 1e-10:
frac = dist / length * p1.z
else:
frac = (exp(dz * dist / length + log(p1.z)) - p1.z) / dz
return Point3.interpolate(p1, p2, frac)

def testZoomInterpolate(p1, p2, dist):
interp = zoomInterpolate(p1, p2, dist)
print "Requested dist %g actual %g" % (dist, zoomLength(p1, interp))

``````
``````

In [129]:

testZoomInterpolate(Point3(0,0,2), Point3(2,2,2.0001), 1)

``````
``````

Requested dist 1 actual 1

``````
``````

In [130]:

testZoomInterpolate(Point3(0,0,2), Point3(1,1,5), 1.2)

``````
``````

Requested dist 1.2 actual 1.2

``````
``````

In [131]:

testZoomInterpolate(Point3(1,1,5), Point3(0,0,2), .3)

``````
``````

Requested dist 0.3 actual 0.3

``````
``````

In [132]:

testZoomInterpolate(Point3(5,6,7), Point3(10,20,30), 1)

``````
``````

Requested dist 1 actual 1

``````
``````

In [299]:

def computePath(a, b):
"""Compute path from a to b and return as a list of points"""

# First, compute a "context point" from which both a and b are visible
xydist = sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
zdist = fabs(a.z - b.z)
ctx_height = 0.5 * (xydist - zdist)

if ctx_height > 0:
# Zoom out from a to the context point (parabola vertex) and then zoom in to b
ctx_z = ctx_height + max(a.z, b.z)
ctx = Point3.interpolate(a, b, (ctx_z - a.z) / xydist)
ctx.z = ctx_z
p1 = Point3(Line2.from_intercepts(-1, a.x, 0, ctx.x), # (-1, a.x) - (0, ctx.x)
Line2.from_intercepts(-1, a.y, 0, ctx.y), # (-1, a.y) - (0, ctx.y)
Parabola2(a.z - ctx.z, ctx.z)) # (-1, a.z) - (0, ctx.z)
p2 = Point3(Line2.from_intercepts(0, ctx.x, 1, b.x), # (0, ctx.x) - (1, b.x)
Line2.from_intercepts(0, ctx.y, 1, b.y), # (0, ctx.y) - (1, b.y)
Parabola2(b.z - ctx.z, ctx.z)) # (0, ctx.z) - (1, b.z)
return [(p1 if u < 0 else p2).eval(u) for u in linspace(-1, 1, 21)]
elif 2 * xydist > zdist + 1e-10:
# No context point, but can follow parabolic zoom in or out without vertex
if a.z < b.z:
# This code only works for zooming in;  convert zooming out to zooming in
return list(reversed(computePath(b, a)))
c = (xydist ** 2) / (2.0 * xydist - zdist)
p = Point3(Line2.from_intercepts(c - xydist, a.x, c, b.x),
Line2.from_intercepts(c - xydist, a.y, c, b.y),
Parabola2(-1.0 / c, c + b.z))
return [p.eval(u) for u in linspace(c - xydist, c, 11)]
else:
# Zoom is much larger than translation;  go in a straight line
return [a, b]

``````

Translation > zoom; use 2-phase parabolic path with "context point" at vertex

``````

In [300]:

# Zooming out
path = computePath(Point3(2, 1, 1), Point3(6, 2, 2))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])

``````
``````

Out[300]:

[<matplotlib.lines.Line2D at 0x10e090450>]

``````
``````

In [301]:

# Zooming in
path = computePath(Point3(2, 1, 2), Point3(6, 2, 1))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])

``````
``````

Out[301]:

[<matplotlib.lines.Line2D at 0x10e057c10>]

``````

Zoom > translation; no context point, but can follow parabolic zoom in or out without vertex

``````

In [302]:

# Zoom in
path = computePath(Point3(2, 1, 14.7), Point3(3, 2, 13))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])

``````
``````

Out[302]:

[<matplotlib.lines.Line2D at 0x10e799b90>]

``````
``````

In [303]:

# Zoom out
path = computePath(Point3(2, 1, 13), Point3(3, 2, 14.7))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])

``````
``````

Out[303]:

[<matplotlib.lines.Line2D at 0x10e7aa9d0>]

``````

Zoom >> translation; go in a straight line

``````

In [304]:

path = computePath(Point3(2, 1, 20), Point3(6, 2, 2))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])

``````
``````

Out[304]:

[<matplotlib.lines.Line2D at 0x10e7c6f50>]

``````

Translation == 0; go in a straight line

``````

In [305]:

path = computePath(Point3(2, 1, 20), Point3(2, 1, 2))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])
path

``````
``````

Out[305]:

[(2,1,20), (2,1,2)]

``````

Edge case between vertex and no vertex

``````

In [306]:

path = computePath(Point3(0, 0, 14), Point3(0, 1, 13))
plot([pt.x for pt in path], [pt.z for pt in path])
plot([pt.y for pt in path], [pt.z for pt in path])

``````
``````

Out[306]:

[<matplotlib.lines.Line2D at 0x10f1a1390>]

``````
``````

In [306]:

``````