//#include "Commander.h" #include "fptrig.h" #include #include #include #include #include #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 points; mutable CubicCurve cached_cubic_curve; mutable Decimal cached_cubic_curve_from; mutable Decimal cached_cubic_curve_to; std::tuple 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 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 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 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 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 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; std::tuple 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 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 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>> pixels(canvas_height); assert(pixels.size() == canvas_height); std::vector> 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(((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 line_pixels; std::vector 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> 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(((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 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 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 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 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{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 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); }