Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synced slider with frame index rather than active keyframe #99

Merged
merged 5 commits into from
Jul 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions napari_animation/_qt/animation_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def _add_callbacks(self):
keyframe_list.selection.events.active.connect(
self._on_active_keyframe_changed
)
self.animation._frames.events._current_index.connect(
self._on_frame_index_changed
)

def _input_state(self):
"""Get current state of input widgets as {key->value} parameters."""
Expand Down Expand Up @@ -126,27 +129,23 @@ def _on_keyframes_changed(self, event=None):
self.keyframesListWidget.setEnabled(has_frames)
self.frameWidget.setEnabled(has_frames)

def _on_active_keyframe_changed(self, event=None):
n_frames = len(self.animation._frames)
active_keyframe = event.value

if active_keyframe and n_frames:
self.animationSlider.blockSignals(True)
kf1_list = [
self.animation._frames._frame_index[n][0]
for n in range(n_frames)
]
frame_index = kf1_list.index(active_keyframe)
self.animationSlider.setValue(frame_index)
self.animationSlider.blockSignals(False)
def _on_frame_index_changed(self, event=None):
"""Callback on change of last set frame index."""
frame_index = event.value
self.animationSlider.blockSignals(True)
self.animationSlider.setValue(frame_index)
self.animationSlider.blockSignals(False)

def _on_active_keyframe_changed(self, event):
"""Callback on change of active keyframe in the key frames list."""
active_keyframe = event.value
self.keyframesListControlWidget.deleteButton.setEnabled(
bool(active_keyframe)
)

def _on_slider_moved(self, event=None):
frame_index = event
if frame_index < len(self.animation._frames) - 1:
if frame_index < len(self.animation._frames):
with self.animation.key_frames.selection.events.active.blocker():
self.animation.set_movie_frame_index(frame_index)

Expand Down
4 changes: 2 additions & 2 deletions napari_animation/_tests/test_frame_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ def test_frame_seq_caching(frame_sequence: FrameSequence):
) as mock:
frame_5 = fs[5]

# it should have been called once, and a single frame cached
# it should have been called once, and a 2 frames cached (the initial one too)
mock.assert_called_once()
assert isinstance(frame_5, ViewerState)
assert len(fs._cache) == 1
assert len(fs._cache) == 2

# indexing the same frame again will not require re-interpolation
with patch.object(
Expand Down
35 changes: 26 additions & 9 deletions napari_animation/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path

import imageio
import numpy as np
from napari.utils.io import imsave

from .easing import Easing
Expand Down Expand Up @@ -97,20 +98,26 @@ def _validate_animation(self):
)

def set_key_frame_index(self, index: int):
self.key_frames.selection.active = self.key_frames[index]
frame_index = self._keyframe_frame_index(index)
self.set_movie_frame_index(frame_index)

def set_movie_frame_index(self, index: int):
"""Set state to a specific frame in the final movie."""
try:
self._frames[index].apply(self.viewer)
except KeyError:
return
if index < 0:
index += len(self._frames)

if index < 0:
index += len(self._frames)
key_frame = self._frames._frame_index[index][0]

key_frame = self._frames._frame_index[index][0]
self.key_frames.selection.active = key_frame
# to prevent active callback again
if self.key_frames.selection.active != key_frame:
self.key_frames.selection.active = key_frame

self._frames.set_movie_frame_index(self.viewer, index)
self._current_frame = index

except KeyError:
return

def animate(
self,
Expand Down Expand Up @@ -208,6 +215,15 @@ def animate(
if not save_as_folder:
writer.close()

def _keyframe_frame_index(self, keyframe_index):
"""Gets the frame index of the keyframe corresponding to keyframe_index."""
# Get all steps leading to keyframe.
steps_to_keyframe = [
kf.steps for kf in self.key_frames[1 : keyframe_index + 1]
]
Comment on lines +221 to +223
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is looking much nicer! 🙂

frame_index = np.sum(steps_to_keyframe) if steps_to_keyframe else 0
return int(frame_index)

def _on_keyframe_removed(self, event):
self.key_frames.selection.active = None

Expand All @@ -217,4 +233,5 @@ def _on_keyframe_changed(self, event):
def _on_active_keyframe_changed(self, event):
active_keyframe = event.value
if active_keyframe:
active_keyframe.viewer_state.apply(self.viewer)
keyframe_index = self.key_frames.index(active_keyframe)
self.set_key_frame_index(keyframe_index)
40 changes: 32 additions & 8 deletions napari_animation/frame_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ def __init__(self, key_frames: KeyFrameList) -> None:
key_frames.events.changed.connect(self._rebuild_frame_index)
key_frames.events.reordered.connect(self._rebuild_frame_index)

self.events = EmitterGroup(source=self, n_frames=None)
self.__current_index = 0
self.events = EmitterGroup(
source=self, n_frames=None, _current_index=None
)

self.state_interpolation_map: InterpolationMap = {
"camera.angles": Interpolation.SLERP,
Expand All @@ -57,16 +60,23 @@ def _rebuild_frame_index(self, event=None):
"""Create a map of frame number -> (kf0, kf1, fraction)"""
self._frame_index.clear()
self._cache.clear()
if len(self._key_frames) < 2:

n_keyframes = len(self._key_frames)

if n_keyframes == 0:
self.events.n_frames(value=len(self))
return
elif n_keyframes == 1:
f = 0
kf1 = self._key_frames[0]
else:
f = 0
for kf0, kf1 in pairwise(self._key_frames):
for s in range(kf1.steps):
fraction = kf1.ease(s / kf1.steps)
self._frame_index[f] = (kf0, kf1, fraction)
f += 1

f = 0
for kf0, kf1 in pairwise(self._key_frames):
for s in range(kf1.steps):
fraction = kf1.ease(s / kf1.steps)
self._frame_index[f] = (kf0, kf1, fraction)
f += 1
self._frame_index[f] = (kf1, kf1, 0)
self.events.n_frames(value=len(self))

Expand Down Expand Up @@ -160,3 +170,17 @@ def iter_frames(
frame = ndi.zoom(frame, (scale_factor, scale_factor, 1))
frame = frame.astype(np.uint8)
yield frame

def set_movie_frame_index(self, viewer: napari.viewer.Viewer, index: int):
self[index].apply(viewer)
self._current_index = index

@property
def _current_index(self):
return self.__current_index

@_current_index.setter
def _current_index(self, frame_index):
if frame_index != self._frame_index:
self.__current_index = frame_index
self.events._current_index(value=frame_index)
Comment on lines +178 to +186
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like having this directly on the FrameSequence