Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

Wheel Progression Lab

Стенд для замера срабатываний колеса мыши и тачпада. Отвечает на один вопрос: как сделать так, чтобы одинаковый физический жест давал одинаковое число срабатываний на любом устройстве.

Один HTML-файл, без зависимостей и сборки. Открывается двойным кликом.


Запуск

Открой index.html в браузере:

open index.html

Ничего ставить и собирать не нужно: ни сборки, ни сервера, ни внешних запросов. Всё работает с file://.


Зачем это

У разных мышей разные контроллеры и разное преобразование поворота колеса в пиксели. Если вешать логику на пройденный путь в пикселях, то на медленной мыши 30° поворота не дадут ничего, а на чувствительной те же 30° дадут несколько срабатываний. Тачпад ломает картину окончательно — он шлёт десятки мелких событий вместо щелчков.

Прогрессия убирает эту зависимость: она смотрит не на путь, а на наличие ускорения в коротком окне. Величина события делится на автоопределяемый шаг устройства, умножается на текущую скорость и копится в заряде с экспоненциальным затуханием. Срабатывание — когда заряд перевалил порог.

Один и тот же физический жест (примерно оборот колеса за ~230 мс) на дефолтных параметрах:

профиль наивно прогрессия
Мышь, 100 px/щелчок 8 4
Мышь, 114 px/щелчок 7 3
Мышь высокой чувствительности 15 4
Свободное колесо (MX Master) 40 4
Тачпад 120 Гц 30 4
Тачпад 240 Гц 60 4
Firefox, deltaMode=1 8 4
контроль: медленное кручение 8 0

Разброс 1 против 53. Медленное вдумчивое кручение на том же пути в 800 px не срабатывает вообще.

Побочный выход — velocity, нормированная скорость в единицах в секунду. Её можно использовать для плавной прокрутки таймлайна, чтобы не было рывков на быстрой мыши и бесконечного скролла на медленной.


Что внутри

1 · Записи реальных жестов. Пишет последовательность (Δpx, Δt) с твоего устройства. Каждая запись показывает результат текущими параметрами и пересчитывается на каждое движение любого ползунка: сколько срабатываний даёт прогрессия, сколько наивная обработка, и разбивка по жестам внутри записи. Когда число меняется, рядом появляется ▲ было 46 — видно направление и величину эффекта, ничего переигрывать не надо.

Кнопка дополнительно прокручивает запись в реальном времени: события подаются по их же интервалам, ровно как при живом кручении, так что графики, вспышки и зелёные метки срабатываний ведут себя один в один. Перед стартом даётся фора 1.2 с, чтобы успеть перевести взгляд на панели; в строке рядом идёт обратный отсчёт, потом проценты и живой счёт. На графиках момент старта помечен вертикальным пунктиром старт. Повторное нажатие () останавливает.

Чипы в шапке (deltaMode, ось, шаг устройства, интервал) описывают живое устройство и при проигрывании записи не трогаются — иначе характеристики файла залипали бы в них как свои. Шаг конкретной записи виден в её строке и в логе жестов.

Записи выгружаются в JSON — сними жест на медленной мыши, на быстрой, на тачпаде, и балансируй сразу по всем трём. Стоит первой секцией, чтобы кнопка записи была под рукой, пока крутишь колесо над панелями ниже.

2 · Наивная обработка. Shift + колесо, каждое wheel-событие = срабатывание (либо порог в пикселях — вторая наивная стратегия, тоже не спасающая). Лог сырых событий: номер, дельта, интервал, ось, deltaMode, накопленный путь, метка срабатывания.

3 · Прогрессия. Тот же ввод через описанный алгоритм. 15 параметров, живой график заряда с линией порога, кнопки экспорта готового класса и загрузки настроек обратно.

4 · Эталонные устройства. Синтетические профили из таблицы выше прогоняются текущими параметрами на каждое движение ползунка. Двигаешь «полураспад» — сразу видишь, разъезжается ли столбец. Метрика разброса и контрольные строки на медленное кручение.


Как мерить

  1. Наведи курсор на панель, зажми Shift, покрути колесо. Считает только панель под курсором. Галка «один жест → обе панели» кормит оба алгоритма одним жестом, когда нужно честное сравнение.
  2. Сделай одинаковый физический жест — например, ровно 3 щелчка колеса или один короткий флик по тачпаду.
  3. Смотри строку жеста в логе: жест #3 · событий 30 · px 810 · срабатываний 4 · шаг 27 · пик v 96 · 232 мс.
  4. Повтори на другом устройстве. Наивная панель даст разброс, прогрессия должна дать то же число.
  5. Кнопка «Скопировать замер» выгружает UA, параметры, лог жестов и записи — удобно сравнивать между машинами.

