Skip to content

Commit 2d29a34

Browse files
committed
feat: add Ethereal Glow theme to github thumbnail generator
1 parent 74ef5b8 commit 2d29a34

File tree

3 files changed

+332
-0
lines changed

3 files changed

+332
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const THEME_OPTIONS = [
7272
{ value: 'vinylRecord', label: 'VINYL_RECORD' },
7373
{ value: 'hauteCouture', label: 'HAUTE_COUTURE' },
7474
{ value: 'missionControl', label: 'MISSION_CONTROL' },
75+
{ value: 'etherealGlow', label: 'ETHEREAL_GLOW' },
7576
];
7677

7778
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { passportStamp } from './themes/passportStamp';
5252
import { vinylRecord } from './themes/vinylRecord';
5353
import { hauteCouture } from './themes/hauteCouture';
5454
import { missionControl } from './themes/missionControl';
55+
import { etherealGlow } from './themes/etherealGlow';
5556

5657
export const themeRenderers = {
5758
modern,
@@ -108,4 +109,5 @@ export const themeRenderers = {
108109
vinylRecord,
109110
hauteCouture,
110111
missionControl,
112+
etherealGlow,
111113
};
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
import { wrapText } from '../utils';
2+
3+
export const etherealGlow = (ctx, width, height, scale, data) => {
4+
const {
5+
primaryColor,
6+
secondaryColor,
7+
repoOwner,
8+
repoName,
9+
description,
10+
language,
11+
stars,
12+
forks,
13+
supportUrl,
14+
bgColor,
15+
showPattern,
16+
} = data;
17+
18+
// ===== RICH GRADIENT BACKGROUND =====
19+
const bgGrad = ctx.createLinearGradient(0, 0, width * 0.4, height);
20+
bgGrad.addColorStop(0, bgColor);
21+
bgGrad.addColorStop(0.5, bgColor);
22+
bgGrad.addColorStop(1, bgColor);
23+
ctx.fillStyle = bgGrad;
24+
ctx.fillRect(0, 0, width, height);
25+
26+
// ===== LUMINOUS ORBS — layered, blurred, overlapping =====
27+
const drawOrb = (ox, oy, or, color, opacity) => {
28+
ctx.save();
29+
const orbGrad = ctx.createRadialGradient(ox, oy, 0, ox, oy, or);
30+
orbGrad.addColorStop(0, color);
31+
orbGrad.addColorStop(0.4, color);
32+
orbGrad.addColorStop(1, 'transparent');
33+
ctx.globalAlpha = opacity;
34+
ctx.filter = `blur(${Math.round(or * 0.4)}px)`;
35+
ctx.fillStyle = orbGrad;
36+
ctx.beginPath();
37+
ctx.arc(ox, oy, or, 0, Math.PI * 2);
38+
ctx.fill();
39+
ctx.restore();
40+
};
41+
42+
// Primary large orb — top right
43+
drawOrb(width * 0.78, height * 0.15, 350 * scale, primaryColor, 0.18);
44+
// Secondary orb — bottom left
45+
drawOrb(width * 0.18, height * 0.85, 300 * scale, secondaryColor, 0.15);
46+
// Accent orb — center left
47+
drawOrb(width * 0.35, height * 0.4, 200 * scale, primaryColor, 0.08);
48+
// Small warm orb — top left
49+
drawOrb(width * 0.08, height * 0.1, 150 * scale, secondaryColor, 0.1);
50+
// Deep orb — bottom right
51+
drawOrb(width * 0.88, height * 0.75, 250 * scale, primaryColor, 0.12);
52+
53+
// ===== PRISMATIC LIGHT STREAKS =====
54+
if (showPattern) {
55+
ctx.save();
56+
// Diagonal light ray 1
57+
ctx.globalAlpha = 0.04;
58+
const ray1Grad = ctx.createLinearGradient(width * 0.3, 0, width * 0.7, height);
59+
ray1Grad.addColorStop(0, 'transparent');
60+
ray1Grad.addColorStop(0.3, primaryColor);
61+
ray1Grad.addColorStop(0.5, '#fff');
62+
ray1Grad.addColorStop(0.7, secondaryColor);
63+
ray1Grad.addColorStop(1, 'transparent');
64+
ctx.fillStyle = ray1Grad;
65+
ctx.beginPath();
66+
ctx.moveTo(width * 0.45, 0);
67+
ctx.lineTo(width * 0.52, 0);
68+
ctx.lineTo(width * 0.22, height);
69+
ctx.lineTo(width * 0.15, height);
70+
ctx.closePath();
71+
ctx.fill();
72+
73+
// Diagonal light ray 2 — thinner
74+
ctx.globalAlpha = 0.03;
75+
const ray2Grad = ctx.createLinearGradient(width * 0.5, 0, width * 0.8, height);
76+
ray2Grad.addColorStop(0, 'transparent');
77+
ray2Grad.addColorStop(0.4, secondaryColor);
78+
ray2Grad.addColorStop(0.6, '#fff');
79+
ray2Grad.addColorStop(1, 'transparent');
80+
ctx.fillStyle = ray2Grad;
81+
ctx.beginPath();
82+
ctx.moveTo(width * 0.6, 0);
83+
ctx.lineTo(width * 0.63, 0);
84+
ctx.lineTo(width * 0.33, height);
85+
ctx.lineTo(width * 0.3, height);
86+
ctx.closePath();
87+
ctx.fill();
88+
89+
// Horizontal light bloom across middle
90+
ctx.globalAlpha = 0.025;
91+
const bloomGrad = ctx.createLinearGradient(0, height * 0.35, 0, height * 0.65);
92+
bloomGrad.addColorStop(0, 'transparent');
93+
bloomGrad.addColorStop(0.5, '#fff');
94+
bloomGrad.addColorStop(1, 'transparent');
95+
ctx.fillStyle = bloomGrad;
96+
ctx.fillRect(0, height * 0.35, width, height * 0.3);
97+
ctx.restore();
98+
99+
// ===== SPARKLE PARTICLES =====
100+
ctx.save();
101+
for (let i = 0; i < 40; i++) {
102+
const sx = ((i * 173 + 29) % 127) / 127 * width;
103+
const sy = ((i * 89 + 47) % 113) / 113 * height;
104+
const ss = (0.8 + (i % 4) * 0.6) * scale;
105+
ctx.globalAlpha = 0.08 + (i % 6) * 0.04;
106+
ctx.fillStyle = '#fff';
107+
108+
// Four-point star sparkle
109+
ctx.beginPath();
110+
ctx.moveTo(sx, sy - ss * 3);
111+
ctx.lineTo(sx + ss, sy);
112+
ctx.lineTo(sx, sy + ss * 3);
113+
ctx.lineTo(sx - ss, sy);
114+
ctx.closePath();
115+
ctx.fill();
116+
}
117+
ctx.restore();
118+
}
119+
120+
// ===== FROSTED GLASS CONTENT CARD =====
121+
const cardPad = 70 * scale;
122+
const cardX = cardPad;
123+
const cardY = cardPad;
124+
const cardW = width - cardPad * 2;
125+
const cardH = height - cardPad * 2;
126+
const cardR = 24 * scale;
127+
128+
// Card frosted background
129+
ctx.save();
130+
ctx.beginPath();
131+
ctx.roundRect(cardX, cardY, cardW, cardH, cardR);
132+
ctx.clip();
133+
134+
// Semi-transparent fill
135+
ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';
136+
ctx.fillRect(cardX, cardY, cardW, cardH);
137+
138+
// Inner light gradient — top edge glow
139+
const innerGlow = ctx.createLinearGradient(cardX, cardY, cardX, cardY + 80 * scale);
140+
innerGlow.addColorStop(0, 'rgba(255, 255, 255, 0.08)');
141+
innerGlow.addColorStop(1, 'rgba(255, 255, 255, 0)');
142+
ctx.fillStyle = innerGlow;
143+
ctx.fillRect(cardX, cardY, cardW, 80 * scale);
144+
ctx.restore();
145+
146+
// Card border — subtle luminous stroke
147+
ctx.save();
148+
ctx.beginPath();
149+
ctx.roundRect(cardX, cardY, cardW, cardH, cardR);
150+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
151+
ctx.lineWidth = 1.5 * scale;
152+
ctx.stroke();
153+
ctx.restore();
154+
155+
// Top-edge highlight (glass refraction)
156+
ctx.save();
157+
ctx.beginPath();
158+
ctx.roundRect(cardX + 1, cardY + 1, cardW - 2, cardR * 2, [cardR, cardR, 0, 0]);
159+
ctx.clip();
160+
const edgeGrad = ctx.createLinearGradient(cardX, cardY, cardX + cardW, cardY);
161+
edgeGrad.addColorStop(0, 'rgba(255,255,255,0)');
162+
edgeGrad.addColorStop(0.3, 'rgba(255,255,255,0.08)');
163+
edgeGrad.addColorStop(0.7, 'rgba(255,255,255,0.03)');
164+
edgeGrad.addColorStop(1, 'rgba(255,255,255,0)');
165+
ctx.fillStyle = edgeGrad;
166+
ctx.fillRect(cardX, cardY, cardW, cardR * 2);
167+
ctx.restore();
168+
169+
// ===== CONTENT =====
170+
const cx = cardX + 50 * scale;
171+
const contentMaxW = cardW - 100 * scale;
172+
173+
// Owner — delicate, airy
174+
ctx.fillStyle = primaryColor;
175+
ctx.globalAlpha = 0.7;
176+
ctx.font = `500 ${20 * scale}px "JetBrains Mono"`;
177+
ctx.textAlign = 'left';
178+
ctx.textBaseline = 'top';
179+
ctx.fillText(repoOwner, cx, cardY + 40 * scale);
180+
ctx.globalAlpha = 1;
181+
182+
// Tiny accent dot after owner
183+
ctx.fillStyle = secondaryColor;
184+
ctx.globalAlpha = 0.5;
185+
const ownerW = ctx.measureText(repoOwner).width;
186+
ctx.beginPath();
187+
ctx.arc(cx + ownerW + 12 * scale, cardY + 50 * scale, 3 * scale, 0, Math.PI * 2);
188+
ctx.fill();
189+
ctx.globalAlpha = 1;
190+
191+
// Repo name — large, elegant, white
192+
ctx.fillStyle = '#fff';
193+
let nameSize = 72 * scale;
194+
ctx.font = `700 ${nameSize}px "Inter", sans-serif`;
195+
while (ctx.measureText(repoName).width > contentMaxW && nameSize > 36 * scale) {
196+
nameSize -= 3 * scale;
197+
ctx.font = `700 ${nameSize}px "Inter", sans-serif`;
198+
}
199+
const nameY = cardY + 72 * scale;
200+
ctx.fillText(repoName, cx, nameY);
201+
202+
// Luminous gradient line under name
203+
const nameBottom = nameY + nameSize + 4 * scale;
204+
const lineGrad = ctx.createLinearGradient(cx, 0, cx + 280 * scale, 0);
205+
lineGrad.addColorStop(0, primaryColor);
206+
lineGrad.addColorStop(0.5, secondaryColor);
207+
lineGrad.addColorStop(1, 'transparent');
208+
ctx.fillStyle = lineGrad;
209+
ctx.globalAlpha = 0.5;
210+
ctx.fillRect(cx, nameBottom, 280 * scale, 2.5 * scale);
211+
// Glow under line
212+
ctx.globalAlpha = 0.15;
213+
ctx.filter = `blur(${6 * scale}px)`;
214+
ctx.fillRect(cx, nameBottom - 2 * scale, 280 * scale, 8 * scale);
215+
ctx.filter = 'none';
216+
ctx.globalAlpha = 1;
217+
218+
// Description — soft, breathable
219+
const descY = nameBottom + 28 * scale;
220+
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
221+
ctx.font = `300 ${26 * scale}px "Inter", sans-serif`;
222+
wrapText(ctx, description, cx, descY, contentMaxW, 38 * scale);
223+
224+
// ===== BOTTOM SECTION =====
225+
const bottomY = cardY + cardH - 50 * scale;
226+
227+
// Language pill with glow
228+
ctx.save();
229+
ctx.globalAlpha = 0.1;
230+
ctx.fillStyle = primaryColor;
231+
ctx.filter = `blur(${10 * scale}px)`;
232+
ctx.beginPath();
233+
ctx.roundRect(cx - 5 * scale, bottomY - 30 * scale, 120 * scale, 40 * scale, 20 * scale);
234+
ctx.fill();
235+
ctx.filter = 'none';
236+
ctx.restore();
237+
// Language pill — custom drawn for vertical centering
238+
ctx.textAlign = 'left';
239+
ctx.textBaseline = 'middle';
240+
ctx.font = `bold ${20 * scale}px "JetBrains Mono"`;
241+
const pillPad = 20 * scale;
242+
const pillTextW = ctx.measureText(language).width;
243+
const pillW = pillTextW + pillPad * 2;
244+
const pillH = 40 * scale;
245+
const pillX = cx;
246+
const pillY = bottomY - pillH / 2 - 4 * scale;
247+
248+
ctx.fillStyle = primaryColor;
249+
ctx.globalAlpha = 0.2;
250+
ctx.beginPath();
251+
ctx.roundRect(pillX, pillY, pillW, pillH, pillH / 2);
252+
ctx.fill();
253+
ctx.globalAlpha = 1;
254+
255+
ctx.fillStyle = primaryColor;
256+
ctx.fillText(language, pillX + pillPad, pillY + pillH / 2);
257+
258+
// Stats — soft, right-aligned
259+
ctx.textAlign = 'right';
260+
let statX = cardX + cardW - 50 * scale;
261+
262+
// Support URL
263+
if (supportUrl) {
264+
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
265+
ctx.font = `300 ${16 * scale}px "JetBrains Mono"`;
266+
ctx.textBaseline = 'bottom';
267+
ctx.fillText(supportUrl, statX, bottomY);
268+
statX -= ctx.measureText(supportUrl).width + 36 * scale;
269+
}
270+
271+
// Forks
272+
if (forks) {
273+
ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
274+
ctx.font = `500 ${20 * scale}px "JetBrains Mono"`;
275+
ctx.textBaseline = 'bottom';
276+
ctx.fillText(`${forks}`, statX, bottomY);
277+
const forksNumW = ctx.measureText(`${forks}`).width;
278+
279+
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
280+
ctx.font = `300 ${14 * scale}px "Inter", sans-serif`;
281+
ctx.fillText(' forks', statX - forksNumW - 2 * scale, bottomY);
282+
statX -= forksNumW + ctx.measureText(' forks').width + 30 * scale;
283+
}
284+
285+
// Stars with tiny glow
286+
if (stars) {
287+
ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
288+
ctx.font = `500 ${20 * scale}px "JetBrains Mono"`;
289+
ctx.textBaseline = 'bottom';
290+
ctx.fillText(`${stars}`, statX, bottomY);
291+
const starsNumW = ctx.measureText(`${stars}`).width;
292+
293+
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
294+
ctx.font = `300 ${14 * scale}px "Inter", sans-serif`;
295+
ctx.fillText(' stars', statX - starsNumW - 2 * scale, bottomY);
296+
297+
// Tiny star glow
298+
ctx.save();
299+
ctx.globalAlpha = 0.2;
300+
ctx.fillStyle = secondaryColor;
301+
ctx.beginPath();
302+
ctx.arc(statX - starsNumW - ctx.measureText(' stars').width - 14 * scale, bottomY - 8 * scale, 4 * scale, 0, Math.PI * 2);
303+
ctx.fill();
304+
ctx.globalAlpha = 0.08;
305+
ctx.filter = `blur(${4 * scale}px)`;
306+
ctx.beginPath();
307+
ctx.arc(statX - starsNumW - ctx.measureText(' stars').width - 14 * scale, bottomY - 8 * scale, 10 * scale, 0, Math.PI * 2);
308+
ctx.fill();
309+
ctx.filter = 'none';
310+
ctx.restore();
311+
}
312+
313+
// ===== OUTER AMBIENT GLOW on card edges =====
314+
ctx.save();
315+
// Bottom-right glow
316+
ctx.globalAlpha = 0.06;
317+
ctx.fillStyle = primaryColor;
318+
ctx.filter = `blur(${40 * scale}px)`;
319+
ctx.beginPath();
320+
ctx.arc(cardX + cardW, cardY + cardH, 100 * scale, 0, Math.PI * 2);
321+
ctx.fill();
322+
// Top-left glow
323+
ctx.fillStyle = secondaryColor;
324+
ctx.beginPath();
325+
ctx.arc(cardX, cardY, 80 * scale, 0, Math.PI * 2);
326+
ctx.fill();
327+
ctx.filter = 'none';
328+
ctx.restore();
329+
};

0 commit comments

Comments
 (0)