Skip to content

Commit 7cec7e5

Browse files
committed
refactor(thumbnail): riso print uses full palette + Abril Fatface title
Riso Print now consumes all three color controls and the texture toggle: bgColor becomes the paper stock with auto-contrast ink, primary drives the big halftone burst plus a solid circle mark, secondary runs the top bar, lower-left burst, corner nub, and title misregistration ghost. Texture toggle switches paper grain and a diagonal halftone field. Two spot-color swatches surface in the bottom meta bar. Title now set in Abril Fatface with a measure-and-shrink sizing pass so it never overflows; starting size tuned down to 140*scale.
1 parent 8b5174e commit 7cec7e5

2 files changed

Lines changed: 144 additions & 88 deletions

File tree

public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" rel="stylesheet">
3939
<link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&display=swap" rel="stylesheet">
4040
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,200..900,0..100,0..1;1,9..144,200..900,0..100,0..1&display=swap" rel="stylesheet">
41+
<link href="https://fonts.googleapis.com/css2?family=Abril+Fatface&display=swap" rel="stylesheet">
4142
<title>fezcodex</title>
4243
</head>
4344
<body class="bg-slate-950">
Lines changed: 143 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { wrapText } from '../utils';
22

33
/*
4-
* RISO_PRINT — two-color risograph print with halftone dithering and a
5-
* slight color misregistration. Matte off-white paper, chunky geometric
6-
* sans title with a secondary-color offset shadow, halftone dot shapes
7-
* bleeding under the type, corner print metadata. Uses primary + secondary
8-
* + bg as the "spot colors."
4+
* RISO_PRINT — two-plate risograph print with halftone dithering.
5+
*
6+
* Uses all three palette controls meaningfully:
7+
* - bgColor = paper stock (ground)
8+
* - primaryColor = main halftone plate (big circle burst)
9+
* - secondaryColor = second plate (bar + burst + title misregistration)
10+
*
11+
* Texture toggle switches paper grain and a light halftone field on/off.
12+
* Ink color auto-adapts so dark paper stays readable.
913
*/
1014
export const risoPrint = (ctx, width, height, scale, data) => {
1115
const {
@@ -18,13 +22,11 @@ export const risoPrint = (ctx, width, height, scale, data) => {
1822
primaryColor,
1923
secondaryColor,
2024
bgColor,
25+
showPattern,
2126
} = data;
2227

23-
const PAPER = '#F4EEDA';
24-
const INK = '#1C1817';
25-
2628
const mono = '"JetBrains Mono", "Space Mono", monospace';
27-
const display = '"Syne", "Inter", system-ui, sans-serif';
29+
const display = '"Abril Fatface", "Didot", "Bodoni 72", Georgia, serif';
2830

2931
/* Seeded RNG */
3032
const seed = `${repoOwner || ''}${repoName || ''}`;
@@ -38,97 +40,143 @@ export const risoPrint = (ctx, width, height, scale, data) => {
3840
return ((h ^= h >>> 16) >>> 0) / 4294967296;
3941
};
4042

43+
/* Auto-contrast ink based on paper luminance */
44+
const hexToRgb = (hex) => {
45+
const c = (hex || '#F4EEDA').replace('#', '');
46+
const f = c.length === 3 ? c.split('').map((x) => x + x).join('') : c;
47+
return {
48+
r: parseInt(f.slice(0, 2), 16),
49+
g: parseInt(f.slice(2, 4), 16),
50+
b: parseInt(f.slice(4, 6), 16),
51+
};
52+
};
53+
const { r, g, b } = hexToRgb(bgColor);
54+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
55+
const darkPaper = luminance < 0.5;
56+
const INK = darkPaper ? '#F4EEDA' : '#1C1817';
57+
const INK_SOFT = darkPaper ? 'rgba(244,238,218,0.7)' : 'rgba(28,24,23,0.7)';
58+
4159
/* Paper ground */
42-
ctx.fillStyle = PAPER;
60+
ctx.fillStyle = bgColor || '#F4EEDA';
4361
ctx.fillRect(0, 0, width, height);
4462

45-
/* Halftone circle — primary color, dithered */
46-
const drawHalftone = (cx, cy, r, color) => {
47-
const dotSize = 4 * scale;
63+
/* ── Halftone field overlay (texture-toggle-gated) ───────────────
64+
A wide, low-opacity halftone grid of primary-color dots sweeping
65+
diagonally gives the print depth. Only when texture is applied. */
66+
if (showPattern) {
67+
ctx.save();
68+
ctx.globalAlpha = 0.12;
69+
ctx.fillStyle = primaryColor;
70+
const fieldSpacing = 18 * scale;
71+
for (let yy = -fieldSpacing; yy < height + fieldSpacing; yy += fieldSpacing) {
72+
for (let xx = -fieldSpacing; xx < width + fieldSpacing; xx += fieldSpacing) {
73+
// Gradient from top-left to bottom-right using dot size
74+
const t = 1 - ((xx + yy) / (width + height));
75+
const ds = 1.2 * scale + t * 3 * scale;
76+
ctx.beginPath();
77+
ctx.arc(xx, yy, ds, 0, Math.PI * 2);
78+
ctx.fill();
79+
}
80+
}
81+
ctx.restore();
82+
}
83+
84+
/* Halftone utility — radial density fall-off */
85+
const drawHalftone = (cx, cy, r, color, maxDot = 5) => {
4886
const spacing = 10 * scale;
4987
ctx.fillStyle = color;
50-
for (let y = cy - r; y < cy + r; y += spacing) {
51-
for (let x = cx - r; x < cx + r; x += spacing) {
52-
const dx = x - cx;
53-
const dy = y - cy;
88+
for (let yy = cy - r; yy < cy + r; yy += spacing) {
89+
for (let xx = cx - r; xx < cx + r; xx += spacing) {
90+
const dx = xx - cx;
91+
const dy = yy - cy;
5492
const dist = Math.sqrt(dx * dx + dy * dy);
5593
if (dist > r) continue;
56-
// dot size shrinks near edge for the gradient feel
5794
const t = 1 - dist / r;
58-
const ds = dotSize * (0.4 + t * 0.8);
59-
ctx.globalAlpha = 0.85;
95+
const ds = maxDot * scale * (0.3 + t * 0.9);
96+
ctx.globalAlpha = 0.88;
6097
ctx.beginPath();
61-
ctx.arc(x, y, ds / 2, 0, Math.PI * 2);
98+
ctx.arc(xx, yy, ds / 2, 0, Math.PI * 2);
6299
ctx.fill();
63100
}
64101
}
65102
ctx.globalAlpha = 1;
66103
};
67104

68-
/* Big primary-color halftone circle on the right */
69-
drawHalftone(width * 0.72, height * 0.45, height * 0.42, primaryColor);
105+
/* ── Primary-plate halftone: big burst top-right ── */
106+
drawHalftone(width * 0.74, height * 0.42, height * 0.46, primaryColor, 6);
70107

71-
/* Smaller secondary halftone burst lower-left */
72-
drawHalftone(width * 0.15, height * 0.85, height * 0.25, secondaryColor);
108+
/* ── Secondary-plate bar across the top (with misregistration nub) ── */
109+
ctx.fillStyle = secondaryColor;
110+
ctx.globalAlpha = 0.95;
111+
ctx.fillRect(width * 0.05, height * 0.1, width * 0.9, 8 * scale);
112+
// Print-misalignment ghost: a faint offset copy
113+
ctx.globalAlpha = 0.45;
114+
ctx.fillRect(width * 0.05 + 3 * scale, height * 0.1 + 3 * scale, width * 0.9, 8 * scale);
115+
ctx.globalAlpha = 1;
116+
// Tiny secondary square nub on the right of the bar
117+
ctx.fillStyle = secondaryColor;
118+
ctx.fillRect(width * 0.9, height * 0.1 - 10 * scale, 12 * scale, 28 * scale);
119+
120+
/* ── Secondary-plate halftone burst lower-left ── */
121+
drawHalftone(width * 0.16, height * 0.78, height * 0.28, secondaryColor, 5);
73122

74-
/* Solid secondary-color geometric bar on top */
123+
/* ── Secondary solid geometric mark: a filled square overlapping the
124+
lower-left burst, creating an opaque + halftone composition ── */
75125
ctx.fillStyle = secondaryColor;
76-
ctx.globalAlpha = 0.92;
77-
ctx.fillRect(width * 0.05, height * 0.08, width * 0.9, 6 * scale);
78-
ctx.fillRect(width * 0.05, height * 0.08 + 14 * scale, width * 0.45, 2 * scale);
126+
ctx.globalAlpha = 0.9;
127+
ctx.fillRect(width * 0.06, height * 0.82, 60 * scale, 60 * scale);
79128
ctx.globalAlpha = 1;
80129

81-
/* Top-left print metadata */
130+
/* ── Primary mark: a solid circle punctuating the description block ── */
131+
ctx.fillStyle = primaryColor;
132+
ctx.beginPath();
133+
ctx.arc(width * 0.58, height * 0.72, 18 * scale, 0, Math.PI * 2);
134+
ctx.fill();
135+
136+
/* ── Top meta row ── */
82137
ctx.fillStyle = INK;
83138
ctx.font = `700 ${14 * scale}px ${mono}`;
84139
ctx.textBaseline = 'alphabetic';
85-
ctx.fillText('EDITION 01 / ∞', width * 0.05, height * 0.08 - 14 * scale);
140+
ctx.fillText('EDITION 01 / ∞', width * 0.05, height * 0.1 - 14 * scale);
86141

87-
/* Top-right stamp: PRINT SPECS */
88142
ctx.font = `700 ${12 * scale}px ${mono}`;
89-
const stamp = `2-COLOUR · RISO · ${language || 'MIXED'}`.toUpperCase();
143+
const stamp = `2-COLOUR · RISO · ${(language || 'MIXED').toUpperCase()}`;
90144
const stampW = ctx.measureText(stamp).width;
91-
ctx.fillStyle = INK;
92-
ctx.fillText(stamp, width - width * 0.05 - stampW, height * 0.08 - 14 * scale);
145+
ctx.fillText(stamp, width - width * 0.05 - stampW, height * 0.1 - 14 * scale);
93146

94-
/* ── The big title — with misregistration offset ── */
147+
/* ── The big misregistered title ── */
95148
const title = (repoName || 'untitled').toUpperCase();
96-
const titleSize = Math.min(180, 1200 / Math.max(title.length, 6)) * scale;
97149
const titleY = height * 0.55;
150+
const titleMaxW = width * 0.9;
151+
let titleSize = 140 * scale;
152+
ctx.font = `400 ${titleSize}px ${display}`;
153+
// Measure-and-shrink so the title always fits inside 90% of canvas width
154+
while (ctx.measureText(title).width > titleMaxW && titleSize > 24 * scale) {
155+
titleSize *= 0.94;
156+
ctx.font = `400 ${titleSize}px ${display}`;
157+
}
98158

99-
// Offset ghost in secondary color
159+
// Single misregistration ghost in secondary
100160
ctx.fillStyle = secondaryColor;
101161
ctx.globalAlpha = 0.75;
102-
ctx.font = `800 ${titleSize}px ${display}`;
103162
ctx.fillText(title, width * 0.05 + 6 * scale, titleY + 6 * scale);
104163
ctx.globalAlpha = 1;
105164

106-
// Main ink
165+
// Main ink on top
107166
ctx.fillStyle = INK;
108167
ctx.fillText(title, width * 0.05, titleY);
109168

110-
/* Owner tag — mono, tight */
169+
/* Owner handle */
111170
ctx.fillStyle = INK;
112171
ctx.font = `700 ${22 * scale}px ${mono}`;
113-
ctx.fillText(
114-
`@ ${repoOwner || 'anon'}`.toLowerCase(),
115-
width * 0.05,
116-
titleY + 40 * scale,
117-
);
172+
ctx.fillText(`@ ${(repoOwner || 'anon').toLowerCase()}`, width * 0.05, titleY + 40 * scale);
118173

119-
/* Description — wrapped mono */
174+
/* Description */
120175
ctx.fillStyle = INK;
121176
ctx.font = `400 ${20 * scale}px ${mono}`;
122-
wrapText(
123-
ctx,
124-
description || '',
125-
width * 0.05,
126-
titleY + 80 * scale,
127-
width * 0.55,
128-
28 * scale,
129-
);
130-
131-
/* Bottom meta bar — three mono data blocks */
177+
wrapText(ctx, description || '', width * 0.05, titleY + 80 * scale, width * 0.55, 28 * scale);
178+
179+
/* Bottom meta bar */
132180
const barY = height * 0.9;
133181
ctx.strokeStyle = INK;
134182
ctx.lineWidth = 2 * scale;
@@ -140,53 +188,60 @@ export const risoPrint = (ctx, width, height, scale, data) => {
140188
ctx.fillStyle = INK;
141189
ctx.font = `700 ${18 * scale}px ${mono}`;
142190
const metaY = barY + 28 * scale;
143-
ctx.fillText(`★ ${stars || 0}`, width * 0.05, metaY);
144-
ctx.fillText(`⑂ ${forks || 0}`, width * 0.22, metaY);
145-
ctx.fillText(
146-
(language || 'MIXED').toUpperCase(),
147-
width * 0.38,
148-
metaY,
149-
);
150-
151-
// Right side: small catalog number
191+
192+
// Small spot-color swatches with labels — makes all three colors visible in the meta
193+
ctx.fillStyle = primaryColor;
194+
ctx.fillRect(width * 0.05, metaY - 14 * scale, 14 * scale, 14 * scale);
195+
ctx.fillStyle = secondaryColor;
196+
ctx.fillRect(width * 0.05 + 20 * scale, metaY - 14 * scale, 14 * scale, 14 * scale);
197+
198+
ctx.fillStyle = INK;
199+
ctx.fillText(`★ ${stars || 0}`, width * 0.05 + 52 * scale, metaY);
200+
ctx.fillText(`⑂ ${forks || 0}`, width * 0.22 + 52 * scale, metaY);
201+
202+
ctx.fillStyle = INK_SOFT;
203+
ctx.font = `400 ${14 * scale}px ${mono}`;
204+
const texStatus = showPattern ? 'GRAIN · DOTS' : 'CLEAN';
205+
ctx.fillText(`TEXTURE · ${texStatus}`, width * 0.45, metaY);
206+
207+
// Catalog number right
208+
ctx.fillStyle = INK;
209+
ctx.font = `700 ${18 * scale}px ${mono}`;
152210
const catNum = `№ ${String((h >>> 0) % 999).padStart(3, '0')}`;
153211
const catW = ctx.measureText(catNum).width;
154212
ctx.fillText(catNum, width - width * 0.05 - catW, metaY);
155213

156-
/* Print registration marks — classic riso corner crosses */
214+
/* Registration crosses */
157215
const drawCross = (cx, cy) => {
158216
ctx.strokeStyle = INK;
159217
ctx.lineWidth = 1 * scale;
160-
const r = 8 * scale;
218+
const rr = 8 * scale;
161219
ctx.beginPath();
162-
ctx.moveTo(cx - r, cy);
163-
ctx.lineTo(cx + r, cy);
164-
ctx.moveTo(cx, cy - r);
165-
ctx.lineTo(cx, cy + r);
220+
ctx.moveTo(cx - rr, cy);
221+
ctx.lineTo(cx + rr, cy);
222+
ctx.moveTo(cx, cy - rr);
223+
ctx.lineTo(cx, cy + rr);
166224
ctx.stroke();
167225
ctx.beginPath();
168-
ctx.arc(cx, cy, r - 2 * scale, 0, Math.PI * 2);
226+
ctx.arc(cx, cy, rr - 2 * scale, 0, Math.PI * 2);
169227
ctx.stroke();
170228
};
171229
drawCross(20 * scale, 20 * scale);
172230
drawCross(width - 20 * scale, 20 * scale);
173231
drawCross(20 * scale, height - 20 * scale);
174232
drawCross(width - 20 * scale, height - 20 * scale);
175233

176-
/* Final paper grain noise on top of everything */
177-
ctx.save();
178-
ctx.globalAlpha = 0.05;
179-
for (let i = 0; i < 2400; i++) {
180-
const x = rng() * width;
181-
const y = rng() * height;
182-
const s = rng() * 1.4 * scale;
183-
ctx.fillStyle = rng() > 0.5 ? '#000' : '#fff';
184-
ctx.fillRect(x, y, s, s);
234+
/* Paper grain — texture-toggle-gated */
235+
if (showPattern) {
236+
ctx.save();
237+
ctx.globalAlpha = darkPaper ? 0.07 : 0.05;
238+
for (let i = 0; i < 2400; i++) {
239+
const x = rng() * width;
240+
const y = rng() * height;
241+
const s = rng() * 1.4 * scale;
242+
ctx.fillStyle = rng() > 0.5 ? '#000' : '#fff';
243+
ctx.fillRect(x, y, s, s);
244+
}
245+
ctx.restore();
185246
}
186-
ctx.restore();
187-
188-
/* Bg color accent: a thin stripe on the left edge using bgColor so the
189-
control remains meaningful even on a light riso print. */
190-
ctx.fillStyle = bgColor;
191-
ctx.fillRect(0, 0, 6 * scale, height);
192247
};

0 commit comments

Comments
 (0)