Skip to content

Commit b836beb

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

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const THEME_OPTIONS = [
6161
{ value: 'postModern', label: 'POST_MODERN_ARTSY' },
6262
{ value: 'topographic', label: 'TOPOGRAPHIC_SURVEY' },
6363
{ value: 'starChart', label: 'STELLAR_CHART' },
64+
{ value: 'sonarPing', label: 'SONAR_PING' },
6465
];
6566

6667
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { newspaper } from './themes/newspaper';
4141
import { postModern } from './themes/postModern';
4242
import { topographic } from './themes/topographic';
4343
import { starChart } from './themes/starChart';
44+
import { sonarPing } from './themes/sonarPing';
4445

4546
export const themeRenderers = {
4647
modern,
@@ -86,4 +87,5 @@ export const themeRenderers = {
8687
postModern,
8788
topographic,
8889
starChart,
90+
sonarPing,
8991
};
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
export const sonarPing = (ctx, width, height, scale, data) => {
2+
const {
3+
primaryColor,
4+
secondaryColor,
5+
bgColor,
6+
showPattern,
7+
repoOwner,
8+
repoName,
9+
description,
10+
language,
11+
stars,
12+
forks,
13+
supportUrl,
14+
} = data;
15+
16+
// --- CRT background (uses bgColor) ---
17+
ctx.save();
18+
ctx.fillStyle = bgColor;
19+
ctx.fillRect(0, 0, width, height);
20+
ctx.restore();
21+
22+
// --- Vignette (radial darkening at edges) ---
23+
ctx.save();
24+
const vigGrad = ctx.createRadialGradient(
25+
width / 2, height / 2, height * 0.2,
26+
width / 2, height / 2, height * 0.75,
27+
);
28+
vigGrad.addColorStop(0, 'transparent');
29+
vigGrad.addColorStop(1, 'rgba(0,0,0,0.6)');
30+
ctx.fillStyle = vigGrad;
31+
ctx.fillRect(0, 0, width, height);
32+
ctx.restore();
33+
34+
const cx = width / 2;
35+
const cy = height / 2;
36+
const maxR = height * 0.42;
37+
38+
// --- Concentric range rings ---
39+
const ringCount = 6;
40+
for (let i = 1; i <= ringCount; i++) {
41+
const r = (i / ringCount) * maxR;
42+
ctx.save();
43+
ctx.strokeStyle = primaryColor;
44+
ctx.globalAlpha = i === ringCount ? 0.25 : 0.08;
45+
ctx.lineWidth = i === ringCount ? 2 * scale : 1 * scale;
46+
ctx.beginPath();
47+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
48+
ctx.stroke();
49+
ctx.restore();
50+
51+
// Range labels on right side
52+
ctx.save();
53+
ctx.fillStyle = primaryColor;
54+
ctx.globalAlpha = 0.2;
55+
ctx.font = `400 ${10 * scale}px "JetBrains Mono", monospace`;
56+
ctx.textAlign = 'left';
57+
ctx.fillText(`${i * 100}`, cx + r + 6 * scale, cy + 4 * scale);
58+
ctx.restore();
59+
}
60+
61+
// --- Bearing lines (radial, every 30 degrees) ---
62+
for (let deg = 0; deg < 360; deg += 30) {
63+
const rad = (deg * Math.PI) / 180;
64+
const isMajor = deg % 90 === 0;
65+
66+
ctx.save();
67+
ctx.strokeStyle = primaryColor;
68+
ctx.globalAlpha = isMajor ? 0.15 : 0.04;
69+
ctx.lineWidth = 1 * scale;
70+
71+
if (!isMajor) {
72+
ctx.setLineDash([2 * scale, 6 * scale]);
73+
}
74+
75+
ctx.beginPath();
76+
ctx.moveTo(cx, cy);
77+
ctx.lineTo(cx + Math.cos(rad) * maxR, cy + Math.sin(rad) * maxR);
78+
ctx.stroke();
79+
ctx.setLineDash([]);
80+
ctx.restore();
81+
82+
// Bearing labels around perimeter
83+
if (isMajor) {
84+
const labelR = maxR + 18 * scale;
85+
const lx = cx + Math.cos(rad) * labelR;
86+
const ly = cy + Math.sin(rad) * labelR;
87+
ctx.save();
88+
ctx.fillStyle = primaryColor;
89+
ctx.globalAlpha = 0.3;
90+
ctx.font = `600 ${12 * scale}px "JetBrains Mono", monospace`;
91+
ctx.textAlign = 'center';
92+
ctx.textBaseline = 'middle';
93+
ctx.fillText(`${deg}°`, lx, ly);
94+
ctx.restore();
95+
}
96+
}
97+
98+
// --- Tick marks around outer ring (every 10 degrees) ---
99+
ctx.save();
100+
ctx.strokeStyle = primaryColor;
101+
ctx.globalAlpha = 0.12;
102+
ctx.lineWidth = 1 * scale;
103+
for (let deg = 0; deg < 360; deg += 10) {
104+
if (deg % 30 === 0) continue;
105+
const rad = (deg * Math.PI) / 180;
106+
const inner = maxR - 6 * scale;
107+
ctx.beginPath();
108+
ctx.moveTo(cx + Math.cos(rad) * inner, cy + Math.sin(rad) * inner);
109+
ctx.lineTo(cx + Math.cos(rad) * maxR, cy + Math.sin(rad) * maxR);
110+
ctx.stroke();
111+
}
112+
ctx.restore();
113+
114+
// --- Sweep line (phosphor glow wedge) ---
115+
ctx.save();
116+
const sweepAngle = -0.4; // radians, pointing upper-left-ish
117+
const sweepSpread = 0.35;
118+
119+
// Sweep trail (fading wedge)
120+
const sweepGrad = ctx.createConicGradient(sweepAngle - sweepSpread, cx, cy);
121+
sweepGrad.addColorStop(0, 'transparent');
122+
sweepGrad.addColorStop(0.08, primaryColor);
123+
sweepGrad.addColorStop(0.12, 'transparent');
124+
125+
ctx.globalAlpha = 0.12;
126+
ctx.fillStyle = sweepGrad;
127+
ctx.beginPath();
128+
ctx.moveTo(cx, cy);
129+
ctx.arc(cx, cy, maxR, sweepAngle - sweepSpread, sweepAngle + 0.02);
130+
ctx.closePath();
131+
ctx.fill();
132+
133+
// Bright sweep edge
134+
ctx.strokeStyle = primaryColor;
135+
ctx.globalAlpha = 0.5;
136+
ctx.lineWidth = 2 * scale;
137+
ctx.beginPath();
138+
ctx.moveTo(cx, cy);
139+
ctx.lineTo(
140+
cx + Math.cos(sweepAngle) * maxR,
141+
cy + Math.sin(sweepAngle) * maxR,
142+
);
143+
ctx.stroke();
144+
ctx.restore();
145+
146+
// --- Center dot (origin) ---
147+
ctx.save();
148+
ctx.fillStyle = primaryColor;
149+
ctx.globalAlpha = 0.6;
150+
ctx.beginPath();
151+
ctx.arc(cx, cy, 4 * scale, 0, Math.PI * 2);
152+
ctx.fill();
153+
ctx.globalAlpha = 0.15;
154+
ctx.beginPath();
155+
ctx.arc(cx, cy, 10 * scale, 0, Math.PI * 2);
156+
ctx.fill();
157+
ctx.restore();
158+
159+
// --- Noise/static texture (showPattern) ---
160+
if (showPattern) {
161+
ctx.save();
162+
ctx.globalAlpha = 0.015;
163+
ctx.fillStyle = primaryColor;
164+
for (let i = 0; i < 500; i++) {
165+
const h = (i * 7919 + 31);
166+
const nx = (h % 1280) / 1280 * width;
167+
const ny = ((h * 6271) % 640) / 640 * height;
168+
const dist = Math.hypot(nx - cx, ny - cy);
169+
if (dist < maxR) {
170+
ctx.globalAlpha = 0.02 + Math.random() * 0.02;
171+
ctx.fillRect(nx, ny, 2 * scale, 2 * scale);
172+
}
173+
}
174+
ctx.restore();
175+
}
176+
177+
// --- Data blips on the radar ---
178+
const drawBlip = (angle, distance, size, color, label, value) => {
179+
const rad = (angle * Math.PI) / 180;
180+
const r = (distance / 600) * maxR;
181+
const bx = cx + Math.cos(rad) * r;
182+
const by = cy + Math.sin(rad) * r;
183+
184+
// Glow
185+
ctx.save();
186+
ctx.fillStyle = color;
187+
ctx.globalAlpha = 0.08;
188+
ctx.beginPath();
189+
ctx.arc(bx, by, size * 4 * scale, 0, Math.PI * 2);
190+
ctx.fill();
191+
ctx.restore();
192+
193+
// Blip dot
194+
ctx.save();
195+
ctx.fillStyle = color;
196+
ctx.globalAlpha = 0.8;
197+
ctx.beginPath();
198+
ctx.arc(bx, by, size * scale, 0, Math.PI * 2);
199+
ctx.fill();
200+
ctx.restore();
201+
202+
// Pulsing ring
203+
ctx.save();
204+
ctx.strokeStyle = color;
205+
ctx.globalAlpha = 0.2;
206+
ctx.lineWidth = 1 * scale;
207+
ctx.beginPath();
208+
ctx.arc(bx, by, size * 2.5 * scale, 0, Math.PI * 2);
209+
ctx.stroke();
210+
ctx.restore();
211+
212+
// Label
213+
if (label) {
214+
ctx.save();
215+
ctx.fillStyle = color;
216+
ctx.globalAlpha = 0.6;
217+
ctx.font = `600 ${11 * scale}px "JetBrains Mono", monospace`;
218+
ctx.textAlign = 'left';
219+
ctx.fillText(label, bx + size * 3 * scale, by - 6 * scale);
220+
if (value) {
221+
ctx.globalAlpha = 0.35;
222+
ctx.font = `400 ${10 * scale}px "JetBrains Mono", monospace`;
223+
ctx.fillText(value, bx + size * 3 * scale, by + 8 * scale);
224+
}
225+
ctx.restore();
226+
}
227+
};
228+
229+
// Stars blip (upper-left quadrant)
230+
if (stars) drawBlip(225, 320, 6, primaryColor, `★ ${stars}`, 'STARGAZERS');
231+
// Forks blip (lower-right quadrant)
232+
if (forks) drawBlip(45, 400, 5, secondaryColor, `⑂ ${forks}`, 'FORKS');
233+
// Language blip (right)
234+
drawBlip(350, 250, 4, primaryColor, language, 'CLASSIFICATION');
235+
// Small noise blips for atmosphere
236+
drawBlip(120, 480, 2, primaryColor, null, null);
237+
drawBlip(170, 350, 1.5, primaryColor, null, null);
238+
drawBlip(280, 520, 1.5, secondaryColor, null, null);
239+
drawBlip(70, 200, 1, primaryColor, null, null);
240+
drawBlip(310, 440, 1, primaryColor, null, null);
241+
242+
// --- Repo name at center (below origin) ---
243+
ctx.save();
244+
ctx.textAlign = 'center';
245+
ctx.fillStyle = secondaryColor;
246+
ctx.font = `800 ${42 * scale}px "Inter", sans-serif`;
247+
let fontSize = 42;
248+
const maxNameW = maxR * 1.4;
249+
while (ctx.measureText(repoName).width > maxNameW && fontSize > 24) {
250+
fontSize -= 2;
251+
ctx.font = `800 ${fontSize * scale}px "Inter", sans-serif`;
252+
}
253+
ctx.fillText(repoName, cx, cy + 50 * scale);
254+
255+
// Owner above
256+
ctx.fillStyle = primaryColor;
257+
ctx.globalAlpha = 0.5;
258+
ctx.font = `500 ${14 * scale}px "JetBrains Mono", monospace`;
259+
ctx.fillText(`${repoOwner} //`, cx, cy + 22 * scale);
260+
ctx.restore();
261+
262+
// --- HUD readouts in corners ---
263+
264+
// Top-left: status block
265+
const hudPad = 30 * scale;
266+
ctx.save();
267+
ctx.textAlign = 'left';
268+
ctx.fillStyle = primaryColor;
269+
ctx.globalAlpha = 0.35;
270+
ctx.font = `500 ${11 * scale}px "JetBrains Mono", monospace`;
271+
ctx.fillText('SONAR ACTIVE', hudPad, hudPad + 14 * scale);
272+
ctx.fillText(`SIG: ${repoName.toUpperCase()}`, hudPad, hudPad + 30 * scale);
273+
ctx.fillText(`RANGE: 600m`, hudPad, hudPad + 46 * scale);
274+
ctx.fillStyle = showPattern ? primaryColor : 'rgba(255,255,255,0.15)';
275+
ctx.fillText(showPattern ? '● NOISE FILTER: ON' : '○ NOISE FILTER: OFF', hudPad, hudPad + 62 * scale);
276+
ctx.restore();
277+
278+
// Top-right: spectrum colors
279+
ctx.save();
280+
ctx.textAlign = 'right';
281+
ctx.fillStyle = primaryColor;
282+
ctx.globalAlpha = 0.3;
283+
ctx.font = `500 ${11 * scale}px "JetBrains Mono", monospace`;
284+
ctx.fillText('SIGNAL SPECTRUM', width - hudPad, hudPad + 14 * scale);
285+
286+
// Color bars
287+
const barW = 60 * scale;
288+
const barH = 8 * scale;
289+
const barX = width - hudPad - barW;
290+
[
291+
{ color: bgColor, label: 'BASE' },
292+
{ color: primaryColor, label: 'PRI' },
293+
{ color: secondaryColor, label: 'SEC' },
294+
].forEach(({ color, label }, i) => {
295+
const by = hudPad + 24 * scale + i * 18 * scale;
296+
ctx.fillStyle = color;
297+
ctx.globalAlpha = 0.7;
298+
ctx.fillRect(barX, by, barW, barH);
299+
ctx.globalAlpha = 0.25;
300+
ctx.fillStyle = primaryColor;
301+
ctx.font = `400 ${9 * scale}px "JetBrains Mono", monospace`;
302+
ctx.textAlign = 'right';
303+
ctx.fillText(label, barX - 8 * scale, by + 7 * scale);
304+
});
305+
ctx.restore();
306+
307+
// Bottom-left: description
308+
ctx.save();
309+
ctx.textAlign = 'left';
310+
ctx.fillStyle = primaryColor;
311+
ctx.globalAlpha = 0.2;
312+
ctx.font = `500 ${10 * scale}px "JetBrains Mono", monospace`;
313+
ctx.fillText('// INTERCEPT LOG', hudPad, height - hudPad - 52 * scale);
314+
315+
ctx.globalAlpha = 0.4;
316+
ctx.fillStyle = secondaryColor;
317+
ctx.font = `300 ${15 * scale}px "Inter", sans-serif`;
318+
// Truncate description to fit in corner
319+
const maxDescW = width * 0.38;
320+
let desc = description;
321+
while (ctx.measureText(desc).width > maxDescW && desc.length > 10) {
322+
desc = desc.slice(0, -4) + '...';
323+
}
324+
ctx.fillText(desc, hudPad, height - hudPad - 32 * scale);
325+
326+
// Second line if description was long
327+
if (description.length > desc.length) {
328+
let desc2 = description.slice(desc.length - 3);
329+
while (ctx.measureText(desc2).width > maxDescW && desc2.length > 10) {
330+
desc2 = desc2.slice(0, -4) + '...';
331+
}
332+
ctx.fillText(desc2, hudPad, height - hudPad - 14 * scale);
333+
}
334+
ctx.restore();
335+
336+
// Bottom-right: support URL + timestamp feel
337+
if (supportUrl) {
338+
ctx.save();
339+
ctx.textAlign = 'right';
340+
ctx.fillStyle = primaryColor;
341+
ctx.globalAlpha = 0.2;
342+
ctx.font = `400 ${11 * scale}px "JetBrains Mono", monospace`;
343+
ctx.fillText(`NET: ${supportUrl}`, width - hudPad, height - hudPad - 14 * scale);
344+
ctx.restore();
345+
}
346+
347+
// --- Scanline overlay for CRT feel ---
348+
ctx.save();
349+
ctx.globalAlpha = 0.03;
350+
ctx.fillStyle = '#000000';
351+
for (let y = 0; y < height; y += 4 * scale) {
352+
ctx.fillRect(0, y, width, 2 * scale);
353+
}
354+
ctx.restore();
355+
};

0 commit comments

Comments
 (0)