Экспорт в проект

Кнопки «Скопировать класс (JS)» и «(TS)» генерируют WheelProgression с текущими значениями ползунков, вшитыми в static defaults, с датой калибровки и UA устройства, на котором крутили.

const wheel = new WheelProgression();

element.addEventListener('wheel', (e) => {
  if (!e.shiftKey) return;
  e.preventDefault();
  const { steps, direction, velocity, progress } = wheel.handleEvent(e);

  for (let i = 0; i < steps; i++) nextFrame(direction);   // дискретный выход
  // timeline.scrollBy(velocity * direction * SCALE);      // непрерывный выход
}, { passive: false });

handleEvent сам разбирается с deltaMode и с тем, что Shift переносит дельту на ось X. progress (0..1) — заполненность заряда, годится для индикатора «вот-вот сработает».

Сгенерированный класс — точный порт того, что крутится в стенде: он прогоняется по всем эталонным профилям и даёт те же цифры до единицы.

Загрузить настройки обратно

«Применить настройки…» открывает поле, куда можно вставить и раскатать на ползунки:

  • голый JSON настроек — {"threshold": 2.5, "halfLifeMs": 300, "release": "half"}, частичный тоже годится;
  • полный замер из «Скопировать замер» — подхватит и параметры прогрессии, и наивной панели;
  • блок static defaults из сгенерированного класса или весь файл класса целиком, с комментариями и кодом — ключи без кавычек, одинарные кавычки и висячие запятые разбираются наравне с JSON.

Значения проверяются: выходящие за диапазон ползунка зажимаются, нераспознанные и лишние ключи пропускаются, и всё это перечисляется в отчёте под кнопкой. Настройки, уехавшие в проект, всегда можно вернуть в стенд и продолжить крутить с того же места — круговой прогон идёт без потерь.


Алгоритм

norm  = min(|Δpx| / шаг_устройства, зажим)          // снимаем разницу контроллеров и DPI
v     = EMA(norm / Δt)                              // скорость в единицах/с
вклад = norm · (база + усиление · (v / v₀)^экспонента)
заряд = заряд · 0.5^(Δt / полураспад) + вклад       // экспоненциальная утечка

Срабатывание при заряд ≥ порог. Шаг устройства — медиана последних 96 величин: у мыши это ~100 px, у тачпада ~9 px, и обе приводятся к одной шкале сами.

Параметры

группа параметр что делает
Ядро Порог заряда сколько единиц нужно накопить; линейный множитель пути до срабатывания
Полураспад заряда за сколько мс заряд падает вдвое; меньше — жёстче требование к ускорению
Кулдаун жёсткий пол во времени между срабатываниями
После срабатывания вычесть порог переносит остаток, обнулить теряет
Ускорение База вклад события при нулевой скорости; 0 — без ускорения не копится вообще
Вклад ускорения / Экспонента / Опорная скорость форма прогрессии: насколько резко путь сокращается со скоростью
Потолок усиления / Сглаживание скорости зажим множителя и инерционность EMA
Нормировка Шаг устройства авто (перцентиль), фиксированный, выкл
Перцентиль / Зажим шага робастность автоопределения и потолок вклада одного события
Жест Пауза между жестами тишина дольше — сброс состояния
Сброс при развороте / Зажим Δt поведение при смене направления и верхняя граница интервала

Прямого параметра «путь в пикселях» нет — в этом и смысл. Эффективный путь:

путь_px ≈ шаг_устройства × порог / (база + усиление·(v/v₀)^экспонента)

снизу ограниченный кулдауном: путь_px ≥ скорость_px_в_мс × кулдаун. На дефолтах и обычных скоростях связывает именно кулдаун, а не порог — если кажется, что порог не влияет, дело в этом.


Особенности, о которые легко споткнуться

  • Shift + колесо переносит дельту на ось X в Chrome и Firefox на всех платформах. Если читать только deltaY, на реальном железе получишь нули. Стенд берёт доминирующую ось и показывает её в шапке.
  • deltaMode бывает 1 (строки) и 2 (страницы) — в Firefox регулярно. Приводится к пикселям до нормировки.
  • Инерционный хвост тачпада: macOS после отрыва пальцев продолжает слать события с затухающими дельтами. Сейчас они попадают в тот же жест и могут дать лишнее срабатывание. Не обработано.
  • Буфер обмена на file:// может быть недоступен в зависимости от браузера. Все кнопки копирования в этом случае показывают текст прямо на странице — забирай оттуда.

Файлы

index.html   весь стенд одним файлом: разметка, стили, алгоритм, эталоны, записи
README.md    этот файл

About

Interactive lab & JS/TS helper to normalize mouse wheel and touchpad scrolling acceleration using dynamic EMA decay.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages