Чому доступність симуляцій — складна задача
Стандартні рекомендації з веб-доступності охоплюють текст, форми та навігацію. Симуляція — WebGL-канвас, що оновлюється 60 разів на секунду, реагує на перетягування мишею та передає стан візуально — порушує майже кожне припущення, на якому побудовані ці рекомендації.
Ключ у тому, щоб прийняти: програма читання з екрана не може відобразити симуляцію гравітації, тож натомість слід забезпечити, щоб: (1) кожна симуляція мала змістовний текстовий опис, (2) кожен інтерактивний елемент керування був досяжним із клавіатури, і (3) канвас ніколи не завдавав вестибулярної шкоди.
1. ARIA live-регіони для стану симуляції
Програми читання з екрана ігнорують елементи canvas, якщо ви явно не дублюєте стан симуляції в ARIA live-регіон. Додайте візуально прихований live-регіон поруч із канвасом і оновлюйте його з людським темпом (кожні 1–2 секунди), а не з частотою кадрів.
<!-- 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>
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 з канвасу, не
надавши власне кільце фокусу.
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 дозволяє
виявити цю перевагу й запропонувати статичну або уповільнену
альтернативу.
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-симуляції це зазвичай не проблема — за винятком тонких ефектів на кшталт слабких слідів частинок, згладжених країв темної сфери на майже чорному тлі та кодування даних лише кольором.
- Ніколи не покладайтесь лише на колір для передачі інформації — додавайте форму, підпис або патерн. У нашій симуляції поширення хвороби заражені агенти — це червоні кола з іконкою хрестика, а не просто червоні кола.
- Перевіряйте аналізатором контрасту, зробивши скриншот канвасу й прогнавши його через інструмент на кшталт TPGi CCA.
- Надайте перемикач режиму високого контрасту, який перемикає uniform-значення WebGL на палітру максимальної яскравості.
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-орієнтирів:
-
<header>— заголовок і опис (завжди присутній, індексується) -
<main>— містить канвас, елементи керування та live-регіон -
<section aria-label="Controls">— усі поля параметрів із парами<label>/for -
<section aria-label="About this simulation">— освітній текст, математичний опис <footer>— атрибуція та посилання
Швидкий чек-лист відповідності
-
Канвас має
aria-labelіaria-describedby -
Канвас доступний із клавіатури (
tabindex="0",role="application") - Видиме кільце фокусу на канвасі (не приховане)
- Усі інтерактивні елементи керування досяжні через Tab
-
Усі повзунки й кнопки мають пов'язаний
<label> - Легенда клавіатурних скорочень присутня в DOM
- ARIA live-регіон оголошує стан з інтервалом ≤ 1.5 с
-
prefers-reduced-motionвраховано — рух зменшено вдвічі або зупинено - Колір не є єдиним каналом для жодних даних
- Доступний перемикач режиму високого контрасту
- Графічні елементи відповідають коефіцієнту контрасту 3:1 щодо фону
-
Стани помилок оголошуються для програм читання з екрана через
role="alert"