diff --git a/README.md b/README.md index f758ce3..ff4e9a8 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,13 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme= canvasstringlightBackground color of avatar canvas: light / darkdark shapestringcircleAvatar shape: circle or roundedrounded radiusinteger25Corner radius for rounded shape in px50 +textstringoptionalCustom text to display on avatarHello World +textColorstring#ffffffColor of text in HEX format#ff0000 +textSizeinteger20Size of text in pixels (8-100)24 +textPositionstringbottomPosition of text: top / bottom / centertop +emojisstringoptionalComma-separated list of emojis🚀,💻,🔥 +emojiSizeinteger40Size of emojis in pixels (16-120)30 +emojiPositionstringtopPosition of emojis: top / bottom / cornerscorners @@ -95,6 +102,25 @@ https://github-avatar-frame-api.onrender.com/api/framed-avatar/{username}?theme=

Combine all three to customize your avatar:

+ +

Text & Emoji Overlays

+ +

Add personalized text and emojis to make your avatar more expressive:

+ + + +

Example with text and emojis:

+
+https://github-avatar-frame-api.onrender.com/api/framed-avatar/octocat?theme=base&text=GitHub%20User&textColor=%23ffffff&textSize=20&textPosition=bottom&emojis=%F0%9F%9A%80%2C%F0%9F%92%BB%2C%F0%9F%94%A5&emojiSize=30&emojiPosition=top
+
diff --git a/api/server.ts b/api/server.ts index bf3ec90..a25483a 100644 --- a/api/server.ts +++ b/api/server.ts @@ -22,6 +22,100 @@ const ASSET_BASE_PATH = path.join(__dirname, ".."); //serve static files app.use(express.static(path.join(ASSET_BASE_PATH,"public"))); + +// Helper function to create text overlay +async function createTextOverlay(text: string, textColor: string, textSize: number, textPosition: string, canvasSize: number): Promise { + if (!text || text.trim() === "") return null; + + // Create SVG for text overlay + const svg = ` + + + + + + + + ${text} + + + `; + + return Buffer.from(svg); +} + +// Helper function to create emoji overlay +async function createEmojiOverlay(emojis: string, emojiSize: number, emojiPosition: string, canvasSize: number): Promise { + if (!emojis || emojis.trim() === "") return null; + + const emojiList = emojis.split(',').map(e => e.trim()).filter(e => e.length > 0); + if (emojiList.length === 0) return null; + + let emojiElements = ''; + const spacing = emojiSize + 10; + + if (emojiPosition === 'corners' && emojiList.length >= 4) { + // Place emojis in corners + const positions = [ + { x: emojiSize/2 + 5, y: emojiSize/2 + 5 }, // top-left + { x: canvasSize - emojiSize/2 - 5, y: emojiSize/2 + 5 }, // top-right + { x: emojiSize/2 + 5, y: canvasSize - emojiSize/2 - 5 }, // bottom-left + { x: canvasSize - emojiSize/2 - 5, y: canvasSize - emojiSize/2 - 5 } // bottom-right + ]; + + emojiList.slice(0, 4).forEach((emoji, index) => { + emojiElements += ` + + ${emoji} + + `; + }); + } else { + // Place emojis in a row at top or bottom + const y = emojiPosition === 'top' ? emojiSize + 5 : canvasSize - 5; + const totalWidth = emojiList.length * spacing; + const startX = (canvasSize - totalWidth) / 2 + emojiSize / 2; + + emojiList.forEach((emoji, index) => { + const x = startX + index * spacing; + emojiElements += ` + + ${emoji} + + `; + }); + } + + const svg = ` + + ${emojiElements} + + `; + + return Buffer.from(svg); +} /** * GET /api/framed-avatar/:username * Example: /api/framed-avatar/octocat?theme=base&size=256&accentColor=%23ff6b6b @@ -33,6 +127,13 @@ app.use(express.static(path.join(ASSET_BASE_PATH,"public"))); * - radius: Corner radius for rounded/rect shapes * - canvas: "light", "dark", or "transparent" (default: "light") * - accentColor: Custom color for frame tinting (hex format, e.g., "#ff6b6b") + * - text: Custom text to display (e.g., "Darshan Parmar") + * - textColor: Color of text in HEX or keyword (default: #ffffff) + * - textSize: Size of text in pixels (default: 20) + * - textPosition: Position of text — top | bottom | center (default: bottom) + * - emojis: Comma-separated list of emojis (e.g., 🚀,💻,🔥) + * - emojiSize: Size of emojis in pixels (default: 40) + * - emojiPosition: Position of emojis — top | bottom | corners (default: top) */ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => { try { @@ -43,6 +144,17 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => { const radiusStr = req.query.radius as string | undefined; const canvasParam = (req.query.canvas as string)?.toLowerCase() || "light"; // "dark" or "light" const accentColor = req.query.accentColor as string | undefined; + + // Text overlay parameters + const text = req.query.text as string | undefined; + const textColor = (req.query.textColor as string) || "#ffffff"; + const textSizeStr = (req.query.textSize as string) || "20"; + const textPosition = ((req.query.textPosition as string) || "bottom").toLowerCase(); + + // Emoji overlay parameters + const emojis = req.query.emojis as string | undefined; + const emojiSizeStr = (req.query.emojiSize as string) || "40"; + const emojiPosition = ((req.query.emojiPosition as string) || "top").toLowerCase(); // Validate username if (!username || typeof username !== "string" || username.trim() === "") { @@ -76,6 +188,24 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => { }); } + // Validate text parameters + const textSize = Math.max(8, Math.min(parseInt(textSizeStr, 10), 100)); + if (!["top", "bottom", "center"].includes(textPosition)) { + return res.status(400).json({ + error: "Bad Request", + message: "textPosition must be 'top', 'bottom', or 'center'.", + }); + } + + // Validate emoji parameters + const emojiSize = Math.max(16, Math.min(parseInt(emojiSizeStr, 10), 120)); + if (!["top", "bottom", "corners"].includes(emojiPosition)) { + return res.status(400).json({ + error: "Bad Request", + message: "emojiPosition must be 'top', 'bottom', or 'corners'.", + }); + } + // determine corner radius let cornerRadius: number; if (shape === "circle") cornerRadius = Math.floor(size / 2); @@ -194,6 +324,26 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => { .png() .toBuffer(); + // Create text and emoji overlays + const textOverlay = text ? await createTextOverlay(text, textColor, textSize, textPosition, size) : null; + const emojiOverlay = emojis ? await createEmojiOverlay(emojis, emojiSize, emojiPosition, size) : null; + + // Build composite layers + const compositeLayers = [ + { input: avatarMasked, gravity: "center" }, + { input: paddedFrame, gravity: "center" }, + ]; + + // Add text overlay if provided + if (textOverlay) { + compositeLayers.push({ input: textOverlay, gravity: "center" }); + } + + // Add emoji overlay if provided + if (emojiOverlay) { + compositeLayers.push({ input: emojiOverlay, gravity: "center" }); + } + // Compose final image on custom canvas color const finalImage = await sharp({ create: { @@ -203,10 +353,7 @@ app.get("/api/framed-avatar/:username", async (req: Request, res: Response) => { background: canvasColor, }, }) - .composite([ - { input: avatarMasked, gravity: "center" }, - { input: paddedFrame, gravity: "center" }, - ]) + .composite(compositeLayers) .png() .toBuffer(); diff --git a/client/src/App.jsx b/client/src/App.jsx index e1b23be..2296c68 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -199,6 +199,17 @@ function App() { const [shape, setShape] = useState("circle"); const [radius, setRadius] = useState(38); const [frameStyle, setFrameStyle] = useState("default"); + + // Text overlay parameters + const [text, setText] = useState(""); + const [textColor, setTextColor] = useState("#ffffff"); + const [textSize, setTextSize] = useState(20); + const [textPosition, setTextPosition] = useState("bottom"); + + // Emoji overlay parameters + const [emojis, setEmojis] = useState(""); + const [emojiSize, setEmojiSize] = useState(40); + const [emojiPosition, setEmojiPosition] = useState("top"); const [loading, setLoading] = useState(false); const [themesLoading, setThemesLoading] = useState(true); @@ -319,6 +330,16 @@ function App() { apiUrl += `&accentColor=${encodeURIComponent(customAccentColor)}`; } + // Add text parameters if provided + if (text.trim()) { + apiUrl += `&text=${encodeURIComponent(text)}&textColor=${encodeURIComponent(textColor)}&textSize=${textSize}&textPosition=${textPosition}`; + } + + // Add emoji parameters if provided + if (emojis.trim()) { + apiUrl += `&emojis=${encodeURIComponent(emojis)}&emojiSize=${emojiSize}&emojiPosition=${emojiPosition}`; + } + try { // Use document.execCommand('copy') for better compatibility in iframe environments const tempInput = document.createElement("textarea"); @@ -355,6 +376,16 @@ function App() { if (customAccentColor) { url += `&accentColor=${encodeURIComponent(customAccentColor)}`; } + + // Add text parameters if provided + if (text.trim()) { + url += `&text=${encodeURIComponent(text)}&textColor=${encodeURIComponent(textColor)}&textSize=${textSize}&textPosition=${textPosition}`; + } + + // Add emoji parameters if provided + if (emojis.trim()) { + url += `&emojis=${encodeURIComponent(emojis)}&emojiSize=${emojiSize}&emojiPosition=${emojiPosition}`; + } // Create AbortController for timeout const controller = new AbortController(); @@ -1158,6 +1189,182 @@ function App() { )} + {/* Text Overlay Controls */} +
+ +
+ setText(e.target.value)} + style={{ + flex: 1, + padding: "10px 12px", + borderRadius: "8px", + border: `2px solid ${colors.border}`, + background: colors.bgInput, + color: colors.textPrimary, + fontSize: "14px", + outline: "none", + transition: "border-color 0.2s", + }} + /> + setTextColor(e.target.value)} + style={{ + width: "50px", + height: "40px", + borderRadius: "8px", + border: `2px solid ${colors.border}`, + cursor: "pointer", + }} + /> +
+
+
+ + setTextSize(parseInt(e.target.value))} + className="range-slider" + style={{ + width: "100%", + height: "6px", + borderRadius: "3px", + background: isDark ? "#374151" : "linear-gradient(to right, #a78bfa, #c4b5fd)", + outline: "none", + cursor: "pointer", + WebkitAppearance: "none", + }} + /> +
+
+ +
+ setTextPosition("top")} + isSelected={textPosition === "top"} + isDark={isDark}> + Top + + setTextPosition("center")} + isSelected={textPosition === "center"} + isDark={isDark}> + Center + + setTextPosition("bottom")} + isSelected={textPosition === "bottom"} + isDark={isDark}> + Bottom + +
+
+
+
+ + {/* Emoji Overlay Controls */} +
+ +
+ setEmojis(e.target.value)} + style={{ + flex: 1, + padding: "10px 12px", + borderRadius: "8px", + border: `2px solid ${colors.border}`, + background: colors.bgInput, + color: colors.textPrimary, + fontSize: "14px", + outline: "none", + transition: "border-color 0.2s", + }} + /> +
+
+
+ + setEmojiSize(parseInt(e.target.value))} + className="range-slider" + style={{ + width: "100%", + height: "6px", + borderRadius: "3px", + background: isDark ? "#374151" : "linear-gradient(to right, #a78bfa, #c4b5fd)", + outline: "none", + cursor: "pointer", + WebkitAppearance: "none", + }} + /> +
+
+ +
+ setEmojiPosition("top")} + isSelected={emojiPosition === "top"} + isDark={isDark}> + Top + + setEmojiPosition("bottom")} + isSelected={emojiPosition === "bottom"} + isDark={isDark}> + Bottom + + setEmojiPosition("corners")} + isSelected={emojiPosition === "corners"} + isDark={isDark}> + Corners + +
+
+
+
+ {/* Generate Button */}