import Toybox.Application; import Toybox.Graphics; import Toybox.Lang; import Toybox.Math; import Toybox.ActivityMonitor; import Toybox.Activity; import Toybox.Position; import Toybox.Weather; import Toybox.System; import Toybox.Time; import Toybox.Time.Gregorian; import Toybox.WatchUi; import Themes; import Fields; import Toybox.SensorHistory; // Every literal below is a coordinate on the 500x500 design canvas, measured // from design/watchface-*.svg. Nothing here is eyeballed: ring geometry comes // from design/rebuild.py, element positions are the ink bounding boxes of the // traced SVG paths. At draw time everything is multiplied by s = width / 500. class Fenix8V3View extends WatchUi.WatchFace { // ---------------------------------------------------------------- ring // Ticks are annular sectors of constant angular width, not radial lines. const R_IN = 231.09; const R_OUT = 249.10; const TOP_N = 21; 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; 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; // distance of the anchor dots from centre const DOT_R = 9.00; const DOT_ANG = [0.0, 93.15, 180.0, 266.85]; // ------------------------------------------------------------- layout const BATT_X = 192.70; // battery body, left edge const BATT_Y = 44.02; const BATT_W = 26.20; // body only, terminal sits to its right const BATT_H = 17.81; const BATT_WALL = 2.20; 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 complications, centred const RIGHT_X = 343.70; const ICON_CY = 89.50; 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; const BT_CY = 306.99; const BT_HW = 8.89; // half width of the bluetooth rune 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 DST lines const STEP_L = 267.40; // left edge of both STEP lines // Cap heights of the three text sizes in the design. const CAP_BAND = 25.34; // date band, battery percentage const CAP_COMP = 31.50; // complications, bottom row const CAP_TIME = 98.70; // time digits // ------------------------------------------------------------- cached private var mS = 1.0f; private var mW = 0; private var mH = 0; private var mCx = 0; private var mCy = 0; private var mFontBand = Graphics.FONT_SMALL; private var mFontComp = Graphics.FONT_MEDIUM; private var mFontTime = Graphics.FONT_NUMBER_HOT; // Always-on display: AMOLED devices must cut lit pixels hard while // asleep, and shift what is left so nothing burns in. private var mSleeping = false; private var mBurnIn = false; private var mIconSunrise = null; private var mIconSunset = null; private var mIconWeather = null; function initialize() { WatchFace.initialize(); } function onEnterSleep() as Void { mSleeping = true; WatchUi.requestUpdate(); } function onExitSleep() as Void { mSleeping = false; WatchUi.requestUpdate(); } function onLayout(dc as Dc) as Void { mW = dc.getWidth(); mH = dc.getHeight(); mS = mW / 500.0; mCx = mW / 2; mCy = mH / 2; var ds = System.getDeviceSettings(); mBurnIn = (ds != null) && (ds has :requiresBurnInProtection) && ds.requiresBurnInProtection; var text = [Graphics.FONT_XTINY, Graphics.FONT_TINY, Graphics.FONT_SMALL, Graphics.FONT_MEDIUM, Graphics.FONT_LARGE]; var nums = [Graphics.FONT_NUMBER_MILD, Graphics.FONT_NUMBER_MEDIUM, Graphics.FONT_NUMBER_HOT, Graphics.FONT_NUMBER_THAI_HOT]; mFontBand = pickFont(text, CAP_BAND * mS, CAP_OF_TEXT); mFontComp = pickFont(text, CAP_COMP * mS, CAP_OF_TEXT); mFontTime = pickFont(nums, CAP_TIME * mS, CAP_OF_NUM); loadStrings(); } function loadStrings() as Void { Fields.loadStrings(); mIconSunrise = WatchUi.loadResource(Rez.Drawables.IconSunrise); mIconSunset = WatchUi.loadResource(Rez.Drawables.IconSunset); mIconWeather = WatchUi.loadResource(Rez.Drawables.IconWeather); } // Connect IQ exposes ascent and descent but not cap height, and the ascent // reserves room for diacritics no digit or capital ever uses. These two // ratios were measured by rendering in the simulator and reading the ink // back off the screenshot (fenix847mm: xtiny ascent 29 -> caps 22 px, // numberMild ascent 82 -> digits 58 px). static const CAP_OF_TEXT = 0.76; static const CAP_OF_NUM = 0.71; function capHeight(font as Graphics.FontDefinition, ratio as Float) as Float { return Graphics.getFontAscent(font) * ratio; } // Closest cap height wins -- not the largest that fits, which would leave a // whole size on the table 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; } function px(v as Numeric) as Number { return Math.round(v * mS).toNumber(); } // ----------------------------------------------------------------- utils function getSetting(name as String, def as Number) as Number { var v = Application.Properties.getValue(name); if (v == null) { return def; } return v as Number; } function getBool(name as String, def as Boolean) as Boolean { var v = Application.Properties.getValue(name); if (v == null) { return def; } return v as Boolean; } function themeIndex() as Number { var i = getSetting("Theme", 1) - 1; if (i < 0 || i >= Themes.NAMES.size()) { i = 0; } return i; } // TEXT_JUSTIFY_VCENTER centres the font box. Caps 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(font as Graphics.FontDefinition, dc as Dc, cy as Numeric, ratio as Float) as Number { var fh = dc.getFontHeight(font); var asc = Graphics.getFontAscent(font); return px(cy) + ((fh / 2.0) - asc + capHeight(font, ratio) / 2.0).toNumber(); } function text(dc as Dc, x as Numeric, cy 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(font, dc, cy, CAP_OF_TEXT), font, str, just | Graphics.TEXT_JUSTIFY_VCENTER); } // Draw str once per gradient stop, each pass clipped to its own horizontal // band -- Monkey C cannot fill glyphs with a gradient directly. The first // and last band are extended off-glyph so nothing is ever left undrawn. function gradText(dc as Dc, x as Numeric, cy as Numeric, font as Graphics.FontDefinition, str as String, just as Number, ramp as Array) as Void { var n = ramp.size(); var y = inkY(font, dc, cy, CAP_OF_NUM); var ink = capHeight(font, CAP_OF_NUM).toNumber(); var top = px(cy) - 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 = mH; } if (y1 <= y0) { continue; } dc.setClip(0, y0, mW, 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. function polarX(ang as Float, r as Float) as Number { return Math.round(mCx + Math.sin(Math.toRadians(ang)) * r).toNumber(); } function polarY(ang as Float, r as Float) as Number { return Math.round(mCy - Math.cos(Math.toRadians(ang)) * r).toNumber(); } // progress 0..1 for a metric index function metricProgress(idx as Number, info as ActivityMonitor.Info) as Float { if (info == null) { return 0.0f; } var p = 0.0f; if (idx == 0) { // Steps if (info.stepGoal != null && info.stepGoal > 0 && info.steps != null) { p = info.steps.toFloat() / info.stepGoal.toFloat(); } } else if (idx == 1) { // Calories if (info.calories != null) { p = info.calories.toFloat() / 2000.0f; } } else if (idx == 2) { // Distance (cm -> 10 km cap) if (info.distance != null) { p = info.distance.toFloat() / 1000000.0f; } } else if (idx == 3) { // Floors if (info.floorsClimbed != null) { var goal = (info.floorsClimbedGoal != null && info.floorsClimbedGoal > 0) ? info.floorsClimbedGoal.toFloat() : 10.0f; p = info.floorsClimbed.toFloat() / goal; } } else if (idx == 5) { // Active minutes vs the weekly goal if (info.activeMinutesWeek != null && info.activeMinutesWeekGoal != null && info.activeMinutesWeekGoal > 0) { p = info.activeMinutesWeek.total.toFloat() / info.activeMinutesWeekGoal.toFloat(); } } else if (idx == 6) { // Body battery is already 0..100 var bb = Fields.latest(SensorHistory.getBodyBatteryHistory({:period => 1})); if (bb != null) { p = bb.toFloat() / 100.0; } } else if (idx == 4) { // Move bar: an integer level, not a ratio if (info.moveBarLevel != null) { var span = (ActivityMonitor.MOVE_BAR_LEVEL_MAX - ActivityMonitor.MOVE_BAR_LEVEL_MIN).toFloat(); if (span > 0) { p = (info.moveBarLevel - ActivityMonitor.MOVE_BAR_LEVEL_MIN) / span; } } } if (p > 1.0f) { p = 1.0f; } if (p < 0.0f) { p = 0.0f; } return p; } // ----------------------------------------------------------------- ring function tick(dc as Dc, ang as Float, hw as Float) as Void { var a0 = ang - hw; var a1 = ang + hw; dc.fillPolygon([[polarX(a0, R_OUT * mS), polarY(a0, R_OUT * mS)], [polarX(a1, R_OUT * mS), polarY(a1, R_OUT * mS)], [polarX(a1, R_IN * mS), polarY(a1, R_IN * mS)], [polarX(a0, R_IN * mS), polarY(a0, R_IN * mS)]]); } // Both top arcs fill from the 9/3 o'clock end towards 12; both bottom arcs // fill outwards from 6 o'clock. That is what the comps show. function arc(dc as Dc, ramp as Array, off as Number, lit as Number, phase as Float, pitch as Float, hw as Float, n as Number, base as Float, mirror as Boolean, fromEnd as Boolean) as Void { for (var i = 0; i < n; i++) { var a = phase + pitch * i; var on = fromEnd ? (i >= n - lit) : (i < lit); dc.setColor(on ? ramp[i] : off, Graphics.COLOR_TRANSPARENT); tick(dc, base + (mirror ? -a : a), hw); } } function drawRing(dc as Dc, ti as Number, pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void { var top = Themes.RINGTOP[ti] as Array; var bot = Themes.RINGBOTTOM[ti] as Array; var off = Themes.TICKOFF[ti] as Number; arc(dc, top, off, (pTR * TOP_N).toNumber(), TOP_PH, TOP_PITCH, TOP_HW, TOP_N, 0.0, false, true); arc(dc, top, off, (pTL * TOP_N).toNumber(), TOP_PH, TOP_PITCH, TOP_HW, TOP_N, 0.0, true, true); arc(dc, bot, off, (pBL * BOT_N).toNumber(), BOT_PH, BOT_PITCH, BOT_HW, BOT_N, 180.0, false, false); arc(dc, bot, off, (pBR * BOT_N).toNumber(), BOT_PH, BOT_PITCH, BOT_HW, BOT_N, 180.0, true, false); dc.setColor(Themes.ANCHORDOT[ti] as Number, Graphics.COLOR_TRANSPARENT); var r = px(DOT_R); for (var i = 0; i < DOT_ANG.size(); i++) { var a = DOT_ANG[i]; dc.fillCircle(polarX(a, DOT_RAD * mS), polarY(a, DOT_RAD * mS), r); } } // ------------------------------------------------------------ icons // The design ships three icons; they are rebuilt here from primitives at // the sub-shape positions measured out of the SVG paths. function iconBattery(dc as Dc, x as Numeric, cy as Numeric, scale as Float, col as Number, level as Float) as Void { var w = BATT_W * scale; var h = BATT_H * scale; var wall = BATT_WALL * scale; var x0 = px(x); var y0 = px(cy - h / 2); var pw = px(w); var ph = px(h); var r = px(3.4 * scale); dc.setColor(col, Graphics.COLOR_TRANSPARENT); dc.fillRoundedRectangle(x0, y0, pw, ph, r); dc.fillRectangle(x0 + pw, px(cy - BATT_TH * scale / 2), px(BATT_TW * scale), px(BATT_TH * scale)); var ix = x0 + px(wall); var iy = y0 + px(wall); var iw = pw - 2 * px(wall); var ih = ph - 2 * px(wall); dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT); dc.fillRectangle(ix, iy, iw, ih); var fill = (iw * level).toNumber(); if (fill > 0) { dc.setColor(col, Graphics.COLOR_TRANSPARENT); dc.fillRectangle(ix, iy, fill, ih); } } function stub(dc as 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)); } // The comps' sunrise and weather icons are traced outlines with two- and // three-pixel features. Rebuilding them from circles and lines held up at // 454 px and turned to mush at 280, so they ship as bitmaps rasterised // from the design paths at each screen size (tools/gen_icons.py) and get // the theme's accent applied through :tintColor. function blitIcon(dc as Dc, bmp, cx as Numeric, cy as Numeric, col as Number) as Void { if (bmp == null) { return; } dc.drawBitmap2(px(cx) - bmp.getWidth() / 2, px(cy) - bmp.getHeight() / 2, bmp, {:tintColor => col}); } function maxNum(a as Number, b as Number) as Number { return a > b ? a : b; } function drawComplicationIcon(dc as Dc, cx as Numeric, cy as Numeric, scale as Float, col as Number, typeIdx as Number, level as Float) as Void { if (typeIdx == 712 || typeIdx == 700) { blitIcon(dc, mIconSunrise, cx, ICON_CY, col); } else if (typeIdx == 717 || typeIdx == 731) { blitIcon(dc, mIconSunset, cx, ICON_CY, col); } else if (typeIdx == 306 || typeIdx == 639 || typeIdx == 300 || typeIdx == 301 || typeIdx == 302 || typeIdx == 304 || typeIdx == 305 || typeIdx == 308 || typeIdx == 309 || typeIdx == 310 || typeIdx == 307) { blitIcon(dc, mIconWeather, cx, ICON_CY, col); } else if (typeIdx == 250 || typeIdx == 251 || typeIdx == 257) { iconBattery(dc, cx - 14.4 * scale, cy, scale, col, level); } else { var r = 13.0 * scale; dc.setColor(col, Graphics.COLOR_TRANSPARENT); dc.setPenWidth(maxNum(2, px(3.0 * scale))); if (typeIdx == 2 || typeIdx == 603) { // calories: flame dc.fillPolygon([[px(cx), px(cy - r)], [px(cx + r * 0.72), px(cy + r * 0.25)], [px(cx + r * 0.40), px(cy + r * 0.85)], [px(cx - r * 0.40), px(cy + r * 0.85)], [px(cx - r * 0.72), px(cy + r * 0.25)]]); } else if (typeIdx == 1 || typeIdx == 14 || typeIdx == 707) { // steps dc.fillRoundedRectangle(px(cx - r * 0.85), px(cy - r * 0.75), px(r * 0.62), px(r * 1.15), px(r * 0.3)); dc.fillRoundedRectangle(px(cx + r * 0.22), px(cy - r * 0.35), px(r * 0.62), px(r * 1.15), px(r * 0.3)); } else if (typeIdx == 3 || typeIdx == 9 || typeIdx == 615) { // distance dc.fillPolygon([[px(cx), px(cy - r)], [px(cx + r * 0.75), px(cy + r * 0.8)], [px(cx), px(cy + r * 0.35)], [px(cx - r * 0.75), px(cy + r * 0.8)]]); } else if (typeIdx == 618 || typeIdx == 276 || typeIdx == 623) { // heart dc.fillCircle(px(cx - r * 0.38), px(cy - r * 0.28), px(r * 0.44)); dc.fillCircle(px(cx + r * 0.38), px(cy - r * 0.28), px(r * 0.44)); dc.fillPolygon([[px(cx - r * 0.78), px(cy - r * 0.12)], [px(cx + r * 0.78), px(cy - r * 0.12)], [px(cx), px(cy + r * 0.85)]]); } else if (typeIdx == 4 || typeIdx == 5 || typeIdx == 6) { // stairs for (var i = 0; i < 3; i++) { dc.fillRectangle(px(cx - r * 0.85 + r * 0.58 * i), px(cy + r * 0.6 - r * 0.55 * i), px(r * 0.62), px(r * 0.4)); } } // Everything else -- dates, week numbers, custom text -- reads fine // without a glyph, so no icon is drawn. } } // ------------------------------------------------------------ elements function drawBattery(dc as Dc, ti as Number, stats as System.Stats) as Void { var level = 0.0f; if (stats != null && stats.battery != null) { level = stats.battery / 100.0; } if (level > 1.0f) { level = 1.0f; } if (level < 0.0f) { level = 0.0f; } iconBattery(dc, BATT_X, BATT_CY, 1.0, Themes.ACCENT[ti] as Number, level); text(dc, BATT_VX, BATT_CY, mFontBand, (level * 100.0).toNumber().format("%d") + "%", Graphics.TEXT_JUSTIFY_LEFT, Themes.TEXTPRIMARY[ti] as Number); } function drawTopComplication(dc as Dc, ti as Number, typeIdx as Number, isLeft as Boolean, ctx as Fields.Ctx, level as Float) as Void { var x = isLeft ? LEFT_X : RIGHT_X; drawComplicationIcon(dc, x, ICON_CY, 1.0, Themes.ACCENT[ti] as Number, typeIdx, level); text(dc, x, VALUE_CY, mFontComp, Fields.value(typeIdx, ctx), Graphics.TEXT_JUSTIFY_CENTER, Themes.TEXTPRIMARY[ti] as Number); } function drawBluetooth(dc as Dc, col as Number) as Void { dc.setColor(col, Graphics.COLOR_TRANSPARENT); dc.setPenWidth(maxNum(2, px(3.2))); var l = BT_CX - BT_HW; var r = BT_CX + BT_HW; var t = BT_CY - BT_HH; var b = BT_CY + BT_HH; var qt = BT_CY - BT_HH / 2; var qb = BT_CY + BT_HH / 2; stub(dc, l, qt, r, qb); stub(dc, r, qb, BT_CX, b); stub(dc, BT_CX, b, BT_CX, t); stub(dc, BT_CX, t, r, qt); stub(dc, r, qt, l, qb); } function drawDateBand(dc as Dc, ti as Number, ds as System.DeviceSettings) as Void { var fill = Themes.BANDFILL[ti] as Number; dc.setColor(fill, fill); dc.fillRectangle(0, px(BAND_Y), mW, px(BAND_H)); var col = Themes.BANDTEXT[ti] as Number; drawBluetooth(dc, (ds != null && !ds.phoneConnected) ? blend(col, fill, 0.6) : col); // FORMAT_SHORT, not FORMAT_MEDIUM: only the short form returns // day_of_week and month as numbers rather than localised strings. var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT); text(dc, DOW_CX, BAND_CY, mFontBand, Fields.dow(g), Graphics.TEXT_JUSTIFY_CENTER, col); text(dc, MD_CX, BAND_CY, mFontBand, Fields.monthDay(g), Graphics.TEXT_JUSTIFY_CENTER, col); if (ds == null || !ds.is24Hour) { text(dc, AMPM_R, BAND_CY, mFontBand, Fields.meridiem(g), Graphics.TEXT_JUSTIFY_RIGHT, col); } } function blend(c1 as Number, c2 as Number, f as Float) as Number { var r1 = (c1 >> 16) & 0xFF; var g1 = (c1 >> 8) & 0xFF; var b1 = c1 & 0xFF; var r = r1 + (((((c2 >> 16) & 0xFF) - r1) * f)).toNumber(); var g = g1 + (((((c2 >> 8) & 0xFF) - g1) * f)).toNumber(); var b = b1 + ((((c2 & 0xFF) - b1) * f)).toNumber(); return (r << 16) | (g << 8) | b; } function drawTime(dc as Dc, ti as Number, ds as System.DeviceSettings) as Void { var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT); var h = g.hour; if (ds == null || !ds.is24Hour) { if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; } } gradText(dc, HOURS_R, TIME_CY, mFontTime, h.format("%d"), Graphics.TEXT_JUSTIFY_RIGHT, Themes.HOURS[ti] as Array); gradText(dc, MINUTES_L, TIME_CY, mFontTime, g.min.format("%02d"), Graphics.TEXT_JUSTIFY_LEFT, Themes.MINUTES[ti] as Array); dc.setColor(Themes.COLON[ti] as Number, Graphics.COLOR_TRANSPARENT); dc.fillRectangle(px(COLON_X), px(COLON_Y1), px(COLON_W), px(COLON_H)); dc.fillRectangle(px(COLON_X), px(COLON_Y2), px(COLON_W), px(COLON_H)); } function drawBottom(dc as Dc, ti as Number, ctx as Fields.Ctx) as Void { var value = Themes.TEXTPRIMARY[ti] as Number; var label = Themes.ACCENT[ti] as Number; var left = getSetting("BottomLeft", 3); var right = getSetting("BottomRight", 1); text(dc, DIST_R, BOT_VAL_CY, mFontComp, Fields.value(left, ctx), Graphics.TEXT_JUSTIFY_RIGHT, value); text(dc, DIST_R, BOT_LAB_CY, mFontComp, Fields.label(left), Graphics.TEXT_JUSTIFY_RIGHT, label); text(dc, STEP_L, BOT_VAL_CY, mFontComp, Fields.value(right, ctx), Graphics.TEXT_JUSTIFY_LEFT, value); text(dc, STEP_L, BOT_LAB_CY, mFontComp, Fields.label(right), Graphics.TEXT_JUSTIFY_LEFT, label); } // ------------------------------------------------------------------ AOD function dim(col as Number, f as Float) as Number { var r = (((col >> 16) & 0xFF) * f).toNumber(); var g = (((col >> 8) & 0xFF) * f).toNumber(); var b = ((col & 0xFF) * f).toNumber(); return (r << 16) | (g << 8) | b; } // Low-power face: no band fill, no unlit ticks, no complications -- just a // dimmed time, the lit part of the ring, and the date. The whole thing // walks a few pixels each minute so no pixel stays lit in one place. function drawAmbient(dc as Dc, ti as Number, ds as System.DeviceSettings, pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void { var g = Gregorian.info(Time.now(), Time.FORMAT_SHORT); var ox = ((g.min % 5) - 2) * 2.0 / mS; // +/- 4 device px, in design units var oy = ((g.min / 5) % 5 - 2) * 2.0 / mS; var top = Themes.RINGTOP[ti] as Array; var bot = Themes.RINGBOTTOM[ti] as Array; var litTR = (pTR * TOP_N).toNumber(); var litTL = (pTL * TOP_N).toNumber(); var litBR = (pBR * BOT_N).toNumber(); var litBL = (pBL * BOT_N).toNumber(); for (var i = 0; i < TOP_N; i++) { var a = TOP_PH + TOP_PITCH * i; if (i >= TOP_N - litTR) { dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, a, TOP_HW); } if (i >= TOP_N - litTL) { dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, -a, TOP_HW); } } for (var i = 0; i < BOT_N; i++) { var a = BOT_PH + BOT_PITCH * i; if (i < litBL) { dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, 180.0 + a, BOT_HW); } if (i < litBR) { dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, 180.0 - a, BOT_HW); } } var h = g.hour; if (ds == null || !ds.is24Hour) { if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; } } var hours = Themes.HOURS[ti] as Array; var mins = Themes.MINUTES[ti] as Array; text(dc, HOURS_R + ox, TIME_CY + oy, mFontTime, h.format("%d"), Graphics.TEXT_JUSTIFY_RIGHT, dim(hours[hours.size() / 2], 0.55)); text(dc, MINUTES_L + ox, TIME_CY + oy, mFontTime, g.min.format("%02d"), Graphics.TEXT_JUSTIFY_LEFT, dim(mins[mins.size() / 2], 0.55)); dc.setColor(dim(Themes.COLON[ti] as Number, 0.55), Graphics.COLOR_TRANSPARENT); dc.fillRectangle(px(COLON_X + ox), px(COLON_Y1 + oy), px(COLON_W), px(COLON_H)); dc.fillRectangle(px(COLON_X + ox), px(COLON_Y2 + oy), px(COLON_W), px(COLON_H)); var band = dim(Themes.ACCENT[ti] as Number, 0.55); text(dc, DOW_CX + ox, BAND_CY + oy, mFontBand, Fields.dow(g), Graphics.TEXT_JUSTIFY_CENTER, band); text(dc, MD_CX + ox, BAND_CY + oy, mFontBand, Fields.monthDay(g), Graphics.TEXT_JUSTIFY_CENTER, band); } // ----------------------------------------------------------------- main function onUpdate(dc as Dc) as Void { if (mW != dc.getWidth() || !Fields.ready()) { onLayout(dc); } if (dc has :setAntiAlias) { dc.setAntiAlias(true); } var ti = themeIndex(); var ds = System.getDeviceSettings(); var ctx = new Fields.Ctx(ds, getSetting("TempUnit", 0)); var info = ctx.info(); var stats = ctx.stats(); dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK); dc.clear(); var pTR = metricProgress(getSetting("RingTR", 0), info); var pTL = metricProgress(getSetting("RingTL", 1), info); var pBR = metricProgress(getSetting("RingBR", 4), info); var pBL = metricProgress(getSetting("RingBL", 3), info); if (mSleeping && mBurnIn) { drawAmbient(dc, ti, ds, pTR, pTL, pBR, pBL); return; } drawRing(dc, ti, pTR, pTL, pBR, pBL); if (getBool("ShowTop", true)) { var level = 0.0f; if (stats != null && stats.battery != null) { level = stats.battery / 100.0; } drawBattery(dc, ti, stats); drawTopComplication(dc, ti, getSetting("LeftTop", 712), true, ctx, level); drawTopComplication(dc, ti, getSetting("RightTop", 639), false, ctx, level); } drawTime(dc, ti, ds); if (getBool("ShowBand", true)) { drawDateBand(dc, ti, ds); } if (getBool("ShowBottom", true)) { drawBottom(dc, ti, ctx); } } }