Skip to content

Commit 5d0ffc1

Browse files
committed
feat: add Passport Stamp theme and expand Split Flap description to 4 rows
1 parent 2b1ad77 commit 5d0ffc1

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const THEME_OPTIONS = [
6868
{ value: 'cassetteJCard', label: 'CASSETTE_J_CARD' },
6969
{ value: 'modernNature', label: 'MODERN_NATURE' },
7070
{ value: 'splitFlap', label: 'SPLIT_FLAP_BOARD' },
71+
{ value: 'passportStamp', label: 'PASSPORT_STAMP' },
7172
];
7273

7374
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { circlesBg } from './themes/circlesBg';
4848
import { cassetteJCard } from './themes/cassetteJCard';
4949
import { modernNature } from './themes/modernNature';
5050
import { splitFlap } from './themes/splitFlap';
51+
import { passportStamp } from './themes/passportStamp';
5152

5253
export const themeRenderers = {
5354
modern,
@@ -100,4 +101,5 @@ export const themeRenderers = {
100101
cassetteJCard,
101102
modernNature,
102103
splitFlap,
104+
passportStamp,
103105
};
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
export const passportStamp = (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+
// --- Passport page background ---
17+
// Off-white / cream paper base
18+
const paperColor = '#f5f0e8';
19+
ctx.fillStyle = paperColor;
20+
ctx.fillRect(0, 0, width, height);
21+
22+
// Subtle paper grain texture (if showPattern)
23+
if (showPattern) {
24+
ctx.save();
25+
ctx.globalAlpha = 0.04;
26+
for (let i = 0; i < 600; i++) {
27+
const x = ((i * 97 + 31) % 1280) * (width / 1280);
28+
const y = ((i * 53 + 17) % 640) * (height / 640);
29+
const size = (1 + (i % 3)) * scale;
30+
ctx.fillStyle = i % 3 === 0 ? '#8b7355' : '#a09080';
31+
ctx.fillRect(x, y, size, size);
32+
}
33+
ctx.restore();
34+
35+
// Faint security guilloche lines
36+
ctx.save();
37+
ctx.globalAlpha = 0.05;
38+
ctx.strokeStyle = bgColor;
39+
ctx.lineWidth = 0.8 * scale;
40+
for (let i = 0; i < 12; i++) {
41+
ctx.beginPath();
42+
for (let x = 0; x <= width; x += 2) {
43+
const y = height / 2 + Math.sin(x / (60 + i * 10) + i) * (80 + i * 15) * scale;
44+
if (x === 0) ctx.moveTo(x, y);
45+
else ctx.lineTo(x, y);
46+
}
47+
ctx.stroke();
48+
}
49+
ctx.restore();
50+
}
51+
52+
// --- Faded background watermark (country emblem style) ---
53+
ctx.save();
54+
ctx.globalAlpha = 0.04;
55+
ctx.strokeStyle = '#5a4a3a';
56+
ctx.lineWidth = 2 * scale;
57+
const cx = width * 0.5;
58+
const cy = height * 0.48;
59+
const emblemR = 200 * scale;
60+
// Outer circle
61+
ctx.beginPath();
62+
ctx.arc(cx, cy, emblemR, 0, Math.PI * 2);
63+
ctx.stroke();
64+
// Inner circle
65+
ctx.beginPath();
66+
ctx.arc(cx, cy, emblemR * 0.75, 0, Math.PI * 2);
67+
ctx.stroke();
68+
// Spokes
69+
for (let i = 0; i < 12; i++) {
70+
const angle = (Math.PI * 2 * i) / 12;
71+
ctx.beginPath();
72+
ctx.moveTo(cx + Math.cos(angle) * emblemR * 0.75, cy + Math.sin(angle) * emblemR * 0.75);
73+
ctx.lineTo(cx + Math.cos(angle) * emblemR, cy + Math.sin(angle) * emblemR);
74+
ctx.stroke();
75+
}
76+
ctx.restore();
77+
78+
// --- Main stamp (repo name) — large circular stamp ---
79+
const stampX = width * 0.35;
80+
const stampY = height * 0.42;
81+
const stampR = 160 * scale;
82+
const stampAngle = -0.12; // Slightly tilted
83+
84+
ctx.save();
85+
ctx.translate(stampX, stampY);
86+
ctx.rotate(stampAngle);
87+
88+
// Stamp ink color from primaryColor
89+
ctx.strokeStyle = primaryColor;
90+
ctx.fillStyle = primaryColor;
91+
ctx.globalAlpha = 0.75;
92+
ctx.lineWidth = 4 * scale;
93+
94+
// Double circle border
95+
ctx.beginPath();
96+
ctx.arc(0, 0, stampR, 0, Math.PI * 2);
97+
ctx.stroke();
98+
ctx.beginPath();
99+
ctx.arc(0, 0, stampR - 10 * scale, 0, Math.PI * 2);
100+
ctx.stroke();
101+
102+
// Repo name curved along top of stamp
103+
ctx.font = `bold ${28 * scale}px "JetBrains Mono"`;
104+
ctx.textAlign = 'center';
105+
ctx.textBaseline = 'middle';
106+
const nameUpper = repoName.toUpperCase();
107+
const nameArcR = stampR - 35 * scale;
108+
const nameSpread = Math.min(Math.PI * 0.9, nameUpper.length * 0.14);
109+
const nameStart = -Math.PI / 2 - nameSpread / 2;
110+
for (let i = 0; i < nameUpper.length; i++) {
111+
const charAngle = nameStart + (i / (nameUpper.length - 1 || 1)) * nameSpread;
112+
ctx.save();
113+
ctx.translate(Math.cos(charAngle) * nameArcR, Math.sin(charAngle) * nameArcR);
114+
ctx.rotate(charAngle + Math.PI / 2);
115+
ctx.fillText(nameUpper[i], 0, 0);
116+
ctx.restore();
117+
}
118+
119+
// Center: star icon and stars count
120+
ctx.font = `bold ${36 * scale}px serif`;
121+
ctx.fillText('★', 0, -10 * scale);
122+
if (stars) {
123+
ctx.font = `bold ${20 * scale}px "JetBrains Mono"`;
124+
ctx.fillText(String(stars), 0, 20 * scale);
125+
}
126+
127+
// Owner curved along bottom
128+
ctx.font = `bold ${16 * scale}px "JetBrains Mono"`;
129+
const ownerUpper = repoOwner.toUpperCase();
130+
const ownerArcR = stampR - 30 * scale;
131+
const ownerSpread = Math.min(Math.PI * 0.7, ownerUpper.length * 0.11);
132+
const ownerStart = Math.PI / 2 - ownerSpread / 2;
133+
for (let i = 0; i < ownerUpper.length; i++) {
134+
const charAngle = ownerStart + (i / (ownerUpper.length - 1 || 1)) * ownerSpread;
135+
ctx.save();
136+
ctx.translate(Math.cos(charAngle) * ownerArcR, Math.sin(charAngle) * ownerArcR);
137+
ctx.rotate(charAngle - Math.PI / 2);
138+
ctx.fillText(ownerUpper[i], 0, 0);
139+
ctx.restore();
140+
}
141+
142+
ctx.restore();
143+
144+
// --- Rectangular entry stamp (language + forks) ---
145+
const rectX = width * 0.68;
146+
const rectY = height * 0.22;
147+
const rectW = 260 * scale;
148+
const rectH = 100 * scale;
149+
const rectAngle = 0.08;
150+
151+
ctx.save();
152+
ctx.translate(rectX, rectY);
153+
ctx.rotate(rectAngle);
154+
ctx.globalAlpha = 0.7;
155+
156+
// Border
157+
ctx.strokeStyle = secondaryColor;
158+
ctx.lineWidth = 3 * scale;
159+
ctx.strokeRect(-rectW / 2, -rectH / 2, rectW, rectH);
160+
ctx.strokeRect(-rectW / 2 + 6 * scale, -rectH / 2 + 6 * scale, rectW - 12 * scale, rectH - 12 * scale);
161+
162+
// Language header
163+
ctx.fillStyle = secondaryColor;
164+
ctx.font = `bold ${14 * scale}px "JetBrains Mono"`;
165+
ctx.textAlign = 'center';
166+
ctx.textBaseline = 'middle';
167+
ctx.fillText('LANGUAGE', 0, -rectH / 2 + 24 * scale);
168+
169+
// Language value
170+
ctx.font = `bold ${32 * scale}px "JetBrains Mono"`;
171+
ctx.fillText(language.toUpperCase(), 0, 4 * scale);
172+
173+
// Forks at bottom
174+
if (forks) {
175+
ctx.font = `${14 * scale}px "JetBrains Mono"`;
176+
ctx.fillText(`FORKS: ${forks}`, 0, rectH / 2 - 20 * scale);
177+
}
178+
179+
ctx.restore();
180+
181+
// --- Small date/exit stamp (description area) ---
182+
const smallStampX = width * 0.72;
183+
const smallStampY = height * 0.68;
184+
const smallR = 70 * scale;
185+
const smallAngle = 0.3;
186+
187+
ctx.save();
188+
ctx.translate(smallStampX, smallStampY);
189+
ctx.rotate(smallAngle);
190+
ctx.globalAlpha = 0.5;
191+
ctx.strokeStyle = '#7a6a5a';
192+
ctx.fillStyle = '#7a6a5a';
193+
ctx.lineWidth = 2.5 * scale;
194+
195+
ctx.beginPath();
196+
ctx.arc(0, 0, smallR, 0, Math.PI * 2);
197+
ctx.stroke();
198+
199+
// Horizontal lines through center
200+
ctx.fillRect(-smallR + 10 * scale, -6 * scale, smallR * 2 - 20 * scale, 1.5 * scale);
201+
ctx.fillRect(-smallR + 10 * scale, 6 * scale, smallR * 2 - 20 * scale, 1.5 * scale);
202+
203+
ctx.font = `bold ${11 * scale}px "JetBrains Mono"`;
204+
ctx.textAlign = 'center';
205+
ctx.textBaseline = 'middle';
206+
ctx.fillText('OPEN SOURCE', 0, -18 * scale);
207+
ctx.font = `bold ${14 * scale}px "JetBrains Mono"`;
208+
ctx.fillText('APPROVED', 0, 0);
209+
ctx.font = `${10 * scale}px "JetBrains Mono"`;
210+
ctx.fillText('ENTRY GRANTED', 0, 18 * scale);
211+
212+
ctx.restore();
213+
214+
// --- Description: handwritten-style text along left side ---
215+
const descX = 50 * scale;
216+
const descY = height * 0.72;
217+
218+
ctx.save();
219+
ctx.globalAlpha = 0.6;
220+
ctx.fillStyle = '#3a3028';
221+
ctx.font = `italic ${24 * scale}px "Georgia", serif`;
222+
ctx.textAlign = 'left';
223+
ctx.textBaseline = 'top';
224+
225+
// Manual word wrap
226+
const words = description.split(' ');
227+
let line = '';
228+
let lineY = descY;
229+
const maxW = width * 0.45;
230+
for (let i = 0; i < words.length; i++) {
231+
const test = line + words[i] + ' ';
232+
if (ctx.measureText(test).width > maxW && i > 0) {
233+
ctx.fillText(line.trim(), descX, lineY);
234+
line = words[i] + ' ';
235+
lineY += 32 * scale;
236+
} else {
237+
line = test;
238+
}
239+
}
240+
ctx.fillText(line.trim(), descX, lineY);
241+
ctx.restore();
242+
243+
// --- Support URL as a small printed footer ---
244+
if (supportUrl) {
245+
ctx.save();
246+
ctx.globalAlpha = 0.3;
247+
ctx.fillStyle = '#5a4a3a';
248+
ctx.font = `${14 * scale}px "JetBrains Mono"`;
249+
ctx.textAlign = 'right';
250+
ctx.textBaseline = 'bottom';
251+
ctx.fillText(supportUrl, width - 30 * scale, height - 20 * scale);
252+
ctx.restore();
253+
}
254+
255+
// --- Page number / passport number top-right ---
256+
ctx.save();
257+
ctx.globalAlpha = 0.15;
258+
ctx.fillStyle = '#3a3028';
259+
ctx.font = `${12 * scale}px "JetBrains Mono"`;
260+
ctx.textAlign = 'right';
261+
ctx.textBaseline = 'top';
262+
ctx.fillText('PAGE 01 / 01', width - 30 * scale, 20 * scale);
263+
ctx.restore();
264+
265+
// --- Perforated edge on right side (passport page edge) ---
266+
ctx.save();
267+
ctx.globalAlpha = 0.12;
268+
ctx.fillStyle = '#8b7355';
269+
for (let y = 20 * scale; y < height - 20 * scale; y += 12 * scale) {
270+
ctx.beginPath();
271+
ctx.arc(width - 6 * scale, y, 2 * scale, 0, Math.PI * 2);
272+
ctx.fill();
273+
}
274+
ctx.restore();
275+
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const splitFlap = (ctx, width, height, scale, data) => {
171171
ctx.font = `bold ${16 * scale}px "JetBrains Mono"`;
172172
const descUpper = description.toUpperCase();
173173
const descRows = Math.ceil(descUpper.length / descMaxPerRow);
174-
const maxRows = 2;
174+
const maxRows = 4;
175175
for (let row = 0; row < Math.min(descRows, maxRows); row++) {
176176
const slice = descUpper.substring(row * descMaxPerRow, (row + 1) * descMaxPerRow);
177177
drawFlapRow(contentX, descY + row * (descCellH + 4 * scale), slice, descCellW, descCellH, descGap, 'rgba(255,255,255,0.7)', slice.length);

0 commit comments

Comments
 (0)