neu_glance.movie.animate

Keyframe animation over neuroglancer states — a timeline, and the interpolation under it.

A Timeline wraps one base state (a link tuned in the browser) and a list of tweens (Tween), each moving one property over a window of time with an easing. Anything no tween mentions keeps its base value, so the base can be edited without touching the tweens.

Pure: plain dicts in and out, stdlib only, no neuroglancer. neu_glance.movie.rendering is the half that needs a viewer.

neuroglancer’s own ViewerState.interpolate is not used: in 2.41.2 it raises for any state with a layer (it reads a nonexistent layer_position), it covers few layer properties, and it adds orientations a state did not have. Its zoom and quaternion formulas are ported here (interpolate_zoom(), interpolate_quaternion()), and a test checks them against neuroglancer’s.

Attributes

Exceptions

AnimateProblem

A timeline could not be built, or would not animate what it claims to.

Classes

Timeline

A base state plus tweens over it. Every frame it emits is a plain state dict.

Tween

One property, moving from one value to another over one window of time.

Functions

axis_quaternion(axis, degrees) → list[float]

A rotation of degrees about axis as a unit quaternion, xyzw.

ease(name, t) → float

EASINGS[name] applied to t, clamped to [0, 1].

interpolate_color_map(a, b, t, *[, default]) → dict

segmentColors, interpolated per segment id.

interpolate_hex_color(a, b, t, *[, space]) → str

A colour ramp, in linear light by default. Returns #rrggbb.

interpolate_linear(a, b, t) → Any

Straight lerp, element-wise for equal-length sequences.

interpolate_orbit(a, b, t) → list[float]

Rotate a progressively about an axis. b is {"axis", "degrees", "screen"}.

interpolate_quaternion(a, b, t) → list[float]

Spherical linear interpolation. Ported from viewer_state.py:75-104 (via gl-matrix).

interpolate_step(a, b, t) → Any

Hold a, take b at the end of the transition.

interpolate_zoom(a, b, t) → Any

Exponential interpolation, for the two zoom scales. Ported from viewer_state.py:107.

interpolate_zoom_by(a, b, t) → Any

Scale a by a factor, geometrically. b is the factor at the end of the window.

kind_for(prop) → str

The interpolator name for prop. Unknown properties step; see PROPERTY_KINDS.

Module Contents

exception neu_glance.movie.animate.AnimateProblem

Bases: RuntimeError

A timeline could not be built, or would not animate what it claims to.

Initialize self. See help(type(self)) for accurate signature.

class neu_glance.movie.animate.Timeline

A base state plus tweens over it. Every frame it emits is a plain state dict.

Frame i is at i / fps; fps is kept here so a saved timeline fully describes its frames.

at(seconds: float) → dict

The state at seconds. A plain dict; the caller’s base is never mutated.

check() → list[str]

Raise on anything unworkable; return notes worth printing. Called by the renderer.

frame_on(box: Any, *, at: float | None = None, seconds: float, ease: str = DEFAULT_EASE, margin: float = 1.15, units: str = 'nm', zoom: bool = True, min_scale: float | None = None, rotation_safe: bool = False) → Timeline

Pan (and by default zoom) so box fills the view.

box is (lo_zyx, hi_zyx) or anything with .lo / .hi (a neu_lib.BBox), e.g. from neu_glance.sources.segment_boxes().

units="nm" (the default, what segment_boxes returns) is converted to the state’s voxels with its dimensions, since neuroglancer’s position is in voxels; pass units="voxels" for a box already in viewer coordinates. A wrong choice does not raise; it misplaces the camera by the voxel size.

Framing matches neu_glance.state.default_view(), with margin as the room around the box; zoom=False only pans. rotation_safe=True fits the box’s diagonal, for a shot that turns: projectionScale fixes the vertical extent, so a box framed at one angle can overflow at another. min_scale caps how close the camera goes. An orbit pivots about position, which this also centres.

frames(*, start_frame: int = 0, end_frame: int | None = None) → Iterator[tuple[int, float, dict]]

(index, seconds, state) per frame.

Times are index / fps, computed rather than accumulated, so they do not drift.

