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 option to set gradient backgrounds #481

Merged
merged 10 commits into from
Apr 1, 2023
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