Skip to content

Commit 8b5174e

Browse files
committed
feat(thumbnail): add Atelier Gallery + Riso Print styles
Atelier Gallery: museum wall-label composition matching the new Atelier app chrome. Cream paper with gilt hairline frame, left-side painterly color blocks, right-side plaque with accession number, italic Garamond title (jade period), wrapped caption, medium/stars/forks/provenance columns, and a jade wax seal. Riso Print: two-colour risograph with halftone dithering. Paper ground, chunky Syne title with secondary-colour misregistration offset, halftone dot bursts in primary + secondary, registration crosshairs, edition / catalog number, and paper grain overlay.
1 parent 10b18ac commit 8b5174e

4 files changed

Lines changed: 457 additions & 0 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ const THEME_OPTIONS = [
9898
{ value: 'abstractNonsense', label: 'Abstract Nonsense' },
9999
{ value: 'needForSpeed', label: 'Need For Speed' },
100100
{ value: 'terracotta', label: 'Terracotta Plate' },
101+
{ value: 'atelierGallery', label: 'Atelier Gallery' },
102+
{ value: 'risoPrint', label: 'Riso Print' },
101103
];
102104

103105
const PALETTE_BANK = [

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ import { dithered } from './themes/dithered';
7070
import { abstractNonsense } from './themes/abstractNonsense';
7171
import { needForSpeed } from './themes/needForSpeed';
7272
import { terracotta } from './themes/terracotta';
73+
import { atelierGallery } from './themes/atelierGallery';
74+
import { risoPrint } from './themes/risoPrint';
7375

7476
export const themeRenderers = {
7577
modern,
@@ -141,4 +143,6 @@ export const themeRenderers = {
141143
abstractNonsense,
142144
needForSpeed,
143145
terracotta,
146+
atelierGallery,
147+
risoPrint,
144148
};
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { wrapText } from '../utils';
2+
3+
/*
4+
* ATELIER_GALLERY — a museum wall-label composition.
5+
*
6+
* Cream paper with a gilt hairline frame. Left: a painterly composition of
7+
* overlapping color blocks using the palette. Right: a gallery plaque with
8+
* accession number, italic wordmark, dimensions, medium, and descriptive
9+
* caption set in EB Garamond. Signature move: a small jade period after the
10+
* repo name and a faint jade seal in the corner.
11+
*/
12+
export const atelierGallery = (ctx, width, height, scale, data) => {
13+
const {
14+
repoOwner,
15+
repoName,
16+
description,
17+
language,
18+
stars,
19+
forks,
20+
primaryColor,
21+
secondaryColor,
22+
bgColor,
23+
} = data;
24+
25+
const WALL = '#E4D9D6';
26+
const PAPER = '#F5EFEC';
27+
const PAPER_SOFT = '#EEE7E2';
28+
const INK = '#2D1F2E';
29+
const INK_SOFT = '#6B5A65';
30+
const JADE = '#3F7D6B';
31+
const GILT = '#B89968';
32+
33+
const serif = '"EB Garamond", "Iowan Old Style", serif';
34+
const sans = '"Nunito", system-ui, sans-serif';
35+
36+
/* Seeded RNG for deterministic composition */
37+
const seed = `${repoOwner || ''}${repoName || ''}`;
38+
let h = 0xb8_99_68_f0;
39+
for (let i = 0; i < seed.length; i++) {
40+
h = Math.imul(h ^ seed.charCodeAt(i), 2654435761);
41+
}
42+
const rng = () => {
43+
h = Math.imul(h ^ (h >>> 16), 2246822507);
44+
h = Math.imul(h ^ (h >>> 13), 3266489909);
45+
return ((h ^= h >>> 16) >>> 0) / 4294967296;
46+
};
47+
48+
/* Paper ground with a faint mauve wash at the edges */
49+
ctx.fillStyle = PAPER;
50+
ctx.fillRect(0, 0, width, height);
51+
const wash = ctx.createRadialGradient(
52+
width * 0.5,
53+
height * 0.5,
54+
width * 0.2,
55+
width * 0.5,
56+
height * 0.5,
57+
width * 0.75,
58+
);
59+
wash.addColorStop(0, 'rgba(228,217,214,0)');
60+
wash.addColorStop(1, 'rgba(228,217,214,0.45)');
61+
ctx.fillStyle = wash;
62+
ctx.fillRect(0, 0, width, height);
63+
64+
/* Subtle paper grain */
65+
ctx.save();
66+
ctx.globalAlpha = 0.04;
67+
for (let i = 0; i < 1400; i++) {
68+
const x = rng() * width;
69+
const y = rng() * height;
70+
const s = rng() * 1.5 * scale;
71+
ctx.fillStyle = rng() > 0.5 ? '#000' : '#fff';
72+
ctx.fillRect(x, y, s, s);
73+
}
74+
ctx.restore();
75+
76+
/* Gilt hairline frame, inset */
77+
const inset = 36 * scale;
78+
ctx.strokeStyle = GILT;
79+
ctx.lineWidth = 1 * scale;
80+
ctx.strokeRect(inset, inset, width - inset * 2, height - inset * 2);
81+
82+
/* Layout split: left composition 40%, right label 60% */
83+
const padX = 60 * scale;
84+
const padY = 60 * scale;
85+
const splitX = width * 0.42;
86+
87+
/* ── LEFT: painterly composition ── */
88+
ctx.save();
89+
ctx.beginPath();
90+
ctx.rect(padX, padY, splitX - padX - 20 * scale, height - padY * 2);
91+
ctx.clip();
92+
93+
// Large ground block (bg color)
94+
ctx.fillStyle = bgColor;
95+
ctx.fillRect(padX, padY, splitX - padX - 20 * scale, height - padY * 2);
96+
97+
// Overlapping primary rectangle
98+
const pW = (splitX - padX - 20 * scale) * (0.55 + rng() * 0.15);
99+
const pH = (height - padY * 2) * (0.55 + rng() * 0.15);
100+
const pX = padX + (splitX - padX - 20 * scale - pW) * rng();
101+
const pY = padY + (height - padY * 2 - pH) * rng();
102+
ctx.fillStyle = primaryColor;
103+
ctx.fillRect(pX, pY, pW, pH);
104+
105+
// Smaller secondary circle or square
106+
const sSize = (height - padY * 2) * (0.3 + rng() * 0.1);
107+
const sX = padX + rng() * (splitX - padX - sSize - 20 * scale);
108+
const sY = padY + rng() * (height - padY * 2 - sSize);
109+
ctx.fillStyle = secondaryColor;
110+
if (rng() > 0.5) {
111+
ctx.beginPath();
112+
ctx.arc(sX + sSize / 2, sY + sSize / 2, sSize / 2, 0, Math.PI * 2);
113+
ctx.fill();
114+
} else {
115+
ctx.fillRect(sX, sY, sSize, sSize);
116+
}
117+
118+
// Jade punctuation — a tiny square
119+
const jSize = 18 * scale;
120+
ctx.fillStyle = JADE;
121+
ctx.fillRect(
122+
padX + (splitX - padX - 20 * scale) - jSize - 24 * scale,
123+
padY + (height - padY * 2) - jSize - 24 * scale,
124+
jSize,
125+
jSize,
126+
);
127+
128+
ctx.restore();
129+
130+
// Gilt frame around the composition
131+
ctx.strokeStyle = GILT;
132+
ctx.lineWidth = 2 * scale;
133+
ctx.strokeRect(padX, padY, splitX - padX - 20 * scale, height - padY * 2);
134+
135+
/* ── RIGHT: the wall label ── */
136+
const labelX = splitX + 40 * scale;
137+
const labelW = width - labelX - padX;
138+
let y = padY + 20 * scale;
139+
140+
// Accession number
141+
ctx.fillStyle = INK_SOFT;
142+
ctx.font = `400 ${14 * scale}px ${sans}`;
143+
ctx.textBaseline = 'alphabetic';
144+
ctx.fillText('ACCESSION', labelX, y);
145+
ctx.fillStyle = INK;
146+
ctx.font = `italic 400 ${28 * scale}px ${serif}`;
147+
ctx.fillText(
148+
`№ ${String((h >>> 0) % 999).padStart(3, '0')} / MMXXVI`,
149+
labelX,
150+
y + 32 * scale,
151+
);
152+
y += 70 * scale;
153+
154+
// Hairline
155+
ctx.strokeStyle = 'rgba(45,31,46,0.2)';
156+
ctx.lineWidth = 1 * scale;
157+
ctx.beginPath();
158+
ctx.moveTo(labelX, y);
159+
ctx.lineTo(labelX + labelW, y);
160+
ctx.stroke();
161+
y += 26 * scale;
162+
163+
// Small-caps collection kicker
164+
ctx.fillStyle = INK_SOFT;
165+
ctx.font = `600 ${13 * scale}px ${sans}`;
166+
ctx.fillText('THE FEZCODEX COLLECTION', labelX, y);
167+
y += 36 * scale;
168+
169+
// Repo name — italic Garamond with jade period
170+
ctx.fillStyle = INK;
171+
const titleSize = Math.min(72, 560 / Math.max(repoName.length, 8)) * scale;
172+
ctx.font = `italic 400 ${titleSize}px ${serif}`;
173+
const titleText = repoName || 'untitled';
174+
ctx.fillText(titleText, labelX, y + titleSize * 0.75);
175+
const titleW = ctx.measureText(titleText).width;
176+
ctx.fillStyle = JADE;
177+
ctx.fillText('.', labelX + titleW, y + titleSize * 0.75);
178+
y += titleSize + 14 * scale;
179+
180+
// By-line
181+
ctx.fillStyle = INK_SOFT;
182+
ctx.font = `italic 400 ${22 * scale}px ${serif}`;
183+
ctx.fillText(`by ${repoOwner || 'anonymous'}`, labelX, y);
184+
y += 44 * scale;
185+
186+
// Caption — wrapped Garamond italic
187+
ctx.fillStyle = INK;
188+
ctx.font = `italic 400 ${20 * scale}px ${serif}`;
189+
wrapText(ctx, description || '', labelX, y, labelW, 28 * scale);
190+
y += 28 * scale * 3.3;
191+
192+
// Bottom hairline
193+
ctx.strokeStyle = 'rgba(45,31,46,0.2)';
194+
ctx.beginPath();
195+
ctx.moveTo(labelX, height - padY - 70 * scale);
196+
ctx.lineTo(labelX + labelW, height - padY - 70 * scale);
197+
ctx.stroke();
198+
199+
// Bottom meta row — 3 columns: medium, dimensions, provenance
200+
const metaY = height - padY - 36 * scale;
201+
ctx.fillStyle = INK_SOFT;
202+
ctx.font = `600 ${11 * scale}px ${sans}`;
203+
204+
// Medium
205+
ctx.fillText('MEDIUM', labelX, metaY - 18 * scale);
206+
ctx.fillStyle = INK;
207+
ctx.font = `italic 400 ${18 * scale}px ${serif}`;
208+
ctx.fillText(language || '—', labelX, metaY + 4 * scale);
209+
210+
// Stars
211+
const col2 = labelX + labelW * 0.38;
212+
ctx.fillStyle = INK_SOFT;
213+
ctx.font = `600 ${11 * scale}px ${sans}`;
214+
ctx.fillText('STARS', col2, metaY - 18 * scale);
215+
ctx.fillStyle = INK;
216+
ctx.font = `italic 400 ${18 * scale}px ${serif}`;
217+
ctx.fillText(stars || '0', col2, metaY + 4 * scale);
218+
219+
// Forks
220+
const col3 = labelX + labelW * 0.62;
221+
ctx.fillStyle = INK_SOFT;
222+
ctx.font = `600 ${11 * scale}px ${sans}`;
223+
ctx.fillText('FORKS', col3, metaY - 18 * scale);
224+
ctx.fillStyle = INK;
225+
ctx.font = `italic 400 ${18 * scale}px ${serif}`;
226+
ctx.fillText(forks || '0', col3, metaY + 4 * scale);
227+
228+
// Provenance
229+
ctx.fillStyle = INK_SOFT;
230+
ctx.font = `600 ${11 * scale}px ${sans}`;
231+
ctx.fillText('PROVENANCE', labelX + labelW * 0.78, metaY - 18 * scale);
232+
ctx.fillStyle = INK;
233+
ctx.font = `italic 400 ${18 * scale}px ${serif}`;
234+
ctx.fillText('github', labelX + labelW * 0.78, metaY + 4 * scale);
235+
236+
/* Jade seal in lower-right corner */
237+
const sealR = 28 * scale;
238+
const sealX = width - padX - sealR - 4 * scale;
239+
const sealY = padY + sealR + 4 * scale;
240+
ctx.strokeStyle = JADE;
241+
ctx.lineWidth = 1.5 * scale;
242+
ctx.beginPath();
243+
ctx.arc(sealX, sealY, sealR, 0, Math.PI * 2);
244+
ctx.stroke();
245+
ctx.beginPath();
246+
ctx.arc(sealX, sealY, sealR - 4 * scale, 0, Math.PI * 2);
247+
ctx.stroke();
248+
ctx.fillStyle = JADE;
249+
ctx.font = `italic 400 ${18 * scale}px ${serif}`;
250+
ctx.textAlign = 'center';
251+
ctx.fillText('ƒ', sealX, sealY + 6 * scale);
252+
ctx.textAlign = 'left';
253+
254+
/* Tiny atelier signature at bottom-left of label area */
255+
ctx.fillStyle = PAPER_SOFT; // drop-dummy for completeness
256+
ctx.fillStyle = INK_SOFT;
257+
ctx.font = `italic 400 ${13 * scale}px ${serif}`;
258+
ctx.fillText('the atelier, mmxxvi', labelX, height - padY - 6 * scale);
259+
};

0 commit comments

Comments
 (0)