import Toybox.Graphics; import Toybox.Lang; import Toybox.Math; import Toybox.ActivityMonitor; import Toybox.SensorHistory; import Toybox.System; import Toybox.Time; import Toybox.Time.Gregorian; import Toybox.WatchUi; import Themes; import Fields; import Layout; import Icons; import Settings; // The face itself: the progress ring, the elements laid over it, and the // stripped-down variant drawn while an AMOLED watch is asleep. // // Anything about *where* things go lives in Layout, anything about *what* a // data slot says lives in Fields, and the colours come from Themes. What is // left here is the drawing order. class Fenix8V3View extends WatchUi.WatchFace { // Always-on display state. AMOLED panels have to cut their lit pixel // count while asleep; MIP panels do not care and are left alone. private var mSleeping = false; private var mBurnIn = false; function initialize() { WatchFace.initialize(); } function onEnterSleep() as Void { mSleeping = true; WatchUi.requestUpdate(); } function onExitSleep() as Void { mSleeping = false; WatchUi.requestUpdate(); } // Called once when the face is loaded, and again by onUpdate if anything // it caches has gone stale (a resolution change, or resources not yet in). function onLayout(dc as Dc) as Void { Layout.init(dc); Fields.loadStrings(); Icons.load(); var ds = System.getDeviceSettings(); mBurnIn = (ds != null) && (ds has :requiresBurnInProtection) && ds.requiresBurnInProtection; } // ------------------------------------------------------------- ring // Progress for one of the four arcs, 0..1. These ids are the ring's own // short list, not the data field catalogue. function ringProgress(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, 10 km ring 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 week goal if ((info has :activeMinutesWeek) && info.activeMinutesWeek != null && (info has :activeMinutesWeekGoal) && 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: a 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; } // One tick: an annular sector drawn as a quad. At three degrees the chord // sagitta is under a tenth of a pixel, so straight edges are exact enough. function tick(dc as Dc, ang as Float, hw as Float) as Void { var a0 = ang - hw; var a1 = ang + hw; var ro = Layout.R_OUT * Layout.scale; var ri = Layout.R_IN * Layout.scale; dc.fillPolygon([[Layout.polarX(a0, ro), Layout.polarY(a0, ro)], [Layout.polarX(a1, ro), Layout.polarY(a1, ro)], [Layout.polarX(a1, ri), Layout.polarY(a1, ri)], [Layout.polarX(a0, ri), Layout.polarY(a0, ri)]]); } // 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 * Layout.TOP_N).toNumber(), Layout.TOP_PH, Layout.TOP_PITCH, Layout.TOP_HW, Layout.TOP_N, 0.0, false, true); arc(dc, top, off, (pTL * Layout.TOP_N).toNumber(), Layout.TOP_PH, Layout.TOP_PITCH, Layout.TOP_HW, Layout.TOP_N, 0.0, true, true); arc(dc, bot, off, (pBL * Layout.BOT_N).toNumber(), Layout.BOT_PH, Layout.BOT_PITCH, Layout.BOT_HW, Layout.BOT_N, 180.0, false, false); arc(dc, bot, off, (pBR * Layout.BOT_N).toNumber(), Layout.BOT_PH, Layout.BOT_PITCH, Layout.BOT_HW, Layout.BOT_N, 180.0, true, false); dc.setColor(Themes.ANCHORDOT[ti] as Number, Graphics.COLOR_TRANSPARENT); var r = Layout.px(Layout.DOT_R); var rad = Layout.DOT_RAD * Layout.scale; for (var i = 0; i < Layout.DOT_ANG.size(); i++) { var a = Layout.DOT_ANG[i]; dc.fillCircle(Layout.polarX(a, rad), Layout.polarY(a, rad), r); } } // ---------------------------------------------------------- elements function drawBattery(dc as Dc, ti as Number, level as Float) as Void { Icons.battery(dc, Layout.BATT_X, Layout.BATT_CY, 1.0, Themes.ACCENT[ti] as Number, level); Layout.text(dc, Layout.BATT_VX, Layout.BATT_CY, Layout.fontBand, (level * 100.0).toNumber().format("%d") + "%", Graphics.TEXT_JUSTIFY_LEFT, Themes.TEXTPRIMARY[ti] as Number); } function drawTopSlot(dc as Dc, ti as Number, id as Number, isLeft as Boolean, ctx as Fields.Ctx, level as Float) as Void { var x = isLeft ? Layout.LEFT_X : Layout.RIGHT_X; Icons.forField(dc, id, x, Themes.ACCENT[ti] as Number, level); Layout.fittedText(dc, x, Layout.VALUE_CY, Layout.fontComp, Fields.value(id, ctx), Graphics.TEXT_JUSTIFY_CENTER, Themes.TEXTPRIMARY[ti] as Number, Layout.TOP_MAX_W); } 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, Layout.px(Layout.BAND_Y), Layout.width, Layout.px(Layout.BAND_H)); var col = Themes.BANDTEXT[ti] as Number; // Dim the rune rather than hide it, so the band does not change shape // every time the phone wanders out of range. Icons.bluetooth(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); Layout.text(dc, Layout.DOW_CX, Layout.BAND_CY, Layout.fontBand, Fields.dow(g), Graphics.TEXT_JUSTIFY_CENTER, col); Layout.text(dc, Layout.MD_CX, Layout.BAND_CY, Layout.fontBand, Fields.monthDay(g), Graphics.TEXT_JUSTIFY_CENTER, col); if (ds == null || !ds.is24Hour) { Layout.text(dc, Layout.AMPM_R, Layout.BAND_CY, Layout.fontBand, Fields.meridiem(g), Graphics.TEXT_JUSTIFY_RIGHT, col); } } 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; } } Layout.gradText(dc, Layout.HOURS_R, Layout.TIME_CY, Layout.fontTime, h.format("%d"), Graphics.TEXT_JUSTIFY_RIGHT, Themes.HOURS[ti] as Array); Layout.gradText(dc, Layout.MINUTES_L, Layout.TIME_CY, Layout.fontTime, 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(Layout.px(Layout.COLON_X), Layout.px(Layout.COLON_Y1), Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H)); dc.fillRectangle(Layout.px(Layout.COLON_X), Layout.px(Layout.COLON_Y2), Layout.px(Layout.COLON_W), Layout.px(Layout.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 = Settings.bottomLeft(); var right = Settings.bottomRight(); Layout.fittedText(dc, Layout.DIST_R, Layout.BOT_VAL_CY, Layout.fontComp, Fields.value(left, ctx), Graphics.TEXT_JUSTIFY_RIGHT, value, Layout.BOT_MAX_W); Layout.fittedText(dc, Layout.DIST_R, Layout.BOT_LAB_CY, Layout.fontComp, Fields.label(left), Graphics.TEXT_JUSTIFY_RIGHT, label, Layout.BOT_MAX_W); Layout.fittedText(dc, Layout.STEP_L, Layout.BOT_VAL_CY, Layout.fontComp, Fields.value(right, ctx), Graphics.TEXT_JUSTIFY_LEFT, value, Layout.BOT_MAX_W); Layout.fittedText(dc, Layout.STEP_L, Layout.BOT_LAB_CY, Layout.fontComp, Fields.label(right), Graphics.TEXT_JUSTIFY_LEFT, label, Layout.BOT_MAX_W); } // ------------------------------------------------------------ colour 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 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; } // --------------------------------------------------------------- AOD // Low-power face: no band fill, no unlit ticks, no data slots -- 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 / Layout.scale; // +/- 4 device px var oy = ((g.min / 5) % 5 - 2) * 2.0 / Layout.scale; var top = Themes.RINGTOP[ti] as Array; var bot = Themes.RINGBOTTOM[ti] as Array; var litTR = (pTR * Layout.TOP_N).toNumber(); var litTL = (pTL * Layout.TOP_N).toNumber(); var litBR = (pBR * Layout.BOT_N).toNumber(); var litBL = (pBL * Layout.BOT_N).toNumber(); for (var i = 0; i < Layout.TOP_N; i++) { var a = Layout.TOP_PH + Layout.TOP_PITCH * i; if (i >= Layout.TOP_N - litTR) { dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, a, Layout.TOP_HW); } if (i >= Layout.TOP_N - litTL) { dc.setColor(dim(top[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, -a, Layout.TOP_HW); } } for (var i = 0; i < Layout.BOT_N; i++) { var a = Layout.BOT_PH + Layout.BOT_PITCH * i; if (i < litBL) { dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, 180.0 + a, Layout.BOT_HW); } if (i < litBR) { dc.setColor(dim(bot[i], 0.45), Graphics.COLOR_TRANSPARENT); tick(dc, 180.0 - a, Layout.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; Layout.text(dc, Layout.HOURS_R + ox, Layout.TIME_CY + oy, Layout.fontTime, h.format("%d"), Graphics.TEXT_JUSTIFY_RIGHT, dim(hours[hours.size() / 2], 0.55)); Layout.text(dc, Layout.MINUTES_L + ox, Layout.TIME_CY + oy, Layout.fontTime, 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(Layout.px(Layout.COLON_X + ox), Layout.px(Layout.COLON_Y1 + oy), Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H)); dc.fillRectangle(Layout.px(Layout.COLON_X + ox), Layout.px(Layout.COLON_Y2 + oy), Layout.px(Layout.COLON_W), Layout.px(Layout.COLON_H)); var band = dim(Themes.ACCENT[ti] as Number, 0.55); Layout.text(dc, Layout.DOW_CX + ox, Layout.BAND_CY + oy, Layout.fontBand, Fields.dow(g), Graphics.TEXT_JUSTIFY_CENTER, band); Layout.text(dc, Layout.MD_CX + ox, Layout.BAND_CY + oy, Layout.fontBand, Fields.monthDay(g), Graphics.TEXT_JUSTIFY_CENTER, band); } // -------------------------------------------------------------- main function onUpdate(dc as Dc) as Void { if (!Layout.ready() || Layout.width != dc.getWidth() || !Fields.ready() || !Icons.ready()) { onLayout(dc); } if (dc has :setAntiAlias) { dc.setAntiAlias(true); } var ti = Settings.themeIndex(Themes.NAMES.size()); var ds = System.getDeviceSettings(); var ctx = new Fields.Ctx(ds, Settings.tempUnit()); var info = ctx.info(); var stats = ctx.stats(); 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; } dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK); dc.clear(); var pTR = ringProgress(Settings.ringTR(), info); var pTL = ringProgress(Settings.ringTL(), info); var pBR = ringProgress(Settings.ringBR(), info); var pBL = ringProgress(Settings.ringBL(), info); if (mSleeping && mBurnIn) { drawAmbient(dc, ti, ds, pTR, pTL, pBR, pBL); return; } drawRing(dc, ti, pTR, pTL, pBR, pBL); if (Settings.showTop()) { drawBattery(dc, ti, level); drawTopSlot(dc, ti, Settings.leftTop(), true, ctx, level); drawTopSlot(dc, ti, Settings.rightTop(), false, ctx, level); } drawTime(dc, ti, ds); if (Settings.showBand()) { drawDateBand(dc, ti, ds); } if (Settings.showBottom()) { drawBottom(dc, ti, ctx); } } }