Note
Go to the end to download the full example code.
Morphing through the shapes zoo¶
run this example as a notebook — or grab the .ipynb (also linked at the bottom of the page)
HyperTools’ “shapes zoo” (bunny, cube, dragon, sphere, teapot, vase,
biplane – see the A zoo of 3D shapes example) can be morphed smoothly
from one point cloud to the next with the animate='morph' hyp.plot
style (PR #272, maintainer request 2026-07-06 – see the animate/
rotations/morph_samples entries of the hyp.plot docstring for the
full spec). Under the hood, every shape keeps its own full point count by
default (smaller shapes are padded up to the largest shape’s count by
duplicating random points of their own) – but the zoo’s shapes range up
to ~90,000 points (the biplane), and the Hungarian assignment used to
match consecutive shapes point-for-point is roughly O(n^3), so
morph_samples=1000 below first downsamples every shape to a tractable
1000 points (build-time: the full, uncapped 90k-point target would make
the matching step infeasible for a gallery build). Consecutive shapes are
matched point-for-point with the Hungarian algorithm
(scipy.optimize.linear_sum_assignment) so that each point travels the
shortest total distance to its partner in the next shape, and the
coordinates are eased between shapes frame by frame while the camera
spins around the scene – exactly the hand-rolled recipe this example
used to implement itself before animate=’morph’ existed, now built into
the library behind a single hyp.plot call. rotations also
accepts a per-segment list for finer camera control: below, holds spin a
slow, easy-to-watch full rotation while each transition only spins a
brisk quarter-turn, so the camera visibly “steps” forward every time one
shape morphs into the next. Camera speed (degrees/frame) is always
CONSTANT across the whole animation – a segment’s rotations entry sets
how much SCREEN TIME it gets, not how fast it spins, so the full-rotation
holds below play roughly 4x longer than the quarter-turn transitions
(1 / 0.25), never faster.
/home/docs/checkouts/readthedocs.org/user_builds/hypertools/checkouts/latest/hypertools/plot/backend.py:1353: UserWarning: Failed to switch to any interactive backend (TkAgg, QtAgg, Qt5Agg, Qt4Agg, GTK4Agg, GTK3Agg, WXAgg). Falling back to 'Agg'.
warnings.warn(BACKEND_WARNING)
# Code source: Contextual Dynamics Laboratory
# License: MIT
import numpy as np
import hypertools as hyp
# every point cloud in the shapes zoo, visited in this order
shapes = ['bunny', 'cube', 'dragon', 'sphere', 'teapot', 'vase', 'biplane']
def normalize_shape(points):
"""Center and scale a point cloud into the hypertools [-1, 1] cube."""
points = np.asarray(points, dtype=np.float64)
points = points - points.mean(axis=0)
return points / np.abs(points).max()
clouds = [normalize_shape(hyp.load(shape)) for shape in shapes]
# frame schedule: hold, morph, hold, morph, ..., hold -- 2 * n_shapes - 1 =
# 13 segments in all. `rotations` gives each segment its OWN camera-spin
# count: holds get a slow, easy-to-watch full rotation (1) while
# transitions get a brisk quarter-turn (0.25), so the camera visibly steps
# forward every time a shape morphs into the next one. Camera speed stays
# CONSTANT (degrees/frame) across the whole animation, so each segment's
# SCREEN TIME is now proportional to its own rotation count -- the 7
# full-rotation holds get ~46 frames each and the 6 quarter-turn
# transitions get ~11-12 frames each (390 frames total @ 30 fps, ~13 sec,
# same total length as an equal-time split would have given).
rotations = [1, 0.25] * (len(shapes) - 1) + [1]
# morph_samples=1000: caps every shape at 1000 points (build tractability --
# see module docstring) before the Hungarian matching step.
fig, ani = hyp.plot(clouds, fmt='.', color='k', markersize=1.5,
animate='morph', rotations=rotations,
morph_samples=1000,
duration=len(rotations), frame_rate=30)
Total running time of the script: (0 minutes 14.223 seconds)