orbix.viz.anim
==============

.. py:module:: orbix.viz.anim

.. autoapi-nested-parse::

   Orbit animation: a ghost of the full path, a growing trail, a moving head.

   Built on ``eyepiece.animate`` in update mode: the figure is drawn once
   (the full track as a faint ghost, via the static plot functions, so an
   animated figure's ghost matches its still counterpart's path), the animated
   artists are created empty, and each frame is a ``set_data`` -- nothing is
   cleared and no artist is created inside the frame loop.

   The ``history`` vocabulary carries over from the key-strategy idea in
   earlier orbit-animation code: ``"all"`` accumulates the trail from the
   first epoch, an int keeps a trailing window of that many frames, and
   ``"none"`` moves the head marker alone.



Functions
---------

.. autoapisummary::

   orbix.viz.anim._history_slice
   orbix.viz.anim._track_alphas
   orbix.viz.anim._auto_camera
   orbix.viz.anim.depth_size
   orbix.viz.anim.depth_scale
   orbix.viz.anim.animate_orbit
   orbix.viz.anim._single


Module Contents
---------------

.. py:function:: _history_slice(i, history)

   The [start, stop) trail slice for frame ``i`` under ``history``.


.. py:function:: _track_alphas(n_tracks, weights)

   Per-track alpha for the animated artists: opaque, faded by weight.


.. py:function:: _auto_camera(tracks, n_frames, roll_deg, tilt_deg=55.0, sweep_deg=40.0)

   A sweep that turns the orbit without changing how big it is drawn.

   Sweeping raw azimuth from wherever the axes happened to be pointing
   changes the projected area of a fixed orbit -- from matplotlib's default
   camera this orbit's drawn area swings by a factor of six, which a reader
   reads as a change in the orbit. It is the same failure as an axis that
   rescales between frames, with a rotation matrix in front of it.

   A camera on a cone about the orbit normal cannot do that: the projected
   area of a planar ellipse is ``pi * a * b * cos(tilt)``, so holding the
   tilt fixed holds the drawn size fixed exactly, and the sweep turns the
   ellipse in the page plane instead of inflating it. The parallax that
   makes the geometry legible is unchanged.

   :param tracks: Positions, shape ``(K, T, 3)``, in the plot's own units.
   :param n_frames: Number of frames to generate angles for.
   :param roll_deg: Camera roll, held fixed across the sweep.
   :param tilt_deg: Angle between the camera and the orbit normal. Well away
                    from 0 (face-on, where the depth cue flattens to nothing) and
                    from 90 (edge-on, where the orbit collapses to a line).
   :param sweep_deg: How far around the cone to travel.

   :returns: Dict with ``"azim"``, ``"elev"`` and ``"roll"`` arrays.


.. py:function:: depth_size(depth)

   Map a [0, 1] depth factor onto a marker-diameter multiplier.

   The tuning is inherited from the original hand-tuned orbit renders:
   the marker's scatter AREA grew additively by at most half its base
   on the near side, which in diameter terms is ``sqrt(1 + 0.5 * d)``
   -- a swell that peaks at about 22 percent. The far side is the
   anchor at exactly the base size, so the resting size stays free to
   encode physical meaning such as a planet radius; depth reads as a
   brief near-side swell, not a shrink.

   Callers styling a scatter artist (whose ``s`` is an area) should
   square this factor to stay in area units.


.. py:function:: depth_scale(positions, azim_deg, elev_deg)

   Per-point head-marker scale in [0, 1] from the camera geometry.

   The same viewer-angle cue ``eyepiece.trail`` bakes into its per-point
   markers -- ``(1 + cos(angle)) / 2`` between each position vector and
   the position-to-viewer vector, with the viewer far along the camera
   direction -- but here it drives the one moving head marker instead of
   beads along the whole path: in an animation the head can carry the
   depth cue itself, so the path stays a clean line.