classmethod from_json(obj: Mapping[str, Any]) → Timeline

The inverse of to_json(), so a rendered timeline can be edited and re-run.

group(name: str, layers: Sequence[str]) → Timeline

Name a set of layers that appear together — a neuron and the neuropils it innervates.

hold(seconds: float) → Timeline

Advance the cursor. The only thing that moves it.

orbit(degrees: float, *, axis: Any = 'up', at: float | None = None, seconds: float, ease: str = DEFAULT_EASE) → Timeline

Rotate the camera degrees about axis. Relative — no keyframe needed.

axis defaults to "up", the screen’s vertical: a turntable from any starting view. A volume axis ("z", or a 3-sequence) is reproducible across scenes. A full turn is orbit(360) (see interpolate_orbit()).

resolved() → list[Tween]

The tweens in application order, each with its start filled in.

On each property, the first tween starts from the base state and each later one from where the previous ended.

sequence(names: Sequence[str], *, start: float | None = None, seconds: float = 1.2, stagger: float | None = None, ease: str = DEFAULT_EASE, fade_from: float = FADE_FROM) → Timeline

Fade in each group in turn, stagger seconds apart. Returns self.

stagger=None means back to back; a shorter stagger overlaps the fades. Each layer also starts at objectAlpha = fade_from from t=0, so the scene starts empty.

set(*, layer: str | None = None, **props: Any) → Timeline

An opening value at t=0, held until a later tween takes the property over.

sets(layer: str, subsets: Mapping[str, Sequence[Any]], *, colors: Mapping[str, str] | None = None, object_alpha: float | None = None) → dict[str, str]

One layer per named SET of segments. Returns {set: layer_name}.

For populations that fade together (neu_glance.subset_layers()); use split() for individual objects.

split(layer: str, *, prefix: str | None = None, names: Mapping[str, str] | None = None, segments: Sequence[Any] | None = None) → dict[str, str]

One layer per segment of layer. Returns {segment_id: layer_name}.

Needed to fade objects individually (neu_glance.split_segment_layer()). Build groups from the returned layer names.

to_json() → dict

The whole timeline, base state included, for writing beside the frames.

tween(*, layer: str | None = None, at: float | None = None, seconds: float, ease: str = DEFAULT_EASE, start: Any = None, kind: str | None = None, **props: Any) → Timeline

Move each named property to its given value over seconds, starting at at.

at=None uses the cursor, which only hold() moves, so consecutive calls run at the same time unless the cursor is advanced.

start=None takes the value the property last held: an earlier tween’s end, else the base state, else LAYER_DEFAULTS / VIEW_DEFAULTS (not zero).

view(target: Mapping[str, Any] | str, *, at: float | None = None, seconds: float, ease: str = DEFAULT_EASE) → Timeline

Move the camera to the view held by another state. Returns self.

target is a state dict or a neuroglancer URL: fly the camera in the browser, copy the link, paste it here. Only CAMERA_KEYS are taken; the target’s layers and layout are ignored.

voxel_size_nm() → tuple[float, float, float]

The state’s own voxel size in nm, zyx, from its dimensions.

zoom(factor: float, *, at: float | None = None, seconds: float, ease: str = DEFAULT_EASE) → Timeline

Scale the 3D zoom by factor. Relative: 0.5 is twice as close.

Needs no knowledge of the current projectionScale.

base: dict
cursor: float = 0.0
property duration: float

Seconds. The furthest tween end, or the cursor, whichever is later.

fps: float = 30.0
property frame_count: int

Frames, including the last one — a 2 s timeline at 30 fps is 61 frames.

groups: dict[str, tuple[str, ...]]
notes: list[str] = []
tweens: list[Tween] = []
class neu_glance.movie.animate.Tween

One property, moving from one value to another over one window of time.

classmethod from_json(obj: Mapping[str, Any]) → Tween
to_json() → dict
value_at(t: float, start: Any) → Any

The value at t, or _NOTHING before the tween begins (so the earlier value shows). After the window, the end value holds.

