Skip to content

Commit 00a033e

Browse files
feat(svg): add option to toggle heavy SVG Glow Filters (glow=true/false)
1 parent 4fc855a commit 00a033e

6 files changed

Lines changed: 147 additions & 20 deletions

File tree

app/api/streak/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export async function GET(request: Request) {
9191
gradient,
9292
tz: tzParam,
9393
disable_particles,
94+
glow,
9495
} = parseResult.data;
9596

9697
const themeName = theme || 'dark';
@@ -159,6 +160,7 @@ export async function GET(request: Request) {
159160
shading,
160161
gradient,
161162
disable_particles,
163+
glow,
162164
};
163165

164166
let calendar;

lib/svg/generator.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
generateMonthlySVG,
55
generateNotFoundSVG,
66
generateRateLimitSVG,
7+
generateHeatmapSVG,
78
particleCount,
89
escapeXML,
910
getSizeScale,
@@ -1443,4 +1444,58 @@ describe('Radar Scan Line Animation Alignment', () => {
14431444
const geometryLong = extractGeometry(svgLong);
14441445
expect(geometryLong).toEqual(geometryBaseline);
14451446
});
1447+
1448+
describe('glow parameter', () => {
1449+
const mockStats: StreakStats = {
1450+
currentStreak: 5,
1451+
longestStreak: 10,
1452+
totalContributions: 100,
1453+
todayDate: '2024-06-12',
1454+
};
1455+
const mockCalendar = {
1456+
weeks: [
1457+
{
1458+
contributionDays: [
1459+
{ contributionCount: 0, date: '2024-06-10' },
1460+
{ contributionCount: 5, date: '2024-06-11' },
1461+
{ contributionCount: 15, date: '2024-06-12' },
1462+
],
1463+
},
1464+
],
1465+
} as ContributionCalendar;
1466+
1467+
it('renders glow filter and attributes by default', () => {
1468+
const svg = generateSVG(mockStats, { user: 'avi' } as unknown as BadgeParams, mockCalendar);
1469+
expect(svg).toContain('<filter id="glow"');
1470+
expect(svg).toContain('filter="url(#glow)"');
1471+
});
1472+
1473+
it('omits glow filter and attributes when glow=false is requested', () => {
1474+
const svg = generateSVG(
1475+
mockStats,
1476+
{ user: 'avi', glow: false } as unknown as BadgeParams,
1477+
mockCalendar
1478+
);
1479+
expect(svg).not.toContain('<filter id="glow"');
1480+
expect(svg).not.toContain('filter="url(#glow)"');
1481+
});
1482+
1483+
it('omits heatmap glow filter and cell filter attributes when glow=false is requested in heatmap', () => {
1484+
const svgWithGlow = generateHeatmapSVG(
1485+
mockStats,
1486+
{ user: 'avi', view: 'heatmap' } as unknown as BadgeParams,
1487+
mockCalendar
1488+
);
1489+
expect(svgWithGlow).toContain('<filter id="hm-glow"');
1490+
expect(svgWithGlow).toContain('filter="url(#hm-glow)"');
1491+
1492+
const svgNoGlow = generateHeatmapSVG(
1493+
mockStats,
1494+
{ user: 'avi', view: 'heatmap', glow: false } as unknown as BadgeParams,
1495+
mockCalendar
1496+
);
1497+
expect(svgNoGlow).not.toContain('<filter id="hm-glow"');
1498+
expect(svgNoGlow).not.toContain('filter="url(#hm-glow)"');
1499+
});
1500+
});
14461501
});

lib/svg/generator.ts

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,13 @@ function renderDefs(sf: number, params: BadgeParams): string {
159159
}
160160
}
161161

