Files
GfxCanvas/gfx_canvas.cpp

575 lines
19 KiB
C++

// gfx_canvas.cpp — Drawing algorithm implementations.
#include "gfx_canvas.h"
#include <cstring>
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<int16_t>(sa / dy02);
int16_t b = x0 + static_cast<int16_t>(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<int16_t>(sa / dy02);
int16_t b = x1 + static_cast<int16_t>(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<char>(font_->first_char) ||
c > static_cast<char>(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<GfxCanvas*>(this)->drawPixel(
x + col, y + row, fg);
} else if (text_bg_opaque_) {
const_cast<GfxCanvas*>(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<uint16_t>(max_x - min_x + 1);
bh = static_cast<uint16_t>(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