fix(sponsorkit): bake avatar shape masks into the bitmaps for transparent corners#5745
Conversation
…rent corners Avatars were embedded as square JPEGs shaped only by a runtime SVG clip-path. Browsers do not clip <image> elements reliably: WebKit and Chromium's software rasterizer (and occasionally GPU Chrome during paint) draw the raw square bitmap, so avatar corners showed outside the circle/squircle rings on the credits pages. Cut the shape into the pixels at generation time instead: the circle (sponsors) or superellipse squircle (contributors, matching squirclePathD) is applied to the alpha channel with an anti-aliased edge. Photographic avatars embed as JPEG with corners flattened onto the card background colour (JPEG has no alpha; the card is always behind them); flat-colour avatars embed as lossless palette PNGs with genuinely transparent corners. The smaller encoding wins per avatar. The runtime clip-path attributes stay as a best-effort for the rare undecodable (webp) passthrough avatars. Verified in headless WebKit and Chromium, which reproduced the unclipped corners with the old files and render clean shapes with the new ones. Both images regenerated with the new tool.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (2)
WalkthroughAvatar generation in tools/sponsorkit is changed to apply superellipse alpha masks (circle or squircle) instead of flattening transparency directly. FetchAvatars/fetchAvatar gain a maskExp parameter, new helpers toPaletted and maskShape are added, and encoding adaptively chooses PNG or JPEG. main.go wires circle masks for sponsors and squircle masks for contributors. ChangesMasked Avatar Generation
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Main as generateSponsors/generateContributors
participant FetchAvatars
participant fetchAvatar
participant maskShape
Main->>FetchAvatars: FetchAvatars(..., maskExp)
FetchAvatars->>fetchAvatar: fetchAvatar(url, size, quality, maskExp)
fetchAvatar->>maskShape: compute superellipse alpha mask
maskShape-->>fetchAvatar: anti-aliased mask
fetchAvatar-->>FetchAvatars: PNG or JPEG data URI
Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… avatar shape masks into the bitmaps for transparent corners
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 updates the avatar pipeline in tools/sponsorkit to bake circle/squircle masking into downloaded avatar bitmaps and choose an optimal embedded image encoding (PNG vs JPEG) to avoid unreliable SVG <image> clipping behavior.
Changes:
- Extend
FetchAvatars/fetchAvatarto apply a superellipse-based mask (circle for sponsors, squircle for contributors). - Re-encode avatars as either PNG (optionally paletted) or JPEG depending on which yields a smaller data URI.
- Add masking, palettization, and PNG encoding helpers.
Reviewed changes
Copilot reviewed 2 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tools/sponsorkit/main.go | Updates call sites to pass circle/squircle mask exponents into avatar fetching. |
| tools/sponsorkit/avatar.go | Implements bitmap masking and size-based PNG/JPEG re-encoding (including optional paletted PNG). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| rx, ry := float64(w)/2, float64(h)/2 | ||
| for y := 0; y < h; y++ { | ||
| fy := math.Abs((float64(y) + 0.5 - ry) / ry) | ||
| for x := 0; x < w; x++ { | ||
| fx := math.Abs((float64(x) + 0.5 - rx) / rx) | ||
| // Normalised superellipse "radius" of this pixel; the shape edge | ||
| // sits at d=1 and d scales roughly linearly with distance near it. | ||
| d := math.Pow(math.Pow(fx, exp)+math.Pow(fy, exp), 1/exp) | ||
| cov := (1-d)*rx + 0.5 |
| for y := 0; y < h; y++ { | ||
| fy := math.Abs((float64(y) + 0.5 - ry) / ry) | ||
| for x := 0; x < w; x++ { | ||
| fx := math.Abs((float64(x) + 0.5 - rx) / rx) | ||
| // Normalised superellipse "radius" of this pixel; the shape edge | ||
| // sits at d=1 and d scales roughly linearly with distance near it. | ||
| d := math.Pow(math.Pow(fx, exp)+math.Pow(fy, exp), 1/exp) |
| var pbuf bytes.Buffer | ||
| enc := png.Encoder{CompressionLevel: png.BestCompression} | ||
| if pal := toPaletted(masked); pal != nil { |
| // scaled for hi-dpi), bakes the shape mask into the bitmap and returns data | ||
| // URIs keyed by login. The mask is applied to the pixels rather than left to | ||
| // a runtime clip-path because browsers do not clip <image> elements reliably | ||
| // (WebKit, and Chromium's software rasterizer, intermittently paint the raw | ||
| // square bitmap). Failures are reported but non-fatal: missing entries fall | ||
| // back to a placeholder at render time. | ||
| func FetchAvatars(client *http.Client, sponsors []Sponsor, sizes map[string]int, quality int, maskExp float64) map[string]string { |
Fixes the square avatar corners visible outside the circle/squircle rings on the credits pages.
Why corners showed
Avatars were embedded as square JPEGs and shaped only by a runtime
clip-path. Browsers don't clip SVG<image>elements reliably: headless WebKit and Chromium's software rasterizer consistently paint the raw square bitmap (and GPU Chrome does so intermittently during paint), so the JPEG corners poked out past the rings.The fix
Cut the shape into the pixels at generation time, so no renderer clipping is needed anywhere (pages, READMEs, any engine):
maskShapeapplies the circle (sponsors) or the same n=4 superellipse squircle used bysquirclePathD(contributors) to the alpha channel, with a ~1px anti-aliased edge.clip-pathstays as best-effort for the rare undecodable webp passthrough.Sizes
Verification
Reproduced the bug and verified the fix with headless WebKit + Chromium (Playwright) rendering the actual generated files: old files show square corners in both engines, new files render clean circles/squircles, squircle edge checked at 3× zoom. Both images in this PR were regenerated with the new tool using the maintainer token (tier data correct: Champion/Gold/Silver/Bronze groups match the live card).
Summary by CodeRabbit
New Features
Bug Fixes