Skip to content

feat: add dynamic contribution shading and volumetric gradients#808

Open
Sahitya3105 wants to merge 3 commits into
JhaSourav07:mainfrom
Sahitya3105:feat/shading-gradients
Open

feat: add dynamic contribution shading and volumetric gradients#808
Sahitya3105 wants to merge 3 commits into
JhaSourav07:mainfrom
Sahitya3105:feat/shading-gradients

Conversation

@Sahitya3105
Copy link
Copy Markdown

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

  • API Parameters: Added shading, gradient, and array-support for accent parameters in BadgeParams and streakParamsSchema.
  • Tower Math: Integrated quartile calculation logic (1 to 4) inside computeTowers based on maximum commit counts.
  • Volumetric Gradients: Added dynamic SVG <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.
  • Multi-accent Mapping: Allowed comma-separated color lists under the accent parameter mapping directly to quartile levels 1-4.
  • Safety Refactors: Resolved array mapping in generateMonthlySVG to safe fallback strings.
  • Testing: Added 4 comprehensive test suites in lib/svg/generator.test.ts covering shading, disabling shading, gradients, and multi-accent arrays.

Pillar

  • 🎨 Pillar 1 — New Theme Design
  • 📐 Pillar 2 — Geometric SVG Improvement
  • 🕐 Pillar 3 — Timezone Logic Optimization
  • 🛠️ Other

Visual Preview

  • Flat Shading (shading=true): Towers fade into the floor for lower-intensity contribution days.
  • Volumetric Gradients (gradient=true): Sleek vertical linear gradients fading from the background up to the tower tops.

Checklist before requesting a review:

  • I have read the CONTRIBUTING.md file.
  • I have tested these changes locally (localhost:3000/api/streak?user=YOUR_USERNAME).
  • I have run npm run format and npm run lint locally and resolved all errors (CI will fail otherwise).
  • My commits follow the Conventional Commits format (e.g., feat(themes): ..., fix(calculate): ...).
  • I have updated README.md if I added a new theme or URL parameter.
  • I have started the repo.
  • I have made sure that i have only one commit to merge in this PR.
  • The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts).
  • (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.

Copilot AI review requested due to automatic review settings May 27, 2026 19:45
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 27, 2026

@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.

@github-actions
Copy link
Copy Markdown

👋 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:

  • ✅ You've read the CONTRIBUTING.md checklist
  • npm run lint, npm run format, and npm run test all pass locally
  • ✅ Your PR has a visual preview if it touches any SVG output
  • 💬 You've joined our Discord for faster PR feedback

A maintainer will review your PR shortly. Hang tight! 🚀

@github-actions github-actions Bot added the type:feature New features, additions, or enhancements label May 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 accent to be either a single hex color or a list of hex colors, and compute per-day intensity quartiles.
  • Add shading and gradient parameters, 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.

Comment thread lib/svg/generator.ts Outdated
Comment on lines +222 to +233
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}`;
}
Comment thread lib/svg/generator.ts Outdated
Comment on lines +256 to +264
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}"`;
Comment thread lib/svg/generator.ts
Comment on lines +285 to +296
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);
}
Comment thread lib/svg/generator.ts
Comment on lines +121 to +148
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>`;
});
}
}
Comment thread lib/validations.ts
Comment on lines +26 to +32
.transform((val) => {
if (!val) return undefined;
if (val.includes(',')) {
return val.split(',').map((c) => sanitizeHexColor(c.trim(), '00ffaa'));
}
return sanitizeHexColor(val, '00ffaa');
}),
Comment thread lib/validations.ts Outdated
Comment on lines +104 to +110
.string()
.optional()
.transform((val) => val !== 'false'),
gradient: z
.string()
.optional()
.transform((val) => val === 'true'),
Comment thread lib/validations.ts
Comment on lines +26 to +32
.transform((val) => {
if (!val) return undefined;
if (val.includes(',')) {
return val.split(',').map((c) => sanitizeHexColor(c.trim(), '00ffaa'));
}
return sanitizeHexColor(val, '00ffaa');
}),
Comment thread lib/svg/generator.ts Outdated
Comment on lines +213 to +215
let strokeColor = '';
let fillClassLeftRight = '';
let fillClassTop = '';
Comment thread lib/svg/generator.ts Outdated
Comment on lines +236 to +237
fillClassLeftRight = `fill="${resolvedSolidColor}"`;
fillClassTop = fillClassLeftRight;
@JhaSourav07 JhaSourav07 requested a review from Copilot May 27, 2026 19:59
@JhaSourav07
Copy link
Copy Markdown
Owner

@Sahitya3105

Attach visual preview
video or screenshot for review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Comment thread lib/svg/generator.ts Outdated
Comment on lines +226 to +231
const baseAccentColor = Array.isArray(accent) ? accent[accent.length - 1] : accent;

const accentColorHex = baseAccentColor.startsWith('#')
? baseAccentColor
: `#${baseAccentColor}`;
const textColorHex = text.startsWith('#') ? text : `#${text}`;
Comment thread lib/svg/generator.ts Outdated
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;
Comment thread lib/validations.ts Outdated
Comment on lines +29 to +34
return val
.split(',')
.map((c) => c.trim())
.filter((c) => c.length > 0)
.slice(0, 4)
.map((c) => sanitizeHexColor(c, '00ffaa'));
Comment thread lib/svg/generator.ts
Comment on lines +249 to +254
if (!isGhost && t.intensityLevel > 0 && params.shading !== false) {
const mult = opacityMultipliers[t.intensityLevel - 1];
leftFaceOpacity *= mult;
rightFaceOpacity *= mult;
topFaceOpacity *= mult;
}
Comment thread lib/svg/generator.test.ts Outdated
});
});

describe('shading and gradients', () => {
@JhaSourav07
Copy link
Copy Markdown
Owner

Address copilot comment and resolve comflicts please

@Sahitya3105
Copy link
Copy Markdown
Author

@Sahitya3105

Attach visual preview video or screenshot for review

@Sahitya3105 Sahitya3105 reopened this May 27, 2026
- 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)
@Sahitya3105
Copy link
Copy Markdown
Author

Address copilot comment and resolve comflicts please

done

@JhaSourav07
Copy link
Copy Markdown
Owner

@Sahitya3105

kindly attach a visual preview

you can get the token from the developer setting.
for more info refer to CONTRIBUTING.md

@github-actions github-actions Bot added the needs-rebase This PR has merge conflicts and needs a rebase. label May 28, 2026
@github-actions
Copy link
Copy Markdown

⚠️ Hey @Sahitya3105, this PR has merge conflicts with the main branch.

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-lease

Once resolved, the needs-rebase label will be removed automatically on the next check. 🙌

1 similar comment
@github-actions
Copy link
Copy Markdown

⚠️ Hey @Sahitya3105, this PR has merge conflicts with the main branch.

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-lease

Once resolved, the needs-rebase label will be removed automatically on the next check. 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase This PR has merge conflicts and needs a rebase. status: hold type:feature New features, additions, or enhancements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: dynamic contribution-level color shading and volumetric tower gradients

3 participants