import Toybox.Application; import Toybox.Graphics; import Toybox.Lang; import Toybox.Math; import Toybox.ActivityMonitor; import Toybox.Weather; import Toybox.System; import Toybox.Time; import Toybox.Time.Gregorian; import Toybox.WatchUi; import Themes; class Fenix8V3View extends WatchUi.WatchFace { // ----------------------------------------------------------------- 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 currentTheme() as Dictionary { var idx = getSetting("Theme", 1); // 1..7 var i = idx - 1; if (i < 0) { i = 0; } if (i > 6) { i = 0; } var id = Themes.ORDER[i]; return Themes.THEMES[id] as Dictionary; } function lerpColor(c1 as Number, c2 as Number, f as Float) as Number { var r1 = (c1 & 0xFF0000) >> 16; var g1 = (c1 & 0x00FF00) >> 8; var b1 = (c1 & 0x0000FF); var r2 = (c2 & 0xFF0000) >> 16; var g2 = (c2 & 0x00FF00) >> 8; var b2 = (c2 & 0x0000FF); var r = (r1 + ((r2 - r1) * f).toNumber()).toNumber(); var g = (g1 + ((g2 - g1) * f).toNumber()).toNumber(); var b = (b1 + ((b2 - b1) * f).toNumber()).toNumber(); return (r << 16) | (g << 8) | b; } // interpolate a gradient stop array [[angle,color],...] (angles ascending) function lerpStops(stops as Array>, t as Number) as Number { var n = stops.size(); if (n == 0) { return 0x888888; } var first = stops[0]; var last = stops[n - 1]; if (t <= first[0]) { return first[1]; } if (t >= last[0]) { return last[1]; } for (var i = 0; i < n - 1; i++) { var a = stops[i]; var b = stops[i + 1]; if (t >= a[0] && t <= b[0]) { var f = ((t - a[0]) / (b[0] - a[0])).toFloat(); return lerpColor(a[1], b[1], f); } } return last[1]; } // 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 == 4) { // Move Bar if (info.moveBarLevel != null) { p = info.moveBarLevel.toFloat(); } } if (p > 1.0f) { p = 1.0f; } if (p < 0.0f) { p = 0.0f; } return p; } // value string for a top complication type function complicationValue(typeIdx as Number, info as ActivityMonitor.Info, weather as Weather.CurrentConditions, stats as System.Stats) as String { if (typeIdx == 0) { // Weather temp if (weather != null && weather.temperature != null) { return weather.temperature.format("%d") + "\u00B0"; } return "--"; } else if (typeIdx == 1) { // Calories return info != null && info.calories != null ? info.calories.format("%d") : "0"; } else if (typeIdx == 2) { // Steps return info != null && info.steps != null ? info.steps.format("%d") : "0"; } else if (typeIdx == 3) { // Distance km if (info != null && info.distance != null) { return (info.distance.toFloat() / 100000.0).format("%.1f"); } return "0.0"; } else if (typeIdx == 4) { // Floors return info != null && info.floorsClimbed != null ? info.floorsClimbed.format("%d") : "0"; } else { // Battery var pct = 0; if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); } return pct.format("%d") + "%"; } } // ----------------------------------------------------------------- ring function drawTick(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number, w as Number, ang as Number, col as Number, mirror as Boolean) as Void { var eff = mirror ? -ang : ang; var rad = Math.toRadians(eff); var sin = Math.sin(rad); var cos = Math.cos(rad); var ix = (cx + sin * rIn).toNumber(); var iy = (cy - cos * rIn).toNumber(); var ox = (cx + sin * rOut).toNumber(); var oy = (cy - cos * rOut).toNumber(); dc.setPenWidth(w); dc.setColor(col, Graphics.COLOR_TRANSPARENT); dc.drawLine(ix, iy, ox, oy); } function drawArcTop(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number, w as Number, angles as Array, stops as Array>, off as Number, mirror as Boolean, lit as Number, anchorOuter as Boolean) as Void { var n = angles.size(); for (var i = 0; i < n; i++) { var a = angles[i]; var on = anchorOuter ? (i >= n - lit) : (i < lit); var col = on ? lerpStops(stops, a) : off; drawTick(dc, cx, cy, rIn, rOut, w, a, col, mirror); } } function drawArcBottom(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number, w as Number, botAngles as Array, stops as Array>, off as Number, mirror as Boolean, lit as Number) as Void { var n = botAngles.size(); for (var i = 0; i < n; i++) { var a = botAngles[i]; var on = (i < lit); var ang = (180.0 - a).toNumber(); var col = on ? lerpStops(stops, a) : off; drawTick(dc, cx, cy, rIn, rOut, w, ang, col, mirror); } } function drawRing(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary, pTR as Float, pTL as Float, pBR as Float, pBL as Float) as Void { var rOut = (249.5 * s).toNumber(); var rIn = (231.5 * s).toNumber(); var tickOff = theme["tickOff"] as Number; var topStops = theme["ringTop"] as Array>; var botStops = theme["ringBottom"] as Array>; var topW = (13.0 * s).toNumber(); var botW = (9.0 * s).toNumber(); var TOP = [2,6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74,78,82,86]; var BOT = [6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66]; var litTR = (pTR * 22.0).toNumber(); var litTL = (pTL * 22.0).toNumber(); var litBR = (pBR * 21.0).toNumber(); var litBL = (pBL * 21.0).toNumber(); drawArcTop(dc, cx, cy, rIn, rOut, topW, TOP, topStops, tickOff, false, litTR, true); drawArcTop(dc, cx, cy, rIn, rOut, topW, TOP, topStops, tickOff, true, litTL, true); drawArcBottom(dc, cx, cy, rIn, rOut, botW, BOT, botStops, tickOff, true, litBL); drawArcBottom(dc, cx, cy, rIn, rOut, botW, BOT, botStops, tickOff, false, litBR); // anchor dots var dotR = (9.5 * s).toNumber(); var dotRad = (240.5 * s).toNumber(); var dotCol = theme["anchorDot"] as Number; var dotAngles = [0.0, 93.3, 180.0, 266.7]; dc.setColor(dotCol, Graphics.COLOR_TRANSPARENT); for (var i = 0; i < dotAngles.size(); i++) { var rad = Math.toRadians(dotAngles[i]); var dx = (cx + Math.sin(rad) * dotRad).toNumber(); var dy = (cy - Math.cos(rad) * dotRad).toNumber(); dc.fillCircle(dx, dy, dotR); } } // ------------------------------------------------------------- complications function drawBattery(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary, stats as System.Stats) as Void { var y = (53.5 * s).toNumber(); var pct = 0; if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); } var txt = pct.format("%d") + "%"; var gw = (30.0 * s).toNumber(); var gh = (16.0 * s).toNumber(); var gx = (cx - 52.0 * s).toNumber(); var gy = (y - gh / 2).toNumber(); dc.setPenWidth(2); dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawRectangle(gx, gy, gw, gh); dc.fillRectangle((gx + gw).toNumber(), (gy + gh * 0.28).toNumber(), (3.0 * s).toNumber(), (gh * 0.44).toNumber()); var fillW = ((gw - 4) * (pct.toFloat() / 100.0)).toNumber(); dc.fillRectangle((gx + 2).toNumber(), (gy + 2).toNumber(), fillW, (gh - 4).toNumber()); dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText((cx + 16.0 * s).toNumber(), y, Graphics.FONT_SMALL, txt, Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER); } function drawComplicationIcon(dc as Dc, x as Number, y as Number, r as Number, col as Number, typeIdx as Number) as Void { dc.setPenWidth(2); dc.setColor(col, Graphics.COLOR_TRANSPARENT); if (typeIdx == 0) { // sun dc.fillCircle(x, y, (r * 0.45).toNumber()); for (var i = 0; i < 8; i++) { var rad = Math.toRadians(i * 45.0); var x1 = (x + Math.sin(rad) * r * 0.6).toNumber(); var y1 = (y - Math.cos(rad) * r * 0.6).toNumber(); var x2 = (x + Math.sin(rad) * r).toNumber(); var y2 = (y - Math.cos(rad) * r).toNumber(); dc.drawLine(x1, y1, x2, y2); } } else if (typeIdx == 1) { // flame dc.fillPolygon([[x, (y - r).toNumber()], [(x + r * 0.8).toNumber(), (y + r * 0.6).toNumber()], [(x - r * 0.8).toNumber(), (y + r * 0.6).toNumber()]]); } else if (typeIdx == 2) { // steps dc.drawLine((x - r * 0.5).toNumber(), (y - r * 0.3).toNumber(), (x + r * 0.5).toNumber(), (y - r * 0.3).toNumber()); dc.drawLine((x - r * 0.3).toNumber(), y, (x + r * 0.6).toNumber(), y); } else if (typeIdx == 3) { // distance dot dc.fillCircle(x, y, (r * 0.5).toNumber()); } else if (typeIdx == 4) { // floors chevron dc.fillPolygon([[x, (y - r).toNumber()], [(x + r * 0.7).toNumber(), (y + r * 0.2).toNumber()], [(x - r * 0.7).toNumber(), (y + r * 0.2).toNumber()]]); } else { // battery dc.drawRectangle((x - r * 0.6).toNumber(), (y - r * 0.4).toNumber(), (r * 1.2).toNumber(), (r * 0.8).toNumber()); } } function drawTopComplication(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary, typeIdx as Number, isLeft as Boolean, info as ActivityMonitor.Info, weather as Weather.CurrentConditions, stats as System.Stats) as Void { var sideX = isLeft ? (156.0 * s).toNumber() : (343.0 * s).toNumber(); var iconY = (90.0 * s).toNumber(); var valY = (130.5 * s).toNumber(); var valStr = complicationValue(typeIdx, info, weather, stats); dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText(sideX, valY, Graphics.FONT_MEDIUM, valStr, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); drawComplicationIcon(dc, sideX, iconY, (13.0 * s).toNumber(), theme["accent"] as Number, typeIdx); } function drawBluetooth(dc as Dc, cx as Number, cy as Number, r as Number, col as Number) as Void { dc.setPenWidth(3); dc.setColor(col, Graphics.COLOR_TRANSPARENT); dc.drawLine(cx, (cy - r).toNumber(), cx, (cy + r).toNumber()); dc.drawLine(cx, (cy - r).toNumber(), (cx + r * 0.7).toNumber(), (cy - r * 0.3).toNumber()); dc.drawLine((cx + r * 0.7).toNumber(), (cy - r * 0.3).toNumber(), cx, cy); dc.drawLine(cx, cy, (cx + r * 0.7).toNumber(), (cy + r * 0.3).toNumber()); dc.drawLine((cx + r * 0.7).toNumber(), (cy + r * 0.3).toNumber(), cx, (cy + r).toNumber()); } function drawDateBand(dc as Dc, w as Number, cx as Number, s as Float, theme as Dictionary, ds as System.DeviceSettings) as Void { var by = (286.5 * s).toNumber(); var bh = (44.0 * s).toNumber(); dc.setColor(theme["bandFill"] as Number, theme["bandFill"] as Number); dc.fillRectangle(0, by, w, bh); var txtCol = theme["bandText"] as Number; var yc = (308.5 * s).toNumber(); drawBluetooth(dc, (36.5 * s).toNumber(), yc, (10.0 * s).toNumber(), txtCol); var gi = Gregorian.info(Time.now(), Time.FORMAT_MEDIUM); var DOW = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]; var MON = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]; var dow = DOW[gi.day_of_week - 1]; var md = MON[gi.month - 1] + " " + gi.day.format("%d"); dc.setColor(txtCol, Graphics.COLOR_TRANSPARENT); dc.drawText((177.5 * s).toNumber(), yc, Graphics.FONT_SMALL, dow, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); dc.drawText((297.5 * s).toNumber(), yc, Graphics.FONT_SMALL, md, Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER); var ampm = gi.hour >= 12 ? "PM" : "AM"; dc.drawText((465.0 * s).toNumber(), yc, Graphics.FONT_SMALL, ampm, Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER); } function drawTime(dc as Dc, cx as Number, cy as Number, s as Float, theme as Dictionary) as Void { var gi = Gregorian.info(Time.now(), Time.FORMAT_MEDIUM); var hh = gi.hour; if (hh == 0) { hh = 12; } else if (hh > 12) { hh = hh - 12; } var hStr = hh.format("%d"); var mStr = gi.min.format("%02d"); var ty = (220.0 * s).toNumber(); dc.setColor(theme["hours"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText((211.0 * s).toNumber(), ty, Graphics.FONT_NUMBER_HOT, hStr, Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER); dc.setColor(theme["minutes"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText((272.0 * s).toNumber(), ty, Graphics.FONT_NUMBER_HOT, mStr, Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER); // colon: two blocks centred on x=250 dc.setColor(theme["colon"] as Number, Graphics.COLOR_TRANSPARENT); var cw = (24.0 * s).toNumber(); var ch = (20.0 * s).toNumber(); var cxc = (238.0 * s).toNumber(); dc.fillRectangle(cxc, (184.0 * s).toNumber(), cw, ch); dc.fillRectangle(cxc, (235.0 * s).toNumber(), cw, ch); } function drawBottom(dc as Dc, cx as Number, s as Float, theme as Dictionary, info as ActivityMonitor.Info) as Void { var valY = (366.0 * s).toNumber(); var labY = (410.5 * s).toNumber(); var leftX = (234.0 * s).toNumber(); var rightX = (266.0 * s).toNumber(); var dist = info != null && info.distance != null ? (info.distance.toFloat() / 100000.0).format("%.1f") : "0.0"; var steps = info != null && info.steps != null ? info.steps.format("%d") : "0"; dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText(leftX, valY, Graphics.FONT_MEDIUM, dist, Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER); dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText(leftX, labY, Graphics.FONT_SMALL, "DST", Graphics.TEXT_JUSTIFY_RIGHT | Graphics.TEXT_JUSTIFY_VCENTER); dc.setColor(theme["textPrimary"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText(rightX, valY, Graphics.FONT_MEDIUM, steps, Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER); dc.setColor(theme["accent"] as Number, Graphics.COLOR_TRANSPARENT); dc.drawText(rightX, labY, Graphics.FONT_SMALL, "STEP", Graphics.TEXT_JUSTIFY_LEFT | Graphics.TEXT_JUSTIFY_VCENTER); } // ----------------------------------------------------------------- main function onUpdate(dc as Dc) as Void { var w = dc.getWidth(); var h = dc.getHeight(); var s = w / 500.0; var cx = w / 2; var cy = h / 2; var theme = currentTheme(); var info = ActivityMonitor.getInfo(); var stats = System.getSystemStats(); var ds = System.getDeviceSettings(); var weather = Weather.getCurrentConditions(); 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); drawRing(dc, cx, cy, s, theme, pTR, pTL, pBR, pBL); if (getBool("ShowTop", true)) { drawBattery(dc, cx, cy, s, theme, stats); drawTopComplication(dc, cx, cy, s, theme, getSetting("LeftTop", 1), true, info, weather, stats); drawTopComplication(dc, cx, cy, s, theme, getSetting("RightTop", 0), false, info, weather, stats); } drawTime(dc, cx, cy, s, theme); if (getBool("ShowBand", true)) { drawDateBand(dc, w, cx, s, theme, ds); } if (getBool("ShowBottom", true)) { drawBottom(dc, cx, s, theme, info); } } }