Meh Belly Lint Collection

That awful moment when you realize,
THIS is YOUR circus and THOSE are YOUR monkeys.

User Tools

Site Tools


arduino_gfx_helpers

This is an old revision of the document!


Arduino_GFX helper functions

Good References

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);
  }
}
arduino_gfx_helpers.1742937967.txt.gz · Last modified: 2025/03/25 21:26 by kenson

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki