Skip to content

6. Холст (Canvas)

AlexGyver edited this page Feb 25, 2024 · 5 revisions

Canvas (холст)

В библиотеке есть возможность "рисовать" в окне браузера командами с устройства при помощи HTML Canvas, а также получать координаты кликов по холсту, что даёт безграничные возможности по визуализации данных и взаимодействию с ними, фактически - беспроводной цветной сенсорный дисплей. Сетевой трафик минимизирован до коротких команд, так что отправка даже больших пакетов графики не потребляет много помяти. Также частично реализован Processing API как более удобный для рисования, чем нативный HTML Canvas API. Примеры использования:

  • Вывод кастомных "графиков"
  • Вывод схемы/карты помещения с точками интереса
  • Графическое представление показаний датчика (наклон, уровень жидкости...)
  • Отображение позиции "головки" станка или других механизмов и их частей
  • Удалённый "тачпад" для управления устройством

Особенности API

  • Независимо от указанного в программе размера холст будет пропорционально отмасштабирован и вписан в свой контейнер (по ширине UI или внутри виджета), также масштаб будет увеличен на устройствах с увеличенной плотностью пикселей. Таким образом графика на холсте будет выглядеть одинаково и чётко при любом размере экрана или виджета, а виртуальный размер холста задаётся для удобства программиста, чтобы ориентироваться в размере своего холста. В то же время реальный размер холста в пикселях будет отличаться от "виртуального" размера, заданного в программе (подробнее ниже)!
  • Начало координат - левый верхний угол, ось Y направлена вниз
  • Отрицательные числа при задании координат графики через функции Canvas (не через кастомный js код) вычитаются из ширины холста, таким образом point(-1, -1) установит точку в правый нижний угол
  • Список функций см. на главной странице документации

Создание холста

Холст создаётся в билдере как обычный виджет:

void build(gh::Builder& b) {
  b.Canvas();           // пустой холст, 400x300px
  b.Canvas(600, 600);   // пустой холст 600x600px
}

Цвет

Функции рисования принимают цвет в формате 24-бит uint32_t, например void stroke(uint32_t hex), вторым аргументом может идти прозрачность 0..255 void stroke(uint32_t hex, uint8_t a). Также в качестве цвета можно передавать встроенный класс gh::Color, как внешнюю переменную или сразу конструктором, например:

gh::Color color;
cv.stroke(color);

cv.stroke(gh::Color(255, 0, 0));          // RGB, красный
cv.stroke(gh::Color(50, 200, 255, true)); // HSV, пастельный зелёный
cv.stroke(gh::Color(200));                // Gray, светло серый

cv.stroke(0xff0000, 200); // прозрачность 200 из 255

Рисование

Для рисования внутри билдера нужно соблюдать следующий строгий порядок вызова функций:

  1. Создать объект холста gh::Canvas
  2. Передать его в BeginCanvas()
  3. Рисовать
  4. Вызвать EndCanvas()

Между созданием gh::Canvas и завершением рисования EndCanvas() категорически не должно быть других виджетов!

Функции вывода графики, вызванные внутри билдера, добавляют данные сразу к буферу билдера и не создают новых строк, что уменьшает расход и фрагментацию памяти

void build() {
  gh::Canvas cv;                   // создать холст
  hub.BeginCanvas(300, 300, &cv);  // начать рисование
  // линии крест-накрест
  cv.line(0, 0, -1, -1);
  cv.line(0, -1, -1, 0);
  hub.EndCanvas();                 // закончить
}

Обновление

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

  1. Создать объект обновления холста gh::CanvasUpdate с указанием имени и объекта GyverHub
  2. Рисовать
  3. Вызвать send()
void build(gh::Builder& b) {
  b.Canvas_(F("cv"), 300, 300);  // холст с именем cv, 300x300
}

void loop() {
  static gh::Timer tmr(300);
  if (tmr) {
    gh::CanvasUpdate cv("cv", &hub);
    // вывести круг случайного цвета в случайном месте
    cv.fill(gh::Color(random(255), random(255), random(255)), random(100, 255));
    cv.circle(random(0, 600), random(0, 600), random(10, 50));
    cv.send();
  }
}

Обновление в билдере

Функции рисования на gh::Canvas внутри билдера работают только в том случае, если билдер вызван для сборки ПУ, в этот же момент собираются остальные виджеты. Если билдер вызван для обработки действия - функции рисования в теле билдера игнорируются, но можно отправлять обновления через gh::CanvasUpdate. Пример: по клику по кнопке добавлять кружочки на холст с изначальным рисунком:

