Конечные автоматы в менюшном движке. Повышаем доступность доисторического приключения на ink. Часть 4

Продолжение части 3. Ссылки на итоговую версию игры «Доисторическая схватка» приведены в конце текста.

В этой статье будут рассмотрены базовые вопросы повышения доступности интерфейса текстовой игры преимущественно для пользователей вспомогательных (ассистивных) технологий невизуального доступа к экранной информации, реализация поддержки которых может добавить даже очень нишевой IF-игре несколько сотен человек активной целевой аудитории, а потом ещё неопределённое количество длинным хвостом (на английском языке величины возрастают на порядки). Применительно к конечным автоматам всё это уже некоторый оффтоп, но надо же всё-таки довести игру до ума…

Словом «доступность» в русском языке может обозначаться очень большое количество вещей, вплоть до женского поведения. Но и даже конкретно внутри IT-индустрии часто путаются понятия доступность в смысле accessibility и доступность в смысле availability, поэтому явно уточним, что данный материал посвящён вопросам accessibility (также известной как a11y), то есть способности какого-либо продукта (материального или нематериального), быть использованным как можно большим количеством разных пользователей, независимо от их физических и технических ограничений.

Предупреждение о необходимости JavaScript

Уточнение про не только физические, но и технические ограничения в определении доступности неслучайно. Маленький экран при мелком тексте заставляет абсолютно здорового человека почувствовать себя слабовидящим, поэтому не стоит сужать предметную область доступности до людей с физическими ограничениями. Да и физические ограничения имеют более широкую природу, чем просто прямое следствие дефекта, например, попытка заказать через приложение пиццу в состоянии сильного опьянения зачастую не сильно отличается от работы пользователя с сочетными двигательными и ментальными нарушениями. Однако вопросы размераи интервалов текста, минимального коэффициента контрастности или агрессивных спецэффектов, способных вызывать у пользователя физический дискомфорт, интуитивно понятны любому мало-мальски грамотному дизайнеру и относятся скорей к общим принципам, поэтому в рамках данного разговора специально на них останавливаться не будем, а вот общий вопрос отсутствия поддержки JavaScript представляется уже не столь очевидным.

Например, Chrome скоро может начать по умолчанию автоматически отключать JavaScript при низкой скорости соединения, так что интерфейс, критически зависящий от данной технологии, если и умирает, то пускай делает это хотя бы с предсмертным стоном. В связи с этим, имеет смысл добавить в статичную часть интерфейса в файле index.html соответствующее оповещение средствами тега noscript:


<noscript><p>Для работы игры требуется поддержка JavaScript, которая отсутствует или отключена в вашем браузере.</p></noscript>

Теперь пользователь хотя бы будет понимать причину, почему игра не работает.

Между прочем, в некоторых компаниях, например, в Facebook, разработчиков заставляют работать определённое количество часов со своими собственными продуктами на скоростях уровня GPRS, чтобы они не забывали, что не все их пользователи живут в Долине.

Указание языка текста

Текст — это, пожалуй, наиболее доступный вид контента, максимально пригодный для всевозможных способов восприятия, поэтому делать недоступные текстовые игры вообще должно быть стыдно, так как это всё равно что забивать молотком шурупы. При должной технической реализации он может быть представлен в любом альтернативном виде практически без каких-либо потерь, а соответствующее преобразование будет выполняться легко и просто прямо на пользовательском устройстве в соответствии с любыми специфическими потребностями: для визуального восприятия его можно отобразить в иной цветовой гамме, с любым размером символов или любым шрифтом; для тактильного восприятия его можно вывести рельефно-точечным шрифтом в соответствии с любой трансляционной таблицей Брайля; для слухового восприятия его можно прочитать синтезатором речи; для комбинированного восприятия можно делать чтение вслух синтезатором, сопровождая это специальным визуальным выделением читаемого фрагмента а-ля караоке. Однако некоторые способы альтернативного представления могут потребовать дополнительных метаданных, в частности, знания языка и диалекта, на котором написан текст, чтобы вспомогательная технология могла, например, автоматически задействовать соответствующий синтезатор речи.

HTML имеет специальные средства указания языка текста посредством атрибута lang. Не стоит пренебрегать данным аспектом, так как это не менее важно, чем указание кодировки текста через charset, а главное и не сложнее этого.

Конечно, можно и пренебречь явным указанием языка, переложив задачу его определения на самого пользователя или применяемую им технологию, однако это, во-первых, халтура, причём в простых для реализации вещах, а во-вторых, используя готовые решения, такие как CMS или web-платформы для написания текстовых игр, вы можете столкнуться с тем, что какой-то язык там уже определён и, как правило, это будет английский. Ну а некорректное указание языка текста — это уже проблема совсем другого уровня, чем просто отсутствие такого указания. Это может привести к инициативному ухудшению удобства работы пользователя со стороны вашего интерфейса, когда он будет отправлять информацию, специально заставляющую программное обеспечение работать с русскоязычным текстом как с англоязычным.

В случае с используемым web-интерпретатором ink мы наблюдаем как раз эту самую ситуацию — в файле index.html явно определён английский язык для всего содержимого страницы. Поскольку рассматриваемая игра является русскоязычной, то переопределим язык на русский:


<html lang="ru">

Альтернативное представление недоступного контента

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

Если игра содержит, например, видео с говорящим человеком, то обеспечить его доступность без прямого слухового восприятия можно посредством субтитров. В HTML контейнеры audio и video позволяют через тег track добавить файл синхронизированных субтитров в формате VTT.

Впрочем, видео с монологами в текстовых играх обычно встречается на последних местах КРИЛа, а более распространённой задачей является обеспечение доступности графических иллюстраций в текстовой форме. В HTML для этого у тега img существует специальный атрибут alt, содержащий тот текст, который будет восприниматься пользователем программы экранного доступа (screenreader) или человеком, работающим в браузере, где отключён или не поддерживается показ картинок.

Следует осознавать, что иллюстрации могут играть ключевое значение для понимания текста, например, как в финале рассматриваемой игры:

«Путь свободен, победа!

[картинка, которую мы не видим]

После долгих месяцев скитаний охотник нашёл свою судьбу.»
Без знания того, что изображено на картинке, судьба охотника остаётся непонятной. Это, в частности, к вопросу о том, когда же именно текстовая игра теряет проходимость без графики. Здесь формально всё проходимо, но часть смысла утрачена.

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

Пока всё это была теория, а с практикой по этому вопросу в случае рассматриваемой игры начинаются проблемы. Дело в том, что ink как встраиваемая технология управления графом абзацев категорией картинок не оперирует в принципе. Возможность вставки картинки реализуется уже на стороне конкретного интерпретатора, который может принять некую пару из свойства и его значения и интерпретировать это в соответствии с принятой конвенцией.

Используемый web-интерпретатор ink в принципе умеет принимать свойство IMAGE и его значение в виде пути к файлу изображения, из чего потом генерирует элемент img с атрибутом src, но проблема в том, что мы для решения поставленной задачи нуждаемся в двух отдельных значениях для пути к файлу (атрибут src) и для описания изображения (атрибут alt). Одно из возможных решений может выглядеть как расширение диалекта ink для используемого интерпретатора, когда мы введём дополнительное соглашение, что значение свойства IMAGE может представлять собой либо путь к файлу, либо путь к файлу и описание изображения, которые разделены вертикальной чертой «|»:


// Картинка без описания (как и изначально)
# IMAGE: images/logo.gif
// Картинка с описанием
# IMAGE: images/logo.gif|Вступительная анимация с охотником у костра под деревом

Ну а после этого мы идём в файл main.js и дорабатываем код генерации элемента img, который располагается после комментария «IMAGE: src»:


// IMAGE: src
if( splitTag && splitTag.property == "IMAGE" ) {
	var imageElement = document.createElement('img');
	var valArr = splitTag.val.split('|');
	if ( valArr.length == 2 ) {
		imageElement.src = valArr[0];
		imageElement.alt = valArr[1];
	} else {
		imageElement.src = splitTag.val;
	}
	storyContainer.appendChild(imageElement);

	showAfter(delay, imageElement);
	delay += 200.0;
}

Вообще-то по спецификации атрибут alt должен быть всегда, даже если картинка не несёт содержательного смысла, но намерено не будем добавлять пустой alt для IMAGE без описания, чтобы при случае автора дёрнул валидатор или инспектор из какого-нибудь набора инструментов web-разработки, так как наверняка все внедрения через IMAGE будут содержательным контентом и отсутствие подписи для таких случаев будет ничем иным, как упущением, а не осознанным решением.

Семантический каркас

Пользователь, взаимодействующий с визуально ориентированным web-интерфейсом невизуальными методами, воспринимает его линейно в соответствии с порядком элементов в дереве DOM, поэтому не имеет контекста в виде позиционирования блоков, а также работает без восприятия большей части графического акцентирования и цветовой дифференциации. По этой причине выделение, например, панели инвентаря отдельной колонкой с отличающейся фоновой подложкой, ничем такому пользователю не поможет.

