arduino_gfx_helpers
This is an old revision of the document!
Table of Contents
Arduino_GFX helper functions
Good References
Embrient Logo
- X(π‘) = cos(π‘) β 0.5cos(5π‘)
- Y(π‘) = sin(π‘) + 0.5sin(5π‘)
- Z(π‘) = 1.32204cos(3π‘)
Where π‘ is in degrees, i.e., π‘ β [0,360)
To normalize the diameter to '1' Use the following equation
- X(π‘) = 0.378122(cos(t) β 0.5cos(5t))
- Y(π‘) = 0.378122(sin(t) + 0.5sin(5t))
- Z(π‘) = 0.5cos(3t)
β
Xt = 0.378122*(cos(t)β0.5*cos(5*t)); Yt = 0.378122*(sin(t)+0.5*sin(5*t)); Zt = 0.5*cos(3t);
Thick Lines
Arduino_GFX doesn't let you draw lines with thickness, here I have two functions, one thats βsimpleβ and one thats a bit more optimized. I like to chain lines together to make a polyline so rounded edges make it look better.
This is a βnaiveβ function which computes two triangles representing the thickened line and then plops two circles on the ends. It gets the job done, but it isn't efficient.
// Draw a thick line with rounded ends using Adafruit_GFX primitives void DrawNaiveThickLineRounded(int x1, int y1, int x2, int y2, int thickness, int color) { // Calculate the differences between endpoints int deltaX = x1 - x2; int deltaY = y1 - y2; // Compute the length once float len = sqrt(sq(deltaX) + sq(deltaY)); // If the endpoints are nearly identical, draw a single circle if (len < 0.001) { tft.fillCircle(x1, y1, thickness / 2, color); return; } // Compute factor for the perpendicular offset float factor = (thickness / 2.0) / len; // Calculate the perpendicular offsets for rounded thickness float offsetX = factor * (y1 - y2); float offsetY = factor * (x1 - x2); // Draw the thick line as two triangles forming a rectangle tft.fillTriangle(x1 + offsetX, y1 - offsetY, x1 - offsetX, y1 + offsetY, x2 + offsetX, y2 - offsetY, color); tft.fillTriangle(x1 - offsetX, y1 + offsetY, x2 - offsetX, y2 + offsetY, x2 + offsetX, y2 - offsetY, color); // Add rounded caps at both endpoints tft.fillCircle(x1, y1, thickness / 2, color); tft.fillCircle(x2, y2, thickness / 2, color); }
This is more optimized code focusing on eliminating redundant calculations.
// Draw a thick line with rounded ends using a unified scanline fill approach. // Focuses more on integer optimization, further optimization can be made by eliminating division. void DrawThickLineRoundedHLines(int x1, int y1, int x2, int y2, int thickness, uint16_t color) { // Half thickness (using integer division is fine for even thicknesses) int r = thickness >> 1; int dxLine = x2 - x1; int dyLine = y2 - y1; int d2 = dxLine * dxLine + dyLine * dyLine; // If endpoints coincide, draw a full circle. if (d2 == 0) { tft.fillCircle(x1, y1, r, color); return; } // Compute the length and normalized line direction. float len = sqrt((float)d2); float lx = dxLine / (float)len; float ly = dyLine / (float)len; // Compute perpendicular offset (using (dy, -dx)) scaled to half thickness. float factor = (thickness / 2.0f) / len; float offX = factor * dyLine; float offY = factor * -dxLine; // Compute the four vertices (of the quadr) in clockwise order. // These define the main body of the line. float Ax = x1 + offX, Ay = y1 + offY; float Bx = x1 - offX, By = y1 - offY; float Cx = x2 - offX, Cy = y2 - offY; float Dx = x2 + offX, Dy = y2 + offY; // Determine overall bounding box (including the circular caps). float bboxMinX = min(min(min(Ax, Bx), min(Cx, Dx)), (float)min(x1 - r, x2 - r)); float bboxMaxX = max(max(max(Ax, Bx), max(Cx, Dx)), (float)max(x1 + r, x2 + r)); float bboxMinY = min(min(min(Ay, By), min(Cy, Dy)), (float)min(y1 - r, y2 - r)); float bboxMaxY = max(max(max(Ay, By), max(Cy, Dy)), (float)max(y1 + r, y2 + r)); int iMinY = (int)floor(bboxMinY); int iMaxY = (int)ceil(bboxMaxY); // Lambda to process an edge of the quadr. // Returns the x coordinate where the horizontal scanline at 'y' intersects the edge, // or NAN if there is no intersection. auto processEdge = [=](float x0, float y0, float x1, float y1, int y) -> float { if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) { float t = (y - y0) / (y1 - y0); return x0 + t * (x1 - x0); } return NAN; }; // For each scanline, we compute up to three segments: one from the quadr body, // one from the start cap, and one from the end cap. // We then merge overlapping segments and draw them. struct Segment { float x0, x1; }; for (int y = iMinY; y <= iMaxY; y++) { Segment segments[3]; int segCount = 0; // 1. Quadr segment: process all four edges of the quadr. float inters[4]; int count = 0; float ttemp; ttemp = processEdge(Ax, Ay, Bx, By, y); if (!isnan(ttemp)) inters[count++] = ttemp; ttemp = processEdge(Bx, By, Cx, Cy, y); if (!isnan(ttemp)) inters[count++] = ttemp; ttemp = processEdge(Cx, Cy, Dx, Dy, y); if (!isnan(ttemp)) inters[count++] = ttemp; ttemp = processEdge(Dx, Dy, Ax, Ay, y); if (!isnan(ttemp)) inters[count++] = ttemp; if (count >= 2) { float qx0 = inters[0], qx1 = inters[0]; for (int i = 1; i < count; i++) { if (inters[i] < qx0) qx0 = inters[i]; if (inters[i] > qx1) qx1 = inters[i]; } segments[segCount++] = { qx0, qx1 }; } // 2. Start cap (centered at (x1,y1)): if y is within the circle's vertical span. if (y >= y1 - r && y <= y1 + r) { float dyCap = y - y1; float dxCap = sqrt((float)(r*r - dyCap*dyCap)); float capStart = x1 - dxCap; float capEnd = x1 + dxCap; // Clip to the half circle: include only points for which dot((x-x1,y-y1),(lx,ly)) < 0. // Solve: (x - x1)*lx + (y - y1)*ly < 0 => if lx != 0, x < x1 - ((y-y1)*ly)/lx when lx > 0, // or x > x1 - ((y-y1)*ly)/lx when lx < 0. if (fabs(lx) > 1e-6) { float xBoundary = x1 - ((y - y1) * ly) / lx; if (lx > 0) { capEnd = min(capEnd, xBoundary); } else { capStart = max(capStart, xBoundary); } } else { // For near-vertical lines, if the condition isn't met, skip drawing the cap on this scanline. if ((ly > 0 && y >= y1) || (ly < 0 && y <= y1)) { capStart = 1, capEnd = 0; // empty } } if (capEnd >= capStart) segments[segCount++] = { capStart, capEnd }; } // 3. End cap (centered at (x2,y2)): if y is within the circle's vertical span. if (y >= y2 - r && y <= y2 + r) { float dyCap = y - y2; float dxCap = sqrt((float)(r*r - dyCap*dyCap)); float capStart = x2 - dxCap; float capEnd = x2 + dxCap; // Clip to the half circle: include only points for which dot((x-x2,y-y2),(lx,ly)) > 0. if (fabs(lx) > 1e-6) { float xBoundary = x2 - ((y - y2) * ly) / lx; if (lx > 0) { capStart = max(capStart, xBoundary); } else { capEnd = min(capEnd, xBoundary); } } else { if ((ly > 0 && y <= y2) || (ly < 0 && y >= y2)) { capStart = 1, capEnd = 0; } } if (capEnd >= capStart) segments[segCount++] = { capStart, capEnd }; } // If no segments were found on this scanline, continue. if (segCount == 0) continue; // Merge segments: sort by starting x and then combine overlapping ones. // (Since there are few segments, a simple bubble sort is sufficient.) for (int i = 0; i < segCount - 1; i++) { for (int j = i + 1; j < segCount; j++) { if (segments[j].x0 < segments[i].x0) { Segment tmp = segments[i]; segments[i] = segments[j]; segments[j] = tmp; } } } // Merge overlapping segments. float mergedStart = segments[0].x0; float mergedEnd = segments[0].x1; for (int i = 1; i < segCount; i++) { if (segments[i].x0 <= mergedEnd + 1) { // overlapping or contiguous if (segments[i].x1 > mergedEnd) mergedEnd = segments[i].x1; } else { // Draw the current merged segment. int ix0 = (int)ceil(mergedStart); int ix1 = (int)floor(mergedEnd); if (ix1 >= ix0) tft.drawFastHLine(ix0, y, ix1 - ix0 + 1, color); mergedStart = segments[i].x0; mergedEnd = segments[i].x1; } } // Draw the final merged segment. int ix0 = (int)ceil(mergedStart); int ix1 = (int)floor(mergedEnd); if (ix1 >= ix0) tft.drawFastHLine(ix0, y, ix1 - ix0 + 1, color); } }
In practice
#include <Adafruit_GFX.h>
#include <Adafruit_ST7796S.h>
#define TFT_CS 2
#define TFT_RST -1
#define TFT_DC 4
Adafruit_ST7796S tft = Adafruit_ST7796S(TFT_CS, TFT_DC, TFT_RST);
#define SCREEN_WIDTH 480
#define SCREEN_HEIGHT 360
GFXcanvas16 canvas(SCREEN_WIDTH, SCREEN_HEIGHT); // Off-screen framebuffer
uint8_t rotate = 0;
const float PI_F = 3.14159265;
const float SCALE = 0.378122;
const float Z_SCALE = 0.5;
const float Z_OFFSET = 2.0;
const int NUM_SEGMENTS = 72;
const uint16_t blackish = 0x0000;
float rotation = 0.0;
struct Segment3D {
int x1, y1, x2, y2;
float avgZ;
int thickness;
};
Segment3D segments[NUM_SEGMENTS];
struct Vec3 {
float x, y, z;
};
Vec3 rotateY(const Vec3& v, float angleDeg) {
float a = angleDeg * PI_F / 180.0;
float cosA = cos(a);
float sinA = sin(a);
return {
cosA * v.x + sinA * v.z,
v.y,
-sinA * v.x + cosA * v.z
};
}
void project(const Vec3& pt, int &px, int &py) {
float f = 1.0 / (Z_OFFSET - pt.z);
float x_proj = pt.x * f;
float y_proj = pt.y * f;
px = (int)((x_proj + 1.0) * SCREEN_WIDTH / 2.0);
py = (int)((1.0 - y_proj) * SCREEN_HEIGHT / 2.0);
}
inline uint16_t rgb888_to_565(uint8_t r, uint8_t g, uint8_t b) {
uint16_t r5 = (r * 249 + 1014) >> 11;
uint16_t g6 = (g * 253 + 505) >> 10;
uint16_t b5 = (b * 249 + 1014) >> 11;
return (r5 << 11) | (g6 << 5) | b5;
}
void DrawThickLineRoundedHLines(GFXcanvas16& canvas, int x1, int y1, int x2, int y2, int thickness, uint16_t color) {// Draw a thick line with rounded ends using a unified scanline fill approach.
// Half thickness (using integer division is fine for even thicknesses)
int r = thickness >> 1;
int dxLine = x2 - x1;
int dyLine = y2 - y1;
int d2 = dxLine * dxLine + dyLine * dyLine;
// If endpoints coincide, draw a full circle.
if (d2 == 0) {
canvas.fillCircle(x1, y1, r, color);
return;
}
// Compute the length and normalized line direction.
float len = sqrt((float)d2);
float lx = dxLine / (float)len;
float ly = dyLine / (float)len;
// Compute perpendicular offset (using (dy, -dx)) scaled to half thickness.
float factor = (thickness / 2.0f) / len;
float offX = factor * dyLine;
float offY = factor * -dxLine;
// Compute the four vertices (of the quadr) in clockwise order.
// These define the main body of the line.
float Ax = x1 + offX, Ay = y1 + offY;
float Bx = x1 - offX, By = y1 - offY;
float Cx = x2 - offX, Cy = y2 - offY;
float Dx = x2 + offX, Dy = y2 + offY;
// Determine overall bounding box (including the circular caps).
float bboxMinX = min(min(min(Ax, Bx), min(Cx, Dx)), (float)min(x1 - r, x2 - r));
float bboxMaxX = max(max(max(Ax, Bx), max(Cx, Dx)), (float)max(x1 + r, x2 + r));
float bboxMinY = min(min(min(Ay, By), min(Cy, Dy)), (float)min(y1 - r, y2 - r));
float bboxMaxY = max(max(max(Ay, By), max(Cy, Dy)), (float)max(y1 + r, y2 + r));
int iMinY = (int)floor(bboxMinY);
int iMaxY = (int)ceil(bboxMaxY);
// Lambda to process an edge of the quadr.
// Returns the x coordinate where the horizontal scanline at 'y' intersects the edge,
// or NAN if there is no intersection.
auto processEdge = [=](float x0, float y0, float x1, float y1, int y) -> float {
if ((y0 <= y && y1 > y) || (y1 <= y && y0 > y)) {
float t = (y - y0) / (y1 - y0);
return x0 + t * (x1 - x0);
}
return NAN;
};
// For each scanline, we compute up to three segments: one from the quadr body,
// one from the start cap, and one from the end cap.
// We then merge overlapping segments and draw them.
struct Segment { float x0, x1; };
for (int y = iMinY; y <= iMaxY; y++) {
Segment segments[3];
int segCount = 0;
// 1. Quadr segment: process all four edges of the quadr.
float inters[4];
int count = 0;
float ttemp;
ttemp = processEdge(Ax, Ay, Bx, By, y); if (!isnan(ttemp)) inters[count++] = ttemp;
ttemp = processEdge(Bx, By, Cx, Cy, y); if (!isnan(ttemp)) inters[count++] = ttemp;
ttemp = processEdge(Cx, Cy, Dx, Dy, y); if (!isnan(ttemp)) inters[count++] = ttemp;
ttemp = processEdge(Dx, Dy, Ax, Ay, y); if (!isnan(ttemp)) inters[count++] = ttemp;
if (count >= 2) {
float qx0 = inters[0], qx1 = inters[0];
for (int i = 1; i < count; i++) {
if (inters[i] < qx0) qx0 = inters[i];
if (inters[i] > qx1) qx1 = inters[i];
}
segments[segCount++] = { qx0, qx1 };
}
// 2. Start cap (centered at (x1,y1)): if y is within the circle's vertical span.
if (y >= y1 - r && y <= y1 + r) {
float dyCap = y - y1;
float dxCap = sqrt((float)(r*r - dyCap*dyCap));
float capStart = x1 - dxCap;
float capEnd = x1 + dxCap;
// Clip to the half circle: include only points for which dot((x-x1,y-y1),(lx,ly)) < 0.
// Solve: (x - x1)*lx + (y - y1)*ly < 0 => if lx != 0, x < x1 - ((y-y1)*ly)/lx when lx > 0,
// or x > x1 - ((y-y1)*ly)/lx when lx < 0.
if (fabs(lx) > 1e-6) {
float xBoundary = x1 - ((y - y1) * ly) / lx;
if (lx > 0) {
capEnd = min(capEnd, xBoundary);
} else {
capStart = max(capStart, xBoundary);
}
} else {
// For near-vertical lines, if the condition isn't met, skip drawing the cap on this scanline.
if ((ly > 0 && y >= y1) || (ly < 0 && y <= y1)) {
capStart = 1, capEnd = 0; // empty
}
}
if (capEnd >= capStart)
segments[segCount++] = { capStart, capEnd };
}
// 3. End cap (centered at (x2,y2)): if y is within the circle's vertical span.
if (y >= y2 - r && y <= y2 + r) {
float dyCap = y - y2;
float dxCap = sqrt((float)(r*r - dyCap*dyCap));
float capStart = x2 - dxCap;
float capEnd = x2 + dxCap;
// Clip to the half circle: include only points for which dot((x-x2,y-y2),(lx,ly)) > 0.
if (fabs(lx) > 1e-6) {
float xBoundary = x2 - ((y - y2) * ly) / lx;
if (lx > 0) {
capStart = max(capStart, xBoundary);
} else {
capEnd = min(capEnd, xBoundary);
}
} else {
if ((ly > 0 && y <= y2) || (ly < 0 && y >= y2)) {
capStart = 1, capEnd = 0;
}
}
if (capEnd >= capStart)
segments[segCount++] = { capStart, capEnd };
}
// If no segments were found on this scanline, continue.
if (segCount == 0)
continue;
// Merge segments: sort by starting x and then combine overlapping ones.
// (Since there are few segments, a simple bubble sort is sufficient.)
for (int i = 0; i < segCount - 1; i++) {
for (int j = i + 1; j < segCount; j++) {
if (segments[j].x0 < segments[i].x0) {
Segment tmp = segments[i];
segments[i] = segments[j];
segments[j] = tmp;
}
}
}
// Merge overlapping segments.
float mergedStart = segments[0].x0;
float mergedEnd = segments[0].x1;
for (int i = 1; i < segCount; i++) {
if (segments[i].x0 <= mergedEnd + 1) { // overlapping or contiguous
if (segments[i].x1 > mergedEnd)
mergedEnd = segments[i].x1;
} else {
// Draw the current merged segment.
int ix0 = (int)ceil(mergedStart);
int ix1 = (int)floor(mergedEnd);
if (ix1 >= ix0)
canvas.drawFastHLine(ix0, y, ix1 - ix0 + 1, color);
mergedStart = segments[i].x0;
mergedEnd = segments[i].x1;
}
}
// Draw the final merged segment.
int ix0 = (int)ceil(mergedStart);
int ix1 = (int)floor(mergedEnd);
if (ix1 >= ix0)
canvas.drawFastHLine(ix0, y, ix1 - ix0 + 1, color);
}
}
void setup() {
tft.init(320, 480, 0, 0, ST7796S_BGR);
tft.setSPISpeed(40000000);
tft.invertDisplay(true);
tft.setRotation(rotate);
}
void loop() {
canvas.fillScreen(blackish);
const float baseThickness = 24.0;
int segmentIndex = 0;
float prev_px = -1, prev_py = -1;
float prev_z = 0;
int prev_px_i = 0, prev_py_i = 0;
for (int i = 0; i <= NUM_SEGMENTS; ++i) {
float t = (360.0 / NUM_SEGMENTS) * i;
float rad = t * PI_F / 180.0;
float x = SCALE * (cos(rad) - 0.5 * cos(5 * rad));
float y = SCALE * (sin(rad) + 0.5 * sin(5 * rad));
float z = Z_SCALE * cos(3 * rad);
Vec3 point = { x, y, z };
Vec3 rotated = rotateY(point, rotation);
int px, py;
project(rotated, px, py);
if (i > 0 && px >= 0 && px < SCREEN_WIDTH && py >= 0 && py < SCREEN_HEIGHT) {
float avgZ = (rotated.z + prev_z) / 2.0;
float perspectiveScale = 1.0f / (Z_OFFSET - avgZ);
int thickness = baseThickness * perspectiveScale;
if (thickness < 1) thickness = 1;
if (thickness > 20) thickness = 20;
segments[segmentIndex++] = {
prev_px_i, prev_py_i, px, py, avgZ, thickness
};
}
prev_px_i = px;
prev_py_i = py;
prev_z = rotated.z;
}
for (int i = 0; i < segmentIndex - 1; ++i) {
for (int j = i + 1; j < segmentIndex; ++j) {
if (segments[j].avgZ < segments[i].avgZ) {
Segment3D tmp = segments[i];
segments[i] = segments[j];
segments[j] = tmp;
}
}
}
for (int i = 0; i < segmentIndex; ++i) {
float depthFactor = (Z_OFFSET - segments[i].avgZ - 1.5f);
depthFactor = constrain(depthFactor, 0.0f, 1.0f);
uint8_t brightness = 255 - (uint8_t)(depthFactor * 192);
uint16_t color = rgb888_to_565(0, brightness, brightness);
DrawThickLineRoundedHLines(
canvas,
segments[i].x1, segments[i].y1,
segments[i].x2, segments[i].y2,
segments[i].thickness,
color
);
}
tft.drawRGBBitmap(0, 0, canvas.getBuffer(), SCREEN_WIDTH, SCREEN_HEIGHT);
rotation += 2.0;
if (rotation >= 360.0) rotation -= 360.0;
}
arduino_gfx_helpers.1743041900.txt.gz Β· Last modified: 2025/03/27 02:18 by kenson