162+
const filterGlow =
163+
params.glow !== false
164+
? `<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>`
165+
: '';
166+
162167
return `<defs>
163-
<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>
168+
${filterGlow}
164169
${gradients}
165170
</defs>`;
166171
}
@@ -172,15 +177,16 @@ function renderStatsSection(
172177
params: BadgeParams
173178
): string {
174179
const totalLabel = params.mode === 'loc' ? 'TOTAL LINES OF CODE' : labels.ANNUAL_SYNC_TOTAL;
180+
const glowAttr = params.glow !== false ? ' filter="url(#glow)"' : '';
175181

176182
return `
177183
<g transform="translate(${s(100)}, ${s(340)})" text-anchor="middle">
178184
<text class="label">${labels.CURRENT_STREAK}</text>
179-
<text y="${s(40)}" class="stats" filter="url(#glow)">${stats.currentStreak}</text>
185+
<text y="${s(40)}" class="stats"${glowAttr}>${stats.currentStreak}</text>
180186
</g>
181187
<g transform="translate(${s(300)}, ${s(340)})" text-anchor="middle">
182188
<text class="label">${totalLabel}</text>
183-
<text y="${s(40)}" class="total-val" filter="url(#glow)">${stats.totalContributions}</text>
189+
<text y="${s(40)}" class="total-val"${glowAttr}>${stats.totalContributions}</text>
184190
</g>
185191
<g transform="translate(${s(500)}, ${s(340)})" text-anchor="middle">
186192
<text class="label">${labels.PEAK_STREAK}</text>
@@ -906,6 +912,16 @@ export function generateWrappedSVG(
906912
? 'class="cp-accent-stroke" stroke-opacity="0.15" stroke-width="1.5"'
907913
: borderAttr;
908914

915+
const filterGlow =
916+
params.glow !== false
917+
? `<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
918+
<feGaussianBlur stdDeviation="3.5" result="blur"/>
919+
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
920+
</filter>`
921+
: '';
922+
923+
const glowAttr = params.glow !== false ? ' filter="url(#glow)"' : '';
924+
909925
return `
910926
<svg
911927
xmlns="http://www.w3.org/2000/svg"
@@ -917,10 +933,7 @@ export function generateWrappedSVG(
917933
>
918934
<title>${safeUser}'s GitHub Wrapped ${year}</title>
919935
<defs>
920-
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
921-
<feGaussianBlur stdDeviation="3.5" result="blur"/>
922-
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
923-
</filter>
936+
${filterGlow}
924937
</defs>
925938
926939
<style>
@@ -975,7 +988,7 @@ export function generateWrappedSVG(
975988
</g>
976989
977990
<g transform="translate(25, 120)">
978-
<text x="0" y="15" class="total-commits" ${accentClass} filter="url(#glow)">${stats.totalContributions}</text>
991+
<text x="0" y="15" class="total-commits" ${accentClass}${glowAttr}>${stats.totalContributions}</text>
979992
<text x="2" y="38" class="total-label" ${textClass}>TOTAL CONTRIBUTIONS</text>
980993
</g>
981994
@@ -1154,7 +1167,8 @@ function renderHeatmapGrid(
11541167
sf: number,
11551168
todayDate: string,
11561169
mode: 'commits' | 'loc' = 'commits',
1157-
isAutoTheme: boolean = false
1170+
isAutoTheme: boolean = false,
1171+
glow: boolean = true
11581172
): string {
11591173
const weeks = calendar.weeks.slice(-14);
11601174
const cellSize = Math.round(HEATMAP_CELL_SIZE * sf);
@@ -1202,7 +1216,7 @@ function renderHeatmapGrid(
12021216
const fillAttr = isAutoTheme ? 'fill="var(--cp-accent)"' : `fill="${accent}"`;
12031217

12041218
// Glow on high-intensity cells
1205-
const filterAttr = intensity === 4 ? ' filter="url(#hm-glow)"' : '';
1219+
const filterAttr = intensity === 4 && glow !== false ? ' filter="url(#hm-glow)"' : '';
12061220

12071221
cells += `
12081222
<rect
@@ -1328,21 +1342,35 @@ export function generateHeatmapSVG(
13281342
const labelFill = isLightBg ? text : accent;
13291343
const labelOpacity = isLightBg ? 0.8 : 0.7;
13301344

1331-
const grid = renderHeatmapGrid(calendar, accent, text, sf, stats.todayDate, params.mode, false);
1345+
const grid = renderHeatmapGrid(
1346+
calendar,
1347+
accent,
1348+
text,
1349+
sf,
1350+
stats.todayDate,
1351+
params.mode,
1352+
false,
1353+
params.glow !== false
1354+
);
13321355
const legend = renderHeatmapLegend(accent, text, sf, s(60), s(270), false);
13331356

13341357
const unit = params.mode === 'loc' ? 'lines of code' : 'total contributions';
13351358

1359+
const filterGlow =
1360+
params.glow !== false
1361+
? `<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
1362+
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
1363+
<feComposite in="SourceGraphic" in2="blur" operator="over" />
1364+
</filter>`
1365+
: '';
1366+
13361367
return `
13371368
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 ${W} ${H}" fill="none" role="img">
13381369
<title>CommitPulse Heatmap for ${safeUser}</title>
13391370
<desc>${safeUser} has ${stats.totalContributions} ${unit} and a longest streak of ${stats.longestStreak} days.</desc>
13401371
13411372
<defs>
1342-
<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
1343-
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
1344-
<feComposite in="SourceGraphic" in2="blur" operator="over" />
1345-
</filter>
1373+
${filterGlow}
13461374
</defs>
13471375
13481376
<style>
@@ -1444,21 +1472,35 @@ function generateAutoThemeHeatmapSVG(
14441472
const H = Math.round(300 * sf);
14451473
const s = createScaler(sf);
14461474

1447-
const grid = renderHeatmapGrid(calendar, '', '', sf, stats.todayDate, params.mode, true);
1475+
const grid = renderHeatmapGrid(
1476+
calendar,
1477+
'',
1478+
'',
1479+
sf,
1480+
stats.todayDate,
1481+
params.mode,
1482+
true,
1483+
params.glow !== false
1484+
);
14481485
const legend = renderHeatmapLegend('', '', sf, s(60), s(270), true);
14491486

14501487
const unit = params.mode === 'loc' ? 'lines of code' : 'total contributions';
14511488

1489+
const filterGlow =
1490+
params.glow !== false
1491+
? `<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
1492+
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
1493+
<feComposite in="SourceGraphic" in2="blur" operator="over" />
1494+
</filter>`
1495+
: '';
1496+
14521497
return `
14531498
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 ${W} ${H}" fill="none" role="img">
14541499
<title>CommitPulse Heatmap for ${safeUser}</title>
14551500
<desc>${safeUser} has ${stats.totalContributions} ${unit} and a longest streak of ${stats.longestStreak} days.</desc>
14561501
14571502
<defs>
1458-
<filter id="hm-glow" x="-50%" y="-50%" width="200%" height="200%">
1459-
<feGaussianBlur stdDeviation="${Math.round(3 * sf)}" result="blur" />
1460-
<feComposite in="SourceGraphic" in2="blur" operator="over" />
1461-
</filter>
1503+
${filterGlow}
14621504
</defs>
14631505
14641506
<style>

lib/validations.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,25 @@ describe('streakParamsSchema — boolean transform fields', () => {
631631
expect(parse({}).hide_background).toBe(false);
632632
});
633633
});
634+
635+
// ── glow ──────────────────────────────────────────────────────────────────
636+
describe('glow', () => {
637+
it('returns true when glow="true"', () => {
638+
expect(parse({ glow: 'true' }).glow).toBe(true);
639+
});
640+
641+
it('returns true when glow="1"', () => {
642+
expect(parse({ glow: '1' }).glow).toBe(true);
643+
});
644+
645+
it('returns false when glow="false"', () => {
646+
expect(parse({ glow: 'false' }).glow).toBe(false);
647+
});
648+
649+
it('returns true when glow is omitted', () => {
650+
expect(parse({}).glow).toBe(true);
651+
});
652+
});
634653
});
635654

636655
describe('ogParamsSchema', () => {

lib/validations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ export const streakParamsSchema = z.object({
240240
.string()
241241
.optional()
242242
.transform((val) => val === 'true' || val === '1'),
243+
glow: z
244+
.string()
245+
.optional()
246+
.transform((val) => {
247+
if (val === undefined) return true;
248+
return val === 'true' || val === '1';
249+
})
250+
.default(true),
243251
});
244252

245253
export const githubParamsSchema = z.object({

types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export interface BadgeParams {
210210
gradient?: boolean;
211211

212212
disable_particles?: boolean;
213+
glow?: boolean;
213214
}
214215

215216
export interface GraphNode {

0 commit comments

Comments
 (0)