Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Core modules
.. automodule:: tachypy.scrollbar
:members:

Scrollbar interaction
---------------------

.. automodule:: tachypy.scrollbar_interaction
:members:

.. automodule:: tachypy.psychophysics
:members:

Expand Down
6 changes: 0 additions & 6 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ abstractions for display/input, and helper utilities for experiment workflows.
timing_validation
text_rendering
audio
scrollbar
wooting
examples
contributing
Expand Down
88 changes: 88 additions & 0 deletions docs/scrollbar.rst
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 1 addition & 1 deletion docs/text_rendering.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
160 changes: 145 additions & 15 deletions docs/wooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------
Expand All @@ -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

Expand All @@ -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
----------------
Expand Down Expand Up @@ -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<gamma<1`` is more responsive. ``gamma=0`` is an
on/off step and is rejected. The default is ``pressure_gamma=2.5``.

``X`` cannot confirm while ``Z`` or ``C`` is active. Confirmation is
edge-triggered, and all three keys must be released below ``release_threshold``
before the next trial is armed.

Input modes and customization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The default ``input_mode="keyboard"`` uses analog ``Z``/``C`` movement and
``X`` confirmation. Use ``input_mode="mouse_keyboard"`` to allow both mouse
and analog-key movement, with either a mouse click or ``X`` for confirmation:

.. code-block:: python

value, reaction_time = acq.interact_slider(
slider=scrollbar,
screen=screen,
input_mode="mouse_keyboard",
confirm_key="x",
)

The cursor is hidden and recentered at screen edges. A left click or ``X``
confirms after ``mouse_quiet_period`` seconds without mouse movement. Pressure
on any analog key temporarily gives the keyboard exclusive control. ``X``
must be released and pressed again if it was held during mouse movement.

The interaction loop does not recreate the scrollbar, so all of its visual
customization remains available. Pass ``drawables`` for instruction text or
other TachyPy objects, and tune ``initial_value``, ``movement_speed``,
``pressure_deadzone``, and ``pressure_gamma`` as needed.

Demo and API
~~~~~~~~~~~~~~~~~~~~~

Try the three-trial demo with:

.. code-block:: bash

tachypy-wooting-slider-demo

The complete API, including the keyboard-agnostic
:func:`~tachypy.scrollbar_interaction.run_slider_interaction`, is documented in
:mod:`tachypy.scrollbar_interaction`.

Logging and a full experiment loop
----------------------------------

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test =
pytest-cov>=5.0
ruff>=0.6
wooting =
tachywooting>=0.2.2
tachywooting>=0.2.4

[options.entry_points]
console_scripts =
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
'system_text': ['freetype-py>=2.4', 'uharfbuzz>=0.39'],
'glfw': ['glfw>=2.7'],
'audio': [],
'wooting': ['tachywooting>=0.2.2'],
'wooting': ['tachywooting>=0.2.4'],
},
python_requires='>=3.10',
author='Ian Charest, Mathias Salvas-Hebert and Frederic Gosselin',
Expand All @@ -41,6 +41,7 @@
'tachypy-clock-demo=tachypy.examples.clock_timer_demo:main',
'tachypy-wooting-fixation-demo=tachypy.wooting.demos.visual_fixation_demo:main',
'tachypy-wooting-mini-bw=tachypy.wooting.demos.mini_bw_experiment:main',
'tachypy-wooting-slider-demo=tachypy.wooting.demos.slider_demo:main',
],
},
)
Loading
Loading