keyed 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. keyed-0.1.0/LICENSE +28 -0
  2. keyed-0.1.0/PKG-INFO +121 -0
  3. keyed-0.1.0/README.md +46 -0
  4. keyed-0.1.0/pyproject.toml +167 -0
  5. keyed-0.1.0/setup.cfg +4 -0
  6. keyed-0.1.0/src/keyed/__init__.py +38 -0
  7. keyed-0.1.0/src/keyed/animation.py +278 -0
  8. keyed-0.1.0/src/keyed/annotations.py +49 -0
  9. keyed-0.1.0/src/keyed/base.py +266 -0
  10. keyed-0.1.0/src/keyed/cli.py +210 -0
  11. keyed-0.1.0/src/keyed/color.py +270 -0
  12. keyed-0.1.0/src/keyed/compositor/__init__.py +2 -0
  13. keyed-0.1.0/src/keyed/compositor/blend.py +8 -0
  14. keyed-0.1.0/src/keyed/compositor/compositor_cairo.py +37 -0
  15. keyed-0.1.0/src/keyed/compositor/compositor_taichi.py +508 -0
  16. keyed-0.1.0/src/keyed/compositor/core.py +33 -0
  17. keyed-0.1.0/src/keyed/config.py +107 -0
  18. keyed-0.1.0/src/keyed/constants.py +163 -0
  19. keyed-0.1.0/src/keyed/context.py +25 -0
  20. keyed-0.1.0/src/keyed/curve.py +395 -0
  21. keyed-0.1.0/src/keyed/easing.py +454 -0
  22. keyed-0.1.0/src/keyed/effects.py +36 -0
  23. keyed-0.1.0/src/keyed/extras.py +24 -0
  24. keyed-0.1.0/src/keyed/geometry.py +218 -0
  25. keyed-0.1.0/src/keyed/group.py +514 -0
  26. keyed-0.1.0/src/keyed/helpers.py +78 -0
  27. keyed-0.1.0/src/keyed/highlight.py +147 -0
  28. keyed-0.1.0/src/keyed/line.py +424 -0
  29. keyed-0.1.0/src/keyed/parser.py +53 -0
  30. keyed-0.1.0/src/keyed/plot.py +592 -0
  31. keyed-0.1.0/src/keyed/previewer.py +385 -0
  32. keyed-0.1.0/src/keyed/py.typed +0 -0
  33. keyed-0.1.0/src/keyed/renderer.py +212 -0
  34. keyed-0.1.0/src/keyed/scene.py +482 -0
  35. keyed-0.1.0/src/keyed/shapes.py +516 -0
  36. keyed-0.1.0/src/keyed/text.py +602 -0
  37. keyed-0.1.0/src/keyed/transforms.py +1062 -0
  38. keyed-0.1.0/src/keyed/types.py +28 -0
  39. keyed-0.1.0/src/keyed.egg-info/PKG-INFO +121 -0
  40. keyed-0.1.0/src/keyed.egg-info/SOURCES.txt +58 -0
  41. keyed-0.1.0/src/keyed.egg-info/dependency_links.txt +1 -0
  42. keyed-0.1.0/src/keyed.egg-info/entry_points.txt +2 -0
  43. keyed-0.1.0/src/keyed.egg-info/requires.txt +37 -0
  44. keyed-0.1.0/src/keyed.egg-info/top_level.txt +1 -0
  45. keyed-0.1.0/tests/test_align.py +32 -0
  46. keyed-0.1.0/tests/test_char_find.py +27 -0
  47. keyed-0.1.0/tests/test_common.py +44 -0
  48. keyed-0.1.0/tests/test_constants.py +158 -0
  49. keyed-0.1.0/tests/test_curves.py +51 -0
  50. keyed-0.1.0/tests/test_easing.py +69 -0
  51. keyed-0.1.0/tests/test_examples.py +173 -0
  52. keyed-0.1.0/tests/test_expression.py +69 -0
  53. keyed-0.1.0/tests/test_helpers.py +63 -0
  54. keyed-0.1.0/tests/test_loop.py +17 -0
  55. keyed-0.1.0/tests/test_pingpong.py +28 -0
  56. keyed-0.1.0/tests/test_property.py +204 -0
  57. keyed-0.1.0/tests/test_scene.py +201 -0
  58. keyed-0.1.0/tests/test_scene_find.py +35 -0
  59. keyed-0.1.0/tests/test_selection.py +22 -0
  60. keyed-0.1.0/tests/test_transforms.py +83 -0
