11import { 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 */
1014export 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