Morphing through the shapes zoo

Open in Colab  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)

Gallery generated by Sphinx-Gallery