Skip to content

Commit b3c15b6

Browse files
committed
feat: new github thumbnails
1 parent b836beb commit b3c15b6

File tree

3 files changed

+357
-0
lines changed

3 files changed

+357
-0
lines changed

src/pages/apps/github-thumbnail/GithubThumbnailGeneratorPage.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const THEME_OPTIONS = [
6262
{ value: 'topographic', label: 'TOPOGRAPHIC_SURVEY' },
6363
{ value: 'starChart', label: 'STELLAR_CHART' },
6464
{ value: 'sonarPing', label: 'SONAR_PING' },
65+
{ value: 'macosGlass', label: 'MACOS_GLASS' },
6566
];
6667

6768
const GithubThumbnailGeneratorPage = () => {

src/pages/apps/github-thumbnail/themes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { postModern } from './themes/postModern';
4242
import { topographic } from './themes/topographic';
4343
import { starChart } from './themes/starChart';
4444
import { sonarPing } from './themes/sonarPing';
45+
import { macosGlass } from './themes/macosGlass';
4546

4647
export const themeRenderers = {
4748
modern,
@@ -88,4 +89,5 @@ export const themeRenderers = {
8889
topographic,
8990
starChart,
9091
sonarPing,
92+
macosGlass,
9193
};
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import { wrapText } from '../utils';
2+
3+
// Helper to draw a rounded rect path without filling/stroking
4+
const roundRectPath = (ctx, x, y, w, h, r) => {
5+
ctx.beginPath();
6+
ctx.roundRect(x, y, w, h, r);
7+
};
8+
9+
export const macosGlass = (ctx, width, height, scale, data) => {
10+
const {
11+
primaryColor,
12+
secondaryColor,
13+
bgColor,
14+
showPattern,
15+
repoOwner,
16+
repoName,
17+
description,
18+
language,
19+
stars,
20+
forks,
21+
supportUrl,
22+
} = data;
23+
24+
// --- Wallpaper background ---
25+
// Soft gradient mesh inspired by macOS Sequoia wallpaper
26+
ctx.save();
27+
ctx.fillStyle = bgColor;
28+
ctx.fillRect(0, 0, width, height);
29+
30+
// Large blurred color blobs
31+
ctx.filter = 'blur(120px)';
32+
ctx.globalAlpha = 0.5;
33+
34+
ctx.fillStyle = primaryColor;
35+
ctx.beginPath();
36+
ctx.ellipse(width * 0.2, height * 0.3, 400 * scale, 300 * scale, 0.3, 0, Math.PI * 2);
37+
ctx.fill();
38+
39+
ctx.fillStyle = secondaryColor;
40+
ctx.beginPath();
41+
ctx.ellipse(width * 0.75, height * 0.25, 350 * scale, 250 * scale, -0.2, 0, Math.PI * 2);
42+
ctx.fill();
43+
44+
ctx.fillStyle = primaryColor;
45+
ctx.globalAlpha = 0.35;
46+
ctx.beginPath();
47+
ctx.ellipse(width * 0.5, height * 0.85, 450 * scale, 200 * scale, 0, 0, Math.PI * 2);
48+
ctx.fill();
49+
50+
ctx.fillStyle = secondaryColor;
51+
ctx.globalAlpha = 0.25;
52+
ctx.beginPath();
53+
ctx.ellipse(width * 0.85, height * 0.7, 300 * scale, 250 * scale, 0.5, 0, Math.PI * 2);
54+
ctx.fill();
55+
56+
ctx.restore();
57+
58+
// --- Subtle grid pattern (showPattern) ---
59+
if (showPattern) {
60+
ctx.save();
61+
ctx.globalAlpha = 0.03;
62+
ctx.strokeStyle = '#ffffff';
63+
ctx.lineWidth = 1 * scale;
64+
const gridSize = 50 * scale;
65+
for (let x = 0; x < width; x += gridSize) {
66+
ctx.beginPath();
67+
ctx.moveTo(x, 0);
68+
ctx.lineTo(x, height);
69+
ctx.stroke();
70+
}
71+
for (let y = 0; y < height; y += gridSize) {
72+
ctx.beginPath();
73+
ctx.moveTo(0, y);
74+
ctx.lineTo(width, y);
75+
ctx.stroke();
76+
}
77+
ctx.restore();
78+
}
79+
80+
// --- Main glass window ---
81+
const winW = width * 0.62;
82+
const winH = height * 0.72;
83+
const winX = (width - winW) / 2;
84+
const winY = (height - winH) / 2 + 10 * scale;
85+
const winR = 16 * scale;
86+
87+
// Window shadow
88+
ctx.save();
89+
ctx.filter = 'blur(40px)';
90+
ctx.fillStyle = 'rgba(0,0,0,0.35)';
91+
roundRectPath(ctx, winX + 4 * scale, winY + 8 * scale, winW, winH, winR);
92+
ctx.fill();
93+
ctx.restore();
94+
95+
// Glass fill (frosted translucent)
96+
ctx.save();
97+
ctx.fillStyle = 'rgba(255,255,255,0.12)';
98+
roundRectPath(ctx, winX, winY, winW, winH, winR);
99+
ctx.fill();
100+
ctx.restore();
101+
102+
// Glass inner highlight (top edge glow)
103+
ctx.save();
104+
const highlightGrad = ctx.createLinearGradient(0, winY, 0, winY + 60 * scale);
105+
highlightGrad.addColorStop(0, 'rgba(255,255,255,0.15)');
106+
highlightGrad.addColorStop(1, 'rgba(255,255,255,0)');
107+
ctx.fillStyle = highlightGrad;
108+
roundRectPath(ctx, winX, winY, winW, 60 * scale, [winR, winR, 0, 0]);
109+
ctx.fill();
110+
ctx.restore();
111+
112+
// Glass border
113+
ctx.save();
114+
ctx.strokeStyle = 'rgba(255,255,255,0.18)';
115+
ctx.lineWidth = 1.5 * scale;
116+
roundRectPath(ctx, winX, winY, winW, winH, winR);
117+
ctx.stroke();
118+
ctx.restore();
119+
120+
// --- Title bar ---
121+
const titleBarH = 52 * scale;
122+
const tbY = winY;
123+
124+
// Title bar separator
125+
ctx.save();
126+
ctx.fillStyle = 'rgba(255,255,255,0.06)';
127+
ctx.fillRect(winX, tbY + titleBarH, winW, 1 * scale);
128+
ctx.restore();
129+
130+
// Traffic lights
131+
const tlY = tbY + titleBarH / 2;
132+
const tlStartX = winX + 22 * scale;
133+
const tlR = 7 * scale;
134+
const tlGap = 22 * scale;
135+
136+
const trafficColors = ['#ff5f57', '#febc2e', '#28c840'];
137+
trafficColors.forEach((color, i) => {
138+
const tx = tlStartX + i * tlGap;
139+
ctx.save();
140+
ctx.fillStyle = color;
141+
ctx.beginPath();
142+
ctx.arc(tx, tlY, tlR, 0, Math.PI * 2);
143+
ctx.fill();
144+
145+
// Subtle inner shadow
146+
ctx.globalAlpha = 0.3;
147+
ctx.fillStyle = 'rgba(0,0,0,0.15)';
148+
ctx.beginPath();
149+
ctx.arc(tx, tlY + 1 * scale, tlR - 1 * scale, 0, Math.PI * 2);
150+
ctx.fill();
151+
ctx.restore();
152+
});
153+
154+
// Window title (repo name in title bar)
155+
ctx.save();
156+
ctx.textAlign = 'center';
157+
ctx.fillStyle = 'rgba(255,255,255,0.5)';
158+
ctx.font = `500 ${14 * scale}px -apple-system, "SF Pro Text", "Inter", sans-serif`;
159+
ctx.fillText(`${repoOwner}/${repoName}`, winX + winW / 2, tlY + 5 * scale);
160+
ctx.restore();
161+
162+
// --- Window content ---
163+
const contentX = winX + 36 * scale;
164+
const contentY = tbY + titleBarH + 30 * scale;
165+
const contentW = winW - 72 * scale;
166+
167+
// Owner
168+
ctx.save();
169+
ctx.textAlign = 'left';
170+
ctx.fillStyle = secondaryColor;
171+
ctx.globalAlpha = 0.7;
172+
ctx.font = `500 ${16 * scale}px "JetBrains Mono", monospace`;
173+
ctx.fillText(repoOwner, contentX, contentY);
174+
ctx.restore();
175+
176+
// Repo name — large
177+
ctx.save();
178+
ctx.fillStyle = '#ffffff';
179+
ctx.font = `700 ${52 * scale}px -apple-system, "SF Pro Display", "Inter", sans-serif`;
180+
let fontSize = 52;
181+
while (ctx.measureText(repoName).width > contentW && fontSize > 28) {
182+
fontSize -= 2;
183+
ctx.font = `700 ${fontSize * scale}px -apple-system, "SF Pro Display", "Inter", sans-serif`;
184+
}
185+
ctx.fillText(repoName, contentX, contentY + 52 * scale);
186+
ctx.restore();
187+
188+
// Description
189+
ctx.save();
190+
ctx.fillStyle = 'rgba(255,255,255,0.55)';
191+
ctx.font = `400 ${18 * scale}px -apple-system, "SF Pro Text", "Inter", sans-serif`;
192+
wrapText(ctx, description, contentX, contentY + 86 * scale, contentW, 26 * scale);
193+
ctx.restore();
194+
195+
// --- Pill badges row ---
196+
const badgeY = contentY + 165 * scale;
197+
let badgeX = contentX;
198+
199+
const drawGlassPill = (text, color) => {
200+
ctx.save();
201+
ctx.font = `600 ${13 * scale}px -apple-system, "SF Pro Text", "Inter", sans-serif`;
202+
const tw = ctx.measureText(text).width;
203+
const dotSpace = 22 * scale; // space for the colored dot + gap
204+
const hPad = 14 * scale;
205+
const pillW = dotSpace + tw + hPad * 2;
206+
const pillH = 34 * scale;
207+
const pillR = pillH / 2;
208+
209+
// Pill glass bg
210+
ctx.fillStyle = 'rgba(255,255,255,0.08)';
211+
roundRectPath(ctx, badgeX, badgeY, pillW, pillH, pillR);
212+
ctx.fill();
213+
214+
// Pill border
215+
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
216+
ctx.lineWidth = 1 * scale;
217+
roundRectPath(ctx, badgeX, badgeY, pillW, pillH, pillR);
218+
ctx.stroke();
219+
220+
// Colored dot
221+
ctx.fillStyle = color;
222+
ctx.beginPath();
223+
ctx.arc(badgeX + hPad + 4 * scale, badgeY + pillH / 2, 4 * scale, 0, Math.PI * 2);
224+
ctx.fill();
225+
226+
// Text (vertically centered using textBaseline)
227+
ctx.textBaseline = 'middle';
228+
ctx.fillStyle = 'rgba(255,255,255,0.7)';
229+
ctx.fillText(text, badgeX + hPad + dotSpace, badgeY + pillH / 2);
230+
231+
ctx.restore();
232+
badgeX += pillW + 10 * scale;
233+
};
234+
235+
drawGlassPill(language, primaryColor);
236+
if (stars) drawGlassPill(`★ ${stars}`, '#febc2e');
237+
if (forks) drawGlassPill(`⑂ ${forks}`, secondaryColor);
238+
239+
// --- Color palette dots (bottom of window) ---
240+
const paletteY = winY + winH - 44 * scale;
241+
let dotX = contentX;
242+
243+
ctx.save();
244+
ctx.fillStyle = 'rgba(255,255,255,0.2)';
245+
ctx.font = `500 ${10 * scale}px "JetBrains Mono", monospace`;
246+
ctx.textAlign = 'left';
247+
ctx.fillText('THEME', dotX, paletteY - 2 * scale);
248+
dotX += 55 * scale;
249+
250+
[bgColor, primaryColor, secondaryColor].forEach((color) => {
251+
// Dot border
252+
ctx.save();
253+
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
254+
ctx.lineWidth = 1.5 * scale;
255+
ctx.beginPath();
256+
ctx.arc(dotX, paletteY - 6 * scale, 8 * scale, 0, Math.PI * 2);
257+
ctx.stroke();
258+
ctx.fillStyle = color;
259+
ctx.fill();
260+
ctx.restore();
261+
dotX += 26 * scale;
262+
});
263+
264+
// Pattern status
265+
dotX += 16 * scale;
266+
ctx.fillStyle = showPattern ? primaryColor : 'rgba(255,255,255,0.15)';
267+
ctx.font = `500 ${10 * scale}px "JetBrains Mono", monospace`;
268+
ctx.fillText(showPattern ? '● Grid' : '○ Grid', dotX, paletteY - 2 * scale);
269+
ctx.restore();
270+
271+
// Support URL (bottom right of window)
272+
if (supportUrl) {
273+
ctx.save();
274+
ctx.textAlign = 'right';
275+
ctx.fillStyle = 'rgba(255,255,255,0.2)';
276+
ctx.font = `400 ${12 * scale}px -apple-system, "SF Pro Text", "Inter", sans-serif`;
277+
ctx.fillText(supportUrl, winX + winW - 36 * scale, paletteY - 2 * scale);
278+
ctx.restore();
279+
}
280+
281+
// --- Floating mini widget (top-right, outside window) ---
282+
const widgetW = 160 * scale;
283+
const widgetH = 70 * scale;
284+
const widgetX = winX + winW + 20 * scale;
285+
const widgetY = winY + 30 * scale;
286+
287+
// Only draw if it fits
288+
if (widgetX + widgetW < width - 20 * scale) {
289+
// Widget shadow
290+
ctx.save();
291+
ctx.filter = 'blur(20px)';
292+
ctx.fillStyle = 'rgba(0,0,0,0.25)';
293+
roundRectPath(ctx, widgetX + 2 * scale, widgetY + 4 * scale, widgetW, widgetH, 12 * scale);
294+
ctx.fill();
295+
ctx.restore();
296+
297+
// Widget glass
298+
ctx.save();
299+
ctx.fillStyle = 'rgba(255,255,255,0.1)';
300+
roundRectPath(ctx, widgetX, widgetY, widgetW, widgetH, 12 * scale);
301+
ctx.fill();
302+
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
303+
ctx.lineWidth = 1 * scale;
304+
roundRectPath(ctx, widgetX, widgetY, widgetW, widgetH, 12 * scale);
305+
ctx.stroke();
306+
ctx.restore();
307+
308+
// Widget content — stars count
309+
ctx.save();
310+
ctx.textAlign = 'left';
311+
ctx.fillStyle = 'rgba(255,255,255,0.35)';
312+
ctx.font = `500 ${10 * scale}px -apple-system, "SF Pro Text", "Inter", sans-serif`;
313+
ctx.fillText('Stargazers', widgetX + 16 * scale, widgetY + 22 * scale);
314+
ctx.fillStyle = '#ffffff';
315+
ctx.font = `700 ${26 * scale}px -apple-system, "SF Pro Display", "Inter", sans-serif`;
316+
ctx.fillText(stars || '0', widgetX + 16 * scale, widgetY + 52 * scale);
317+
ctx.restore();
318+
}
319+
320+
// --- Floating mini widget (bottom-left, outside window) ---
321+
const widget2W = 140 * scale;
322+
const widget2H = 60 * scale;
323+
const widget2X = winX - widget2W - 20 * scale;
324+
const widget2Y = winY + winH - widget2H - 40 * scale;
325+
326+
if (widget2X > 20 * scale) {
327+
ctx.save();
328+
ctx.filter = 'blur(20px)';
329+
ctx.fillStyle = 'rgba(0,0,0,0.25)';
330+
roundRectPath(ctx, widget2X + 2 * scale, widget2Y + 4 * scale, widget2W, widget2H, 12 * scale);
331+
ctx.fill();
332+
ctx.restore();
333+
334+
ctx.save();
335+
ctx.fillStyle = 'rgba(255,255,255,0.1)';
336+
roundRectPath(ctx, widget2X, widget2Y, widget2W, widget2H, 12 * scale);
337+
ctx.fill();
338+
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
339+
ctx.lineWidth = 1 * scale;
340+
roundRectPath(ctx, widget2X, widget2Y, widget2W, widget2H, 12 * scale);
341+
ctx.stroke();
342+
ctx.restore();
343+
344+
ctx.save();
345+
ctx.textAlign = 'left';
346+
ctx.fillStyle = 'rgba(255,255,255,0.35)';
347+
ctx.font = `500 ${10 * scale}px -apple-system, "SF Pro Text", "Inter", sans-serif`;
348+
ctx.fillText('Language', widget2X + 14 * scale, widget2Y + 22 * scale);
349+
ctx.fillStyle = primaryColor;
350+
ctx.font = `700 ${20 * scale}px -apple-system, "SF Pro Display", "Inter", sans-serif`;
351+
ctx.fillText(language, widget2X + 14 * scale, widget2Y + 46 * scale);
352+
ctx.restore();
353+
}
354+
};

0 commit comments

Comments
 (0)