Порада: Доступність інтерактивних симуляцій — ARIA, клавіатура, зменшений рух

Фізична симуляція за своєю природою візуальна. Зробити її корисною для всіх — користувачів лише з клавіатурою, користувачів програм читання з екрана, людей із вестибулярними розладами, людей зі слабким зором — вимагає свідомого, багаторівневого підходу до дизайну. Ось наша повна бібліотека патернів доступності.

Чому доступність симуляцій — складна задача

Стандартні рекомендації з веб-доступності охоплюють текст, форми та навігацію. Симуляція — WebGL-канвас, що оновлюється 60 разів на секунду, реагує на перетягування мишею та передає стан візуально — порушує майже кожне припущення, на якому побудовані ці рекомендації.

Ключ у тому, щоб прийняти: програма читання з екрана не може відобразити симуляцію гравітації, тож натомість слід забезпечити, щоб: (1) кожна симуляція мала змістовний текстовий опис, (2) кожен інтерактивний елемент керування був досяжним із клавіатури, і (3) канвас ніколи не завдавав вестибулярної шкоди.

1. ARIA live-регіони для стану симуляції

Програми читання з екрана ігнорують елементи canvas, якщо ви явно не дублюєте стан симуляції в ARIA live-регіон. Додайте візуально прихований live-регіон поруч із канвасом і оновлюйте його з людським темпом (кожні 1–2 секунди), а не з частотою кадрів.

HTML — live-регіон поруч із канвасом
<!-- Live-регіон має існувати в DOM ще до старту симуляції -->
<canvas id="sim" aria-label="Pendulum simulation"
        aria-describedby="sim-desc"></canvas>

<p id="sim-desc" class="visually-hidden">
  An interactive double pendulum simulation.
  Use the controls below to change mass and length.
</p>

<!-- Оновлюється JS кожні ~1 с для оголошення поточного стану -->
<div id="sim-status"
     role="status"
     aria-live="polite"
     aria-atomic="true"
     class="visually-hidden"></div>

<style>
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
</style>
JavaScript — обмежені за частотою оголошення стану
const statusEl = document.getElementById('sim-status');
let lastAnnounce = 0;

function announceState(theta1, theta2, energy) {
  const now = performance.now();
  if (now - lastAnnounce < 1500) return;  // максимум раз на 1.5 с
  lastAnnounce = now;

  const deg1 = (theta1 * 180 / Math.PI).toFixed(0);
  const deg2 = (theta2 * 180 / Math.PI).toFixed(0);
  statusEl.textContent =
    `Pendulum 1: ${deg1}°, Pendulum 2: ${deg2}°, ` +
    `System energy: ${energy.toFixed(2)} J`;
}

2. Повне керування з клавіатури

Кожна дія, доступна через перетягування мишею, повинна бути досяжною з клавіатури. Фокусуйте канвас і обробляйте події keydown. Забезпечте видимий індикатор фокусу — ніколи не прибирайте outline з канвасу, не надавши власне кільце фокусу.

JavaScript — обробник клавіатури для канвасу
const canvas = document.getElementById('sim');
canvas.setAttribute('tabindex', '0');
canvas.setAttribute('role', 'application');

canvas.addEventListener('keydown', (e) => {
  switch (e.key) {
    case ' ':
      e.preventDefault();
      togglePause();
      break;
    case 'ArrowLeft':
      e.preventDefault();
      applyImpulse(-0.1, 0);
      break;
    case 'ArrowRight':
      e.preventDefault();
      applyImpulse(+0.1, 0);
      break;
    case 'r':
    case 'R':
      resetSimulation();
      break;
    case '?':
      showKeyboardHelpDialog();
      break;
  }
});

// Видиме кільце фокусу
canvas.addEventListener('focus', () => canvas.classList.add('focused'));
canvas.addEventListener('blur',  () => canvas.classList.remove('focused'));

Додайте легенду клавіатурних скорочень під кожною симуляцією — невеликий елемент <details> зі списком усіх клавіш. Це слугує і для виявлення можливостей зрячими користувачами клавіатури, і як підказка для користувачів програм читання з екрана, які читають сторінку перед взаємодією.

3. Зменшений рух

Користувачі з вестибулярними розладами можуть відчувати нудоту або мігрень від анімацій із різкими рухами або мерехтінням. Медіазапит prefers-reduced-motion: reduce дозволяє виявити цю перевагу й запропонувати статичну або уповільнену альтернативу.

JavaScript — врахування налаштування зменшеного руху
const prefersReducedMotion =
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

function tick(t) {
  requestAnimationFrame(tick);

  if (prefersReducedMotion) {
    // Пропускаємо рендеринг через кадр (обмеження 30 fps)
    if (frameCount++ % 2 !== 0) return;

    // Зменшуємо крок фізики, щоб рух здавався повільнішим
    physicsStep(0.004);   // половинна швидкість
  } else {
    physicsStep(0.016);   // звичайні 60 fps
  }

  render();
}

// Також слухаємо зміни під час сесії (користувач змінює налаштування ОС)
window.matchMedia('(prefers-reduced-motion: reduce)')
  .addEventListener('change', (e) => {
    if (e.matches) pauseParticleTrails();
    else resumeParticleTrails();
  });

4. Контраст кольорів у WebGL

WCAG 2.1 AA вимагає контрасту 3:1 для великих графічних об'єктів та елементів інтерфейсу відносно фону. У темній 3D-симуляції це зазвичай не проблема — за винятком тонких ефектів на кшталт слабких слідів частинок, згладжених країв темної сфери на майже чорному тлі та кодування даних лише кольором.

Фрагментний GLSL-шейдер — uniform для режиму високого контрасту
uniform bool u_highContrast;
uniform vec3 u_particleColor;

void main() {
  vec3 col = u_highContrast
    ? vec3(1.0, 1.0, 0.0)   // яскраво-жовтий — контраст 18:1 на чорному
    : u_particleColor;
  gl_FragColor = vec4(col, 1.0);
}

5. Структура сторінки для програм читання з екрана

Навіть якщо сам канвас неможливо прочитати, сторінка навколо нього може й повинна бути повністю доступною. Наша стандартна структура сторінки симуляції дотримується найкращих практик ARIA-орієнтирів:

Швидкий чек-лист відповідності