Skip to content

octarine.Animation#

octarine.Animation #

A timeline of camera moves that can be played or rendered to a video.

Segments are appended one after the other; each starts where the previous one ended, so a timeline reads much like the animation itself:

anim = oc.Animation(v) # doctest: +SKIP anim.orbit(duration=6) # one turn around the scene anim.move_to("XZ", duration=2) # swing round to the lateral view anim.hold(1) # ... and stay there for a second anim.render("tour.mp4")

PARAMETER DESCRIPTION
viewer
    Viewer to animate.

TYPE: Viewer

fps
    Frames per second used when rendering (and the rate the
    preview aims for, capped by ``viewer.max_fps``).

TYPE: int DEFAULT: 30

Source code in octarine/anim_utils.py
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
class Animation:
    """A timeline of camera moves that can be played or rendered to a video.

    Segments are appended one after the other; each starts where the previous
    one ended, so a timeline reads much like the animation itself:

    >>> anim = oc.Animation(v)                              # doctest: +SKIP
    >>> anim.orbit(duration=6)          # one turn around the scene
    >>> anim.move_to("XZ", duration=2)  # swing round to the lateral view
    >>> anim.hold(1)                    # ... and stay there for a second
    >>> anim.render("tour.mp4")

    Parameters
    ----------
    viewer :    Viewer
                Viewer to animate.
    fps :       int
                Frames per second used when rendering (and the rate the
                preview aims for, capped by ``viewer.max_fps``).

    """

    def __init__(self, viewer, fps=30):
        self.viewer = viewer
        self.fps = int(fps)
        self.camera = CameraTrack()
        #: All tracks on this animation. Camera-only for now, but a track
        #: animating e.g. object visibility would simply be added here.
        self.tracks = [self.camera]

        self._start_state = None  # where the timeline picks up from
        self._playback = None  # the function registered with the viewer

    def __repr__(self):
        return (
            f"<Animation {self.duration:.2f}s @ {self.fps} fps, "
            f"{len(self.camera)} camera segment(s)>"
        )

    @property
    def duration(self):
        """Length of the animation in seconds."""
        return max((track.duration for track in self.tracks), default=0.0)

    @property
    def n_frames(self):
        """Number of frames this animation renders to."""
        return frame_count(self.duration, self.fps)

    @property
    def current_state(self):
        """Camera state the next segment will start from."""
        state = self.camera.end_state
        if state is not None:
            return state
        if self._start_state is None:
            self._start_state = self.viewer.get_view()
        return self._start_state

    def clear(self):
        """Remove all segments."""
        for track in self.tracks:
            track.clear()
        self._start_state = None
        return self

    def start_at(self, view=None):
        """Set the view the timeline starts from, without animating to it.

        Parameters
        ----------
        view :  str | dict, optional
                A view as accepted by `Viewer.set_view` (e.g. ``"XY"``) or a
                camera state. Defaults to the viewer's current view.

        """
        if self.camera.segments:
            raise ValueError(
                "`start_at` only works on an empty timeline - use `move_to` to "
                "jump to a view mid-animation."
            )
        self._start_state = self._as_state(view)
        return self

    def hold(self, duration):
        """Stay on the current view for `duration` seconds."""
        self.camera.append(Hold(self.current_state, duration))
        return self

    def orbit(
        self,
        objects=None,
        *,
        turns=1,
        duration=6,
        axis="up",
        recenter=True,
        scale=1.0,
        easing="linear",
    ):
        """Circle the camera around object(s).

        Parameters
        ----------
        objects :   str | list of str | visual(s), optional
                    What to orbit around, given as object ID(s) as shown in the
                    legend (or the visuals themselves). Defaults to everything
                    on the viewer.
        turns :     float
                    Number of turns. Negative values orbit the other way.
        duration :  float
                    Seconds the orbit takes.
        axis :      "up" | "x" | "y" | "z" | "view" | (3, ) vector
                    Axis to rotate about. The default, ``"up"``, is the scene's
                    up direction (the camera's ``reference_up``) and gives a
                    turntable rotation from whatever view you start at. Prefix
                    with ``-`` to flip it.
        recenter :  bool
                    If True (default), frame `objects` before orbiting: the
                    pivot is the centre of their bounding box and the camera is
                    moved to fit them in view. If False, keep the current view
                    and orbit around whatever the camera is already looking at
                    - use this to animate exactly what you see on screen.
        scale :     float
                    Only with ``recenter=True``: how much of the viewport the
                    objects fill. Values > 1 zoom out.
        easing :    str | callable
                    See `octarine.anim_utils.EASINGS`. Note that anything but
                    ``"linear"`` will make a full turn start and/or end slowly.

        """
        state = self.current_state
        if recenter:
            pivot, radius = _bounding_sphere(self.viewer, objects)
            state = _framed_state(self.viewer, pivot, radius, scale=scale)
        elif objects is not None:
            pivot, _ = _bounding_sphere(self.viewer, objects)
        else:
            pivot = _pivot(state)

        self.camera.append(
            Orbit(state, pivot, axis, turns=turns, duration=duration, easing=easing)
        )
        return self

    def move_to(self, view=None, *, duration=2, easing="in_out", path="arc"):
        """Move the camera to another view.

        Parameters
        ----------
        view :      str | dict, optional
                    Target view - anything `Viewer.set_view` takes (``"XY"``,
                    ``"-XZ"``, ...) or a camera state as returned by
                    `Viewer.get_view`. Defaults to the viewer's current view,
                    which is what makes "fly to where I am looking now" work
                    from the GUI.
        duration :  float
                    Seconds the move takes.
        easing :    str | callable
                    Defaults to ``"in_out"`` so the camera accelerates out of
                    and decelerates into the move.
        path :      "arc" | "linear"
                    ``"arc"`` swings around the point being looked at,
                    ``"linear"`` moves the camera in a straight line.

        """
        self.camera.append(
            Transition(
                self.current_state,
                self._as_state(view),
                duration,
                easing=easing,
                path=path,
            )
        )
        return self

    def zoom(self, factor=2, *, duration=2, easing="in_out"):
        """Zoom in (`factor` > 1) or out (`factor` < 1) on the current view."""
        if factor <= 0:
            raise ValueError(f"Zoom factor must be > 0, got {factor}")
        state = _copy_state(self.current_state)
        pivot = _pivot(state)
        for key in ("width", "height", "depth"):
            state[key] = state[key] / factor
        state["position"] = _position_from_pivot(state, pivot)
        self.camera.append(
            Transition(self.current_state, state, duration, easing=easing)
        )
        return self

    def _as_state(self, view):
        """Resolve a view specifier into a camera state."""
        if isinstance(view, dict):
            return view
        # `None` gives the current view, a name (e.g. "XY") what that view
        # would be - neither moves the camera
        return self.viewer.get_view(view)

    def set_time(self, t):
        """Apply the animation's state at time `t` (in seconds)."""
        for track in self.tracks:
            track.apply(self.viewer, t)
        return self

    def times(self, fps=None):
        """Times (in seconds) of the frames this animation renders to.

        The last frame stops one frame short of the end so that a full turn
        loops seamlessly.

        """
        fps = int(fps or self.fps)
        return [i / fps for i in range(frame_count(self.duration, fps))]

    def play(self, loop=False, speed=1.0):
        """Play the animation in the viewer.

        Playback is driven by the viewer's animation loop and follows the wall
        clock, so it runs at the same speed no matter the frame rate (see
        `Viewer.max_fps` if it looks choppy).

        Parameters
        ----------
        loop :  bool
                Whether to start over when the end is reached.
        speed : float
                Playback speed multiplier.

        """
        if not self.duration:
            raise ValueError("Nothing to play - this animation is empty.")

        self.stop()
        start = time.perf_counter()

        def _tick():
            t = (time.perf_counter() - start) * speed
            if t >= self.duration:
                if loop:
                    t = t % self.duration
                else:
                    self.set_time(self.duration)
                    self.stop()
                    return
            self.set_time(t)

        self._playback = _tick
        self.viewer.add_animation(_tick, on_error="log")
        return self

    def stop(self):
        """Stop playback (see `Animation.play`)."""
        if self._playback is not None:
            self.viewer.remove_animation(self._playback)
            self._playback = None
        return self

    @property
    def playing(self):
        """Whether the animation is currently being played in the viewer."""
        return self._playback is not None

    def recorder(self, filename=None, **kwargs):
        """Return a `Recorder` that renders this animation frame by frame.

        Use this instead of `Animation.render` when you need to keep control
        between frames - the controls panel drives one of these off a timer to
        keep the GUI responsive while recording.

        """
        return Recorder(self, filename, **kwargs)

    def render(
        self,
        filename=None,
        *,
        fps=None,
        size=None,
        pixel_ratio=None,
        alpha=False,
        supersample=1,
        restore=True,
        progress=True,
    ):
        """Render the animation.

        Parameters
        ----------
        filename :  str | pathlib.Path, optional
                    Where to write to. The format follows the extension:

                     - ``.mp4`` (and other video formats) is written frame by
                       frame and requires ``imageio`` + ``imageio-ffmpeg``
                     - ``.gif`` requires ``imageio``
                     - a path without an extension is treated as a directory
                       and gets a numbered PNG per frame - no extra
                       dependencies, and you can encode it yourself

                    If ``None``, the frames are returned as a list of numpy
                    arrays instead.
        fps :       int, optional
                    Overrides the animation's own frame rate.
        size :      (width, height), optional
                    Size of the video. Defaults to the current canvas size.
                    Note that the canvas is resized for the recording.
        pixel_ratio : float, optional
                    Factor by which to scale the canvas size, see
                    `Viewer.screenshot`. You probably want ``1`` when passing
                    an explicit `size`.
        alpha :     bool
                    Whether to keep the background transparent. Only useful for
                    PNG sequences - videos are written without an alpha channel
                    either way.
        supersample : int
                    Anti-aliasing quality, see `Viewer.screenshot`. The cost is
                    paid per frame, hence the conservative default.
        restore :   bool
                    Whether to put the camera back where it was afterwards.
        progress :  bool | callable
                    Show a progress bar (requires ``tqdm``), or a callable
                    ``f(frame, n_frames)`` called after every frame.

        Returns
        -------
        list of numpy arrays
                    If `filename` is ``None``.
        pathlib.Path
                    The file (or directory) written to, otherwise.

        """
        rec = self.recorder(
            filename,
            fps=fps,
            size=size,
            pixel_ratio=pixel_ratio,
            alpha=alpha,
            supersample=supersample,
            restore=restore,
        )

        callback = _progress_callback(progress, rec.n_frames)
        try:
            while rec.step():
                if callback:
                    callback(rec.frame, rec.n_frames)
        except BaseException:
            rec.cancel()
            raise
        finally:
            if callback:
                callback.close()
        return rec.finish()

current_state property #

Camera state the next segment will start from.

duration property #

Length of the animation in seconds.

n_frames property #

Number of frames this animation renders to.

playing property #

Whether the animation is currently being played in the viewer.

clear() #

Remove all segments.

Source code in octarine/anim_utils.py
514
515
516
517
518
519
def clear(self):
    """Remove all segments."""
    for track in self.tracks:
        track.clear()
    self._start_state = None
    return self

hold(duration) #

Stay on the current view for duration seconds.

Source code in octarine/anim_utils.py
539
540
541
542
def hold(self, duration):
    """Stay on the current view for `duration` seconds."""
    self.camera.append(Hold(self.current_state, duration))
    return self

move_to(view=None, *, duration=2, easing='in_out', path='arc') #

Move the camera to another view.

PARAMETER DESCRIPTION
view
    Target view - anything `Viewer.set_view` takes (``"XY"``,
    ``"-XZ"``, ...) or a camera state as returned by
    `Viewer.get_view`. Defaults to the viewer's current view,
    which is what makes "fly to where I am looking now" work
    from the GUI.

TYPE: str | dict DEFAULT: None

duration
    Seconds the move takes.

TYPE: float DEFAULT: 2

easing
    Defaults to ``"in_out"`` so the camera accelerates out of
    and decelerates into the move.

