feat: refine watch UI — arc HR gauge, proper padel court, spacing

- HR shown as a top arc gauge in Garmin zone colors (grey/blue/green/
  orange/red) with a marker on the current zone and the bpm number below
- Redraw court to a proper padel layout: net centered, service lines at
  the 3m-from-back-wall proportion, center line only between service lines
- Wider court with a configurable blue surface (bgColor param on
  CourtRenderer, ready for the future settings screen)
- Serve ball marks the back zone (farthest from the net) of the server
- Roomier score spacing; games column white, sets accent green; "Pts"
- HR + time on a matched FONT_MEDIUM

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 16:42:33 +02:00
parent 33d3f4f08e
commit 926f876a6e
2 changed files with 81 additions and 50 deletions

View File

@@ -5,26 +5,34 @@ import Toybox.Graphics;
// in the given quadrant. Half-width HW and half-height HH keep it inside the // in the given quadrant. Half-width HW and half-height HH keep it inside the
// central band of the round screen. // central band of the round screen.
class CourtRenderer { class CourtRenderer {
private const HW = 60; // half court width (px) private const HW = 92; // half court width (px)
private const HH = 78; // half court height (px) private const HH = 95; // half court height (px)
private const BALL_R = 6; private const BALL_R = 7;
// bgColor fills the court surface (blue for now; a future settings screen
// will drive it via RulesConfig).
public function draw(dc as Graphics.Dc, cx as Number, cy as Number, public function draw(dc as Graphics.Dc, cx as Number, cy as Number,
quadrant as Number, showBall as Boolean) as Void { quadrant as Number, showBall as Boolean,
bgColor as Number) as Void {
var left = cx - HW; var left = cx - HW;
var top = cy - HH; var top = cy - HH;
// Service line sits 3m from the back wall (7m from the net) on a 10m
// half, i.e. 0.7 of the way from the net to the back boundary.
var sl = (7 * HH) / 10;
dc.setColor(bgColor, Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(left, top, 2 * HW, 2 * HH); // court surface
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.setPenWidth(2); dc.setPenWidth(2);
dc.drawRectangle(left, top, 2 * HW, 2 * HH); // outer court dc.drawRectangle(left, top, 2 * HW, 2 * HH); // outer boundary
dc.drawLine(cx, top, cx, cy + HH); // center service line dc.drawLine(left, cy - sl, left + 2 * HW, cy - sl); // upper service line
dc.drawLine(left, cy + sl, left + 2 * HW, cy + sl); // lower service line
dc.drawLine(cx, cy - sl, cx, cy + sl); // center line (between service lines only)
dc.setPenWidth(1); dc.setPenWidth(4);
dc.drawLine(left, cy - HH / 2, left + 2 * HW, cy - HH / 2); // upper service line dc.drawLine(left, cy, left + 2 * HW, cy); // net (boldest)
dc.drawLine(left, cy + HH / 2, left + 2 * HW, cy + HH / 2); // lower service line
dc.setPenWidth(3);
dc.drawLine(left, cy, left + 2 * HW, cy); // net
if (showBall) { if (showBall) {
var pos = _ballPos(quadrant, cx, cy); var pos = _ballPos(quadrant, cx, cy);
@@ -35,7 +43,7 @@ class CourtRenderer {
private function _ballPos(quadrant as Number, cx as Number, cy as Number) as Array<Number> { private function _ballPos(quadrant as Number, cx as Number, cy as Number) as Array<Number> {
var dx = HW / 2; var dx = HW / 2;
var dy = HH / 2; var dy = (17 * HH) / 20; // centered in the back zone, farthest from the net
if (quadrant == QUAD_BOTTOM_RIGHT) { return [cx + dx, cy + dy]; } if (quadrant == QUAD_BOTTOM_RIGHT) { return [cx + dx, cy + dy]; }
if (quadrant == QUAD_BOTTOM_LEFT) { return [cx - dx, cy + dy]; } if (quadrant == QUAD_BOTTOM_LEFT) { return [cx - dx, cy + dy]; }
if (quadrant == QUAD_UPPER_LEFT) { return [cx - dx, cy - dy]; } if (quadrant == QUAD_UPPER_LEFT) { return [cx - dx, cy - dy]; }

View File

@@ -3,10 +3,12 @@ import Toybox.Graphics;
import Toybox.WatchUi; import Toybox.WatchUi;
import Toybox.System; import Toybox.System;
import Toybox.Timer; import Toybox.Timer;
import Toybox.Math;
class garmin_padelView extends WatchUi.View { class garmin_padelView extends WatchUi.View {
private const ACCENT = 0x00E0A0; // padel green (sets/games) private const ACCENT = 0x00E0A0; // padel green (sets/games)
private const LABEL_COLOR = 0x808080; // muted grey (S/G/pts labels) private const LABEL_COLOR = 0x808080; // muted grey (S/G/pts labels)
private const COURT_BLUE = 0x1560BD; // court surface fill (configurable later)
private var _court as CourtRenderer; private var _court as CourtRenderer;
private var _timer as Timer.Timer; private var _timer as Timer.Timer;
@@ -39,11 +41,11 @@ class garmin_padelView extends WatchUi.View {
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK); dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
dc.clear(); dc.clear();
_court.draw(dc, cx, cy, m.servingQuadrant(), !m.isMatchOver()); _court.draw(dc, cx, cy, m.servingQuadrant(), !m.isMatchOver(), COURT_BLUE);
_drawLeftColumn(dc, m); _drawLeftColumn(dc, m);
_drawRightColumn(dc, m, w); _drawRightColumn(dc, m, w);
_drawHeartRate(dc, cx, 40); _drawHeartRate(dc, cx, cy);
_drawTime(dc, cx, h - 44); _drawTime(dc, cx, h - 78);
if (m.isMatchOver()) { if (m.isMatchOver()) {
_drawWinner(dc, cx, cy, m); _drawWinner(dc, cx, cy, m);
@@ -52,11 +54,11 @@ class garmin_padelView extends WatchUi.View {
// Left: S and G mini-columns, your team on top, opponents below. // Left: S and G mini-columns, your team on top, opponents below.
private function _drawLeftColumn(dc as Graphics.Dc, m as PadelMatch) as Void { private function _drawLeftColumn(dc as Graphics.Dc, m as PadelMatch) as Void {
var sX = 24; var sX = 30;
var gX = 60; var gX = 76;
var labelY = 150; var labelY = 148;
var usY = 178; var usY = 184;
var themY = 206; var themY = 240;
dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT);
dc.drawText(sX, labelY, Graphics.FONT_XTINY, "S", Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(sX, labelY, Graphics.FONT_XTINY, "S", Graphics.TEXT_JUSTIFY_CENTER);
@@ -65,58 +67,79 @@ class garmin_padelView extends WatchUi.View {
dc.setColor(ACCENT, Graphics.COLOR_TRANSPARENT); dc.setColor(ACCENT, Graphics.COLOR_TRANSPARENT);
dc.drawText(sX, usY, Graphics.FONT_TINY, m.sets(US).toString(), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(sX, usY, Graphics.FONT_TINY, m.sets(US).toString(), Graphics.TEXT_JUSTIFY_CENTER);
dc.drawText(sX, themY, Graphics.FONT_TINY, m.sets(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(sX, themY, Graphics.FONT_TINY, m.sets(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER);
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.drawText(gX, usY, Graphics.FONT_TINY, m.games(US).toString(), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(gX, usY, Graphics.FONT_TINY, m.games(US).toString(), Graphics.TEXT_JUSTIFY_CENTER);
dc.drawText(gX, themY, Graphics.FONT_TINY, m.games(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(gX, themY, Graphics.FONT_TINY, m.games(THEM).toString(), Graphics.TEXT_JUSTIFY_CENTER);
} }
// Right: pts column, your team on top, opponents below. // Right: Pts column, your team on top, opponents below.
private function _drawRightColumn(dc as Graphics.Dc, m as PadelMatch, w as Number) as Void { private function _drawRightColumn(dc as Graphics.Dc, m as PadelMatch, w as Number) as Void {
var pX = w - 42; var pX = w - 46;
var labelY = 150; var labelY = 148;
var usY = 178; var usY = 184;
var themY = 206; var themY = 240;
dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT); dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT);
dc.drawText(pX, labelY, Graphics.FONT_XTINY, "pts", Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(pX, labelY, Graphics.FONT_XTINY, "Pts", Graphics.TEXT_JUSTIFY_CENTER);
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.drawText(pX, usY, Graphics.FONT_TINY, m.pointLabel(US), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(pX, usY, Graphics.FONT_TINY, m.pointLabel(US), Graphics.TEXT_JUSTIFY_CENTER);
dc.drawText(pX, themY, Graphics.FONT_TINY, m.pointLabel(THEM), Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(pX, themY, Graphics.FONT_TINY, m.pointLabel(THEM), Graphics.TEXT_JUSTIFY_CENTER);
} }
private function _drawHeartRate(dc as Graphics.Dc, cx as Number, y as Number) as Void { // Heart rate as a top arc gauge: 5 zone-colored segments (grey/blue/green/
// orange/red), a white marker on the current zone, and the bpm number just
// below — mirroring the standard Garmin run-activity HR view.
private function _drawHeartRate(dc as Graphics.Dc, cx as Number, cy as Number) as Void {
var hr = getApp().getHeartRate(); var hr = getApp().getHeartRate();
var bpm = hr.getHeartRate(); var bpm = hr.getHeartRate();
var text = (bpm == null) ? "-- bpm" : bpm.toString() + " bpm"; var zone = hr.getZone();
dc.setColor(Graphics.COLOR_RED, Graphics.COLOR_TRANSPARENT); var r = cx - 9; // arc radius, hugging the top bezel
dc.drawText(cx, y, Graphics.FONT_TINY, text, Graphics.TEXT_JUSTIFY_CENTER);
_drawZoneBar(dc, cx, y + 26, hr.getZone()); // 5 zone segments across the top (150deg .. 30deg, clockwise)
dc.setPenWidth(14);
for (var k = 0; k < 5; k += 1) {
var segStart = 150 - k * 24;
dc.setColor(_zoneColor(k + 1), Graphics.COLOR_TRANSPARENT);
dc.drawArc(cx, cy, r, Graphics.ARC_CLOCKWISE, segStart - 2, segStart - 22);
}
dc.setPenWidth(1);
// marker on the current zone
if (zone >= 1 && zone <= 5) {
var mid = 150 - (zone - 1) * 24 - 12;
var rad = Math.toRadians(mid);
var mx = (cx + r * Math.cos(rad)).toNumber();
var my = (cy - r * Math.sin(rad)).toNumber();
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.fillCircle(mx, my, 8);
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
dc.drawCircle(mx, my, 8);
} }
// 5-segment bar; segments up to the current zone are filled. // exact rate, tucked just under the arc (dim placeholder when no reading,
private function _drawZoneBar(dc as Graphics.Dc, cx as Number, y as Number, zone as Number) as Void { // so it never reads as a court line)
var segW = 12; var text = (bpm == null) ? "--" : bpm.toString();
var segH = 6; var col = (bpm == null) ? LABEL_COLOR : Graphics.COLOR_WHITE;
var gap = 3; dc.setColor(col, Graphics.COLOR_TRANSPARENT);
var totalW = 5 * segW + 4 * gap; dc.drawText(cx, cy - 178, Graphics.FONT_MEDIUM, text, Graphics.TEXT_JUSTIFY_CENTER);
var x = cx - totalW / 2;
for (var i = 1; i <= 5; i += 1) {
if (i <= zone) {
dc.setColor(ACCENT, Graphics.COLOR_TRANSPARENT);
dc.fillRectangle(x, y, segW, segH);
} else {
dc.setColor(LABEL_COLOR, Graphics.COLOR_TRANSPARENT);
dc.drawRectangle(x, y, segW, segH);
}
x += segW + gap;
} }
// Garmin HR zone colors: 1 grey, 2 blue, 3 green, 4 orange, 5 red.
private function _zoneColor(z as Number) as Number {
if (z == 1) { return Graphics.COLOR_LT_GRAY; }
if (z == 2) { return Graphics.COLOR_BLUE; }
if (z == 3) { return Graphics.COLOR_GREEN; }
if (z == 4) { return Graphics.COLOR_ORANGE; }
return Graphics.COLOR_RED;
} }
private function _drawTime(dc as Graphics.Dc, cx as Number, y as Number) as Void { private function _drawTime(dc as Graphics.Dc, cx as Number, y as Number) as Void {
var now = System.getClockTime(); var now = System.getClockTime();
var text = now.hour.format("%02d") + ":" + now.min.format("%02d"); var text = now.hour.format("%02d") + ":" + now.min.format("%02d");
dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT); dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
dc.drawText(cx, y, Graphics.FONT_SMALL, text, Graphics.TEXT_JUSTIFY_CENTER); dc.drawText(cx, y, Graphics.FONT_MEDIUM, text, Graphics.TEXT_JUSTIFY_CENTER);
} }
private function _drawWinner(dc as Graphics.Dc, cx as Number, cy as Number, m as PadelMatch) as Void { private function _drawWinner(dc as Graphics.Dc, cx as Number, cy as Number, m as PadelMatch) as Void {