.. py:function:: animate_orbit(orbit_or_tracks, t_jd=None, *, Ms_kg=None, dist_pc=None, trig_solver=None, kind='sky', history='all', rotate='auto', fps=10, style=None, base_ms=6.0, weights=None, data=None, iwa=None, marks=None)

   Animate one or more orbits and return a lazy ``eyepiece.Animation``.

   Nothing renders until ``.save``, ``.jshtml``, or ``.video`` is called
   on the result, so one animation can go to several sinks in one pass
   and the test path never needs ffmpeg.

   :param orbit_or_tracks: An ``AbstractOrbit``, or bare tracks in the same
                           forms the static functions accept (``(ra, dec)`` arrays or
                           ``(2, T)`` / ``(K, 2, T)`` for ``kind="sky"``; ``(T, 3)`` /
                           ``(K, T, 3)`` for ``kind="3d"``).
   :param t_jd: Times in Julian Days, shape ``(T,)``. Required on the orbit
                door; on the bare-track door it is optional and used only for
                the elapsed-time label (length must match the track).
   :param Ms_kg: Stellar mass in kg. Orbit door only.
   :param dist_pc: Distance in parsecs. Orbit door with ``kind="sky"`` only.
   :param trig_solver: Optional Kepler solver; None uses orbix's default.
   :param kind: ``"sky"`` (sky-plane arcsec, via ``plot_sky_track``) or
                ``"3d"`` (star-centric AU, via ``plot_orbit``).
   :param history: ``"all"`` grows the trail from the first epoch, an int
                   keeps a trailing window of that many frames, ``"none"`` moves
                   the head marker alone.
   :param rotate: Camera sweep, ``kind="3d"`` only. The default ``"auto"``
                  travels 40 degrees around a cone about the ORBIT NORMAL, at a
                  fixed tilt to it, so azimuth and elevation both vary. That is
                  deliberate: holding the tilt fixed holds the projected area
                  ``pi * a * b * cos(tilt)`` fixed, where a plain azimuth sweep
                  about z at held elevation changes the tilt and so swings the
                  drawn size -- by 5.7x on the three-planet system in the docs,
                  which a reader misreads as the orbit growing. ``None`` holds
                  the camera still. A dict maps any of ``"azim"``, ``"elev"``, ``"roll"``
                  to a ``(start_deg, stop_deg)`` pair interpolated linearly
                  across the frames, for full control. The head's depth cue
                  tracks the moving camera frame by frame. Ignored for
                  ``kind="sky"`` unless a dict is passed, which raises.
   :param fps: Default playback rate carried by the returned animation.
   :param style: A color or ``SourceStyles`` entry for the tracks, forwarded
                 to the static function and used for the animated artists. For
                 ``kind="3d"``, None gives the star-chart default: heads in the
                 mode's text color (white dots on a dark background) over
                 transparent dashed gray trails; a ``style`` opts into that
                 source's solid color instead.
   :param base_ms: Head-marker diameter in points, a scalar or one value
                   per track. The base size is the anchor the depth cue swells
                   around, so it is where physical meaning lives -- pass
                   ``size_by_radius(radii)`` to encode planet radii.
   :param weights: Optional per-track weights (length K), fading both the
                   ghost fan and the animated trails.
   :param data: Optional ``(ra, dec, err)`` observed epochs (sky only).
   :param iwa: Optional inner-working-angle disk radius (sky only).
   :param marks: Optional ``{"periapsis", "nodes"}`` (3d orbit door only).

   :returns: An ``eyepiece.Animation`` bound to the built figure, with one
             frame per epoch of the track.

   :raises ValueError: If ``kind`` or ``history`` is not one of the
       documented values, or if a labeled ``t_jd`` does not match the
       track length.


.. py:function:: _single(orbit_or_tracks)

   Whether the input is a single track (K == 1), cheaply and safely.

   Only tunes the ghost's alpha, so an exotic ``AbstractOrbit`` without
   element arrays to inspect just keeps the fan default.


