Skip to content

Commit

Permalink
feat: Added option to set gradient backgrounds (#481)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonah Lawrence <[email protected]>
  • Loading branch information
AndroiableDroid and DenverCoder1 authored Apr 1, 2023
1 parent 4efffe6 commit e1f4836
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 47 deletions.
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,27 @@ The `user` field is the only required option. All other fields are optional.

If the `theme` parameter is specified, any color customizations specified will be applied on top of the theme, overriding the theme's values.

| Parameter | Details | Example |
| :------------------: | :---------------------------------------------: | :-----------------------------------------------------------------------: |
| `user` | GitHub username to show stats for | `DenverCoder1` |
| `theme` | The theme to apply (Default: `default`) | `dark`, `radical`, etc. [🎨➜](./docs/themes.md) |
| `hide_border` | Make the border transparent (Default: `false`) | `true` or `false` |
| `border_radius` | Set the roundness of the edges (Default: `4.5`) | Number `0` (sharp corners) to `248` (ellipse) |
| `background` | Background color | **hex code** without `#` or **css color** |
| `border` | Border color | **hex code** without `#` or **css color** |
| `stroke` | Stroke line color between sections | **hex code** without `#` or **css color** |
| `ring` | Color of the ring around the current streak | **hex code** without `#` or **css color** |
| `fire` | Color of the fire in the ring | **hex code** without `#` or **css color** |
| `currStreakNum` | Current streak number | **hex code** without `#` or **css color** |
| `sideNums` | Total and longest streak numbers | **hex code** without `#` or **css color** |
| `currStreakLabel` | Current streak label | **hex code** without `#` or **css color** |
| `sideLabels` | Total and longest streak labels | **hex code** without `#` or **css color** |
| `dates` | Date range text color | **hex code** without `#` or **css color** |
| `date_format` | Date format pattern or empty for locale format | See note below on [📅 Date Formats](#-date-formats) |
| `locale` | Locale for labels and numbers (Default: `en`) | ISO 639-1 code - See [🗪 Locales](#-locales) |
| `type` | Output format (Default: `svg`) | Current options: `svg`, `png` or `json` |
| `mode` | Streak mode (Default: `daily`) | `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week) |
| `disable_animations` | Disable SVG animations (Default: `false`) | `true` or `false` |
| Parameter | Details | Example |
| :------------------: | :---------------------------------------------: | :------------------------------------------------------------------------------------------------: |
| `user` | GitHub username to show stats for | `DenverCoder1` |
| `theme` | The theme to apply (Default: `default`) | `dark`, `radical`, etc. [🎨➜](./docs/themes.md) |
| `hide_border` | Make the border transparent (Default: `false`) | `true` or `false` |
| `border_radius` | Set the roundness of the edges (Default: `4.5`) | Number `0` (sharp corners) to `248` (ellipse) |
| `background` | Background color (eg. `f2f2f2`, `35,d22,00f`) | **hex code** without `#`, **css color**, or gradient in the form `angle,start_color,...,end_color` |
| `border` | Border color | **hex code** without `#` or **css color** |
| `stroke` | Stroke line color between sections | **hex code** without `#` or **css color** |
| `ring` | Color of the ring around the current streak | **hex code** without `#` or **css color** |
| `fire` | Color of the fire in the ring | **hex code** without `#` or **css color** |
| `currStreakNum` | Current streak number | **hex code** without `#` or **css color** |
| `sideNums` | Total and longest streak numbers | **hex code** without `#` or **css color** |
| `currStreakLabel` | Current streak label | **hex code** without `#` or **css color** |
| `sideLabels` | Total and longest streak labels | **hex code** without `#` or **css color** |
| `dates` | Date range text color | **hex code** without `#` or **css color** |
| `date_format` | Date format pattern or empty for locale format | See note below on [📅 Date Formats](#-date-formats) |
| `locale` | Locale for labels and numbers (Default: `en`) | ISO 639-1 code - See [🗪 Locales](#-locales) |
| `type` | Output format (Default: `svg`) | Current options: `svg`, `png` or `json` |
| `mode` | Streak mode (Default: `daily`) | `daily` (contribute daily) or `weekly` (contribute once per Sun-Sat week) |
| `disable_animations` | Disable SVG animations (Default: `false`) | `true` or `false` |

### 🖌 Themes

Expand Down
31 changes: 28 additions & 3 deletions src/card.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ function getRequestedTheme(array $params): array
// set property
$theme[$prop] = $param;
}
// if the property is background gradient is allowed (angle,start_color,...,end_color)
elseif ($prop == "background" && preg_match("/^-?[0-9]+,[a-f0-9]{3,8}(,[a-f0-9]{3,8})+$/", $param)) {
// set property
$theme[$prop] = $param;
}
}
}

Expand Down Expand Up @@ -274,6 +279,24 @@ function generateCard(array $stats, array $params = null): string
// read border_radius parameter, default to 4.5 if not set
$borderRadius = $params["border_radius"] ?? "4.5";

// Set Background
$backgroundParts = explode(",", $theme["background"] ?? "");
$backgroundIsGradient = count($backgroundParts) >= 3;

$background = $theme["background"];
$gradient = "";
if ($backgroundIsGradient) {
$background = "url(#gradient)";
$gradient = "<defs><linearGradient id='gradient' gradientTransform='rotate({$backgroundParts[0]})' gradientUnits='userSpaceOnUse'>";
$backgroundColors = array_slice($backgroundParts, 1);
$colorCount = count($backgroundColors);
for ($index = 0; $index < $colorCount; $index++) {
$offset = ($index * 100) / ($colorCount - 1);
$gradient .= "<stop offset='{$offset}%' stop-color='#{$backgroundColors[$index]}' />";
}
$gradient .= "</linearGradient></defs>";
}

// total contributions
$totalContributions = $numFormatter->format($stats["totalContributions"]);
$firstContribution = formatDate($stats["firstContribution"], $dateFormat, $localeCode);
Expand Down Expand Up @@ -325,6 +348,7 @@ function generateCard(array $stats, array $params = null): string
100% { opacity: 1; }
}
</style>
{$gradient}
<defs>
<clipPath id='outer_rectangle'>
<rect width='495' height='195' rx='{$borderRadius}'/>
Expand All @@ -336,7 +360,7 @@ function generateCard(array $stats, array $params = null): string
</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='494' height='194'/>
<rect stroke='{$theme["border"]}' fill='{$background}' rx='{$borderRadius}' x='0.5' y='0.5' width='494' height='194'/>
</g>
<g style='isolation: isolate'>
<line x1='330' y1='28' x2='330' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
Expand Down Expand Up @@ -547,13 +571,14 @@ function convertHexColors(string $svg): string

// convert hex colors to 6 digits and corresponding opacity attribute
$svg = preg_replace_callback(
"/(fill|stroke)=['\"]#([0-9a-fA-F]{4}|[0-9a-fA-F]{8})['\"]/m",
"/(fill|stroke|stop-color)=['\"]#([0-9a-fA-F]{4}|[0-9a-fA-F]{8})['\"]/m",
function ($matches) {
$attribute = $matches[1];
$opacityAttribute = $attribute === "stop-color" ? "stop-opacity" : "{$attribute}-opacity";
$result = convertHexColor($matches[2]);
$color = $result["color"];
$opacity = $result["opacity"];
return "{$attribute}='{$color}' {$attribute}-opacity='{$opacity}'";
return "{$attribute}='{$color}' {$opacityAttribute}='{$opacity}'";
},
$svg
);
Expand Down
18 changes: 18 additions & 0 deletions src/demo/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,24 @@ input:focus:invalid {
grid-template-columns: auto 1fr auto;
}

.advanced .grid-middle {
display: grid;
grid-template-columns: 30% 35% 35%;
}

.input-text-group {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.25em;
}

.input-text-group span {
font-size: 0.8em;
font-weight: bold;
padding-right: 1.5em;
}

.advanced .color-properties label:first-of-type {
font-weight: bold;
}
Expand Down
133 changes: 110 additions & 23 deletions src/demo/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,100 @@ const preview = {
onChange: `preview.pickerChange(this, '${propertyName}')`,
onInput: `preview.pickerChange(this, '${propertyName}')`,
};
const input = document.createElement("input");
input.className = "param jscolor";
input.id = propertyName;
input.name = propertyName;
input.setAttribute("data-property", propertyName);
input.setAttribute("data-jscolor", JSON.stringify(jscolorConfig));
input.value = value;

const parent = document.querySelector(".advanced .color-properties");
if (propertyName === "background") {
const valueParts = value.split(",");
let angleValue = "45";
let color1Value = "#EB5454FF";
let color2Value = "#EB5454FF";
if (valueParts.length === 3) {
angleValue = valueParts[0];
color1Value = valueParts[1];
color2Value = valueParts[2];
}

const input = document.createElement("span");
input.className = "grid-middle";
input.setAttribute("data-property", propertyName);

const rotateInputGroup = document.createElement("div");
rotateInputGroup.className = "input-text-group";

const rotate = document.createElement("input");
rotate.className = "param";
rotate.type = "number";
rotate.id = "rotate";
rotate.placeholder = "45";
rotate.value = angleValue;

const degText = document.createElement("span");
degText.innerText = "\u00B0"; // degree symbol

rotateInputGroup.appendChild(rotate);
rotateInputGroup.appendChild(degText);

const color1 = document.createElement("input");
color1.className = "param jscolor";
color1.id = "background-color1";
color1.setAttribute(
"data-jscolor",
JSON.stringify({
format: "hexa",
onChange: `preview.pickerChange(this, '${color1.id}')`,
onInput: `preview.pickerChange(this, '${color1.id}')`,
})
);
const color2 = document.createElement("input");
color2.className = "param jscolor";
color2.id = "background-color2";
color2.setAttribute(
"data-jscolor",
JSON.stringify({
format: "hexa",
onChange: `preview.pickerChange(this, '${color2.id}')`,
onInput: `preview.pickerChange(this, '${color2.id}')`,
})
);
rotate.name = color1.name = color2.name = propertyName;
color1.value = color1Value;
color2.value = color2Value;
// add elements
parent.appendChild(label);
input.appendChild(rotateInputGroup);
input.appendChild(color1);
input.appendChild(color2);
parent.appendChild(input);
// initialise jscolor on elements
jscolor.install(input);
// check initial color values
this.checkColor(color1.value, color1.id);
this.checkColor(color2.value, color2.id);
} else {
const input = document.createElement("input");
input.className = "param jscolor";
input.id = propertyName;
input.name = propertyName;
input.setAttribute("data-property", propertyName);
input.setAttribute("data-jscolor", JSON.stringify(jscolorConfig));
input.value = value;
// add elements
parent.appendChild(label);
parent.appendChild(input);
// initialise jscolor on element
jscolor.install(parent);
// check initial color value
this.checkColor(value, propertyName);
}
// removal button
const minus = document.createElement("button");
minus.className = "minus btn";
minus.setAttribute("onclick", "return preview.removeProperty(this.getAttribute('data-property'));");
minus.setAttribute("type", "button");
minus.innerText = "−";
minus.setAttribute("data-property", propertyName);
// add elements
const parent = document.querySelector(".advanced .color-properties");
parent.appendChild(label);
parent.appendChild(input);
parent.appendChild(minus);

// initialise jscolor on element
jscolor.install(parent);

// check initial color value
this.checkColor(value, propertyName);

// update and exit
this.update();
}
Expand Down Expand Up @@ -162,6 +230,12 @@ const preview = {
value = value.replace(/[Ff]{2}$/, "");
}
}
// if the property already exists, append the value to the existing one
if (next.name in obj) {
obj[next.name] = `${obj[next.name]},${value}`;
return obj;
}
// otherwise, add the value to the object
obj[next.name] = value;
return obj;
}, {});
Expand All @@ -176,12 +250,15 @@ const preview = {
const selectedOption = themeSelect.options[themeSelect.selectedIndex];
const defaultParams = selectedOption.dataset;
// get parameters with the advanced options
const advancedParams = this.objectFromElements(document.querySelectorAll(".advanced .param.jscolor"));
const advancedParams = this.objectFromElements(document.querySelectorAll(".advanced .param"));
// update default values with the advanced options
const params = { ...defaultParams, ...advancedParams };
// convert parameters to PHP code
const mappings = Object.keys(params)
.map((key) => ` "${key}" => "#${params[key]}",`)
.map((key) => {
const value = params[key].includes(",") ? params[key] : `#${params[key]}`;
return ` "${key}" => "${value}",`;
})
.join("\n");
const output = `[\n${mappings}\n]`;
// set the textarea value to the output
Expand All @@ -196,9 +273,9 @@ const preview = {
* @param {string} input - the property name, or id of the element to update
*/
checkColor(color, input) {
// if color has hex alpha value -> remove it
if (color.length === 9 && color.slice(-2) === "FF") {
// if color has hex alpha value -> remove it
document.querySelector(`[name="${input}"]`).value = color.slice(0, -2);
document.querySelector(`#${input}`).value = color.slice(0, -2);
}
},

Expand Down Expand Up @@ -259,17 +336,27 @@ window.addEventListener(
element.addEventListener("change", refresh, false);
});
// set input boxes to match URL parameters
new URLSearchParams(window.location.search).forEach((val, key) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.forEach((val, key) => {
const paramInput = document.querySelector(`[name="${key}"]`);
if (paramInput) {
// set parameter value
paramInput.value = val;
} else {
// add advanced property
document.querySelector("details.advanced").open = true;
preview.addProperty(key, val);
preview.addProperty(key, searchParams.getAll(key).join(","));
}
});
// set background angle and colors
const backgroundParams = searchParams.getAll("background");
if (backgroundParams.length > 0) {
document.querySelector("#rotate").value = backgroundParams[0];
document.querySelector("#background-color1").value = backgroundParams[1];
document.querySelector("#background-color2").value = backgroundParams[2];
preview.checkColor(backgroundParams[1], "background-color1");
preview.checkColor(backgroundParams[2], "background-color2");
}
// update previews
preview.update();
},
Expand Down
Loading

0 comments on commit e1f4836

Please sign in to comment.