keyed-0.1.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Doug Mercer
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
keyed-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: keyed
3
+ Version: 0.1.0
4
+ Summary: A reactive animation library.
5
+ Author-email: Doug Mercer <dougmerceryt@gmail.com>
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2024, Doug Mercer
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+
35
+ Keywords: keyed,animation,reactive
36
+ Classifier: Development Status :: 4 - Beta
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Requires-Python: <3.13,>=3.11
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Requires-Dist: pyav
44
+ Requires-Dist: pillow
45
+ Requires-Dist: pycairo
46
+ Requires-Dist: pydantic
47
+ Requires-Dist: pygments
48
+ Requires-Dist: scipy
49
+ Requires-Dist: shapely
50
+ Requires-Dist: signified
51
+ Requires-Dist: tqdm
52
+ Requires-Dist: typer
53
+ Provides-Extra: lint
54
+ Requires-Dist: ruff; extra == "lint"
55
+ Requires-Dist: pyright; extra == "lint"
56
+ Provides-Extra: test
57
+ Requires-Dist: hypothesis; extra == "test"
58
+ Requires-Dist: pytest; extra == "test"
59
+ Requires-Dist: pytest-cov; extra == "test"
60
+ Requires-Dist: syrupy; extra == "test"
61
+ Provides-Extra: docs
62
+ Requires-Dist: beautifulsoup4; extra == "docs"
63
+ Requires-Dist: mkdocs; extra == "docs"
64
+ Requires-Dist: mkdocs-material; extra == "docs"
65
+ Requires-Dist: mkdocstrings[python]; extra == "docs"
66
+ Requires-Dist: mkdocs-material-extensions; extra == "docs"
67
+ Provides-Extra: previewer
68
+ Requires-Dist: pyside6; extra == "previewer"
69
+ Requires-Dist: watchdog; extra == "previewer"
70
+ Provides-Extra: gpu-compositor
71
+ Requires-Dist: taichi; extra == "gpu-compositor"
72
+ Provides-Extra: all
73
+ Requires-Dist: keyed[docs,gpu-compositor,lint,previewer,test]; extra == "all"
74
+ Dynamic: license-file
75
+
76
+ # Keyed
77
+
78
+ [![PyPI - Downloads](https://img.shields.io/pypi/dw/keyed)](https://pypi.org/project/keyed/)
79
+ [![PyPI - Version](https://img.shields.io/pypi/v/keyed)](https://pypi.org/project/keyed/)
80
+ [![Tests Status](https://github.com/dougmercer/keyed/actions/workflows/tests.yml/badge.svg)](https://github.com/dougmercer/keyed/actions/workflows/tests.yml?query=branch%3Amain)
81
+
82
+ ---
83
+
84
+ **Documentation**: [https://dougmercer.github.io/keyed](https://dougmercer.github.io/keyed)
85
+ **Source Code**: [https://github.com/dougmercer/keyed](https://github.com/dougmercer/keyed)
86
+
87
+ ---
88
+
89
+ Keyed is a Python library for creating programmatically defined animations. Named after [key frames](https://en.wikipedia.org/wiki/Key_frame), the defining points in an animation sequence, Keyed makes it easy to create sophisticated animations through code.
90
+
91
+ ## Features
92
+
93
+ - **Reactive Programming Model**: Built using the reactive programming library [signified](https://github.com/dougmercer/signified) to make declaratively defining highly dynamic animations a breeze
94
+ - **Vector Graphics**: [Cairo](https://www.cairographics.org)-based rendering for crisp, scalable graphics
95
+ - **Flexible Shape System**: Define basic lines, shapes, curves, and complex geometries
96
+ - **Code Animation**: Animate syntax highled code snippets
97
+
98
+ ## Installation
99
+
100
+ Keyed requires a couple system level dependencies (e.g., [Cairo](https://www.cairographics.org/download/) and [ffmpeg](https://www.ffmpeg.org/)).
101
+
102
+ For detailed installation instructions visit our [Installation Guide](https://dougmercer.github.io/keyed/install)
103
+ .
104
+
105
+ But, once you have the necessary system dependencies installed, installing `keyed` is as simple as,
106
+
107
+ ```console
108
+ pip install keyed
109
+ ```
110
+
111
+ ## Project Status
112
+
113
+ This project is in beta, so APIs may change.
114
+
115
+ ## Alternatives
116
+ While I find `keyed` very fun and useful (particularly for animating syntax highlighted code in my [YouTube videos](https://youtube.com/@dougmercer)), there are several other excellent and far more mature animation libraries that you should probably use instead.
117
+
118
+ Before you decide to use `keyed`, be sure to check out:
119
+
120
+ * [Manim](https://manim.community): Comprehensive mathematical animation system originally created by Grant Sanderson of the YouTube channel 3blue1brown, but later adopted and extended by the manim community.
121
+ * [py5](https://py5coding.org): A Python wrapper for p5, the Java animation library.
keyed-0.1.0/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Keyed
2
+
3
+ [![PyPI - Downloads](https://img.shields.io/pypi/dw/keyed)](https://pypi.org/project/keyed/)
4
+ [![PyPI - Version](https://img.shields.io/pypi/v/keyed)](https://pypi.org/project/keyed/)
5
+ [![Tests Status](https://github.com/dougmercer/keyed/actions/workflows/tests.yml/badge.svg)](https://github.com/dougmercer/keyed/actions/workflows/tests.yml?query=branch%3Amain)
6
+
7
+ ---
8
+
9
+ **Documentation**: [https://dougmercer.github.io/keyed](https://dougmercer.github.io/keyed)
10
+ **Source Code**: [https://github.com/dougmercer/keyed](https://github.com/dougmercer/keyed)
11
+
12
+ ---
13
+
14
+ Keyed is a Python library for creating programmatically defined animations. Named after [key frames](https://en.wikipedia.org/wiki/Key_frame), the defining points in an animation sequence, Keyed makes it easy to create sophisticated animations through code.
15
+
16
+ ## Features
17
+
18
+ - **Reactive Programming Model**: Built using the reactive programming library [signified](https://github.com/dougmercer/signified) to make declaratively defining highly dynamic animations a breeze
19
+ - **Vector Graphics**: [Cairo](https://www.cairographics.org)-based rendering for crisp, scalable graphics
20
+ - **Flexible Shape System**: Define basic lines, shapes, curves, and complex geometries
21
+ - **Code Animation**: Animate syntax highled code snippets
22
+
23
+ ## Installation
24
+
25
+ Keyed requires a couple system level dependencies (e.g., [Cairo](https://www.cairographics.org/download/) and [ffmpeg](https://www.ffmpeg.org/)).
26
+
27
+ For detailed installation instructions visit our [Installation Guide](https://dougmercer.github.io/keyed/install)
28
+ .
29
+
30
+ But, once you have the necessary system dependencies installed, installing `keyed` is as simple as,
31
+
32
+ ```console
33
+ pip install keyed
34
+ ```
35
+
36
+ ## Project Status
37
+
38
+ This project is in beta, so APIs may change.
39
+
40
+ ## Alternatives
41
+ While I find `keyed` very fun and useful (particularly for animating syntax highlighted code in my [YouTube videos](https://youtube.com/@dougmercer)), there are several other excellent and far more mature animation libraries that you should probably use instead.
42
+
43
+ Before you decide to use `keyed`, be sure to check out:
44
+
45
+ * [Manim](https://manim.community): Comprehensive mathematical animation system originally created by Grant Sanderson of the YouTube channel 3blue1brown, but later adopted and extended by the manim community.
46
+ * [py5](https://py5coding.org): A Python wrapper for p5, the Java animation library.
@@ -0,0 +1,167 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "keyed"
7
+ description = "A reactive animation library."
8
+ version = "0.1.0"
9
+ readme = "README.md"
10
+ license = {file="LICENSE"}
11
+ authors = [
12
+ {name = "Doug Mercer", email = "dougmerceryt@gmail.com"}
13
+ ]
14
+ requires-python = ">=3.11,<3.13" # taichi is most restrictive
15
+ dependencies = [
16
+ "pyav",
17
+ "pillow",
18
+ "pycairo",
19
+ "pydantic",
20
+ "pygments",
21
+ "scipy",
22
+ "shapely",
23
+ "signified",
24
+ "tqdm",
25
+ "typer",
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 4 - Beta",
29
+ "Intended Audience :: Developers",
30
+ "Programming Language :: Python :: 3.11",
31
+ "Programming Language :: Python :: 3.12",
32
+ ]
33
+ keywords = ["keyed", "animation", "reactive"]
34
+
35
+
36
+ [project.optional-dependencies]
37
+ lint = [
38
+ "ruff",
39
+ "pyright",
40
+ ]
41
+ test = [
42
+ "hypothesis",
43
+ "pytest",
44
+ "pytest-cov",
45
+ "syrupy",
46
+ ]
47
+ docs = [
48
+ "beautifulsoup4",
49
+ "mkdocs",
50
+ "mkdocs-material",
51
+ "mkdocstrings[python]",
52
+ "mkdocs-material-extensions",
53
+ ]
54
+ previewer = [
55
+ "pyside6",
56
+ "watchdog",
57
+ ]
58
+ gpu-compositor = [
59
+ "taichi",
60
+ ]
61
+ all = ["keyed[lint,test,docs,previewer,gpu-compositor]"]
62
+
63
+ [tool.setuptools.package-data]
64
+ keyed = ["py.typed"]
65
+
66
+ [tool.pytest.ini_options]
67
+ addopts = "--cov=src --cov-report=xml --junitxml=junit/test-results.xml --durations=10"
68
+ filterwarnings = ["ignore::DeprecationWarning"]
69
+ testpaths = ["tests", "src"]
70
+ markers = {snapshot = "marks tests as snapshot (deselect with '-m \"not snapshot\"')"}
71
+
72
+ [tool.coverage.report]
73
+ exclude_also = [
74
+ "pragma: no cover",
75
+ "if TYPE_CHECKING:",
76
+ "pass"
77
+ ]
78
+
79
+ [tool.ruff]
80
+ # Exclude a variety of commonly ignored directories.
81
+ exclude = [
82
+ ".bzr",
83
+ ".direnv",
84
+ ".eggs",
85
+ ".git",
86
+ ".git-rewrite",
87
+ ".hg",
88
+ ".ipynb_checkpoints",
89
+ ".mypy_cache",
90
+ ".nox",
91
+ ".pants.d",
92
+ ".pyenv",
93
+ ".pytest_cache",
94
+ ".pytype",
95
+ ".ruff_cache",
96
+ ".svn",
97
+ ".tox",
98
+ ".venv",
99
+ ".vscode",
100
+ ".__pycache__",
101
+ "__pypackages__",
102
+ "_build",
103
+ "buck-out",
104
+ "build",
105
+ "dist",
106
+ "docs",
107
+ "envs",
108
+ "htmlcov",
109
+ "results",
110
+ "significant.egg-info",
111
+ "junk",
112
+ ".hypothesis",
113
+ "node_modules",
114
+ "site-packages",
115
+ "venv",
116
+ ]
117
+
118
+ line-length = 120
119
+ indent-width = 4
120
+ target-version = "py39"
121
+
122
+ [tool.ruff.lint]
123
+ # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
124
+ # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
125
+ # McCabe complexity (`C901`) by default.
126
+ select = ["E4", "E7", "E9", "F", "I"]
127
+ ignore = []
128
+
129
+ # Allow fix for all enabled rules (when `--fix`) is provided.
130
+ fixable = ["ALL"]
131
+ unfixable = []
132
+
133
+ # Allow unused variables when underscore-prefixed.
134
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
135
+
136
+ [tool.ruff.format]
137
+ # Like Black, use double quotes for strings.
138
+ quote-style = "double"
139
+
140
+ # Like Black, indent with spaces, rather than tabs.
141
+ indent-style = "space"
142
+
143
+ # Like Black, respect magic trailing commas.
144
+ skip-magic-trailing-comma = false
145
+
146
+ # Like Black, automatically detect the appropriate line ending.
147
+ line-ending = "auto"
148
+
149
+ # Enable auto-formatting of code examples in docstrings. Markdown,
150
+ # reStructuredText code/literal blocks and doctests are all supported.
151
+ #
152
+ # This is currently disabled by default, but it is planned for this
153
+ # to be opt-out in the future.
154
+ docstring-code-format = false
155
+
156
+ # Set the line length limit used when formatting code snippets in
157
+ # docstrings.
158
+ #
159
+ # This only has an effect when the `docstring-code-format` setting is
160
+ # enabled.
161
+ docstring-code-line-length = "dynamic"
162
+
163
+ [tool.ruff.lint.isort]
164
+ known-first-party = ["keyed_*", "helpers"]
165
+
166
+ [project.scripts]
167
+ keyed = "keyed.cli:main"
keyed-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """A key-frame focused animation engine."""
2
+
3
+ # Taichi makes it an absolute nightmare to squelch startup noise.
4
+ import os
5
+
6
+ os.environ["ENABLE_TAICHI_HEADER_PRINT"] = "False"
7
+ try:
8
+ from taichi._logging import ERROR, set_logging_level # noqa: E402 # type: ignore
9
+
10
+ set_logging_level(ERROR)
11
+ del set_logging_level
12
+ except ImportError:
13
+ pass
14
+ finally:
15
+ del os
16
+
17
+ from . import easing # noqa
18
+ from . import highlight # noqa
19
+ from . import transforms # noqa
20
+ from .animation import * # noqa
21
+ from .annotations import * # noqa
22
+ from .base import * # noqa
23
+ from .text import * # noqa
24
+ from .constants import * # noqa
25
+ from .color import * # noqa
26
+ from .compositor import * # noqa
27
+ from .curve import * # noqa
28
+ from .effects import * # noqa
29
+ from .highlight import * # noqa
30
+ from .line import * # noqa
31
+ from .geometry import * # noqa
32
+ from .plot import * # noqa
33
+ from .scene import * # noqa
34
+ from .group import * # noqa
35
+ from .shapes import * # noqa
36
+
37
+ # This must go last
38
+ from .extras import * # noqa
@@ -0,0 +1,278 @@
1
+ """Animation related classes/functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum, auto
6
+ from functools import partial
7
+ from typing import Any, Generic, TypeVar
8
+
9
+ from signified import Computed, HasValue, ReactiveValue, Signal, computed
10
+
11
+ from .constants import ALWAYS
12
+ from .easing import EasingFunctionT, easing_function, linear_in_out
13
+
14
+ __all__ = [
15
+ "AnimationType",
16
+ "Animation",
17
+ "stagger",
18
+ "Loop",
19
+ "PingPong",
20
+ "step",
21
+ ]
22
+
23
+
24
+ class AnimationType(Enum):
25
+ """Specifies the mathematical operation used to combine the original and animated values."""
26
+
27
+ MULTIPLY = auto()
28
+ """Multiplies the original value by the animated value."""
29
+ ABSOLUTE = auto()
30
+ """Replaces the original value with the animated value."""
31
+ ADD = auto()
32
+ """Adds the animated value to the original value."""
33
+
34
+
35
+ T = TypeVar("T")
36
+ A = TypeVar("A")
37
+
38
+
39
+ class Animation(Generic[T]):
40
+ """Define an animation.
41
+
42
+ Animations vary a parameter over time.
43
+
44
+ Generally, Animations become active at ``start_frame`` and smoothly change
45
+ according to the ``easing`` function until terminating to a final value at
46
+ ``end_frame``. The animation will remain active (i.e., the parameter will
47
+ not suddenly jump back to it's pre-animation state), but will cease varying.
48
+
49
+ Args:
50
+ start: Frame at which the animation will become active.
51
+ end: Frame at which the animation will stop varying.
52
+ start_value: Value at which the animation will start.
53
+ end_value: Value at which the animation will end.
54
+ ease: The rate in which the value will change throughout the animation.
55
+ animation_type: How the animation value will affect the original value.
56
+
57
+ Raises:
58
+ ValueError: When ``start_frame > end_frame``
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ start: int,
64
+ end: int,
65
+ start_value: HasValue[T],
66
+ end_value: HasValue[T],
67
+ ease: EasingFunctionT = linear_in_out,
68
+ animation_type: AnimationType = AnimationType.ABSOLUTE,
69
+ ) -> None:
70
+ if start > end:
71
+ raise ValueError("Ending frame must be after starting frame.")
72
+ if not hasattr(self, "start_frame"):
73
+ self.start_frame = start
74
+ if not hasattr(self, "end_frame"):
75
+ self.end_frame = end
76
+ self.start_value = start_value
77
+ self.end_value = end_value
78
+ self.ease = ease
79
+ self.animation_type = animation_type
80
+
81
+ def __call__(self, value: HasValue[A], frame: ReactiveValue[int]) -> Computed[A | T]:
82
+ """Bind the animation to the input value and frame."""
83
+ easing = easing_function(start=self.start_frame, end=self.end_frame, ease=self.ease, frame=frame)
84
+
85
+ @computed
86
+ def f(value: A, frame: int, easing: float, start: T, end: T) -> A | T:
87
+ eased_value = end * easing + start * (1 - easing) # pyright: ignore[reportOperatorIssue] # noqa: E501
88
+
89
+ match self.animation_type:
90
+ case AnimationType.ABSOLUTE:
91
+ pass
92
+ case AnimationType.ADD:
93
+ eased_value = value + eased_value
94
+ case AnimationType.MULTIPLY:
95
+ eased_value = value * eased_value
96
+ case _:
97
+ raise ValueError("Undefined AnimationType")
98
+
99
+ return value if frame < self.start_frame else eased_value
100
+
101
+ return f(value, frame, easing, self.start_value, self.end_value)
102
+
103
+ def __len__(self) -> int:
104
+ """Return number of frames in the animation."""
105
+ return self.end_frame - self.start_frame + 1
106
+
107
+
108
+ class Loop(Animation):
109
+ """Loop an animation.
110
+
111
+ Args:
112
+ animation: The animation to loop.
113
+ n: Number of times to loop the animation.
114
+ """
115
+
116
+ def __init__(self, animation: Animation, n: int = 1):
117
+ self.animation = animation
118
+ self.n = n
119
+ super().__init__(self.start_frame, self.end_frame, 0, 0)
120
+
121
+ @property
122
+ def start_frame(self) -> int: # type: ignore[override]
123
+ """Frame at which the animation will become active."""
124
+ return self.animation.start_frame
125
+
126
+ @property
127
+ def end_frame(self) -> int: # type: ignore[override]
128
+ """Frame at which the animation will stop varying."""
129
+ return self.animation.start_frame + len(self.animation) * self.n
130
+
131
+ def __call__(self, value: HasValue[T], frame: ReactiveValue[int]) -> Computed[T]:
132
+ """Apply the animation to the current value at the current frame.
133
+
134
+ Args:
135
+ frame: The frame at which the animation is applied.
136
+ value: The initial value.
137
+
138
+ Returns:
139
+ The value after the animation.
140
+ """
141
+ effective_frame = self.animation.start_frame + (frame - self.animation.start_frame) % len(self.animation)
142
+ active_anim = self.animation(value, effective_frame)
143
+ post_anim = self.animation(value, Signal(self.animation.end_frame))
144
+
145
+ @computed
146
+ def f(frame: int, value: Any, active_anim: Any, post_anim: Any) -> Any:
147
+ if frame < self.start_frame:
148
+ return value
149
+ elif frame < self.end_frame:
150
+ return active_anim
151
+ else:
152
+ return post_anim
153
+
154
+ return f(frame, value, active_anim, post_anim)
155
+
156
+ def __repr__(self) -> str:
157
+ return f"Loop(animation={self.animation}, n={self.n})"
158
+
159
+
160
+ class PingPong(Animation):
161
+ """Play an animation forward, then backwards n times.
162
+
163
+ Args:
164
+ animation: The animation to ping-pong.
165
+ n: Number of full back-and-forth cycles
166
+ """
167
+
168
+ def __init__(self, animation: Animation, n: int = 1):
169
+ self.animation = animation
170
+ self.n = n
171
+ super().__init__(self.start_frame, self.end_frame, 0, 0)
172
+
173
+ @property
174
+ def start_frame(self) -> int: # type: ignore[override]
175
+ """Returns the frame at which the animation begins."""
176
+ return self.animation.start_frame
177
+
178
+ @property
179
+ def end_frame(self) -> int: # type: ignore[override]
180
+ """Returns the frame at which the animation stops varying.
181
+
182
+ Notes:
183
+ Each cycle consists of going forward and coming back.
184
+ """
185
+ return self.animation.start_frame + self.cycle_len * self.n
186
+
187
+ @property
188
+ def cycle_len(self) -> int:
189
+ """Returns the number of frames in one cycle."""
190
+ return 2 * (len(self.animation) - 1)
191
+
192
+ def __call__(self, value: HasValue[T], frame: ReactiveValue[int]) -> Computed[T]:
193
+ """Apply the animation to the current value at the current frame.
194
+
195
+ Args:
196
+ frame: The frame at which the animation is applied.
197
+ value: The initial value.
198
+
199
+ Returns:
200
+ The value after the animation.
201
+ """
202
+
203
+ # Calculate effective frame based on whether we're in the forward or backward cycle
204
+ @computed
205
+ def effective_frame_(frame: int) -> int:
206
+ frame_in_cycle = (frame - self.start_frame) % self.cycle_len
207
+ return (
208
+ self.animation.start_frame + frame_in_cycle
209
+ if frame_in_cycle < len(self.animation)
210
+ else self.animation.end_frame - (frame_in_cycle - len(self.animation) + 1)
211
+ )
212
+
213
+ effective_frame = effective_frame_(frame)
214
+ anim = self.animation(value, effective_frame)
215
+
216
+ @computed
217
+ def f(frame: int, value: Any) -> Any:
218
+ return value if frame < self.start_frame or frame > self.end_frame else anim.value
219
+
220
+ return f(frame, value)
221
+
222
+ def __repr__(self) -> str:
223
+ return f"PingPong(animation={self.animation}, n={self.n})"
224
+
225
+
226
+ def stagger(
227
+ start_value: float = 0,
228
+ end_value: float = 1,
229
+ easing: EasingFunctionT = linear_in_out,
230
+ animation_type: AnimationType = AnimationType.ABSOLUTE,
231
+ ) -> partial[Animation]:
232
+ """Partially-initialize an animation for use with [Group.write_on][keyed.group.Group.write_on].
233
+
234
+ This will set the animations values, easing, and type without setting its start/end frames.
235
+
236
+ Args:
237
+ start_value: Value at which the animation will start.
238
+ end_value: Value at which the animation will end.
239
+ easing: The rate in which the value will change throughout the animation.
240
+ animation_type: How the animation value will affect the original value.
241
+
242
+ Returns:
243
+ Partially initialized animation.
244
+ """
245
+ return partial(
246
+ Animation,
247
+ start_value=start_value,
248
+ end_value=end_value,
249
+ ease=easing,
250
+ animation_type=animation_type,
251
+ )
252
+
253
+
254
+ def step(
255
+ value: HasValue[T], frame: int = ALWAYS, animation_type: AnimationType = AnimationType.ABSOLUTE
256
+ ) -> Animation[T]:
257
+ """Return an animation that applies a step function to the Variable at a particular frame.
258
+
259
+ Args:
260
+ value: The value to step to.
261
+ frame: The frame at which the step will be applied.
262
+ animation_type: See :class:`AnimationType`.
263
+
264
+ Returns:
265
+ An animation that applies a step function to the Variable at a particular frame.
266
+ """
267
+ # Can this be simpler? Something like...
268
+ # def step_builder(initial_value: HasValue[A], frame_rx: ReactiveValue[int]) -> Computed[A|T]:
269
+ # return (frame_rx >= frame).where(value, initial_value)
270
+
271
+ # return step_builder # Callable[[HasValue[A], ReactiveValue[int]], Computed[A|T]]
272
+ return Animation(
273
+ start=frame,
274
+ end=frame,
275
+ start_value=value,
276
+ end_value=value,
277
+ animation_type=animation_type,
278
+ )