Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added card_height option #673

Merged
merged 9 commits into from
May 5, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ If the `theme` parameter is specified, any color customizations specified will b
| `exclude_days` | List of days of the week to exclude from streaks | Comma-separated list of day abbreviations (Sun, Mon, Tue, Wed, Thu, Fri, Sat) e.g. `Sun,Sat` |
| `disable_animations` | Disable SVG animations (Default: `false`) | `true` or `false` |
| `card_width` | Width of the card in pixels (Default: `495`) | Positive integer, minimum width is 100px per column |
| `card_height` | Height of the card in pixels (Default: `195`) | Positive integer, minimum height is 170px |
| `hide_total_contributions` | Hide the total contributions (Default: `false`) | `true` or `false` |
| `hide_current_streak` | Hide the current streak (Default: `false`) | `true` or `false` |
| `hide_longest_streak` | Hide the longest streak (Default: `false`) | `true` or `false` |
Expand Down
92 changes: 66 additions & 26 deletions src/card.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,19 @@ function getCardWidth(array $params, int $numColumns = 3): int
return max($minimumWidth, intval($params["card_width"] ?? $defaultWidth));
}

/**
* Get the card height from params taking into account minimum and default values
*
* @param array<string,string> $params Request parameters
* @return int Card width
*/
function getCardHeight(array $params): int
{
$defaultHeight = 195;
$minimumHeight = 170;
return max($minimumHeight, intval($params["card_height"] ?? $defaultHeight));
}

