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

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
arduino_gfx_helpers [2025/03/25 21:06] โ€“ kensonarduino_gfx_helpers [2025/03/28 03:08] (current) โ€“ kenson
Line 1: Line 1:
 +====== Arduino_GFX helper functions ======
 +
 +===== Good References =====
 +
 +  * [[https://blog.dzl.dk/2020/04/04/crazy-fast-tft-action-with-adafruit-gfx-and-esp32/|SPI_WRITE16() optimization for Adafruit GFX]]
 +  * [[https://github.com/moononournation/Arduino_GFX|moononournation/Arduino_GFX: Rewrite of Adafruit GFX for speed]]
 +  * [[https://www.codeproject.com/Articles/5302085/GFX-Forever-The-Complete-Guide-to-GFX-for-IoT|GFX-Forever: Another optimized library]]
 +
 +===== 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)
 +โ€‹
 +<code cpp>
 +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);
 +</code>
 +
 +
 +
 +===== 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.
 <code cpp> <code cpp>
 // Draw a thick line with rounded ends using Adafruit_GFX primitives // Draw a thick line with rounded ends using Adafruit_GFX primitives
Line 36: Line 70:
 </code> </code>
  
 +This is more optimized code focusing on eliminating redundant calculations.
 <code cpp> <code cpp>
 // Draw a thick line with rounded ends using a unified scanline fill approach. // Draw a thick line with rounded ends using a unified scanline fill approach.
Line 204: Line 239:
   }   }
 } }
 +</code>
 +
 +===== Put in practice as =====
 +
 +<code cpp>
 +#include <Adafruit_GFX.h>
 +#include <Adafruit_ST7796S.h>
 +#include <math.h>
 +#include <vector>
 +
 +using std::vector;
 +
 +#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  360
 +#define SCREEN_HEIGHT 480
 +
 +GFXcanvas16 canvas(SCREEN_WIDTH, SCREEN_HEIGHT);  // Off-screen framebuffer
 +
 +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 = 60;
 +
 +float baseThickness = 24.0;
 +const uint16_t blackish = 0x0000;
 +
 +float rotation = 0.0;
 +
 +struct Vec3 {
 +  float x, y, z;
 +};
 +
 +struct Segment3D {
 +  int x1, y1, x2, y2;
 +  float avgZ;
 +  int thickness;
 +};
 +
 +// Rotate a 3D point around the X-axis.
 +Vec3 rotateX(const Vec3& v, float angleDeg) {
 +  float a = angleDeg * PI_F / 180.0;
 +  float cosA = cos(a);
 +  float sinA = sin(a);
 +  return { 
 +    v.x, 
 +    cosA * v.y - sinA * v.z, 
 +    sinA * v.y + cosA * v.z 
 +  };
 +}
 +
 +// Rotate a 3D point around the Y-axis.
 +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
 +  };
 +}
 +
 +// Rotate a 3D point around the Z-axis.
 +Vec3 rotateZ(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.y,
 +    sinA * v.x + cosA * v.y,
 +    v.z
 +  };
 +}
 +
 +// Project a 3D point into 2D canvas coordinates.
 +void project3D(const Vec3& pt, int centerX, int centerY, float size, int &px, int &py) {
 +  baseThickness = size / 5;
 +  float target_size = size - baseThickness;
 +  float f = 1.0 / (1.15 - pt.z);
 +  float x_proj = pt.x * f * target_size;
 +  float y_proj = pt.y * f * target_size;
 +  px = static_cast<int>(centerX + x_proj);
 +  py = static_cast<int>(centerY - y_proj);
 +}
 +
 +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;
 +}
 +
 +// Draw a thick line with rounded ends using a unified scanline fill approach.
 +void DrawThickLineRoundedHLines(GFXcanvas16& canvas, int x1, int y1, int x2, int y2, int thickness, uint16_t color) {
 +  int r = thickness >> 1;
 +  int dxLine = x2 - x1;
 +  int dyLine = y2 - y1;
 +  int d2 = dxLine * dxLine + dyLine * dyLine;
 +
 +  if (d2 == 0) {
 +    canvas.fillCircle(x1, y1, r, color);
 +    return;
 +  }
 +
 +  float len = sqrt((float)d2);
 +  float lx = dxLine / (float)len;
 +  float ly = dyLine / (float)len;
 +  float factor = (thickness / 2.0f) / len;
 +  float offX = factor * dyLine;
 +  float offY = factor * -dxLine;
 +
 +  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;
 +
 +  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);
 +
 +  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;
 +  };
 +
 +  struct Segment { float x0, x1; };
 +
 +  for (int y = iMinY; y <= iMaxY; y++) {
 +    Segment segments[3];
 +    int segCount = 0;
 +    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 };
 +    }
 +
 +    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;
 +      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 {
 +        if ((ly > 0 && y >= y1) || (ly < 0 && y <= y1)) {
 +          capStart = 1, capEnd = 0;
 +        }
 +      }
 +      if (capEnd >= capStart)
 +        segments[segCount++] = { capStart, capEnd };
 +    }
 +
 +    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;
 +      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 (segCount == 0)
 +      continue;
 +
 +    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;
 +        }
 +      }
 +    }
 +
 +    float mergedStart = segments[0].x0;
 +    float mergedEnd = segments[0].x1;
 +    for (int i = 1; i < segCount; i++) {
 +      if (segments[i].x0 <= mergedEnd + 1) {
 +        if (segments[i].x1 > mergedEnd)
 +          mergedEnd = segments[i].x1;
 +      } else {
 +        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;
 +      }
 +    }
 +    int ix0 = (int)ceil(mergedStart);
 +    int ix1 = (int)floor(mergedEnd);
 +    if (ix1 >= ix0)
 +      canvas.drawFastHLine(ix0, y, ix1 - ix0 + 1, color);
 +  }
 +}
 +
 +//========================================================================
 +// 1. Create the shape (a list of 3D points) defined by sin/cos functions.
 +vector<Vec3> createShape() {
 +  vector<Vec3> shape;
 +  for (int i = 0; i <= NUM_SEGMENTS; ++i) {
 +    float t = (360.0 / NUM_SEGMENTS) * i;
 +    float rad = t * PI_F / 180.0;
 +    float y = -SCALE * (cos(rad) - 0.5 * cos(5 * rad));
 +    float x =  SCALE * (sin(rad) + 0.5 * sin(5 * rad));
 +    float z = Z_SCALE * cos(3 * rad);
 +    shape.push_back({ x, y, z });
 +  }
 +  return shape;
 +}
 +
 +//========================================================================
 +// 2. Rotate the shape
 +vector<Vec3> rotateShape(const vector<Vec3>& shape, float angleX, float angleY, float angleZ) {
 +  vector<Vec3> rotated;
 +  for (size_t i = 0; i < shape.size(); ++i) {
 +    // Apply rotations in order: X, then Y, then Z.
 +    Vec3 point = rotateX(shape[i], angleX);
 +    point = rotateY(point, angleY);
 +    point = rotateZ(point, angleZ);
 +    rotated.push_back(point);
 +  }
 +  return rotated;
 +}
 +
 +
 +//========================================================================
 +// 3. Render the shape by projecting the rotated 3D points into 2D, building segments,
 +//    sorting them by depth, and drawing them with thick lines.
 +void renderShape(const vector<Vec3>& shape, int centerX, int centerY, float size) {
 +  int segmentIndex = 0;
 +  int prev_px_i = 0, prev_py_i = 0;
 +  float prev_z = 0;
 +  bool firstPoint = true;
 +  Segment3D segmentsLocal[NUM_SEGMENTS];
 +
 +  for (size_t i = 0; i < shape.size(); i++) {
 +    int px, py;
 +    project3D(shape[i], centerX, centerY, size, px, py); 
 +    
 +    if (!firstPoint && px >= 0 && px < SCREEN_WIDTH && py >= 0 && py < SCREEN_HEIGHT) {
 +      float avgZ = (shape[i].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;
 +      segmentsLocal[segmentIndex++] = { prev_px_i, prev_py_i, px, py, avgZ, thickness };
 +    }
 +    
 +    firstPoint = false;
 +    prev_px_i = px;
 +    prev_py_i = py;
 +    prev_z = shape[i].z;
 +  }
 +  
 +  // Depth sort (back to front)
 +  for (int i = 0; i < segmentIndex - 1; ++i) {
 +    for (int j = i + 1; j < segmentIndex; ++j) {
 +      if (segmentsLocal[j].avgZ < segmentsLocal[i].avgZ) {
 +        Segment3D tmp = segmentsLocal[i];
 +        segmentsLocal[i] = segmentsLocal[j];
 +        segmentsLocal[j] = tmp;
 +      }
 +    }
 +  }
 +  
 +  // Draw each segment with color based on depth.
 +  for (int i = 0; i < segmentIndex; ++i) {
 +    float depthFactor = (Z_OFFSET - segmentsLocal[i].avgZ - 1.5f);
 +    depthFactor = constrain(depthFactor, 0.0f, 1.0f);
 +    uint8_t brightnessg = (uint8_t)(99.0 - (depthFactor * 99.0 * 0.5));
 +    uint8_t brightnessb = (uint8_t)(168.0 - (depthFactor * 168.0 * 0.5));
 +    uint16_t color = rgb888_to_565(0, brightnessg, brightnessb);
 +    DrawThickLineRoundedHLines(
 +      canvas,
 +      segmentsLocal[i].x1, segmentsLocal[i].y1,
 +      segmentsLocal[i].x2, segmentsLocal[i].y2,
 +      segmentsLocal[i].thickness,
 +      color
 +    );
 +  }
 +}
 +
 +void setup() {
 +  tft.init(320, 480, 0, 0, ST7796S_BGR);
 +  tft.setSPISpeed(40000000);
 +  tft.invertDisplay(true);
 +  tft.setRotation(0);
 +}
 +
 +void loop() {
 +  canvas.fillScreen(blackish);
 +
 +  // Create the base shape only once (static initialization).
 +  static vector<Vec3> baseShape = createShape();
 +  vector<Vec3> rotatedShape = rotateShape(baseShape, rotation, rotation, rotation);
 +  renderShape(rotatedShape, 30, 30, 50);
 +  canvas.setCursor(5, 60);
 +  canvas.setTextColor(rgb888_to_565(0, 99, 168));
 +  canvas.print("EMBRIENT");
 +  
 +  // Draw the off-screen canvas onto the display.
 +  tft.drawRGBBitmap(0, 0, canvas.getBuffer(), SCREEN_WIDTH, SCREEN_HEIGHT);
 +
 +  // Increment rotation for next frame.
 +  rotation += 4.0;
 +  if (rotation >= 360.0) rotation -= 360.0;
 +}
 +
 </code> </code>
arduino_gfx_helpers.1742936782.txt.gz ยท Last modified: 2025/03/25 21:06 by kenson

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki