Skip to content

Commit e1f4836

Browse files
feat: Added option to set gradient backgrounds (#481)
Co-authored-by: Jonah Lawrence <[email protected]>
1 parent 4efffe6 commit e1f4836

File tree

6 files changed

+206
-47
lines changed

6 files changed

+206
-47
lines changed

README.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,27 @@ The `user` field is the only required option. All other fields are optional.
4343

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

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

6868
### 🖌 Themes
6969

src/card.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ function getRequestedTheme(array $params): array
107107
// set property
108108
$theme[$prop] = $param;
109109
}
110+
// if the property is background gradient is allowed (angle,start_color,...,end_color)
111+
elseif ($prop == "background" && preg_match("/^-?[0-9]+,[a-f0-9]{3,8}(,[a-f0-9]{3,8})+$/", $param)) {
112+
// set property
113+
$theme[$prop] = $param;
114+
}
110115
}
111116
}
112117

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

282+
// Set Background
283+
$backgroundParts = explode(",", $theme["background"] ?? "");
284+
$backgroundIsGradient = count($backgroundParts) >= 3;
285+
286+
$background = $theme["background"];
287+
$gradient = "";
288+
if ($backgroundIsGradient) {
289+
$background = "url(#gradient)";
290+
$gradient = "<defs><linearGradient id='gradient' gradientTransform='rotate({$backgroundParts[0]})' gradientUnits='userSpaceOnUse'>";
291+
$backgroundColors = array_slice($backgroundParts, 1);
292+
$colorCount = count($backgroundColors);
293+
for ($index = 0; $index < $colorCount; $index++) {
294+
$offset = ($index * 100) / ($colorCount - 1);
295+
$gradient .= "<stop offset='{$offset}%' stop-color='#{$backgroundColors[$index]}' />";
296+
}
297+
$gradient .= "</linearGradient></defs>";
298+
}
299+
277300
// total contributions
278301
$totalContributions = $numFormatter->format($stats["totalContributions"]);
279302
$firstContribution = formatDate($stats["firstContribution"], $dateFormat, $localeCode);
@@ -325,6 +348,7 @@ function generateCard(array $stats, array $params = null): string
325348
100% { opacity: 1; }
326349
}
327350
</style>
351+
{$gradient}
328352
<defs>
329353
<clipPath id='outer_rectangle'>
330354
<rect width='495' height='195' rx='{$borderRadius}'/>
@@ -336,7 +360,7 @@ function generateCard(array $stats, array $params = null): string
336360
</defs>
337361
<g clip-path='url(#outer_rectangle)'>
338362
<g style='isolation: isolate'>
339-
<rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='494' height='194'/>
363+
<rect stroke='{$theme["border"]}' fill='{$background}' rx='{$borderRadius}' x='0.5' y='0.5' width='494' height='194'/>
340364
</g>
341365
<g style='isolation: isolate'>
342366
<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'/>
@@ -547,13 +571,14 @@ function convertHexColors(string $svg): string
547571

548572
// convert hex colors to 6 digits and corresponding opacity attribute
549573
$svg = preg_replace_callback(
550-
"/(fill|stroke)=['\"]#([0-9a-fA-F]{4}|[0-9a-fA-F]{8})['\"]/m",
574+
"/(fill|stroke|stop-color)=['\"]#([0-9a-fA-F]{4}|[0-9a-fA-F]{8})['\"]/m",
551575
function ($matches) {
552576
$attribute = $matches[1];
577+
$opacityAttribute = $attribute === "stop-color" ? "stop-opacity" : "{$attribute}-opacity";
553578
$result = convertHexColor($matches[2]);
554579
$color = $result["color"];
555580
$opacity = $result["opacity"];
556-
return "{$attribute}='{$color}' {$attribute}-opacity='{$opacity}'";
581+
return "{$attribute}='{$color}' {$opacityAttribute}='{$opacity}'";
557582
},
558583
$svg
559584
);

src/demo/css/style.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,24 @@ input:focus:invalid {
211211
grid-template-columns: auto 1fr auto;
212212
}
213213

214+
.advanced .grid-middle {
215+
display: grid;
216+
grid-template-columns: 30% 35% 35%;
217+
}
218+
219+
.input-text-group {
220+
display: flex;
221+
align-items: center;
222+
justify-content: space-between;
223+
gap: 0.25em;
224+
}
225+
226+
.input-text-group span {
227+
font-size: 0.8em;
228+
font-weight: bold;
229+
padding-right: 1.5em;
230+
}
231+
214232
.advanced .color-properties label:first-of-type {
215233
font-weight: bold;
216234
}

src/demo/js/script.js

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -83,32 +83,100 @@ const preview = {
8383
onChange: `preview.pickerChange(this, '${propertyName}')`,
8484
onInput: `preview.pickerChange(this, '${propertyName}')`,
8585
};
86-
const input = document.createElement("input");
87-
input.className = "param jscolor";
88-
input.id = propertyName;
89-
input.name = propertyName;
90-
input.setAttribute("data-property", propertyName);
91-
input.setAttribute("data-jscolor", JSON.stringify(jscolorConfig));
92-
input.value = value;
86+
87+
const parent = document.querySelector(".advanced .color-properties");
88+
if (propertyName === "background") {
89+
const valueParts = value.split(",");
90+
let angleValue = "45";
91+
let color1Value = "#EB5454FF";
92+
let color2Value = "#EB5454FF";
93+
if (valueParts.length === 3) {
94+
angleValue = valueParts[0];
95+
color1Value = valueParts[1];
96+
color2Value = valueParts[2];
97+
}
98+
99+
const input = document.createElement("span");
100+
input.className = "grid-middle";
101+
input.setAttribute("data-property", propertyName);
102+
103+
const rotateInputGroup = document.createElement("div");
104+
rotateInputGroup.className = "input-text-group";
105+
106+
const rotate = document.createElement("input");
107+
rotate.className = "param";
108+
rotate.type = "number";
109+
rotate.id = "rotate";
110+
rotate.placeholder = "45";
111+
rotate.value = angleValue;
112+
113+
const degText = document.createElement("span");
114+
degText.innerText = "\u00B0"; // degree symbol
115+
116+
rotateInputGroup.appendChild(rotate);
117+
rotateInputGroup.appendChild(degText);
118+
119+
const color1 = document.createElement("input");
120+
color1.className = "param jscolor";
121+
color1.id = "background-color1";
122+
color1.setAttribute(
123+
"data-jscolor",
124+
JSON.stringify({
125+
format: "hexa",
126+
onChange: `preview.pickerChange(this, '${color1.id}')`,
127+
onInput: `preview.pickerChange(this, '${color1.id}')`,
128+
})
129+
);
130+
const color2 = document.createElement("input");
131+
color2.className = "param jscolor";
132+
color2.id = "background-color2";
133+
color2.setAttribute(
134+
"data-jscolor",
135+
JSON.stringify({
136+
format: "hexa",
137+
onChange: `preview.pickerChange(this, '${color2.id}')`,
138+
onInput: `preview.pickerChange(this, '${color2.id}')`,
139+
})
140+
);
141+
rotate.name = color1.name = color2.name = propertyName;
142+
color1.value = color1Value;
143+
color2.value = color2Value;
144+
// add elements
145+
parent.appendChild(label);
146+
input.appendChild(rotateInputGroup);
147+
input.appendChild(color1);
148+
input.appendChild(color2);
149+
parent.appendChild(input);
150+
// initialise jscolor on elements
151+
jscolor.install(input);
152+
// check initial color values
153+
this.checkColor(color1.value, color1.id);
154+
this.checkColor(color2.value, color2.id);
155+
} else {
156+
const input = document.createElement("input");
157+
input.className = "param jscolor";
158+
input.id = propertyName;
159+
input.name = propertyName;
160+
input.setAttribute("data-property", propertyName);
161+
input.setAttribute("data-jscolor", JSON.stringify(jscolorConfig));
162+
input.value = value;
163+
// add elements
164+
parent.appendChild(label);
165+
parent.appendChild(input);
166+
// initialise jscolor on element
167+
jscolor.install(parent);
168+
// check initial color value
169+
this.checkColor(value, propertyName);
170+
}
93171
// removal button
94172
const minus = document.createElement("button");
95173
minus.className = "minus btn";
96174
minus.setAttribute("onclick", "return preview.removeProperty(this.getAttribute('data-property'));");
97175
minus.setAttribute("type", "button");
98176
minus.innerText = "−";
99177
minus.setAttribute("data-property", propertyName);
100-
// add elements
101-
const parent = document.querySelector(".advanced .color-properties");
102-
parent.appendChild(label);
103-
parent.appendChild(input);
104178
parent.appendChild(minus);
105179

