feat: add dynamic contribution shading and volumetric gradients#808
feat: add dynamic contribution shading and volumetric gradients#808Sahitya3105 wants to merge 3 commits into
Conversation
|
@Sahitya3105 is attempting to deploy a commit to the jhasourav07's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
👋 Hey @Sahitya3105, welcome to CommitPulse! 🎉 Thanks for opening your first pull request — this is a big deal and we appreciate the effort! While you wait for a review, please double-check:
A maintainer will review your PR shortly. Hang tight! 🚀 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR extends the streak SVG rendering to support multi-accent (quartile-based) coloring plus optional shading/gradient effects, and wires the new query params through validation and the API route.
Changes:
- Allow
accentto be either a single hex color or a list of hex colors, and compute per-day intensity quartiles. - Add
shadingandgradientparameters, including Zod parsing and SVG rendering updates (gradient<defs>+ opacity adjustments). - Add tests covering gradient
<linearGradient>output and accent-array color mapping.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| types/index.ts | Expands BadgeParams to support accent arrays and adds shading/gradient flags. |
| lib/validations.ts | Parses comma-separated accent lists and introduces shading/gradient boolean transforms. |
| lib/svg/layout.ts | Computes intensityLevel for each tower based on max contributions in the window. |
| lib/svg/generator.ts | Renders gradients in <defs>, applies shading/gradient fills per intensity, and reuses a unified renderTowers. |
| lib/svg/generator.test.ts | Adds tests for gradient defs and accent-array mapping. |
| app/api/streak/route.ts | Plumbs shading and gradient through request parsing into params. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const baseAccentColor = Array.isArray(accent) ? accent[accent.length - 1] : accent; | ||
|
|
||
| const accentColorHex = baseAccentColor.startsWith('#') | ||
| ? baseAccentColor | ||
| : `#${baseAccentColor}`; | ||
| const textColorHex = text.startsWith('#') ? text : `#${text}`; | ||
|
|
||
| let resolvedSolidColor = isGhost ? textColorHex : accentColorHex; | ||
| if (!isGhost && t.intensityLevel > 0 && Array.isArray(accent)) { | ||
| const quartileColor = accent[t.intensityLevel - 1]; | ||
| resolvedSolidColor = quartileColor.startsWith('#') ? quartileColor : `#${quartileColor}`; | ||
| } |
| leftFillAttr = `fill="url(#tower-grad-level-${t.intensityLevel})"`; | ||
| rightFillAttr = `fill="url(#tower-grad-level-${t.intensityLevel})"`; | ||
|
|
||
| if (isAutoTheme) { | ||
| topFillAttr = 'class="cp-accent-fill"'; | ||
| } else { | ||
| const baseAccentColor = Array.isArray(accent) ? accent[t.intensityLevel - 1] : accent; | ||
| const capColor = baseAccentColor.startsWith('#') ? baseAccentColor : `#${baseAccentColor}`; | ||
| topFillAttr = `fill="${capColor}"`; |
| if (t.contributionCount >= 10) { | ||
| const pColor = isAutoTheme | ||
| ? '' | ||
| : Array.isArray(accent) | ||
| ? accent[t.intensityLevel - 1].startsWith('#') | ||
| ? accent[t.intensityLevel - 1] | ||
| : `#${accent[t.intensityLevel - 1]}` | ||
| : accent.startsWith('#') | ||
| ? accent | ||
| : `#${accent}`; | ||
| towers += generateParticles(t.x, t.y, t.h, t.contributionCount, sf, isAutoTheme, pColor); | ||
| } |
| let gradients = ''; | ||
| if (gradient) { | ||
| if (isAutoTheme) { | ||
| for (let i = 0; i < 4; i++) { | ||
| const level = i + 1; | ||
| gradients += ` | ||
| <linearGradient id="tower-grad-level-${level}" x1="0" y1="1" x2="0" y2="0"> | ||
| <stop offset="0%" stop-color="var(--cp-bg)" stop-opacity="0.1" /> | ||
| <stop offset="100%" stop-color="var(--cp-accent)" stop-opacity="${0.4 + i * 0.2}" /> | ||
| </linearGradient>`; | ||
| } | ||
| } else { | ||
| const colors = Array.isArray(accent) | ||
| ? accent.map((c) => (c.startsWith('#') ? c : `#${c}`)) | ||
| : [1, 2, 3, 4].map(() => (String(accent).startsWith('#') ? String(accent) : `#${accent}`)); | ||
|
|
||
| const bgHex = bg.startsWith('#') ? bg : `#${bg}`; | ||
|
|
||
| colors.forEach((c, idx) => { | ||
| const level = idx + 1; | ||
| gradients += ` | ||
| <linearGradient id="tower-grad-level-${level}" x1="0" y1="1" x2="0" y2="0"> | ||
| <stop offset="0%" stop-color="${bgHex}" stop-opacity="0.1" /> | ||
| <stop offset="100%" stop-color="${c}" stop-opacity="${0.4 + idx * 0.2}" /> | ||
| </linearGradient>`; | ||
| }); | ||
| } | ||
| } |
| .transform((val) => { | ||
| if (!val) return undefined; | ||
| if (val.includes(',')) { | ||
| return val.split(',').map((c) => sanitizeHexColor(c.trim(), '00ffaa')); | ||
| } | ||
| return sanitizeHexColor(val, '00ffaa'); | ||
| }), |
| .string() | ||
| .optional() | ||
| .transform((val) => val !== 'false'), | ||
| gradient: z | ||
| .string() | ||
| .optional() | ||
| .transform((val) => val === 'true'), |
| .transform((val) => { | ||
| if (!val) return undefined; | ||
| if (val.includes(',')) { | ||
| return val.split(',').map((c) => sanitizeHexColor(c.trim(), '00ffaa')); | ||
| } | ||
| return sanitizeHexColor(val, '00ffaa'); | ||
| }), |
| let strokeColor = ''; | ||
| let fillClassLeftRight = ''; | ||
| let fillClassTop = ''; |
| fillClassLeftRight = `fill="${resolvedSolidColor}"`; | ||
| fillClassTop = fillClassLeftRight; |
|
Attach visual preview |
| const baseAccentColor = Array.isArray(accent) ? accent[accent.length - 1] : accent; | ||
|
|
||
| const accentColorHex = baseAccentColor.startsWith('#') | ||
| ? baseAccentColor | ||
| : `#${baseAccentColor}`; | ||
| const textColorHex = text.startsWith('#') ? text : `#${text}`; |
| const towers = renderTowers(towerData, accent, text, sf); | ||
| const towers = renderTowers(towerData, params, accent, text, sf, false); | ||
|
|
||
| const mainAccent = Array.isArray(accent) ? accent[accent.length - 1] : accent; |
| return val | ||
| .split(',') | ||
| .map((c) => c.trim()) | ||
| .filter((c) => c.length > 0) | ||
| .slice(0, 4) | ||
| .map((c) => sanitizeHexColor(c, '00ffaa')); |
| if (!isGhost && t.intensityLevel > 0 && params.shading !== false) { | ||
| const mult = opacityMultipliers[t.intensityLevel - 1]; | ||
| leftFaceOpacity *= mult; | ||
| rightFaceOpacity *= mult; | ||
| topFaceOpacity *= mult; | ||
| } |
| }); | ||
| }); | ||
|
|
||
| describe('shading and gradients', () => { |
|
Address copilot comment and resolve comflicts please |
|
- Guard against empty accent array crash: in validations.ts, accent=,,, now returns undefined instead of [] so theme default applies - Normalise empty accent array in generateSVG: params.accent=[] falls back to '00ffaa' before reaching renderTowers - Guard baseAccentColor in renderTowers: Array.isArray(accent) path now returns '00ffaa' fallback when array is empty - Guard mainAccent in generateSVG: same empty-array fallback - Resolve merge conflict with upstream/main: kept gradient-aware renderHeader, integrated upstream's renderDefs refactoring by accepting gradients as an optional parameter - Rename describe block 'shading and gradients' to 'gradients and multi-accent mapping' to match actual test coverage (Copilot LOW) - Add explicit shading describe block with three tests covering shading=true vs shading=false opacity differences and empty-array fallback (Copilot MEDIUM)
done |
|
kindly attach a visual preview you can get the token from the developer setting. |
|
Please pull the latest changes and resolve the conflicts so we can review it! git fetch origin
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-leaseOnce resolved, the |
1 similar comment
|
Please pull the latest changes and resolve the conflicts so we can review it! git fetch origin
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-leaseOnce resolved, the |
Description
Fixes #793
This PR implements premium dynamic contribution-level color shading, multi-accent color mapping, and volumetric tower gradients on the 3D contribution monolith.
Changes Made
shading,gradient, and array-support foraccentparameters inBadgeParamsandstreakParamsSchema.computeTowersbased on maximum commit counts.<linearGradient>injection fading upward from the background color to the tower's level-specific accent color. Left and right faces use the gradient fill, while the top cap retains a solid glow.accentparameter mapping directly to quartile levels 1-4.generateMonthlySVGto safe fallback strings.lib/svg/generator.test.tscovering shading, disabling shading, gradients, and multi-accent arrays.Pillar
Visual Preview
shading=true): Towers fade into the floor for lower-intensity contribution days.gradient=true): Sleek vertical linear gradients fading from the background up to the tower tops.Checklist before requesting a review:
CONTRIBUTING.mdfile.localhost:3000/api/streak?user=YOUR_USERNAME).npm run formatandnpm run lintlocally and resolved all errors (CI will fail otherwise).feat(themes): ...,fix(calculate): ...).README.mdif I added a new theme or URL parameter.