// SPDX-License-Identifier: GPL-3.0-or-later
// solhint-disable max-line-length,quotes
pragma solidity >=0.8.22;
import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
library SVGElements {
using Strings for string;
using Strings for uint256;
/*//////////////////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////////////////*/
string internal constant BACKGROUND =
'';
string internal constant BACKGROUND_COLOR = "hsl(230,21%,11%)";
string internal constant FLOATING_TEXT =
'';
string internal constant GLOW = '';
string internal constant LOGO =
'';
string internal constant HOURGLASS_BACKGROUND_CIRCLE =
'';
string internal constant HOURGLASS_FILL =
'';
string internal constant HOURGLASS_STROKE =
'';
string internal constant HOURGLASS_LOWER_BULB_LARGE =
'';
string internal constant HOURGLASS_LOWER_BULB_SMALL =
'';
string internal constant HOURGLASS_UPPER_BULB =
'';
string internal constant NOISE =
'';
/// @dev Escape character for "≥".
string internal constant SIGN_GE = "≥";
/// @dev Escape character for ">".
string internal constant SIGN_GT = ">";
/// @dev Escape character for "<".
string internal constant SIGN_LT = "<";
/*//////////////////////////////////////////////////////////////////////////
DATA TYPES
//////////////////////////////////////////////////////////////////////////*/
enum CardType {
PROGRESS,
STATUS,
AMOUNT,
DURATION
}
/*//////////////////////////////////////////////////////////////////////////
COMPONENTS
//////////////////////////////////////////////////////////////////////////*/
function card(CardType cardType, string memory content) internal pure returns (uint256, string memory) {
return card({ cardType: cardType, content: content, circle: "" });
}
function card(
CardType cardType,
string memory content,
string memory circle
)
internal
pure
returns (uint256 width, string memory card_)
{
string memory caption = stringifyCardType(cardType);
// The progress card can have a fixed width because the content is never longer than the caption. The former
// has 6 characters (at most, e.g. "42.09%"), whereas the latter has 8 characters ("Progress").
if (cardType == CardType.PROGRESS) {
// The progress can be 0%, in which case the circle is not rendered.
if (circle.equal("")) {
width = 144; // 2 * 20 (margins) + 8 * 13 (charWidth)
} else {
width = 208; // 3 * 20 (margins) + 8 * 13 (charWidth) + 44 (diameter)
}
}
// For the other cards, the width is calculated dynamically based on the number of characters.
else {
uint256 captionWidth = calculatePixelWidth({ text: caption, largeFont: false });
uint256 contentWidth = calculatePixelWidth({ text: content, largeFont: true });
// Use the greater of the two widths, and add the left and the right margin.
unchecked {
width = Math.max(captionWidth, contentWidth) + 40;
}
}
card_ = string.concat(
'',
'',
'',
caption,
"",
'',
content,
"",
circle,
""
);
}
function floatingText(string memory offset, string memory text) internal pure returns (string memory) {
return string.concat(
'',
'',
text,
""
);
}
function gradients(string memory accentColor) internal pure returns (string memory) {
string memory radialGlow = string.concat(
'',
'',
'',
""
);
string memory sandTop = string.concat(
'',
'',
'',
""
);
string memory sandBottom = string.concat(
'',
'',
'',
'',
""
);
// Needs to be declared last so that the stroke is painted on top of the sand.
string memory hourglassStroke = string.concat(
'',
'',
'',
""
);
return string.concat(radialGlow, sandTop, sandBottom, hourglassStroke);
}
function hourglass(string memory status) internal pure returns (string memory) {
bool settledOrDepleted = status.equal("Settled") || status.equal("Depleted");
return string.concat(
'',
HOURGLASS_BACKGROUND_CIRCLE,
HOURGLASS_FILL,
settledOrDepleted ? "" : HOURGLASS_UPPER_BULB, // empty or filled
settledOrDepleted ? HOURGLASS_LOWER_BULB_LARGE : HOURGLASS_LOWER_BULB_SMALL,
HOURGLASS_STROKE, // needs to be declared last so that the stroke is painted on top of the sand
""
);
}
function progressCircle(
uint256 progressNumerical,
string memory accentColor
)
internal
pure
returns (string memory)
{
if (progressNumerical == 0) {
return "";
}
return string.concat(
'',
'',
'',
""
);
}
/*//////////////////////////////////////////////////////////////////////////
HELPERS
//////////////////////////////////////////////////////////////////////////*/
/// @notice Calculates the pixel width of the provided string.
/// @dev Notes:
/// - A factor of ~0.6 is applied to the two font sizes used in the SVG (26px and 22px) to approximate the average
/// character width.
/// - It is assumed that escaped characters are placed at the beginning of `text`.
/// - It is further assumed that there is no other semicolon in `text`.
function calculatePixelWidth(string memory text, bool largeFont) internal pure returns (uint256 width) {
uint256 length = bytes(text).length;
if (length == 0) {
return 0;
}
unchecked {
uint256 charWidth = largeFont ? 16 : 13;
uint256 semicolonIndex;
for (uint256 i = 0; i < length; ++i) {
if (bytes(text)[i] == ";") {
semicolonIndex = i;
}
width += charWidth;
}
// Account for escaped characters (such as ≥).
width -= charWidth * semicolonIndex;
}
}
/// @notice Retrieves the card type as a string.
function stringifyCardType(CardType cardType) internal pure returns (string memory) {
if (cardType == CardType.PROGRESS) {
return "Progress";
} else if (cardType == CardType.STATUS) {
return "Status";
} else if (cardType == CardType.AMOUNT) {
return "Amount";
} else {
return "Duration";
}
}
}