{"id":21332787,"url":"https://github.com/lowfatcode/pretty-poly","last_synced_at":"2025-07-12T10:31:43.430Z","repository":{"id":193763658,"uuid":"527170931","full_name":"lowfatcode/pretty-poly","owner":"lowfatcode","description":"A super-sampling complex polygon renderer for low resource platforms.","archived":false,"fork":false,"pushed_at":"2024-07-25T07:20:52.000Z","size":1735,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-07-25T08:39:16.606Z","etag":null,"topics":["antialiasing","cpp","header-only","microcontrollers","vector-graphics"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lowfatcode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-21T10:18:10.000Z","updated_at":"2024-07-25T07:20:56.000Z","dependencies_parsed_at":"2024-07-23T01:38:41.010Z","dependency_job_id":null,"html_url":"https://github.com/lowfatcode/pretty-poly","commit_stats":null,"previous_names":["lowfatcode/pretty-poly"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lowfatcode%2Fpretty-poly","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lowfatcode%2Fpretty-poly/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lowfatcode%2Fpretty-poly/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lowfatcode%2Fpretty-poly/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lowfatcode","download_url":"https://codeload.github.com/lowfatcode/pretty-poly/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225814744,"owners_count":17528295,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["antialiasing","cpp","header-only","microcontrollers","vector-graphics"],"created_at":"2024-11-21T22:53:18.598Z","updated_at":"2024-11-21T22:53:19.174Z","avatar_url":"https://github.com/lowfatcode.png","language":"C","readme":"\u003cimg src=\"logo.png\" alt=\"Kiwi standing on oval\" width=\"300px\"\u003e\n\n# Pretty Poly - A super-sampling complex polygon renderer for low resource platforms. 🦜\n\n- [Why?](#why)\n- [Approach](#approach)\n- [Using Pretty Poly](#using-pretty-poly)\n  - [Defining your polygons](#defining-your-polygons)\n  - [Primitive shapes](#primitive-shapes)\n  - [Clipping](#clipping)\n  - [Antialiasing](#antialiasing)\n  - [Transformations](#transformations)\n  - [Rendering](#rendering)\n  - [Implementing the tile renderer callback](#implementing-the-tile-renderer-callback)\n- [Types](#types)\n  - [`pp_tile_callback_t`](#pp_tile_callback_t)\n  - [`pp_tile_t`](#pp_tile_t)\n  - [`pp_point_t`](#pp_point_t)\n  - [`pp_path_t`](#pp_path_t)\n  - [`pp_poly_t`](#pp_poly_t)\n  - [`pp_rect_t`](#pp_rect_t)\n  - [`pp_mat3_t`](#pp_mat3_t)\n  - [`pp_antialias_t`](#pp_antialias_t)\n- [Performance considerations](#performance-considerations)\n  - [CPU speed](#cpu-speed)\n  - [Antialiasing](#antialiasing-1)\n  - [Coordinate type](#coordinate-type)\n  - [Tile size](#tile-size)\n- [Memory usage](#memory-usage)\n\n## Why?\n\nGenerally, microcontrollers struggle to support high-resolution displays. \nThis limitation arises from their lack of high-speed peripherals and \ninsufficient memory to house a large framebuffer. \n\nHowever, today's microcontrollers possess the processing muscle to execute \nreal-time anti-aliasing, thereby enabling high-quality vector graphics and text \non displays with relatively low dot pitch.\n\n\u003e The logo you see above is crafted by Pretty Poly! It consists of a single \n\u003e polygon featuring eleven contours: one for the outline and ten more for the \n\u003e lettering's holes. To see how it's done, take a look at examples/logo.c!\n\nYour hardware project doesn't have to look like a relic from the '80s \nanymore - unless that's the vibe you're going for. In which case, you can \neffortlessly recreate that retro aesthetic even better than the real thing!\n\nPretty Poly offers a pixel-format-agnostic, anti-aliased complex polygon \ndrawing engine, specifically engineered for optimal performance on \nresource-limited microcontrollers.\n\n## Approach\n\nTo optimize memory usage, Pretty Poly utilizes a tile-based rendering \ntechnique. This allows you to render intricate polygons with up to 16x \nanti-aliasing, all while requiring just around 6kB of statically allocated \nmemory - ideal for many embedded projects.\n\nEach tile is generated as an 8-bit mask image. This flexible approach allows \nyou to easily blend these mask images into your existing framebuffer, offering\nyou greater control and compatibility with various display configurations.\n\nFeatures:\n\n- **Renders polygons**: concave, self-intersecting, multi contour, holes, etc.\n- **C17 header only library**: simply copy the header file into your project\n- **Tile based renderer**: low memory footprint, cache coherency\n- **Low memory usage**: A few kilobytes of heap memory required\n- **High speed on low resource platforms**: optionally no floating point\n- **Antialiasing**: X1 (none), X4 and X16 super sampling supported\n- **Bounds clipping**: all results clipped to supplied clip rectangle\n- **Pixel format agnostic**: renders a \"tile\" to blend into your framebuffer\n- **RP2040 goodies**: hardware interpolators (thanks @MichaelBell!)\n\nIt's a resource-efficient, high-quality polygon rendering solution specifically \ntailored for microcontrollers.\n\n## Using Pretty Poly\n\nPretty Poly is a header only C17 library.\n\nMaking use of it is as easy as copying `pretty-poly.h` into your project and \nincluding it in the source file where you need to access.\n\nA basic example might look like:\n\n```c\n#include \"pretty-poly.h\"\n\nvoid callback(const tile_t *tile) {\n  // TODO: process the tile data here - see below for details\n}\n\nint main() {\n  // supply your tile blending callback function\n  pp_tile_callback(callback); \n\n  // specificy the level of antialiasing\n  pp_antialias(PP_AA_X4);\n\n  // set the clip rectangle\n  pp_clip(0, 0, WIDTH, HEIGHT);\n\n  // create a 256 x 256 square centered around 0, 0 with a 128 x 128 hole\n  pp_point_t outline[] = {{-128, -128}, {128, -128}, {128, 128}, {-128, 128}};\n  pp_point_t hole[]    = {{ -64,   64}, { 64,   64}, { 64, -64}, { -64, -64}};\n  pp_path_t paths[] = {\n    {.points = outline, .count = 4},\n    {.points = hole,    .count = 4}\n  };\n  pp_poly_t poly = {.paths = paths, .count = 2};\n\n  // draw the polygon\n  pp_render(\u0026poly);\n\n  return 0;\n}\n```\n\n### Defining your polygons\n\nA polygon is constructed using one or more distinct paths. These paths have two key roles: they either sketch out the external perimeter of the polygon (points in clockwise order), or they delineate empty spaces within it, which are often referred to as holes (anti-clockwise).\n\nEach path consists of a series of points that form a closed figure. Implicitly, the final point is connected back to the initial one to complete the shape.\n\nFor example:\n\n```c\n  // other setup code here...\n\n  // create a 256 x 256 square centered around 0, 0 with a 128 x 128 hole\n  pp_point_t outline[] = {{-128, -128}, {128, -128}, {128, 128}, {-128, 128}};\n  pp_point_t hole[]    = {{ -64,   64}, { 64,   64}, { 64, -64}, { -64, -64}};\n  pp_path_t paths[] = {\n    {.points = outline, .count = 4},\n    {.points = hole,    .count = 4}\n  };\n  pp_poly_t poly = {.paths = paths, .count = 2};\n  pp_render(\u0026poly);\n```\n\n### Primitive shapes\n\n\n\n### Clipping\n\nAll rendering will be clipped to the supplied coordinates meaning you do not\nneed to perform any bounds checking in your rendering callback function - \nnormally you would set this to your screen bounds though it could also be used \nto limit drawing to a specific area of the screen.\n\n```c\n  // other setup code here...\n  pp_clip(0, 0, 320, 240); // set clipping region to bounds of screen\n  pp_render(\u0026poly);        // render my poly\n```\n\n### Antialiasing\n\nOne of the most interesting features of Pretty Poly is the ability of its\nrasteriser to antialias (AKA super-sample) the output - this is achieved by\nrendering the polygon at a larger scale and then counting how many pixels\nwithin each sampling area fall inside or outside of the polygon.\n\nThe supported antialiasing levels are:\n\n  - `PP_AA_NONE`: no antialiasing\n  - `PP_AA_X4`: 4x super-sampling (2x2 sample grid)\n  - `PP_AA_X16`: 16x super-sampling (4x4 sample grid)\n\nExample:\n\n```c\n  // other setup code here...\n  pp_antialias(PP_AA_X4); // set 4x antialiasing\n  pp_render(\u0026poly);       // render my poly\n```\n  \n### Transformations\n\nDuring rendering you can optionally supply a transformation matrix which will\nbe applied to all geometry of the polygon being rendered. This is extremely\nhandy if you want to rotate, scale, or move the polygon.\n\n```c\n  // other setup code here...\n  pp_mat3_t t = pp_mat3_identity(); // get a fresh identity matrix\n  pp_transform(\u0026t);                 // set transformation matrix\n  \n  pp_mat3_rotate(\u0026t, 30);           // rotate by 30 degrees\n  pp_render(\u0026poly);                // render my poly\n\n  // move \"right\" by 50 units, because `pp_transform()` took a pointer to\n  // our matrix `t` we can modify the matrix and in doing so also modify\n  // the transform applied by `pp_polygon``\n  pp_mat3_translate(\u0026t, 50, 0);     \n  pp_render(\u0026poly);                // render my poly again\n```\n\nThere are a number of helper methods to create and manipulate matrices:\n\n- `pp_mat3_identity()`: returns a new identity matrix\n- `pp_mat3_rotate(*m, a);`: rotate `m` by `a` degrees\n- `pp_mat3_rotate_rad(*m, a);`: rotate `m` by `a` radians\n- `pp_mat3_translate(*m, x, y);`: translate `m` by `x`, `y` units\n- `pp_mat3_scale(*m, x, y);`: scale `m` by `x`, `y` units\n- `pp_mat3_mul(*m1, *m2);`: multiple `m1` by `m2`\n\n### Rendering\n\nOnce you have setup your polygon, clipping, antialiasing, and transform set \nthen a call to `pp_render` will do the rest. As each tile is processed it \nwill be passed into your tile rendering callback function so that you can \nblend it into your framebuffer.\n\n```c\n  void blend_tile(const pp_tile_t *t) {\n    // iterate over each pixel in the rendered tile\n    for(int32_t y = t-\u003ey; y \u003c t-\u003ey + t-\u003eh; y++) {\n      for(int32_t x = t-\u003ex; x \u003c t-\u003ex + t-\u003ew; x++) {     \n        // get the \"value\" at x, y - this will be a value between 0 and 255\n        // which can be used as an alpha value for your blend function\n        uint8_t v = pp_tile_get(t, x, y));\n\n        // call your blending function here\n        buffer[y][x] = blend(buffer[y][x], v); // \u003c- it might look like this\n      }\n    }\n  }\n\n  int main() {\n    // other setup code here...\n    pp_tile_callback(blend_tile);\n  }\n```\n\n\u003e Pretty Poly provides you the rasterised polygon information as a single\n\u003e 8-bit per pixel mask image. It doesn't care what format or bit depth your \n\u003e framebuffer is allowing it to work any combination of software and hardware.\n\nYour callback function will be called multiple times per polygon depending on \nthe size and shape, the capacity of the tile buffer, and the level of \nantialiasing used.\n\n  \n### Implementing the tile renderer callback\n\nYour tile renderer callback function will be passed a const pointer to a \n[`pp_tile_t`](#pp_tile_t) object which contains all of the information needed\nto blend the rendered tile into your framebuffer.\n\n```c\nvoid tile_blend_callback(const pp_tile_t *tile) {\n  // process the tile image data here\n}\n```\n\n\u003e `pp_tile_t` bounds are in framebuffer coordinate space and will always be \n\u003e clipped against your supplied clip rectangle so it is not necessary for you \n\u003e to do bounds checking again when rendering.\n\nThe `x` and `y` properties contain the offset within the framebuffer where\nthis tile needs to be blended (i.e. the top left corner). \n\nEach tile returned by the renderer can be a different size depending on the\nsize and shape of the polygon and your supplied clipping rectangle. You need\nto use the `w` and `h` properties to determine the area of the framebuffer\nthis tile covers.\n\nThere are two main approaches to implementing your callback function.\n\n**1\\. Using `pp_tile_get(t, x, y)` - the slower, easier, option**\n\nPretty Poly provides a simple way to get the value of a specific coordinate of \nthe tile.\n\nThe `tile` object provides a `get_value()` method which always returns a value \nbetween `0` and `255` - this is slower that reading the tile data directly \n(since we need a function call per pixel) but can be helpful to get up and \nrunning more quickly.\n\n```c\nvoid callback(const pp_tile_t *t) {\n  for(int y = t-\u003ey; y \u003c t-\u003ey + t-\u003eh; y++) {\n    for(int x = t-\u003ex; x \u003c t-\u003ex + t-\u003ew; x++) {      \n      uint8_t alpha = pp_tile_get(t, x, y);\n      // call your blend function here      \n    }\n  }\n}\n```\n\nIf this is fast enough for your usecase then congratulations! 🥳 You have just \nsaved future you from some debugging... 🤦\n\n**2\\. Using `pp_tile_t.data` directly - much faster, but more complicated**\n\nWith this approach you need to handle the raw tile data. This is a lot faster \nthan using the `pp_tile_get()` helper function as it avoids making a function \ncall for every pixel.\n\nYou can also potentially optimise in other ways:\n\n- read the buffer in larger chunks (32 bits at a time for example)\n- check if the next word, or dword is 0 and skip multiple pixels in one go\n- equally check if the value is 0xff, 0xffff, etc and write multiple opaque \n  pixels in one go\n- scale `value` to better match your framebuffer format\n- scale `value` in other ways (not necessarily linear!) to apply effects\n\nHere we assume we're using X4 supersampling - this is not intended to show the \nfastest possible implementation but rather one that's relatively \nstraightforward to understand.\n\n```c\nvoid callback(const pp_tile_t *t) {\n  // pointer to start of tile data\n  uint8_t *p = t-\u003edata;\n\n  // iterate over the valid portion of tile data\n  for(int y = t-\u003ey; y \u003c t-\u003ey + t-\u003eh; y++) {\n    for(int x = t-\u003ex; x \u003c t-\u003ex + t-\u003ew; x++) {           \n      uint8_t alpha = *p++;\n      // call your blend function here      \n    }\n\n    // advance to start of next row of tile data\n    p += t-\u003estride - t-\u003ew;\n  }\n}\n```\n## Types\n\n### `pp_tile_callback_t`\n\nCallback function prototype.\n\n```c\ntypedef void (*pp_tile_callback_t)(const pp_tile_t *tile);\n```\n\nCreate your own matching callback function to supply to `pp_tile_callback()` - \nfor example:\n\n```c\nvoid tile_render_callback(const pp_tile_t *tile) {\n    // perform your framebuffer blending here\n}\n```\n\nNote that on RP2040 interp1 is used by pretty poly.  If your callback uses \ninterp1 it must save and restore the state.\n\n### `pp_tile_t`\n\nInformation needed to blend a rendered tile into your framebuffer.\n\n```c\n  struct pp_tile_t {\n    int32_t x, y, w, h;  // bounds of tile in framebuffer coordinates\n    uint32_t stride;     // row stride of tile data\n    uint8_t *data;       // pointer to start of mask data\n  };\n\n  uint8_t pp_tile_get(const pp_tile_t *tile, const int32_t x, const int32_t y);\n```\n\nThis object is passed into your callback function for each tile providing the \narea of the framebuffer to write to with the mask data needed for blending.\n\n`uint8_t pp_tile_get(pp_tile_t *tile, int32_t x, int32_t y)`\n\nReturns the value in the tile at `x`, `y`.\n\n### `pp_point_t`\n\nDefines a coordinate in a polygon path.\n\n```c\n  typedef struct __attribute__((__packed__)) {\n    PP_COORD_TYPE x, y;\n  } pp_point_t;\n\n  pp_point_t pp_point_add(pp_point_t *p1, pp_point_t *p2);\n  pp_point_t pp_point_sub(pp_point_t *p1, pp_point_t *p2);\n  pp_point_t pp_point_mul(pp_point_t *p1, pp_point_t *p2);\n  pp_point_t pp_point_div(pp_point_t *p1, pp_point_t *p2);\n  pp_point_t pp_point_transform(pp_point_t *p, pp_mat3_t *m);\n```\n\n### `pp_path_t`\n\n```c\ntypedef struct {\n  pp_point_t *points;\n  uint32_t count;\n} pp_path_t;\n```\n\n### `pp_poly_t`\n\n```c\ntypedef struct {\n  pp_path_t *paths;\n  uint32_t count;\n} pp_poly_t;\n```\n\n### `pp_rect_t`\n\nDefines a rectangle with a top left corner, width, and height.\n\n```c\n  typedef struct {\n    int32_t x, y, w, h;\n  } pp_rect_t;\n\n  bool pp_rect_empty(pp_rect_t *r);\n  pp_rect_t pp_rect_intersection(pp_rect_t *r1, pp_rect_t *r2);\n  pp_rect_t pp_rect_merge(pp_rect_t *r1, pp_rect_t *r2);\n  pp_rect_t pp_rect_transform(pp_rect_t *r, pp_mat3_t *m);\n```\n\nUsed to define clipping rectangle and tile bounds.\n\n### `pp_mat3_t`\n\n3x3 matrix type for defining 2D transforms.\n\n```c\n  typedef struct {\n    float v00, v10, v20, v01, v11, v21, v02, v12, v22;\n  } pp_mat3_t;\n\n  pp_mat3_t pp_mat3_identity();\n  void pp_mat3_rotate(pp_mat3_t *m, float a);\n  void pp_mat3_rotate_rad(pp_mat3_t *m, float a);\n  void pp_mat3_translate(pp_mat3_t *m, float x, float y);\n  void pp_mat3_scale(pp_mat3_t *m, float x, float y);\n  void pp_mat3_mul(pp_mat3_t *m1, pp_mat3_t *m2);\n```\n\n### `pp_antialias_t`\n\nEnumeration of valid anti-aliasing modes.\n\n```c\nenum antialias_t {\n  PP_AA_NONE = 0, // no antialiasing\n  PP_AA_X4   = 1, // 4x super sampling (2x2 grid)\n  PP_AA_X16  = 2  // 16x super sampling (4x4 grid)\n};\n```\n\n## Performance considerations\n\n### CPU speed\n\nIn principle Pretty Poly can function on any speed of processor - even down to\nKHz clock speeds (if you don't mind waiting!).\n\nIt runs really nicely on Cortex M0 and above - ideally at 50MHz+ with an \ninstruction and data cache. On hardware at this level and above it can achieve \nsurprisingly smooth animation of complex shapes in realtime.\n\n\u003e Originally Pretty Poly was developed for use on Pimoroni's RP2040 (a Cortex \n\u003e M0+ @ 125MHz) based products.\n\n### Antialiasing\n\nAntialiasing can have a big effect on performance since the rasteriser has to \ndraw polygons either 4 or 16 times larger to achieve its sampling.\n\n### Coordinate type\n\nBy default Pretty Poly uses single precision `float` values to store \ncoordinates allowing for sub pixel accuracy. On systems where floating point \noperations are too slow, or if you want to reduce the size of coordinates \nstored in memory you can override this setting by defining `PP_COORD_TYPE` \nbefore including `pretty-poly.h`.\n\nFor example:\n\n```c\n  #define PP_COORD_TYPE int16_t\n  #include \"pretty-poly.h\"\n\n  // points will be 4 bytes and have integer coordinates\n  pp_point_t p = {.x = 314, .y = 159}; \n```\n\nUsing integer coordinates may result in some \"jitter\" if you animating shapes \nas their coordinates while have to snap to individual pixels.\n\n_Note_: transformations using the `pp_mat3_t` struct will also us `float` \noperations regardless of what coordinate type you're using.\n\n### Tile size\n\nThe more memory you can afford to assign to the tile buffer the better. \nPreparing and rasterising each tile has a fixed overhead so fewer, larger, \ntiles is always preferable.\n\nI've found 4KB to be the sweet spot when trading off between speed and memory \nusage on the RP2040 but if you are working under tighter memory limitations you \nmay wish to reduce the buffer size down to 1KB or even 256 bytes.\n\n## Memory usage\n\nBy default Pretty Polly allocates a few buffers used for rendering tile data,\ncalculating scanline intersections, and maintaining state. The default \nconfiguration reserves about 6kB on the heap for this purpose.\n\n\u003e The default values have been selected as a good compromise between memory use\n\u003e and performance and we don't recommend changing them unless you have a good\n\u003e reason to!\n\nYou can reduce the amount of memory used at the cost of some performance by\ndefining the rasteriser parameters before including the Pretty Poly header \nfile.\n\n```c\n#define PP_NODE_BUFFER_HEIGHT 16\n#define PP_MAX_NODES_PER_SCANLINE 16\n#define PP_TILE_BUFFER_SIZE 4096\n#include \"pretty-poly.h\"\n```\n\n`PP_NODE_BUFFER_HEIGHT`  \nDefault: `16`\n\nThe maximum number of scanlines per tile - doesn't normally have a big impact \non performance. Larger values will quickly consume more memory.\n\n`PP_MAX_NODES_PER_SCANLINE`  \nDefault: `16`\n\nThe maximum number of line segments that can pass through any given scanline. \nYou may need to increase this value if you have very complex polygons.\n\n`PP_TILE_BUFFER_SIZE`  \nDefault: `4096`\n\nThe number of bytes to allocate for the tile buffer - in combination with the\ncurrent antialias setting and the `PP_NODE_BUFFER_HEIGHT` this will determine\nthe maximum width of rendererd tiles.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flowfatcode%2Fpretty-poly","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flowfatcode%2Fpretty-poly","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flowfatcode%2Fpretty-poly/lists"}