Индикатор искусственного горизонта на HTML5 canvas +7


AliExpress RU&CIS

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



Назначение и область применения искусственного горизонта


Искусственный горизонт в рассматриваемом здесь узком смысле представляет собой визуализацию наклона объекта относительно местной вертикали, используемую для управления его движением. Наклон определяется значениями двух углов Эйлера, крена и тангажа. Авиационному термину «тангаж» моряки предпочитают синоним «дифферент».

Родственные искусственному горизонту (но не вполне синонимичные) русскоязычные термины: «авиагоризонт», «пилотажно-командный прибор». В английском языке используются выражения «attitude indicator», «artificial horizon» или «gyro horizon».

Известные способы визуализации


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

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

  • фон разделен на две части, символизирующие небо и землю, линией, обозначающей горизонт;
  • силуэт самолета представляет собой упрощенный «вид сзади», контрастирующий по цвету с фоном;
  • угол крена определяется по индикатору как угол между символической линией горизонта и линией, соединяющей законцовки крыла силуэта (обычно для точного считывания присутствует шкала отсчета);
  • угол тангажа отсчитывается вдоль шкалы, перпендикулярной условному горизонту по положению контрольной точки в центре силуэта.



Реализованные в массовом производстве системы имеют ряд общих решений:

  • информация задается взаимным расположением силуэта и фона;
  • изменение угла крена связано с угловым перемещением силуэта относительно фона;
  • изменение угла тангажа связано с линейным перемещением силуэта относительно фона.

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

1. Неподвижный силуэт, подвижный по крену и тангажу фон. Употребляемые названия: «прямая индикация», «вид с самолета на землю», реже «эгоцентрическая индикация».



2. Подвижный только по крену силуэт, подвижный только по тангажу фон. Употребляемые названия: «обратная индикация» и «вид с земли на самолет», реже «геоцентрическая индикация».



Заметим, что названия п. 2 применяются к системе в целом, но отражают лишь принятый в ней принцип индикации угла крена. Индикация угла тангажа в обеих используемых системах является «прямой» и «эгоцентрической».

В существующих авиационных симуляторах, например, в Microsoft Flight Simulator и Digital Combat Simulator, можно оценить в действии оба типа индикации.

Стоит отметить, что не все известные решения вписываются в приведенный выше шаблон. Для примера выхода за обозначенные рамки рассмотрим два патента на изобретения: RU 2561311 и RU 2331848.

Первый патент посвящен «Авиагоризонту с разнесенными по высоте указателями крена и тангажа», его авторы: Путинцев В. И. и Литуев Н. А. Схема ниже взята из патента.



При необходимости можно найти расшифровку обозначений и описание работы в тексте первоисточника. В целом замысел изобретения довольно прост: идея «вида с земли на самолет» реализуется и по крену, и по тангажу (полный «геоцентризм»), но индикация разделяется на два самостоятельных компонента.

Второе изобретение имеет более сложное название: «Командно-пилотажный прибор логической индикации положения и управления летательным аппаратом в пространстве». Авторы патента: Пленцов А. П. и Законова Н. А. Идея индикации тангажа и крена здесь весьма необычна.



Расшифровка обозначений схемы, описание прибора, сравнение с аналогами и дополнительные схемы с небольшими отличиями в оформлении приведены в патенте.

Есть одна общая черта с предыдущим изобретением – концепция геоцентризма для обоих каналов. При этом авиагоризонт имеет только один «символ самолета», как на существующих образцах, но это уже не силуэт, а трехмерная модель – «объемный макет». Если движение по крену получается похожим на реализованное в «обратной» индикации, то кабрирования и пикирования на этом приборе выглядят оригинально.



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

Требования к решению


Прежде чем приступить к написанию кода, определим задание:

1. Необходимо написать функцию drawAttitude(), рисующую средствами canvas индикатор искусственного горизонта на основе изобретения Пленцова А. П. и Законовой Н. А.

2. В качестве аргументов функция принимает контекст canvas, координаты центра индикатора, значения углов крена и тангажа в градусах, радиус лицевой части индикатора.

3. Значения угла тангажа ограничены интервалом от минус 30 до плюс 30 градусов.

4. Значения угла крена ограничены интервалом от минус 45 до плюс 45 градусов.

5. В случае выхода значения аргумента за указанные в пп. 3 и 4 пределы индикатор показывает ближайшее разрешенное значение.

Создание функции


Код функции включает следующие части:

1. Проверка введенных значений на предмет выхода за ограничения.

2. Перевод значений углов в радианы.

3. Масштабирование характерного размера «макета» и шрифта по значению радиуса индикатора.

4. Рисование компонентов:
а) Корпус индикатора.
б) Макет.
в) Шкалы тангажа и крена.

Приведенная ниже функция написана именно в такой последовательности, а ее части разделены комментариями.

Полный код
Код файла index.html:

<!DOCTYPE html>
<html>

<head>
  <title>Attitude</title>
  <script src="js/attitude.js"></script>
</head>

<body>
  <canvas id="drawingCanvas" width="640" height="480"></canvas>
</body>

</html>

Код файла attitude.js:

window.onload = function () {

    let canvas = document.getElementById("drawingCanvas");
    let context = canvas.getContext("2d");
    
    let run = function () {
        drawAttitude(context, 320, 240, 30 * Math.sin(performance.now() / 2000), 45 * Math.sin(performance.now() / 5000), 200);
    }

    let interval = setInterval(run, 1000 / 60);
};


drawAttitude = function (ctx, centreX, centreY, pitch, roll, radius = 100) {
    //Проверка значений по ограничениям:
    if (pitch > 30) pitch = 30;
    if (pitch < -30) pitch = -30;

    if (roll > 45) roll = 45;
    if (roll < -45) roll = -45;
    //перевод в радианы:
    roll *= Math.PI / 180;
    pitch *= Math.PI / 180;
    //размер "макета" и шрифта:
    let vehicleSize = radius * 0.8;
    ctx.font = Math.round(radius / 8) + "px Arial";
    //корпус индикатора и заливка фона:
    ctx.lineWidth = 2;
    ctx.strokeStyle = "Black";
    //нижний полукруг:
    ctx.beginPath();
    ctx.arc(centreX, centreY, radius, 0, Math.PI, false);
    ctx.fillStyle = "Maroon";
    ctx.stroke();
    ctx.fill();
    //верхний полукруг:
    ctx.beginPath();
    ctx.arc(centreX, centreY, radius, 0, Math.PI, true);
    ctx.fillStyle = "SkyBlue";
    ctx.stroke();
    ctx.fill();
    //"макет":
    ctx.beginPath();
    //цвет:
    let topSideIsVisible = (pitch >= 0);
    ctx.strokeStyle = topSideIsVisible ? "Orange" : "Brown";
    ctx.fillStyle = topSideIsVisible ? "Yellow" : "Red";
    ctx.lineWidth = 3;
    //контур
    //проходит через 4 точки, начиная с задней центральной против часовой стрелки,
    //если смотреть сверху:
    ctx.moveTo(centreX, centreY - Math.sin(pitch) * vehicleSize / 2);
    ctx.lineTo(centreX + vehicleSize * Math.cos(roll), centreY + vehicleSize * Math.sin(roll) * Math.cos(pitch));
    ctx.lineTo(centreX, centreY - 2 * Math.sin(pitch) * vehicleSize);
    ctx.lineTo(centreX - vehicleSize * Math.cos(roll), centreY - vehicleSize * Math.sin(roll) * Math.cos(pitch));
    ctx.lineTo(centreX, centreY - Math.sin(pitch) * vehicleSize / 2);
    ctx.stroke();
    ctx.fill();
    //шкала тангажа:
    //верхняя часть:
    ctx.beginPath();
    ctx.strokeStyle = "Black";
    ctx.fillStyle = "Black";
    ctx.lineWidth = 1;
    //текст:
    ctx.fillText(30, centreX - radius * 0.28, centreY - vehicleSize + radius / 20);
    ctx.fillText(20, centreX - radius * 0.28, centreY - vehicleSize * 0.684 + radius / 20);
    ctx.fillText(10, centreX - radius * 0.28, centreY - vehicleSize * 0.348 + radius / 20);
    //метки - линии:
    ctx.moveTo(centreX - radius / 10, centreY - vehicleSize);
    ctx.lineTo(centreX + radius / 10, centreY - vehicleSize);
    ctx.stroke();

    ctx.moveTo(centreX - radius / 10, centreY - vehicleSize * 0.684);
    ctx.lineTo(centreX + radius / 10, centreY - vehicleSize * 0.684);
    ctx.stroke();

    ctx.moveTo(centreX - radius / 10, centreY - vehicleSize * 0.348);
    ctx.lineTo(centreX + radius / 10, centreY - vehicleSize * 0.348);
    ctx.stroke();
    //нижняя часть:
    ctx.beginPath();
    ctx.strokeStyle = "White";
    ctx.fillStyle = "White";
    //текст:
    ctx.fillText(30, centreX - radius * 0.28, centreY + vehicleSize + radius / 20);
    ctx.fillText(20, centreX - radius * 0.28, centreY + vehicleSize * 0.684 + radius / 20);
    ctx.fillText(10, centreX - radius * 0.28, centreY + vehicleSize * 0.348 + radius / 20);
    //метки - линии:
    ctx.moveTo(centreX - radius / 10, centreY + vehicleSize);
    ctx.lineTo(centreX + radius / 10, centreY + vehicleSize);
    ctx.stroke();

    ctx.moveTo(centreX - radius / 10, centreY + vehicleSize * 0.684);
    ctx.lineTo(centreX + radius / 10, centreY + vehicleSize * 0.684);
    ctx.stroke();

    ctx.moveTo(centreX - radius / 10, centreY + vehicleSize * 0.348);
    ctx.lineTo(centreX + radius / 10, centreY + vehicleSize * 0.348);
    ctx.stroke();

    //шкала крена:
    ctx.lineWidth = 2;

    //+-15 градусов:
    ctx.fillText(15, centreX + radius * 0.6, centreY + radius * 0.22);
    ctx.moveTo(centreX + 0.966 * 0.8 * radius, centreY + 0.259 * 0.8 * radius);
    ctx.lineTo(centreX + 0.966 * 0.95 * radius, centreY + 0.259 * 0.95 * radius);

    ctx.fillText(15, centreX - radius * 0.75, centreY + radius * 0.22);
    ctx.moveTo(centreX - 0.966 * 0.8 * radius, centreY + 0.259 * 0.8 * radius);
    ctx.lineTo(centreX - 0.966 * 0.95 * radius, centreY + 0.259 * 0.95 * radius);

    //+-30 градусов:
    ctx.moveTo(centreX + 0.866 * 0.8 * radius, centreY + 0.5 * 0.8 * radius);
    ctx.lineTo(centreX + 0.866 * 0.95 * radius, centreY + 0.5 * 0.95 * radius);

    ctx.moveTo(centreX - 0.866 * 0.8 * radius, centreY + 0.5 * 0.8 * radius);
    ctx.lineTo(centreX - 0.866 * 0.95 * radius, centreY + 0.5 * 0.95 * radius);

    //+-45 градусов:
    ctx.moveTo(centreX + 0.707 * 0.8 * radius, centreY + 0.707 * 0.8 * radius);
    ctx.lineTo(centreX + 0.707 * 0.95 * radius, centreY + 0.707 * 0.95 * radius);

    ctx.moveTo(centreX - 0.707 * 0.8 * radius, centreY + 0.707 * 0.8 * radius);
    ctx.lineTo(centreX - 0.707 * 0.95 * radius, centreY + 0.707 * 0.95 * radius);

    ctx.stroke();
}



Наиболее сложным для понимания представляется код рисования «макета». Рассмотрим его более детально. В качестве макета решено использовать плоскую симметричную фигуру, имеющую в плане форму стрелки.



Верхняя и нижняя поверхности макета отличаются друг от друга по цвету контура и заливки. Выбор текущего цветового решения – первая часть кода.
Далее происходит построение контура фигуры.

Наиболее сложная задача – определение координат проекций вершин фигуры на плоскость YOZ. Именно ее решают выражения с тригонометрическими функциями. Обход вершин в коде производится по порядку их нумерации на рисунке.

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

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

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



Заключение


Строго говоря, представленный выше код визуализации крена и тангажа следует называть индикацией «по мотивам» изобретения Пленцова А. П. и Законовой Н. А. Некоторые отступления от оригинальных схем сделаны с целью упрощения задачи, другие – для улучшения реализации.

Представленный индикатор далек от идеала в плане дизайна. Не являются оптимальными по какому-либо объективному критерию и принятые ограничения отображаемых значений. Тем не менее, задачу создания демонстратора интересной технологии можно считать решенной.




Комментарии (0):