#ifdef MESHTASTIC_INCLUDE_INKHUD #include "./Applet.h" #include "main.h" #include "RTC.h" using namespace NicheGraphics; InkHUD::AppletFont InkHUD::Applet::fontLarge; // General purpose font. Set by setDefaultFonts InkHUD::AppletFont InkHUD::Applet::fontSmall; // General purpose font. Set by setDefaultFonts constexpr float InkHUD::Applet::LOGO_ASPECT_RATIO; // Ratio of the Meshtastic logo InkHUD::Applet::Applet() : GFX(0, 0) { // GFX is given initial dimensions of 0 // The width and height will change dynamically, depending on Applet tiling // If you're getting a "divide by zero error", consider it an assert: // WindowManager should be the only one controlling the rendering inkhud = InkHUD::getInstance(); settings = &inkhud->persistence->settings; latestMessage = &inkhud->persistence->latestMessage; } // Draw a single pixel // The raw pixel output generated by AdafruitGFX drawing all passes through here // Hand off to the applet's tile, which will in-turn pass to the renderer void InkHUD::Applet::drawPixel(int16_t x, int16_t y, uint16_t color) { // Only render pixels if they fall within user's cropped region if (x >= cropLeft && x < (cropLeft + cropWidth) && y >= cropTop && y < (cropTop + cropHeight)) assignedTile->handleAppletPixel(x, y, (Color)color); } // Link our applet to a tile // This can only be called by Tile::assignApplet // The tile determines the applets dimensions // Pixel output is passed to tile during render() void InkHUD::Applet::setTile(Tile *t) { // If we're setting (not clearing), make sure the link is "reciprocal" if (t) assert(t->getAssignedApplet() == this); assignedTile = t; } // The tile to which our applet is assigned InkHUD::Tile *InkHUD::Applet::getTile() { return assignedTile; } // Draw the applet void InkHUD::Applet::render() { assert(assignedTile); // Ensure that we have a tile assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile // WindowManager::update has now consumed the info about our update request // Clear everything for future requests wantRender = false; // Flag set by requestUpdate wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored. wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted. updateDimensions(); resetDrawingSpace(); onRender(); // Derived applet's drawing takes place here // Handle "Tile Highlighting" // Some devices may use an auxiliary button to switch between tiles // When this happens, we temporarily highlight the newly focused tile with a border // If our tile is (or was) highlighted, to indicate a change in focus if (Tile::highlightTarget == assignedTile) { // Draw the highlight if (!Tile::highlightShown) { drawRect(0, 0, width(), height(), BLACK); Tile::startHighlightTimeout(); Tile::highlightShown = true; } // Clear the highlight else { Tile::cancelHighlightTimeout(); Tile::highlightShown = false; Tile::highlightTarget = nullptr; } } } // Does the applet want to render now? // Checks whether the applet called requestUpdate recently, in response to an event // Used by WindowManager::update bool InkHUD::Applet::wantsToRender() { return wantRender; } // Does the applet want to be moved to foreground before next render, to show new data? // User specifies whether an applet has permission for this, using the on-screen menu // Used by WindowManager::update bool InkHUD::Applet::wantsToAutoshow() { return wantAutoshow; } // Which technique would this applet prefer that the display use to change the image? // Used by WindowManager::update Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType() { return wantUpdateType; } // Get size of the applet's drawing space from its tile // Performed immediately before derived applet's drawing code runs void InkHUD::Applet::updateDimensions() { assert(assignedTile); WIDTH = assignedTile->getWidth(); HEIGHT = assignedTile->getHeight(); _width = WIDTH; _height = HEIGHT; } // Ensure that render() always starts with the same initial drawing config void InkHUD::Applet::resetDrawingSpace() { resetCrop(); // Allow pixel from any region of the applet to draw setTextColor(BLACK); // Reset text params setCursor(0, 0); setTextWrap(false); setFont(fontSmall); } // Tell InkHUD::Renderer that we want to render now // Applets should internally listen for events they are interested in, via MeshModule, CallbackObserver etc // When an applet decides it has heard something important, and wants to redraw, it calls this method // Once the renderer has given other applets a chance to process whatever event we just detected, // it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground) // We should requestUpdate even if our applet is currently background, because this might be changed by autoshow void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type) { wantRender = true; wantUpdateType = type; inkhud->requestUpdate(); } // Ask window manager to move this applet to foreground at start of next render // Users select which applets have permission for this using the on-screen menu void InkHUD::Applet::requestAutoshow() { wantAutoshow = true; } // Called when an Applet begins running // Active applets are considered "enabled" // They should now listen for events, and request their own updates // They may also be unexpectedly renderer at any time by other InkHUD components // Applets can be activated at run-time through the on-screen menu void InkHUD::Applet::activate() { onActivate(); // Call derived class' handler active = true; } // Called when an Applet stops running // Inactive applets are considered "disabled" // They should not listen for events, process data // They will not be rendered // Applets can be deactivated at run-time through the on-screen menu void InkHUD::Applet::deactivate() { // If applet is still in foreground, run its onBackground code first if (isForeground()) sendToBackground(); // If applet is active, run its onDeactivate code first if (isActive()) onDeactivate(); // Derived class' handler active = false; } // Is the Applet running? // Note: active / inactive is not related to background / foreground // An inactive applet is *fully* disabled bool InkHUD::Applet::isActive() { return active; } // Begin showing the Applet // It will be rendered immediately to whichever tile it is assigned // The Renderer will also now honor requestUpdate() calls from this applet void InkHUD::Applet::bringToForeground() { if (!foreground) { foreground = true; onForeground(); // Run derived applet class' handler } requestUpdate(); } // Stop showing the Applet // Calls to requestUpdate() will no longer be honored // When one applet moves to background, another should move to foreground (exception: some system applets) void InkHUD::Applet::sendToBackground() { if (foreground) { foreground = false; onBackground(); // Run derived applet class' handler } } // Is the applet currently displayed on a tile // Note: in some uncommon situations, an applet may be "foreground", and still not visible. // This can occur when a system applet is covering the screen (e.g. during BLE pairing) // This is not our applets responsibility to handle, // as in those situations, the system applet will have "locked" rendering bool InkHUD::Applet::isForeground() { return foreground; } // Limit drawing to a certain region of the applet // Pixels outside this region will be discarded void InkHUD::Applet::setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height) { cropLeft = left; cropTop = top; cropWidth = width; cropHeight = height; } // Allow drawing to any region of the Applet // Reverses Applet::setCrop void InkHUD::Applet::resetCrop() { setCrop(0, 0, width(), height()); } // Convert relative width to absolute width, in px // X(0) is 0 // X(0.5) is width() / 2 // X(1) is width() uint16_t InkHUD::Applet::X(float f) { return width() * f; } // Convert relative hight to absolute height, in px // Y(0) is 0 // Y(0.5) is height() / 2 // Y(1) is height() uint16_t InkHUD::Applet::Y(float f) { return height() * f; } // Print text, specifying the position of any edge / corner of the textbox void InkHUD::Applet::printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha, VerticalAlignment va) { // We do still have to run getTextBounds to find the width int16_t textOffsetX, textOffsetY; uint16_t textWidth, textHeight; getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); int16_t cursorX = 0; int16_t cursorY = 0; switch (ha) { case LEFT: cursorX = x - textOffsetX; break; case CENTER: cursorX = (x - textOffsetX) - (textWidth / 2); break; case RIGHT: cursorX = (x - textOffsetX) - textWidth; break; } // We're using a fixed line height, rather than sizing to text (getTextBounds) switch (va) { case TOP: cursorY = y + currentFont.heightAboveCursor(); break; case MIDDLE: cursorY = (y + currentFont.heightAboveCursor()) - (currentFont.lineHeight() / 2); break; case BOTTOM: cursorY = (y + currentFont.heightAboveCursor()) - currentFont.lineHeight(); break; } setCursor(cursorX, cursorY); print(text); } // Print text, specifying the position of any edge / corner of the textbox void InkHUD::Applet::printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha, VerticalAlignment va) { printAt(x, y, text.c_str(), ha, va); } // Set which font should be used for subsequent drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data void InkHUD::Applet::setFont(AppletFont f) { GFX::setFont(f.gfxFont); currentFont = f; } // Get which font is currently being used for drawing // This is AppletFont type, which is a wrapper for AdafruitGFX font, with some precalculated dimension data InkHUD::AppletFont InkHUD::Applet::getFont() { return currentFont; } // Parse any text which might have "special characters" // Re-encodes UTF-8 characters to match our 8-bit encoded fonts std::string InkHUD::Applet::parse(std::string text) { return getFont().decodeUTF8(text); } // Get the best version of a node's short name available to us // Parses any non-ascii chars // Swaps for last-four of node-id if the real short name is unknown or can't be rendered (emoji) std::string InkHUD::Applet::parseShortName(meshtastic_NodeInfoLite *node) { assert(node); // Use the true shortname if known, and doesn't contain any unprintable characters (emoji, etc.) if (node->has_user) { std::string parsed = parse(node->user.short_name); if (isPrintable(parsed)) return parsed; } // Otherwise, use the "last 4" of node id // - if short name unknown, or // - if short name is emoji (we can't render this) std::string nodeID = hexifyNodeNum(node->num); return nodeID.substr(nodeID.length() - 4); } // Determine if all characters of a string are printable using the current font bool InkHUD::Applet::isPrintable(std::string text) { // Scan for DEL (0x7F), which is the value assigned by AppletFont::applyEncoding if a unicode character is not handled // Todo: move this to from DEL to SUB, once the fonts have been changed for this for (char &c : text) { if (c == '\x7F') return false; } // No unprintable characters found return true; } // Gets rendered width of a string // Wrapper for getTextBounds uint16_t InkHUD::Applet::getTextWidth(const char *text) { // We do still have to run getTextBounds to find the width int16_t textOffsetX, textOffsetY; uint16_t textWidth, textHeight; getTextBounds(text, 0, 0, &textOffsetX, &textOffsetY, &textWidth, &textHeight); return textWidth; } // Gets rendered width of a string // Wrapper for getTextBounds uint16_t InkHUD::Applet::getTextWidth(std::string text) { return getTextWidth(text.c_str()); } // Evaluate SNR and RSSI to qualify signal strength at one of four discrete levels // Roughly comparable to values used by the iOS app; // I didn't actually go look up the code, just fit to a sample graphic I have of the iOS signal indicator InkHUD::Applet::SignalStrength InkHUD::Applet::getSignalStrength(float snr, float rssi) { uint8_t score = 0; // Give a score for the SNR if (snr > -17.5) score += 2; else if (snr > -26.0) score += 1; // Give a score for the RSSI if (rssi > -115.0) score += 3; else if (rssi > -120.0) score += 2; else if (rssi > -126.0) score += 1; // Combine scores, then give a result if (score >= 5) return SIGNAL_GOOD; else if (score >= 4) return SIGNAL_FAIR; else if (score > 0) return SIGNAL_BAD; else return SIGNAL_NONE; } // Apply the standard "node id" formatting to a nodenum int: !0123abdc std::string InkHUD::Applet::hexifyNodeNum(NodeNum num) { // Not found in nodeDB, show a hex nodeid instead char nodeIdHex[10]; sprintf(nodeIdHex, "!%0x", num); // Convert to the typical "fixed width hex with !" format return std::string(nodeIdHex); } // Print text, with word wrapping // Avoids splitting words in half, instead moving the entire word to a new line wherever possible void InkHUD::Applet::printWrapped(int16_t left, int16_t top, uint16_t width, std::string text) { // Place the AdafruitGFX cursor to suit our "top" coord setCursor(left, top + getFont().heightAboveCursor()); // How wide a space character is // Used when simulating print, for dimensioning // Works around issues where getTextDimensions() doesn't account for whitespace const uint8_t wSp = getFont().widthBetweenWords(); // Move through our text, character by character uint16_t wordStart = 0; for (uint16_t i = 0; i < text.length(); i++) { // Found: end of word (split by spaces or newline) // Also handles end of string if (text[i] == ' ' || text[i] == '\n' || i == text.length() - 1) { // Isolate this word uint16_t wordLength = (i - wordStart) + 1; // Plus one. Imagine: "a". End - Start is 0, but length is 1 std::string word = text.substr(wordStart, wordLength); wordStart = i + 1; // Next word starts *after* the space // If word is terminated by a newline char, don't actually print it. // We'll manually add a new line later if (word.back() == '\n') word.pop_back(); // Measure the word, in px int16_t l, t; uint16_t w, h; getTextBounds(word.c_str(), getCursorX(), getCursorY(), &l, &t, &w, &h); // Word is short if (w < width) { // Word fits on current line if ((l + w + wSp) < left + width) print(word.c_str()); // Word doesn't fit on current line else { setCursor(left, getCursorY() + getFont().lineHeight()); // Newline print(word.c_str()); } } // Word is really long // (wider than applet) else { // Horribly inefficient: // Rather than working directly with the glyph sizes, // we're going to run everything through getTextBounds as a c-string of length 1 // This is because AdafruitGFX has special internal handling for their legacy 6x8 font, // which would be a pain to add manually here. // These super-long strings probably don't come up often so we can maybe tolerate this. // Todo: rewrite making use of AdafruitGFX native text wrapping char cstr[] = {0, 0}; int16_t l, t; uint16_t w, h; for (uint16_t c = 0; c < word.length(); c++) { // Shove next char into a c string cstr[0] = word[c]; getTextBounds(cstr, getCursorX(), getCursorY(), &l, &t, &w, &h); // Manual newline, if next character will spill beyond screen edge if ((l + w) > left + width) setCursor(left, getCursorY() + getFont().lineHeight()); // Print next character print(word[c]); } } } // If word was terminated by a newline char, manually add the new line now if (text[i] == '\n') { setCursor(left, getCursorY() + getFont().lineHeight()); // Manual newline wordStart = i + 1; // New word begins after the newline. Otherwise print will add an *extra* line } } } // Simulate running printWrapped, to determine how tall the block of text will be. // This is a wasteful way of handling things. Maybe some way to optimize in future? uint32_t InkHUD::Applet::getWrappedTextHeight(int16_t left, uint16_t width, std::string text) { // Cache the current crop region int16_t cL = cropLeft; int16_t cT = cropTop; uint16_t cW = cropWidth; uint16_t cH = cropHeight; setCrop(-1, -1, 0, 0); // Set crop to temporarily discard all pixels printWrapped(left, 0, width, text); // Simulate only - no pixels drawn // Restore previous crop region cropLeft = cL; cropTop = cT; cropWidth = cW; cropHeight = cH; // Note: printWrapped() offsets the initial cursor position by heightAboveCursor() val, // so we need to account for that when determining the height return (getCursorY() + getFont().heightBelowCursor()); } // Fill a region with sparse diagonal lines, to create a pseudo-translucent fill void InkHUD::Applet::hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color) { // Cache the currently cropped region int16_t oldCropL = cropLeft; int16_t oldCropT = cropTop; uint16_t oldCropW = cropWidth; uint16_t oldCropH = cropHeight; setCrop(x, y, w, h); // Draw lines starting along the top edge, every few px for (int16_t ix = x; ix < x + w; ix += spacing) { for (int16_t i = 0; i < w || i < h; i++) { drawPixel(ix + i, y + i, color); } } // Draw lines starting along the left edge, every few px for (int16_t iy = y; iy < y + h; iy += spacing) { for (int16_t i = 0; i < w || i < h; i++) { drawPixel(x + i, iy + i, color); } } // Restore any previous crop // If none was set, this will clear cropLeft = oldCropL; cropTop = oldCropT; cropWidth = oldCropW; cropHeight = oldCropH; } // Get a human readable time representation of an epoch time (seconds since 1970) // If time is invalid, this will be an empty string std::string InkHUD::Applet::getTimeString(uint32_t epochSeconds) { #ifdef BUILD_EPOCH constexpr uint32_t validAfterEpoch = BUILD_EPOCH - (SEC_PER_DAY * 30 * 6); // 6 Months prior to build #else constexpr uint32_t validAfterEpoch = 1738368000 - (SEC_PER_DAY * 30 * 6); // 6 Months prior to Feb 1, 2025 12:00:00 AM GMT #endif uint32_t epochNow = getValidTime(RTCQuality::RTCQualityDevice, true); int32_t daysAgo = (epochNow - epochSeconds) / SEC_PER_DAY; int32_t hoursAgo = (epochNow - epochSeconds) / SEC_PER_HOUR; // Times are invalid: rtc is much older than when code was built // Don't give any human readable string if (epochNow <= validAfterEpoch) return ""; // Times are invalid: argument time is significantly ahead of RTC // Don't give any human readable string if (daysAgo < -2) return ""; // Times are probably invalid: more than 6 months ago if (daysAgo > 6 * 30) return ""; if (daysAgo > 1) return to_string(daysAgo) + " days ago"; else if (hoursAgo > 18) return "Yesterday"; else { uint32_t hms = epochSeconds % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; // Tear apart hms into h:m uint32_t hour = hms / SEC_PER_HOUR; uint32_t min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; // Format the clock string, either 12 hour or 24 hour char clockStr[11]; if (config.display.use_12h_clock) sprintf(clockStr, "%u:%02u %s", (hour % 12 == 0 ? 12 : hour % 12), min, hour > 11 ? "PM" : "AM"); else sprintf(clockStr, "%02u:%02u", hour, min); return clockStr; } } // If no argument specified, get time string for the current RTC time std::string InkHUD::Applet::getTimeString() { return getTimeString(getValidTime(RTCQuality::RTCQualityDevice, true)); } // Calculate how many nodes have been seen within our preferred window of activity // This period is set by user, via the menu // Todo: optimize to calculate once only per WindowManager::render uint16_t InkHUD::Applet::getActiveNodeCount() { // Don't even try to count nodes if RTC isn't set // The last heard values in nodedb will be incomprehensible if (getRTCQuality() == RTCQualityNone) return 0; uint16_t count = 0; // For each node in db for (uint16_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); // Check if heard recently, and not our own node if (sinceLastSeen(node) < settings->recentlyActiveSeconds && node->num != nodeDB->getNodeNum()) count++; } return count; } // Get an abbreviated, human readable, distance string // Honors config.display.units, to offer both metric and imperial std::string InkHUD::Applet::localizeDistance(uint32_t meters) { constexpr float FEET_PER_METER = 3.28084; constexpr uint16_t FEET_PER_MILE = 5280; // Resulting string std::string localized; // Imperial if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { uint32_t feet = meters * FEET_PER_METER; // Distant (miles, rounded) if (feet > FEET_PER_MILE / 2) { localized += to_string((uint32_t)roundf(feet / FEET_PER_MILE)); localized += "mi"; } // Nearby (feet) else { localized += to_string(feet); localized += "ft"; } } // Metric else { // Distant (kilometers, rounded) if (meters >= 500) { localized += to_string((uint32_t)roundf(meters / 1000.0)); localized += "km"; } // Nearby (meters) else { localized += to_string(meters); localized += "m"; } } return localized; } // Print text with a "faux bold" effect, by drawing it multiple times, offsetting slightly void InkHUD::Applet::printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY) { // How many times to draw along x axis int16_t xStart; int16_t xEnd; switch (thicknessX) { case 0: assert(false); case 1: xStart = xCenter; xEnd = xCenter; break; case 2: xStart = xCenter; xEnd = xCenter + 1; break; default: xStart = xCenter - (thicknessX / 2); xEnd = xCenter + (thicknessX / 2); } // How many times to draw along Y axis int16_t yStart; int16_t yEnd; switch (thicknessY) { case 0: assert(false); case 1: yStart = yCenter; yEnd = yCenter; break; case 2: yStart = yCenter; yEnd = yCenter + 1; break; default: yStart = yCenter - (thicknessY / 2); yEnd = yCenter + (thicknessY / 2); } // Print multiple times, overlapping for (int16_t x = xStart; x <= xEnd; x++) { for (int16_t y = yStart; y <= yEnd; y++) { printAt(x, y, text, CENTER, MIDDLE); } } } // Allow this applet to suppress notifications // Asked before a notification is shown via the NotificationApplet // An applet might want to suppress a notification if the applet itself already displays this info // Example: AllMessageApplet should not approve notifications for messages, if it is in foreground bool InkHUD::Applet::approveNotification(NicheGraphics::InkHUD::Notification &n) { // By default, no objection return true; } // Draw the standard header, used by most Applets /* ┌───────────────────────────────┐ │ Applet::name here │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ │ │ │ │ │ └───────────────────────────────┘ */ void InkHUD::Applet::drawHeader(std::string text) { // Y position for divider // - between header text and messages constexpr int16_t padDivH = 2; const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; // Print header printAt(0, padDivH, text); // Divider // - below header text: separates message // - above header text: separates other applets for (int16_t x = 0; x < width(); x += 2) { drawPixel(x, 0, BLACK); drawPixel(x, headerDivY, BLACK); // Dotted 50% } } // Get the height of the standard applet header // This will vary, depending on font // Applets use this value to avoid drawing overtop the header uint16_t InkHUD::Applet::getHeaderHeight() { // Y position for divider // - between header text and messages constexpr int16_t padDivH = 2; const int16_t headerDivY = padDivH + fontSmall.lineHeight() + padDivH - 1; return headerDivY + 1; // "Plus one": height is always one more than Y position } // "Scale to fit": width of Meshtastic logo to fit given region, maintaining aspect ratio uint16_t InkHUD::Applet::getLogoWidth(uint16_t limitWidth, uint16_t limitHeight) { // Determine whether we're limited by width or height // Makes sure we draw the logo as large as possible, within the specified region, // while still maintaining correct aspect ratio if (limitWidth > limitHeight * LOGO_ASPECT_RATIO) return limitHeight * LOGO_ASPECT_RATIO; else return limitWidth; } // "Scale to fit": height of Meshtastic logo to fit given region, maintaining aspect ratio uint16_t InkHUD::Applet::getLogoHeight(uint16_t limitWidth, uint16_t limitHeight) { // Determine whether we're limited by width or height // Makes sure we draw the logo as large as possible, within the specified region, // while still maintaining correct aspect ratio if (limitHeight > limitWidth / LOGO_ASPECT_RATIO) return limitWidth / LOGO_ASPECT_RATIO; else return limitHeight; } // Draw a scalable Meshtastic logo // Make sure to provide dimensions which have the correct aspect ratio (~2) // Three paths, drawn thick using quads, with one corner "radiused" /* - ^ /- /-\ // // \\ // // \\ // // \\ // // \\ */ void InkHUD::Applet::drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height, Color color) { struct Point { int x; int y; }; typedef Point Distance; int16_t logoTh = width * 0.068; // Thickness scales with width. Measured from logo at meshtastic.org. int16_t logoL = centerX - (width / 2) + (logoTh / 2); int16_t logoT = centerY - (height / 2) + (logoTh / 2); int16_t logoW = width - logoTh; int16_t logoH = height - logoTh; int16_t logoR = logoL + logoW - 1; int16_t logoB = logoT + logoH - 1; // Points for paths (a, b, and c) /* +-----------------------------+ --| a2 b2/c1 | | | | | | | --| a1 b1 c2 | +-----------------------------+ | | | | */ Point a1 = {map(0, 0, 3, logoL, logoR), logoB}; Point a2 = {map(1, 0, 3, logoL, logoR), logoT}; Point b1 = {map(1, 0, 3, logoL, logoR), logoB}; Point b2 = {map(2, 0, 3, logoL, logoR), logoT}; Point c1 = {map(2, 0, 3, logoL, logoR), logoT}; Point c2 = {map(3, 0, 3, logoL, logoR), logoB}; // Find angle of the path(s) // Used to thicken the single pixel paths /* +-------------------------------+ | a2 | | -| | | -/ | | | -/ | | | -/# | | | -/ # | | | / # | | | a1---------- | +-------------------------------+ */ Distance deltaA = {abs(a2.x - a1.x), abs(a2.y - a1.y)}; float angle = tanh((float)deltaA.y / deltaA.x); // Distance (at right angle to the paths), which will give corners for our "quads" // The distance is unsigned. We will vary the signedness of the x and y components to suit the path and corner /* | a2 | . | .. | aq1 .. | # .. | | # .. |fromPath.y | # .. | +----a1 | | fromPath.x +-------------------------------- */ Distance fromPath; fromPath.x = cos(radians(90) - angle) * logoTh * 0.5; fromPath.y = sin(radians(90) - angle) * logoTh * 0.5; // Make the paths thick // Corner points for the rectangles (quads): /* aq2 a2 / aq3 / / aq1 / a1 aq3 */ // Filled as two triangles per quad: /* aq2 # # ### ## # aq3 ## ### - ## #### -/ ## ### -/ ## #### -/ aq1 ## -/ --- -/ \---aq4 */ // Make the path thick: path a becomes quad a Point aq1{a1.x - fromPath.x, a1.y - fromPath.y}; Point aq2{a2.x - fromPath.x, a2.y - fromPath.y}; Point aq3{a2.x + fromPath.x, a2.y + fromPath.y}; Point aq4{a1.x + fromPath.x, a1.y + fromPath.y}; fillTriangle(aq1.x, aq1.y, aq2.x, aq2.y, aq3.x, aq3.y, color); fillTriangle(aq1.x, aq1.y, aq3.x, aq3.y, aq4.x, aq4.y, color); // Make the path thick: path b becomes quad b Point bq1{b1.x - fromPath.x, b1.y - fromPath.y}; Point bq2{b2.x - fromPath.x, b2.y - fromPath.y}; Point bq3{b2.x + fromPath.x, b2.y + fromPath.y}; Point bq4{b1.x + fromPath.x, b1.y + fromPath.y}; fillTriangle(bq1.x, bq1.y, bq2.x, bq2.y, bq3.x, bq3.y, color); fillTriangle(bq1.x, bq1.y, bq3.x, bq3.y, bq4.x, bq4.y, color); // Make the path thick: path c becomes quad c Point cq1{c1.x - fromPath.x, c1.y + fromPath.y}; Point cq2{c2.x - fromPath.x, c2.y + fromPath.y}; Point cq3{c2.x + fromPath.x, c2.y - fromPath.y}; Point cq4{c1.x + fromPath.x, c1.y - fromPath.y}; fillTriangle(cq1.x, cq1.y, cq2.x, cq2.y, cq3.x, cq3.y, color); fillTriangle(cq1.x, cq1.y, cq3.x, cq3.y, cq4.x, cq4.y, color); // Radius the intersection of quad b and quad c /* b2 / c1 #### ## ## / \ / \/ \ / /\ \ / / \ \ */ // Don't attempt if logo is tiny if (logoTh > 3) { // The radius for the cap *should* be the same as logoTh, but it's not, due to accumulated rounding // We get better results just re-deriving it int16_t capRad = sqrt(pow(fromPath.x, 2) + pow(fromPath.y, 2)); fillCircle(b2.x, b2.y, capRad, color); } } #endif