# -*- coding: utf-8 -*-
"""
================================
Morphing through the shapes zoo
================================

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.
"""

# 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)
