From ed211b028e1e89f0fe1df778ae43184d687ec183 Mon Sep 17 00:00:00 2001 From: Mohd Faraz Date: Thu, 30 Mar 2023 07:30:13 +0530 Subject: [PATCH 01/10] feat: Added gradient background tuner Signed-off-by: Mohd Faraz --- README.md | 1 + src/card.php | 12 +++- src/demo/css/style.css | 5 ++ src/demo/index.php | 3 +- src/demo/js/script.js | 121 +++++++++++++++++++++++++++-------- src/properties.php | 15 +++++ tests/expected/test_card.svg | 6 ++ 7 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 src/properties.php diff --git a/README.md b/README.md index a3d827c6..d28efdb1 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ If the `theme` parameter is specified, any color customizations specified will b | `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** | +| `gradientBg` | Background color | **rotation** and two colors **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** | diff --git a/src/card.php b/src/card.php index d1de03df..a2adad11 100644 --- a/src/card.php +++ b/src/card.php @@ -274,6 +274,10 @@ 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 + $bg = $params['gradientBg'] ? 'url(#gradient)' : $theme["background"]; + $gradient = explode(",",$params['gradientBg'] ?? ""); + // total contributions $totalContributions = $numFormatter->format($stats["totalContributions"]); $firstContribution = formatDate($stats["firstContribution"], $dateFormat, $localeCode); @@ -325,6 +329,12 @@ function generateCard(array $stats, array $params = null): string 100% { opacity: 1; } } + + + + + + @@ -336,7 +346,7 @@ function generateCard(array $stats, array $params = null): string - + diff --git a/src/demo/css/style.css b/src/demo/css/style.css index 9c5d6097..039c524f 100644 --- a/src/demo/css/style.css +++ b/src/demo/css/style.css @@ -211,6 +211,11 @@ input:focus:invalid { grid-template-columns: auto 1fr auto; } +.advanced .grid-middle { + display: grid; + grid-template-columns: 30% 35% 35%; +} + .advanced .color-properties label:first-of-type { font-weight: bold; } diff --git a/src/demo/index.php b/src/demo/index.php index db8b666c..7bc15afd 100644 --- a/src/demo/index.php +++ b/src/demo/index.php @@ -1,6 +1,7 @@ diff --git a/src/demo/js/script.js b/src/demo/js/script.js index b2ff793b..4ad2e860 100644 --- a/src/demo/js/script.js +++ b/src/demo/js/script.js @@ -24,7 +24,12 @@ const preview = { // convert parameters to query string const query = Object.keys(params) .filter((key) => params[key] !== this.defaults[key]) - .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .map((key) => { + if (key === 'gradientBg') { + return `${encodeURIComponent(key)}=${encodeURIComponent(params[key][0])},${encodeURIComponent(params[key][1])},${encodeURIComponent(params[key][2])}` + } + return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}` + }) .join("&"); // generate links and markdown const imageURL = `${window.location.origin}?${query}`; @@ -61,18 +66,12 @@ const preview = { */ addProperty(property, value = "#EB5454FF") { const selectElement = document.querySelector("#properties"); + Array.prototype.find.call(selectElement.options, (o) => o.value === property); // if no property passed, get the currently selected property const propertyName = property || selectElement.value; if (!selectElement.disabled) { // disable option in menu Array.prototype.find.call(selectElement.options, (o) => o.value === propertyName).disabled = true; - // select first unselected option - const firstAvailable = Array.prototype.find.call(selectElement.options, (o) => !o.disabled); - if (firstAvailable) { - firstAvailable.selected = true; - } else { - selectElement.disabled = true; - } // label const label = document.createElement("label"); label.innerText = propertyName; @@ -83,13 +82,71 @@ 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 === "gradientBg") { + Array.prototype.find.call(selectElement.options, (o) => o.value === 'background').disabled = true; + + const input = document.createElement("span"); + input.className = "grid-middle"; + input.setAttribute("data-property", propertyName); + + const rotate = document.createElement("input"); + rotate.className = "param"; + rotate.type = "text"; + rotate.id = "rotate"; + rotate.placeholder = "30deg"; + rotate.value = "30deg"; + rotate.pattern = "^-[0-9]+deg|^[0-9]+[deg]+" + + const color1 = document.createElement("input"); + color1.className = "param jscolor"; + color1.id = "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 = "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 = color2.value = value; + // add elements + parent.appendChild(label); + input.appendChild(rotate); + input.appendChild(color1); + input.appendChild(color2); + parent.appendChild(input); + } 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); + } + + if (propertyName === 'background') { + Array.prototype.find.call(selectElement.options, (o) => o.value === 'gradientBg').disabled = true; + } + + // select first unselected option + const firstAvailable = Array.prototype.find.call(selectElement.options, (o) => !o.disabled); + if (firstAvailable) { + firstAvailable.selected = true; + } else { + selectElement.disabled = true; + } // removal button const minus = document.createElement("button"); minus.className = "minus btn"; @@ -97,12 +154,8 @@ const preview = { 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); @@ -127,6 +180,11 @@ const preview = { const option = Array.prototype.find.call(selectElement.options, (o) => o.value === property); selectElement.disabled = false; option.disabled = false; + if (property === 'gradientBg') { + Array.prototype.find.call(selectElement.options, (o) => o.value === 'background').disabled = false; + } else if (property === 'background') { + Array.prototype.find.call(selectElement.options, (o) => o.value === 'gradientBg').disabled = false; + } // update and exit this.update(); }, @@ -151,8 +209,12 @@ const preview = { * @returns {Object} the key-value mapping */ objectFromElements(elements) { + let mCount = 0; return Array.from(elements).reduce((acc, next) => { const obj = { ...acc }; + if (obj.gradientBg !== undefined) { + mCount++; + } else if (mCount >= 3) mCount = 0; let value = next.value; if (value.indexOf("#") >= 0) { // if the value is colour, remove the hash sign @@ -161,8 +223,12 @@ const preview = { // if the value is in hexa and opacity is 1, remove FF value = value.replace(/[Ff]{2}$/, ""); } + } else if (value.indexOf("deg") >= 0) { + value = value.replace(/deg/g, ""); } - obj[next.name] = value; + if (mCount <= 0) + obj[next.name] = []; + obj[next.name].push(value); return obj; }, {}); }, @@ -194,12 +260,15 @@ const preview = { * Remove "FF" from a hex color if opacity is 1 * @param {string} color - the hex color * @param {string} input - the property name, or id of the element to update + * @param {boolean} setColor - if true set the color to the input else update original value */ - checkColor(color, input) { + checkColor(color, input, setColor = false) { + // 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); - } + for (const el of document.querySelectorAll(`[name="${input}"]`)) + if (el.value.length === 9 && color.slice(-2) === "FF") + el.value = setColor ? color.slice(0, -2) : el.value.slice(0, -2); + } }, /** @@ -209,7 +278,7 @@ const preview = { */ pickerChange(picker, input) { // color was changed by picker - check it - this.checkColor(picker.toHEXAString(), input); + this.checkColor(picker.toHEXAString(), input, true); // update preview this.update(); }, diff --git a/src/properties.php b/src/properties.php new file mode 100644 index 00000000..d35029f1 --- /dev/null +++ b/src/properties.php @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/tests/expected/test_card.svg b/tests/expected/test_card.svg index 8429c8be..9bcf8228 100644 --- a/tests/expected/test_card.svg +++ b/tests/expected/test_card.svg @@ -11,6 +11,12 @@ 100% { opacity: 1; } } + + + + + + From 2a263089833c48f70dedb199b11d4d48221b1127 Mon Sep 17 00:00:00 2001 From: Mohd Faraz Date: Thu, 30 Mar 2023 20:04:30 +0530 Subject: [PATCH 02/10] feat: gradientBg: Simplify and added support for multiple colors * background=DEG,COLOR1,COLOR2,....COLORN Signed-off-by: Mohd Faraz --- README.md | 3 +-- src/card.php | 26 +++++++++++++++++-------- src/demo/index.php | 3 +-- src/demo/js/script.js | 37 ++++++++++++------------------------ src/properties.php | 15 --------------- tests/expected/test_card.svg | 7 +------ 6 files changed, 33 insertions(+), 58 deletions(-) delete mode 100644 src/properties.php diff --git a/README.md b/README.md index d28efdb1..170d8eda 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,7 @@ If the `theme` parameter is specified, any color customizations specified will b | `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** | -| `gradientBg` | Background color | **rotation** and two colors **hex code** without `#` or **css color** | +| `background` | Background color | Optional **rotation** and **hex code** without `#` or **css color**, if **rotation** is provided gradient will be formed | | `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** | diff --git a/src/card.php b/src/card.php index a2adad11..4c8e78c8 100644 --- a/src/card.php +++ b/src/card.php @@ -275,8 +275,23 @@ function generateCard(array $stats, array $params = null): string $borderRadius = $params["border_radius"] ?? "4.5"; // Set Background - $bg = $params['gradientBg'] ? 'url(#gradient)' : $theme["background"]; - $gradient = explode(",",$params['gradientBg'] ?? ""); + $colors = explode(',', $params['background'] ?? ""); + $isBgGradient = count($colors) >= 3 ? true : false; + + $bg = $isBgGradient ? 'url(#gradient)' : $theme["background"]; + $gradient = ""; + if ($isBgGradient) { + $gradient = " + "; + $colors = array_slice($colors, 1); + $colorCount = count($colors); + for($index = 0; $index < $colorCount; $index++) { + $offset = ($index * 100) / ($colorCount - 1); + $gradient .= ""; + } + $gradient .= " + "; + } // total contributions $totalContributions = $numFormatter->format($stats["totalContributions"]); @@ -329,12 +344,7 @@ function generateCard(array $stats, array $params = null): string 100% { opacity: 1; } } - - - - - - + {$gradient} diff --git a/src/demo/index.php b/src/demo/index.php index 7bc15afd..db8b666c 100644 --- a/src/demo/index.php +++ b/src/demo/index.php @@ -1,7 +1,6 @@ diff --git a/src/demo/js/script.js b/src/demo/js/script.js index 4ad2e860..9cc13b58 100644 --- a/src/demo/js/script.js +++ b/src/demo/js/script.js @@ -25,7 +25,7 @@ const preview = { const query = Object.keys(params) .filter((key) => params[key] !== this.defaults[key]) .map((key) => { - if (key === 'gradientBg') { + if (key === 'background') { return `${encodeURIComponent(key)}=${encodeURIComponent(params[key][0])},${encodeURIComponent(params[key][1])},${encodeURIComponent(params[key][2])}` } return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}` @@ -66,7 +66,13 @@ const preview = { */ addProperty(property, value = "#EB5454FF") { const selectElement = document.querySelector("#properties"); - Array.prototype.find.call(selectElement.options, (o) => o.value === property); + // select first unselected option + const firstAvailable = Array.prototype.find.call(selectElement.options, (o) => !o.disabled); + if (firstAvailable) { + firstAvailable.selected = true; + } else { + selectElement.disabled = true; + } // if no property passed, get the currently selected property const propertyName = property || selectElement.value; if (!selectElement.disabled) { @@ -84,9 +90,7 @@ const preview = { }; const parent = document.querySelector(".advanced .color-properties"); - if (propertyName === "gradientBg") { - Array.prototype.find.call(selectElement.options, (o) => o.value === 'background').disabled = true; - + if (propertyName === "background") { const input = document.createElement("span"); input.className = "grid-middle"; input.setAttribute("data-property", propertyName); @@ -95,8 +99,8 @@ const preview = { rotate.className = "param"; rotate.type = "text"; rotate.id = "rotate"; - rotate.placeholder = "30deg"; - rotate.value = "30deg"; + rotate.placeholder = "0deg"; + rotate.value = "0deg"; rotate.pattern = "^-[0-9]+deg|^[0-9]+[deg]+" const color1 = document.createElement("input"); @@ -135,18 +139,6 @@ const preview = { parent.appendChild(label); parent.appendChild(input); } - - if (propertyName === 'background') { - Array.prototype.find.call(selectElement.options, (o) => o.value === 'gradientBg').disabled = true; - } - - // select first unselected option - const firstAvailable = Array.prototype.find.call(selectElement.options, (o) => !o.disabled); - if (firstAvailable) { - firstAvailable.selected = true; - } else { - selectElement.disabled = true; - } // removal button const minus = document.createElement("button"); minus.className = "minus btn"; @@ -180,11 +172,6 @@ const preview = { const option = Array.prototype.find.call(selectElement.options, (o) => o.value === property); selectElement.disabled = false; option.disabled = false; - if (property === 'gradientBg') { - Array.prototype.find.call(selectElement.options, (o) => o.value === 'background').disabled = false; - } else if (property === 'background') { - Array.prototype.find.call(selectElement.options, (o) => o.value === 'gradientBg').disabled = false; - } // update and exit this.update(); }, @@ -212,7 +199,7 @@ const preview = { let mCount = 0; return Array.from(elements).reduce((acc, next) => { const obj = { ...acc }; - if (obj.gradientBg !== undefined) { + if (obj.background !== undefined) { mCount++; } else if (mCount >= 3) mCount = 0; let value = next.value; diff --git a/src/properties.php b/src/properties.php deleted file mode 100644 index d35029f1..00000000 --- a/src/properties.php +++ /dev/null @@ -1,15 +0,0 @@ - \ No newline at end of file diff --git a/tests/expected/test_card.svg b/tests/expected/test_card.svg index 9bcf8228..2c4e492e 100644 --- a/tests/expected/test_card.svg +++ b/tests/expected/test_card.svg @@ -11,12 +11,7 @@ 100% { opacity: 1; } } - - - - - - + From 06d90e50c239674782af2e2b1cbaf9d324f38412 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 18:01:07 +0300 Subject: [PATCH 03/10] style: format with prettier --- src/card.php | 6 +++--- src/demo/js/script.js | 45 +++++++++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/card.php b/src/card.php index 4c8e78c8..d44aa7b5 100644 --- a/src/card.php +++ b/src/card.php @@ -275,17 +275,17 @@ function generateCard(array $stats, array $params = null): string $borderRadius = $params["border_radius"] ?? "4.5"; // Set Background - $colors = explode(',', $params['background'] ?? ""); + $colors = explode(",", $params["background"] ?? ""); $isBgGradient = count($colors) >= 3 ? true : false; - $bg = $isBgGradient ? 'url(#gradient)' : $theme["background"]; + $bg = $isBgGradient ? "url(#gradient)" : $theme["background"]; $gradient = ""; if ($isBgGradient) { $gradient = " "; $colors = array_slice($colors, 1); $colorCount = count($colors); - for($index = 0; $index < $colorCount; $index++) { + for ($index = 0; $index < $colorCount; $index++) { $offset = ($index * 100) / ($colorCount - 1); $gradient .= ""; } diff --git a/src/demo/js/script.js b/src/demo/js/script.js index 9cc13b58..0510dee4 100644 --- a/src/demo/js/script.js +++ b/src/demo/js/script.js @@ -25,10 +25,12 @@ const preview = { const query = Object.keys(params) .filter((key) => params[key] !== this.defaults[key]) .map((key) => { - if (key === 'background') { - return `${encodeURIComponent(key)}=${encodeURIComponent(params[key][0])},${encodeURIComponent(params[key][1])},${encodeURIComponent(params[key][2])}` + if (key === "background") { + return `${encodeURIComponent(key)}=${encodeURIComponent(params[key][0])},${encodeURIComponent( + params[key][1] + )},${encodeURIComponent(params[key][2])}`; } - return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}` + return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`; }) .join("&"); // generate links and markdown @@ -88,7 +90,7 @@ const preview = { onChange: `preview.pickerChange(this, '${propertyName}')`, onInput: `preview.pickerChange(this, '${propertyName}')`, }; - + const parent = document.querySelector(".advanced .color-properties"); if (propertyName === "background") { const input = document.createElement("span"); @@ -101,24 +103,30 @@ const preview = { rotate.id = "rotate"; rotate.placeholder = "0deg"; rotate.value = "0deg"; - rotate.pattern = "^-[0-9]+deg|^[0-9]+[deg]+" + rotate.pattern = "^-[0-9]+deg|^[0-9]+[deg]+"; const color1 = document.createElement("input"); color1.className = "param jscolor"; color1.id = "color1"; - color1.setAttribute("data-jscolor", JSON.stringify({ - format: "hexa", - onChange: `preview.pickerChange(this, '${color1.id}')`, - onInput: `preview.pickerChange(this, '${color1.id}')`, - })); + 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 = "color2"; - color2.setAttribute("data-jscolor", JSON.stringify({ - format: "hexa", - onChange: `preview.pickerChange(this, '${color2.id}')`, - onInput: `preview.pickerChange(this, '${color2.id}')`, - })); + 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 = color2.value = value; // add elements @@ -147,7 +155,7 @@ const preview = { minus.innerText = "−"; minus.setAttribute("data-property", propertyName); parent.appendChild(minus); - + // initialise jscolor on element jscolor.install(parent); @@ -213,8 +221,7 @@ const preview = { } else if (value.indexOf("deg") >= 0) { value = value.replace(/deg/g, ""); } - if (mCount <= 0) - obj[next.name] = []; + if (mCount <= 0) obj[next.name] = []; obj[next.name].push(value); return obj; }, {}); @@ -255,7 +262,7 @@ const preview = { for (const el of document.querySelectorAll(`[name="${input}"]`)) if (el.value.length === 9 && color.slice(-2) === "FF") el.value = setColor ? color.slice(0, -2) : el.value.slice(0, -2); - } + } }, /** From b9feb122d91f9da745b7bf2578c7da6396ba30a7 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 18:01:25 +0300 Subject: [PATCH 04/10] docs: Update documentation --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 170d8eda..8c695f97 100644 --- a/README.md +++ b/README.md @@ -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 | Optional **rotation** and **hex code** without `#` or **css color**, if **rotation** is provided gradient will be formed | -| `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 From 86ae88a4a514f1fc724d1e52c75a31c8dbc0e4b4 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 19:16:17 +0300 Subject: [PATCH 05/10] refactor and fix some demo site issues --- src/demo/css/style.css | 13 ++++++ src/demo/js/script.js | 91 ++++++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/demo/css/style.css b/src/demo/css/style.css index 039c524f..5d563dae 100644 --- a/src/demo/css/style.css +++ b/src/demo/css/style.css @@ -216,6 +216,19 @@ input:focus:invalid { 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; } diff --git a/src/demo/js/script.js b/src/demo/js/script.js index 0510dee4..153e018a 100644 --- a/src/demo/js/script.js +++ b/src/demo/js/script.js @@ -24,14 +24,7 @@ const preview = { // convert parameters to query string const query = Object.keys(params) .filter((key) => params[key] !== this.defaults[key]) - .map((key) => { - if (key === "background") { - return `${encodeURIComponent(key)}=${encodeURIComponent(params[key][0])},${encodeURIComponent( - params[key][1] - )},${encodeURIComponent(params[key][2])}`; - } - return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`; - }) + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) .join("&"); // generate links and markdown const imageURL = `${window.location.origin}?${query}`; @@ -68,18 +61,18 @@ const preview = { */ addProperty(property, value = "#EB5454FF") { const selectElement = document.querySelector("#properties"); - // select first unselected option - const firstAvailable = Array.prototype.find.call(selectElement.options, (o) => !o.disabled); - if (firstAvailable) { - firstAvailable.selected = true; - } else { - selectElement.disabled = true; - } // if no property passed, get the currently selected property const propertyName = property || selectElement.value; if (!selectElement.disabled) { // disable option in menu Array.prototype.find.call(selectElement.options, (o) => o.value === propertyName).disabled = true; + // select first unselected option + const firstAvailable = Array.prototype.find.call(selectElement.options, (o) => !o.disabled); + if (firstAvailable) { + firstAvailable.selected = true; + } else { + selectElement.disabled = true; + } // label const label = document.createElement("label"); label.innerText = propertyName; @@ -97,17 +90,25 @@ const preview = { 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 = "text"; + rotate.type = "number"; rotate.id = "rotate"; - rotate.placeholder = "0deg"; - rotate.value = "0deg"; - rotate.pattern = "^-[0-9]+deg|^[0-9]+[deg]+"; + rotate.placeholder = "45"; + rotate.value = "45"; + + 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 = "color1"; + color1.id = "background-color1"; color1.setAttribute( "data-jscolor", JSON.stringify({ @@ -118,7 +119,7 @@ const preview = { ); const color2 = document.createElement("input"); color2.className = "param jscolor"; - color2.id = "color2"; + color2.id = "background-color2"; color2.setAttribute( "data-jscolor", JSON.stringify({ @@ -131,10 +132,15 @@ const preview = { color1.value = color2.value = value; // add elements parent.appendChild(label); - input.appendChild(rotate); + 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"; @@ -146,6 +152,10 @@ const preview = { // 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"); @@ -156,12 +166,6 @@ const preview = { minus.setAttribute("data-property", propertyName); parent.appendChild(minus); - // initialise jscolor on element - jscolor.install(parent); - - // check initial color value - this.checkColor(value, propertyName); - // update and exit this.update(); } @@ -204,12 +208,8 @@ const preview = { * @returns {Object} the key-value mapping */ objectFromElements(elements) { - let mCount = 0; return Array.from(elements).reduce((acc, next) => { const obj = { ...acc }; - if (obj.background !== undefined) { - mCount++; - } else if (mCount >= 3) mCount = 0; let value = next.value; if (value.indexOf("#") >= 0) { // if the value is colour, remove the hash sign @@ -218,11 +218,14 @@ const preview = { // if the value is in hexa and opacity is 1, remove FF value = value.replace(/[Ff]{2}$/, ""); } - } else if (value.indexOf("deg") >= 0) { - value = value.replace(/deg/g, ""); } - if (mCount <= 0) obj[next.name] = []; - obj[next.name].push(value); + // 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; }, {}); }, @@ -236,12 +239,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 @@ -254,14 +260,11 @@ const preview = { * Remove "FF" from a hex color if opacity is 1 * @param {string} color - the hex color * @param {string} input - the property name, or id of the element to update - * @param {boolean} setColor - if true set the color to the input else update original value */ - checkColor(color, input, setColor = false) { + checkColor(color, input) { // if color has hex alpha value -> remove it if (color.length === 9 && color.slice(-2) === "FF") { - for (const el of document.querySelectorAll(`[name="${input}"]`)) - if (el.value.length === 9 && color.slice(-2) === "FF") - el.value = setColor ? color.slice(0, -2) : el.value.slice(0, -2); + document.querySelector(`#${input}`).value = color.slice(0, -2); } }, @@ -272,7 +275,7 @@ const preview = { */ pickerChange(picker, input) { // color was changed by picker - check it - this.checkColor(picker.toHEXAString(), input, true); + this.checkColor(picker.toHEXAString(), input); // update preview this.update(); }, From 559537a298442358c4547335e52394e66bf39618 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 19:18:02 +0300 Subject: [PATCH 06/10] style: formatting fixes --- src/demo/js/script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/demo/js/script.js b/src/demo/js/script.js index 153e018a..4ddff0f7 100644 --- a/src/demo/js/script.js +++ b/src/demo/js/script.js @@ -101,7 +101,7 @@ const preview = { rotate.value = "45"; const degText = document.createElement("span"); - degText.innerText = "\u00B0"; // degree symbol + degText.innerText = "\u00B0"; // degree symbol rotateInputGroup.appendChild(rotate); rotateInputGroup.appendChild(degText); @@ -221,7 +221,7 @@ const preview = { } // if the property already exists, append the value to the existing one if (next.name in obj) { - obj[next.name] = obj[next.name] + "," + value; + obj[next.name] = `${obj[next.name]},${value}`; return obj; } // otherwise, add the value to the object From 1758da4191d973adeffbdd19d548a0373b0bd64f Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 19:49:16 +0300 Subject: [PATCH 07/10] ci: refactor and add unit tests --- src/card.php | 28 ++++++++++++++++------------ tests/RenderTest.php | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/card.php b/src/card.php index d44aa7b5..7ae0214a 100644 --- a/src/card.php +++ b/src/card.php @@ -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; + } } } @@ -275,22 +280,21 @@ function generateCard(array $stats, array $params = null): string $borderRadius = $params["border_radius"] ?? "4.5"; // Set Background - $colors = explode(",", $params["background"] ?? ""); - $isBgGradient = count($colors) >= 3 ? true : false; + $backgroundParts = explode(",", $theme["background"] ?? ""); + $backgroundIsGradient = count($backgroundParts) >= 3; - $bg = $isBgGradient ? "url(#gradient)" : $theme["background"]; + $background = $theme["background"]; $gradient = ""; - if ($isBgGradient) { - $gradient = " - "; - $colors = array_slice($colors, 1); - $colorCount = count($colors); + if ($backgroundIsGradient) { + $background = "url(#gradient)"; + $gradient = ""; + $backgroundColors = array_slice($backgroundParts, 1); + $colorCount = count($backgroundColors); for ($index = 0; $index < $colorCount; $index++) { $offset = ($index * 100) / ($colorCount - 1); - $gradient .= ""; + $gradient .= ""; } - $gradient .= " - "; + $gradient .= ""; } // total contributions @@ -356,7 +360,7 @@ function generateCard(array $stats, array $params = null): string - + diff --git a/tests/RenderTest.php b/tests/RenderTest.php index d77718cd..89af91db 100644 --- a/tests/RenderTest.php +++ b/tests/RenderTest.php @@ -176,4 +176,26 @@ public function testAlphaInHexColors(): void $render = generateOutput($this->testStats, $this->testParams)["body"]; $this->assertStringContainsString("stroke='#00ff00' stroke-opacity='0.50196078431373'", $render); } + + /** + * Test gradient background + */ + public function testGradientBackground(): void + { + $this->testParams["background"] = "45,f00,e11"; + $render = generateOutput($this->testStats, $this->testParams)["body"]; + $this->assertStringContainsString("fill='url(#gradient)'", $render); + $this->assertStringContainsString("", $render); + } + + /** + * Test gradient background with more than 2 colors + */ + public function testGradientBackgroundWithMoreThan2Colors(): void + { + $this->testParams["background"] = "-45,f00,4e5,ddd,fff"; + $render = generateOutput($this->testStats, $this->testParams)["body"]; + $this->assertStringContainsString("fill='url(#gradient)'", $render); + $this->assertStringContainsString("", $render); + } } From bc5d7434818bfc3fbe64e6b5f1bdd9b5f49ef0ea Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 19:52:35 +0300 Subject: [PATCH 08/10] format with prettier --- tests/RenderTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/RenderTest.php b/tests/RenderTest.php index 89af91db..ea338ea0 100644 --- a/tests/RenderTest.php +++ b/tests/RenderTest.php @@ -185,7 +185,10 @@ public function testGradientBackground(): void $this->testParams["background"] = "45,f00,e11"; $render = generateOutput($this->testStats, $this->testParams)["body"]; $this->assertStringContainsString("fill='url(#gradient)'", $render); - $this->assertStringContainsString("", $render); + $this->assertStringContainsString( + "", + $render + ); } /** @@ -196,6 +199,9 @@ public function testGradientBackgroundWithMoreThan2Colors(): void $this->testParams["background"] = "-45,f00,4e5,ddd,fff"; $render = generateOutput($this->testStats, $this->testParams)["body"]; $this->assertStringContainsString("fill='url(#gradient)'", $render); - $this->assertStringContainsString("", $render); + $this->assertStringContainsString( + "", + $render + ); } } From 2c536089c26fbc735fdf5e89e9c45f99c415a47b Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 20:09:35 +0300 Subject: [PATCH 09/10] fix PNG mode with transparent stop colors --- src/card.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/card.php b/src/card.php index 7ae0214a..66b5b2a5 100644 --- a/src/card.php +++ b/src/card.php @@ -571,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 ); From 3328fee8ebe3adf5b462807cd2e9357afd50b2c3 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Thu, 30 Mar 2023 20:30:14 +0300 Subject: [PATCH 10/10] fix filling in fields from permalink URL --- src/demo/js/script.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/demo/js/script.js b/src/demo/js/script.js index 4ddff0f7..752eb579 100644 --- a/src/demo/js/script.js +++ b/src/demo/js/script.js @@ -86,6 +86,16 @@ const preview = { 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); @@ -98,7 +108,7 @@ const preview = { rotate.type = "number"; rotate.id = "rotate"; rotate.placeholder = "45"; - rotate.value = "45"; + rotate.value = angleValue; const degText = document.createElement("span"); degText.innerText = "\u00B0"; // degree symbol @@ -129,7 +139,8 @@ const preview = { }) ); rotate.name = color1.name = color2.name = propertyName; - color1.value = color2.value = value; + color1.value = color1Value; + color2.value = color2Value; // add elements parent.appendChild(label); input.appendChild(rotateInputGroup); @@ -325,7 +336,8 @@ 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 @@ -333,9 +345,18 @@ window.addEventListener( } 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(); },