fix(pwa): give maskable icons a real background and safe zone - #239
Merged
Conversation
The manifest pointed `purpose: "maskable"` at the same full-bleed,
transparent PNGs it used for `purpose: "any"`. Those are different
contracts: Android applies its own mask (circle, squircle, teardrop)
and only guarantees the central circle of 80% diameter survives.
Two consequences, both measured against the committed 512px icon:
- 4.3% of the logo's opaque pixels sit outside the safe zone, so the
swoosh tips get clipped. Content reached 0.527 of the icon width
from centre against a 0.400 budget.
- Every pixel outside the glyph is transparent, so the launcher draws
the logo straight onto the wallpaper instead of a solid tile.
Generate a dedicated maskable family instead. The artwork is trimmed to
its opaque bounds (which also re-centres it — it sits 82px from the left
of favicon.svg's viewBox and 50px from the right), scaled so its farthest
opaque pixel lands inside the safe circle, then flattened onto the
manifest's own background_color so icon and splash screen agree.
Scaling by farthest-pixel radius rather than by the bounding box matters:
the glyph is irregular, so its bounding-box corners are empty and the
box rule would shrink it more than the mask requires.
The `any`, favicon and Windows-tile families keep their transparency —
that is correct for those, and only the maskable pair is flattened.
Tests assert the two properties on the actual pixels, not just the
manifest wiring; all five fail if the manifest is pointed back at the
android-chrome files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan96 finding(s) HIGH/CRITICAL: 3 | MEDIUM: 17 | LOW: 76
…and 46 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #236. The manifest declared
purpose: "maskable"on the same full-bleed, transparent PNGs it used forpurpose: "any"— but those are different contracts. Android applies its own mask (circle, squircle, teardrop) and only guarantees the central circle of 80% diameter survives.What was wrong
Measured against the committed
android-chrome-512x512.png:So the swoosh tips were croppable, and the launcher drew the logo onto bare wallpaper rather than a solid tile.
The fix
A dedicated maskable family —
icon-maskable-192x192.pngandicon-maskable-512x512.png— rather than re-labelling the existing icons. The generator trims the artwork to its opaque bounds, scales it so its farthest opaque pixel lands inside the safe circle, centres it, and flattens onto the manifest's ownbackground_color(#ffffff) so the icon and the splash screen it launches into share a background.Two details worth calling out:
favicon.svg's viewBox — 82px of padding on the left against 50px on the right — which a plain resize preserves and the mask then crops unevenly.The
any, favicon and Windows-tile families keep their transparency — that is correct for those. Only the maskable pair is flattened.Verification
Five new tests assert the two properties against the decoded pixels, not the manifest wiring: fully opaque, and all artwork inside
0.4 × widthof centre. Pointing the manifest back at theandroid-chromefiles fails all five, so they aren't vacuous.Full suite: 80 files, 480 tests green.
No existing icon binary is touched — tooling, manifest and two new files only.
Not included
apple-touch-icon-*.pngare also transparent (alpha min = 0), and iOS composites those onto black rather than supplying a background. Same class of bug, but fixing it changes how the icon looks on the iOS home screen, so it's left as a separate call.🤖 Generated with Claude Code