# Canvas (холст) В библиотеке есть возможность "рисовать" в окне браузера командами с устройства при помощи HTML Canvas, а также получать координаты кликов по холсту, что даёт безграничные возможности по визуализации данных и взаимодействию с ними, фактически - **беспроводной цветной сенсорный дисплей**. Сетевой трафик минимизирован до коротких команд, так что отправка даже больших пакетов графики не потребляет много помяти. Также частично реализован [Processing API](https://processing.org/reference/) как более удобный для рисования, чем нативный [HTML Canvas API](https://www.w3schools.com/tags/ref_canvas.asp). Примеры использования: - Вывод кастомных "графиков" - Вывод схемы/карты помещения с точками интереса - Графическое представление показаний датчика (наклон, уровень жидкости...) - Отображение позиции "головки" станка или других механизмов и их частей - Удалённый "тачпад" для управления устройством ## Особенности API - Независимо от указанного в программе размера холст будет пропорционально отмасштабирован и вписан в свой контейнер (по ширине UI или внутри виджета), также масштаб будет увеличен на устройствах с увеличенной плотностью пикселей. Таким образом графика на холсте будет выглядеть одинаково и чётко при любом размере экрана или виджета, а *виртуальный* размер холста задаётся для удобства программиста, чтобы ориентироваться в размере своего холста. В то же время реальный размер холста в пикселях будет отличаться от "виртуального" размера, заданного в программе (подробнее ниже)! - Начало координат - левый верхний угол, ось Y направлена вниз - Отрицательные числа при задании координат графики **через функции Canvas** (не через кастомный js код) вычитаются из ширины холста, таким образом `point(-1, -1)` установит точку в правый нижний угол - Список функций см. на главной странице документации ## Создание холста Холст создаётся в билдере как обычный виджет: ```cpp 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`, как внешнюю переменную или сразу конструктором, например: ```cpp 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()` категорически не должно быть других виджетов! > Функции вывода графики, вызванные внутри билдера, добавляют данные сразу к буферу билдера и не создают новых строк, что уменьшает расход и фрагментацию памяти ```cpp 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()` ```cpp 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`. Пример: по клику по кнопке добавлять кружочки на холст с изначальным рисунком: ```cpp 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()` с позиции: ```cpp 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()`: ```cpp 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(); } } ``` Можно реагировать на клики прямо в билдере: ```cpp 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` также позволяет обрабатывать клики внутри геометрии. Пример холста, на который выведен прямоугольник и окружность, при клике они меняют цвет заливки: ```cpp 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(); } } ``` ### Текстовые команды Свои [команды рисования](https://www.w3schools.com/tags/ref_canvas.asp) можно вводить в текстовом виде в функцию `.custom()`. Особенности: - В коде холста текущий *Canvas* всегда называется `cv`, а его *Context* - `cx` - Не работает задание отрицательных координат, т.к. функции вызываются вне парсера Canvas API - Для ввода строковых значений допускаются как одинарные кавычки (`'`/`\'`), так и двойные (`"`/`\"`) - Область видимости созданных переменных - внутри всего блока между созданием `Canvas` и его отправкой/завершением - В области видимости холста есть функция `scale()`, при помощи которой можно привести виртуальные координаты к реальным. Нужно просто умножить значение на эту функцию Пример с горизонтальным градиентом от красного к белому на весь холст: ```cpp 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(); } ``` ### Вывод изображений Нужно указать путь к файлу с изображением: ```cpp 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