import Toybox.Lang; import Toybox.Graphics; import Toybox.Math; // Geometry and text plumbing for the face. // // Every literal here is a coordinate on the design's 500x500 canvas, measured // from design/watchface-*.svg -- ring numbers come from design/rebuild.py, the // element positions are the ink bounding boxes of the traced SVG paths. At // draw time everything is multiplied by s = screenWidth / 500, so one set of // numbers serves screens from 240 px (fenix 7S) to 454 px (venu 3). // // Call init() from the view's onLayout before anything else touches this. module Layout { // ---------------------------------------------------------------- ring // Ticks are annular sectors of constant angular width, not radial lines, // which is why they are drawn as four-point polygons rather than strokes. const R_IN = 231.09; // inner edge of the tick band const R_OUT = 249.10; // outer edge const TOP_N = 21; // ticks per top arc const TOP_PH = 6.500; // angle of tick 0, degrees clockwise from 12 const TOP_PITCH = 4.000; const TOP_HW = 1.554; // half of the 3.108 deg tick width const BOT_N = 21; // ticks per bottom arc const BOT_PH = 6.005; // measured clockwise from 6 o'clock const BOT_PITCH = 3.000; const BOT_HW = 1.043; const DOT_RAD = 240.10; // anchor dots sit on the band's mid-radius const DOT_R = 9.00; const DOT_ANG = [0.0, 93.15, 180.0, 266.85]; // ------------------------------------------------------------- elements const BATT_X = 192.70; // battery body, left edge const BATT_W = 26.20; // body only; the terminal sits to its right const BATT_H = 17.81; const BATT_WALL = 2.20; // outline thickness const BATT_TW = 2.53; // terminal const BATT_TH = 9.20; const BATT_VX = 232.00; // "40%" left edge const BATT_CY = 53.20; const LEFT_X = 157.00; // top data slots, centred const RIGHT_X = 343.70; const ICON_CY = 89.50; // matches the icon bitmaps' own centre const VALUE_CY = 131.06; const TIME_CY = 221.10; const HOURS_R = 211.40; // hour digits, right edge const MINUTES_L = 276.02; // minute digits, left edge const COLON_X = 237.88; const COLON_W = 24.24; const COLON_H = 21.30; const COLON_Y1 = 183.68; const COLON_Y2 = 234.57; const BAND_Y = 286.30; const BAND_H = 44.19; const BAND_CY = 308.10; const BT_CX = 36.88; // bluetooth rune const BT_CY = 306.99; const BT_HW = 8.89; const BT_HH = 12.92; const DOW_CX = 178.05; const MD_CX = 298.28; const AMPM_R = 465.89; const BOT_VAL_CY = 366.76; const BOT_LAB_CY = 410.88; const DIST_R = 234.30; // right edge of both left-hand bottom lines const STEP_L = 267.40; // left edge of both right-hand bottom lines // Cap heights of the design's three text sizes. const CAP_BAND = 25.34; // date band, battery percentage const CAP_COMP = 31.50; // data slots, bottom row const CAP_TIME = 98.70; // time digits // How much room a value may take before it is stepped down a size. The // top slots are centred, so this is the full width they may span; the // bottom pair are edge-aligned and share the middle of the dial. const TOP_MAX_W = 185.0; const BOT_MAX_W = 150.0; // Connect IQ exposes ascent and descent but not cap height, and the ascent // reserves room for diacritics that no digit or capital ever uses. These // two ratios were measured by rendering into the simulator and reading the // ink back off the screenshot (fenix847mm: FONT_XTINY ascent 29 -> caps // 22 px; FONT_NUMBER_MILD ascent 82 -> digits 58 px). const CAP_OF_TEXT = 0.76; const CAP_OF_NUM = 0.71; // --------------------------------------------------------------- state var scale = 1.0; // device px per design px var width = 0; var height = 0; var cx = 0; var cy = 0; var fontBand = Graphics.FONT_SMALL; var fontComp = Graphics.FONT_MEDIUM; var fontTime = Graphics.FONT_NUMBER_HOT; // Text sizes in ascending order, so a value that will not fit can walk // down the list until it does. var textFonts = [Graphics.FONT_XTINY, Graphics.FONT_TINY, Graphics.FONT_SMALL, Graphics.FONT_MEDIUM, Graphics.FONT_LARGE]; function init(dc as Graphics.Dc) as Void { width = dc.getWidth(); height = dc.getHeight(); scale = width / 500.0; cx = width / 2; cy = height / 2; var nums = [Graphics.FONT_NUMBER_MILD, Graphics.FONT_NUMBER_MEDIUM, Graphics.FONT_NUMBER_HOT, Graphics.FONT_NUMBER_THAI_HOT]; fontBand = pickFont(textFonts, CAP_BAND * scale, CAP_OF_TEXT); fontComp = pickFont(textFonts, CAP_COMP * scale, CAP_OF_TEXT); fontTime = pickFont(nums, CAP_TIME * scale, CAP_OF_NUM); } function ready() as Boolean { return width > 0; } // Design pixels to device pixels. function px(v as Numeric) as Number { return Math.round(v * scale).toNumber(); } function capHeight(font as Graphics.FontDefinition, ratio as Float) as Float { return Graphics.getFontAscent(font) * ratio; } // Closest cap height wins -- not "largest that still fits", which throws // away a whole size whenever the design falls between two fonts. function pickFont(cands as Array, targetCap as Float, ratio as Float) as Graphics.FontDefinition { var best = cands[0]; var bestErr = -1.0; for (var i = 0; i < cands.size(); i++) { var err = capHeight(cands[i], ratio) - targetCap; if (err < 0) { err = -err; } if (bestErr < 0 || err < bestErr) { bestErr = err; best = cands[i]; } } return best; } // A long value -- twelve characters of custom text, say -- would run off // the dial at the design's size. Step down until it fits the slot. function fitFont(dc as Graphics.Dc, str as String, font as Graphics.FontDefinition, maxDesignW as Float) as Graphics.FontDefinition { var limit = px(maxDesignW); var f = font; var i = textFonts.size() - 1; while (i >= 0) { if (textFonts[i] == f) { break; } i--; } if (i < 0) { return f; } // not one of the text fonts while (i >= 0) { if (dc.getTextWidthInPixels(str, textFonts[i]) <= limit) { return textFonts[i]; } i--; } return textFonts[0]; } // TEXT_JUSTIFY_VCENTER centres the font box, but capitals occupy only the // band from the baseline up by capHeight, so the ink sits high in that box. // Push the draw down by the difference to land the ink on cy. function inkY(dc as Graphics.Dc, font as Graphics.FontDefinition, cyDesign as Numeric, ratio as Float) as Number { var fh = dc.getFontHeight(font); var asc = Graphics.getFontAscent(font); return px(cyDesign) + ((fh / 2.0) - asc + capHeight(font, ratio) / 2.0).toNumber(); } function text(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric, font as Graphics.FontDefinition, str as String, just as Number, col as Number) as Void { dc.setColor(col, Graphics.COLOR_TRANSPARENT); dc.drawText(px(x), inkY(dc, font, cyDesign, CAP_OF_TEXT), font, str, just | Graphics.TEXT_JUSTIFY_VCENTER); } // Same, but shrinks the font if the string is too wide for its slot. function fittedText(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric, font as Graphics.FontDefinition, str as String, just as Number, col as Number, maxDesignW as Float) as Void { text(dc, x, cyDesign, fitFont(dc, str, font, maxDesignW), str, just, col); } // Monkey C cannot fill glyphs with a gradient, so the string is drawn once // per stop with dc.setClip holding each pass to its own horizontal band. // The first and last band are extended off-glyph so no sliver of the // digits is ever left undrawn. function gradText(dc as Graphics.Dc, x as Numeric, cyDesign as Numeric, font as Graphics.FontDefinition, str as String, just as Number, ramp as Array) as Void { var n = ramp.size(); var y = inkY(dc, font, cyDesign, CAP_OF_NUM); var ink = capHeight(font, CAP_OF_NUM).toNumber(); var top = px(cyDesign) - ink / 2; for (var i = 0; i < n; i++) { var y0 = top + (ink * i) / n; var y1 = top + (ink * (i + 1)) / n; if (i == 0) { y0 = 0; } if (i == n - 1) { y1 = height; } if (y1 <= y0) { continue; } dc.setClip(0, y0, width, y1 - y0); dc.setColor(ramp[i], Graphics.COLOR_TRANSPARENT); dc.drawText(px(x), y, font, str, just | Graphics.TEXT_JUSTIFY_VCENTER); } dc.clearClip(); } // Design angles run clockwise from 12 o'clock; screen y grows downward. function polarX(ang as Float, r as Float) as Number { return Math.round(cx + Math.sin(Math.toRadians(ang)) * r).toNumber(); } function polarY(ang as Float, r as Float) as Number { return Math.round(cy - Math.cos(Math.toRadians(ang)) * r).toNumber(); } function line(dc as Graphics.Dc, x1 as Numeric, y1 as Numeric, x2 as Numeric, y2 as Numeric) as Void { dc.drawLine(px(x1), px(y1), px(x2), px(y2)); } // Pen widths round to zero on small screens; never let one vanish. function pen(v as Numeric) as Number { var w = px(v); return w < 2 ? 2 : w; } }