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