From 5b2a39216660548ccb34cd69cc584751a9e08508 Mon Sep 17 00:00:00 2001 From: Iago Martins Date: Fri, 19 Sep 2025 10:24:12 -0300 Subject: [PATCH 1/5] Added 3D layout for language stats. --- api/top-langs.js | 4 +- src/cards/top-languages.js | 228 +++++++++++++++++++++++++++++++++++++ src/cards/types.d.ts | 2 +- vercel.json | 2 +- 4 files changed, 233 insertions(+), 3 deletions(-) diff --git a/api/top-langs.js b/api/top-langs.js index 10ecc5d9f997c..bac172dab8192 100644 --- a/api/top-langs.js +++ b/api/top-langs.js @@ -79,7 +79,9 @@ export default async (req, res) => { if ( layout !== undefined && (typeof layout !== "string" || - !["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout)) + !["compact", "normal", "donut", "donut-vertical", "pie", "3d"].includes( + layout, + )) ) { return res.send( renderError("Something went wrong", "Incorrect layout input"), diff --git a/src/cards/top-languages.js b/src/cards/top-languages.js index fec72c7c391c8..a75487e82fe8e 100644 --- a/src/cards/top-languages.js +++ b/src/cards/top-languages.js @@ -154,6 +154,16 @@ const calculatePieLayoutHeight = (totalLangs) => { return 300 + Math.round(totalLangs / 2) * 25; }; +/** + * Calculates height for the 3D layout. + * + * @param {number} totalLangs Total number of languages. + * @returns {number} Card height. + */ +const calculate3DLayoutHeight = (totalLangs) => { + return 200 + Math.max(totalLangs - 3, 0) * 30; +}; + /** * Calculates the center translation needed to keep the donut chart centred. * @param {number} totalLangs Total number of languages. @@ -723,6 +733,218 @@ const renderDonutLayout = (langs, width, totalLanguageSize, statsFormat) => { `; }; +/** + * Creates a 3D bar for a programming language. + * + * @param {object} props Function properties. + * @param {Lang} props.lang Programming language object. + * @param {number} props.totalSize Total size of all languages. + * @param {number} props.index Index of the programming language. + * @param {number} props.maxHeight Maximum height for bars. + * @param {number} props.barWidth Width of each bar. + * @param {number} props.x X position of the bar. + * @param {string} props.statsFormat Stats format. + * @returns {string} 3D bar SVG node. + */ +const create3DBar = ({ + lang, + totalSize, + index, + maxHeight, + barWidth, + x, + statsFormat, +}) => { + const percentage = (lang.size / totalSize) * 100; + const displayValue = getDisplayValue(lang.size, percentage, statsFormat); + const barHeight = (percentage / 100) * maxHeight; + const color = lang.color || DEFAULT_LANG_COLOR; + + // 3D effect parameters + const depth = 15; // Depth of the 3D effect + const staggerDelay = (index + 3) * 150; + + // Calculate 3D points for isometric projection + const frontTopLeft = { x: x, y: 150 - barHeight }; + const frontTopRight = { x: x + barWidth, y: 150 - barHeight }; + const frontBottomLeft = { x: x, y: 150 }; + const frontBottomRight = { x: x + barWidth, y: 150 }; + + // Back face (offset by depth) + const backTopLeft = { x: x + depth, y: 150 - barHeight - depth }; + const backTopRight = { x: x + barWidth + depth, y: 150 - barHeight - depth }; + const backBottomLeft = { x: x + depth, y: 150 - depth }; + const backBottomRight = { x: x + barWidth + depth, y: 150 - depth }; + + /** + * Lightens a color by a percentage. + * + * @param {string} color Hex color string. + * @param {number} percent Percentage to lighten (0-100). + * @returns {string} Lightened hex color. + */ + const lightenColor = (color, percent) => { + const num = parseInt(color.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = (num >> 16) + amt; + const G = ((num >> 8) & 0x00ff) + amt; + const B = (num & 0x0000ff) + amt; + return ( + "#" + + ( + 0x1000000 + + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + + (B < 255 ? (B < 1 ? 0 : B) : 255) + ) + .toString(16) + .slice(1) + ); + }; + + /** + * Darkens a color by a percentage. + * + * @param {string} color Hex color string. + * @param {number} percent Percentage to darken (0-100). + * @returns {string} Darkened hex color. + */ + const darkenColor = (color, percent) => { + const num = parseInt(color.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = (num >> 16) - amt; + const G = ((num >> 8) & 0x00ff) - amt; + const B = (num & 0x0000ff) - amt; + return ( + "#" + + ( + 0x1000000 + + (R > 255 ? 255 : R < 0 ? 0 : R) * 0x10000 + + (G > 255 ? 255 : G < 0 ? 0 : G) * 0x100 + + (B > 255 ? 255 : B < 0 ? 0 : B) + ) + .toString(16) + .slice(1) + ); + }; + + // Create gradient for 3D effect + const gradientId = `gradient-${index}`; + const lightColor = lightenColor(color, 20); + const darkColor = darkenColor(color, 20); + + return ` + + + + + + + + + + + + + + + + + + + + + + + + ${lang.name} + + + ${displayValue} + + + `; +}; + +/** + * Renders the 3D language card layout. + * + * @param {Lang[]} langs Array of programming languages. + * @param {number} width Card width. + * @param {number} totalLanguageSize Total size of all languages. + * @param {string} statsFormat Stats format. + * @returns {string} 3D layout card SVG object. + */ +const render3DLayout = (langs, width, totalLanguageSize, statsFormat) => { + const maxHeight = 80; + const barWidth = Math.max(20, (width - 40) / langs.length - 5); + const startX = 20; + + const bars = langs + .map((lang, index) => { + const x = startX + index * (barWidth + 5); + return create3DBar({ + lang, + totalSize: totalLanguageSize, + index, + maxHeight, + barWidth, + x, + statsFormat, + }); + }) + .join(""); + + return ` + + + ${bars} + + + + + + + + + `; +}; + /** * @typedef {import("./types").TopLangOptions} TopLangOptions * @typedef {TopLangOptions["layout"]} Layout @@ -762,6 +984,8 @@ const getDefaultLanguagesCountByLayout = ({ layout, hide_progress }) => { return DONUT_VERTICAL_LAYOUT_DEFAULT_LANGS_COUNT; } else if (layout === "pie") { return PIE_LAYOUT_DEFAULT_LANGS_COUNT; + } else if (layout === "3d") { + return 6; // 3D layout default count } else { return NORMAL_LAYOUT_DEFAULT_LANGS_COUNT; } @@ -846,6 +1070,9 @@ const renderTopLanguages = (topLangs, options = {}) => { totalLanguageSize, stats_format, ); + } else if (layout === "3d") { + height = calculate3DLayoutHeight(langs.length); + finalLayout = render3DLayout(langs, width, totalLanguageSize, stats_format); } else if (layout === "compact" || hide_progress == true) { height = calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0); @@ -956,6 +1183,7 @@ export { calculateDonutLayoutHeight, calculateDonutVerticalLayoutHeight, calculatePieLayoutHeight, + calculate3DLayoutHeight, // Add this export donutCenterTranslation, trimTopLanguages, renderTopLanguages, diff --git a/src/cards/types.d.ts b/src/cards/types.d.ts index 94f36adc624b7..b50a5df91c797 100644 --- a/src/cards/types.d.ts +++ b/src/cards/types.d.ts @@ -40,7 +40,7 @@ export type TopLangOptions = CommonOptions & { hide_title: boolean; card_width: number; hide: string[]; - layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie"; + layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie" | "3d"; custom_title: string; langs_count: number; disable_animations: boolean; diff --git a/vercel.json b/vercel.json index f26984795d0b9..531c33282de74 100644 --- a/vercel.json +++ b/vercel.json @@ -8,7 +8,7 @@ "redirects": [ { "source": "/", - "destination": "https://github.com/anuraghazra/github-readme-stats" + "destination": "https://github.com/iagomartins/github-readme-stats" } ] } \ No newline at end of file From e6e352752f08075e6130302e71e08912c6fcf443 Mon Sep 17 00:00:00 2001 From: Iago Martins Date: Fri, 19 Sep 2025 10:24:54 -0300 Subject: [PATCH 2/5] Added 3D layout for language stats. --- src/cards/top-languages.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cards/top-languages.js b/src/cards/top-languages.js index a75487e82fe8e..cb3a2a0514830 100644 --- a/src/cards/top-languages.js +++ b/src/cards/top-languages.js @@ -765,9 +765,8 @@ const create3DBar = ({ const staggerDelay = (index + 3) * 150; // Calculate 3D points for isometric projection - const frontTopLeft = { x: x, y: 150 - barHeight }; + const frontTopLeft = { x, y: 150 - barHeight }; const frontTopRight = { x: x + barWidth, y: 150 - barHeight }; - const frontBottomLeft = { x: x, y: 150 }; const frontBottomRight = { x: x + barWidth, y: 150 }; // Back face (offset by depth) From dc4a661322cb7e6f06d1503725e16a7f7f44a59c Mon Sep 17 00:00:00 2001 From: Iago Martins Date: Fri, 19 Sep 2025 10:47:30 -0300 Subject: [PATCH 3/5] Added 3D layout for language stats. --- vercel.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/vercel.json b/vercel.json index 531c33282de74..886334478849b 100644 --- a/vercel.json +++ b/vercel.json @@ -4,11 +4,5 @@ "memory": 128, "maxDuration": 10 } - }, - "redirects": [ - { - "source": "/", - "destination": "https://github.com/iagomartins/github-readme-stats" - } - ] + } } \ No newline at end of file From e5ccb3605f32842eb59cd66fb11de8c0ace5f23e Mon Sep 17 00:00:00 2001 From: Iago Martins Date: Fri, 19 Sep 2025 10:57:35 -0300 Subject: [PATCH 4/5] feat(3DLayout): Applyied new layout config: 3D. --- src/cards/top-languages.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/cards/top-languages.js b/src/cards/top-languages.js index cb3a2a0514830..87092c78ac585 100644 --- a/src/cards/top-languages.js +++ b/src/cards/top-languages.js @@ -908,8 +908,14 @@ const create3DBar = ({ */ const render3DLayout = (langs, width, totalLanguageSize, statsFormat) => { const maxHeight = 80; - const barWidth = Math.max(20, (width - 40) / langs.length - 5); - const startX = 20; + const depth = 15; // 3D depth offset + const rightPadding = 20; // Add right padding to prevent overflow + const leftPadding = 20; + + // Calculate available width considering 3D depth and padding + const availableWidth = width - leftPadding - rightPadding - depth; + const barWidth = Math.max(20, availableWidth / langs.length - 5); + const startX = leftPadding; const bars = langs .map((lang, index) => { @@ -932,14 +938,14 @@ const render3DLayout = (langs, width, totalLanguageSize, statsFormat) => { ${bars} - - - + + `; }; @@ -1071,6 +1077,7 @@ const renderTopLanguages = (topLangs, options = {}) => { ); } else if (layout === "3d") { height = calculate3DLayoutHeight(langs.length); + width = width + 30; // Add padding for 3D depth finalLayout = render3DLayout(langs, width, totalLanguageSize, stats_format); } else if (layout === "compact" || hide_progress == true) { height = From 08dd03716b43dff9a23711f78eb7a346b4506b7b Mon Sep 17 00:00:00 2001 From: Iago Martins Date: Fri, 19 Sep 2025 12:27:09 -0300 Subject: [PATCH 5/5] feat(3DLayout): Applyied new layout config: 3D. --- src/cards/top-languages.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/cards/top-languages.js b/src/cards/top-languages.js index 87092c78ac585..51b0114664b36 100644 --- a/src/cards/top-languages.js +++ b/src/cards/top-languages.js @@ -909,7 +909,7 @@ const create3DBar = ({ const render3DLayout = (langs, width, totalLanguageSize, statsFormat) => { const maxHeight = 80; const depth = 15; // 3D depth offset - const rightPadding = 20; // Add right padding to prevent overflow + const rightPadding = 40; // Increased padding to account for 3D depth and text labels const leftPadding = 20; // Calculate available width considering 3D depth and padding @@ -936,16 +936,6 @@ const render3DLayout = (langs, width, totalLanguageSize, statsFormat) => { ${bars} - - - - - - - `; };