Для передачи информации о структуре страницы существует специальный раздел технологии WAI-ARIA, называющийся landmarks. Он позволяет обозначить блоки страницы в соответствии с несколькими базовыми семантическими ролями (верхний и нижний колон-титулы, основное содержимое, навигационная область и пр.), причём, сделать это в иерархической форме, за счёт чего донести дополнительную информацию о семантической структуре страницы до пользователей вспомогательных технологий, а также дать им дополнительный слой навигации по этим областям.

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

Весь текст игры выводится в контейнер div с id="story", статически прописанный в index.html (в коде движка он фигурирует как элемент storyContainer). Именно его и обозначим как область основного содержимого средствами атрибута role:


<div id="story" class="container" role="main">

После семантического каркаса логично было бы перейти к структурной вёрстке, но в процессе реализации всплыл один нюанс, поэтому с целью более последовательного изложения сперва поговорим о live regions, хотя это и более сложная тема, чем структурная вёрстка.

Автоматическое прочтение описания

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

Решением данной проблемы является автоматическое прочтение нового описания. Для нативных приложений на этот случай существуют специальные системные API или API самого вспомогательного программного обеспечения, ну а для web в спецификации WAI-ARIA существует отдельный вид семантических областей, называемых live regions.

Разработчик может пометить те области интерфейса, обновление содержимого в которых достаточно важно для того, чтобы автоматически зачитывать новый контент. В нашем случае автоматически зачитывать имеет смысл просто весь блок с обновляющемся текстом игры, то есть уже упомянутый div с id="story", что мы и обозначим средствами атрибута aria-live:


<div id="story" class="container" role="main" aria-live="polite">

Примечание для копипастеров: К сожалению, с имплементацией WAI-ARIA наблюдается такой же бардак, как и со всеми web-технологиями для front-end разработки, то есть неравномерность и неконсистентность поддержки для различных пользовательских конфигураций. Причём, к чехарде с браузерами и их версиями добавляется ещё чехарда с самими программами экранного доступа и их версиями, так что бардак возрастает экспоненциально. Live regions как раз относится к той части спецификации WAI-ARIA, для которой данные проблемы по-прежнему справедливы. В итоге, в нашем случае приведённая реализация вполне приемлема, но при более сложных задачах, когда надо будет управлять приоритетностью зачитывания или выстраивать иерархию таких областей, простое копирование этого решения может оказаться не лучшим вариантом, и надо будет самостоятельно вырабатывать решение с реальным тестированием результата.

Ещё одним нюансом является то, что опции выбора действий главного героя также выводятся параграфами во всё тот же div с id="story", а значит будут зачитываться вместе с изменившемся описанием. Кому-то это может показаться даже удобным, но с другой стороны, перечисление всех опций подряд в общем потоке описания может оказаться недостаточно информативным и даже в чём-то запутывающем, когда несколько вариантов могут восприниматься как один. К тому же в рассматриваемой игре варианты всё равно неизменны, да и пользователю в любом случае предстоит взаимодействовать с этим участком интерфейса, где мы предоставим ему эту информацию в более структурированном виде, поэтому, а также с целью иллюстрации различных технических приёмов, реализуем исключение опций выбора из автоматически прочитывающегося описания.

Это делается через всё тот же атрибут aria-live, но со значением off. Области с aria-live="off" внутри других live regions будут игнорироваться и пропускаться при чтении, поэтому нам всего лишь надо поместить весь блок вариантов выбора в такую «замалчивающуюся» область. Однако нам всё равно предстоит доделывать область выборов, так что на конкретный код уже со всеми изменениями посмотрим чуть позже.

Структурная вёрстка

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

Идея правильной структурной вёрстки сводится к тому, что контент, имеющий определённый структурный смысл, должен не только визуально выглядеть как, например, заголовок или таблица, но также и являться этим с точки зрения HTML, используя соответствующие теги описания элементов страницы.

В случае рассматриваемой игры мы наблюдаем ситуацию, что варианты выбора действий главного героя по смыслу являются перечислением, то есть списком, но свёрстаны как простая последовательность параграфов, ничем не отличающихся от параграфов описания. Да, они при этом ссылки, так что не всё так плохо, как могло бы быть, но если бы они при этом были ещё и реальным HTML-списком, то пользователь программы экранного доступа получил бы, во-первых, дополнительную информацию, так как ему бы сразу сообщалось, сколько элементов в списке, а он, как уже говорилось, не имеет возможности быстро охватить интерфейс взглядом и сразу это понять, а во-вторых, дополнительные возможности по навигации, а именно быстрое перемещение к списку вариантов и быструю навигацию по элементам внутри списка или к его началу и концу. В итоге, нам следует оформить перечень вариантов выбора как HTML-список.

Здесь возникает проблема, что старые добрые структурные теги HTML в браузерах уже имеют некоторые дефолтные стилевые оформления, так что когда мы обозначаем что-то HTML-заголовком или HTML-списком, то это сразу приводит к тому, что этот элемент получает набор каких-то стилей, из-за чего плывёт весь интерфейс, так как изначально этого никто не предполагал.

Это стандартная проблема для всех проектов, где доступностью начинают заниматься уже после основной стадии разработки. Если разрабатывать интерфейс с учётом всего этого с самого начала, то таких проблем не будет. Однако реальность такова, что доступность чаще достраивают потом, нежели закладывают с самого начала, поэтому спецификация WAI-ARIA на 50%, а может и на все 75%, состоит из костылей, дублирующих стандартные структурные элементы, которые позволяют добиться тех же результатов, но не затрагивая визуальную часть интерфейса. В нашем случае переделать всё на HTML-списки возможно даже было бы проще, но с этим справится кто угодно, поэтому покажем более сложный путь через WAI-ARIA, который к тому же более ценен как общий пример.

Как известно, HTML-список состоит из общего контейнера (ul или ol), внутрь которого вкладываются элементы (li). С точки зрения WAI-ARIA всё выглядит точно также: то есть нам надо создать элемент с ролью общего контейнера (list), внутрь которого поместить элементы с ролью пунктов списка (listitem).

Также вспомним, что у нас подвисла задача с необходимостью исключить из автоматического зачитывания варианты выбора, так что атрибут aria-live="off" как раз имеет смысл назначить контейнеру списка.

Генерация перечня вариантов выбора происходит внутри движка, так что нам придётся снова залезть в файл main.js, где соответствующий код располагается после комментария «Create HTML choices from ink choices».

Сначала создадим элемент div с атрибутами role="list" и aria-live="off", после чего модифицируем функцию генерации вариантов выбора, вставив туда присвоение атрибута role="listitem" элементам параграфов и реализовав их добавление внутрь div тела списка, а не div всего описания:


var choiceParagraphRegion = document.createElement('div');
choiceParagraphRegion.setAttribute('role', 'list');
choiceParagraphRegion.setAttribute('aria-live', 'off');
storyContainer.appendChild(choiceParagraphRegion);

// Create HTML choices from ink choices
story.currentChoices.forEach(function(choice) {

	// Create paragraph with anchor element
	var choiceParagraphElement = document.createElement('p');
	choiceParagraphElement.classList.add("choice");
	choiceParagraphElement.setAttribute('role', 'listitem');
	choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
	choiceParagraphRegion.appendChild(choiceParagraphElement);

(Здесь показана только изменённая часть и код оборван в середине тела функции, так что аккуратнее.)

Теперь блок вариантов выбора будет генерироваться вот в таком виде:


<div role="list" aria-live="off">
	<p role="listitem">...</p>
	<p role="listitem">...</p>
</div>

Это никак не затронет визуальную вёрстку, а программы экранного доступа заставит обрабатывать этот блок как обычный HTML-список, да к тому же не озвучивать его содержимое при изменении.

Не в этот раз, так в следующий...

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

Во-первых, определённая часть пользователей может работать без мышки: либо по причине сложившихся привычек, либо по причине физических ограничений, либо по причине технических ограничений. Причём, работа на мобильном устройстве, где нет никакой мышки, — это уже одно из таких технических ограничений, так что ситуация распространена крайне широко. В связи с этим следует обеспечить полную доступность всех элементов управления для навигации с клавиатуры (для web см. атрибут tabindex), а также отказаться от всех важных реакций и ключевых элементов интерфейса, основанных на движении курсора мыши (для web привязка к событиям onmouseover, всплывающие сообщения в атрибутах title и пр.).

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

В-третьих, в интерфейсе могут быть и другие элементы управления, сохраняющие актуальность на протяжении всего процесса взаимодействия. Например, для игр это могут быть кнопки сохранения или восстановления. Будет не лишним сделать горячие клавиши для их активации (для web либо просто через атрибут accesskey, либо более сложно через перехват нажатий).

На этом всё. Делайте доступные интерфейсы и не забывайте, что ink пишется с маленькой буквы.

Ссылки

Игра «Доисторическая схватка»: