781 lines
20 KiB
C++
781 lines
20 KiB
C++
//#include "Commander.h"
|
|
|
|
#include "fptrig.h"
|
|
|
|
#include <Arduino.h>
|
|
#include <WiFi.h>
|
|
#include <Adafruit_SSD1351.h>
|
|
#include <SPI.h>
|
|
#include <vector>
|
|
|
|
#include "screen-settings.h"
|
|
#include "colors.h"
|
|
|
|
#include "FixedPoint.h"
|
|
|
|
|
|
typedef FixedPoint<1000000> Decimal;
|
|
|
|
constexpr Decimal operator "" _d(long double v) {
|
|
return v;
|
|
}
|
|
|
|
constexpr Decimal operator "" _d(unsigned long long int v) {
|
|
return v;
|
|
}
|
|
|
|
static_assert(1_d == 1.0_d, "1 == 1.0");
|
|
static_assert(1_d == 1_d, "1 == 1");
|
|
static_assert(1_d + 1_d == 2_d, "1 + 1 == 2");
|
|
static_assert(2_d * 3_d == 6_d, "2 * 3 == 6");
|
|
static_assert(1.25_d * 2.3_d == 2.875_d, "1.25 * 2.3 == 2.875");
|
|
static_assert(.9_d / 3_d == .3_d, ".9 / 3 == .3");
|
|
|
|
SPIClass hspi(HSPI);
|
|
Adafruit_SSD1351 screen(SCREEN_WIDTH, SCREEN_HEIGHT, &hspi, CS_PIN, DC_PIN, RST_PIN);
|
|
|
|
//Commander commander;
|
|
|
|
|
|
struct Point {
|
|
Decimal x;
|
|
Decimal y;
|
|
};
|
|
|
|
enum InterpolationMethod {
|
|
NEAREST_NEIGHBOR,
|
|
LINEAR,
|
|
COSINE,
|
|
CUBIC_HERMITE,
|
|
MONOTONIC_CUBIC_HERMITE,
|
|
};
|
|
|
|
enum Visualisation {
|
|
LINE_DRAW, // todo: this is just for debugging
|
|
BAR,
|
|
LINE,
|
|
AREA,
|
|
AREA_WITH_LINE,
|
|
};
|
|
|
|
struct CubicCurve {
|
|
Decimal a;
|
|
Decimal b;
|
|
Decimal c;
|
|
Decimal d;
|
|
|
|
Decimal GetValueAt(Decimal x) const {
|
|
return a * x * x * x
|
|
+ b * x * x
|
|
+ c * x
|
|
+ d;
|
|
}
|
|
|
|
Decimal GetSlopeAt(Decimal x) const {
|
|
return 3_d * a * x * x
|
|
+ 2_d * b * x
|
|
+ c;
|
|
}
|
|
|
|
bool const IsEmpty() const {
|
|
return a == 0 && b == 0 && c == 0 && d == 0;
|
|
}
|
|
|
|
static constexpr CubicCurve make(Decimal x1, Decimal y1, Decimal m1, Decimal x2, Decimal y2, Decimal m2) {
|
|
Decimal a = (m1 + m2 - 2 * (y2 - y1) / (x2 - x1)) / ((x1 - x2) * (x1 - x2));
|
|
Decimal b = (m2 - m1) / (2 * (x2 - x1)) - 3 / 2 * (x1 + x2) * a;
|
|
Decimal c = m1 - 3 * x1 * x1 * a - 2 * x1 * b;
|
|
Decimal d = y1 - x1 * x1 * x1 * a - x1 * x1 * b - x1 * c;
|
|
return CubicCurve{a, b, c, d};
|
|
}
|
|
|
|
static constexpr CubicCurve makeLeftEnd(Decimal x1, Decimal y1, Decimal x2, Decimal y2, Decimal m2) {
|
|
// set second derivative to 0 at first point
|
|
// todo: actually implement this
|
|
|
|
/*
|
|
* f(x) = ax^3 + bx^2 + cx + d
|
|
* f'(x) = 3ax^2 + 2bx + c
|
|
* f''(x) = 6ax + 2b
|
|
*
|
|
y1 = a*x1^3 + b*x1^2 + c*x1 + d
|
|
y2 = a*x2^3 + b*x2^2 + c*x2 + d
|
|
m2 = 3*a*x2^2 + 2*b*x2 + c
|
|
0 = 6*a*x1 + 2*b
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
|
|
return make(x1, y1, 0, x2, y2, m2);
|
|
|
|
}
|
|
|
|
static constexpr CubicCurve makeRightEnd(Decimal x1, Decimal y1, Decimal m1, Decimal x2, Decimal y2) {
|
|
// set second derivative to 0 at second point
|
|
// todo: actually implement this
|
|
return make(x1, y1, m1, x2, y2, 0);
|
|
}
|
|
|
|
};
|
|
|
|
struct Series {
|
|
RGB color;
|
|
String label;
|
|
Visualisation visualisation;
|
|
InterpolationMethod interpolation; // does not apply to "bar" visualisation
|
|
// point x and y values are scaled with 1000
|
|
std::vector<Point> points;
|
|
|
|
mutable CubicCurve cached_cubic_curve;
|
|
mutable Decimal cached_cubic_curve_from;
|
|
mutable Decimal cached_cubic_curve_to;
|
|
|
|
std::tuple<Decimal, bool> Interpolate(Decimal at) const {
|
|
if (points.size() < 2) {
|
|
return std::make_tuple(0, false);
|
|
}
|
|
if (at < points.front().x || at > points.back().x) {
|
|
return std::make_tuple(0, false);
|
|
}
|
|
switch (interpolation) {
|
|
case NEAREST_NEIGHBOR: return InterpolateNearestNeighbour(at);
|
|
case LINEAR: return InterpolateLinear(at);
|
|
case COSINE: return InterpolateCosine(at);
|
|
case CUBIC_HERMITE: return InterpolateCubicHermite(at);
|
|
case MONOTONIC_CUBIC_HERMITE: return InterpolateMonotonicCubicHermite(at);
|
|
}
|
|
|
|
return std::make_tuple(0, false);
|
|
}
|
|
|
|
std::tuple<Decimal, bool> InterpolateNearestNeighbour(Decimal at) const {
|
|
assert(points.size() >= 2);
|
|
|
|
if (at == points.front().x) {
|
|
return std::make_tuple(points.front().y, true);
|
|
}
|
|
|
|
for (auto current = points.begin() + 1; current < points.end(); current++) {
|
|
if (at == current->x) {
|
|
return std::make_tuple(current->y, true);
|
|
}
|
|
auto prev = current - 1;
|
|
if (at > prev->x && at < current->x) {
|
|
if (at - prev->x < current->x - at) {
|
|
return std::make_tuple(prev->y, true);
|
|
} else {
|
|
return std::make_tuple(current->y, true);
|
|
}
|
|
}
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
std::tuple<Decimal, bool> InterpolateLinear(Decimal at) const {
|
|
assert(points.size() >= 2);
|
|
|
|
if (at == points.front().x) {
|
|
return std::make_tuple(points.front().y, true);
|
|
}
|
|
|
|
for (auto current = points.begin() + 1; current < points.end(); current++) {
|
|
if (at == current->x) {
|
|
return std::make_tuple(current->y, true);
|
|
}
|
|
auto prev = current - 1;
|
|
if (at > prev->x && at < current->x) {
|
|
Decimal dist = current->x - prev->x;
|
|
Decimal dist_val = at - prev->x;
|
|
Decimal val = (current->y * dist_val + prev->y * (dist - dist_val)) / dist;
|
|
return std::make_tuple(val, true);
|
|
}
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
std::tuple<Decimal, bool> InterpolateCosine(Decimal at) const {
|
|
assert(points.size() >= 2);
|
|
|
|
if (at == points.front().x) {
|
|
return std::make_tuple(points.front().y, true);
|
|
}
|
|
|
|
for (auto current = points.begin() + 1; current < points.end(); current++) {
|
|
if (at == current->x) {
|
|
return std::make_tuple(current->y, true);
|
|
}
|
|
auto prev = current - 1;
|
|
if (at > prev->x && at < current->x) {
|
|
Decimal dist = current->x - prev->x;
|
|
Decimal dist_val = at - prev->x;
|
|
Decimal mu =
|
|
0; // todo: rework this to work with the FixedPoint class (dist - fpcos((1 << 14) * dist_val / dist) * dist / 8192 / 2);
|
|
if (mu > dist) {
|
|
Serial.print("mu = ");
|
|
Serial.println(mu.toString().c_str());
|
|
Serial.print("dist = ");
|
|
Serial.println(dist.toString().c_str());
|
|
for (;;);
|
|
}
|
|
assert(mu <= dist);
|
|
Decimal val = (prev->y * (dist - mu) + current->y * mu) / dist;
|
|
return std::make_tuple(val, true);
|
|
}
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
CubicCurve &GetCubicCurve(Decimal at, bool monotonic) const {
|
|
if (!cached_cubic_curve.IsEmpty()) {
|
|
if (at >= cached_cubic_curve_from && at <= cached_cubic_curve_to) {
|
|
return cached_cubic_curve;
|
|
}
|
|
}
|
|
|
|
for (auto p2 = points.begin() + 1; p2 < points.end(); p2++) {
|
|
auto p1 = p2 - 1;
|
|
|
|
if (at >= p1->x && at <= p2->x) {
|
|
cached_cubic_curve_from = p1->x;
|
|
cached_cubic_curve_to = p2->x;
|
|
|
|
Decimal m_after_p1 = (p2->y - p1->y) / (p2->x - p1->x);
|
|
Decimal m_before_p2 = (p2->y - p1->y) / (p2->x - p1->x);
|
|
|
|
Decimal m_before_p1, m_after_p2, m1, m2;
|
|
|
|
auto p0 = p1 - 1;
|
|
auto p3 = p2 + 1;
|
|
bool is_beginning = p0 < points.begin();
|
|
bool is_end = p3 >= points.end();
|
|
|
|
if (!is_beginning) {
|
|
m_before_p1 = (p1->y - p0->y) / (p1->x - p0->x);
|
|
|
|
if (monotonic &&
|
|
(p0->y == p1->y
|
|
|| p1->y == p2->y
|
|
|| (p0->y < p1->y && p2->y < p1->y)
|
|
|| (p0->y > p1->y && p2->y > p1->y)
|
|
)) {
|
|
m1 = 0;
|
|
} else {
|
|
m1 = (m_before_p1 + m_after_p1) / 2; // todo: use a mean that is weighted towards 0?
|
|
}
|
|
|
|
}
|
|
|
|
if (!is_end) {
|
|
m_after_p2 = (p3->y - p2->y) / (p3->x - p2->x);
|
|
|
|
if (monotonic &&
|
|
(p1->y == p2->y
|
|
|| p2->y == p3->y
|
|
|| (p1->y < p2->y && p3->y < p2->y)
|
|
|| (p1->y > p2->y && p3->y > p2->y)
|
|
)) {
|
|
m1 = 0;
|
|
} else {
|
|
m2 = (m_before_p2 + m_after_p2) / 2; // todo: use a mean that is weighted towards 0?
|
|
}
|
|
}
|
|
|
|
if (is_beginning) {
|
|
cached_cubic_curve = CubicCurve::makeLeftEnd(p1->x, p1->y, p2->x, p2->y, m2);
|
|
} else if (is_end) {
|
|
cached_cubic_curve = CubicCurve::makeRightEnd(p1->x, p1->y, m1, p2->x, p2->y);
|
|
} else {
|
|
cached_cubic_curve = CubicCurve::make(p1->x, p1->y, m1, p2->x, p2->y, m2);
|
|
}
|
|
|
|
return cached_cubic_curve;
|
|
}
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
std::tuple<Decimal, bool> InterpolateCubicHermite(Decimal at) const {
|
|
assert(points.size() >= 2);
|
|
|
|
if (points.size() == 2) {
|
|
return InterpolateLinear(at);
|
|
}
|
|
|
|
if (at < points.front().x || at > points.back().x) {
|
|
return std::make_tuple(0, false);
|
|
}
|
|
|
|
CubicCurve curve = GetCubicCurve(at, false);
|
|
|
|
return std::make_tuple(curve.GetValueAt(at), true);
|
|
}
|
|
|
|
|
|
std::tuple<Decimal, bool> InterpolateMonotonicCubicHermite(Decimal at) const {
|
|
assert(points.size() >= 2);
|
|
|
|
if (points.size() == 2) {
|
|
return InterpolateLinear(at);
|
|
}
|
|
|
|
if (at < points.front().x || at > points.back().x) {
|
|
return std::make_tuple(0, false);
|
|
}
|
|
|
|
CubicCurve curve = GetCubicCurve(at, true);
|
|
|
|
return std::make_tuple(curve.GetValueAt(at), true);
|
|
}
|
|
|
|
};
|
|
|
|
#define PADDING_BETWEEN_NUMBERS_AND_GRAPH 5
|
|
#define PADDING_BETWEEN_NUMBERS 5
|
|
|
|
[[gnu::pure]] Decimal find_step(Decimal step) {
|
|
int exp = 0;
|
|
|
|
while (step < 1) {
|
|
step *= 10;
|
|
exp--;
|
|
}
|
|
|
|
while (step >= 10) {
|
|
step /= 10;
|
|
exp++;
|
|
}
|
|
|
|
if (step > 5) {
|
|
step = 10;
|
|
} else if (step > 2.5) {
|
|
step = 5;
|
|
} else if (step > 2) {
|
|
step = 2.5;
|
|
} else if (step > 1) {
|
|
step = 2;
|
|
} else {
|
|
step = 1;
|
|
}
|
|
|
|
return step.exp10(exp);
|
|
}
|
|
|
|
struct Chart {
|
|
RGB background_color;
|
|
RGB foreground_color;
|
|
bool show_axes;
|
|
bool show_labels;
|
|
bool show_numbers;
|
|
bool auto_range_ordinate;
|
|
bool auto_range_abscissa;
|
|
Decimal ordinate_from;
|
|
Decimal ordinate_to;
|
|
Decimal abscissa_from;
|
|
Decimal abscissa_to;
|
|
|
|
std::vector<Series> series;
|
|
|
|
std::tuple<Decimal, Decimal> GetAbscissaRange() const {
|
|
Decimal from = abscissa_from;
|
|
Decimal to = abscissa_to;
|
|
if (auto_range_abscissa) {
|
|
from = INT32_MAX;
|
|
to = INT32_MIN;
|
|
for (const auto &s : series) {
|
|
for (const auto &p : s.points) {
|
|
if (p.x < from) {
|
|
from = p.x;
|
|
}
|
|
if (p.x > to) {
|
|
to = p.x;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return std::make_tuple(from, to);
|
|
}
|
|
|
|
std::tuple<Decimal, Decimal> GetOrdinateRange() const {
|
|
Decimal from = ordinate_from;
|
|
Decimal to = ordinate_to;
|
|
if (auto_range_ordinate) {
|
|
from = INT32_MAX;
|
|
to = INT32_MIN;
|
|
for (const auto &s : series) {
|
|
for (const auto &p : s.points) {
|
|
if (p.y < from) {
|
|
from = p.y;
|
|
}
|
|
if (p.y > to) {
|
|
to = p.y;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return std::make_tuple(from, to);
|
|
}
|
|
|
|
void Draw(Adafruit_GFX &target,
|
|
int16_t rect_x, int16_t rect_y, uint16_t rect_w, uint16_t rect_h) {
|
|
target.fillRect(rect_x, rect_y, rect_w, rect_h, background_color.to_565());
|
|
target.setTextSize(1);
|
|
|
|
int16_t canvas_x = rect_x;
|
|
int16_t canvas_y = rect_y;
|
|
uint16_t canvas_width = rect_w;
|
|
uint16_t canvas_height = rect_h;
|
|
|
|
Decimal ord_min, ord_max, abs_min, abs_max;
|
|
std::tie(ord_min, ord_max) = GetOrdinateRange();
|
|
std::tie(abs_min, abs_max) = GetAbscissaRange();
|
|
|
|
if (show_labels) {
|
|
// todo: draw labels, change canvas size accordingly
|
|
}
|
|
|
|
if (show_numbers) {
|
|
DrawNumbers(target, canvas_x, canvas_y, canvas_width, canvas_height);
|
|
}
|
|
|
|
DrawGraph(target, canvas_x, canvas_y, canvas_width, canvas_height);
|
|
|
|
if (show_axes) {
|
|
int16_t ord_pos = val_to_screen(0, canvas_x, canvas_width, abs_min, abs_max, false);
|
|
target.drawLine(ord_pos, canvas_y, ord_pos, canvas_y + canvas_height, foreground_color.to_565());
|
|
|
|
int16_t abs_pos = val_to_screen(0, canvas_y, canvas_height, ord_min, ord_max, true);
|
|
target.drawLine(canvas_x, abs_pos, canvas_x + canvas_width, abs_pos, foreground_color.to_565());
|
|
}
|
|
|
|
}
|
|
|
|
void DrawGraph(Adafruit_GFX &target,
|
|
int16_t &canvas_x,
|
|
int16_t &canvas_y,
|
|
uint16_t &canvas_width,
|
|
uint16_t &canvas_height) {
|
|
|
|
Decimal ord_min, ord_max, abs_min, abs_max;
|
|
std::tie(ord_min, ord_max) = GetOrdinateRange();
|
|
std::tie(abs_min, abs_max) = GetAbscissaRange();
|
|
|
|
std::vector<Series *> normal_series;
|
|
for (auto &s : series) {
|
|
if (s.visualisation == Visualisation::LINE_DRAW || s.visualisation == Visualisation::BAR) {
|
|
continue;
|
|
}
|
|
normal_series.push_back(&s);
|
|
}
|
|
|
|
enum PixelType {
|
|
NONE,
|
|
LINE,
|
|
AREA,
|
|
};
|
|
|
|
std::vector<std::vector<std::tuple<PixelType, const Series *>>> pixels(canvas_height);
|
|
assert(pixels.size() == canvas_height);
|
|
std::vector<std::tuple<int16_t, const Series *>> values;
|
|
if (!normal_series.empty()) {
|
|
for (uint16_t x = 0; x < canvas_width; x++) {
|
|
Decimal abs = (abs_min * (canvas_width - 1 - x) + abs_max * x) / (canvas_width - 1);
|
|
values.clear();
|
|
for (auto &p : pixels) {
|
|
p.clear();
|
|
}
|
|
|
|
for (auto s : normal_series) {
|
|
Decimal val;
|
|
bool success;
|
|
std::tie(val, success) = s->Interpolate(abs);
|
|
if (!success) continue;
|
|
int16_t y = static_cast<int16_t>(((val - ord_min) * (canvas_height - 1) / (ord_max - ord_min)).toInt());
|
|
values.emplace_back(y, s);
|
|
}
|
|
|
|
for (const auto &item : values) {
|
|
// todo: combine this loop with the previous
|
|
int16_t y;
|
|
const Series *s;
|
|
std::tie(y, s) = item;
|
|
assert(y >= 0 && y < canvas_height);
|
|
|
|
if (s->visualisation == Visualisation::LINE || s->visualisation == Visualisation::AREA_WITH_LINE) {
|
|
pixels[y].emplace_back(PixelType::LINE, s);
|
|
// todo: this leaves gaps between line segments
|
|
}
|
|
if (s->visualisation == Visualisation::AREA || s->visualisation == Visualisation::AREA_WITH_LINE) {
|
|
for (size_t i = 0; i <= y; ++i) {
|
|
pixels[i].emplace_back(PixelType::AREA, s);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
std::vector<RGB> line_pixels;
|
|
std::vector<RGB> area_pixels;
|
|
const Series *s;
|
|
PixelType t;
|
|
for (int16_t y = 0; y < canvas_height; ++y) {
|
|
line_pixels.clear();
|
|
area_pixels.clear();
|
|
auto pixel = pixels[y];
|
|
for (const auto &item : pixel) {
|
|
std::tie(t, s) = item;
|
|
if (t == PixelType::LINE) {
|
|
line_pixels.push_back(s->color);
|
|
} else if (t == PixelType::AREA) {
|
|
area_pixels.push_back(s->color);
|
|
}
|
|
}
|
|
|
|
int16_t screen_x = canvas_x + x;
|
|
int16_t screen_y = canvas_y + canvas_height - y;
|
|
|
|
if (!line_pixels.empty()) {
|
|
// draw line pixel
|
|
RGB color = RGB::blend(line_pixels);
|
|
target.drawPixel(screen_x, screen_y, color.to_565());
|
|
} else if (!area_pixels.empty()) {
|
|
// draw area pixel
|
|
RGB color = RGB::blend(area_pixels);
|
|
uint16_t c2 = color.alpha_blend_over_to565(background_color, 100); // TODO: make configurable
|
|
target.drawPixel(screen_x, screen_y, c2);
|
|
} else {
|
|
// draw background pixel
|
|
target.drawPixel(screen_x, screen_y, background_color.to_565());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto &s : series) {
|
|
if (s.visualisation == Visualisation::LINE_DRAW) {
|
|
const Point *last = nullptr;
|
|
for (const auto &p : s.points) {
|
|
if (last != nullptr) {
|
|
int16_t tox = val_to_screen(p.x, canvas_x, canvas_width, abs_min, abs_max, false);
|
|
int16_t toy = val_to_screen(p.y, canvas_y, canvas_height, ord_min, ord_max, true);
|
|
int16_t fromx = val_to_screen(last->x, canvas_x, canvas_width, abs_min, abs_max, false);
|
|
int16_t fromy = val_to_screen(last->y, canvas_y, canvas_height, ord_min, ord_max, true);
|
|
target.drawLine(fromx, fromy, tox, toy, s.color.to_565());
|
|
}
|
|
last = &p;
|
|
}
|
|
} else if (s.visualisation == Visualisation::BAR) {
|
|
// todo: draw bar series
|
|
}
|
|
}
|
|
|
|
/*for (const auto &s : series) {
|
|
if (s.visualisation != Visualisation::BAR) {
|
|
for (const auto &p : s.points) {
|
|
int16_t x = val_to_screen(p.x, canvas_x, canvas_width, abs_min, abs_max, false);
|
|
int16_t y = val_to_screen(p.y, canvas_y, canvas_height, ord_min, ord_max, true);
|
|
target.drawCircle(x, y, 1, s.color.to_565());
|
|
}
|
|
}
|
|
}*/
|
|
}
|
|
|
|
void DrawNumbers(Adafruit_GFX &target,
|
|
int16_t &canvas_x,
|
|
int16_t &canvas_y,
|
|
uint16_t &canvas_width,
|
|
uint16_t &canvas_height) const {
|
|
Decimal ord_min, ord_max, abs_min, abs_max;
|
|
std::tie(ord_min, ord_max) = GetOrdinateRange();
|
|
std::tie(abs_min, abs_max) = GetAbscissaRange();
|
|
|
|
int16_t dummy_x, dummy_y;
|
|
uint16_t text_width, text_height;
|
|
|
|
target.getTextBounds("0", 0, 0, &dummy_x, &dummy_y, &text_width, &text_height);
|
|
|
|
uint16_t text_y_offset = (text_height + 1) / 2; // rounded-up half height of text
|
|
|
|
canvas_height -= text_height + PADDING_BETWEEN_NUMBERS_AND_GRAPH;
|
|
|
|
canvas_height -= 2 * text_y_offset;
|
|
|
|
canvas_y += text_y_offset;
|
|
|
|
Decimal ord_max_num = ord_max;
|
|
Decimal ord_min_num = ord_min;
|
|
|
|
uint8_t max_num_ord_numbers = canvas_height / (text_height + PADDING_BETWEEN_NUMBERS);
|
|
|
|
Decimal ord_num_step = (ord_max_num - ord_min_num) / max_num_ord_numbers;
|
|
|
|
ord_num_step = find_step(ord_num_step);
|
|
|
|
if (ord_max_num > 0) {
|
|
ord_max_num = (ord_max_num / ord_num_step) * ord_num_step;
|
|
} else {
|
|
ord_max_num = ((ord_max_num - ord_num_step) / ord_num_step) * ord_num_step;
|
|
}
|
|
|
|
if (ord_min_num > 0) {
|
|
ord_min_num = ((ord_min_num + ord_num_step) / ord_num_step) * ord_num_step;
|
|
} else {
|
|
ord_min_num = (ord_min_num / ord_num_step) * ord_num_step;
|
|
}
|
|
|
|
String str;
|
|
// value text string width
|
|
std::vector<std::tuple<Decimal, String, uint16_t>> ord_steps;
|
|
uint16_t max_width = 0;
|
|
|
|
for (Decimal step = ord_min_num; step <= ord_max_num; step += ord_num_step) {
|
|
str =
|
|
step.toString(2, ord_min_num.abs() < 1000 && ord_max_num.abs() < 1000); // todo: use digits of ord_num_step
|
|
target.getTextBounds(str, 0, 0, &dummy_x, &dummy_y, &text_width, &text_height);
|
|
ord_steps.emplace_back(step, str, text_width);
|
|
max_width = std::max(max_width, text_width);
|
|
}
|
|
|
|
canvas_x += max_width + PADDING_BETWEEN_NUMBERS_AND_GRAPH;
|
|
canvas_width -= max_width + PADDING_BETWEEN_NUMBERS_AND_GRAPH;
|
|
|
|
target.setTextColor(foreground_color.to_565());
|
|
for (const auto &step : ord_steps) {
|
|
Decimal val;
|
|
String text;
|
|
uint16_t width;
|
|
std::tie(val, text, width) = step;
|
|
int16_t posx = canvas_x - width - PADDING_BETWEEN_NUMBERS_AND_GRAPH;
|
|
int16_t posy = val_to_screen(val, canvas_y, canvas_height, ord_min, ord_max, true);
|
|
|
|
target.setCursor(posx, posy - text_y_offset);
|
|
target.print(text);
|
|
target.drawLine(canvas_x - 1, posy, canvas_x + 1, posy, foreground_color.to_565());
|
|
}
|
|
|
|
Decimal abs_max_num = abs_max;
|
|
Decimal abs_min_num = abs_min;
|
|
|
|
}
|
|
|
|
[[gnu::pure]] static int16_t val_to_screen(Decimal val,
|
|
int16_t canvas_start,
|
|
uint16_t canvas_size,
|
|
Decimal val_min,
|
|
Decimal val_max,
|
|
bool reverse) {
|
|
int16_t canvas_val = static_cast<int16_t>(((val - val_min) * canvas_size / (val_max - val_min)).toInt());
|
|
if (reverse) {
|
|
return canvas_start + canvas_size - canvas_val;
|
|
} else {
|
|
return canvas_start + canvas_val;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
RGB color_white = "FFFFFF"_rgb;
|
|
RGB color_black = "000000"_rgb;
|
|
|
|
std::vector<Point> v1 = {{0_d, 10_d}, {5_d, 20_d}, {10_d, 16_d}, {15_d, 50_d},
|
|
{20_d, 0_d}, {25_d, 40_d}};
|
|
std::vector<Point>
|
|
v2 = {{0_d, 5_d}, {3_d, 10_d}, {7_d, 20_d}, {11_d, 6_d}, {15_d, 22_d}, {22_d, 36_d}, {25_d, 50_d}};
|
|
std::vector<Point>
|
|
v3 = {{-10_d, 100000_d}, {-5_d, 2000_d}, {0_d, 100000_d}, {5_d, 2000_d}, {10_d, -16000_d}, {15_d, 50000_d},
|
|
{20_d, 1000_d}, {25_d, 40000_d}};
|
|
std::vector<Point> v4 =
|
|
{{0, 12374_d}, {3_d, 43563_d}, {7_d, 6512_d}, {11_d, 14434_d}, {15_d, 43143_d}, {22_d, 298_d}, {25_d, 93834_d}};
|
|
|
|
Series s1{
|
|
"00FFFF"_rgb,
|
|
"CYAN",
|
|
Visualisation::AREA_WITH_LINE,
|
|
InterpolationMethod::MONOTONIC_CUBIC_HERMITE,
|
|
v1
|
|
};
|
|
Series s2{
|
|
"FF0000"_rgb,
|
|
"RED",
|
|
Visualisation::AREA_WITH_LINE,
|
|
InterpolationMethod::MONOTONIC_CUBIC_HERMITE,
|
|
v2
|
|
};
|
|
|
|
std::vector<Series> series{s1, s2};
|
|
Chart chart1 = {color_black, color_white,
|
|
true, true, true,
|
|
true, true, 0, 0, 0, 0,
|
|
series};
|
|
|
|
Series s3{
|
|
"00FFFF"_rgb,
|
|
"CYAN",
|
|
Visualisation::AREA_WITH_LINE,
|
|
InterpolationMethod::CUBIC_HERMITE,
|
|
v1
|
|
};
|
|
Series s4{
|
|
"FF0000"_rgb,
|
|
"RED",
|
|
Visualisation::AREA_WITH_LINE,
|
|
InterpolationMethod::CUBIC_HERMITE,
|
|
v2
|
|
};
|
|
|
|
std::vector<Series> series2{s3, s4};
|
|
|
|
|
|
Chart chart2 = {color_white, color_black,
|
|
true, true, true,
|
|
true, false, 0, 0, 0_d, 5_d,
|
|
series2};
|
|
|
|
|
|
|
|
void setup() {
|
|
Serial.begin(9600);
|
|
screen.begin(32000000);
|
|
|
|
// commander.EnableSerial();
|
|
// commander.SetPrompt("graph> ");
|
|
// commander.Begin();
|
|
|
|
|
|
screen.fillScreen("555555"_rgb565);
|
|
|
|
|
|
chart1.Draw(screen, 5, 5, screen.width() - 10, screen.height() / 2 - 10);
|
|
chart2.Draw(screen, 5, screen.height() / 2 + 5, screen.width() - 10, screen.height() / 2 - 10);
|
|
}
|
|
|
|
|
|
Decimal chart2_offset = 0_d;
|
|
Decimal chart2_step = 0.2;
|
|
bool chart2_direction = true;
|
|
|
|
void loop() {
|
|
if (chart2_direction) {
|
|
chart2_offset += chart2_step;
|
|
if (chart2_offset > 20) {
|
|
chart2_offset = 20;
|
|
chart2_direction = false;
|
|
}
|
|
} else {
|
|
chart2_offset -= chart2_step;
|
|
if (chart2_offset < 0) {
|
|
chart2_offset = 0;
|
|
chart2_direction = true;
|
|
}
|
|
}
|
|
|
|
chart2.abscissa_from = chart2_offset;
|
|
chart2.abscissa_to = chart2_offset + 5;
|
|
|
|
|
|
chart2.Draw(screen, 5, screen.height() / 2 + 5, screen.width() - 10, screen.height() / 2 - 10);
|
|
|
|
delay(100);
|
|
|
|
} |