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..51b0114664b36 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,213 @@ 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, y: 150 - barHeight };
+ const frontTopRight = { x: x + barWidth, y: 150 - barHeight };
+ 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 depth = 15; // 3D depth offset
+ const rightPadding = 40; // Increased padding to account for 3D depth and text labels
+ 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) => {
+ 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 +979,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 +1065,10 @@ const renderTopLanguages = (topLangs, options = {}) => {
totalLanguageSize,
stats_format,
);
+ } 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 =
calculateCompactLayoutHeight(langs.length) + (hide_progress ? -25 : 0);
@@ -956,6 +1179,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..886334478849b 100644
--- a/vercel.json
+++ b/vercel.json
@@ -4,11 +4,5 @@
"memory": 128,
"maxDuration": 10
}
- },
- "redirects": [
- {
- "source": "/",
- "destination": "https://github.com/anuraghazra/github-readme-stats"
- }
- ]
+ }
}
\ No newline at end of file