# 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