// gfx_canvas.cpp — Drawing algorithm implementations. #include "gfx_canvas.h" #include namespace gfx { // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- static void swap16(int16_t& a, int16_t& b) { int16_t t = a; a = b; b = t; } static int16_t abs16(int16_t v) { return v < 0 ? -v : v; } static int16_t min16(int16_t a, int16_t b) { return a < b ? a : b; } static int16_t max16(int16_t a, int16_t b) { return a > b ? a : b; } // --------------------------------------------------------------------------- // Construction // --------------------------------------------------------------------------- GfxCanvas::GfxCanvas(int16_t width, int16_t height) : raw_width_(width), raw_height_(height) {} // --------------------------------------------------------------------------- // Geometry (accounts for rotation) // --------------------------------------------------------------------------- int16_t GfxCanvas::width() const { return (rotation_ == Rotation::deg_90 || rotation_ == Rotation::deg_270) ? raw_height_ : raw_width_; } int16_t GfxCanvas::height() const { return (rotation_ == Rotation::deg_90 || rotation_ == Rotation::deg_270) ? raw_width_ : raw_height_; } void GfxCanvas::setRotation(Rotation r) { rotation_ = r; } // --------------------------------------------------------------------------- // drawPixel — bounds check + rotation → drawPixelImpl // --------------------------------------------------------------------------- void GfxCanvas::drawPixel(int16_t x, int16_t y, Color color) { if (x < 0 || x >= width() || y < 0 || y >= height()) return; int16_t px = x, py = y; switch (rotation_) { case Rotation::deg_0: break; case Rotation::deg_90: px = raw_height_ - 1 - y; py = x; break; case Rotation::deg_180: px = raw_width_ - 1 - x; py = raw_height_ - 1 - y; break; case Rotation::deg_270: px = y; py = raw_width_ - 1 - x; break; } drawPixelImpl(px, py, color); } // --------------------------------------------------------------------------- // Horizontal line — clip + rotation → physical HLine or VLine // --------------------------------------------------------------------------- void GfxCanvas::drawHLine(int16_t x, int16_t y, int16_t w, Color color) { if (w <= 0 || y < 0 || y >= height()) return; if (x < 0) { w += x; x = 0; } if (x + w > width()) w = width() - x; if (w <= 0) return; switch (rotation_) { case Rotation::deg_0: drawHLineImpl(x, y, w, color); break; case Rotation::deg_90: drawVLineImpl(raw_height_ - 1 - y, x, w, color); break; case Rotation::deg_180: drawHLineImpl(raw_width_ - x - w, raw_height_ - 1 - y, w, color); break; case Rotation::deg_270: drawVLineImpl(y, raw_width_ - x - w, w, color); break; } } // --------------------------------------------------------------------------- // Vertical line — clip + rotation → physical VLine or HLine // --------------------------------------------------------------------------- void GfxCanvas::drawVLine(int16_t x, int16_t y, int16_t h, Color color) { if (h <= 0 || x < 0 || x >= width()) return; if (y < 0) { h += y; y = 0; } if (y + h > height()) h = height() - y; if (h <= 0) return; switch (rotation_) { case Rotation::deg_0: drawVLineImpl(x, y, h, color); break; case Rotation::deg_90: drawHLineImpl(raw_height_ - y - h, x, h, color); break; case Rotation::deg_180: drawVLineImpl(raw_width_ - 1 - x, raw_height_ - y - h, h, color); break; case Rotation::deg_270: drawHLineImpl(y, raw_width_ - 1 - x, h, color); break; } } // --------------------------------------------------------------------------- // Arbitrary line — Bresenham // --------------------------------------------------------------------------- void GfxCanvas::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, Color color) { if (x0 == x1) { if (y0 > y1) swap16(y0, y1); drawVLine(x0, y0, y1 - y0 + 1, color); return; } if (y0 == y1) { if (x0 > x1) swap16(x0, x1); drawHLine(x0, y0, x1 - x0 + 1, color); return; } bool steep = abs16(y1 - y0) > abs16(x1 - x0); if (steep) { swap16(x0, y0); swap16(x1, y1); } if (x0 > x1) { swap16(x0, x1); swap16(y0, y1); } int16_t dx = x1 - x0; int16_t dy = abs16(y1 - y0); int16_t err = dx / 2; int16_t ystep = (y0 < y1) ? 1 : -1; int16_t y = y0; for (int16_t x = x0; x <= x1; ++x) { if (steep) drawPixel(y, x, color); else drawPixel(x, y, color); err -= dy; if (err < 0) { y += ystep; err += dx; } } } // --------------------------------------------------------------------------- // Rectangles // --------------------------------------------------------------------------- void GfxCanvas::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, Color color) { if (w <= 0 || h <= 0) return; drawHLine(x, y, w, color); drawHLine(x, y + h - 1, w, color); drawVLine(x, y + 1, h - 2, color); drawVLine(x + w - 1, y + 1, h - 2, color); } void GfxCanvas::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, Color color) { if (w <= 0 || h <= 0) return; // Clip to screen bounds. if (x < 0) { w += x; x = 0; } if (y < 0) { h += y; y = 0; } if (x + w > width()) w = width() - x; if (y + h > height()) h = height() - y; if (w <= 0 || h <= 0) return; // Transform the rectangle to physical coordinates. // After rotation, an axis-aligned rect stays axis-aligned. int16_t px = x, py = y, pw = w, ph = h; switch (rotation_) { case Rotation::deg_0: break; case Rotation::deg_90: px = raw_height_ - y - h; py = x; pw = h; ph = w; break; case Rotation::deg_180: px = raw_width_ - x - w; py = raw_height_ - y - h; break; case Rotation::deg_270: px = y; py = raw_width_ - x - w; pw = h; ph = w; break; } fillRectImpl(px, py, pw, ph, color); } // --------------------------------------------------------------------------- // Circles — midpoint algorithm // --------------------------------------------------------------------------- void GfxCanvas::drawCircle(int16_t cx, int16_t cy, int16_t r, Color color) { if (r <= 0) { drawPixel(cx, cy, color); return; } drawCircleQuadrants(cx, cy, r, 0x0F, color); } // Draws selected quadrant arcs using the midpoint circle algorithm. // bit 0 (0x01): top-right // bit 1 (0x02): top-left // bit 2 (0x04): bottom-left // bit 3 (0x08): bottom-right void GfxCanvas::drawCircleQuadrants(int16_t cx, int16_t cy, int16_t r, uint8_t corners, Color color) { int16_t x = 0, y = r; int16_t d = 1 - r; while (x <= y) { if (corners & 0x01) { drawPixel(cx + x, cy - y, color); drawPixel(cx + y, cy - x, color); } if (corners & 0x02) { drawPixel(cx - y, cy - x, color); drawPixel(cx - x, cy - y, color); } if (corners & 0x04) { drawPixel(cx - x, cy + y, color); drawPixel(cx - y, cy + x, color); } if (corners & 0x08) { drawPixel(cx + y, cy + x, color); drawPixel(cx + x, cy + y, color); } if (d < 0) { d += 2 * x + 3; } else { d += 2 * (x - y) + 5; --y; } ++x; } } void GfxCanvas::fillCircle(int16_t cx, int16_t cy, int16_t r, Color color) { if (r <= 0) { drawPixel(cx, cy, color); return; } drawVLine(cx, cy - r, 2 * r + 1, color); fillCircleSpans(cx, cy, r, 0x03, 0, color); } // Fills horizontal spans for circle quadrants. // bit 0: right half (cx to cx+x) // bit 1: left half (cx-x to cx) // delta: extra width added to each span (used by fillRoundRect). void GfxCanvas::fillCircleSpans(int16_t cx, int16_t cy, int16_t r, uint8_t corners, int16_t delta, Color color) { int16_t x = 0, y = r; int16_t d = 1 - r; int16_t prev_y = -1; // track to avoid duplicate spans while (x <= y) { if (d >= 0) { if (corners & 0x01) { drawHLine(cx - x, cy + y, 2 * x + 1 + delta, color); drawHLine(cx - x, cy - y, 2 * x + 1 + delta, color); } if (corners & 0x02) { drawHLine(cx - x, cy + y, 2 * x + 1 + delta, color); drawHLine(cx - x, cy - y, 2 * x + 1 + delta, color); } d += 2 * (x - y) + 5; --y; } else { d += 2 * x + 3; } if (x != prev_y) { if (corners & 0x01) { drawHLine(cx - y, cy + x, 2 * y + 1 + delta, color); drawHLine(cx - y, cy - x, 2 * y + 1 + delta, color); } if (corners & 0x02) { drawHLine(cx - y, cy + x, 2 * y + 1 + delta, color); drawHLine(cx - y, cy - x, 2 * y + 1 + delta, color); } } prev_y = y; ++x; } } // --------------------------------------------------------------------------- // Rounded rectangles // --------------------------------------------------------------------------- void GfxCanvas::drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, Color color) { if (w <= 0 || h <= 0) return; if (r <= 0) { drawRect(x, y, w, h, color); return; } int16_t max_r = min16(w, h) / 2; if (r > max_r) r = max_r; // Straight edges (between the arc endpoints). drawHLine(x + r, y, w - 2 * r, color); // top drawHLine(x + r, y + h - 1, w - 2 * r, color); // bottom drawVLine(x, y + r, h - 2 * r, color); // left drawVLine(x + w - 1, y + r, h - 2 * r, color); // right // Four corner arcs. drawCircleQuadrants(x + w - 1 - r, y + r, r, 0x01, color); // top-right drawCircleQuadrants(x + r, y + r, r, 0x02, color); // top-left drawCircleQuadrants(x + r, y + h - 1 - r, r, 0x04, color); // bottom-left drawCircleQuadrants(x + w - 1 - r, y + h - 1 - r, r, 0x08, color);// bottom-right } void GfxCanvas::fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, Color color) { if (w <= 0 || h <= 0) return; if (r <= 0) { fillRect(x, y, w, h, color); return; } int16_t max_r = min16(w, h) / 2; if (r > max_r) r = max_r; // Central rectangle. fillRect(x + r, y, w - 2 * r, h, color); // Side rectangles. fillRect(x, y + r, r, h - 2 * r, color); fillRect(x + w - r, y + r, r, h - 2 * r, color); // Four filled corners using spans. int16_t cx_l = x + r; int16_t cx_r = x + w - 1 - r; int16_t cy_t = y + r; int16_t cy_b = y + h - 1 - r; int16_t xi = 0, yi = r; int16_t d = 1 - r; while (xi <= yi) { // Top corners. drawHLine(cx_l - xi, cy_t - yi, xi + (cx_r - cx_l) + xi + 1, color); drawHLine(cx_l - yi, cy_t - xi, yi + (cx_r - cx_l) + yi + 1, color); // Bottom corners. drawHLine(cx_l - xi, cy_b + yi, xi + (cx_r - cx_l) + xi + 1, color); drawHLine(cx_l - yi, cy_b + xi, yi + (cx_r - cx_l) + yi + 1, color); if (d < 0) { d += 2 * xi + 3; } else { d += 2 * (xi - yi) + 5; --yi; } ++xi; } } // --------------------------------------------------------------------------- // Triangles // --------------------------------------------------------------------------- void GfxCanvas::drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, Color color) { drawLine(x0, y0, x1, y1, color); drawLine(x1, y1, x2, y2, color); drawLine(x2, y2, x0, y0, color); } void GfxCanvas::fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, Color color) { // Sort by y-coordinate ascending. if (y0 > y1) { swap16(x0, x1); swap16(y0, y1); } if (y1 > y2) { swap16(x1, x2); swap16(y1, y2); } if (y0 > y1) { swap16(x0, x1); swap16(y0, y1); } // Degenerate: all on same row. if (y0 == y2) { int16_t a = min16(min16(x0, x1), x2); int16_t b = max16(max16(x0, x1), x2); drawHLine(a, y0, b - a + 1, color); return; } // Edge slopes using fixed-point (16.16) arithmetic. // Walk from top to bottom, interpolating left and right x boundaries. int32_t dx02 = x2 - x0, dy02 = y2 - y0; int32_t dx01 = x1 - x0, dy01 = y1 - y0; int32_t dx12 = x2 - x1, dy12 = y2 - y1; // Accumulator for edge 0→2 (spans the full height). int32_t sa = 0; // Accumulator for edge 0→1 (top half) then 1→2 (bottom half). int32_t sb = 0; // Top half: y0 → y1. int16_t last_y = (y1 == y2) ? y1 : y1 - 1; for (int16_t y = y0; y <= last_y; ++y) { int16_t a = x0 + static_cast(sa / dy02); int16_t b = x0 + static_cast(sb / dy01); sa += dx02; sb += dx01; if (a > b) swap16(a, b); drawHLine(a, y, b - a + 1, color); } // Bottom half: y1 → y2. sb = dx12 * (last_y + 1 - y1); for (int16_t y = last_y + 1; y <= y2; ++y) { int16_t a = x0 + static_cast(sa / dy02); int16_t b = x1 + static_cast(sb / dy12); sa += dx02; sb += dx12; if (a > b) swap16(a, b); drawHLine(a, y, b - a + 1, color); } } // --------------------------------------------------------------------------- // Screen // --------------------------------------------------------------------------- void GfxCanvas::fillScreen(Color color) { fillRect(0, 0, width(), height(), color); } // --------------------------------------------------------------------------- // Text — configuration // --------------------------------------------------------------------------- void GfxCanvas::setFont(const Font& font) { font_ = &font; } void GfxCanvas::setCursor(int16_t x, int16_t y) { cursor_x_ = x; cursor_y_ = y; } void GfxCanvas::setTextColor(Color fg) { text_fg_ = fg; text_bg_opaque_ = false; } void GfxCanvas::setTextColor(Color fg, Color bg) { text_fg_ = fg; text_bg_ = bg; text_bg_opaque_ = true; } void GfxCanvas::setTextWrap(bool wrap) { text_wrap_ = wrap; } // --------------------------------------------------------------------------- // Text — character rendering (column-major font encoding) // --------------------------------------------------------------------------- void GfxCanvas::drawChar(int16_t x, int16_t y, char c, Color fg, Color bg) const { if (!font_) return; if (c < static_cast(font_->first_char) || c > static_cast(font_->last_char)) return; int idx = c - font_->first_char; const uint8_t* glyph = font_->bitmap + idx * font_->glyph_width; // Column-major: each byte is one column, bit 0 = top row. for (int16_t col = 0; col < font_->glyph_width; ++col) { uint8_t column_bits = glyph[col]; for (int16_t row = 0; row < font_->glyph_height; ++row) { if (column_bits & (1 << row)) { const_cast(this)->drawPixel( x + col, y + row, fg); } else if (text_bg_opaque_) { const_cast(this)->drawPixel( x + col, y + row, bg); } } } } // --------------------------------------------------------------------------- // Text — string rendering // --------------------------------------------------------------------------- void GfxCanvas::drawString(int16_t x, int16_t y, const char* str, Color color) { if (!font_ || !str) return; while (*str) { drawChar(x, y, *str, color, text_bg_); x += font_->x_advance; ++str; } } void GfxCanvas::print(char c) { if (!font_) return; if (c == '\n') { cursor_x_ = 0; cursor_y_ += font_->y_advance; return; } if (text_wrap_ && (cursor_x_ + font_->x_advance > width())) { cursor_x_ = 0; cursor_y_ += font_->y_advance; } drawChar(cursor_x_, cursor_y_, c, text_fg_, text_bg_); cursor_x_ += font_->x_advance; } void GfxCanvas::print(const char* str) { if (!str) return; while (*str) { print(*str); ++str; } } // --------------------------------------------------------------------------- // Text — measurement // --------------------------------------------------------------------------- void GfxCanvas::getTextBounds(const char* str, int16_t cx, int16_t cy, int16_t& bx, int16_t& by, uint16_t& bw, uint16_t& bh) const { bx = cx; by = cy; bw = 0; bh = 0; if (!font_ || !str) return; int16_t x = cx; int16_t min_x = cx, max_x = cx; int16_t min_y = cy, max_y = cy; while (*str) { if (*str == '\n') { x = cx; cy += font_->y_advance; } else { int16_t gx = x; int16_t gy = cy; int16_t gx2 = gx + font_->glyph_width - 1; int16_t gy2 = gy + font_->glyph_height - 1; if (gx < min_x) min_x = gx; if (gx2 > max_x) max_x = gx2; if (gy < min_y) min_y = gy; if (gy2 > max_y) max_y = gy2; x += font_->x_advance; } ++str; } if (max_x >= min_x) { bx = min_x; by = min_y; bw = static_cast(max_x - min_x + 1); bh = static_cast(max_y - min_y + 1); } } // --------------------------------------------------------------------------- // Default Impl methods — loop over drawPixelImpl // --------------------------------------------------------------------------- void GfxCanvas::drawHLineImpl(int16_t x, int16_t y, int16_t w, Color color) { for (int16_t i = 0; i < w; ++i) drawPixelImpl(x + i, y, color); } void GfxCanvas::drawVLineImpl(int16_t x, int16_t y, int16_t h, Color color) { for (int16_t i = 0; i < h; ++i) drawPixelImpl(x, y + i, color); } void GfxCanvas::fillRectImpl(int16_t x, int16_t y, int16_t w, int16_t h, Color color) { for (int16_t row = 0; row < h; ++row) drawHLineImpl(x, y + row, w, color); } } // namespace gfx