Skip to content

Commit 97a0bbf

Browse files
committed
feat: add Classified Document theme with readable redactions and adjustable background
1 parent 2d29a34 commit 97a0bbf

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const THEME_OPTIONS = [
7373
{ value: 'hauteCouture', label: 'HAUTE_COUTURE' },
7474
{ value: 'missionControl', label: 'MISSION_CONTROL' },
7575
{ value: 'etherealGlow', label: 'ETHEREAL_GLOW' },
76+
{ value: 'boldMinimal', label: 'CLASSIFIED_DOC' },
7677
];
7778

7879
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { vinylRecord } from './themes/vinylRecord';
5353
import { hauteCouture } from './themes/hauteCouture';
5454
import { missionControl } from './themes/missionControl';
5555
import { etherealGlow } from './themes/etherealGlow';
56+
import { boldMinimal } from './themes/boldMinimal';
5657

5758
export const themeRenderers = {
5859
modern,
@@ -110,4 +111,5 @@ export const themeRenderers = {
110111
hauteCouture,
111112
missionControl,
112113
etherealGlow,
114+
boldMinimal,
113115
};
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
export const boldMinimal = (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+
// === PAPER BACKGROUND ===
17+
ctx.fillStyle = bgColor;
18+
ctx.fillRect(0, 0, width, height);
19+
20+
// Aged paper texture
21+
if (showPattern) {
22+
ctx.save();
23+
ctx.globalAlpha = 0.03;
24+
for (let i = 0; i < 500; i++) {
25+
const x = ((i * 151 + 19) % 1280) * (width / 1280);
26+
const y = ((i * 83 + 41) % 640) * (height / 640);
27+
ctx.fillStyle = i % 2 === 0 ? '#c8b89a' : '#a09078';
28+
ctx.fillRect(x, y, (1 + (i % 3)) * scale, scale);
29+
}
30+
ctx.restore();
31+
32+
// Coffee stain ring — top right
33+
ctx.save();
34+
ctx.globalAlpha = 0.035;
35+
ctx.strokeStyle = '#8b7355';
36+
ctx.lineWidth = 8 * scale;
37+
ctx.beginPath();
38+
ctx.arc(width * 0.82, height * 0.18, 55 * scale, 0.3, Math.PI * 1.7);
39+
ctx.stroke();
40+
ctx.restore();
41+
}
42+
43+
// Fold crease — faint vertical line
44+
ctx.save();
45+
ctx.globalAlpha = 0.06;
46+
ctx.strokeStyle = '#8b7355';
47+
ctx.lineWidth = 1 * scale;
48+
ctx.beginPath();
49+
ctx.moveTo(width * 0.5, 0);
50+
ctx.lineTo(width * 0.5, height);
51+
ctx.stroke();
52+
ctx.restore();
53+
54+
const pad = 70 * scale;
55+
const contentW = width - pad * 2;
56+
57+
// === HEADER: Classification banner ===
58+
ctx.fillStyle = primaryColor;
59+
ctx.fillRect(0, 0, width, 44 * scale);
60+
ctx.fillStyle = '#fff';
61+
ctx.font = `bold ${18 * scale}px "JetBrains Mono"`;
62+
ctx.textAlign = 'center';
63+
ctx.textBaseline = 'middle';
64+
ctx.fillText('CLASSIFIED — CONFIDENTIAL — AUTHORIZED PERSONNEL ONLY', width / 2, 22 * scale);
65+
66+
// Bottom banner mirror
67+
ctx.fillStyle = primaryColor;
68+
ctx.fillRect(0, height - 44 * scale, width, 44 * scale);
69+
ctx.fillStyle = '#fff';
70+
ctx.font = `bold ${18 * scale}px "JetBrains Mono"`;
71+
ctx.textAlign = 'center';
72+
ctx.fillText('CLASSIFIED — CONFIDENTIAL — AUTHORIZED PERSONNEL ONLY', width / 2, height - 22 * scale);
73+
74+
// === DOCUMENT BODY ===
75+
let curY = 44 * scale + pad * 0.6;
76+
77+
// Document header
78+
ctx.fillStyle = '#2a2a2a';
79+
ctx.font = `${18 * scale}px "JetBrains Mono"`;
80+
ctx.textAlign = 'left';
81+
ctx.textBaseline = 'top';
82+
ctx.fillText('DEPARTMENT OF OPEN SOURCE INTELLIGENCE', pad, curY);
83+
curY += 26 * scale;
84+
ctx.fillStyle = '#888';
85+
ctx.font = `${15 * scale}px "JetBrains Mono"`;
86+
ctx.fillText(`CASE FILE: ${repoOwner.toUpperCase()}-${repoName.toUpperCase().substring(0, 8)}-001`, pad, curY);
87+
curY += 34 * scale;
88+
89+
// Thin rule
90+
ctx.fillStyle = '#ccc';
91+
ctx.fillRect(pad, curY, contentW, 1 * scale);
92+
curY += 20 * scale;
93+
94+
// Field: SUBJECT
95+
ctx.fillStyle = '#888';
96+
ctx.font = `${15 * scale}px "JetBrains Mono"`;
97+
ctx.fillText('SUBJECT:', pad, curY);
98+
99+
// Repo name — large typewriter style
100+
curY += 24 * scale;
101+
ctx.fillStyle = '#1a1a1a';
102+
let nameSize = 90 * scale;
103+
ctx.font = `bold ${nameSize}px "Courier New", monospace`;
104+
while (ctx.measureText(repoName).width > contentW && nameSize > 30 * scale) {
105+
nameSize -= 4 * scale;
106+
ctx.font = `bold ${nameSize}px "Courier New", monospace`;
107+
}
108+
ctx.fillText(repoName, pad, curY);
109+
curY += nameSize + 12 * scale;
110+
111+
// Field: ORIGINATOR
112+
ctx.fillStyle = '#888';
113+
ctx.font = `${15 * scale}px "JetBrains Mono"`;
114+
ctx.fillText('ORIGINATOR:', pad, curY);
115+
ctx.fillStyle = '#2a2a2a';
116+
ctx.font = `${20 * scale}px "Courier New", monospace`;
117+
ctx.fillText(repoOwner, pad + 140 * scale, curY);
118+
curY += 34 * scale;
119+
120+
// Field: DETAILS — with partial redaction
121+
ctx.fillStyle = '#888';
122+
ctx.font = `${15 * scale}px "JetBrains Mono"`;
123+
ctx.fillText('DETAILS:', pad, curY);
124+
curY += 24 * scale;
125+
126+
ctx.fillStyle = '#333';
127+
ctx.font = `${24 * scale}px "Courier New", monospace`;
128+
const words = description.split(' ');
129+
let line = '';
130+
const descLines = [];
131+
for (let i = 0; i < words.length; i++) {
132+
const test = line + words[i] + ' ';
133+
if (ctx.measureText(test).width > contentW && i > 0) {
134+
descLines.push(line.trim());
135+
line = words[i] + ' ';
136+
} else {
137+
line = test;
138+
}
139+
}
140+
descLines.push(line.trim());
141+
142+
descLines.forEach((dl, idx) => {
143+
ctx.fillStyle = '#333';
144+
ctx.font = `${24 * scale}px "Courier New", monospace`;
145+
ctx.fillText(dl, pad, curY);
146+
147+
// Redact random words (deterministic based on index)
148+
if (idx < descLines.length - 1) {
149+
const dlWords = dl.split(' ');
150+
let wx = pad;
151+
dlWords.forEach((w, wi) => {
152+
const ww = ctx.measureText(w + ' ').width;
153+
if ((wi + idx * 3) % 5 === 2 && w.length > 3) {
154+
// Redaction bar with inverted text
155+
ctx.fillStyle = '#1a1a1a';
156+
ctx.fillRect(wx - 2 * scale, curY - 2 * scale, ww - 4 * scale, 28 * scale);
157+
ctx.fillStyle = bgColor;
158+
ctx.fillText(w, wx, curY);
159+
}
160+
wx += ww;
161+
});
162+
}
163+
164+
curY += 32 * scale;
165+
});
166+
167+
curY += 10 * scale;
168+
169+
// Field row: LANG / STARS / FORKS
170+
ctx.fillStyle = '#ccc';
171+
ctx.fillRect(pad, curY, contentW, 1 * scale);
172+
curY += 16 * scale;
173+
174+
ctx.font = `${15 * scale}px "JetBrains Mono"`;
175+
ctx.textBaseline = 'top';
176+
177+
// Language
178+
ctx.fillStyle = '#888';
179+
ctx.fillText('LANG:', pad, curY);
180+
ctx.fillStyle = '#2a2a2a';
181+
ctx.font = `bold ${20 * scale}px "Courier New", monospace`;
182+
ctx.fillText(language, pad + 70 * scale, curY);
183+
184+
// Stars
185+
if (stars) {
186+
ctx.fillStyle = '#888';
187+
ctx.font = `${15 * scale}px "JetBrains Mono"`;
188+
ctx.fillText('STARS:', pad + 280 * scale, curY);
189+
ctx.fillStyle = '#2a2a2a';
190+
ctx.font = `bold ${20 * scale}px "Courier New", monospace`;
191+
ctx.fillText(String(stars), pad + 355 * scale, curY);
192+
}
193+
194+
// Forks
195+
if (forks) {
196+
ctx.fillStyle = '#888';
197+
ctx.font = `${15 * scale}px "JetBrains Mono"`;
198+
ctx.fillText('FORKS:', pad + 480 * scale, curY);
199+
ctx.fillStyle = '#2a2a2a';
200+
ctx.font = `bold ${20 * scale}px "Courier New", monospace`;
201+
ctx.fillText(String(forks), pad + 555 * scale, curY);
202+
}
203+
204+
// === CLASSIFIED STAMP — large, rotated ===
205+
ctx.save();
206+
ctx.translate(width * 0.72, height * 0.45);
207+
ctx.rotate(-0.18);
208+
ctx.globalAlpha = 0.2;
209+
210+
// Stamp border
211+
ctx.strokeStyle = secondaryColor;
212+
ctx.lineWidth = 4 * scale;
213+
const stampW = 340 * scale;
214+
const stampH = 110 * scale;
215+
ctx.strokeRect(-stampW / 2, -stampH / 2, stampW, stampH);
216+
217+
// Double border
218+
ctx.lineWidth = 2 * scale;
219+
ctx.strokeRect(-stampW / 2 + 6 * scale, -stampH / 2 + 6 * scale, stampW - 12 * scale, stampH - 12 * scale);
220+
221+
// Stamp text
222+
ctx.fillStyle = secondaryColor;
223+
ctx.font = `bold ${58 * scale}px "Courier New", monospace`;
224+
ctx.textAlign = 'center';
225+
ctx.textBaseline = 'middle';
226+
ctx.fillText('CLASSIFIED', 0, 0);
227+
ctx.restore();
228+
229+
// === APPROVAL STAMP — smaller, different angle ===
230+
ctx.save();
231+
ctx.translate(width * 0.2, height * 0.7);
232+
ctx.rotate(0.12);
233+
ctx.globalAlpha = 0.15;
234+
235+
ctx.strokeStyle = primaryColor;
236+
ctx.lineWidth = 3 * scale;
237+
ctx.beginPath();
238+
ctx.arc(0, 0, 58 * scale, 0, Math.PI * 2);
239+
ctx.stroke();
240+
ctx.beginPath();
241+
ctx.arc(0, 0, 50 * scale, 0, Math.PI * 2);
242+
ctx.stroke();
243+
244+
ctx.fillStyle = primaryColor;
245+
ctx.font = `bold ${20 * scale}px "Courier New", monospace`;
246+
ctx.textAlign = 'center';
247+
ctx.textBaseline = 'middle';
248+
ctx.fillText('APPROVED', 0, -8 * scale);
249+
ctx.font = `${13 * scale}px "Courier New", monospace`;
250+
ctx.fillText('OPEN SOURCE', 0, 14 * scale);
251+
ctx.restore();
252+
253+
// === Support URL as file reference ===
254+
if (supportUrl) {
255+
ctx.fillStyle = '#aaa';
256+
ctx.font = `${14 * scale}px "JetBrains Mono"`;
257+
ctx.textAlign = 'right';
258+
ctx.textBaseline = 'bottom';
259+
ctx.fillText(`REF: ${supportUrl}`, width - pad, height - 44 * scale - 12 * scale);
260+
}
261+
};

0 commit comments

Comments
 (0)