at: float = 0.0
ease: str = 'in-out'
end: Any
kind: str | None = None
path: tuple[str, ...]
seconds: float = 0.0
start: Any = None
property stop: float
neu_glance.movie.animate.axis_quaternion(axis: Any, degrees: float) → list[float]

A rotation of degrees about axis as a unit quaternion, xyzw.

neu_glance.movie.animate.ease(name: str, t: float) → float

EASINGS[name] applied to t, clamped to [0, 1].

neu_glance.movie.animate.interpolate_color_map(a: Any, b: Any, t: float, *, default: str | None = None) → dict

segmentColors, interpolated per segment id.

A key on both sides ramps. A key on one side only ramps to or from default (the layer’s segmentDefaultColor) if given, and otherwise steps at the end of the window.

neu_glance.movie.animate.interpolate_hex_color(a: Any, b: Any, t: float, *, space: str = 'linear') → str

A colour ramp, in linear light by default. Returns #rrggbb.

Endpoints are anything neu_glance.shaders.as_hex_color() accepts. Hex channels are gamma-encoded, so averaging them directly makes black-to-red pass through a dark maroon; mixing in linear light gives a midpoint that looks halfway. space="srgb" mixes the encoded values, to match ramps made elsewhere.

neu_glance.movie.animate.interpolate_linear(a: Any, b: Any, t: float) → Any

Straight lerp, element-wise for equal-length sequences.

Sequences of different lengths hold a, as neuroglancer does.

neu_glance.movie.animate.interpolate_orbit(a: Any, b: Any, t: float) → list[float]

Rotate a progressively about an axis. b is {"axis", "degrees", "screen"}.

Slerp between two views always takes the shortest arc, so a full turn would be a no-op and 200 degrees would become 160 the other way. Here the angle varies, so any rotation works, including several turns. screen reads the axis in the camera’s frame (post-multiply) or, if false, the volume’s (pre-multiply).

neu_glance.movie.animate.interpolate_quaternion(a: Any, b: Any, t: float) → list[float]

Spherical linear interpolation. Ported from viewer_state.py:75-104 (via gl-matrix).

q and -q are the same rotation, so b is negated when needed to take the short way round (otherwise two views 20 degrees apart can turn 340 degrees). Inputs are assumed to be unit quaternions, as neuroglancer writes them.

neu_glance.movie.animate.interpolate_step(a: Any, b: Any, t: float) → Any

Hold a, take b at the end of the transition.

Not at the midpoint, where a flip mid-move looks like an artefact. For a change at another time, use a zero-length tween there.

neu_glance.movie.animate.interpolate_zoom(a: Any, b: Any, t: float) → Any

Exponential interpolation, for the two zoom scales. Ported from viewer_state.py:107.

Zoom is perceived multiplicatively, so a linear zoom visibly accelerates towards the close end. A non-positive endpoint falls back to a linear lerp rather than raising mid-render.

neu_glance.movie.animate.interpolate_zoom_by(a: Any, b: Any, t: float) → Any

Scale a by a factor, geometrically. b is the factor at the end of the window.

Relative and exponential, like interpolate_zoom(): a factor of 0.5 is twice as close, from wherever the camera is.

neu_glance.movie.animate.kind_for(prop: str) → str

The interpolator name for prop. Unknown properties step; see PROPERTY_KINDS.

neu_glance.movie.animate.AXES: dict[str, tuple[float, float, float]]
neu_glance.movie.animate.CAMERA_KEYS = ('position', 'projectionOrientation', 'projectionScale', 'projectionDepth',...
neu_glance.movie.animate.DEFAULT_EASE = 'in-out'
neu_glance.movie.animate.EASINGS: dict[str, Callable[[float], float]]
neu_glance.movie.animate.FADE_FROM = 0.001
neu_glance.movie.animate.INTERPOLATORS: dict[str, Callable[[Any, Any, float], Any]]
neu_glance.movie.animate.LAYER_DEFAULTS: dict[str, Any]
neu_glance.movie.animate.PROPERTY_KINDS: dict[str, str]
neu_glance.movie.animate.SCREEN_AXES
neu_glance.movie.animate.VIEW_DEFAULTS: dict[str, Any]