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.
<!-- 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>
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.
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ę.
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.
- Nigdy nie polegaj wyłącznie na kolorze, by przekazać informację — dodawaj kształt, etykietę lub wzór. W naszej symulacji rozprzestrzeniania się choroby zarażone agenty to czerwone kółka z ikoną krzyżyka, a nie po prostu czerwone.
- Testuj analizatorem kontrastu, robiąc zrzut ekranu canvasu i przepuszczając go przez narzędzie takie jak TPGi CCA.
- Zapewnij przełącznik trybu wysokiego kontrastu, który przełącza uniformy WebGL na paletę o maksymalnej jasności.
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:
-
<header>— tytuł i opis (zawsze obecny, indeksowalny) -
<main>— zawiera canvas, elementy sterujące i region live -
<section aria-label="Controls">— wszystkie pola parametrów z parami<label>/for -
<section aria-label="About this simulation">— tekst edukacyjny, opis matematyczny <footer>— atrybucja i odnośniki
Szybka lista kontrolna zgodności
-
Canvas ma
aria-labeliaria-describedby -
Canvas jest osiągalny fokusem klawiatury
(
tabindex="0",role="application") - Widoczny pierścień fokusu na canvasie (nieukryty)
- Wszystkie interaktywne elementy sterujące osiągalne klawiszem Tab
-
Wszystkie suwaki i przyciski mają powiązaną
<label> - Legenda skrótów klawiaturowych obecna w DOM
- Region ARIA live ogłasza stan w interwałach ≤ 1.5 s
-
prefers-reduced-motionuwzględniony — ruch zmniejszony o połowę lub wstrzymany - Kolor nie jest jedynym kanałem przekazu żadnych danych
- Dostępny przełącznik trybu wysokiego kontrastu
- Elementy graficzne spełniają współczynnik kontrastu 3:1 względem tła
-
Stany błędów są ogłaszane czytnikom ekranu przez
role="alert"