@@ -102,52 +102,47 @@ function generateAutoParticles(
102102 return `<g class="heat-particles">${ particles } </g>` ;
103103}
104104
105- // main renderers
106- export function generateSVG (
107- stats : StreakStats ,
108- params : BadgeParams ,
109- calendar : ContributionCalendar
110- ) : string {
111- // Dispatch to the auto-theme renderer when the caller requests it.
112- // This keeps the existing static-theme path completely unchanged.
113- if ( params . autoTheme ) {
114- return generateAutoThemeSVG ( stats , params , calendar ) ;
115- }
116- const safeUser = escapeXML ( params . user || 'GitHub User' ) ;
117-
118- const bg = `#${ sanitizeHexColor ( params . bg , '0d1117' ) } ` ;
119- const accent = `#${ sanitizeHexColor ( params . accent , '00ffaa' ) } ` ;
120- const text = `#${ sanitizeHexColor ( params . text , 'ffffff' ) } ` ;
105+ // ── Section helpers for generateSVG ──────────────────────────────────────
121106
122- const sanitizedFont = sanitizeFont ( params . font ) ;
123- const predefinedFont = sanitizedFont ? FONT_MAP [ sanitizedFont . toLowerCase ( ) ] : null ;
124- const isPredefinedFont = Boolean ( predefinedFont ) ;
125- const selectedFont = isPredefinedFont
126- ? predefinedFont
127- : sanitizedFont
128- ? `"${ sanitizedFont } ", sans-serif`
129- : null ;
107+ function renderHeader ( safeUser : string , stats : StreakStats , sf : number ) : string {
108+ const fs = ( n : number ) => Math . round ( n * sf * 10 ) / 10 ;
109+ return `
110+ <title>CommitPulse Stats for ${ safeUser } </title>
111+ <desc>
112+ ${ safeUser } has ${ stats . totalContributions } total contributions and a longest streak of ${ stats . longestStreak } days.
113+ </desc>
114+ <defs>
115+ <filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${ fs ( 5 ) } " result="blur" /><feComposite in="SourceGraphic" in2="blur" operator="over" /></filter>
116+ </defs>` ;
117+ }
130118
131- const statsFont = selectedFont || '"Space Grotesk", sans-serif' ;
132- const sf = getSizeScale ( params . size ) ;
133- const radius = sanitizeRadius ( params . radius , 8 ) * sf ;
134- const labels = getLabels ( params . lang ) ;
119+ function renderStyle (
120+ selectedFont : string | null ,
121+ statsFont : string ,
122+ googleFontsImport : string ,
123+ text : string ,
124+ accent : string ,
125+ sf : number
126+ ) : string {
127+ const fs = ( n : number ) => Math . round ( n * sf * 10 ) / 10 ;
128+ return `
129+ <style>
130+ @import url('https://fonts.googleapis.com/css2?family=Fira+Code&family=JetBrains+Mono&family=Roboto&display=swap');
131+ ${ googleFontsImport }
132+ ${ TOWER_ANIMATION_CSS }
133+ .title { font-family: ${ selectedFont || '"Syncopate", sans-serif' } ; fill: ${ text } ; font-size: ${ fs ( 18 ) } px; letter-spacing: ${ fs ( 6 ) } px; font-weight: 400; opacity: 0.8; }
134+ .stats { font-family: ${ statsFont } ; fill: ${ text } ; font-size: ${ fs ( 42 ) } px; font-weight: 500; letter-spacing: 0; }
135+ .total-val { font-family: ${ statsFont } ; fill: ${ accent } ; font-size: ${ fs ( 24 ) } px; font-weight: 500; }
136+ .label { font-family: "Roboto", sans-serif; fill: ${ accent } ; font-size: ${ fs ( 11 ) } px; font-weight: 400; letter-spacing: ${ fs ( 2 ) } px; opacity: 0.7; }
137+ @media (prefers-reduced-motion: reduce) { .heat-particles { display: none; } }
138+ </style>` ;
139+ }
135140
136- const W = Math . round ( 600 * sf ) ;
137- const H = Math . round ( 420 * sf ) ;
138- const towerData = scaleTowerData ( computeTowers ( calendar , params . scale , stats . todayDate ) , sf ) ;
141+ function renderTowers ( towerData : TowerData [ ] , accent : string , text : string , sf : number ) : string {
139142 let towers = '' ;
140-
141143 for ( const t of towerData ) {
142144 const color = t . isGhost ? text : accent ;
143- // Stagger delay creates a diagonal wave across the isometric grid (back-to-front)
144145 const delay = ( ( t . row + t . col ) * 0.015 ) . toFixed ( 3 ) ;
145-
146- // The outer <g> positions the group at the ground tile (t.x, t.y).
147- // The inner <g class="cp-tower"> is what CSS animates with scaleY.
148- // Keeping these two responsibilities in separate elements prevents the
149- // CSS transform from fighting the SVG translate — they operate independently.
150- // Geometry paths are drawn offset by -t.h so they extend upward from y=10 (ground).
151146 towers += `
152147 <g transform="translate(${ t . x } , ${ t . y } )">
153148 <g class="cp-tower" style="animation-delay: ${ delay } s;">
@@ -162,80 +157,86 @@ export function generateSVG(
162157 if ( t . contributionCount >= 10 )
163158 towers += generateParticles ( t . x , t . y , t . h , accent , t . contributionCount , sf ) ;
164159 }
160+ return towers ;
161+ }
165162
166- // dynamic google fonts import
167- const googleFontsImport =
168- sanitizedFont && ! isPredefinedFont
169- ? `@import url('https://fonts.googleapis.com/css2?family=${ encodeURIComponent ( sanitizedFont ) . replace ( / % 2 0 / g, '+' ) } &display=swap');`
170- : '' ;
171-
163+ function renderFooter (
164+ stats : StreakStats ,
165+ params : BadgeParams ,
166+ labels : ReturnType < typeof getLabels > ,
167+ safeUser : string ,
168+ accent : string ,
169+ sf : number
170+ ) : string {
172171 const s = ( n : number ) => Math . round ( n * sf ) ;
173- const fs = ( n : number ) => Math . round ( n * sf * 10 ) / 10 ;
174-
175172 return `
176- <svg
177- xmlns="http://www.w3.org/2000/svg"
178- width="${ W } "
179- height="${ H } "
180- viewBox="0 0 ${ W } ${ H } "
181- fill="none"
182- role="img"
183- >
184- <title>CommitPulse Stats for ${ safeUser } </title>
185- <desc>
186- ${ params . user || 'This user' } has ${ stats . totalContributions } total contributions and a longest streak of ${ stats . longestStreak } days.
187- </desc>
188- <defs>
189- <filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${ fs ( 5 ) } " result="blur" /><feComposite in="SourceGraphic" in2="blur" operator="over" /></filter>
190- </defs>
191-
192- <style>
193- @import url('https://fonts.googleapis.com/css2?family=Fira+Code&family=JetBrains+Mono&family=Roboto&display=swap');
194- ${ googleFontsImport }
195- ${ TOWER_ANIMATION_CSS }
196-
197- .title { font-family: ${ selectedFont || '"Syncopate", sans-serif' } ; fill: ${ text } ; font-size: ${ fs ( 18 ) } px; letter-spacing: ${ fs ( 6 ) } px; font-weight: 400; opacity: 0.8; }
198- .stats { font-family: ${ statsFont } ; fill: ${ text } ; font-size: ${ fs ( 42 ) } px; font-weight: 500; letter-spacing: 0; }
199- .total-val { font-family: ${ statsFont } ; fill: ${ accent } ; font-size: ${ fs ( 24 ) } px; font-weight: 500; }
200- .label { font-family: "Roboto", sans-serif; fill: ${ accent } ; font-size: ${ fs ( 11 ) } px; font-weight: 400; letter-spacing: ${ fs ( 2 ) } px; opacity: 0.7; }
201-
202- @media (prefers-reduced-motion: reduce) { .heat-particles { display: none; } }
203- </style>
204-
205- <rect width="${ W } " height="${ H } " rx="${ radius } " fill="${ params . hideBackground ? 'transparent' : bg } " />
206-
207- <g transform="translate(0, ${ s ( 20 ) } )">${ towers } </g>
208173 ${
209174 ! params . hide_stats
210175 ? `
211176 <g transform="translate(${ s ( 40 ) } , ${ s ( 340 ) } )">
212177 <text class="label">${ labels . CURRENT_STREAK } </text>
213178 <text y="${ s ( 40 ) } " class="stats" filter="url(#glow)">${ stats . currentStreak } </text>
214179 </g>
215-
216180 <g transform="translate(${ s ( 300 ) } , ${ s ( 340 ) } )" text-anchor="middle">
217181 <text class="label">${ labels . ANNUAL_SYNC_TOTAL } </text>
218182 <text y="${ s ( 40 ) } " class="total-val" filter="url(#glow)">${ stats . totalContributions } </text>
219183 </g>
220-
221184 <g transform="translate(${ s ( 560 ) } , ${ s ( 340 ) } )" text-anchor="end">
222185 <text class="label">${ labels . PEAK_STREAK } </text>
223186 <text y="${ s ( 40 ) } " class="stats">${ stats . longestStreak } </text>
224- </g>
225- `
187+ </g>`
226188 : ''
227189 }
228- ${
229- ! params . hide_title
230- ? `<text x="${ s ( 300 ) } " y="${ s ( 50 ) } " text-anchor="middle" class="title">${ safeUser . toUpperCase ( ) } </text>`
231- : ''
232- }
233-
190+ ${ ! params . hide_title ? `<text x="${ s ( 300 ) } " y="${ s ( 50 ) } " text-anchor="middle" class="title">${ safeUser . toUpperCase ( ) } </text>` : '' }
234191 <rect x="${ s ( 100 ) } " y="${ s ( 60 ) } " width="${ s ( 400 ) } " height="${ sf } " fill="${ accent } " fill-opacity="0.3">
235192 <animate attributeName="y" values="${ s ( 80 ) } ;${ s ( 320 ) } ;${ s ( 80 ) } " dur="${ params . speed || '8s' } " repeatCount="indefinite" />
236- </rect>
237- </svg>
238- ` ;
193+ </rect>` ;
194+ }
195+
196+ // ── Main static-theme renderer ────────────────────────────────────────────
197+ export function generateSVG (
198+ stats : StreakStats ,
199+ params : BadgeParams ,
200+ calendar : ContributionCalendar
201+ ) : string {
202+ if ( params . autoTheme ) return generateAutoThemeSVG ( stats , params , calendar ) ;
203+
204+ const safeUser = escapeXML ( params . user || 'GitHub User' ) ;
205+ const bg = `#${ sanitizeHexColor ( params . bg , '0d1117' ) } ` ;
206+ const accent = `#${ sanitizeHexColor ( params . accent , '00ffaa' ) } ` ;
207+ const text = `#${ sanitizeHexColor ( params . text , 'ffffff' ) } ` ;
208+
209+ const sanitizedFont = sanitizeFont ( params . font ) ;
210+ const predefinedFont = sanitizedFont ? FONT_MAP [ sanitizedFont . toLowerCase ( ) ] : null ;
211+ const isPredefinedFont = Boolean ( predefinedFont ) ;
212+ const selectedFont = isPredefinedFont
213+ ? predefinedFont
214+ : sanitizedFont
215+ ? `"${ sanitizedFont } ", sans-serif`
216+ : null ;
217+ const statsFont = selectedFont || '"Space Grotesk", sans-serif' ;
218+ const googleFontsImport =
219+ sanitizedFont && ! isPredefinedFont
220+ ? `@import url('https://fonts.googleapis.com/css2?family=${ encodeURIComponent ( sanitizedFont ) . replace ( / % 2 0 / g, '+' ) } &display=swap');`
221+ : '' ;
222+
223+ const sf = getSizeScale ( params . size ) ;
224+ const radius = sanitizeRadius ( params . radius , 8 ) * sf ;
225+ const labels = getLabels ( params . lang ) ;
226+ const W = Math . round ( 600 * sf ) ;
227+ const H = Math . round ( 420 * sf ) ;
228+
229+ const towerData = scaleTowerData ( computeTowers ( calendar , params . scale , stats . todayDate ) , sf ) ;
230+ const towers = renderTowers ( towerData , accent , text , sf ) ;
231+
232+ return `
233+ <svg xmlns="http://www.w3.org/2000/svg" width="${ W } " height="${ H } " viewBox="0 0 ${ W } ${ H } " fill="none" role="img">
234+ ${ renderHeader ( safeUser , stats , sf ) }
235+ ${ renderStyle ( selectedFont , statsFont , googleFontsImport , text , accent , sf ) }
236+ <rect width="${ W } " height="${ H } " rx="${ radius } " fill="${ params . hideBackground ? 'transparent' : bg } " />
237+ <g transform="translate(0, ${ Math . round ( 20 * sf ) } )">${ towers } </g>
238+ ${ renderFooter ( stats , params , labels , safeUser , accent , sf ) }
239+ </svg>` ;
239240}
240241
241242//generates an svg for the non existent users
0 commit comments