glplot 0.1.9__tar.gz → 0.1.11__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.
- {glplot-0.1.9 → glplot-0.1.11}/.gitignore +0 -2
- {glplot-0.1.9 → glplot-0.1.11}/PKG-INFO +4 -4
- {glplot-0.1.9 → glplot-0.1.11}/README.md +3 -3
- {glplot-0.1.9 → glplot-0.1.11}/glplot/__init__.py +1 -1
- {glplot-0.1.9 → glplot-0.1.11}/glplot/animation.py +334 -70
- {glplot-0.1.9 → glplot-0.1.11}/glplot/engine.py +187 -17
- glplot-0.1.11/glplot/gui/background.py +111 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/datasets.py +37 -9
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/expressions.py +17 -1
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/layerops.py +323 -38
- glplot-0.1.11/glplot/gui/mathadvise.py +397 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/mathops.py +1151 -75
- glplot-0.1.11/glplot/gui/mathops2d.py +786 -0
- glplot-0.1.11/glplot/gui/mathopsnd.py +405 -0
- glplot-0.1.11/glplot/gui/models.py +132 -0
- glplot-0.1.11/glplot/gui/notifications.py +73 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/base.py +6 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/data_editor.py +466 -65
- glplot-0.1.11/glplot/gui/panels/mathlab.py +6037 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/pipeline.py +202 -2
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/scene.py +6 -6
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/style.py +68 -9
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/timeline.py +9 -3
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/styles.py +25 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/widgets.py +688 -34
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/workspace.py +156 -14
- {glplot-0.1.9 → glplot-0.1.11}/glplot/managers/hud.py +8 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/options.py +7 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/pyplot.py +132 -36
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/axis.py +16 -3
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/colorbar.py +81 -20
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/contour_labels.py +3 -1
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/scatter.py +6 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/anim_export.py +11 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/export.py +23 -8
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/preview.py +141 -26
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/shaders.py +20 -2
- {glplot-0.1.9 → glplot-0.1.11}/pyproject.toml +1 -1
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_animation_api.py +167 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_animation_integration.py +217 -1
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_encodings.py +19 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_export.py +112 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gallery_integration.py +30 -5
- glplot-0.1.11/tests/test_gui_background.py +170 -0
- glplot-0.1.11/tests/test_gui_data_editor.py +1844 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_datasets.py +48 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_input_regressions.py +89 -1
- glplot-0.1.11/tests/test_gui_layer_kinds.py +1130 -0
- glplot-0.1.11/tests/test_gui_mathadvise.py +295 -0
- glplot-0.1.11/tests/test_gui_mathlab.py +5921 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_mathops.py +1236 -0
- glplot-0.1.11/tests/test_gui_mathops2d.py +548 -0
- glplot-0.1.11/tests/test_gui_mathopsnd.py +297 -0
- glplot-0.1.11/tests/test_gui_models.py +87 -0
- glplot-0.1.11/tests/test_gui_notifications.py +102 -0
- glplot-0.1.11/tests/test_gui_pipeline.py +572 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_styles.py +50 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_timeline_panel.py +22 -0
- glplot-0.1.11/tests/test_gui_widgets.py +538 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_mpl_compat.py +105 -27
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_plot_style_api.py +30 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_robustness.py +38 -1
- glplot-0.1.11/tests/test_scatter_default_size.py +191 -0
- glplot-0.1.9/glplot/gui/panels/mathlab.py +0 -2498
- glplot-0.1.9/tests/test_gui_data_editor.py +0 -837
- glplot-0.1.9/tests/test_gui_layer_kinds.py +0 -537
- glplot-0.1.9/tests/test_gui_mathlab.py +0 -1209
- glplot-0.1.9/tests/test_gui_pipeline.py +0 -268
- {glplot-0.1.9 → glplot-0.1.11}/CHANGELOG.md +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/CITATION.cff +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/LICENSE +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/__main__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/anim/__init__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/anim/applier.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/anim/primitives.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/anim/processes.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/backend.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/controllers.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/__init__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/camera3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/context.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/layers.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/layout.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/legacy.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/panel.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/core/timeline.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/__init__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/actions.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/app.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/clipboard.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/commands.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/dataio.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/dynamics.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/fuzzy.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/generators3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/history.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/icons.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/keys.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/layerops3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/__init__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/annotate.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/dynamics.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/functions.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/help.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/objects3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/palette.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/presentation.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/selection.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/panels/view3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/pipeline.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/gui/theme.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/managers/__init__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/managers/axis.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/managers/effects.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/managers/hud_state.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/managers/picking.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/managers/renderer_manager.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/policy.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/__init__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/axes3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/base.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/density.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/exact.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/fractal.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/geometry3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/interaction.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/legend.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/line_family.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/patch.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/polyline.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/renderers/text.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/__init__.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/blending.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/gl_utils.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/mpl_bridge.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/mpl_process.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/glplot/utils/scale.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/conftest.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_anim_applier.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_anim_export.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_anim_primitives.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_anim_processes.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_api_consistency.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_axes.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_axes3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_axis_margins.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_blending_3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_camera3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_camera_anisotropy.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_camera_projections.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_compositing_3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_compositing_api.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_contour_labels.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_dataio.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_density_tint.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_edge_cases.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_effects_post.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_engine.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_fractal.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_function_layer.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_generators3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_generators3d_extra.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_geometry3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_3d_panels.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_actions.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_annotate.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_clipboard.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_commands.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_dynamics.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_engine_integration.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_expressions.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_fields.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_fuzzy.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_history.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_import_safety.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_impostor_alignment.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_keyboard_capture.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_keys.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_layer_colormaps.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_layer_params.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_legend.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_regression_r2.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_scene_delete.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_scene_order.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_style_layer_editor.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_gui_styles_extra.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_helpers.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_impostor_scatter_fidelity.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_layerops3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_layers.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_mpl_bridge.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_mpl_compat_3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_mpl_parity_surface.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_mpl_process.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_objects3d_preview.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_options.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_outline.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_outline_2d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_panels.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_performance_benchmarks.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_polyline_lod.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_pyplot.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_pyplot_3d.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_pyplot_integration.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_regression.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_scatter_renderer.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_selection_model.py +0 -0
- {glplot-0.1.9 → glplot-0.1.11}/tests/test_timeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glplot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: High-performance OpenGL plotting library for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/AkarisDimitry/GLPlot
|
|
6
6
|
Project-URL: Repository, https://github.com/AkarisDimitry/GLPlot
|
|
@@ -486,7 +486,7 @@ plt.show()
|
|
|
486
486
|
```
|
|
487
487
|
</details>
|
|
488
488
|
|
|
489
|
-
**All of the above render at 60+ FPS** with interactive panning, zooming, and rotation, regardless of point count. Discover more in the [example gallery](examples/gallery/README.md) (28 static scripts) and the [animated gallery](examples/gallery/animations/README.md) (
|
|
489
|
+
**All of the above render at 60+ FPS** with interactive panning, zooming, and rotation, regardless of point count. Discover more in the [example gallery](examples/gallery/README.md) (28 static scripts) and the [animated gallery](examples/gallery/animations/README.md) (16 animated scripts).
|
|
490
490
|
|
|
491
491
|
## Features
|
|
492
492
|
|
|
@@ -547,7 +547,7 @@ interactive path never reads pixels back to the CPU.
|
|
|
547
547
|
|
|
548
548
|

|
|
549
549
|
|
|
550
|
-
See [
|
|
550
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
551
551
|
for the full derivation of each stage, including the density-accumulation math and the
|
|
552
552
|
viewport-relative projection that keeps zoom numerically stable.
|
|
553
553
|
|
|
@@ -568,7 +568,7 @@ for GPU/environment diagnostics.
|
|
|
568
568
|
## Documentation
|
|
569
569
|
|
|
570
570
|
- API reference: docstrings in `glplot.pyplot`, or the built docs — see [docs/README.md](docs/README.md)
|
|
571
|
-
- Architecture: [
|
|
571
|
+
- Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
572
572
|
- Dev tools: [tools/README.md](tools/README.md)
|
|
573
573
|
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) · Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
574
574
|
|
|
@@ -442,7 +442,7 @@ plt.show()
|
|
|
442
442
|
```
|
|
443
443
|
</details>
|
|
444
444
|
|
|
445
|
-
**All of the above render at 60+ FPS** with interactive panning, zooming, and rotation, regardless of point count. Discover more in the [example gallery](examples/gallery/README.md) (28 static scripts) and the [animated gallery](examples/gallery/animations/README.md) (
|
|
445
|
+
**All of the above render at 60+ FPS** with interactive panning, zooming, and rotation, regardless of point count. Discover more in the [example gallery](examples/gallery/README.md) (28 static scripts) and the [animated gallery](examples/gallery/animations/README.md) (16 animated scripts).
|
|
446
446
|
|
|
447
447
|
## Features
|
|
448
448
|
|
|
@@ -503,7 +503,7 @@ interactive path never reads pixels back to the CPU.
|
|
|
503
503
|
|
|
504
504
|

|
|
505
505
|
|
|
506
|
-
See [
|
|
506
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
507
507
|
for the full derivation of each stage, including the density-accumulation math and the
|
|
508
508
|
viewport-relative projection that keeps zoom numerically stable.
|
|
509
509
|
|
|
@@ -524,7 +524,7 @@ for GPU/environment diagnostics.
|
|
|
524
524
|
## Documentation
|
|
525
525
|
|
|
526
526
|
- API reference: docstrings in `glplot.pyplot`, or the built docs — see [docs/README.md](docs/README.md)
|
|
527
|
-
- Architecture: [
|
|
527
|
+
- Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
528
528
|
- Dev tools: [tools/README.md](tools/README.md)
|
|
529
529
|
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) · Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
530
530
|
|
|
@@ -38,7 +38,7 @@ except Exception: # pragma: no cover - imgui_bundle is optional at import time
|
|
|
38
38
|
from .engine import GPULinePlot
|
|
39
39
|
from .options import BlendMode, EngineOptions, RenderMode
|
|
40
40
|
|
|
41
|
-
__version__ = "0.1.
|
|
41
|
+
__version__ = "0.1.10"
|
|
42
42
|
__author__ = "Juan Manuel Lombardi"
|
|
43
43
|
__license__ = "MIT"
|
|
44
44
|
|
|
@@ -32,25 +32,29 @@ How a frame is produced
|
|
|
32
32
|
-----------------------
|
|
33
33
|
:func:`figure_to_rgb` is the whole bridge, and it has two modes:
|
|
34
34
|
|
|
35
|
-
* **
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
the
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
* **OpenGL** — when ``fig.window`` exists, or ``Animation.save`` is explicitly given
|
|
36
|
+
``savefig_kwargs={"renderer": "opengl"}``, the scene is rendered offscreen through the
|
|
37
|
+
real GPU pipeline at full quality. ``ExportManager.render_rgb`` returns the framebuffer
|
|
38
|
+
directly, so animation frames do not make a temporary PNG round trip.
|
|
39
|
+
* **Headless** — when it does not,
|
|
40
|
+
:func:`glplot.utils.preview.render_preview_array` draws the same scene through
|
|
41
|
+
matplotlib's Agg backend and hands back the rendered pixels directly, with no file
|
|
42
|
+
involved. This is the path that makes ``ani.save(...)`` work in CI, in a notebook, and in
|
|
43
|
+
any script that never called ``show()`` — the common case for a saved animation, and the
|
|
44
|
+
one with hundreds of frames to pay a per-frame cost on. It used to round-trip through a
|
|
45
|
+
PNG the same way the GL path still does; profiling a real animation loop found PIL's PNG
|
|
46
|
+
encoder alone cost as much as matplotlib's entire draw, so that encode plus the disk
|
|
47
|
+
write/read it required were pure overhead for a caller (this one) that only ever wanted
|
|
48
|
+
the array. :func:`glplot.utils.preview.render_preview` (the file-writing function a plain
|
|
49
|
+
``savefig()`` call uses) is unchanged; :func:`render_preview_array` is a second entry
|
|
50
|
+
point onto the same figure-building code, not a rewrite of it — see its docstring.
|
|
51
|
+
|
|
52
|
+
**The matplotlib path is not pixel-identical to the GL path.** ``render_preview`` is a
|
|
50
53
|
matplotlib re-drawing of the scene, not the GPU renderer; it approximates. An animation
|
|
51
|
-
saved without a window
|
|
52
|
-
|
|
53
|
-
``
|
|
54
|
+
saved without a window uses this backwards-compatible path unless a renderer is requested.
|
|
55
|
+
Pass ``savefig_kwargs={"renderer": "opengl"}`` when the GPU look matters, ``"matplotlib"``
|
|
56
|
+
to force the portable preview, or ``"auto"`` to try an invisible GL context and warn before
|
|
57
|
+
falling back to matplotlib when no context can be created.
|
|
54
58
|
|
|
55
59
|
Limitations, stated plainly
|
|
56
60
|
---------------------------
|
|
@@ -59,13 +63,17 @@ Limitations, stated plainly
|
|
|
59
63
|
changes nothing except that it does not raise. Output is identical either way — blitting
|
|
60
64
|
is an optimisation in matplotlib, not a correctness flag — so ignoring it cannot make a
|
|
61
65
|
saved animation wrong. It is recorded in ``ani._blit`` if you want to assert on it.
|
|
62
|
-
* **
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
``
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
* **Live on-screen playback rides the engine's frame callback, not a real timer.**
|
|
67
|
+
``GPULinePlot.run()`` owns its main loop and has no toolkit timer for a
|
|
68
|
+
``TimedAnimation`` to hook the way matplotlib's does — but it does expose
|
|
69
|
+
``add_frame_callback`` (:meth:`glplot.engine.GPULinePlot.add_frame_callback`), a per-frame
|
|
70
|
+
hook driven by wall-clock time, and :class:`TimedAnimation` uses exactly that to step
|
|
71
|
+
itself at ``interval``. A ``FuncAnimation``/``ArtistAnimation`` built against a live
|
|
72
|
+
GLPlot figure therefore *does* play when you call ``plt.show()`` — no ``draw_event``, no
|
|
73
|
+
blitting, just the same full-scene re-render :meth:`Animation.save` uses, paced in real
|
|
74
|
+
time instead of frame-by-frame. :meth:`Animation.pause` / :meth:`Animation.resume` stop and
|
|
75
|
+
restart that stepping. A real matplotlib ``Figure`` is untouched by any of this: it keeps
|
|
76
|
+
using its own canvas and timer, exactly as before.
|
|
69
77
|
* **In-place array mutation is assumed.** A user callback that writes into
|
|
70
78
|
``layer.pts[:, 1]`` leaves no trace numpy can report, so after every callback this module
|
|
71
79
|
marks every layer dirty rather than trying to detect what changed. Cheap, and the only
|
|
@@ -85,7 +93,9 @@ import itertools
|
|
|
85
93
|
import json
|
|
86
94
|
import os
|
|
87
95
|
import shutil
|
|
96
|
+
import sys
|
|
88
97
|
import tempfile
|
|
98
|
+
import threading
|
|
89
99
|
import uuid
|
|
90
100
|
import warnings
|
|
91
101
|
from pathlib import Path
|
|
@@ -275,46 +285,152 @@ def figure_to_rgb(fig: Any, dpi: Optional[float] = None) -> np.ndarray:
|
|
|
275
285
|
|
|
276
286
|
The single bridge between "a figure" and "a frame", and the only function in this
|
|
277
287
|
module that knows a GLPlot engine from a matplotlib one. See the module docstring for
|
|
278
|
-
the two GLPlot render modes and
|
|
288
|
+
the two GLPlot render modes, and :func:`_render_glplot_frame` for which of them still
|
|
289
|
+
goes through a temporary PNG and which does not.
|
|
279
290
|
|
|
280
291
|
Raises :class:`RuntimeError` naming the figure type if neither renderer applies —
|
|
281
292
|
passing something that is not a figure at all is otherwise diagnosed several frames
|
|
282
293
|
later as a confusing Pillow error.
|
|
283
294
|
"""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if _is_mpl_figure(fig):
|
|
295
|
+
if _is_mpl_figure(fig):
|
|
296
|
+
with tempfile.TemporaryDirectory(prefix="glplot-anim-") as tmpdir:
|
|
297
|
+
target = os.path.join(tmpdir, "frame.png")
|
|
288
298
|
fig.savefig(target, dpi=dpi, format="png")
|
|
289
|
-
|
|
290
|
-
|
|
299
|
+
return anim_export.normalize_frame(_read_png(target))
|
|
300
|
+
|
|
301
|
+
if hasattr(fig, "scene"):
|
|
302
|
+
return anim_export.normalize_frame(_render_glplot_frame(fig, _dpi_to_scale(dpi)))
|
|
303
|
+
|
|
304
|
+
raise RuntimeError(
|
|
305
|
+
f"Cannot grab a frame from {type(fig).__name__}: expected a "
|
|
306
|
+
f"glplot.engine.GPULinePlot (what glplot.pyplot.figure() returns) or a "
|
|
307
|
+
f"matplotlib Figure. Pass the figure object itself, not an axes or a "
|
|
308
|
+
f"layer."
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _normalize_frame_renderer(renderer: Any) -> Optional[str]:
|
|
313
|
+
"""Validate the GLPlot-only renderer option carried inside ``savefig_kwargs``."""
|
|
314
|
+
if renderer is None:
|
|
315
|
+
return None
|
|
316
|
+
value = str(renderer).strip().lower()
|
|
317
|
+
if value not in {"auto", "opengl", "matplotlib"}:
|
|
318
|
+
raise ValueError(
|
|
319
|
+
"animation renderer must be 'auto', 'opengl', or 'matplotlib'; "
|
|
320
|
+
f"got {renderer!r}"
|
|
321
|
+
)
|
|
322
|
+
return value
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@contextlib.contextmanager
|
|
326
|
+
def _frame_renderer_override(fig: Any, renderer: str) -> Iterator[None]:
|
|
327
|
+
"""Temporarily select the route used by :func:`figure_to_rgb`."""
|
|
328
|
+
missing = object()
|
|
329
|
+
previous = getattr(fig, "_animation_renderer_override", missing)
|
|
330
|
+
fig._animation_renderer_override = renderer
|
|
331
|
+
try:
|
|
332
|
+
yield
|
|
333
|
+
finally:
|
|
334
|
+
if previous is missing:
|
|
335
|
+
with contextlib.suppress(AttributeError):
|
|
336
|
+
del fig._animation_renderer_override
|
|
291
337
|
else:
|
|
338
|
+
fig._animation_renderer_override = previous
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@contextlib.contextmanager
|
|
342
|
+
def _animation_render_context(fig: Any, renderer: Optional[str]) -> Iterator[None]:
|
|
343
|
+
"""Keep the selected animation renderer ready for one complete save operation.
|
|
344
|
+
|
|
345
|
+
``None`` preserves the historical behaviour: use GL only if the caller already opened
|
|
346
|
+
a context, otherwise use matplotlib. Explicit ``auto`` is opt-in and tries to create an
|
|
347
|
+
invisible context once before falling back. Explicit ``opengl`` reports setup failure
|
|
348
|
+
instead of silently producing a visually different animation.
|
|
349
|
+
"""
|
|
350
|
+
renderer = _normalize_frame_renderer(renderer)
|
|
351
|
+
|
|
352
|
+
if not hasattr(fig, "scene"):
|
|
353
|
+
if renderer == "opengl":
|
|
354
|
+
raise ValueError("renderer='opengl' requires a GLPlot figure")
|
|
355
|
+
yield
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
if renderer is None:
|
|
359
|
+
with _frame_renderer_override(fig, "auto"):
|
|
360
|
+
yield
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
if renderer == "matplotlib":
|
|
364
|
+
with _frame_renderer_override(fig, "matplotlib"):
|
|
365
|
+
yield
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
setup_error: Optional[BaseException] = None
|
|
369
|
+
stack = contextlib.ExitStack()
|
|
370
|
+
try:
|
|
371
|
+
if (
|
|
372
|
+
getattr(fig, "window", None) is None
|
|
373
|
+
and sys.platform == "darwin"
|
|
374
|
+
and threading.current_thread() is not threading.main_thread()
|
|
375
|
+
):
|
|
292
376
|
raise RuntimeError(
|
|
293
|
-
|
|
294
|
-
f"glplot.engine.GPULinePlot (what glplot.pyplot.figure() returns) or a "
|
|
295
|
-
f"matplotlib Figure. Pass the figure object itself, not an axes or a "
|
|
296
|
-
f"layer."
|
|
377
|
+
"macOS requires creation of the GLFW/OpenGL context on the main thread"
|
|
297
378
|
)
|
|
298
379
|
|
|
299
|
-
|
|
380
|
+
context_factory = getattr(fig, "_offscreen_export_context", None)
|
|
381
|
+
if not callable(context_factory):
|
|
382
|
+
raise RuntimeError("this GLPlot figure cannot create an offscreen OpenGL context")
|
|
383
|
+
stack.enter_context(context_factory())
|
|
384
|
+
except Exception as exc:
|
|
385
|
+
setup_error = exc
|
|
386
|
+
stack.close()
|
|
387
|
+
|
|
388
|
+
if setup_error is not None:
|
|
389
|
+
if renderer == "opengl":
|
|
390
|
+
raise RuntimeError(
|
|
391
|
+
"Could not create the OpenGL context requested for animation export"
|
|
392
|
+
) from setup_error
|
|
393
|
+
warnings.warn(
|
|
394
|
+
"OpenGL animation export is unavailable; falling back to the matplotlib "
|
|
395
|
+
f"renderer ({setup_error}).",
|
|
396
|
+
RuntimeWarning,
|
|
397
|
+
stacklevel=3,
|
|
398
|
+
)
|
|
399
|
+
with _frame_renderer_override(fig, "matplotlib"):
|
|
400
|
+
yield
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
with stack:
|
|
404
|
+
with _frame_renderer_override(fig, "opengl"):
|
|
405
|
+
yield
|
|
406
|
+
|
|
300
407
|
|
|
408
|
+
def _render_glplot_frame(fig: Any, scale: float) -> np.ndarray:
|
|
409
|
+
"""Render one GLPlot frame through the selected OpenGL or matplotlib route.
|
|
301
410
|
|
|
302
|
-
|
|
303
|
-
|
|
411
|
+
**Headless** does not: :func:`glplot.utils.preview.render_preview_array` hands back the
|
|
412
|
+
rendered pixels directly from matplotlib's Agg canvas, skipping the PNG encode and the
|
|
413
|
+
disk write/read a file-based caller needs. Profiled: PIL's PNG encoder alone cost as much
|
|
414
|
+
as matplotlib's entire draw, so for an animation's hundreds of headless frames — the
|
|
415
|
+
common case, since a saved animation is usually built without ever calling ``fig.run()``
|
|
416
|
+
— this removes roughly a third of each frame's cost with no change in what gets drawn.
|
|
304
417
|
|
|
305
|
-
``ExportManager.
|
|
306
|
-
|
|
307
|
-
swallowed for the duration — this is the only place that print reaches, and suppressing
|
|
308
|
-
it here beats every alternative that would require editing the engine.
|
|
418
|
+
The OpenGL branch calls ``ExportManager.render_rgb`` so it shares the export pipeline's
|
|
419
|
+
panel, scissor, projection, depth and shader logic without touching the filesystem.
|
|
309
420
|
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
421
|
+
renderer = getattr(fig, "_animation_renderer_override", "auto")
|
|
422
|
+
if renderer != "matplotlib" and getattr(fig, "window", None) is not None:
|
|
423
|
+
export = getattr(fig, "export", None)
|
|
424
|
+
render_rgb = getattr(export, "render_rgb", None)
|
|
425
|
+
if callable(render_rgb):
|
|
426
|
+
return render_rgb(scale=scale)
|
|
427
|
+
|
|
428
|
+
if renderer == "opengl":
|
|
429
|
+
raise RuntimeError("OpenGL animation rendering was requested, but no context is active")
|
|
314
430
|
|
|
315
|
-
from .utils.preview import
|
|
431
|
+
from .utils.preview import render_preview_array
|
|
316
432
|
|
|
317
|
-
|
|
433
|
+
return render_preview_array(fig, scale)
|
|
318
434
|
|
|
319
435
|
|
|
320
436
|
def _read_png(path: str) -> np.ndarray:
|
|
@@ -419,9 +535,9 @@ class _NullTimer:
|
|
|
419
535
|
|
|
420
536
|
:class:`TimedAnimation` needs *an* event source; it does not need one that works in
|
|
421
537
|
order for :meth:`Animation.save` to render every frame, because saving drives the frame
|
|
422
|
-
sequence directly and never consults the timer. Live playback
|
|
423
|
-
|
|
424
|
-
|
|
538
|
+
sequence directly and never consults the timer. Live playback does not go through this
|
|
539
|
+
timer either -- :meth:`TimedAnimation._start_live_playback` drives it straight off
|
|
540
|
+
``fig.add_frame_callback`` instead, since that is the hook the engine actually has.
|
|
425
541
|
"""
|
|
426
542
|
|
|
427
543
|
def __init__(self, interval: Optional[float] = None, callbacks: Any = None) -> None:
|
|
@@ -1195,9 +1311,9 @@ class Animation:
|
|
|
1195
1311
|
"""Base class for animations, with matplotlib's constructor and public methods.
|
|
1196
1312
|
|
|
1197
1313
|
Subclasses supply the frame data (:meth:`new_frame_seq`) and what to do with one
|
|
1198
|
-
(:meth:`_draw_frame`); this class supplies :meth:`save` and the HTML exports
|
|
1199
|
-
|
|
1200
|
-
|
|
1314
|
+
(:meth:`_draw_frame`); this class supplies :meth:`save` and the HTML exports. Live
|
|
1315
|
+
playback (:class:`TimedAnimation`'s ``fig.add_frame_callback`` hook, module docstring)
|
|
1316
|
+
is the other consumer of that same frame-stepping machinery.
|
|
1201
1317
|
"""
|
|
1202
1318
|
|
|
1203
1319
|
def __init__(self, fig: Any, event_source: Any = None, blit: bool = False) -> None:
|
|
@@ -1317,8 +1433,10 @@ class Animation:
|
|
|
1317
1433
|
twice the default size. See :func:`figure_pixel_size` for what that works out to.
|
|
1318
1434
|
|
|
1319
1435
|
``extra_anim`` is honoured: the animations are stepped together, one saved frame per
|
|
1320
|
-
tick, as in matplotlib. ``savefig_kwargs``
|
|
1321
|
-
``
|
|
1436
|
+
tick, as in matplotlib. Most ``savefig_kwargs`` are accepted for compatibility and
|
|
1437
|
+
ignored; GLPlot additionally consumes ``renderer`` with values ``"opengl"``,
|
|
1438
|
+
``"matplotlib"`` or ``"auto"``. Omitting it preserves the historical behaviour:
|
|
1439
|
+
an already-live GL context is used, otherwise rendering stays headless through Agg.
|
|
1322
1440
|
"""
|
|
1323
1441
|
all_anim: List[Animation] = [self]
|
|
1324
1442
|
if extra_anim is not None:
|
|
@@ -1348,6 +1466,7 @@ class Animation:
|
|
|
1348
1466
|
else:
|
|
1349
1467
|
savefig_kwargs = dict(savefig_kwargs)
|
|
1350
1468
|
savefig_kwargs.pop("bbox_inches", None)
|
|
1469
|
+
renderer = _normalize_frame_renderer(savefig_kwargs.pop("renderer", None))
|
|
1351
1470
|
|
|
1352
1471
|
if fps is None and hasattr(self, "_interval"):
|
|
1353
1472
|
fps = MS_PER_SECOND / float(self._interval)
|
|
@@ -1367,17 +1486,18 @@ class Animation:
|
|
|
1367
1486
|
writer = writer_cls(fps if fps is not None else 5, **writer_kwargs)
|
|
1368
1487
|
|
|
1369
1488
|
total_frames = _total_frames(all_anim)
|
|
1370
|
-
with
|
|
1371
|
-
|
|
1372
|
-
anim
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
for anim
|
|
1376
|
-
anim
|
|
1377
|
-
|
|
1378
|
-
progress_callback
|
|
1379
|
-
|
|
1380
|
-
|
|
1489
|
+
with _animation_render_context(self._fig, renderer):
|
|
1490
|
+
with writer.saving(self._fig, str(filename), dpi):
|
|
1491
|
+
for anim in all_anim:
|
|
1492
|
+
anim._init_draw()
|
|
1493
|
+
frame_number = 0
|
|
1494
|
+
for data in zip(*[anim.new_saved_frame_seq() for anim in all_anim]):
|
|
1495
|
+
for anim, datum in zip(all_anim, data):
|
|
1496
|
+
anim._draw_next_frame(datum, blit=False)
|
|
1497
|
+
if progress_callback is not None:
|
|
1498
|
+
progress_callback(frame_number, total_frames)
|
|
1499
|
+
frame_number += 1
|
|
1500
|
+
writer.grab_frame(**savefig_kwargs)
|
|
1381
1501
|
|
|
1382
1502
|
# -- HTML ------------------------------------------------------------------------
|
|
1383
1503
|
|
|
@@ -1557,10 +1677,30 @@ class TimedAnimation(Animation):
|
|
|
1557
1677
|
|
|
1558
1678
|
``interval`` (milliseconds per frame) is what :meth:`Animation.save` turns into ``fps``
|
|
1559
1679
|
when the caller does not pass one, which is why ``FuncAnimation(..., interval=20)``
|
|
1560
|
-
followed by a bare ``save()`` produces a 50 fps file.
|
|
1561
|
-
|
|
1680
|
+
followed by a bare ``save()`` produces a 50 fps file.
|
|
1681
|
+
|
|
1682
|
+
It is also what makes live on-screen playback possible, and *controllable*: when ``fig``
|
|
1683
|
+
is a GLPlot figure (no real matplotlib canvas), :meth:`_start_live_playback` hands this
|
|
1684
|
+
animation's frames to ``fig.active_panel.timeline`` -- the same playhead the GUI's
|
|
1685
|
+
Timeline panel already has Play/Pause/Stop buttons and a scrub bar for -- instead of
|
|
1686
|
+
running its own independent clock. ``plt.show()`` after building a ``FuncAnimation``
|
|
1687
|
+
plays it, and if the Timeline panel is open, its transport controls drive this animation
|
|
1688
|
+
exactly as they drive a hand-keyed one. See :meth:`_start_live_playback`.
|
|
1562
1689
|
"""
|
|
1563
1690
|
|
|
1691
|
+
#: A frame sequence at most this long is materialised into a concrete list so the
|
|
1692
|
+
#: Timeline's scrub bar can jump to any frame directly. Past this (or for an unbounded
|
|
1693
|
+
#: ``frames=None`` generator), only forward playback is exact -- see
|
|
1694
|
+
#: :meth:`_materialize_frames`.
|
|
1695
|
+
_MAX_SEEKABLE_FRAMES = 100_000
|
|
1696
|
+
|
|
1697
|
+
#: Duration assigned to the panel's timeline when the frame count is unknowable
|
|
1698
|
+
#: (``frames=None``) or too large to materialise. Arbitrary but not consequential: it
|
|
1699
|
+
#: only sets how far the scrub bar's grid extends before it loops, not how long the
|
|
1700
|
+
#: animation itself runs, since :meth:`_start_live_playback`'s callback keeps stepping
|
|
1701
|
+
#: forward regardless of where in that grid the playhead sits.
|
|
1702
|
+
_UNKNOWN_DURATION_SECONDS = 60.0
|
|
1703
|
+
|
|
1564
1704
|
def __init__(
|
|
1565
1705
|
self,
|
|
1566
1706
|
fig: Any,
|
|
@@ -1576,16 +1716,140 @@ class TimedAnimation(Animation):
|
|
|
1576
1716
|
self._repeat_delay = repeat_delay if repeat_delay is not None else 0
|
|
1577
1717
|
self._repeat = repeat
|
|
1578
1718
|
self._paused = False
|
|
1719
|
+
self._live_timeline: Any = None
|
|
1579
1720
|
if event_source is None:
|
|
1580
1721
|
canvas = _canvas_for(fig)
|
|
1581
1722
|
event_source = canvas.new_timer(interval=self._interval)
|
|
1582
1723
|
super().__init__(fig, event_source=event_source, *args, **kwargs)
|
|
1724
|
+
self._start_live_playback(fig)
|
|
1583
1725
|
|
|
1584
1726
|
@property
|
|
1585
1727
|
def repeat(self) -> bool:
|
|
1586
1728
|
"""Whether the animation loops. Read-only, as in matplotlib."""
|
|
1587
1729
|
return self._repeat
|
|
1588
1730
|
|
|
1731
|
+
def pause(self) -> None:
|
|
1732
|
+
"""Pause the animation.
|
|
1733
|
+
|
|
1734
|
+
Proxies to ``fig.active_panel.timeline.pause()`` when live playback is bound (see
|
|
1735
|
+
:meth:`_start_live_playback`), so this has the same effect as pressing Pause in the
|
|
1736
|
+
Timeline panel. Otherwise a documented no-op, as it always was; see the module
|
|
1737
|
+
docstring.
|
|
1738
|
+
"""
|
|
1739
|
+
self._paused = True
|
|
1740
|
+
if self._live_timeline is not None:
|
|
1741
|
+
self._live_timeline.pause()
|
|
1742
|
+
if self.event_source is not None:
|
|
1743
|
+
with contextlib.suppress(Exception):
|
|
1744
|
+
self.event_source.stop()
|
|
1745
|
+
|
|
1746
|
+
def resume(self) -> None:
|
|
1747
|
+
"""Resume the animation. See :meth:`pause`; proxies to ``timeline.play()`` likewise."""
|
|
1748
|
+
self._paused = False
|
|
1749
|
+
if self._live_timeline is not None:
|
|
1750
|
+
self._live_timeline.play()
|
|
1751
|
+
if self.event_source is not None:
|
|
1752
|
+
with contextlib.suppress(Exception):
|
|
1753
|
+
self.event_source.start()
|
|
1754
|
+
|
|
1755
|
+
def _materialize_frames(self) -> Optional[List[Any]]:
|
|
1756
|
+
"""This animation's frame data as a concrete, randomly-indexable list, or ``None``.
|
|
1757
|
+
|
|
1758
|
+
``None`` means the Timeline can only step forward through the sequence (an
|
|
1759
|
+
unbounded ``frames=None`` generator, or a declared length past
|
|
1760
|
+
:data:`_MAX_SEEKABLE_FRAMES`) -- scrubbing backward then restarts it from frame 0
|
|
1761
|
+
rather than landing on the exact frame, since an arbitrary generator cannot be
|
|
1762
|
+
rewound. Every other case -- :class:`ArtistAnimation` (already a concrete list),
|
|
1763
|
+
an integer or sized ``frames=``, an explicit ``save_count`` -- seeks exactly,
|
|
1764
|
+
because the whole sequence is known and cheap to hold: it is usually the caller's
|
|
1765
|
+
own list or array already sitting in memory, and ``new_frame_seq()`` just re-reads
|
|
1766
|
+
it rather than duplicating anything expensive.
|
|
1767
|
+
"""
|
|
1768
|
+
existing = getattr(self, "_framedata", None)
|
|
1769
|
+
if isinstance(existing, (list, tuple)):
|
|
1770
|
+
return list(existing)
|
|
1771
|
+
save_count = getattr(self, "_save_count", None)
|
|
1772
|
+
if save_count is not None and 0 < save_count <= self._MAX_SEEKABLE_FRAMES:
|
|
1773
|
+
return list(itertools.islice(self.new_frame_seq(), save_count))
|
|
1774
|
+
return None
|
|
1775
|
+
|
|
1776
|
+
def _start_live_playback(self, fig: Any) -> None:
|
|
1777
|
+
"""Bind this animation's frames to ``fig.active_panel.timeline``'s playhead.
|
|
1778
|
+
|
|
1779
|
+
A real matplotlib ``Figure`` already has a toolkit timer driving it -- ``self._canvas``
|
|
1780
|
+
is the real canvas in that case (:func:`_canvas_for`), so this does nothing and that
|
|
1781
|
+
timer keeps doing its job unmodified. Only a GLPlot figure (``_NullCanvas``) with a
|
|
1782
|
+
panel to bind to gets this treatment.
|
|
1783
|
+
|
|
1784
|
+
The bound timeline is configured to this animation's own cadence (``fps = 1000 /
|
|
1785
|
+
interval``, ``duration`` from the frame count or :data:`_UNKNOWN_DURATION_SECONDS`
|
|
1786
|
+
when that is not knowable, ``loop`` from ``repeat``) and started playing -- an
|
|
1787
|
+
animation you just built plays immediately, same as before. From there the engine's
|
|
1788
|
+
own per-frame hook (``fig.add_frame_callback``, invoked *after*
|
|
1789
|
+
``GPULinePlot._advance_timelines`` already moved the playhead this frame -- see
|
|
1790
|
+
``engine.py``) does one thing every frame: compute which frame the playhead's
|
|
1791
|
+
*current* time corresponds to, and draw it if that differs from what is already on
|
|
1792
|
+
screen. It does not advance the clock itself. That is deliberate: Play/Pause only
|
|
1793
|
+
has to flip ``timeline.playing`` (which gates whether the playhead moves at all) and
|
|
1794
|
+
Stop/seek/step only have to move ``timeline.time`` directly (which the GUI's
|
|
1795
|
+
transport buttons already do) for this animation to follow suit exactly the way a
|
|
1796
|
+
hand-keyed one does -- there is no separate "is it playing" state here to keep in
|
|
1797
|
+
sync with the Timeline panel's.
|
|
1798
|
+
"""
|
|
1799
|
+
register = getattr(fig, "add_frame_callback", None)
|
|
1800
|
+
panel = getattr(fig, "active_panel", None)
|
|
1801
|
+
timeline = getattr(panel, "timeline", None)
|
|
1802
|
+
if not callable(register) or not isinstance(self._canvas, _NullCanvas) or timeline is None:
|
|
1803
|
+
return
|
|
1804
|
+
|
|
1805
|
+
frames = self._materialize_frames()
|
|
1806
|
+
interval_s = max(float(self._interval), 1.0) / MS_PER_SECOND
|
|
1807
|
+
duration = (
|
|
1808
|
+
max(len(frames) - 1, 0) * interval_s if frames else self._UNKNOWN_DURATION_SECONDS
|
|
1809
|
+
)
|
|
1810
|
+
timeline.set_fps(1.0 / interval_s)
|
|
1811
|
+
if timeline.is_empty():
|
|
1812
|
+
# No hand-authored keyframes here yet -- the overwhelmingly common case for a
|
|
1813
|
+
# freshly-built animation -- so this animation may as well own the grid exactly:
|
|
1814
|
+
# duration matches its real length, and the scrub bar has nothing past the last
|
|
1815
|
+
# frame to run into. A timeline that already has keyframes on it keeps its own
|
|
1816
|
+
# length (never shrinks) rather than have this animation truncate them.
|
|
1817
|
+
timeline.set_duration(duration)
|
|
1818
|
+
else:
|
|
1819
|
+
timeline.fit_duration(minimum=duration)
|
|
1820
|
+
timeline.set_loop("loop" if self._repeat else "once")
|
|
1821
|
+
timeline.play()
|
|
1822
|
+
self._live_timeline = timeline
|
|
1823
|
+
register_timeline = getattr(fig, "register_live_animation_timeline", None)
|
|
1824
|
+
if callable(register_timeline):
|
|
1825
|
+
register_timeline(timeline)
|
|
1826
|
+
|
|
1827
|
+
live_seq = {"iter": self.frame_seq}
|
|
1828
|
+
state = {"last_index": None}
|
|
1829
|
+
|
|
1830
|
+
def _tick(now: float) -> None:
|
|
1831
|
+
index = timeline.frame_index()
|
|
1832
|
+
if index == state["last_index"]:
|
|
1833
|
+
return
|
|
1834
|
+
if frames is not None:
|
|
1835
|
+
framedata = frames[min(index, len(frames) - 1)]
|
|
1836
|
+
else:
|
|
1837
|
+
if state["last_index"] is not None and index < state["last_index"]:
|
|
1838
|
+
live_seq["iter"] = self.new_frame_seq() # scrubbed back / looped: restart
|
|
1839
|
+
try:
|
|
1840
|
+
framedata = next(live_seq["iter"])
|
|
1841
|
+
except StopIteration:
|
|
1842
|
+
if not self._repeat:
|
|
1843
|
+
fig.remove_frame_callback(_tick)
|
|
1844
|
+
return
|
|
1845
|
+
live_seq["iter"] = self.new_frame_seq()
|
|
1846
|
+
framedata = next(live_seq["iter"])
|
|
1847
|
+
state["last_index"] = index
|
|
1848
|
+
self._draw_was_started = True
|
|
1849
|
+
self._draw_next_frame(framedata, blit=False)
|
|
1850
|
+
|
|
1851
|
+
register(_tick)
|
|
1852
|
+
|
|
1589
1853
|
|
|
1590
1854
|
class FuncAnimation(TimedAnimation):
|
|
1591
1855
|
"""Animation built by repeatedly calling *func*. The class almost everyone uses.
|