/**
* Generate SVG output for a stats array
*
Expand Down Expand Up @@ -364,7 +377,11 @@ function generateCard(array $stats, array $params = null): string
$rectWidth = $cardWidth - 1;
$columnWidth = $numColumns > 0 ? $cardWidth / $numColumns : 0;

// offsets for the bars between columns
$cardHeight = getCardHeight($params);
$rectHeight = $cardHeight - 1;
$heightOffset = ($cardHeight - 195) / 2;

// X offsets for the bars between columns
$barOffsets = [-999, -999];
for ($i = 0; $i < $numColumns - 1; $i++) {
$barOffsets[$i] = $columnWidth * ($i + 1);
Expand All @@ -384,6 +401,22 @@ function generateCard(array $stats, array $params = null): string
$currentStreakOffset = $showCurrentStreak ? $columnOffsets[$nextColumnIndex++] : -999;
$longestStreakOffset = $showLongestStreak ? $columnOffsets[$nextColumnIndex++] : -999;

// Y offsets for the bars
$barHeightOffsets = [28 + $heightOffset / 2, 170 + $heightOffset];
// Y offsets for the numbers and dates
$longestStreakHeightOffset = $totalContributionsHeightOffset = [
48 + $heightOffset,
84 + $heightOffset,
114 + $heightOffset,
];
$currentStreakHeightOffset = [
48 + $heightOffset,
108 + $heightOffset,
145 + $heightOffset,
71 + $heightOffset,
19.5 + $heightOffset,
];

// total contributions
$totalContributions = $numFormatter->format($stats["totalContributions"]);
$firstContribution = formatDate($stats["firstContribution"], $dateFormat, $localeCode);
Expand Down Expand Up @@ -440,7 +473,7 @@ function generateCard(array $stats, array $params = null): string
}

return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'
style='isolation: isolate' viewBox='0 0 {$cardWidth} 195' width='{$cardWidth}px' height='195px' direction='{$direction}'>
style='isolation: isolate' viewBox='0 0 {$cardWidth} {$cardHeight}' width='{$cardWidth}px' height='{$cardHeight}px' direction='{$direction}'>
<style>
@keyframes currstreak {
0% { font-size: 3px; opacity: 0.2; }
Expand All @@ -454,94 +487,94 @@ function generateCard(array $stats, array $params = null): string
</style>
<defs>
<clipPath id='outer_rectangle'>
<rect width='{$cardWidth}' height='195' rx='{$borderRadius}'/>
<rect width='{$cardWidth}' height='{$cardHeight}' rx='{$borderRadius}'/>
</clipPath>
<mask id='mask_out_ring_behind_fire'>
<rect width='{$cardWidth}' height='195' fill='white'/>
<rect width='{$cardWidth}' height='{$cardHeight}' fill='white'/>
<ellipse id='mask-ellipse' cx='{$currentStreakOffset}' cy='32' rx='13' ry='18' fill='black'/>
</mask>
{$theme["backgroundGradient"]}
</defs>
<g clip-path='url(#outer_rectangle)'>
<g style='isolation: isolate'>
<rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='194'/>
<rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='{$rectHeight}'/>
</g>
<g style='isolation: isolate'>
<line x1='{$barOffsets[0]}' y1='28' x2='{$barOffsets[0]}' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
<line x1='{$barOffsets[1]}' y1='28' x2='{$barOffsets[1]}' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
<line x1='{$barOffsets[0]}' y1='{$barHeightOffsets[0]}' x2='{$barOffsets[0]}' y2='{$barHeightOffsets[1]}' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
<line x1='{$barOffsets[1]}' y1='$barHeightOffsets[0]' x2='{$barOffsets[1]}' y2='$barHeightOffsets[1]' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
</g>
<g style='isolation: isolate'>
<!-- Total Contributions big number -->
<g transform='translate({$totalContributionsOffset},48)'>
<g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[0]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideNums"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
{$totalContributions}
</text>
</g>

<!-- Total Contributions label -->
<g transform='translate({$totalContributionsOffset},84)'>
<g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[1]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideLabels"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.7s'>
{$totalContributionsText}
</text>
</g>

<!-- Total Contributions range -->
<g transform='translate({$totalContributionsOffset},114)'>
<g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[2]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["dates"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.8s'>
{$totalContributionsRange}
</text>
</g>
</g>
<g style='isolation: isolate'>
<!-- Current Streak big number -->
<g transform='translate({$currentStreakOffset},48)'>
<g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[0]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["currStreakNum"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='animation: currstreak 0.6s linear forwards'>
{$currentStreak}
</text>
</g>

<!-- Current Streak label -->
<g transform='translate({$currentStreakOffset},108)'>
<g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[1]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["currStreakLabel"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
{$currentStreakText}
</text>
</g>

<!-- Current Streak range -->
<g transform='translate({$currentStreakOffset},145)'>
<g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[2]})'>
<text x='0' y='21' stroke-width='0' text-anchor='middle' fill='{$theme["dates"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
{$currentStreakRange}
</text>
</g>

<!-- Ring around number -->
<g mask='url(#mask_out_ring_behind_fire)'>
<circle cx='{$currentStreakOffset}' cy='71' r='40' fill='none' stroke='{$theme["ring"]}' stroke-width='5' style='opacity: 0; animation: fadein 0.5s linear forwards 0.4s'></circle>
<circle cx='{$currentStreakOffset}' cy='{$currentStreakHeightOffset[3]}' r='40' fill='none' stroke='{$theme["ring"]}' stroke-width='5' style='opacity: 0; animation: fadein 0.5s linear forwards 0.4s'></circle>
</g>
<!-- Fire icon -->
<g transform='translate({$currentStreakOffset}, 19.5)' stroke-opacity='0' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
<g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[4]})' stroke-opacity='0' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
<path d='M -12 -0.5 L 15 -0.5 L 15 23.5 L -12 23.5 L -12 -0.5 Z' fill='none'/>
<path d='M 1.5 0.67 C 1.5 0.67 2.24 3.32 2.24 5.47 C 2.24 7.53 0.89 9.2 -1.17 9.2 C -3.23 9.2 -4.79 7.53 -4.79 5.47 L -4.76 5.11 C -6.78 7.51 -8 10.62 -8 13.99 C -8 18.41 -4.42 22 0 22 C 4.42 22 8 18.41 8 13.99 C 8 8.6 5.41 3.79 1.5 0.67 Z M -0.29 19 C -2.07 19 -3.51 17.6 -3.51 15.86 C -3.51 14.24 -2.46 13.1 -0.7 12.74 C 1.07 12.38 2.9 11.53 3.92 10.16 C 4.31 11.45 4.51 12.81 4.51 14.2 C 4.51 16.85 2.36 19 -0.29 19 Z' fill='{$theme["fire"]}' stroke-opacity='0'/>
</g>

</g>
<g style='isolation: isolate'>
<!-- Longest Streak big number -->
<g transform='translate({$longestStreakOffset},48)'>
<g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[0]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideNums"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.2s'>
{$longestStreak}
</text>
</g>

<!-- Longest Streak label -->
<g transform='translate({$longestStreakOffset},84)'>
<g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[1]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideLabels"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.3s'>
{$longestStreakText}
</text>
</g>

<!-- Longest Streak range -->
<g transform='translate({$longestStreakOffset},114)'>
<g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[2]})'>
<text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["dates"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.4s'>
{$longestStreakRange}
</text>
Expand Down Expand Up @@ -575,25 +608,31 @@ function generateErrorCard(string $message, array $params = null): string
$rectWidth = $cardWidth - 1;
$centerOffset = $cardWidth / 2;

return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation: isolate' viewBox='0 0 {$cardWidth} 195' width='{$cardWidth}px' height='195px'>
// read card_height parameter
$cardHeight = getCardHeight($params);
$rectHeight = $cardHeight - 1;
$heightOffset = ($cardHeight - 195) / 2;
$errorLabelOffset = $cardHeight / 2 + 10.5;

return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation: isolate' viewBox='0 0 {$cardWidth} {$cardHeight}' width='{$cardWidth}px' height='{$cardHeight}px'>
<style>
a {
fill: {$theme["dates"]};
}
</style>
<defs>
<clipPath id='outer_rectangle'>
<rect width='{$cardWidth}' height='195' rx='{$borderRadius}'/>
<rect width='{$cardWidth}' height='{$cardHeight}' rx='{$borderRadius}'/>
</clipPath>
{$theme["backgroundGradient"]}
</defs>
<g clip-path='url(#outer_rectangle)'>
<g style='isolation: isolate'>
<rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='194'/>
<rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='{$rectHeight}'/>
</g>
<g style='isolation: isolate'>
<!-- Error lable -->
<g transform='translate({$centerOffset},108)'>
<g transform='translate({$centerOffset}, {$errorLabelOffset})'>
<text x='0' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' fill='{$theme["sideLabels"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal'>
{$message}
</text>
Expand All @@ -607,7 +646,7 @@ function generateErrorCard(string $message, array $params = null): string
</mask>
</defs>
<!-- Sad face -->
<g transform='translate({$centerOffset}, 0)'>
<g transform='translate({$centerOffset}, {$heightOffset})'>
<path fill='{$theme["fire"]}' d='M0,35.8c-25.2,0-45.7,20.5-45.7,45.7s20.5,45.8,45.7,45.8s45.7-20.5,45.7-45.7S25.2,35.8,0,35.8z M0,122.3c-11.2,0-21.4-4.5-28.8-11.9c-2.9-2.9-5.4-6.3-7.4-10c-3-5.7-4.6-12.1-4.6-18.9c0-22.5,18.3-40.8,40.8-40.8 c10.7,0,20.4,4.1,27.7,10.9c3.8,3.5,6.9,7.7,9.1,12.4c2.6,5.3,4,11.3,4,17.6C40.8,104.1,22.5,122.3,0,122.3z'/>
<path fill='{$theme["fire"]}' d='M4.8,93.8c5.4,1.1,10.3,4.2,13.7,8.6l3.9-3c-4.1-5.3-10-9-16.6-10.4c-10.6-2.2-21.7,1.9-28.3,10.4l3.9,3 C-13.1,95.3-3.9,91.9,4.8,93.8z'/>
<circle fill='{$theme["fire"]}' cx='-15' cy='71' r='4.9'/>
Expand Down Expand Up @@ -704,7 +743,7 @@ function ($matches) {
* @param int $cardWidth The width of the card
* @return string The generated PNG data
*/
function convertSvgToPng(string $svg, int $cardWidth): string
function convertSvgToPng(string $svg, int $cardWidth, int $cardHeight): string
{
// trim off all whitespaces to make it a valid SVG string
$svg = trim($svg);
Expand All @@ -722,7 +761,7 @@ function convertSvgToPng(string $svg, int $cardWidth): string
// `--export-filename -`: write output to stdout
// `-w 495 -h 195`: set width and height of the output image
// `--export-type png`: set the output format to PNG
$cmd = "echo {$svg} | inkscape --pipe --export-filename - -w {$cardWidth} -h 195 --export-type png";
$cmd = "echo {$svg} | inkscape --pipe --export-filename - -w {$cardWidth} -h {$cardHeight} --export-type png";

// convert svg to png
$png = shell_exec($cmd); // skipcq: PHP-A1009
Expand Down Expand Up @@ -772,7 +811,8 @@ function generateOutput(string|array $output, array $params = null): array
try {
// extract width from SVG
$cardWidth = (int) preg_replace("/.*width=[\"'](\d+)px[\"'].*/", "$1", $svg);
$png = convertSvgToPng($svg, $cardWidth);
$cardHeight = (int) preg_replace("/.*height=[\"'](\d+)px[\"'].*/", "$1", $svg);
$png = convertSvgToPng($svg, $cardWidth, $cardHeight);
return [
"contentType" => "image/png",
"body" => $png,
Expand Down
5 changes: 4 additions & 1 deletion src/demo/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ function gtag() {
<label for="card-width">Card Width</label>
<input class="param" type="number" id="card-width" name="card_width" placeholder="495" value="495" step="1" min="300" />

<label for="card-width">Card Height</label>
<input class="param" type="number" id="card-width" name="card_height" placeholder="195" value="195" step="1" min="170" />

<label for="type">Output Type</label>
<select class="param" id="type" name="type">
<option value="svg">SVG</option>
Expand Down Expand Up @@ -276,4 +279,4 @@ function gtag() {
</a>
</body>

</html>
</html>
1 change: 1 addition & 0 deletions src/demo/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const preview = {
type: "svg",
exclude_days: "",
card_width: "495",
card_height: "195",
hide_total_contributions: "false",
hide_current_streak: "false",
hide_longest_streak: "false",
Expand Down
Loading
Loading