Skip to content

Commit 8f9565b

Browse files
committed
feat: add Haute Couture fashion editorial theme to github thumbnail generator
1 parent 92b1156 commit 8f9565b

File tree

3 files changed

+212
-0
lines changed

3 files changed

+212
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const THEME_OPTIONS = [
7070
{ value: 'splitFlap', label: 'SPLIT_FLAP_BOARD' },
7171
{ value: 'passportStamp', label: 'PASSPORT_STAMP' },
7272
{ value: 'vinylRecord', label: 'VINYL_RECORD' },
73+
{ value: 'hauteCouture', label: 'HAUTE_COUTURE' },
7374
];
7475

7576
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { modernNature } from './themes/modernNature';
5050
import { splitFlap } from './themes/splitFlap';
5151
import { passportStamp } from './themes/passportStamp';
5252
import { vinylRecord } from './themes/vinylRecord';
53+
import { hauteCouture } from './themes/hauteCouture';
5354

5455
export const themeRenderers = {
5556
modern,
@@ -104,4 +105,5 @@ export const themeRenderers = {
104105
splitFlap,
105106
passportStamp,
106107
vinylRecord,
108+
hauteCouture,
107109
};
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
export const hauteCouture = (ctx, width, height, scale, data) => {
2+
const {
3+
primaryColor,
4+
secondaryColor,
5+
repoOwner,
6+
repoName,
7+
description,
8+
language,
9+
stars,
10+
forks,
11+
supportUrl,
12+
bgColor,
13+
showPattern,
14+
} = data;
15+
16+
// --- Stark editorial background ---
17+
ctx.fillStyle = bgColor;
18+
ctx.fillRect(0, 0, width, height);
19+
20+
// Diagonal color block — bold fashion slash
21+
ctx.save();
22+
ctx.beginPath();
23+
ctx.moveTo(width * 0.55, 0);
24+
ctx.lineTo(width * 0.72, 0);
25+
ctx.lineTo(width * 0.42, height);
26+
ctx.lineTo(width * 0.25, height);
27+
ctx.closePath();
28+
ctx.fillStyle = primaryColor;
29+
ctx.globalAlpha = 0.12;
30+
ctx.fill();
31+
ctx.restore();
32+
33+
// Second thinner slash
34+
ctx.save();
35+
ctx.beginPath();
36+
ctx.moveTo(width * 0.74, 0);
37+
ctx.lineTo(width * 0.78, 0);
38+
ctx.lineTo(width * 0.48, height);
39+
ctx.lineTo(width * 0.44, height);
40+
ctx.closePath();
41+
ctx.fillStyle = secondaryColor;
42+
ctx.globalAlpha = 0.08;
43+
ctx.fill();
44+
ctx.restore();
45+
46+
// Fashion grid pattern (if showPattern)
47+
if (showPattern) {
48+
ctx.save();
49+
ctx.globalAlpha = 0.03;
50+
ctx.strokeStyle = '#fff';
51+
ctx.lineWidth = 1 * scale;
52+
// Horizontal rules
53+
for (let y = 0; y < height; y += 40 * scale) {
54+
ctx.beginPath();
55+
ctx.moveTo(0, y);
56+
ctx.lineTo(width, y);
57+
ctx.stroke();
58+
}
59+
// Vertical rules
60+
for (let x = 0; x < width; x += 40 * scale) {
61+
ctx.beginPath();
62+
ctx.moveTo(x, 0);
63+
ctx.lineTo(x, height);
64+
ctx.stroke();
65+
}
66+
ctx.restore();
67+
}
68+
69+
const pad = 60 * scale;
70+
71+
// --- MASSIVE repo name — the hero ---
72+
ctx.save();
73+
ctx.fillStyle = '#fff';
74+
ctx.textAlign = 'left';
75+
ctx.textBaseline = 'top';
76+
77+
// Measure and scale font to be as large as possible
78+
let fontSize = 180 * scale;
79+
ctx.font = `900 ${fontSize}px "Inter", "Helvetica Neue", sans-serif`;
80+
while (ctx.measureText(repoName.toUpperCase()).width > width - pad * 2 && fontSize > 60 * scale) {
81+
fontSize -= 4 * scale;
82+
ctx.font = `900 ${fontSize}px "Inter", "Helvetica Neue", sans-serif`;
83+
}
84+
85+
const nameUpper = repoName.toUpperCase();
86+
const nameY = height * 0.18;
87+
ctx.fillText(nameUpper, pad, nameY);
88+
89+
// Measure actual name height
90+
const nameMetrics = ctx.measureText(nameUpper);
91+
const nameBottom = nameY + nameMetrics.actualBoundingBoxDescent;
92+
93+
// Accent underline — two-tone
94+
const lineY = nameBottom + 12 * scale;
95+
ctx.fillStyle = primaryColor;
96+
ctx.fillRect(pad, lineY, 120 * scale, 6 * scale);
97+
ctx.fillStyle = secondaryColor;
98+
ctx.fillRect(pad + 130 * scale, lineY, 60 * scale, 6 * scale);
99+
ctx.restore();
100+
101+
// --- Owner as spaced-out editorial subtitle ---
102+
const ownerY = lineY + 30 * scale;
103+
ctx.fillStyle = 'rgba(255,255,255,0.4)';
104+
ctx.font = `300 ${22 * scale}px "Inter", sans-serif`;
105+
ctx.textAlign = 'left';
106+
ctx.textBaseline = 'top';
107+
const ownerSpaced = repoOwner.toUpperCase().split('').join(' ');
108+
ctx.fillText(ownerSpaced, pad, ownerY);
109+
110+
// --- Description — elegant, light weight ---
111+
const descY = ownerY + 48 * scale;
112+
ctx.fillStyle = 'rgba(255,255,255,0.55)';
113+
ctx.font = `300 ${24 * scale}px "Inter", sans-serif`;
114+
const words = description.split(' ');
115+
let line = '';
116+
let currentY = descY;
117+
const maxW = width * 0.55;
118+
for (let i = 0; i < words.length; i++) {
119+
const test = line + words[i] + ' ';
120+
if (ctx.measureText(test).width > maxW && i > 0) {
121+
ctx.fillText(line.trim(), pad, currentY);
122+
line = words[i] + ' ';
123+
currentY += 34 * scale;
124+
} else {
125+
line = test;
126+
}
127+
}
128+
ctx.fillText(line.trim(), pad, currentY);
129+
130+
// --- Right side: editorial metadata column ---
131+
const metaX = width * 0.7;
132+
133+
// Language — HUGE vertical text
134+
ctx.save();
135+
ctx.translate(width - pad - 10 * scale, height * 0.15);
136+
ctx.rotate(Math.PI / 2);
137+
ctx.fillStyle = primaryColor;
138+
ctx.globalAlpha = 0.15;
139+
ctx.font = `900 ${120 * scale}px "Inter", sans-serif`;
140+
ctx.textAlign = 'left';
141+
ctx.textBaseline = 'top';
142+
ctx.fillText(language.toUpperCase(), 0, 0);
143+
ctx.restore();
144+
145+
// Stats — stacked, bold numbers
146+
const statsX = metaX;
147+
let statsY = height * 0.45;
148+
149+
if (stars) {
150+
ctx.fillStyle = '#fff';
151+
ctx.font = `900 ${48 * scale}px "Inter", sans-serif`;
152+
ctx.textAlign = 'left';
153+
ctx.textBaseline = 'top';
154+
ctx.fillText(String(stars), statsX, statsY);
155+
156+
ctx.fillStyle = 'rgba(255,255,255,0.3)';
157+
ctx.font = `300 ${14 * scale}px "Inter", sans-serif`;
158+
ctx.fillText('STARS', statsX, statsY + 54 * scale);
159+
statsY += 90 * scale;
160+
}
161+
162+
if (forks) {
163+
ctx.fillStyle = '#fff';
164+
ctx.font = `900 ${48 * scale}px "Inter", sans-serif`;
165+
ctx.textAlign = 'left';
166+
ctx.textBaseline = 'top';
167+
ctx.fillText(String(forks), statsX, statsY);
168+
169+
ctx.fillStyle = 'rgba(255,255,255,0.3)';
170+
ctx.font = `300 ${14 * scale}px "Inter", sans-serif`;
171+
ctx.fillText('FORKS', statsX, statsY + 54 * scale);
172+
}
173+
174+
// --- Bottom bar ---
175+
const bottomY = height - pad;
176+
177+
// Language pill — minimal rectangle
178+
ctx.fillStyle = primaryColor;
179+
ctx.font = `600 ${16 * scale}px "JetBrains Mono"`;
180+
ctx.textAlign = 'left';
181+
ctx.textBaseline = 'bottom';
182+
const langText = language.toUpperCase();
183+
const langW = ctx.measureText(langText).width + 24 * scale;
184+
ctx.globalAlpha = 0.15;
185+
ctx.fillRect(pad, bottomY - 24 * scale, langW, 28 * scale);
186+
ctx.globalAlpha = 1;
187+
ctx.fillStyle = primaryColor;
188+
ctx.fillText(langText, pad + 12 * scale, bottomY);
189+
190+
// Thin horizontal rule
191+
ctx.fillStyle = 'rgba(255,255,255,0.08)';
192+
ctx.fillRect(pad, bottomY - 36 * scale, width - pad * 2, 1 * scale);
193+
194+
// Support URL — right aligned
195+
if (supportUrl) {
196+
ctx.fillStyle = 'rgba(255,255,255,0.25)';
197+
ctx.font = `300 ${14 * scale}px "JetBrains Mono"`;
198+
ctx.textAlign = 'right';
199+
ctx.textBaseline = 'bottom';
200+
ctx.fillText(supportUrl, width - pad, bottomY);
201+
}
202+
203+
// Issue / Season tag
204+
ctx.fillStyle = 'rgba(255,255,255,0.12)';
205+
ctx.font = `300 ${12 * scale}px "JetBrains Mono"`;
206+
ctx.textAlign = 'right';
207+
ctx.textBaseline = 'top';
208+
ctx.fillText('ISSUE No. 01 — DIGITAL EDITION', width - pad, pad);
209+
};

0 commit comments

Comments
 (0)