diff --git a/README.md b/README.md index 45074ac..381afbb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # TachyPy +[![CI](https://github.com/Charestlab/tachypy/actions/workflows/ci.yml/badge.svg)](https://github.com/Charestlab/tachypy/actions/workflows/ci.yml) +[![PyPI version](https://img.shields.io/pypi/v/tachypy.svg)](https://pypi.org/project/tachypy/) +[![Python versions](https://img.shields.io/pypi/pyversions/tachypy.svg)](https://pypi.org/project/tachypy/) [![Docs Status](https://readthedocs.org/projects/tachypy/badge/?version=latest)](https://tachypy.readthedocs.io/en/latest/?badge=latest) +[![License](https://img.shields.io/github/license/Charestlab/tachypy.svg)](https://github.com/Charestlab/tachypy/blob/main/LICENSE) TachyPy is a psychophysics engine for Python focused on precise visual timing with OpenGL rendering, a GLFW-first display/input backend, and experiment-friendly @@ -53,7 +57,8 @@ software timestamps alone. - Psychophysics helpers (`make_gabor`, gratings, normalization, dithering). - Audio playback utility (`Audio`) backed by `tachyaudio`. - Optional Wooting analog-keyboard integration (`tachypy[wooting]`): on-screen - pressure feedback and `WOOTING_ACQUISITION` straight from `tachypy`. + pressure feedback, analog scrollbar interaction, and + `WOOTING_ACQUISITION` straight from `tachypy`. - Test suite for core logic and regressions. ## Installation @@ -88,7 +93,8 @@ pip install -e ".[wooting]" # Wooting analog-keyboard integration ### Wooting analog-keyboard integration `pip install "tachypy[wooting]"` adds support for Wooting analog keyboards -(pressure acquisition, logging, and on-screen visual feedback): +(pressure acquisition, logging, visual feedback, and analog scrollbar +interaction): ```python from tachypy import Screen, WOOTING_ACQUISITION @@ -183,7 +189,7 @@ TACHYPY_FONT="Avenir Next, Helvetica, Arial" python example_tachypy.py - `GLSystemText` supports system font selection by family name, fallback list (e.g. `"Avenir Next, Helvetica, Arial"`), or direct font file path. - For production instruction text, prefer `Text`. -- The old texture-backed constructor is backbenched as `tachypy.text.LegacyText`. +- The old texture-backed constructor is retained as `tachypy.text.LegacyText`. ## API Naming diff --git a/docs/api.rst b/docs/api.rst index ba83517..88555bb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -35,6 +35,12 @@ Core modules .. automodule:: tachypy.scrollbar :members: +Scrollbar interaction +--------------------- + +.. automodule:: tachypy.scrollbar_interaction + :members: + .. automodule:: tachypy.psychophysics :members: diff --git a/docs/examples.rst b/docs/examples.rst index 6d48bdf..b284743 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -29,12 +29,6 @@ Use ``Esc`` to quit, click ``START``/``STOP``/``RESET``, or use ``Space`` and tachypy-clock-demo --windowed -Run the default demo: - -.. code-block:: bash - - python example_tachypy.py - Notes ----- diff --git a/docs/index.rst b/docs/index.rst index 17f1975..0ce2f67 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ abstractions for display/input, and helper utilities for experiment workflows. timing_validation text_rendering audio + scrollbar wooting examples contributing diff --git a/docs/scrollbar.rst b/docs/scrollbar.rst new file mode 100644 index 0000000..a191664 --- /dev/null +++ b/docs/scrollbar.rst @@ -0,0 +1,88 @@ +Scrollbar widget +================ + +TachyPy's :class:`~tachypy.scrollbar.Scrollbar` is a customizable visual +widget for selecting a continuous value, ``0``–``100`` by default. It is +independent of the input device: the same widget can be controlled with the +mouse, an analog keyboard, or another custom interaction loop. + +Normal mouse use +---------------- + +Create the scrollbar with the display dimensions, draw it every frame, and +pass the current mouse position to ``handle_mouse``. The value is selected when +a mouse button is released: + +.. code-block:: python + + from tachypy import ResponseHandler, Screen, Scrollbar + + screen = Screen(fullscreen=False) + responses = ResponseHandler(screen=screen) + scrollbar = Scrollbar( + screen_width=screen.width, + screen_height=screen.height, + position_y=screen.height / 2, + half_bar_length=350, + num_marks=11, + text_left="0", + text_right="100", + content_scale=screen.content_scale, + ) + + value = None + while value is None: + responses.get_events() + if responses.should_quit(): + break + + scrollbar.handle_mouse(*responses.get_mouse_position()) + screen.fill((128, 128, 128)) + scrollbar.draw() + screen.flip() + + for click in responses.get_mouse_clicks(): + if click["type"] == "mouseup": + value = scrollbar.get_value() + + screen.close() + +The widget's value can also be controlled directly: + +.. code-block:: python + + scrollbar.set_value(50) # choose an initial/current value + current = scrollbar.get_value() + +Customization +------------- + +The constructor keeps the appearance and geometry of the scrollbar explicit. +Common options include: + +* ``half_bar_length``, ``bar_thickness`` and ``bar_color`` for the main bar; +* ``num_marks``, ``mark_thickness`` and ``mark_color`` for tick marks; +* ``text_left``, ``text_right``, ``font_name``, ``font_size`` and + ``text_color`` for endpoint labels; +* ``half_end_height``, ``end_thickness`` and ``end_color`` for the endpoints; +* ``limit_mouse`` to require the cursor to stay near the bar's horizontal line; +* ``content_scale=screen.content_scale`` for sharp labels on Retina/HiDPI + displays. + +For the complete constructor reference, see the +:class:`~tachypy.scrollbar.Scrollbar` API documentation. + +Analog keyboard interaction +---------------------------- + +Analog keyboard controls are documented with the Wooting integration because +they include key roles, pressure-to-speed mapping, confirmation safety, Wooting +key validation, and keyboard/mouse modes. The interaction layer keeps this +widget unchanged and simply drives its existing ``set_value``/``draw`` API: + +.. seealso:: + + :doc:`wooting` + +The generic, keyboard-agnostic API is documented in +:mod:`tachypy.scrollbar_interaction`. diff --git a/docs/text_rendering.rst b/docs/text_rendering.rst index 37debc9..851e50e 100644 --- a/docs/text_rendering.rst +++ b/docs/text_rendering.rst @@ -28,7 +28,7 @@ Recommended usage - Use ``Text`` for high-quality instruction screens and overlays. - Use ``GLSystemText`` only when you want the explicit historical class name. - Use ``GLTextSDF`` when scalable text quality matters and shaping is simple. -- The old Pillow texture-backed constructor is backbenched as +- The old Pillow texture-backed constructor is retained as ``tachypy.text.LegacyText`` for compatibility. HiDPI and Retina displays diff --git a/docs/wooting.rst b/docs/wooting.rst index 4063fa0..ea4cb17 100644 --- a/docs/wooting.rst +++ b/docs/wooting.rst @@ -5,9 +5,9 @@ TachyPy integrates with **TachyWooting**, a hardware toolbox for Wooting analog keyboards (analog pressure acquisition, hierarchical HDF5 logging, light-press / release readiness checks). The hardware toolbox is usable on its own; this page documents only what becomes available -**inside TachyPy** once the integration is installed — chiefly on-screen visual -pressure feedback. For the full keyboard/logging reference, see TachyWooting's own -documentation. +**inside TachyPy** once the integration is installed — on-screen visual pressure +feedback and analog scrollbar responses. For the full keyboard/logging +reference, see TachyWooting's own documentation. Installation ------------ @@ -24,8 +24,9 @@ the keyboard through the top-level ``tachypy`` namespace: One import surface ------------------ -The enriched ``WOOTING_ACQUISITION`` — the hardware acquisition class plus TachyPy -visual feedback — is available straight from the top-level package: +The enriched ``WOOTING_ACQUISITION`` — the hardware acquisition class plus +TachyPy visual feedback and analog scrollbar interaction — is available straight +from the top-level package: .. code-block:: python @@ -46,24 +47,28 @@ How the enrichment works ------------------------ ``WOOTING_ACQUISITION`` is enriched in ``tachypy/wooting/__init__.py``: it is a -thin subclass that combines TachyWooting's hardware acquisition class with -:class:`~tachypy.feedback.VisualPressureFeedbackMixin`. The mixin is what adds the -``wait_light_press_visual`` method — nothing else changes: +thin subclass that combines TachyWooting's hardware acquisition class with two +TachyPy mixins. ``VisualPressureFeedbackMixin`` adds +``wait_light_press_visual`` and ``AnalogSliderMixin`` adds ``interact_slider``: .. code-block:: python from tachywooting import WOOTING_ACQUISITION as _BaseAcquisition from tachypy.feedback import VisualPressureFeedbackMixin + from tachypy.scrollbar_interaction import AnalogSliderMixin - class WOOTING_ACQUISITION(_BaseAcquisition, VisualPressureFeedbackMixin): - """Hardware acquisition + logging (base) + TachyPy visual feedback (mixin).""" + class WOOTING_ACQUISITION( + _BaseAcquisition, VisualPressureFeedbackMixin, AnalogSliderMixin + ): + """Hardware acquisition plus TachyPy feedback and slider interaction.""" This keeps the hardware package (TachyWooting) completely free of TachyPy — the -visual method is grafted on here, on TachyPy's side. Because the mixin only relies -on the :class:`~tachypy.feedback.PressureSource` contract (reading pressures plus -the light-press thresholds), the very same pattern enriches any future analog -keyboard: subclass its base acquisition class and mix in -``VisualPressureFeedbackMixin``. +TachyPy features are grafted on here, on the integration side. The visual mixin +relies on the :class:`~tachypy.feedback.PressureSource` contract, while the +slider mixin relies on ``read_pressures(keys)`` and optional +``validate_analog_keys(keys)``. The same pattern can enrich another analog +keyboard by subclassing its acquisition class and selecting the applicable +TachyPy mixins. First-time setup ---------------- @@ -160,6 +165,131 @@ fall outside the acceptable interval. :alt: Interactive fixation cross with real-time pressure feedback :width: 100% +Analog scrollbar responses +-------------------------- + +The Wooting acquisition provides pressure-controlled responses for any +configured :class:`~tachypy.scrollbar.Scrollbar` through ``interact_slider``. +For normal mouse-only use and visual customization, see :doc:`scrollbar`. + +Quick start +~~~~~~~~~~~ + +Pass a normal TachyPy scrollbar to the enriched Wooting acquisition. The +defaults are ``Z`` to decrease, ``C`` to increase, and ``X`` to confirm: + +.. code-block:: python + + from tachypy import Screen, Scrollbar, WOOTING_ACQUISITION + + acq = WOOTING_ACQUISITION() + acq.initialize_keyboard() + screen = Screen(fullscreen=False) + scrollbar = Scrollbar(screen_width=screen.width, screen_height=screen.height, + position_y=screen.height / 2, + content_scale=screen.content_scale) + + try: + value, reaction_time = acq.interact_slider( + slider=scrollbar, + screen=screen, + ) + finally: + acq.uninitialize_keyboard() + screen.close() + +The method returns ``(value, reaction_time)`` on confirmation and +``(None, None)`` when the participant presses ``Escape`` or closes the window. +A default :class:`~tachypy.responses.ResponseHandler` is created automatically. + +Controls and pressure mapping +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The controls are: + +.. list-table:: Default analog controls + :header-rows: 1 + :widths: 18 28 54 + + * - Key + - Role + - Behaviour + * - ``Z`` + - Decrease + - Moves toward the lower end of the scrollbar. Speed depends on pressure. + * - ``C`` + - Increase + - Moves toward the higher end of the scrollbar. Speed depends on pressure. + * - ``X`` + - Confirm + - Selects the current value when its pressure crosses the confirmation threshold. + +Use any three distinct analog keys by passing ``decrease_key``, ``increase_key`` +and ``confirm_key``. TachyPy validates their Wooting analog mappings before the +loop starts and raises if a configured key is unavailable. + +.. code-block:: python + + value, reaction_time = acq.interact_slider( + slider=scrollbar, screen=screen, + decrease_key="a", increase_key="d", confirm_key="space") + +Movement is continuous. For pressure ``p`` above the deadzone ``d``, the +private :func:`~tachypy.scrollbar_interaction._effective_pressure` helper uses: + +.. math:: + + p_{effective} = \left(\frac{p - d}{1 - d}\right)^\gamma + +Pressures at or below ``d`` produce zero movement; the normalized result is +raised to ``pressure_gamma`` and converted to a proportion of +``movement_speed``. ``gamma=1`` is linear after the deadzone, ``gamma>1`` gives +gentler fine control, and ``0