TYPE: str | callable DEFAULT: 'in_out'

path
    ``"arc"`` swings around the point being looked at,
    ``"linear"`` moves the camera in a straight line.

TYPE: "arc" | "linear" DEFAULT: 'arc'

Source code in octarine/anim_utils.py
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
def move_to(self, view=None, *, duration=2, easing="in_out", path="arc"):
    """Move the camera to another view.

    Parameters
    ----------
    view :      str | dict, optional
                Target view - anything `Viewer.set_view` takes (``"XY"``,
                ``"-XZ"``, ...) or a camera state as returned by
                `Viewer.get_view`. Defaults to the viewer's current view,
                which is what makes "fly to where I am looking now" work
                from the GUI.
    duration :  float
                Seconds the move takes.
    easing :    str | callable
                Defaults to ``"in_out"`` so the camera accelerates out of
                and decelerates into the move.
    path :      "arc" | "linear"
                ``"arc"`` swings around the point being looked at,
                ``"linear"`` moves the camera in a straight line.

    """
    self.camera.append(
        Transition(
            self.current_state,
            self._as_state(view),
            duration,
            easing=easing,
            path=path,
        )
    )
    return self

orbit(objects=None, *, turns=1, duration=6, axis='up', recenter=True, scale=1.0, easing='linear') #

Circle the camera around object(s).

PARAMETER DESCRIPTION
objects
    What to orbit around, given as object ID(s) as shown in the
    legend (or the visuals themselves). Defaults to everything
    on the viewer.

TYPE: str | list of str | visual(s) DEFAULT: None

turns
    Number of turns. Negative values orbit the other way.

TYPE: float DEFAULT: 1

duration
    Seconds the orbit takes.

TYPE: float DEFAULT: 6

axis
    Axis to rotate about. The default, ``"up"``, is the scene's
    up direction (the camera's ``reference_up``) and gives a
    turntable rotation from whatever view you start at. Prefix
    with ``-`` to flip it.

TYPE: "up" | "x" | "y" | "z" | "view" | (3, ) vector DEFAULT: 'up'

recenter
    If True (default), frame `objects` before orbiting: the
    pivot is the centre of their bounding box and the camera is
    moved to fit them in view. If False, keep the current view
    and orbit around whatever the camera is already looking at
    - use this to animate exactly what you see on screen.

TYPE: bool DEFAULT: True

scale
    Only with ``recenter=True``: how much of the viewport the
    objects fill. Values > 1 zoom out.

TYPE: float DEFAULT: 1.0

easing
    See `octarine.anim_utils.EASINGS`. Note that anything but
    ``"linear"`` will make a full turn start and/or end slowly.

TYPE: str | callable DEFAULT: 'linear'

Source code in octarine/anim_utils.py
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
def orbit(
    self,
    objects=None,
    *,
    turns=1,
    duration=6,
    axis="up",
    recenter=True,
    scale=1.0,
    easing="linear",
):
    """Circle the camera around object(s).

    Parameters
    ----------
    objects :   str | list of str | visual(s), optional
                What to orbit around, given as object ID(s) as shown in the
                legend (or the visuals themselves). Defaults to everything
                on the viewer.
    turns :     float
                Number of turns. Negative values orbit the other way.
    duration :  float
                Seconds the orbit takes.
    axis :      "up" | "x" | "y" | "z" | "view" | (3, ) vector
                Axis to rotate about. The default, ``"up"``, is the scene's
                up direction (the camera's ``reference_up``) and gives a
                turntable rotation from whatever view you start at. Prefix
                with ``-`` to flip it.
    recenter :  bool
                If True (default), frame `objects` before orbiting: the
                pivot is the centre of their bounding box and the camera is
                moved to fit them in view. If False, keep the current view
                and orbit around whatever the camera is already looking at
                - use this to animate exactly what you see on screen.
    scale :     float
                Only with ``recenter=True``: how much of the viewport the
                objects fill. Values > 1 zoom out.
    easing :    str | callable
                See `octarine.anim_utils.EASINGS`. Note that anything but
                ``"linear"`` will make a full turn start and/or end slowly.

    """
    state = self.current_state
    if recenter:
        pivot, radius = _bounding_sphere(self.viewer, objects)
        state = _framed_state(self.viewer, pivot, radius, scale=scale)
    elif objects is not None:
        pivot, _ = _bounding_sphere(self.viewer, objects)
    else:
        pivot = _pivot(state)

    self.camera.append(
        Orbit(state, pivot, axis, turns=turns, duration=duration, easing=easing)
    )
    return self

play(loop=False, speed=1.0) #

Play the animation in the viewer.

Playback is driven by the viewer's animation loop and follows the wall clock, so it runs at the same speed no matter the frame rate (see Viewer.max_fps if it looks choppy).

PARAMETER DESCRIPTION
loop
Whether to start over when the end is reached.

TYPE: bool DEFAULT: False

speed
Playback speed multiplier.

TYPE: float DEFAULT: 1.0

Source code in octarine/anim_utils.py
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
def play(self, loop=False, speed=1.0):
    """Play the animation in the viewer.

    Playback is driven by the viewer's animation loop and follows the wall
    clock, so it runs at the same speed no matter the frame rate (see
    `Viewer.max_fps` if it looks choppy).

    Parameters
    ----------
    loop :  bool
            Whether to start over when the end is reached.
    speed : float
            Playback speed multiplier.

    """
    if not self.duration:
        raise ValueError("Nothing to play - this animation is empty.")

    self.stop()
    start = time.perf_counter()

    def _tick():
        t = (time.perf_counter() - start) * speed
        if t >= self.duration:
            if loop:
                t = t % self.duration
            else:
                self.set_time(self.duration)
                self.stop()
                return
        self.set_time(t)

    self._playback = _tick
    self.viewer.add_animation(_tick, on_error="log")
    return self

recorder(filename=None, **kwargs) #

Return a Recorder that renders this animation frame by frame.

Use this instead of Animation.render when you need to keep control between frames - the controls panel drives one of these off a timer to keep the GUI responsive while recording.

Source code in octarine/anim_utils.py
718
719
720
721
722
723
724
725
726
def recorder(self, filename=None, **kwargs):
    """Return a `Recorder` that renders this animation frame by frame.

    Use this instead of `Animation.render` when you need to keep control
    between frames - the controls panel drives one of these off a timer to
    keep the GUI responsive while recording.

    """
    return Recorder(self, filename, **kwargs)

render(filename=None, *, fps=None, size=None, pixel_ratio=None, alpha=False, supersample=1, restore=True, progress=True) #

Render the animation.

PARAMETER DESCRIPTION
filename
    Where to write to. The format follows the extension:

     - ``.mp4`` (and other video formats) is written frame by
       frame and requires ``imageio`` + ``imageio-ffmpeg``
     - ``.gif`` requires ``imageio``
     - a path without an extension is treated as a directory
       and gets a numbered PNG per frame - no extra
       dependencies, and you can encode it yourself

    If ``None``, the frames are returned as a list of numpy
    arrays instead.

TYPE: str | pathlib.Path DEFAULT: None

fps
    Overrides the animation's own frame rate.

TYPE: int DEFAULT: None

size
    Size of the video. Defaults to the current canvas size.
    Note that the canvas is resized for the recording.

TYPE: (width, height) DEFAULT: None

pixel_ratio
    Factor by which to scale the canvas size, see
    `Viewer.screenshot`. You probably want ``1`` when passing
    an explicit `size`.

TYPE: float DEFAULT: None

alpha
    Whether to keep the background transparent. Only useful for
    PNG sequences - videos are written without an alpha channel
    either way.

TYPE: bool DEFAULT: False

supersample
    Anti-aliasing quality, see `Viewer.screenshot`. The cost is
    paid per frame, hence the conservative default.

TYPE: int DEFAULT: 1

restore
    Whether to put the camera back where it was afterwards.

TYPE: bool DEFAULT: True

progress
    Show a progress bar (requires ``tqdm``), or a callable
    ``f(frame, n_frames)`` called after every frame.

TYPE: bool | callable DEFAULT: True

RETURNS DESCRIPTION
list of numpy arrays

If filename is None.

pathlib.Path

The file (or directory) written to, otherwise.

Source code in octarine/anim_utils.py
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
def render(
    self,
    filename=None,
    *,
    fps=None,
    size=None,
    pixel_ratio=None,
    alpha=False,
    supersample=1,
    restore=True,
    progress=True,
):
    """Render the animation.

    Parameters
    ----------
    filename :  str | pathlib.Path, optional
                Where to write to. The format follows the extension:

                 - ``.mp4`` (and other video formats) is written frame by
                   frame and requires ``imageio`` + ``imageio-ffmpeg``
                 - ``.gif`` requires ``imageio``
                 - a path without an extension is treated as a directory
                   and gets a numbered PNG per frame - no extra
                   dependencies, and you can encode it yourself

                If ``None``, the frames are returned as a list of numpy
                arrays instead.
    fps :       int, optional
                Overrides the animation's own frame rate.
    size :      (width, height), optional
                Size of the video. Defaults to the current canvas size.
                Note that the canvas is resized for the recording.
    pixel_ratio : float, optional
                Factor by which to scale the canvas size, see
                `Viewer.screenshot`. You probably want ``1`` when passing
                an explicit `size`.
    alpha :     bool
                Whether to keep the background transparent. Only useful for
                PNG sequences - videos are written without an alpha channel
                either way.
    supersample : int
                Anti-aliasing quality, see `Viewer.screenshot`. The cost is
                paid per frame, hence the conservative default.
    restore :   bool
                Whether to put the camera back where it was afterwards.
    progress :  bool | callable
                Show a progress bar (requires ``tqdm``), or a callable
                ``f(frame, n_frames)`` called after every frame.

    Returns
    -------
    list of numpy arrays
                If `filename` is ``None``.
    pathlib.Path
                The file (or directory) written to, otherwise.

    """
    rec = self.recorder(
        filename,
        fps=fps,
        size=size,
        pixel_ratio=pixel_ratio,
        alpha=alpha,
        supersample=supersample,
        restore=restore,
    )

    callback = _progress_callback(progress, rec.n_frames)
    try:
        while rec.step():
            if callback:
                callback(rec.frame, rec.n_frames)
    except BaseException:
        rec.cancel()
        raise
    finally:
        if callback:
            callback.close()
    return rec.finish()

set_time(t) #

Apply the animation's state at time t (in seconds).

Source code in octarine/anim_utils.py
654
655
656
657
658
def set_time(self, t):
    """Apply the animation's state at time `t` (in seconds)."""
    for track in self.tracks:
        track.apply(self.viewer, t)
    return self

start_at(view=None) #

Set the view the timeline starts from, without animating to it.

PARAMETER DESCRIPTION
view
A view as accepted by `Viewer.set_view` (e.g. ``"XY"``) or a
camera state. Defaults to the viewer's current view.

TYPE: str | dict DEFAULT: None

Source code in octarine/anim_utils.py
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
def start_at(self, view=None):
    """Set the view the timeline starts from, without animating to it.

    Parameters
    ----------
    view :  str | dict, optional
            A view as accepted by `Viewer.set_view` (e.g. ``"XY"``) or a
            camera state. Defaults to the viewer's current view.

    """
    if self.camera.segments:
        raise ValueError(
            "`start_at` only works on an empty timeline - use `move_to` to "
            "jump to a view mid-animation."
        )
    self._start_state = self._as_state(view)
    return self

stop() #

Stop playback (see Animation.play).

Source code in octarine/anim_utils.py
706
707
708
709
710
711
def stop(self):
    """Stop playback (see `Animation.play`)."""
    if self._playback is not None:
        self.viewer.remove_animation(self._playback)
        self._playback = None
    return self

times(fps=None) #

Times (in seconds) of the frames this animation renders to.

The last frame stops one frame short of the end so that a full turn loops seamlessly.

Source code in octarine/anim_utils.py
660
661
662
663
664
665
666
667
668
def times(self, fps=None):
    """Times (in seconds) of the frames this animation renders to.

    The last frame stops one frame short of the end so that a full turn
    loops seamlessly.

    """
    fps = int(fps or self.fps)
    return [i / fps for i in range(frame_count(self.duration, fps))]

zoom(factor=2, *, duration=2, easing='in_out') #

Zoom in (factor > 1) or out (factor < 1) on the current view.

Source code in octarine/anim_utils.py
632
633
634
635
636
637
638
639
640
641
642
643
644
def zoom(self, factor=2, *, duration=2, easing="in_out"):
    """Zoom in (`factor` > 1) or out (`factor` < 1) on the current view."""
    if factor <= 0:
        raise ValueError(f"Zoom factor must be > 0, got {factor}")
    state = _copy_state(self.current_state)
    pivot = _pivot(state)
    for key in ("width", "height", "depth"):
        state[key] = state[key] / factor
    state["position"] = _position_from_pivot(state, pivot)
    self.camera.append(
        Transition(self.current_state, state, duration, easing=easing)
    )
    return self

octarine.anim_utils.EASINGS = {'linear': lambda t: t, 'in': lambda t: t * t, 'out': lambda t: 1 - (1 - t) ** 2, 'in_out': _smoothstep, 'smooth': lambda t: _smoothstep(_smoothstep(t))} module-attribute #

octarine.anim_utils.Recorder #

Renders an animation one frame at a time.

Instantiating a recorder starts the recording (it resizes the canvas and opens the output file); each step renders and writes the next frame and returns False once there are none left; finish closes the output and returns it. Animation.render simply runs this to completion - drive it yourself if you need to do something between frames (e.g. keep a GUI responsive).

cancel() #

Abort the recording and discard whatever was written.

finish() #

Close the output and return it (a path, or the list of frames).

step() #

Render and write the next frame.

Returns True if a frame was written and False if there was nothing left to do.

octarine.anim_utils.Segment #

A piece of a track, covering [start, start + duration).

Subclass this to animate something new: implement apply and add the segments to a Track on an Animation. Segments must be able to render any point in their time range without having seen the ones before it.

apply(viewer, t) #

Apply this segment at local time t (seconds from its start).

progress(t) #

Eased progress [0-1] at local time t.

octarine.anim_utils.Track #

An ordered sequence of segments, animating one aspect of the scene.

append(segment) #

Add a segment to the end of this track.

segment_at(t) #

Return (segment, local_time) active at time t, or (None, 0).