Skip to content

Commit 1b3f570

Browse files
committed
feat(apps): add graphic novel and windows 95 themes to github thumbnail generator
1 parent d10ba6e commit 1b3f570

File tree

4 files changed

+333
-1
lines changed

4 files changed

+333
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const THEME_OPTIONS = [
4141
{ value: 'rich', label: 'LUXURY_GOLD' },
4242
{ value: 'abstract', label: 'ABSTRACT_SHAPES' },
4343
{ value: 'nature', label: 'NATURE_VIBES' },
44+
{ value: 'graphicNovel', label: 'SIN_CITY_STYLE' },
45+
{ value: 'win95', label: 'RETRO_WINDOWS_95' },
4446
];
4547

4648
const GithubThumbnailGeneratorPage = () => {

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { gta } from './themes/gta';
2121
import { rich } from './themes/rich';
2222
import { abstract } from './themes/abstract';
2323
import { nature } from './themes/nature';
24+
import { graphicNovel } from './themes/graphicNovel';
25+
import { win95 } from './themes/win95';
2426

2527
export const themeRenderers = {
2628
modern,
@@ -46,4 +48,6 @@ export const themeRenderers = {
4648
rich,
4749
abstract,
4850
nature,
49-
};
51+
graphicNovel,
52+
win95,
53+
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { wrapText } from '../utils';
2+
3+
export const graphicNovel = (ctx, width, height, scale, data) => {
4+
const { repoOwner, repoName, description, language, stars } = data;
5+
// GRAPHIC NOVEL (Sin City Style) - High Contrast Noir
6+
7+
// 1. Background: Pure Black
8+
ctx.fillStyle = '#000000';
9+
ctx.fillRect(0, 0, width, height);
10+
11+
// 2. High Contrast "Light" (Rain/Silhouettes)
12+
ctx.fillStyle = '#ffffff';
13+
14+
// Rain/Slashes
15+
ctx.save();
16+
ctx.rotate(Math.PI / 8);
17+
for(let i=0; i<100; i++) {
18+
const x = Math.random() * width * 1.5 - width * 0.5;
19+
const y = Math.random() * height * 1.5 - height * 0.5;
20+
const w = (Math.random() * 5 + 2) * scale;
21+
const h = (Math.random() * 100 + 50) * scale;
22+
ctx.fillRect(x, y, w, h);
23+
}
24+
ctx.restore();
25+
26+
// 3. Silhouette Cityscape (Bottom)
27+
ctx.fillStyle = '#ffffff';
28+
ctx.beginPath();
29+
ctx.moveTo(0, height);
30+
let currentH = height * 0.7;
31+
for(let x=0; x<=width; x+=50*scale) {
32+
ctx.lineTo(x, currentH);
33+
currentH = height * (0.6 + Math.random() * 0.3);
34+
ctx.lineTo(x + 50*scale, currentH);
35+
}
36+
ctx.lineTo(width, height);
37+
ctx.fill();
38+
39+
// Windows in silhouette (Black)
40+
ctx.fillStyle = '#000000';
41+
for(let i=0; i<20; i++) {
42+
const x = Math.random() * width;
43+
const y = height * 0.7 + Math.random() * height * 0.2;
44+
const size = (Math.random() * 10 + 5) * scale;
45+
ctx.fillRect(x, y, size, size * 1.5);
46+
}
47+
48+
const padding = 60 * scale;
49+
50+
// 4. Text - Comic Book Style
51+
// Main Title: Red Splash
52+
ctx.fillStyle = '#ff0000'; // Blood Red
53+
ctx.font = `900 ${110 * scale}px "Impact", "Arial Black", sans-serif`;
54+
ctx.textAlign = 'left';
55+
56+
// Rough Shadow
57+
ctx.shadowColor = '#fff';
58+
ctx.shadowBlur = 0;
59+
ctx.shadowOffsetX = 4 * scale;
60+
ctx.shadowOffsetY = 4 * scale;
61+
ctx.fillText(repoName.toUpperCase(), padding, height * 0.4);
62+
63+
// Reset Shadow
64+
ctx.shadowOffsetX = 0;
65+
ctx.shadowOffsetY = 0;
66+
67+
// Repo Owner - White blocky
68+
ctx.fillStyle = '#ffffff';
69+
ctx.font = `bold ${40 * scale}px "Arial Narrow", sans-serif`;
70+
ctx.fillText(repoOwner.toUpperCase(), padding, height * 0.25);
71+
72+
// Description - White Serif (Typewriter style)
73+
ctx.fillStyle = '#ffffff';
74+
ctx.font = `normal ${30 * scale}px "Courier New", monospace`;
75+
wrapText(ctx, description, padding, height * 0.55, width * 0.6, 40 * scale);
76+
77+
// 5. Speech Bubble for Stats
78+
const bubbleX = width * 0.75;
79+
const bubbleY = height * 0.2;
80+
const bubbleR = 120 * scale;
81+
82+
ctx.fillStyle = '#ffffff';
83+
ctx.beginPath();
84+
ctx.arc(bubbleX, bubbleY, bubbleR, 0, Math.PI * 2);
85+
ctx.fill();
86+
// Tail
87+
ctx.beginPath();
88+
ctx.moveTo(bubbleX - 50*scale, bubbleY + 80*scale);
89+
ctx.lineTo(bubbleX - 80*scale, bubbleY + 150*scale); // Pointing down/left
90+
ctx.lineTo(bubbleX, bubbleY + 100*scale);
91+
ctx.fill();
92+
93+
// Stats Text (Black inside bubble)
94+
ctx.fillStyle = '#000000';
95+
ctx.textAlign = 'center';
96+
ctx.font = `bold ${30 * scale}px "Comic Sans MS", "Arial", sans-serif`;
97+
ctx.fillText("STATS:", bubbleX, bubbleY - 20 * scale);
98+
ctx.font = `bold ${24 * scale}px "Comic Sans MS", "Arial", sans-serif`;
99+
100+
let statText = language;
101+
if (language.length > 10) statText = language.substring(0, 8) + '..';
102+
103+
ctx.fillText(statText, bubbleX, bubbleY + 20 * scale);
104+
ctx.fillText(`${stars || 0} ★`, bubbleX, bubbleY + 60 * scale);
105+
106+
// 6. Accent Splatter (Red)
107+
ctx.fillStyle = 'rgba(255, 0, 0, 0.7)';
108+
for(let i=0; i<10; i++) {
109+
const r = Math.random() * 20 * scale;
110+
ctx.beginPath();
111+
ctx.arc(width - padding - Math.random()*100*scale, height - padding - Math.random()*100*scale, r, 0, Math.PI*2);
112+
ctx.fill();
113+
}
114+
};
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { wrapText } from '../utils';
2+
3+
export const win95 = (ctx, width, height, scale, data) => {
4+
const { repoOwner, repoName, description } = data;
5+
// WINDOWS 95 Style
6+
7+
// 1. Desktop Background (Teal)
8+
ctx.fillStyle = '#008080';
9+
ctx.fillRect(0, 0, width, height);
10+
11+
// 2. Main Window
12+
const winW = width * 0.7;
13+
const winH = height * 0.6;
14+
const winX = (width - winW) / 2;
15+
const winY = (height - winH) / 2;
16+
17+
const gray = '#c0c0c0';
18+
const darkGray = '#808080';
19+
const white = '#ffffff';
20+
const black = '#000000';
21+
const navy = '#000080';
22+
23+
// Window Body
24+
ctx.fillStyle = gray;
25+
ctx.fillRect(winX, winY, winW, winH);
26+
27+
// 3D Borders (Bevel)
28+
const drawBevel = (x, y, w, h, isPressed = false) => {
29+
const t = 2 * scale; // thickness
30+
// Top/Left
31+
ctx.fillStyle = isPressed ? darkGray : white;
32+
ctx.fillRect(x, y, w, t); // Top
33+
ctx.fillRect(x, y, t, h); // Left
34+
35+
// Bottom/Right
36+
ctx.fillStyle = isPressed ? white : black;
37+
ctx.fillRect(x, y + h - t, w, t); // Bottom
38+
ctx.fillRect(x + w - t, y, t, h); // Right
39+
40+
// Inner shadow for unpressed
41+
if (!isPressed) {
42+
ctx.fillStyle = darkGray;
43+
ctx.fillRect(x + t, y + h - t*2, w - t*2, t);
44+
ctx.fillRect(x + w - t*2, y + t, t, h - t*2);
45+
}
46+
};
47+
48+
drawBevel(winX, winY, winW, winH);
49+
50+
// Title Bar
51+
const titleH = 40 * scale;
52+
const titlePad = 4 * scale;
53+
ctx.fillStyle = navy;
54+
ctx.fillRect(winX + titlePad, winY + titlePad, winW - titlePad*2, titleH);
55+
56+
// Title Text
57+
ctx.fillStyle = white;
58+
ctx.textAlign = 'left';
59+
ctx.font = `bold ${20 * scale}px "Arial", sans-serif`;
60+
ctx.fillText(`${repoOwner} - Notepad`, winX + titlePad + 10*scale, winY + titlePad + 28*scale);
61+
62+
// X Button
63+
const btnSize = titleH - 4*scale;
64+
const btnX = winX + winW - titlePad - btnSize - 2*scale;
65+
const btnY = winY + titlePad + 2*scale;
66+
ctx.fillStyle = gray;
67+
ctx.fillRect(btnX, btnY, btnSize, btnSize);
68+
drawBevel(btnX, btnY, btnSize, btnSize);
69+
ctx.fillStyle = black;
70+
ctx.textAlign = 'center';
71+
ctx.font = `bold ${18 * scale}px "Arial", sans-serif`;
72+
ctx.fillText("X", btnX + btnSize/2, btnY + btnSize/2 + 6*scale);
73+
74+
// Menu Bar (File Edit View...)
75+
const menuY = winY + titlePad + titleH;
76+
77+
ctx.fillStyle = black;
78+
ctx.textAlign = 'left';
79+
ctx.font = `normal ${18 * scale}px "Arial", sans-serif`;
80+
ctx.fillText("File Edit Search Help", winX + 15*scale, menuY + 20*scale);
81+
82+
// Text Area (White input box)
83+
const areaX = winX + 10*scale;
84+
const areaY = menuY + 30*scale;
85+
const areaW = winW - 20*scale;
86+
const areaH = winH - (areaY - winY) - 10*scale;
87+
88+
ctx.fillStyle = white;
89+
ctx.fillRect(areaX, areaY, areaW, areaH);
90+
// Inset border for text area
91+
ctx.fillStyle = darkGray; // Top/Left shadow
92+
ctx.fillRect(areaX, areaY, areaW, 2*scale);
93+
ctx.fillRect(areaX, areaY, 2*scale, areaH);
94+
ctx.fillStyle = '#dfdfdf'; // Bottom/Right highlight (light gray)
95+
ctx.fillRect(areaX, areaY + areaH - 2*scale, areaW, 2*scale);
96+
ctx.fillRect(areaX + areaW - 2*scale, areaY, 2*scale, areaH);
97+
98+
// Content Text
99+
ctx.fillStyle = black;
100+
ctx.font = `bold ${60 * scale}px "Courier New", monospace`; // Monospace for that raw feel
101+
ctx.fillText(repoName, areaX + 20*scale, areaY + 80*scale);
102+
103+
ctx.font = `normal ${24 * scale}px "Courier New", monospace`;
104+
wrapText(ctx, description, areaX + 20*scale, areaY + 140*scale, areaW - 40*scale, 35*scale);
105+
106+
// Cursor (Blinking)
107+
// We can't actually blink in static canvas, but we draw it
108+
// Let's put it after the description? Or just at the end.
109+
// Simplifying: Just drawing it below description
110+
// ctx.fillRect(areaX + 20*scale, areaY + 200*scale, 15*scale, 2*scale); // Underscore cursor
111+
112+
// 3. Desktop Icons (Left side)
113+
const iconSize = 60 * scale;
114+
const startIconY = 40 * scale;
115+
const iconGap = 100 * scale;
116+
117+
const drawIcon = (label, y, type='folder') => {
118+
const x = 40 * scale;
119+
// Icon graphic (Simple pixel art approx)
120+
if (type === 'pc') {
121+
ctx.fillStyle = black;
122+
ctx.fillRect(x, y, iconSize, iconSize * 0.8);
123+
ctx.fillStyle = '#00ffff'; // Screen
124+
ctx.fillRect(x+4*scale, y+4*scale, iconSize-8*scale, iconSize*0.5);
125+
} else {
126+
// Folder
127+
ctx.fillStyle = '#ffd700';
128+
ctx.beginPath();
129+
ctx.moveTo(x, y);
130+
ctx.lineTo(x + iconSize*0.4, y);
131+
ctx.lineTo(x + iconSize*0.5, y + iconSize*0.1);
132+
ctx.lineTo(x + iconSize, y + iconSize*0.1);
133+
ctx.lineTo(x + iconSize, y + iconSize*0.8);
134+
ctx.lineTo(x, y + iconSize*0.8);
135+
ctx.fill();
136+
ctx.stroke();
137+
}
138+
139+
// Label
140+
ctx.fillStyle = white; // Icon text usually has transparent bg in 95 but let's just do white text with shadow
141+
ctx.font = `normal ${16 * scale}px "Arial", sans-serif`;
142+
ctx.textAlign = 'center';
143+
144+
// Dotted focus rect for selected icon?
145+
// ctx.setLineDash([2, 2]);
146+
// ctx.strokeStyle = white;
147+
// ctx.strokeRect(x - 10*scale, y + iconSize, iconSize + 20*scale, 20*scale);
148+
// ctx.setLineDash([]);
149+
150+
ctx.fillText(label, x + iconSize/2, y + iconSize + 20*scale);
151+
};
152+
153+
drawIcon("My Computer", startIconY, 'pc');
154+
drawIcon("Network", startIconY + iconGap, 'pc');
155+
drawIcon("Recycle Bin", startIconY + iconGap*2, 'folder');
156+
157+
// 4. Taskbar
158+
const taskH = 40 * scale;
159+
const taskY = height - taskH;
160+
ctx.fillStyle = gray;
161+
ctx.fillRect(0, taskY, width, taskH);
162+
ctx.fillStyle = white; // Top highlight
163+
ctx.fillRect(0, taskY, width, 2*scale);
164+
165+
// Start Button
166+
const startW = 100 * scale;
167+
const startH = taskH - 6*scale;
168+
const startX = 4*scale;
169+
const startY = taskY + 3*scale;
170+
drawBevel(startX, startY, startW, startH);
171+
172+
ctx.fillStyle = black;
173+
ctx.font = `bold ${18 * scale}px "Arial", sans-serif`;
174+
ctx.textAlign = 'left';
175+
ctx.fillText("Start", startX + 35*scale, startY + 22*scale);
176+
177+
// Windows Logo on start button (Simple blocks)
178+
const logoX = startX + 6*scale;
179+
const logoY = startY + 6*scale;
180+
const logoS = 20*scale;
181+
ctx.fillStyle = '#ff0000'; ctx.fillRect(logoX, logoY, logoS/2, logoS/2);
182+
ctx.fillStyle = '#00ff00'; ctx.fillRect(logoX + logoS/2, logoY, logoS/2, logoS/2);
183+
ctx.fillStyle = '#0000ff'; ctx.fillRect(logoX, logoY + logoS/2, logoS/2, logoS/2);
184+
ctx.fillStyle = '#ffff00'; ctx.fillRect(logoX + logoS/2, logoY + logoS/2, logoS/2, logoS/2);
185+
186+
// Tray Area (Time)
187+
const trayW = 100 * scale;
188+
const trayX = width - trayW - 4*scale;
189+
const trayY = taskY + 4*scale;
190+
const trayH = taskH - 8*scale;
191+
192+
// Sunken tray
193+
ctx.fillStyle = white; ctx.fillRect(trayX + trayW - 2*scale, trayY, 2*scale, trayH); // Right
194+
ctx.fillRect(trayX, trayY + trayH - 2*scale, trayW, 2*scale); // Bottom
195+
ctx.fillStyle = darkGray; ctx.fillRect(trayX, trayY, trayW, 2*scale); // Top
196+
ctx.fillRect(trayX, trayY, 2*scale, trayH); // Left
197+
198+
ctx.fillStyle = black;
199+
ctx.textAlign = 'center';
200+
ctx.fillText("10:00 AM", trayX + trayW/2, trayY + 22*scale);
201+
202+
// App Button on Taskbar (Active)
203+
const appW = 200 * scale;
204+
const appX = startX + startW + 10*scale;
205+
const appY = startY;
206+
207+
// Pressed look
208+
drawBevel(appX, appY, appW, startH, true);
209+
ctx.fillStyle = black;
210+
ctx.textAlign = 'left';
211+
ctx.fillText(`${repoName} - Notepad`, appX + 10*scale, appY + 22*scale);
212+
};

0 commit comments

Comments
 (0)