@mightbeandrew/devcard
Advanced tools
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
+427
-212
@@ -496,2 +496,26 @@ #!/usr/bin/env node | ||
| // src/utils/fonts.ts | ||
| import { GlobalFonts } from "@napi-rs/canvas"; | ||
| // src/utils/assetPath.ts | ||
| import { fileURLToPath } from "url"; | ||
| import path from "path"; | ||
| var __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| function assetPath(relative) { | ||
| return path.join(__dirname, "..", "..", "assets", relative); | ||
| } | ||
| // src/utils/fonts.ts | ||
| var registered = false; | ||
| function registerFonts() { | ||
| if (registered) return; | ||
| GlobalFonts.registerFromPath(assetPath("fonts/Orbitron.ttf"), "Orbitron"); | ||
| GlobalFonts.registerFromPath(assetPath("fonts/Rajdhani-Regular.ttf"), "Rajdhani"); | ||
| GlobalFonts.registerFromPath(assetPath("fonts/Rajdhani-SemiBold.ttf"), "Rajdhani"); | ||
| GlobalFonts.registerFromPath(assetPath("fonts/Rajdhani-Bold.ttf"), "Rajdhani"); | ||
| GlobalFonts.registerFromPath(assetPath("fonts/SpaceMono-Regular.ttf"), "SpaceMono"); | ||
| GlobalFonts.registerFromPath(assetPath("fonts/SpaceMono-Bold.ttf"), "SpaceMono"); | ||
| registered = true; | ||
| } | ||
| // src/card/statMapper.ts | ||
@@ -562,89 +586,103 @@ function logScale(value, max) { | ||
| var CARD_HEIGHT = 1050; | ||
| var MARGIN = 30; | ||
| var MARGIN = 32; | ||
| var INNER_WIDTH = CARD_WIDTH - MARGIN * 2; | ||
| var CORNER_RADIUS = 24; | ||
| var CORNER_RADIUS = 20; | ||
| var HEADER_Y = MARGIN; | ||
| var HEADER_HEIGHT = 54; | ||
| var AVATAR_SIZE = 220; | ||
| var HEADER_HEIGHT = 52; | ||
| var AVATAR_SIZE = 210; | ||
| var AVATAR_X = (CARD_WIDTH - AVATAR_SIZE) / 2; | ||
| var AVATAR_Y = HEADER_Y + HEADER_HEIGHT + 24; | ||
| var AVATAR_Y = HEADER_Y + HEADER_HEIGHT + 22; | ||
| var AVATAR_RADIUS = AVATAR_SIZE / 2; | ||
| var AVATAR_CENTER_X = AVATAR_X + AVATAR_RADIUS; | ||
| var AVATAR_CENTER_Y = AVATAR_Y + AVATAR_RADIUS; | ||
| var NAMEPLATE_Y = AVATAR_Y + AVATAR_SIZE + 18; | ||
| var NAMEPLATE_HEIGHT = 68; | ||
| var NAMEPLATE_Y = AVATAR_Y + AVATAR_SIZE + 16; | ||
| var NAMEPLATE_HEIGHT = 70; | ||
| var NAMEPLATE_X = MARGIN; | ||
| var NAMEPLATE_WIDTH = INNER_WIDTH; | ||
| var TYPE_LINE_Y = NAMEPLATE_Y + NAMEPLATE_HEIGHT + 14; | ||
| var SEP1_Y = TYPE_LINE_Y + 26; | ||
| var BIO_Y = SEP1_Y + 12; | ||
| var BIO_HEIGHT = 56; | ||
| var STATS_Y = BIO_Y + BIO_HEIGHT + 12; | ||
| var STATS_HEIGHT = 178; | ||
| var TYPE_LINE_HEIGHT = 28; | ||
| var BIO_Y = TYPE_LINE_Y + TYPE_LINE_HEIGHT + 10; | ||
| var BIO_HEIGHT = 44; | ||
| var SEP1_Y = BIO_Y + BIO_HEIGHT + 8; | ||
| var STATS_Y = SEP1_Y + 10; | ||
| var STATS_HEIGHT = 192; | ||
| var STATS_X = MARGIN; | ||
| var STATS_WIDTH = INNER_WIDTH; | ||
| var STAT_ROW_HEIGHT = 30; | ||
| var STAT_BAR_WIDTH = 170; | ||
| var STAT_BAR_HEIGHT = 11; | ||
| var STAT_SEGMENTS = 6; | ||
| var REPOS_Y = STATS_Y + STATS_HEIGHT + 16; | ||
| var STAT_ROW_HEIGHT = 32; | ||
| var STAT_BAR_WIDTH = 180; | ||
| var STAT_BAR_HEIGHT = 12; | ||
| var REPOS_Y = STATS_Y + STATS_HEIGHT + 14; | ||
| var REPO_ROW_HEIGHT = 28; | ||
| var LANG_Y = REPOS_Y + REPO_ROW_HEIGHT * 3 + 18; | ||
| var SEP2_Y = LANG_Y + 42; | ||
| var FOOTER_Y = SEP2_Y + 14; | ||
| var LANG_Y = REPOS_Y + REPO_ROW_HEIGHT * 3 + 14; | ||
| var SEP2_Y = LANG_Y + 38; | ||
| var FOOTER_Y = SEP2_Y + 12; | ||
| // src/card/elements/frame.ts | ||
| function drawFrame(ctx, theme, rarity) { | ||
| const bgGrad = ctx.createLinearGradient(0, 0, 0, CARD_HEIGHT); | ||
| roundRect(ctx, 0, 0, CARD_WIDTH, CARD_HEIGHT, CORNER_RADIUS); | ||
| ctx.clip(); | ||
| const bgGrad = ctx.createLinearGradient(0, 0, CARD_WIDTH * 0.4, CARD_HEIGHT); | ||
| bgGrad.addColorStop(0, theme.cardBackground); | ||
| bgGrad.addColorStop(1, theme.cardBackground2); | ||
| roundRect(ctx, 0, 0, CARD_WIDTH, CARD_HEIGHT, CORNER_RADIUS); | ||
| ctx.fillStyle = bgGrad; | ||
| ctx.fill(); | ||
| const borderGrad = ctx.createLinearGradient(0, 0, CARD_WIDTH, CARD_HEIGHT); | ||
| ctx.fillRect(0, 0, CARD_WIDTH, CARD_HEIGHT); | ||
| const highlight = ctx.createRadialGradient(CARD_WIDTH / 2, 0, 0, CARD_WIDTH / 2, 0, CARD_WIDTH * 0.8); | ||
| highlight.addColorStop(0, "rgba(255,255,255,0.06)"); | ||
| highlight.addColorStop(1, "rgba(255,255,255,0)"); | ||
| ctx.fillStyle = highlight; | ||
| ctx.fillRect(0, 0, CARD_WIDTH, CARD_HEIGHT); | ||
| ctx.save(); | ||
| const borderGrad = makeBorderGradient(ctx, rarity, theme); | ||
| ctx.strokeStyle = borderGrad; | ||
| ctx.lineWidth = rarity === "legendary" || rarity === "secret-rare" ? 5 : 4; | ||
| ctx.shadowColor = theme.borderTop; | ||
| ctx.shadowBlur = rarity === "ultra-rare" || rarity === "legendary" || rarity === "secret-rare" ? 18 : 8; | ||
| roundRect(ctx, 2, 2, CARD_WIDTH - 4, CARD_HEIGHT - 4, CORNER_RADIUS - 1); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.strokeStyle = "rgba(255,255,255,0.08)"; | ||
| ctx.lineWidth = 1; | ||
| roundRect(ctx, MARGIN - 4, MARGIN - 4, CARD_WIDTH - (MARGIN - 4) * 2, CARD_HEIGHT - (MARGIN - 4) * 2, CORNER_RADIUS - 8); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| drawCornerGems(ctx, theme, rarity); | ||
| } | ||
| function makeBorderGradient(ctx, rarity, theme) { | ||
| const grad = ctx.createLinearGradient(0, 0, CARD_WIDTH, CARD_HEIGHT); | ||
| if (rarity === "legendary" || rarity === "secret-rare") { | ||
| borderGrad.addColorStop(0, "#ff0000"); | ||
| borderGrad.addColorStop(0.17, "#ff8800"); | ||
| borderGrad.addColorStop(0.33, "#ffff00"); | ||
| borderGrad.addColorStop(0.5, "#00ff00"); | ||
| borderGrad.addColorStop(0.67, "#0088ff"); | ||
| borderGrad.addColorStop(0.83, "#8800ff"); | ||
| borderGrad.addColorStop(1, "#ff0088"); | ||
| grad.addColorStop(0, "#ff0080"); | ||
| grad.addColorStop(0.2, "#ff8800"); | ||
| grad.addColorStop(0.4, "#ffff00"); | ||
| grad.addColorStop(0.6, "#00ff88"); | ||
| grad.addColorStop(0.8, "#0088ff"); | ||
| grad.addColorStop(1, "#ff00ff"); | ||
| } else if (rarity === "ultra-rare") { | ||
| borderGrad.addColorStop(0, "#ffd700"); | ||
| borderGrad.addColorStop(0.5, "#ff00ff"); | ||
| borderGrad.addColorStop(1, "#00ffff"); | ||
| grad.addColorStop(0, "#ffd700"); | ||
| grad.addColorStop(0.5, "#ff6ef7"); | ||
| grad.addColorStop(1, "#00e5ff"); | ||
| } else { | ||
| borderGrad.addColorStop(0, theme.borderTop); | ||
| borderGrad.addColorStop(1, theme.borderBottom); | ||
| grad.addColorStop(0, theme.borderTop); | ||
| grad.addColorStop(1, theme.borderBottom); | ||
| } | ||
| ctx.strokeStyle = borderGrad; | ||
| ctx.lineWidth = theme.borderWidth; | ||
| roundRect(ctx, theme.borderWidth / 2, theme.borderWidth / 2, CARD_WIDTH - theme.borderWidth, CARD_HEIGHT - theme.borderWidth, CORNER_RADIUS - 2); | ||
| ctx.stroke(); | ||
| const gemSize = 10; | ||
| const gemPositions = [ | ||
| [MARGIN - 2, MARGIN - 2], | ||
| [CARD_WIDTH - MARGIN - gemSize + 2, MARGIN - 2], | ||
| [MARGIN - 2, CARD_HEIGHT - MARGIN - gemSize + 2], | ||
| [CARD_WIDTH - MARGIN - gemSize + 2, CARD_HEIGHT - MARGIN - gemSize + 2] | ||
| return grad; | ||
| } | ||
| function drawCornerGems(ctx, theme, rarity) { | ||
| const gemColor = rarity === "legendary" || rarity === "secret-rare" ? "#ffd700" : theme.borderTop; | ||
| const positions = [ | ||
| [MARGIN - 1, MARGIN - 1], | ||
| [CARD_WIDTH - MARGIN - 1, MARGIN - 1], | ||
| [MARGIN - 1, CARD_HEIGHT - MARGIN - 1], | ||
| [CARD_WIDTH - MARGIN - 1, CARD_HEIGHT - MARGIN - 1] | ||
| ]; | ||
| for (const [gx, gy] of gemPositions) { | ||
| for (const [gx, gy] of positions) { | ||
| ctx.save(); | ||
| ctx.translate(gx + gemSize / 2, gy + gemSize / 2); | ||
| ctx.translate(gx, gy); | ||
| ctx.rotate(Math.PI / 4); | ||
| ctx.fillStyle = theme.borderTop; | ||
| ctx.fillRect(-gemSize / 2, -gemSize / 2, gemSize, gemSize); | ||
| ctx.shadowColor = gemColor; | ||
| ctx.shadowBlur = 8; | ||
| ctx.fillStyle = gemColor; | ||
| ctx.fillRect(-5, -5, 10, 10); | ||
| ctx.restore(); | ||
| } | ||
| if (theme.rarityGlow) { | ||
| ctx.save(); | ||
| ctx.shadowColor = theme.rarityGlow; | ||
| ctx.shadowBlur = 30; | ||
| ctx.strokeStyle = theme.rarityGlow; | ||
| ctx.lineWidth = 2; | ||
| roundRect(ctx, MARGIN, MARGIN, CARD_WIDTH - MARGIN * 2, CARD_HEIGHT - MARGIN * 2, CORNER_RADIUS - 4); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| } | ||
| } | ||
@@ -667,48 +705,60 @@ function roundRect(ctx, x, y, w, h, r) { | ||
| function drawHeader(ctx, theme, rarityLabel, rarity, typeName) { | ||
| ctx.fillStyle = theme.headerStrip; | ||
| ctx.fillRect(MARGIN, HEADER_Y, CARD_WIDTH - MARGIN * 2, HEADER_HEIGHT); | ||
| const hGrad = ctx.createLinearGradient(MARGIN, 0, CARD_WIDTH - MARGIN, 0); | ||
| hGrad.addColorStop(0, theme.headerStrip); | ||
| hGrad.addColorStop(1, theme.cardBackground2); | ||
| roundRect(ctx, MARGIN, HEADER_Y, CARD_WIDTH - MARGIN * 2, HEADER_HEIGHT, 10); | ||
| ctx.fillStyle = hGrad; | ||
| ctx.fill(); | ||
| const dx = MARGIN + 22; | ||
| const dy = HEADER_Y + HEADER_HEIGHT / 2; | ||
| ctx.save(); | ||
| ctx.translate(dx, dy); | ||
| ctx.rotate(Math.PI / 4); | ||
| ctx.fillStyle = theme.borderTop; | ||
| ctx.translate(MARGIN + 18, HEADER_Y + 26); | ||
| ctx.rotate(Math.PI / 4); | ||
| ctx.fillRect(-8, -8, 16, 16); | ||
| ctx.shadowColor = theme.borderTop; | ||
| ctx.shadowBlur = 10; | ||
| ctx.fillRect(-7, -7, 14, 14); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.headerText; | ||
| ctx.font = "bold 20px sans-serif"; | ||
| ctx.letterSpacing = "3px"; | ||
| ctx.fillText("DEVCARD", MARGIN + 36, HEADER_Y + 34); | ||
| ctx.font = `bold 18px Orbitron, sans-serif`; | ||
| ctx.letterSpacing = "4px"; | ||
| ctx.fillText("DEVCARD", MARGIN + 38, HEADER_Y + HEADER_HEIGHT / 2 + 7); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.typeColor; | ||
| ctx.font = "12px sans-serif"; | ||
| ctx.font = `600 11px Rajdhani, sans-serif`; | ||
| ctx.letterSpacing = "3px"; | ||
| ctx.textAlign = "center"; | ||
| ctx.fillText(typeName, CARD_WIDTH / 2, HEADER_Y + 20); | ||
| ctx.fillText(typeName.toUpperCase(), CARD_WIDTH / 2, HEADER_Y + HEADER_HEIGHT / 2 + 5); | ||
| ctx.restore(); | ||
| const rarityColors = { | ||
| common: "#94a3b8", | ||
| uncommon: "#c0c0c0", | ||
| uncommon: "#a8b8c8", | ||
| rare: "#ffd700", | ||
| "ultra-rare": "#ff8c00", | ||
| "secret-rare": "#ff00ff", | ||
| legendary: "#ff0000" | ||
| "ultra-rare": "#ff9500", | ||
| "secret-rare": "#e040fb", | ||
| legendary: "#ff1744" | ||
| }; | ||
| const badgeColor = rarityColors[rarity]; | ||
| const badgeText = rarityLabel; | ||
| const badgeX = CARD_WIDTH - MARGIN - 140; | ||
| const badgeY = HEADER_Y + 8; | ||
| const badgeW = 130; | ||
| const badgeH = 28; | ||
| const bW = 140; | ||
| const bH = 26; | ||
| const bX = CARD_WIDTH - MARGIN - bW - 4; | ||
| const bY = HEADER_Y + (HEADER_HEIGHT - bH) / 2; | ||
| ctx.save(); | ||
| ctx.fillStyle = badgeColor + "22"; | ||
| roundRect(ctx, badgeX, badgeY, badgeW, badgeH, 6); | ||
| roundRect(ctx, bX, bY, bW, bH, 5); | ||
| ctx.fillStyle = badgeColor + "25"; | ||
| ctx.fill(); | ||
| ctx.strokeStyle = badgeColor; | ||
| ctx.lineWidth = 1.5; | ||
| roundRect(ctx, badgeX, badgeY, badgeW, badgeH, 6); | ||
| ctx.shadowColor = badgeColor; | ||
| ctx.shadowBlur = 6; | ||
| roundRect(ctx, bX, bY, bW, bH, 5); | ||
| ctx.stroke(); | ||
| ctx.shadowBlur = 0; | ||
| ctx.fillStyle = badgeColor; | ||
| ctx.font = "bold 10px sans-serif"; | ||
| ctx.font = `bold 10px Orbitron, sans-serif`; | ||
| ctx.letterSpacing = "1px"; | ||
| ctx.textAlign = "center"; | ||
| ctx.fillText(badgeText, badgeX + badgeW / 2, badgeY + 18); | ||
| ctx.fillText(rarityLabel, bX + bW / 2, bY + bH / 2 + 4); | ||
| ctx.restore(); | ||
@@ -719,8 +769,11 @@ } | ||
| ctx.fillStyle = "#22c55e"; | ||
| roundRect(ctx, CARD_WIDTH - MARGIN - 80, HEADER_Y + 40, 70, 20, 4); | ||
| ctx.shadowColor = "#22c55e"; | ||
| ctx.shadowBlur = 8; | ||
| roundRect(ctx, CARD_WIDTH - MARGIN - 90, HEADER_Y + 56, 80, 22, 4); | ||
| ctx.fill(); | ||
| ctx.fillStyle = "#ffffff"; | ||
| ctx.font = "bold 10px sans-serif"; | ||
| ctx.font = "bold 10px Orbitron, sans-serif"; | ||
| ctx.textAlign = "center"; | ||
| ctx.fillText("ROOKIE", CARD_WIDTH - MARGIN - 45, HEADER_Y + 54); | ||
| ctx.letterSpacing = "2px"; | ||
| ctx.fillText("ROOKIE", CARD_WIDTH - MARGIN - 50, HEADER_Y + 71); | ||
| ctx.restore(); | ||
@@ -785,22 +838,33 @@ } | ||
| function drawNameplate(ctx, theme, data) { | ||
| const npGrad = ctx.createLinearGradient(NAMEPLATE_X, 0, NAMEPLATE_X + NAMEPLATE_WIDTH, 0); | ||
| npGrad.addColorStop(0, theme.nameplate); | ||
| npGrad.addColorStop(0.6, theme.nameplate); | ||
| npGrad.addColorStop(1, theme.cardBackground2); | ||
| ctx.save(); | ||
| roundRect(ctx, NAMEPLATE_X, NAMEPLATE_Y, NAMEPLATE_WIDTH, NAMEPLATE_HEIGHT, 8); | ||
| ctx.fillStyle = theme.nameplate; | ||
| roundRect(ctx, NAMEPLATE_X, NAMEPLATE_Y, NAMEPLATE_WIDTH, NAMEPLATE_HEIGHT, 10); | ||
| ctx.fillStyle = npGrad; | ||
| ctx.fill(); | ||
| ctx.fillStyle = theme.borderTop; | ||
| ctx.shadowColor = theme.borderTop; | ||
| ctx.shadowBlur = 8; | ||
| ctx.fillRect(NAMEPLATE_X, NAMEPLATE_Y, 4, NAMEPLATE_HEIGHT); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.usernameText; | ||
| ctx.font = "14px monospace"; | ||
| ctx.fillText(`@${data.username}`, NAMEPLATE_X + 14, NAMEPLATE_Y + 22); | ||
| ctx.font = `500 12px Rajdhani, sans-serif`; | ||
| ctx.letterSpacing = "2px"; | ||
| ctx.fillText(`@${data.username}`.toUpperCase(), NAMEPLATE_X + 18, NAMEPLATE_Y + 24); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.nameplateText; | ||
| ctx.font = "bold 22px sans-serif"; | ||
| ctx.font = `bold 24px Orbitron, sans-serif`; | ||
| ctx.shadowColor = theme.nameplateText + "66"; | ||
| ctx.shadowBlur = 12; | ||
| let displayName = data.displayName; | ||
| const maxWidth = NAMEPLATE_WIDTH - 28; | ||
| while (ctx.measureText(displayName).width > maxWidth && displayName.length > 0) { | ||
| const maxW = NAMEPLATE_WIDTH - 28; | ||
| while (ctx.measureText(displayName).width > maxW && displayName.length > 1) { | ||
| displayName = displayName.slice(0, -1); | ||
| } | ||
| if (displayName !== data.displayName) displayName += "\u2026"; | ||
| ctx.fillText(displayName, NAMEPLATE_X + 14, NAMEPLATE_Y + 50); | ||
| ctx.fillText(displayName, NAMEPLATE_X + 18, NAMEPLATE_Y + 56); | ||
| ctx.restore(); | ||
@@ -810,6 +874,5 @@ if (data.location) { | ||
| ctx.fillStyle = theme.usernameText; | ||
| ctx.font = "12px sans-serif"; | ||
| ctx.font = `500 12px Rajdhani, sans-serif`; | ||
| ctx.textAlign = "right"; | ||
| const locText = `@ ${data.location}`.slice(0, 28); | ||
| ctx.fillText(locText, NAMEPLATE_X + NAMEPLATE_WIDTH - 14, NAMEPLATE_Y + 22); | ||
| ctx.fillText(data.location.slice(0, 26), NAMEPLATE_X + NAMEPLATE_WIDTH - 14, NAMEPLATE_Y + 24); | ||
| ctx.restore(); | ||
@@ -821,13 +884,14 @@ } | ||
| ctx.save(); | ||
| ctx.font = "bold 12px sans-serif"; | ||
| const tw = ctx.measureText(typeText).width + 20; | ||
| roundRect(ctx, MARGIN, TYPE_LINE_Y - 14, tw, 20, 4); | ||
| ctx.font = `bold 11px Rajdhani, sans-serif`; | ||
| ctx.letterSpacing = "2px"; | ||
| const tw = ctx.measureText(typeText.toUpperCase()).width + 22; | ||
| roundRect(ctx, MARGIN, TYPE_LINE_Y, tw, TYPE_LINE_HEIGHT, 5); | ||
| ctx.fillStyle = theme.typeBadgeBg; | ||
| ctx.fill(); | ||
| ctx.strokeStyle = theme.typeColor; | ||
| ctx.lineWidth = 1; | ||
| roundRect(ctx, MARGIN, TYPE_LINE_Y - 14, tw, 20, 4); | ||
| ctx.lineWidth = 1.5; | ||
| roundRect(ctx, MARGIN, TYPE_LINE_Y, tw, TYPE_LINE_HEIGHT, 5); | ||
| ctx.stroke(); | ||
| ctx.fillStyle = theme.typeColor; | ||
| ctx.fillText(typeText, MARGIN + 10, TYPE_LINE_Y); | ||
| ctx.fillText(typeText.toUpperCase(), MARGIN + 11, TYPE_LINE_Y + TYPE_LINE_HEIGHT / 2 + 4); | ||
| ctx.restore(); | ||
@@ -837,28 +901,19 @@ if (data.company) { | ||
| ctx.fillStyle = theme.statLabel; | ||
| ctx.font = "12px sans-serif"; | ||
| ctx.fillText(`\u2022 ${data.company}`, MARGIN + tw + 10, TYPE_LINE_Y); | ||
| ctx.font = `500 13px Rajdhani, sans-serif`; | ||
| ctx.fillText(`\u2022 ${data.company}`, MARGIN + tw + 10, TYPE_LINE_Y + TYPE_LINE_HEIGHT / 2 + 5); | ||
| ctx.restore(); | ||
| } | ||
| ctx.save(); | ||
| ctx.strokeStyle = theme.accent; | ||
| ctx.lineWidth = 1; | ||
| ctx.beginPath(); | ||
| ctx.moveTo(MARGIN, SEP1_Y); | ||
| ctx.lineTo(CARD_WIDTH - MARGIN, SEP1_Y); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| } | ||
| function drawBio(ctx, theme, bio) { | ||
| ctx.save(); | ||
| if (!bio) { | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.footerText; | ||
| ctx.font = "italic 13px sans-serif"; | ||
| ctx.fillText("No bio provided.", MARGIN + 4, BIO_Y + 16); | ||
| ctx.font = `italic 14px Rajdhani, sans-serif`; | ||
| ctx.fillText("No bio provided.", MARGIN + 4, BIO_Y + 18); | ||
| ctx.restore(); | ||
| return; | ||
| } | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.bioText; | ||
| ctx.font = "13px sans-serif"; | ||
| const maxW = CARD_WIDTH - MARGIN * 2 - 10; | ||
| ctx.font = `500 15px Rajdhani, sans-serif`; | ||
| const maxW = CARD_WIDTH - MARGIN * 2 - 8; | ||
| const words = bio.split(" "); | ||
@@ -879,26 +934,168 @@ const lines = []; | ||
| lines.forEach((line, i) => { | ||
| ctx.fillText(line, MARGIN + 4, BIO_Y + 16 + i * 22); | ||
| ctx.fillText(line, MARGIN + 4, BIO_Y + 18 + i * 22); | ||
| }); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| const sepGrad = ctx.createLinearGradient(MARGIN, 0, CARD_WIDTH - MARGIN, 0); | ||
| sepGrad.addColorStop(0, theme.typeColor + "88"); | ||
| sepGrad.addColorStop(0.5, theme.typeColor + "44"); | ||
| sepGrad.addColorStop(1, "transparent"); | ||
| ctx.strokeStyle = sepGrad; | ||
| ctx.lineWidth = 1; | ||
| ctx.beginPath(); | ||
| ctx.moveTo(MARGIN, SEP1_Y); | ||
| ctx.lineTo(CARD_WIDTH - MARGIN, SEP1_Y); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| } | ||
| // src/card/elements/stats.ts | ||
| var LANG_COLORS = { | ||
| JavaScript: "#f7df1e", | ||
| TypeScript: "#3178c6", | ||
| Python: "#3572a5", | ||
| Ruby: "#cc342d", | ||
| Go: "#00add8", | ||
| Rust: "#dea584", | ||
| "C++": "#f34b7d", | ||
| C: "#555555", | ||
| Java: "#b07219", | ||
| Kotlin: "#a97bff", | ||
| Swift: "#f05138", | ||
| HTML: "#e34c26", | ||
| CSS: "#563d7c", | ||
| Shell: "#89e051", | ||
| PHP: "#4f5d95", | ||
| Scala: "#c22d40", | ||
| Haskell: "#5e5086", | ||
| Elixir: "#6e4a7e", | ||
| Dart: "#00b4ab", | ||
| Vue: "#41b883", | ||
| "Jupyter Notebook": "#da5b0b" | ||
| }; | ||
| function getLangColor(lang) { | ||
| return LANG_COLORS[lang] ?? "#64748b"; | ||
| } | ||
| function drawStarIcon(ctx, cx, cy, color) { | ||
| const r = 7, ri = 3.5, pts = 5; | ||
| ctx.save(); | ||
| ctx.fillStyle = color; | ||
| ctx.shadowColor = color; | ||
| ctx.shadowBlur = 6; | ||
| ctx.beginPath(); | ||
| for (let i = 0; i < pts * 2; i++) { | ||
| const rad = Math.PI / pts * i - Math.PI / 2; | ||
| const dist = i % 2 === 0 ? r : ri; | ||
| const x = cx + Math.cos(rad) * dist; | ||
| const y = cy + Math.sin(rad) * dist; | ||
| i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); | ||
| } | ||
| ctx.closePath(); | ||
| ctx.fill(); | ||
| ctx.restore(); | ||
| } | ||
| function drawLightningIcon(ctx, cx, cy, color) { | ||
| ctx.save(); | ||
| ctx.fillStyle = color; | ||
| ctx.shadowColor = color; | ||
| ctx.shadowBlur = 6; | ||
| ctx.beginPath(); | ||
| ctx.moveTo(cx + 2, cy - 8); | ||
| ctx.lineTo(cx - 3, cy); | ||
| ctx.lineTo(cx + 1, cy); | ||
| ctx.lineTo(cx - 2, cy + 8); | ||
| ctx.lineTo(cx + 4, cy - 1); | ||
| ctx.lineTo(cx, cy - 1); | ||
| ctx.closePath(); | ||
| ctx.fill(); | ||
| ctx.restore(); | ||
| } | ||
| function drawPeopleIcon(ctx, cx, cy, color) { | ||
| ctx.save(); | ||
| ctx.fillStyle = color; | ||
| ctx.shadowColor = color; | ||
| ctx.shadowBlur = 4; | ||
| ctx.beginPath(); | ||
| ctx.arc(cx - 3, cy - 4, 3.5, 0, Math.PI * 2); | ||
| ctx.fill(); | ||
| ctx.beginPath(); | ||
| ctx.arc(cx + 4, cy - 4, 3.5, 0, Math.PI * 2); | ||
| ctx.fill(); | ||
| ctx.beginPath(); | ||
| ctx.ellipse(cx - 3, cy + 3, 4.5, 4, 0, 0, Math.PI); | ||
| ctx.fill(); | ||
| ctx.beginPath(); | ||
| ctx.ellipse(cx + 4, cy + 3, 4.5, 4, 0, 0, Math.PI); | ||
| ctx.fill(); | ||
| ctx.restore(); | ||
| } | ||
| function drawClockIcon(ctx, cx, cy, color) { | ||
| ctx.save(); | ||
| ctx.strokeStyle = color; | ||
| ctx.fillStyle = "transparent"; | ||
| ctx.shadowColor = color; | ||
| ctx.shadowBlur = 4; | ||
| ctx.lineWidth = 1.5; | ||
| ctx.beginPath(); | ||
| ctx.arc(cx, cy, 7, 0, Math.PI * 2); | ||
| ctx.stroke(); | ||
| ctx.beginPath(); | ||
| ctx.moveTo(cx, cy); | ||
| ctx.lineTo(cx, cy - 4); | ||
| ctx.stroke(); | ||
| ctx.beginPath(); | ||
| ctx.moveTo(cx, cy); | ||
| ctx.lineTo(cx + 3, cy + 1); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| } | ||
| function drawBoxIcon(ctx, cx, cy, color) { | ||
| ctx.save(); | ||
| ctx.strokeStyle = color; | ||
| ctx.shadowColor = color; | ||
| ctx.shadowBlur = 4; | ||
| ctx.lineWidth = 1.5; | ||
| ctx.strokeRect(cx - 6, cy - 5, 12, 10); | ||
| ctx.beginPath(); | ||
| ctx.moveTo(cx - 6, cy - 2); | ||
| ctx.lineTo(cx + 6, cy - 2); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| } | ||
| var STAT_ICON_FNS = [ | ||
| drawStarIcon, | ||
| drawLightningIcon, | ||
| drawPeopleIcon, | ||
| drawClockIcon, | ||
| drawBoxIcon | ||
| ]; | ||
| var STAT_ROWS = [ | ||
| { key: "hp", icon: "*", label: "STARS" }, | ||
| { key: "atk", icon: "~", label: "COMMITS" }, | ||
| { key: "def", icon: "+", label: "FOLLOWERS" }, | ||
| { key: "spd", icon: "@", label: "TENURE" }, | ||
| { key: "lvl", icon: "#", label: "REPOS" } | ||
| { key: "hp", label: "STARS" }, | ||
| { key: "atk", label: "COMMITS" }, | ||
| { key: "def", label: "FOLLOWERS" }, | ||
| { key: "spd", label: "TENURE" }, | ||
| { key: "lvl", label: "REPOS" } | ||
| ]; | ||
| function drawStats(ctx, theme, stats, data) { | ||
| ctx.save(); | ||
| roundRect(ctx, STATS_X, STATS_Y, STATS_WIDTH, STATS_HEIGHT, 8); | ||
| roundRect(ctx, STATS_X, STATS_Y, STATS_WIDTH, STATS_HEIGHT, 10); | ||
| ctx.fillStyle = theme.statsPanelBg; | ||
| ctx.fill(); | ||
| ctx.strokeStyle = theme.typeColor + "55"; | ||
| ctx.lineWidth = 1; | ||
| roundRect(ctx, STATS_X, STATS_Y, STATS_WIDTH, STATS_HEIGHT, 10); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.statLabel; | ||
| ctx.font = "bold 11px sans-serif"; | ||
| ctx.letterSpacing = "2px"; | ||
| ctx.fillText("BASE STATS", STATS_X + 12, STATS_Y + 18); | ||
| ctx.font = `bold 11px Orbitron, sans-serif`; | ||
| ctx.letterSpacing = "3px"; | ||
| ctx.fillText("BASE STATS", STATS_X + 14, STATS_Y + 20); | ||
| const labelW = ctx.measureText("BASE STATS").width; | ||
| ctx.strokeStyle = theme.typeColor + "44"; | ||
| ctx.lineWidth = 1; | ||
| ctx.beginPath(); | ||
| ctx.moveTo(STATS_X + 14 + labelW + 10, STATS_Y + 15); | ||
| ctx.lineTo(STATS_X + STATS_WIDTH - 14, STATS_Y + 15); | ||
| ctx.stroke(); | ||
| ctx.restore(); | ||
@@ -912,35 +1109,31 @@ const rawValues = { | ||
| }; | ||
| STAT_ROWS.forEach(({ key, icon, label }, i) => { | ||
| STAT_ROWS.forEach(({ key, label }, i) => { | ||
| const rowY = STATS_Y + 30 + i * STAT_ROW_HEIGHT; | ||
| const value = stats[key]; | ||
| const iconX = STATS_X + 22; | ||
| const iconY = rowY + STAT_ROW_HEIGHT / 2; | ||
| const iconColor = theme.statBarFill; | ||
| STAT_ICON_FNS[i](ctx, iconX, iconY, iconColor); | ||
| ctx.save(); | ||
| ctx.font = "13px sans-serif"; | ||
| ctx.fillStyle = theme.statLabel; | ||
| ctx.fillText(icon, STATS_X + 12, rowY + 12); | ||
| ctx.font = `600 12px Rajdhani, sans-serif`; | ||
| ctx.letterSpacing = "2px"; | ||
| ctx.fillText(label, STATS_X + 38, rowY + STAT_ROW_HEIGHT / 2 + 5); | ||
| ctx.restore(); | ||
| const barX = STATS_X + 130; | ||
| const barY = rowY + (STAT_ROW_HEIGHT - STAT_BAR_HEIGHT) / 2; | ||
| const fillW = value / 100 * STAT_BAR_WIDTH; | ||
| ctx.save(); | ||
| ctx.font = "bold 11px sans-serif"; | ||
| ctx.fillStyle = theme.statLabel; | ||
| ctx.letterSpacing = "1px"; | ||
| ctx.fillText(label, STATS_X + 34, rowY + 12); | ||
| ctx.restore(); | ||
| const barX = STATS_X + 120; | ||
| const barY = rowY + 2; | ||
| const segW = Math.floor((STAT_BAR_WIDTH - (STAT_SEGMENTS - 1) * 2) / STAT_SEGMENTS); | ||
| const filledSegs = Math.round(value / 100 * STAT_SEGMENTS); | ||
| for (let s = 0; s < STAT_SEGMENTS; s++) { | ||
| const sx = barX + s * (segW + 2); | ||
| ctx.save(); | ||
| if (s < filledSegs) { | ||
| ctx.fillStyle = theme.statBarFill; | ||
| if (value >= 100) { | ||
| ctx.shadowColor = theme.statBarFill; | ||
| ctx.shadowBlur = 8; | ||
| } | ||
| } else { | ||
| ctx.fillStyle = theme.statBarEmpty; | ||
| } | ||
| roundRect(ctx, sx, barY, segW, STAT_BAR_HEIGHT, 2); | ||
| roundRect(ctx, barX, barY, STAT_BAR_WIDTH, STAT_BAR_HEIGHT, STAT_BAR_HEIGHT / 2); | ||
| ctx.fillStyle = theme.statBarEmpty; | ||
| ctx.fill(); | ||
| if (fillW > 0) { | ||
| const barGrad = ctx.createLinearGradient(barX, 0, barX + STAT_BAR_WIDTH, 0); | ||
| barGrad.addColorStop(0, theme.statBarFill); | ||
| barGrad.addColorStop(1, theme.borderTop); | ||
| roundRect(ctx, barX, barY, Math.max(fillW, STAT_BAR_HEIGHT), STAT_BAR_HEIGHT, STAT_BAR_HEIGHT / 2); | ||
| ctx.fillStyle = barGrad; | ||
| ctx.shadowColor = theme.statBarFill; | ||
| ctx.shadowBlur = value > 80 ? 10 : 4; | ||
| ctx.fill(); | ||
| ctx.restore(); | ||
| } | ||
@@ -950,11 +1143,14 @@ if (value >= 100) { | ||
| ctx.fillStyle = "#ffd700"; | ||
| ctx.font = "bold 9px sans-serif"; | ||
| ctx.fillText("MAX!", barX + STAT_BAR_WIDTH + 4, rowY + 12); | ||
| ctx.shadowColor = "#ffd700"; | ||
| ctx.shadowBlur = 8; | ||
| ctx.font = `bold 9px Orbitron, sans-serif`; | ||
| ctx.fillText("MAX", barX + STAT_BAR_WIDTH + 6, barY + STAT_BAR_HEIGHT - 1); | ||
| ctx.restore(); | ||
| } | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.statValue; | ||
| ctx.font = "11px monospace"; | ||
| ctx.font = `500 12px SpaceMono, monospace`; | ||
| ctx.textAlign = "right"; | ||
| ctx.fillText(rawValues[key], STATS_X + STATS_WIDTH - 12, rowY + 12); | ||
| ctx.fillText(rawValues[key], STATS_X + STATS_WIDTH - 14, rowY + STAT_ROW_HEIGHT / 2 + 5); | ||
| ctx.restore(); | ||
@@ -967,11 +1163,10 @@ }); | ||
| ctx.fillStyle = theme.statLabel; | ||
| ctx.font = "bold 11px sans-serif"; | ||
| ctx.letterSpacing = "2px"; | ||
| ctx.font = `bold 11px Orbitron, sans-serif`; | ||
| ctx.letterSpacing = "3px"; | ||
| ctx.fillText("TOP REPOS", MARGIN, REPOS_Y - 4); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.strokeStyle = theme.accent; | ||
| const hw = ctx.measureText("TOP REPOS").width; | ||
| ctx.strokeStyle = theme.typeColor + "44"; | ||
| ctx.lineWidth = 1; | ||
| ctx.beginPath(); | ||
| ctx.moveTo(MARGIN + 88, REPOS_Y - 10); | ||
| ctx.moveTo(MARGIN + hw + 10, REPOS_Y - 10); | ||
| ctx.lineTo(CARD_WIDTH - MARGIN, REPOS_Y - 10); | ||
@@ -981,28 +1176,39 @@ ctx.stroke(); | ||
| data.featuredRepos.slice(0, 3).forEach((repo, i) => { | ||
| const ry = REPOS_Y + i * REPO_ROW_HEIGHT + 16; | ||
| const isLast = i === Math.min(data.featuredRepos.length, 3) - 1; | ||
| const connector = isLast ? ">" : ">"; | ||
| const ry = REPOS_Y + i * REPO_ROW_HEIGHT + 18; | ||
| const dotColor = getLangColor(repo.language); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.accent; | ||
| ctx.font = "13px monospace"; | ||
| ctx.fillText(connector, MARGIN, ry); | ||
| ctx.fillStyle = dotColor; | ||
| ctx.shadowColor = dotColor; | ||
| ctx.shadowBlur = 6; | ||
| ctx.beginPath(); | ||
| ctx.arc(MARGIN + 5, ry - 4, 4, 0, Math.PI * 2); | ||
| ctx.fill(); | ||
| ctx.restore(); | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.repoNameColor; | ||
| ctx.font = "13px monospace"; | ||
| ctx.fillText(repo.name.slice(0, 28), MARGIN + 18, ry); | ||
| ctx.font = `600 14px Rajdhani, sans-serif`; | ||
| ctx.fillText(repo.name.slice(0, 30), MARGIN + 16, ry); | ||
| ctx.restore(); | ||
| if (repo.language && repo.language !== "Unknown") { | ||
| ctx.save(); | ||
| ctx.fillStyle = dotColor + "22"; | ||
| ctx.strokeStyle = dotColor + "66"; | ||
| ctx.lineWidth = 1; | ||
| ctx.font = `500 10px Rajdhani, sans-serif`; | ||
| const lw = ctx.measureText(repo.language).width + 12; | ||
| roundRect(ctx, CARD_WIDTH - MARGIN - 80 - lw - 4, ry - 13, lw, 16, 4); | ||
| ctx.fill(); | ||
| roundRect(ctx, CARD_WIDTH - MARGIN - 80 - lw - 4, ry - 13, lw, 16, 4); | ||
| ctx.stroke(); | ||
| ctx.fillStyle = dotColor; | ||
| ctx.textAlign = "center"; | ||
| ctx.fillText(repo.language, CARD_WIDTH - MARGIN - 80 - lw / 2 - 4, ry - 2); | ||
| ctx.restore(); | ||
| } | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.repoStarColor; | ||
| ctx.font = "12px sans-serif"; | ||
| ctx.font = `600 13px Rajdhani, sans-serif`; | ||
| ctx.textAlign = "right"; | ||
| ctx.fillText(`\u2605 ${formatNumber(repo.stars)}`, CARD_WIDTH - MARGIN, ry); | ||
| ctx.fillText(`* ${formatNumber(repo.stars)}`, CARD_WIDTH - MARGIN, ry); | ||
| ctx.restore(); | ||
| if (repo.language && repo.language !== "Unknown") { | ||
| ctx.save(); | ||
| ctx.fillStyle = theme.accent; | ||
| ctx.font = "10px sans-serif"; | ||
| ctx.fillText(`[${repo.language}]`, CARD_WIDTH - MARGIN - 80, ry); | ||
| ctx.restore(); | ||
| } | ||
| }); | ||
@@ -1013,5 +1219,5 @@ } | ||
| if (langs.length === 0) return; | ||
| const badgeW = 100; | ||
| const badgeH = 26; | ||
| const gap = 12; | ||
| const badgeW = 110; | ||
| const badgeH = 30; | ||
| const gap = 10; | ||
| const totalW = langs.length * badgeW + (langs.length - 1) * gap; | ||
@@ -1021,19 +1227,35 @@ const startX = (CARD_WIDTH - totalW) / 2; | ||
| const bx = startX + i * (badgeW + gap); | ||
| const bg = theme.languageBadgeBgs[i] || theme.languageBadgeBgs[0]; | ||
| const color = getLangColor(lang.name); | ||
| ctx.save(); | ||
| roundRect(ctx, bx, LANG_Y, badgeW, badgeH, 6); | ||
| ctx.fillStyle = bg; | ||
| roundRect(ctx, bx, LANG_Y, badgeW, badgeH, 8); | ||
| ctx.fillStyle = color + "18"; | ||
| ctx.fill(); | ||
| ctx.strokeStyle = theme.typeColor + "66"; | ||
| ctx.lineWidth = 1; | ||
| roundRect(ctx, bx, LANG_Y, badgeW, badgeH, 6); | ||
| ctx.strokeStyle = color + "80"; | ||
| ctx.lineWidth = 1.5; | ||
| roundRect(ctx, bx, LANG_Y, badgeW, badgeH, 8); | ||
| ctx.stroke(); | ||
| ctx.fillStyle = theme.languageBadgeText; | ||
| ctx.font = "bold 11px sans-serif"; | ||
| ctx.textAlign = "center"; | ||
| ctx.fillText(`${lang.name} ${lang.percentage}%`, bx + badgeW / 2, LANG_Y + 17); | ||
| ctx.fillStyle = color; | ||
| ctx.shadowColor = color; | ||
| ctx.shadowBlur = 6; | ||
| ctx.beginPath(); | ||
| ctx.arc(bx + 12, LANG_Y + badgeH / 2, 4, 0, Math.PI * 2); | ||
| ctx.fill(); | ||
| ctx.shadowBlur = 0; | ||
| ctx.fillStyle = color; | ||
| ctx.font = `600 12px Rajdhani, sans-serif`; | ||
| ctx.textAlign = "left"; | ||
| ctx.fillText(`${lang.name}`, bx + 22, LANG_Y + badgeH / 2 + 5); | ||
| ctx.fillStyle = theme.statLabel; | ||
| ctx.font = `500 11px SpaceMono, monospace`; | ||
| ctx.textAlign = "right"; | ||
| ctx.fillText(`${lang.percentage}%`, bx + badgeW - 8, LANG_Y + badgeH / 2 + 4); | ||
| ctx.restore(); | ||
| }); | ||
| ctx.save(); | ||
| ctx.strokeStyle = theme.accent; | ||
| const sepGrad = ctx.createLinearGradient(MARGIN, 0, CARD_WIDTH - MARGIN, 0); | ||
| sepGrad.addColorStop(0, "transparent"); | ||
| sepGrad.addColorStop(0.3, theme.accent); | ||
| sepGrad.addColorStop(0.7, theme.accent); | ||
| sepGrad.addColorStop(1, "transparent"); | ||
| ctx.strokeStyle = sepGrad; | ||
| ctx.lineWidth = 1; | ||
@@ -1050,27 +1272,19 @@ ctx.beginPath(); | ||
| ctx.save(); | ||
| ctx.font = `500 11px SpaceMono, monospace`; | ||
| ctx.fillStyle = theme.footerText; | ||
| ctx.font = "10px monospace"; | ||
| ctx.textAlign = "left"; | ||
| ctx.fillText(`github.com/${username2}`, MARGIN, FOOTER_Y + 14); | ||
| if (isAnniversary) { | ||
| ctx.fillStyle = "#ffd700"; | ||
| ctx.font = `bold 11px Rajdhani, sans-serif`; | ||
| ctx.textAlign = "center"; | ||
| ctx.fillStyle = "#ffd700"; | ||
| ctx.font = "bold 10px sans-serif"; | ||
| ctx.fillText("~ GitHub Anniversary! ~", CARD_WIDTH / 2, FOOTER_Y + 14); | ||
| ctx.fillText("~ GITHUB ANNIVERSARY ~", CARD_WIDTH / 2, FOOTER_Y + 14); | ||
| } | ||
| ctx.fillStyle = theme.footerText; | ||
| ctx.font = `500 11px SpaceMono, monospace`; | ||
| ctx.textAlign = "right"; | ||
| ctx.fillStyle = theme.footerText; | ||
| ctx.font = "10px monospace"; | ||
| ctx.fillText(`#${cardNumber} \u2022 devcard`, CARD_WIDTH - MARGIN, FOOTER_Y + 14); | ||
| ctx.fillText(`#${cardNumber}`, CARD_WIDTH - MARGIN, FOOTER_Y + 14); | ||
| ctx.restore(); | ||
| } | ||
| // src/utils/assetPath.ts | ||
| import { fileURLToPath } from "url"; | ||
| import path from "path"; | ||
| var __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
| function assetPath(relative) { | ||
| return path.join(__dirname, "..", "..", "assets", relative); | ||
| } | ||
| // src/card/renderer.ts | ||
@@ -1082,2 +1296,3 @@ async function renderCard(data, opts2 = {}) { | ||
| const ctx = canvas.getContext("2d"); | ||
| registerFonts(); | ||
| const fallbackAvatar = assetPath("fallback-avatar.png"); | ||
@@ -1084,0 +1299,0 @@ drawFrame(ctx, theme, mapped.rarity); |
+1
-1
| { | ||
| "name": "@mightbeandrew/devcard", | ||
| "version": "1.0.2", | ||
| "version": "1.1.0", | ||
| "description": "Generate a beautiful developer trading card from your GitHub profile", | ||
@@ -5,0 +5,0 @@ "type": "module", |
1467361
2561.01%10
150%1521
16.28%