Skip to content

Commit 2b1ad77

Browse files
committed
feat: add Split Flap Board theme to github thumbnail generator
1 parent 6d3c7f2 commit 2b1ad77

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const THEME_OPTIONS = [
6767
{ value: 'circlesBg', label: 'CIRCLES_BG' },
6868
{ value: 'cassetteJCard', label: 'CASSETTE_J_CARD' },
6969
{ value: 'modernNature', label: 'MODERN_NATURE' },
70+
{ value: 'splitFlap', label: 'SPLIT_FLAP_BOARD' },
7071
];
7172

7273
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { aeroGlass } from './themes/aeroGlass';
4747
import { circlesBg } from './themes/circlesBg';
4848
import { cassetteJCard } from './themes/cassetteJCard';
4949
import { modernNature } from './themes/modernNature';
50+
import { splitFlap } from './themes/splitFlap';
5051

5152
export const themeRenderers = {
5253
modern,
@@ -98,4 +99,5 @@ export const themeRenderers = {
9899
circlesBg,
99100
cassetteJCard,
100101
modernNature,
102+
splitFlap,
101103
};
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { wrapText } from '../utils';
2+
3+
export const splitFlap = (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+
// Board background
19+
ctx.fillStyle = bgColor;
20+
ctx.fillRect(0, 0, width, height);
21+
22+
// Outer frame — thick metallic border
23+
const frameW = 16 * scale;
24+
ctx.fillStyle = '#2a2a2a';
25+
ctx.fillRect(0, 0, width, frameW);
26+
ctx.fillRect(0, height - frameW, width, frameW);
27+
ctx.fillRect(0, 0, frameW, height);
28+
ctx.fillRect(width - frameW, 0, frameW, height);
29+
30+
// Inner bevel
31+
ctx.fillStyle = '#3a3a3a';
32+
ctx.fillRect(frameW, frameW, width - frameW * 2, 4 * scale);
33+
ctx.fillRect(frameW, height - frameW - 4 * scale, width - frameW * 2, 4 * scale);
34+
35+
// Screw holes in corners
36+
const drawScrew = (x, y) => {
37+
ctx.fillStyle = '#555';
38+
ctx.beginPath();
39+
ctx.arc(x, y, 6 * scale, 0, Math.PI * 2);
40+
ctx.fill();
41+
ctx.strokeStyle = '#333';
42+
ctx.lineWidth = 1.5 * scale;
43+
ctx.beginPath();
44+
ctx.moveTo(x - 3 * scale, y - 3 * scale);
45+
ctx.lineTo(x + 3 * scale, y + 3 * scale);
46+
ctx.stroke();
47+
};
48+
const screwInset = 24 * scale;
49+
drawScrew(screwInset, screwInset);
50+
drawScrew(width - screwInset, screwInset);
51+
drawScrew(screwInset, height - screwInset);
52+
drawScrew(width - screwInset, height - screwInset);
53+
54+
// Rivet pattern on top/bottom rail (if showPattern)
55+
if (showPattern) {
56+
ctx.fillStyle = '#444';
57+
for (let i = 0; i < 20; i++) {
58+
const rx = frameW + 50 * scale + i * ((width - frameW * 2 - 100 * scale) / 19);
59+
ctx.beginPath();
60+
ctx.arc(rx, frameW / 2, 2.5 * scale, 0, Math.PI * 2);
61+
ctx.fill();
62+
ctx.beginPath();
63+
ctx.arc(rx, height - frameW / 2, 2.5 * scale, 0, Math.PI * 2);
64+
ctx.fill();
65+
}
66+
}
67+
68+
const padding = 50 * scale;
69+
const contentX = frameW + padding;
70+
const contentY = frameW + padding;
71+
const contentW = width - (frameW + padding) * 2;
72+
73+
// --- Helper: draw a single flap cell ---
74+
const drawFlapCell = (x, y, char, cellW, cellH, color) => {
75+
const r = 4 * scale;
76+
77+
// Cell background
78+
ctx.fillStyle = '#1a1a1a';
79+
ctx.beginPath();
80+
ctx.roundRect(x, y, cellW, cellH, r);
81+
ctx.fill();
82+
83+
// Top half (slightly lighter)
84+
ctx.save();
85+
ctx.beginPath();
86+
ctx.rect(x, y, cellW, cellH / 2);
87+
ctx.clip();
88+
ctx.fillStyle = '#222';
89+
ctx.beginPath();
90+
ctx.roundRect(x, y, cellW, cellH, r);
91+
ctx.fill();
92+
ctx.restore();
93+
94+
// Split line (the mechanical gap)
95+
ctx.fillStyle = '#0a0a0a';
96+
ctx.fillRect(x, y + cellH / 2 - 1 * scale, cellW, 2 * scale);
97+
98+
// Subtle inner shadow at top
99+
const innerShadow = ctx.createLinearGradient(x, y, x, y + 8 * scale);
100+
innerShadow.addColorStop(0, 'rgba(0,0,0,0.4)');
101+
innerShadow.addColorStop(1, 'rgba(0,0,0,0)');
102+
ctx.fillStyle = innerShadow;
103+
ctx.fillRect(x + 1, y + 1, cellW - 2, 8 * scale);
104+
105+
// Character
106+
ctx.fillStyle = color;
107+
ctx.textAlign = 'center';
108+
ctx.textBaseline = 'middle';
109+
ctx.fillText(char, x + cellW / 2, y + cellH / 2 + 1 * scale);
110+
};
111+
112+
// --- Helper: draw a row of flap cells for text ---
113+
const drawFlapRow = (x, y, text, cellW, cellH, gap, color, maxCells) => {
114+
const chars = text.toUpperCase().split('');
115+
const totalCells = maxCells || chars.length;
116+
for (let i = 0; i < totalCells; i++) {
117+
const char = i < chars.length ? chars[i] : ' ';
118+
drawFlapCell(x + i * (cellW + gap), y, char, cellW, cellH, color);
119+
}
120+
};
121+
122+
// --- HEADER: Station-style label ---
123+
ctx.fillStyle = primaryColor;
124+
ctx.font = `bold ${14 * scale}px "JetBrains Mono"`;
125+
ctx.textAlign = 'left';
126+
ctx.textBaseline = 'top';
127+
ctx.fillText('REPOSITORY', contentX, contentY);
128+
129+
ctx.fillStyle = 'rgba(255,255,255,0.25)';
130+
ctx.fillText('OPEN SOURCE DEPARTURES', contentX + 200 * scale, contentY);
131+
132+
// --- ROW 1: Repo name in large flap cells ---
133+
const nameY = contentY + 28 * scale;
134+
const nameCellW = 62 * scale;
135+
const nameCellH = 80 * scale;
136+
const nameGap = 4 * scale;
137+
const maxNameCells = Math.min(repoName.length, Math.floor(contentW / (nameCellW + nameGap)));
138+
139+
ctx.font = `bold ${52 * scale}px "JetBrains Mono"`;
140+
drawFlapRow(contentX, nameY, repoName, nameCellW, nameCellH, nameGap, primaryColor, maxNameCells);
141+
142+
// --- ROW 2: Owner in smaller cells ---
143+
const ownerY = nameY + nameCellH + 16 * scale;
144+
const ownerCellW = 28 * scale;
145+
const ownerCellH = 36 * scale;
146+
const ownerGap = 3 * scale;
147+
148+
ctx.fillStyle = 'rgba(255,255,255,0.3)';
149+
ctx.font = `bold ${11 * scale}px "JetBrains Mono"`;
150+
ctx.textAlign = 'left';
151+
ctx.textBaseline = 'top';
152+
ctx.fillText('MAINTAINER', contentX, ownerY - 14 * scale);
153+
154+
ctx.font = `bold ${22 * scale}px "JetBrains Mono"`;
155+
const ownerDisplay = repoOwner;
156+
drawFlapRow(contentX, ownerY, ownerDisplay, ownerCellW, ownerCellH, ownerGap, '#e0e0e0', ownerDisplay.length);
157+
158+
// --- ROW 3: Description in small flap cells ---
159+
const descY = ownerY + ownerCellH + 24 * scale;
160+
const descCellW = 22 * scale;
161+
const descCellH = 30 * scale;
162+
const descGap = 2 * scale;
163+
const descMaxPerRow = Math.floor(contentW / (descCellW + descGap));
164+
165+
ctx.fillStyle = 'rgba(255,255,255,0.3)';
166+
ctx.font = `bold ${11 * scale}px "JetBrains Mono"`;
167+
ctx.textAlign = 'left';
168+
ctx.textBaseline = 'top';
169+
ctx.fillText('INFO', contentX, descY - 14 * scale);
170+
171+
ctx.font = `bold ${16 * scale}px "JetBrains Mono"`;
172+
const descUpper = description.toUpperCase();
173+
const descRows = Math.ceil(descUpper.length / descMaxPerRow);
174+
const maxRows = 2;
175+
for (let row = 0; row < Math.min(descRows, maxRows); row++) {
176+
const slice = descUpper.substring(row * descMaxPerRow, (row + 1) * descMaxPerRow);
177+
drawFlapRow(contentX, descY + row * (descCellH + 4 * scale), slice, descCellW, descCellH, descGap, 'rgba(255,255,255,0.7)', slice.length);
178+
}
179+
180+
// --- BOTTOM: Info row like a departure board ---
181+
const bottomRowY = height - frameW - padding - 48 * scale;
182+
const infoCellW = 28 * scale;
183+
const infoCellH = 36 * scale;
184+
const infoGap = 3 * scale;
185+
186+
// Divider line
187+
ctx.fillStyle = 'rgba(255,255,255,0.08)';
188+
ctx.fillRect(contentX, bottomRowY - 20 * scale, contentW, 1 * scale);
189+
190+
// Column labels
191+
ctx.fillStyle = 'rgba(255,255,255,0.25)';
192+
ctx.font = `bold ${11 * scale}px "JetBrains Mono"`;
193+
ctx.textAlign = 'left';
194+
ctx.textBaseline = 'top';
195+
196+
let colX = contentX;
197+
198+
// LANG column
199+
ctx.fillText('LANG', colX, bottomRowY - 16 * scale);
200+
ctx.font = `bold ${22 * scale}px "JetBrains Mono"`;
201+
const langText = language.substring(0, 6);
202+
drawFlapRow(colX, bottomRowY, langText, infoCellW, infoCellH, infoGap, secondaryColor, langText.length);
203+
colX += langText.length * (infoCellW + infoGap) + 30 * scale;
204+
205+
// STARS column
206+
if (stars) {
207+
ctx.fillStyle = 'rgba(255,255,255,0.25)';
208+
ctx.font = `bold ${11 * scale}px "JetBrains Mono"`;
209+
ctx.textAlign = 'left';
210+
ctx.fillText('STARS', colX, bottomRowY - 16 * scale);
211+
ctx.font = `bold ${22 * scale}px "JetBrains Mono"`;
212+
const starsText = String(stars);
213+
drawFlapRow(colX, bottomRowY, starsText, infoCellW, infoCellH, infoGap, '#ffd54f', starsText.length);
214+
colX += starsText.length * (infoCellW + infoGap) + 30 * scale;
215+
}
216+
217+
// FORKS column
218+
if (forks) {
219+
ctx.fillStyle = 'rgba(255,255,255,0.25)';
220+
ctx.font = `bold ${11 * scale}px "JetBrains Mono"`;
221+
ctx.textAlign = 'left';
222+
ctx.fillText('FORKS', colX, bottomRowY - 16 * scale);
223+
ctx.font = `bold ${22 * scale}px "JetBrains Mono"`;
224+
const forksText = String(forks);
225+
drawFlapRow(colX, bottomRowY, forksText, infoCellW, infoCellH, infoGap, '#81d4fa', forksText.length);
226+
}
227+
228+
// Support URL — right-aligned, LED-style
229+
if (supportUrl) {
230+
ctx.fillStyle = primaryColor;
231+
ctx.globalAlpha = 0.5;
232+
ctx.font = `${16 * scale}px "JetBrains Mono"`;
233+
ctx.textAlign = 'right';
234+
ctx.textBaseline = 'middle';
235+
ctx.fillText(supportUrl, contentX + contentW, bottomRowY + infoCellH / 2);
236+
ctx.globalAlpha = 1;
237+
}
238+
239+
// Status indicator dot (top-right) — like an active board
240+
ctx.fillStyle = '#4caf50';
241+
ctx.beginPath();
242+
ctx.arc(contentX + contentW - 4 * scale, contentY + 7 * scale, 5 * scale, 0, Math.PI * 2);
243+
ctx.fill();
244+
245+
// Glow on the dot
246+
ctx.save();
247+
ctx.globalAlpha = 0.3;
248+
ctx.fillStyle = '#4caf50';
249+
ctx.beginPath();
250+
ctx.arc(contentX + contentW - 4 * scale, contentY + 7 * scale, 12 * scale, 0, Math.PI * 2);
251+
ctx.fill();
252+
ctx.restore();
253+
};

0 commit comments

Comments
 (0)