Porada: Dostępność interaktywnych symulacji — ARIA, klawiatura, ograniczony ruch

Symulacja fizyczna jest z natury wizualna. Sprawienie, by była użyteczna dla wszystkich — użytkowników korzystających wyłącznie z klawiatury, osób używających czytników ekranu, osób z zaburzeniami przedsionkowymi, osób słabowidzących — wymaga świadomego, wielowarstwowego podejścia do projektowania. Oto nasza pełna biblioteka wzorców dostępności.

Dlaczego dostępność symulacji jest trudna

Standardowe wytyczne dotyczące dostępności stron internetowych obejmują tekst, formularze i nawigację. Symulacja — canvas WebGL, który aktualizuje się 60 razy na sekundę, reaguje na przeciąganie myszą i przekazuje stan wizualnie — łamie niemal każde założenie, na którym te wytyczne są zbudowane.

Kluczem jest przyjęcie, że czytnik ekranu nie jest w stanie wyrenderować symulacji grawitacji, a zamiast tego trzeba zapewnić, że: (1) każda symulacja ma sensowny opis tekstowy, (2) każdy interaktywny element sterujący jest osiągalny za pomocą klawiatury, a (3) canvas nigdy nie powoduje dolegliwości przedsionkowych.

1. Regiony ARIA live dla stanu symulacji

Czytniki ekranu ignorują elementy canvas, chyba że jawnie zdublujesz stan symulacji w regionie ARIA live. Dodaj wizualnie ukryty region live obok canvasu i aktualizuj go w ludzkim tempie (co 1–2 sekundy), a nie z częstotliwością klatek.

HTML — region live obok canvasu
<!-- Region live musi istnieć w DOM zanim symulacja wystartuje -->
<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>

<!-- Aktualizowane przez JS co ~1 s, by ogłaszać bieżący stan -->
<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 — ograniczone częstotliwościowo ogłoszenia stanu
const statusEl = document.getElementById('sim-status');
let lastAnnounce = 0;

function announceState(theta1, theta2, energy) {
  const now = performance.now();
  if (now - lastAnnounce < 1500) return;  // maks. raz na 1.5 s
  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. Pełne sterowanie klawiaturą

Każda akcja dostępna przez przeciąganie myszą musi być osiągalna za pomocą klawiatury. Ustaw fokus na canvasie i obsługuj zdarzenia keydown. Zapewnij widoczny wskaźnik fokusu — nigdy nie usuwaj outline z canvasu bez dostarczenia własnego pierścienia fokusu.

JavaScript — obsługa klawiatury dla canvasu
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;
  }
});

// Widoczny pierścień fokusu
canvas.addEventListener('focus', () => canvas.classList.add('focused'));
canvas.addEventListener('blur',  () => canvas.classList.remove('focused'));

Dodaj legendę skrótów klawiaturowych pod każdą symulacją — mały element <details> z listą wszystkich klawiszy. Służy to zarówno jako sposób odkrycia możliwości przez widzących użytkowników klawiatury, jak i podpowiedź dla użytkowników czytników ekranu czytających stronę przed rozpoczęciem interakcji.

3. Ograniczony ruch

Osoby z zaburzeniami przedsionkowymi mogą odczuwać nudności lub migreny wywołane animacjami z gwałtownym ruchem lub miganiem. Zapytanie medialne prefers-reduced-motion: reduce pozwala wykryć taką preferencję i zaproponować statyczną lub spowolnioną alternatywę.

JavaScript — uwzględnianie preferencji ograniczonego ruchu
const prefersReducedMotion =
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

function tick(t) {
  requestAnimationFrame(tick);

  if (prefersReducedMotion) {
    // Pomijamy renderowanie co drugą klatkę (limit 30 fps)
    if (frameCount++ % 2 !== 0) return;

    // Zmniejszamy krok fizyki, by ruch wyglądał wolniej
    physicsStep(0.004);   // połowa prędkości
  } else {
    physicsStep(0.016);   // normalne 60 fps
  }

  render();
}

// Nasłuchujemy też zmian w trakcie sesji (użytkownik zmienia ustawienie systemowe)
window.matchMedia('(prefers-reduced-motion: reduce)')
  .addEventListener('change', (e) => {
    if (e.matches) pauseParticleTrails();
    else resumeParticleTrails();
  });

4. Kontrast kolorów w WebGL

WCAG 2.1 AA wymaga kontrastu 3:1 dla dużych obiektów graficznych i komponentów interfejsu względem tła. W ciemnym motywie symulacji 3D zwykle nie jest to problem — z wyjątkiem subtelnych efektów, takich jak słabo widoczne smugi cząsteczek, wygładzone krawędzie ciemnej sfery na niemal czarnym tle oraz kodowanie danych wyłącznie kolorem.

Fragment shader GLSL — uniform trybu wysokiego kontrastu
uniform bool u_highContrast;
uniform vec3 u_particleColor;

void main() {
  vec3 col = u_highContrast
    ? vec3(1.0, 1.0, 0.0)   // jaskrawożółty — kontrast 18:1 na czerni
    : u_particleColor;
  gl_FragColor = vec4(col, 1.0);
}

5. Struktura strony dla czytników ekranu

Nawet jeśli sam canvas nie może zostać odczytany, strona wokół niego może i powinna być w pełni dostępna. Nasza standardowa struktura strony symulacji trzyma się dobrych praktyk dotyczących landmarków ARIA:

Szybka lista kontrolna zgodności