void build(gh::Builder& b) {
    if (b.Button().click()) {  // клик по кнопке (обработка действия)
        gh::CanvasUpdate cv("cv", &hub);
        // случайный кружок
        cv.circle(random(0, 30) * 10, random(0, 30) * 10, random(5, 30));
        cv.send();
    }

    gh::Canvas cv;                           // создать холст
    b.BeginCanvas_(F("cv"), 300, 300, &cv);  // и начать рисование

    // это рисование будет выполнено только при сборке панели управления
    cv.line(0, 0, -1, -1);  // линии крест-накрест
    cv.line(0, -1, -1, 0);
    b.EndCanvas();  // закончить
}

Обработка кликов

GyverHub позволяет получить координаты кликов мышкой (и касания пальцем) по холсту в координатах заданного в программе размера холста. Для этого нужно создать переменную обработчика позиции типа gh::Pos и подключить её к виджету холста. Действие можно обработать через click() с холста, либо changed() с позиции:

void build(gh::Builder& b) {
    gh::Pos pos;
    gh::Canvas cv;
    if (b.BeginCanvas(400, 600, &cv, &pos).click()) {
        Serial.print("click: ");
        Serial.print(pos.x);
        Serial.print(',');
        Serial.println(pos.y);
    }
    b.EndCanvas();
    
    // ИЛИ
    if (pos.changed()) {
        Serial.print("pos: ");
        Serial.print(pos.x);
        Serial.print(',');
        Serial.println(pos.y);
    }
}

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

gh::Pos pos;

void build(gh::Builder& b) {
  b.Canvas_(F("cv"), 400, 600, nullptr, &pos);
}

void loop() {
  hub.tick();

  if (pos.changed()) {
    Serial.println(pos.x);
    Serial.println(pos.y);

    // выведем кружок в точку клика
    gh::CanvasUpdate cv("cv", &hub);
    // случайный кружок
    cv.circle(pos.x, pos.y, 10);
    cv.send();
  }
}

Можно реагировать на клики прямо в билдере:

void build(gh::Builder& b) {
    gh::Canvas cv;
    gh::Pos pos;  // создадим локально, глобально в этом случае не нужно

    b.BeginCanvas_(F("cv"), 400, 400, &cv, &pos);
    cv.stroke(0xff0000);
    cv.strokeWeight(5);
    cv.line(0, 0, -1, -1);
    cv.line(0, -1, -1, 0);
    b.EndCanvas();

    if (pos.changed()) {
        // сюда попадаем при клике по холсту
        gh::CanvasUpdate cv("cv", &hub);
        cv.circle(pos.x, pos.y, 10);  // кружок в точку клика
        cv.send();
    }
}

Клики по геометрии

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

void build() {
    gh::Pos pos;
    gh::Canvas cv;

    b.BeginCanvas_(F("cv"), 300, 200, &cv, &pos);
    cv.stroke(0xff0000);
    cv.fill(0);
    cv.strokeWeight(5);
    cv.rect(50, 50, 100, 100);
    cv.circle(200, 100, 30);
    b.EndCanvas();

    if (pos.changed()) {
        gh::CanvasUpdate cv("cv", &hub);
        if (pos.inRect(50, 50, 100, 100)) {
            static bool f = 0;
            f = !f;
            cv.fill(f ? 0xff0000 : 0);
            cv.rect(50, 50, 100, 100);
        }
        if (pos.inCircle(200, 100, 30)) {
            static bool f = 0;
            f = !f;
            cv.fill(f ? 0xff0000 : 0);
            cv.circle(200, 100, 30);
        }
        cv.send();
    }
}

Текстовые команды

Свои команды рисования можно вводить в текстовом виде в функцию .custom(). Особенности:

  • В коде холста текущий Canvas всегда называется cv, а его Context - cx
  • Не работает задание отрицательных координат, т.к. функции вызываются вне парсера Canvas API
  • Для ввода строковых значений допускаются как одинарные кавычки ('/\'), так и двойные ("/\")
  • Область видимости созданных переменных - внутри всего блока между созданием Canvas и его отправкой/завершением
  • В области видимости холста есть функция scale(), при помощи которой можно привести виртуальные координаты к реальным. Нужно просто умножить значение на эту функцию

Пример с горизонтальным градиентом от красного к белому на весь холст:

void build(gh::Builder& b) {
    gh::Canvas cv;
    b.BeginCanvas(400, 300, &cv);
    // градиент шириной 400 виртуальных пикселей - масштабируем в реальные
    cv.custom(F(
        "let grd=cx.createLinearGradient(0,0,400*scale(),0);"
        "grd.addColorStop(0,'red');"
        "grd.addColorStop(1,'white');"
        "cx.fillStyle = grd;"));
    cv.fillRect(0, 0, -1, -1);
    b.EndCanvas();
}

Вывод изображений

Нужно указать путь к файлу с изображением:

void build(gh::Builder& b) {
    gh::Canvas cv;
    b.BeginCanvas_("cv2", 400, 300, &cv);
    cv.drawImage("/image.jpg", 0, 0, 400);
    b.EndCanvas();
}

Также можно вывести напрямую кадр с камеры, перехватив обработчик Fetch: TODO