diff --git a/docs/cli.rst b/docs/cli.rst index b786a28d..44352de2 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -49,8 +49,8 @@ Examples $ data-morph --start-shape music --target-shape bullseye --output-dir path/to/dir -7. Morph the sheep shape into vertical lines, slowly ramping in and out for the animation: +7. Morph the sheep shape into vertical lines, slowly easing in and out for the animation: .. code-block:: console - $ data-morph --start-shape sheep --target-shape v_lines --ramp-in --ramp-out + $ data-morph --start-shape sheep --target-shape v_lines --ease diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 1f1e0ae8..2c11b57c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -49,12 +49,13 @@ within your current working directory: Morphing the panda :class:`.Dataset` into the star :class:`.Shape`. -You can smooth the transition with the ``--ramp-in`` and ``--ramp-out`` flags. The ``--freeze`` -flag allows you to start the animation with the specified number of frames of the initial shape: +You can smooth the transition with the ``--ease`` or ``--ease-in`` and ``--ease-out`` flags. +The ``--freeze`` flag allows you to start the animation with the specified number of frames +of the initial shape: .. code:: console - $ data-morph --start-shape panda --target-shape star --freeze 50 --ramp-in --ramp-out + $ data-morph --start-shape panda --target-shape star --freeze 50 --ease Here is the resulting animation: @@ -115,8 +116,8 @@ With the :class:`.Dataset` and :class:`.Shape` created, here is a minimal exampl start_shape=dataset, target_shape=target_shape, freeze_for=50, - ramp_in=True, - ramp_out=True, + ease_in=True, + ease_out=True, ) .. note:: diff --git a/src/data_morph/cli.py b/src/data_morph/cli.py index 1477fa6b..8d6894a5 100644 --- a/src/data_morph/cli.py +++ b/src/data_morph/cli.py @@ -56,7 +56,9 @@ def generate_parser() -> argparse.ArgumentParser: description='Specify the start and target shapes.', ) shape_config_group.add_argument( + '--start', '--start-shape', + dest='start_shape', required=True, nargs='+', help=( @@ -67,7 +69,9 @@ def generate_parser() -> argparse.ArgumentParser: ), ) shape_config_group.add_argument( + '--target', '--target-shape', + dest='target_shape', required=True, nargs='+', help=( @@ -149,6 +153,7 @@ def generate_parser() -> argparse.ArgumentParser: ), ) file_group.add_argument( + '-o', '--output-dir', default=ARG_DEFAULTS['output_dir'], metavar='DIRECTORY', @@ -168,29 +173,17 @@ def generate_parser() -> argparse.ArgumentParser: 'Animation Configuration', description='Customize aspects of the animation.' ) frame_group.add_argument( - '--forward-only', + '--ease', default=False, action='store_true', help=( - 'By default, this module will create an animation that plays ' - 'first forward (applying the transformation) and then unwinds, ' - 'playing backward to undo the transformation. Pass this argument ' - 'to only play the animation in the forward direction before looping.' - ), - ) - frame_group.add_argument( - '--freeze', - default=ARG_DEFAULTS['freeze'], - type=int, - metavar='NUM_FRAMES', - help=( - 'Number of frames to freeze at the first and final frame of the transition ' - 'in the animation. This only affects the frames selected, not the algorithm. ' - f'Defaults to {ARG_DEFAULTS["freeze"]}.' + 'Whether to slow down the transition near the start and end of the ' + 'transformation. This is a shortcut for --ease-in --ease-out. This only ' + 'affects the frames selected, not the algorithm.' ), ) frame_group.add_argument( - '--ramp-in', + '--ease-in', default=False, action='store_true', help=( @@ -199,7 +192,7 @@ def generate_parser() -> argparse.ArgumentParser: ), ) frame_group.add_argument( - '--ramp-out', + '--ease-out', default=False, action='store_true', help=( @@ -207,6 +200,28 @@ def generate_parser() -> argparse.ArgumentParser: 'of the animation. This only affects the frames selected, not the algorithm.' ), ) + frame_group.add_argument( + '--forward-only', + default=False, + action='store_true', + help=( + 'By default, this module will create an animation that plays ' + 'first forward (applying the transformation) and then rewinds, ' + 'playing backward to undo the transformation. Pass this argument ' + 'to only play the animation in the forward direction before looping.' + ), + ) + frame_group.add_argument( + '--freeze', + default=ARG_DEFAULTS['freeze'], + type=int, + metavar='NUM_FRAMES', + help=( + 'Number of frames to freeze at the first and final frame of the transition ' + 'in the animation. This only affects the frames selected, not the algorithm. ' + f'Defaults to {ARG_DEFAULTS["freeze"]}.' + ), + ) return parser @@ -260,7 +275,7 @@ def main(argv: Sequence[str] | None = None) -> None: target_shape=shape_factory.generate_shape(target_shape), iterations=args.iterations, min_shake=args.shake, - ramp_in=args.ramp_in, - ramp_out=args.ramp_out, + ease_in=args.ease_in or args.ease, + ease_out=args.ease_out or args.ease, freeze_for=args.freeze, ) diff --git a/src/data_morph/morpher.py b/src/data_morph/morpher.py index 5efd1993..58606366 100644 --- a/src/data_morph/morpher.py +++ b/src/data_morph/morpher.py @@ -124,7 +124,7 @@ def __init__( self._looper = tqdm.tnrange if in_notebook else tqdm.trange def _select_frames( - self, iterations: int, ramp_in: bool, ramp_out: bool, freeze_for: int + self, iterations: int, ease_in: bool, ease_out: bool, freeze_for: int ) -> list: """ Identify the frames to capture for the animation. @@ -133,9 +133,9 @@ def _select_frames( ---------- iterations : int The number of iterations. - ramp_in : bool + ease_in : bool Whether to more slowly transition in the beginning. - ramp_out : bool + ease_out : bool Whether to slow down the transition at the end. freeze_for : int The number of frames to freeze at the beginning and end. Must be in the @@ -166,11 +166,11 @@ def _select_frames( # freeze initial frame frames = [0] * freeze_for - if ramp_in and not ramp_out: + if ease_in and not ease_out: easing_function = ease_in_sine - elif ramp_out and not ramp_in: + elif ease_out and not ease_in: easing_function = ease_out_sine - elif ramp_out and ramp_in: + elif ease_out and ease_in: easing_function = ease_in_out_sine else: easing_function = linear @@ -346,8 +346,8 @@ def morph( min_shake: Number = 0.3, max_shake: Number = 1, allowed_dist: Number = 2, - ramp_in: bool = False, - ramp_out: bool = False, + ease_in: bool = False, + ease_out: bool = False, freeze_for: int = 0, ) -> pd.DataFrame: """ @@ -376,10 +376,10 @@ def morph( at ``max_shake`` and move toward ``min_shake``. allowed_dist : numbers.Number The farthest apart the perturbed points can be from the target shape. - ramp_in : bool, default ``False`` + ease_in : bool, default ``False`` Whether to more slowly transition in the beginning. This only affects the frames, not the algorithm. - ramp_out : bool, default ``False`` + ease_out : bool, default ``False`` Whether to slow down the transition at the end. This only affects the frames, not the algorithm. freeze_for : int, default ``0`` @@ -440,8 +440,8 @@ def morph( # iteration numbers that we will end up writing to file as frames frame_numbers = self._select_frames( iterations=iterations, - ramp_in=ramp_in, - ramp_out=ramp_out, + ease_in=ease_in, + ease_out=ease_out, freeze_for=freeze_for, ) diff --git a/tests/test_cli.py b/tests/test_cli.py index c236b32d..8549fc31 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -82,7 +82,7 @@ def test_cli_bad_input_integers(field, value, capsys): @pytest.mark.input_validation @pytest.mark.parametrize('value', [1, 0, 's', -1, 0.5, True, False]) @pytest.mark.parametrize( - 'field', ['ramp-in', 'ramp-out', 'forward-only', 'keep-frames'] + 'field', ['ease-in', 'ease-out', 'forward-only', 'keep-frames'] ) def test_cli_bad_input_boolean(field, value, capsys): """Test that invalid input for Boolean switches are handled correctly.""" @@ -133,8 +133,9 @@ def test_cli_one_shape(start_shape, flag, mocker, tmp_path): 'min_shake': 0.5 if flag else None, 'iterations': 1000, 'freeze': 3 if flag else None, - 'ramp_in': flag, - 'ramp_out': flag, + 'ease_in': flag, + 'ease_out': flag, + 'ease': not flag, } morpher_init = mocker.patch.object(cli.DataMorpher, '__init__', autospec=True) @@ -153,8 +154,9 @@ def test_cli_one_shape(start_shape, flag, mocker, tmp_path): '--forward-only' if init_args['forward_only_animation'] else '', f'--shake={morph_args["min_shake"]}' if morph_args['min_shake'] else '', f'--freeze={morph_args["freeze"]}' if morph_args['freeze'] else '', - '--ramp-in' if morph_args['ramp_in'] else '', - '--ramp-out' if morph_args['ramp_out'] else '', + '--ease-in' if morph_args['ease_in'] else '', + '--ease-out' if morph_args['ease_out'] else '', + '--ease' if morph_args['ease'] else '', ] cli.main([arg for arg in argv if arg]) @@ -171,6 +173,8 @@ def test_cli_one_shape(start_shape, flag, mocker, tmp_path): elif arg == 'start_shape': assert isinstance(value, Dataset) assert value.name == Path(morph_args['start_shape_name']).stem + elif morph_args['ease'] and arg.startswith('ease_'): + assert value elif arg in ['freeze_for', 'min_shake']: arg = 'freeze' if arg == 'freeze_for' else arg assert value == (morph_args[arg] or cli.ARG_DEFAULTS[arg]) diff --git a/tests/test_morpher.py b/tests/test_morpher.py index 1eaeef9a..b6e3446b 100644 --- a/tests/test_morpher.py +++ b/tests/test_morpher.py @@ -70,7 +70,7 @@ def test_input_validation_freeze_for(self, freeze_for): ValueError, match='freeze_for must be a non-negative integer' ): _ = morpher._select_frames( - iterations=100, ramp_in=True, ramp_out=True, freeze_for=freeze_for + iterations=100, ease_in=True, ease_out=True, freeze_for=freeze_for ) @pytest.mark.input_validation @@ -81,11 +81,11 @@ def test_input_validation_iterations(self, iterations): with pytest.raises(ValueError, match='iterations must be a positive integer'): _ = morpher._select_frames( - iterations=iterations, ramp_in=True, ramp_out=True, freeze_for=0 + iterations=iterations, ease_in=True, ease_out=True, freeze_for=0 ) @pytest.mark.parametrize( - ('ramp_in', 'ramp_out', 'expected_frames'), + ('ease_in', 'ease_out', 'expected_frames'), [ (True, True, [0, 1, 2, 5, 8, 12, 15, 18, 19]), (True, False, [0, 0, 1, 3, 5, 7, 10, 13, 17]), @@ -93,7 +93,7 @@ def test_input_validation_iterations(self, iterations): (False, False, [0, 2, 4, 7, 9, 11, 13, 16, 18]), ], ) - def test_frames(self, ramp_in, ramp_out, expected_frames): + def test_frames(self, ease_in, ease_out, expected_frames): """Confirm that frames produced by the _select_frames() method are correct.""" freeze_for = 2 iterations = 20 @@ -103,8 +103,8 @@ def test_frames(self, ramp_in, ramp_out, expected_frames): ) frames = morpher._select_frames( iterations=iterations, - ramp_in=ramp_in, - ramp_out=ramp_out, + ease_in=ease_in, + ease_out=ease_out, freeze_for=freeze_for, ) @@ -166,8 +166,8 @@ def test_no_writing(self, capsys): start_shape=dataset, target_shape=shape_factory.generate_shape(target_shape), iterations=iterations, - ramp_in=False, - ramp_out=False, + ease_in=False, + ease_out=False, freeze_for=0, ) @@ -205,8 +205,8 @@ def test_saving_data(self, tmp_path): start_shape=dataset, target_shape=shape_factory.generate_shape(target_shape), iterations=iterations, - ramp_in=False, - ramp_out=False, + ease_in=False, + ease_out=False, freeze_for=0, )