106-
// initialise jscolor on element
107-
jscolor.install(parent);
108-
109-
// check initial color value
110-
this.checkColor(value, propertyName);
111-
112180
// update and exit
113181
this.update();
114182
}
@@ -162,6 +230,12 @@ const preview = {
162230
value = value.replace(/[Ff]{2}$/, "");
163231
}
164232
}
233+
// if the property already exists, append the value to the existing one
234+
if (next.name in obj) {
235+
obj[next.name] = `${obj[next.name]},${value}`;
236+
return obj;
237+
}
238+
// otherwise, add the value to the object
165239
obj[next.name] = value;
166240
return obj;
167241
}, {});
@@ -176,12 +250,15 @@ const preview = {
176250
const selectedOption = themeSelect.options[themeSelect.selectedIndex];
177251
const defaultParams = selectedOption.dataset;
178252
// get parameters with the advanced options
179-
const advancedParams = this.objectFromElements(document.querySelectorAll(".advanced .param.jscolor"));
253+
const advancedParams = this.objectFromElements(document.querySelectorAll(".advanced .param"));
180254
// update default values with the advanced options
181255
const params = { ...defaultParams, ...advancedParams };
182256
// convert parameters to PHP code
183257
const mappings = Object.keys(params)
184-
.map((key) => ` "${key}" => "#${params[key]}",`)
258+
.map((key) => {
259+
const value = params[key].includes(",") ? params[key] : `#${params[key]}`;
260+
return ` "${key}" => "${value}",`;
261+
})
185262
.join("\n");
186263
const output = `[\n${mappings}\n]`;
187264
// set the textarea value to the output
@@ -196,9 +273,9 @@ const preview = {
196273
* @param {string} input - the property name, or id of the element to update
197274
*/
198275
checkColor(color, input) {
276+
// if color has hex alpha value -> remove it
199277
if (color.length === 9 && color.slice(-2) === "FF") {
200-
// if color has hex alpha value -> remove it
201-
document.querySelector(`[name="${input}"]`).value = color.slice(0, -2);
278+
document.querySelector(`#${input}`).value = color.slice(0, -2);
202279
}
203280
},
204281

@@ -259,17 +336,27 @@ window.addEventListener(
259336
element.addEventListener("change", refresh, false);
260337
});
261338
// set input boxes to match URL parameters
262-
new URLSearchParams(window.location.search).forEach((val, key) => {
339+
const searchParams = new URLSearchParams(window.location.search);
340+
searchParams.forEach((val, key) => {
263341
const paramInput = document.querySelector(`[name="${key}"]`);
264342
if (paramInput) {
265343
// set parameter value
266344
paramInput.value = val;
267345
} else {
268346
// add advanced property
269347
document.querySelector("details.advanced").open = true;
270-
preview.addProperty(key, val);
348+
preview.addProperty(key, searchParams.getAll(key).join(","));
271349
}
272350
});
351+
// set background angle and colors
352+
const backgroundParams = searchParams.getAll("background");
353+
if (backgroundParams.length > 0) {
354+
document.querySelector("#rotate").value = backgroundParams[0];
355+
document.querySelector("#background-color1").value = backgroundParams[1];
356+
document.querySelector("#background-color2").value = backgroundParams[2];
357+
preview.checkColor(backgroundParams[1], "background-color1");
358+
preview.checkColor(backgroundParams[2], "background-color2");
359+
}
273360
// update previews
274361
preview.update();
275362
},

0 commit comments

Comments
 (0)