Skip to content

Commit aad6750

Browse files
committed
feat: new github thumbnails
1 parent 4f8cf2a commit aad6750

File tree

3 files changed

+226
-0
lines changed

3 files changed

+226
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const THEME_OPTIONS = [
5757
{ value: 'tacticalMap', label: 'TACTICAL_MAP' },
5858
{ value: 'modernEdge', label: 'MODERN_EDGE' },
5959
{ value: 'auroraWave', label: 'AURORA_WAVE' },
60+
{ value: 'newspaper', label: 'NEWSPRINT_HERALD' },
6061
];
6162

6263
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { darkMedieval } from './themes/darkMedieval';
3737
import { tacticalMap } from './themes/tacticalMap';
3838
import { modernEdge } from './themes/modernEdge';
3939
import { auroraWave } from './themes/auroraWave';
40+
import { newspaper } from './themes/newspaper';
4041

4142
export const themeRenderers = {
4243
modern,
@@ -78,4 +79,5 @@ export const themeRenderers = {
7879
tacticalMap,
7980
modernEdge,
8081
auroraWave,
82+
newspaper,
8183
};
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { wrapText } from '../utils';
2+
3+
export const newspaper = (ctx, width, height, scale, data) => {
4+
const {
5+
primaryColor,
6+
secondaryColor,
7+
bgColor,
8+
showPattern,
9+
repoOwner,
10+
repoName,
11+
description,
12+
language,
13+
stars,
14+
forks,
15+
supportUrl,
16+
} = data;
17+
18+
// --- Paper background override ---
19+
ctx.save();
20+
ctx.fillStyle = '#f5f0e8';
21+
ctx.fillRect(0, 0, width, height);
22+
ctx.restore();
23+
24+
// --- Aged paper noise texture (when showPattern is on) ---
25+
if (showPattern) {
26+
ctx.save();
27+
ctx.globalAlpha = 0.03;
28+
ctx.fillStyle = bgColor;
29+
const seed = 137;
30+
for (let i = 0; i < 1200; i++) {
31+
const px = (seed * (i + 1) * 4919) % width;
32+
const py = (seed * (i + 1) * 7127) % height;
33+
const r = ((i % 4) + 0.5) * scale;
34+
ctx.beginPath();
35+
ctx.arc(px, py, r, 0, Math.PI * 2);
36+
ctx.fill();
37+
}
38+
ctx.restore();
39+
}
40+
41+
const padding = 80 * scale;
42+
const inkColor = '#1a1a1a';
43+
const mutedInk = '#4a4540';
44+
const ruleColor = '#c8c0b0';
45+
46+
// --- Top masthead rule (double line) ---
47+
ctx.fillStyle = inkColor;
48+
ctx.fillRect(padding, padding - 10 * scale, width - padding * 2, 4 * scale);
49+
ctx.fillRect(padding, padding - 2 * scale, width - padding * 2, 1 * scale);
50+
51+
// --- Masthead: edition info ---
52+
ctx.textAlign = 'left';
53+
ctx.fillStyle = mutedInk;
54+
ctx.font = `italic 400 ${14 * scale}px Georgia, "Times New Roman", serif`;
55+
ctx.fillText('The Open Source Chronicle', padding, padding + 20 * scale);
56+
57+
ctx.textAlign = 'right';
58+
ctx.font = `400 ${14 * scale}px Georgia, "Times New Roman", serif`;
59+
ctx.fillText(`est. ${repoOwner}`, width - padding, padding + 20 * scale);
60+
61+
// --- Thin rule below masthead ---
62+
ctx.fillStyle = ruleColor;
63+
ctx.fillRect(padding, padding + 32 * scale, width - padding * 2, 1 * scale);
64+
65+
// --- Headline (repo name) ---
66+
ctx.textAlign = 'center';
67+
ctx.fillStyle = inkColor;
68+
ctx.font = `900 ${80 * scale}px Georgia, "Times New Roman", serif`;
69+
const headlineY = padding + 120 * scale;
70+
71+
// Measure and potentially scale down for long names
72+
const headlineMaxW = width - padding * 2;
73+
let fontSize = 80;
74+
ctx.font = `900 ${fontSize * scale}px Georgia, "Times New Roman", serif`;
75+
while (ctx.measureText(repoName).width > headlineMaxW && fontSize > 40) {
76+
fontSize -= 4;
77+
ctx.font = `900 ${fontSize * scale}px Georgia, "Times New Roman", serif`;
78+
}
79+
ctx.fillText(repoName, width / 2, headlineY);
80+
81+
// --- Thin rule below headline ---
82+
ctx.fillStyle = ruleColor;
83+
ctx.fillRect(padding, headlineY + 16 * scale, width - padding * 2, 1 * scale);
84+
85+
// --- Subheadline / owner byline ---
86+
ctx.textAlign = 'center';
87+
ctx.fillStyle = mutedInk;
88+
ctx.font = `italic 400 ${22 * scale}px Georgia, "Times New Roman", serif`;
89+
ctx.fillText(`By ${repoOwner} · Written in ${language}`, width / 2, headlineY + 48 * scale);
90+
91+
// --- Column divider (vertical rule in the middle) ---
92+
const columnDivX = width * 0.52;
93+
const bodyTop = headlineY + 72 * scale;
94+
const bodyBottom = height - padding - 40 * scale;
95+
ctx.fillStyle = ruleColor;
96+
ctx.fillRect(columnDivX, bodyTop, 1 * scale, bodyBottom - bodyTop);
97+
98+
// --- Left column: description as article body ---
99+
ctx.textAlign = 'left';
100+
ctx.fillStyle = '#2a2520';
101+
ctx.font = `400 ${20 * scale}px Georgia, "Times New Roman", serif`;
102+
const leftColX = padding;
103+
const leftColW = columnDivX - padding - 30 * scale;
104+
105+
// Drop cap
106+
if (description.length > 0) {
107+
const dropChar = description[0].toUpperCase();
108+
const restText = description.slice(1);
109+
110+
ctx.fillStyle = primaryColor;
111+
ctx.font = `700 ${64 * scale}px Georgia, "Times New Roman", serif`;
112+
ctx.fillText(dropChar, leftColX, bodyTop + 48 * scale);
113+
const dropW = ctx.measureText(dropChar).width + 6 * scale;
114+
115+
// First line after drop cap
116+
ctx.fillStyle = '#2a2520';
117+
ctx.font = `400 ${20 * scale}px Georgia, "Times New Roman", serif`;
118+
wrapText(ctx, restText, leftColX + dropW, bodyTop + 24 * scale, leftColW - dropW, 28 * scale);
119+
120+
// Continue wrapping below
121+
ctx.fillStyle = '#2a2520';
122+
wrapText(ctx, restText, leftColX, bodyTop + 80 * scale, leftColW, 28 * scale);
123+
}
124+
125+
// --- Right column: stats & info panel ---
126+
const rightColX = columnDivX + 26 * scale;
127+
let rightY = bodyTop + 16 * scale;
128+
129+
// "By the Numbers" header
130+
ctx.fillStyle = inkColor;
131+
ctx.font = `700 ${14 * scale}px Georgia, "Times New Roman", serif`;
132+
ctx.textAlign = 'left';
133+
ctx.fillText('BY THE NUMBERS', rightColX, rightY);
134+
rightY += 8 * scale;
135+
136+
ctx.fillStyle = ruleColor;
137+
ctx.fillRect(rightColX, rightY, 160 * scale, 1 * scale);
138+
rightY += 28 * scale;
139+
140+
// Stars stat
141+
if (stars) {
142+
ctx.fillStyle = primaryColor;
143+
ctx.font = `900 ${44 * scale}px Georgia, "Times New Roman", serif`;
144+
ctx.fillText(stars, rightColX, rightY + 10 * scale);
145+
146+
ctx.fillStyle = mutedInk;
147+
ctx.font = `italic 400 ${16 * scale}px Georgia, "Times New Roman", serif`;
148+
ctx.fillText('stargazers', rightColX + ctx.measureText(stars).width + 14 * scale, rightY + 10 * scale);
149+
150+
// Re-measure stars width for proper positioning
151+
ctx.font = `900 ${44 * scale}px Georgia, "Times New Roman", serif`;
152+
rightY += 50 * scale;
153+
}
154+
155+
// Forks stat
156+
if (forks) {
157+
ctx.fillStyle = secondaryColor;
158+
ctx.font = `900 ${44 * scale}px Georgia, "Times New Roman", serif`;
159+
ctx.fillText(forks, rightColX, rightY + 10 * scale);
160+
161+
ctx.fillStyle = mutedInk;
162+
ctx.font = `italic 400 ${16 * scale}px Georgia, "Times New Roman", serif`;
163+
ctx.fillText('forks', rightColX + ctx.measureText(forks).width + 14 * scale, rightY + 10 * scale);
164+
165+
ctx.font = `900 ${44 * scale}px Georgia, "Times New Roman", serif`;
166+
rightY += 50 * scale;
167+
}
168+
169+
// Thin rule
170+
ctx.fillStyle = ruleColor;
171+
ctx.fillRect(rightColX, rightY, 160 * scale, 1 * scale);
172+
rightY += 24 * scale;
173+
174+
// Language badge - styled as a classified tag
175+
ctx.fillStyle = primaryColor;
176+
ctx.globalAlpha = 0.12;
177+
ctx.fillRect(rightColX, rightY - 14 * scale, 160 * scale, 28 * scale);
178+
ctx.globalAlpha = 1;
179+
180+
ctx.fillStyle = primaryColor;
181+
ctx.font = `700 ${14 * scale}px "JetBrains Mono", monospace`;
182+
ctx.fillText(language.toUpperCase(), rightColX + 10 * scale, rightY + 4 * scale);
183+
184+
rightY += 36 * scale;
185+
186+
// Color palette indicators
187+
ctx.fillStyle = mutedInk;
188+
ctx.font = `italic 400 ${12 * scale}px Georgia, "Times New Roman", serif`;
189+
ctx.fillText('Color edition', rightColX, rightY);
190+
191+
const swatchY = rightY + 12 * scale;
192+
const swatchS = 16 * scale;
193+
[bgColor, primaryColor, secondaryColor].forEach((color, i) => {
194+
ctx.fillStyle = color;
195+
ctx.fillRect(rightColX + i * (swatchS + 6 * scale), swatchY, swatchS, swatchS);
196+
ctx.strokeStyle = ruleColor;
197+
ctx.lineWidth = 1 * scale;
198+
ctx.strokeRect(rightColX + i * (swatchS + 6 * scale), swatchY, swatchS, swatchS);
199+
});
200+
201+
// --- Bottom footer rule ---
202+
const footerY = height - padding;
203+
ctx.fillStyle = inkColor;
204+
ctx.fillRect(padding, footerY - 24 * scale, width - padding * 2, 2 * scale);
205+
ctx.fillRect(padding, footerY - 18 * scale, width - padding * 2, 1 * scale);
206+
207+
// Footer text
208+
ctx.textAlign = 'left';
209+
ctx.fillStyle = mutedInk;
210+
ctx.font = `italic 400 ${14 * scale}px Georgia, "Times New Roman", serif`;
211+
ctx.fillText(
212+
showPattern ? '• Textured Edition •' : '• Clean Edition •',
213+
padding,
214+
footerY,
215+
);
216+
217+
if (supportUrl) {
218+
ctx.textAlign = 'right';
219+
ctx.fillStyle = mutedInk;
220+
ctx.font = `400 ${14 * scale}px Georgia, "Times New Roman", serif`;
221+
ctx.fillText(supportUrl, width - padding, footerY);
222+
}
223+
};

0 commit comments

Comments
 (0)