Make leaderboard rendering pixel-perfect and support full rankings - #255
Conversation
The leaderboard read as blurry on the panel and stopped partway through the list. Three separate causes: Anti-aliasing. Press Start 2P only rasterises without anti-aliasing at multiples of 8, but the renderer loaded it at 6/10/12/14 and left PIL's default font mode on. Measured on a 10-team NFL strip, 35% of lit text pixels were partially-lit greys across 39 distinct colours — on a 1:1 LED matrix each of those is a dim LED, not a smooth edge. Text now draws with fontmode "1" (the idiom the news and baseball plugins already use) and default sizes snap to the font's pixel grid, giving exactly two colours and zero anti-aliased pixels. Logos. They were scaled to 120% of the panel height and pasted at a negative offset, so the top and bottom of every logo was cropped, then squashed into a square box regardless of source aspect ratio. LANCZOS also left a ring of semi-transparent edge pixels that lit as a halo. Logos now fit inside the panel, keep their aspect ratio, and have their alpha thresholded. Layout. Measuring and drawing were two passes over the same data that budgeted differently — the width pass always counted a team logo while the draw pass skipped missing ones, and the draw pass advanced 10px further per league than the measure pass. The strip came out either padded with dead space or clipped short of the last team. Both now share one resolved layout, so measured width is drawn width. For showing the full rankings: top_teams accepts 0 to mean every team a league returns. A long list also needs the time to scroll — the display controller allows min(plugin cap, core cap) seconds and then moves on mid-scroll, which is what makes a long list appear to cut off at an arbitrary team. The plugin now computes whether the content fits that budget and warns at startup naming the limiting cap and how much of the list will not be reached; the README documents all three caps. Also seeds self.scroll_speed on the time-based path, where get_info() would otherwise raise AttributeError on the default config. Adds test_pixel_perfect.py (15 tests) covering anti-aliasing, grid snapping, panel bounds, logo alpha/aspect, and measure-vs-draw width, plus a test/harness.json fixture so the core safety harness renders the real drawing path instead of only the no-data fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk
|
Warning Review limit reached
Next review available in: 20 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
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 |
The manifest gate requires the newly-added versions[] entry to use ledmatrix_min_version; ledmatrix_min is the deprecated spelling and is not what the store and loader read. Older entries are left as they are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 81 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
The except/continue added for the duration-cap lookup discarded the exception with no trace, so a persistently broken config manager would show only as the plugin quietly using the default cap. Logs at debug with the traceback, matching how BasePlugin.global_config handles the same failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk
Follows the same three defects fixed in the leaderboard (#255). news already had this -- it is where the fontmode idiom came from -- so it is untouched here. Anti-aliasing. PIL anti-aliases text by default, and on a 1:1 LED matrix a partially-lit pixel is a visibly dim LED rather than a smooth edge. None of these three plugins set fontmode, so all their text carried the fringe. Each now routes every draw surface -- including the scratch canvases used only to measure text, so metrics cannot disagree with what is drawn -- through a _pixel_draw() helper that sets fontmode "1". How much that mattered differed per plugin, measured on the string "AAPL +1.2% Earnings": - stock-news derives its font size from the panel height when one is not configured, and height//3 lands on 10 for a 32px panel. Press Start 2P only rasterises cleanly at multiples of 8, so 57% of the lit pixels were partial. Now 0%. - odds-ticker and ledmatrix-stocks default their element sizes to 8, already on the grid, so their defaults were crisp by luck rather than by construction -- any size a user set in the customization UI was not. At a configured 12: 41% partial before, 0% after. Font grid. stock-news' auto size is now snapped to the font's grid, so the 32px panel gets 8 instead of 10 and glyph pixels map 1:1 rather than alternating 1px and 2px strokes. Only that case changes; 48/64/96px panels already resolved to 16. An explicit font_size is honoured as typed. Logos. odds-ticker sized team logos to int(height * 1.2) and then centred them with (height - logo_size) // 2, which is -3 on a 32px panel -- so the top and bottom of every logo was cropped off the panel, the same bug the leaderboard had. Logos now fit inside the panel. Claude-Session: https://claude.ai/code/session_01WexvwNDtWLVymGVqKD7BGk Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Pull Request
Summary
The leaderboard read as blurry on the panel and stopped partway through the list. This makes text and logos pixel-perfect on the LED grid, fixes a layout bug where the measured strip width disagreed with what was drawn, and lets
top_teams: 0show every team a league returns — with a startup warning when the list is longer than the display-duration budget can reach.Type of change
Plugin(s) affected
ledmatrix-leaderboard(1.2.3 → 1.3.0)Related issues
N/A — reported directly.
What was wrong
1. Anti-aliasing (the blur). Press Start 2P only rasterises without anti-aliasing at multiples of 8, but the renderer loaded it at 6/10/12/14 and left PIL's default font mode on. Measured on a 10-team NFL strip:
On a 1:1 LED matrix each partially-lit pixel is a dim LED, not a smooth edge. Text now draws with
fontmode = "1"— the idiomnewsandbaseball-scoreboardalready use — and default sizes snap to the font's pixel grid (8px on a 32px panel, 16px on 64px).2. Logos. They were scaled to
int(height * 1.2)and pasted at a negative y, so the top and bottom of every logo was cropped off the panel; they were also forced into a square box regardless of source aspect ratio, and LANCZOS left a ring of semi-transparent edge pixels that lit as a halo. Logos now fit inside the panel, keep their aspect ratio, and have their alpha thresholded to hard on/off.3. Layout. Measuring and drawing were two passes over the same data that budgeted differently — the width pass always counted a team logo while the draw pass skipped missing ones, and the draw pass advanced 10px further per league than the measure pass. The strip came out either padded with dead space (1322px allocated for 839px of content in the missing-logo case) or clipped short of the last team. Both passes now share one resolved layout.
4. Showing the full rankings.
top_teamsaccepts0to mean every team the league returns. A long list also needs time to scroll: the display controller allowsmin(plugin cap, core cap)seconds and then moves on mid-scroll, which is what makes a long list appear to cut off at an arbitrary team. The core'sdisplay.dynamic_duration.max_duration_secondsdefaults to 180s and is usually the binding one — all 32 NFL teams is ~3,200px, about 240s at the default 15 px/s. The plugin now reads the real cap and warns at startup naming the limiting setting and roughly how much of the list will not be reached. The README documents all three caps.Also seeds
self.scroll_speedon the time-based path, whereget_info()would otherwise raiseAttributeErroron the default config.New config (
global.appearance)pixel_perfect_text,crisp_logos,text_outline,logo_scale,font_size— all with defaults matching the new behaviour, fine-tuning keys markedx-advanced.pixel_perfect_text: falserestores the old softer look.Test plan
EMULATOR=true python3 run.py)default,descriptionand constraints)Additional automated coverage:
test_pixel_perfect.py(15 tests, all passing) — asserts zero anti-aliased text pixels at three panel heights, font sizes on the pixel grid, nothing drawn past the panel edge, logo alpha thresholding and aspect preservation, and that measured width matches drawn content both with and without logos.test/harness.json+test/fixtures/mock.json— the core safety harness previously only ever rendered the no-data fallback here because CI has no network. With the fixture it renders the real drawing path;check_plugin.pyPASSes at all 8 sizes.scripts/check_module_collisions.py: OK across 43 plugins.Not verified on real hardware — I don't have a panel in this environment, so the crispness claims rest on pixel measurements and rendered PNGs rather than a photo of the matrix.
Required for plugin changes
versioninplugins/<id>/manifest.json(1.2.3 → 1.3.0, with aversionschangelog entry)class_nameinmanifest.jsonmatches the actual class inmanager.pyexactlyentry_pointmatches the real fileREADME.md(new Appearance section and a "How many teams are shown" section covering the duration caps)config_schema.jsonis the source of truth for the web UI form — every new option has adefault,description, and constraintsplugins.jsonsynced to 1.3.0)Checklist
CONTRIBUTING.mdCONTRIBUTING.mdandCODE_OF_CONDUCT.mdNotes for reviewer
global.appearance.font_sizelets users pick either way (values are snapped to the grid).top_teamsminimum changed from 5 to 0 in the schema for every league, so0can mean "all". Existing configs are unaffected.test_pixel_perfect.pycover the same ground more robustly.plugin_manager.config_manager/cache_manager.config_manager(the same resolution orderBasePlugin.global_configuses) because this plugin reassignsself.global_configto its own config slice. It falls back to the core's documented 180s default if neither manager is reachable.Generated by Claude Code