Rebuild the face against the design kit; make fields configurable
Vendor the watchface-kit into design/ and derive everything from it instead
of from hand-transcribed constants.
Geometry, from design/rebuild.py and the ink boxes of the traced SVG paths:
- ring ticks are annular sectors (fillPolygon), not radial lines
- top arc is 21 ticks at 6.5+4k, not 22 at 2+4k
- exact radii, tick widths, anchor dots, band and colon rectangles
- element positions taken from measured ink bounding boxes
Colour, generated by tools/gen_themes.py straight out of the comps:
- per-tick colour tables (21+21) replace 9/7 interpolated stops
- 11-stop vertical gradients on the time digits, drawn with clip banding
- per-element accent / value colours and per-theme unlit tick colour
Data: the design's sunrise and hi/lo fields turn out to be available after
all -- Weather.getSunrise and getDailyForecast both exist in SDK 9.1.0 --
so both are restored, and heart rate is added alongside them.
Bugs found on the way:
- Gregorian FORMAT_MEDIUM returns day_of_week/month as strings, so the
date band crashed on every update (caught in the simulator)
- Stats.battery is already 0-100, multiplying by 100 gave "4000%"
- moveBarLevel is an integer level 0..5, not a 0..1 ratio, so the move
bar arc was all-or-nothing
Also: all four field slots are configurable with labels that follow the
selection, temperature unit is a setting, on-face wording moved into
resources with a Simplified Chinese variant, and 24h/distance/temperature
now follow the watch. Fonts are chosen by measured cap height rather than
by ascent.
Verified in the Connect IQ simulator on fenix847mm and by compiling all
17 target devices.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@ 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;
|
||||
@@ -10,8 +12,163 @@ import Toybox.Time.Gregorian;
|
||||
import Toybox.WatchUi;
|
||||
import Themes;
|
||||
|
||||
// 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 = 90.00;
|
||||
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;
|
||||
private var mDow = null;
|
||||
private var mMon = null;
|
||||
private var mLabels = null;
|
||||
private var mMeridiem = null;
|
||||
private var mDateFmt = " ";
|
||||
|
||||
function initialize() {
|
||||
WatchFace.initialize();
|
||||
}
|
||||
|
||||
function onLayout(dc as Dc) as Void {
|
||||
mW = dc.getWidth();
|
||||
mH = dc.getHeight();
|
||||
mS = mW / 500.0;
|
||||
mCx = mW / 2;
|
||||
mCy = mH / 2;
|
||||
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();
|
||||
}
|
||||
|
||||
// Every piece of on-face wording lives in resources so resources-chn can
|
||||
// replace it. loadResource is not cheap, so pull it all once per layout.
|
||||
function loadStrings() as Void {
|
||||
mDow = splitCsv(WatchUi.loadResource(Rez.Strings.DowNames) as String);
|
||||
mMon = splitCsv(WatchUi.loadResource(Rez.Strings.MonNames) as String);
|
||||
mLabels = splitCsv(WatchUi.loadResource(Rez.Strings.FieldLabels) as String);
|
||||
mMeridiem = splitCsv(WatchUi.loadResource(Rez.Strings.Meridiem) as String);
|
||||
mDateFmt = WatchUi.loadResource(Rez.Strings.DateFormat) as String;
|
||||
}
|
||||
|
||||
// Monkey C has no String.split.
|
||||
function splitCsv(str as String) as Array<String> {
|
||||
var out = [] as Array<String>;
|
||||
var rest = str;
|
||||
var cut = rest.find(",");
|
||||
while (cut != null) {
|
||||
out.add(rest.substring(0, cut) as String);
|
||||
rest = rest.substring(cut + 1, rest.length()) as String;
|
||||
cut = rest.find(",");
|
||||
}
|
||||
out.add(rest);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 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<Graphics.FontDefinition>, 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);
|
||||
@@ -25,45 +182,58 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
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 themeIndex() as Number {
|
||||
var i = getSetting("Theme", 1) - 1;
|
||||
if (i < 0 || i >= Themes.NAMES.size()) { i = 0; }
|
||||
return i;
|
||||
}
|
||||
|
||||
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;
|
||||
// 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();
|
||||
}
|
||||
|
||||
// interpolate a gradient stop array [[angle,color],...] (angles ascending)
|
||||
function lerpStops(stops as Array<Array<Number>>, 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);
|
||||
}
|
||||
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<Number>) 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);
|
||||
}
|
||||
return last[1];
|
||||
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
|
||||
@@ -84,276 +254,430 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
? info.floorsClimbedGoal.toFloat() : 10.0f;
|
||||
p = info.floorsClimbed.toFloat() / goal;
|
||||
}
|
||||
} else if (idx == 4) { // Move Bar
|
||||
if (info.moveBarLevel != null) { p = info.moveBarLevel.toFloat(); }
|
||||
} 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;
|
||||
}
|
||||
|
||||
// value string for a top complication type
|
||||
// ------------------------------------------------------------ data bits
|
||||
function distanceString(info as ActivityMonitor.Info, ds as System.DeviceSettings) as String {
|
||||
if (info == null || info.distance == null) { return "0.0"; }
|
||||
var km = info.distance.toFloat() / 100000.0;
|
||||
if (ds != null && ds.distanceUnits == System.UNIT_STATUTE) { km = km * 0.621371; }
|
||||
return km.format("%.1f");
|
||||
}
|
||||
|
||||
// TempUnit: 0 follow the watch, 1 force Celsius, 2 force Fahrenheit.
|
||||
function tempString(c as Numeric?, ds as System.DeviceSettings) as String {
|
||||
if (c == null) { return "--"; }
|
||||
var t = c.toFloat();
|
||||
var mode = getSetting("TempUnit", 0);
|
||||
var fahrenheit = (mode == 2)
|
||||
|| (mode == 0 && ds != null && ds.temperatureUnits == System.UNIT_STATUTE);
|
||||
if (fahrenheit) { t = t * 9.0 / 5.0 + 32.0; }
|
||||
return t.format("%d") + "°";
|
||||
}
|
||||
|
||||
function clockString(m as Time.Moment?, ds as System.DeviceSettings) as String {
|
||||
if (m == null) { return "--:--"; }
|
||||
var g = Gregorian.info(m, Time.FORMAT_SHORT);
|
||||
var h = g.hour;
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
if (h == 0) { h = 12; } else if (h > 12) { h = h - 12; }
|
||||
}
|
||||
return h.format("%d") + ":" + g.min.format("%02d");
|
||||
}
|
||||
|
||||
function here(weather as Weather.CurrentConditions?) as Position.Location? {
|
||||
if (weather != null && weather.observationLocationPosition != null) {
|
||||
return weather.observationLocationPosition;
|
||||
}
|
||||
var act = Activity.getActivityInfo();
|
||||
if (act != null && act.currentLocation != null) { return act.currentLocation; }
|
||||
return null;
|
||||
}
|
||||
|
||||
// value string for a complication slot
|
||||
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
|
||||
weather as Weather.CurrentConditions?, stats as System.Stats,
|
||||
ds as System.DeviceSettings) as String {
|
||||
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 == 3) { // Distance
|
||||
return distanceString(info, ds);
|
||||
} else if (typeIdx == 4) { // Floors
|
||||
return info != null && info.floorsClimbed != null ? info.floorsClimbed.format("%d") : "0";
|
||||
} else { // Battery
|
||||
return info != null && info.floorsClimbed != null
|
||||
? info.floorsClimbed.format("%d") : "0";
|
||||
} else if (typeIdx == 5) { // Battery
|
||||
var pct = 0;
|
||||
if (stats != null && stats.battery != null) { pct = (stats.battery * 100.0).toNumber(); }
|
||||
return pct.format("%d") + "%";
|
||||
} else if (typeIdx == 6 || typeIdx == 7) { // Sunrise / sunset
|
||||
var loc = here(weather);
|
||||
if (loc == null) { return "--:--"; }
|
||||
var m = typeIdx == 6 ? Weather.getSunrise(loc, Time.now())
|
||||
: Weather.getSunset(loc, Time.now());
|
||||
return clockString(m, ds);
|
||||
} else if (typeIdx == 9) { // Heart rate
|
||||
var act = Activity.getActivityInfo();
|
||||
if (act != null && act.currentHeartRate != null) {
|
||||
return act.currentHeartRate.format("%d");
|
||||
}
|
||||
var hist = ActivityMonitor.getHeartRateHistory(1, true);
|
||||
if (hist != null) {
|
||||
var sample = hist.next();
|
||||
if (sample != null && sample.heartRate != null
|
||||
&& sample.heartRate != ActivityMonitor.INVALID_HR_SAMPLE) {
|
||||
return sample.heartRate.format("%d");
|
||||
}
|
||||
}
|
||||
return "--";
|
||||
} else if (typeIdx == 8) { // Forecast high / low
|
||||
var fc = Weather.getDailyForecast();
|
||||
if (fc != null && fc.size() > 0) {
|
||||
return tempString(fc[0].highTemperature, ds) + "/"
|
||||
+ tempString(fc[0].lowTemperature, ds);
|
||||
}
|
||||
return "--/--";
|
||||
}
|
||||
// 0: current temperature
|
||||
if (weather != null && weather.temperature != null) {
|
||||
return tempString(weather.temperature, ds);
|
||||
}
|
||||
return "--";
|
||||
}
|
||||
|
||||
// Short caption under a bottom-row value. Indexed like the field types, so
|
||||
// the label always follows whatever the slot is set to.
|
||||
function fieldLabel(typeIdx as Number) as String {
|
||||
if (mLabels == null || typeIdx < 0 || typeIdx >= mLabels.size()) { return ""; }
|
||||
return mLabels[typeIdx];
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- 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 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)]]);
|
||||
}
|
||||
|
||||
function drawArcTop(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
|
||||
w as Number, angles as Array<Number>, stops as Array<Array<Number>>,
|
||||
off as Number, mirror as Boolean, lit as Number, anchorOuter as Boolean) as Void {
|
||||
var n = angles.size();
|
||||
// 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<Number>, 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 = 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);
|
||||
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 drawArcBottom(dc as Dc, cx as Number, cy as Number, rIn as Number, rOut as Number,
|
||||
w as Number, botAngles as Array<Number>, stops as Array<Array<Number>>,
|
||||
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, ti as Number, pTR as Float, pTL as Float,
|
||||
pBR as Float, pBL as Float) as Void {
|
||||
var top = Themes.RINGTOP[ti] as Array<Number>;
|
||||
var bot = Themes.RINGBOTTOM[ti] as Array<Number>;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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<Array<Number>>;
|
||||
var botStops = theme["ringBottom"] as Array<Array<Number>>;
|
||||
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);
|
||||
// ------------------------------------------------------------ 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);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Filled half-ellipse, flat side down. The comp's dome is 14.90 wide but
|
||||
// 8.93 tall, so it is not a plain semicircle.
|
||||
function dome(dc as Dc, cx as Numeric, baseY as Numeric,
|
||||
rx as Numeric, ry as Numeric) as Void {
|
||||
var pts = [];
|
||||
for (var i = 0; i <= 12; i++) {
|
||||
var rad = Math.toRadians(180.0 - 15.0 * i);
|
||||
pts.add([Math.round(px(cx) + Math.cos(rad) * px(rx)).toNumber(),
|
||||
Math.round(px(baseY) - Math.sin(rad) * px(ry)).toNumber()]);
|
||||
}
|
||||
dc.fillPolygon(pts);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// Sun over a horizon bar with a chevron. Every ray in the comp is a short
|
||||
// stub roughly as long as the pen is wide -- drawing them longer is what
|
||||
// made this icon read taller than the weather one.
|
||||
// dir = -1 flips the chevron up, which is what the sunset slot uses.
|
||||
function iconSun(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
|
||||
col as Number, dir as Number) as Void {
|
||||
var k = scale;
|
||||
var ox = cx - 156.32 * k; // design-space offset of this icon
|
||||
var oy = cy - 90.34 * k;
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dome(dc, ox + 156.39 * k, oy + 94.02 * k, 7.45 * k, 8.93 * k);
|
||||
dc.setPenWidth(maxNum(2, px(2.96 * k)));
|
||||
stub(dc, ox + 156.39 * k, oy + 79.4 * k, ox + 156.39 * k, oy + 81.1 * k);
|
||||
stub(dc, ox + 148.2 * k, oy + 83.3 * k, ox + 146.5 * k, oy + 84.5 * k);
|
||||
stub(dc, ox + 164.6 * k, oy + 84.5 * k, ox + 166.3 * k, oy + 83.3 * k);
|
||||
stub(dc, ox + 142.6 * k, oy + 92.5 * k, ox + 144.7 * k, oy + 92.5 * k);
|
||||
stub(dc, ox + 168.0 * k, oy + 92.5 * k, ox + 170.1 * k, oy + 92.5 * k);
|
||||
dc.setPenWidth(maxNum(2, px(2.6 * k)));
|
||||
var by = oy + 98.45 * k;
|
||||
stub(dc, ox + 142.3 * k, by, ox + 170.3 * k, by);
|
||||
stub(dc, ox + 152.6 * k, by, ox + 156.3 * k, by + dir * 3.05 * k);
|
||||
stub(dc, ox + 156.3 * k, by + dir * 3.05 * k, ox + 160.0 * k, by);
|
||||
}
|
||||
|
||||
// Sun peeking out from behind a cloud. The comp keeps a dark notch where
|
||||
// the cloud crosses the sun; without it the two shapes merge into one blob
|
||||
// and the icon reads much heavier than the sunrise one next to it. So the
|
||||
// cloud is stamped twice: once inflated in black to cut the notch, then at
|
||||
// true size in the accent colour.
|
||||
function cloudBody(dc as Dc, ox as Numeric, oy as Numeric, k as Float,
|
||||
grow as Float) as Void {
|
||||
dc.fillCircle(px(ox + 336.6 * k), px(oy + 94.4 * k), px(8.7 * k + grow));
|
||||
dc.fillCircle(px(ox + 346.4 * k), px(oy + 96.9 * k), px(6.3 * k + grow));
|
||||
var g = px(grow);
|
||||
dc.fillRoundedRectangle(px(ox + 328.0 * k) - g, px(oy + 95.5 * k) - g,
|
||||
px(24.6 * k) + 2 * g, px(7.75 * k) + g,
|
||||
px(3.4 * k));
|
||||
}
|
||||
|
||||
function iconCloud(dc as Dc, cx as Numeric, cy as Numeric, scale as Float,
|
||||
col as Number) as Void {
|
||||
var k = scale;
|
||||
var ox = cx - 343.55 * k;
|
||||
var oy = cy - 89.25 * k;
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
dc.fillCircle(px(ox + 347.0 * k), px(oy + 88.9 * k), px(7.8 * k));
|
||||
dc.setPenWidth(maxNum(2, px(2.3 * k)));
|
||||
stub(dc, ox + 346.2 * k, oy + 76.0 * k, ox + 346.2 * k, oy + 78.7 * k);
|
||||
stub(dc, ox + 354.9 * k, oy + 87.7 * k, ox + 357.5 * k, oy + 87.7 * k);
|
||||
dc.setPenWidth(maxNum(2, px(2.7 * k)));
|
||||
stub(dc, ox + 339.5 * k, oy + 79.5 * k, ox + 338.1 * k, oy + 80.9 * k);
|
||||
stub(dc, ox + 353.5 * k, oy + 80.9 * k, ox + 354.6 * k, oy + 79.8 * k);
|
||||
dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
|
||||
cloudBody(dc, ox, oy, k, 1.7 * k);
|
||||
dc.setColor(col, Graphics.COLOR_TRANSPARENT);
|
||||
cloudBody(dc, ox, oy, k, 0.0);
|
||||
}
|
||||
|
||||
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 == 6) {
|
||||
iconSun(dc, cx, cy, scale, col, 1);
|
||||
} else if (typeIdx == 7) {
|
||||
iconSun(dc, cx, cy, scale, col, -1);
|
||||
} else if (typeIdx == 0 || typeIdx == 8) {
|
||||
iconCloud(dc, cx, cy, scale, col);
|
||||
} else if (typeIdx == 5) {
|
||||
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 == 1) { // 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 == 2) { // steps: two prints
|
||||
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) { // distance: arrow
|
||||
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 == 9) { // heart rate
|
||||
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 { // floors: 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));
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
// ------------------------------------------------------------ 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 drawBluetooth(dc as Dc, cx as Number, cy as Number, r as Number, col as Number) as Void {
|
||||
dc.setPenWidth(3);
|
||||
function drawTopComplication(dc as Dc, ti as Number, typeIdx as Number, isLeft as Boolean,
|
||||
info as ActivityMonitor.Info, weather as Weather.CurrentConditions?,
|
||||
stats as System.Stats, ds as System.DeviceSettings) as Void {
|
||||
var x = isLeft ? LEFT_X : RIGHT_X;
|
||||
var level = 0.0f;
|
||||
if (stats != null && stats.battery != null) { level = stats.battery / 100.0; }
|
||||
drawComplicationIcon(dc, x, ICON_CY, 1.0, Themes.ACCENT[ti] as Number, typeIdx, level);
|
||||
text(dc, x, VALUE_CY, mFontComp,
|
||||
complicationValue(typeIdx, info, weather, stats, ds),
|
||||
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.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());
|
||||
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, 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 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, mDow[g.day_of_week - 1],
|
||||
Graphics.TEXT_JUSTIFY_CENTER, col);
|
||||
// DateFormat is "$1$ $2$" in English and "$1$$2$日" in Chinese, where
|
||||
// the Chinese month names already carry their own 月.
|
||||
text(dc, MD_CX, BAND_CY, mFontBand,
|
||||
Lang.format(mDateFmt, [mMon[g.month - 1], g.day.format("%d")]),
|
||||
Graphics.TEXT_JUSTIFY_CENTER, col);
|
||||
if (ds == null || !ds.is24Hour) {
|
||||
text(dc, AMPM_R, BAND_CY, mFontBand, mMeridiem[g.hour >= 12 ? 1 : 0],
|
||||
Graphics.TEXT_JUSTIFY_RIGHT, col);
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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);
|
||||
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<Number>);
|
||||
gradText(dc, MINUTES_L, TIME_CY, mFontTime, g.min.format("%02d"),
|
||||
Graphics.TEXT_JUSTIFY_LEFT, Themes.MINUTES[ti] as Array<Number>);
|
||||
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, info as ActivityMonitor.Info,
|
||||
weather as Weather.CurrentConditions?, stats as System.Stats,
|
||||
ds as System.DeviceSettings) 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", 2);
|
||||
text(dc, DIST_R, BOT_VAL_CY, mFontComp,
|
||||
complicationValue(left, info, weather, stats, ds),
|
||||
Graphics.TEXT_JUSTIFY_RIGHT, value);
|
||||
text(dc, DIST_R, BOT_LAB_CY, mFontComp, fieldLabel(left),
|
||||
Graphics.TEXT_JUSTIFY_RIGHT, label);
|
||||
text(dc, STEP_L, BOT_VAL_CY, mFontComp,
|
||||
complicationValue(right, info, weather, stats, ds),
|
||||
Graphics.TEXT_JUSTIFY_LEFT, value);
|
||||
text(dc, STEP_L, BOT_LAB_CY, mFontComp, fieldLabel(right),
|
||||
Graphics.TEXT_JUSTIFY_LEFT, label);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- 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;
|
||||
if (mW != dc.getWidth() || mDow == null) { onLayout(dc); }
|
||||
if (dc has :setAntiAlias) { dc.setAntiAlias(true); }
|
||||
|
||||
var theme = currentTheme();
|
||||
var ti = themeIndex();
|
||||
var info = ActivityMonitor.getInfo();
|
||||
var stats = System.getSystemStats();
|
||||
var ds = System.getDeviceSettings();
|
||||
@@ -362,26 +686,26 @@ class Fenix8V3View extends WatchUi.WatchFace {
|
||||
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);
|
||||
drawRing(dc, ti,
|
||||
metricProgress(getSetting("RingTR", 0), info),
|
||||
metricProgress(getSetting("RingTL", 1), info),
|
||||
metricProgress(getSetting("RingBR", 4), info),
|
||||
metricProgress(getSetting("RingBL", 3), info));
|
||||
|
||||
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);
|
||||
drawBattery(dc, ti, stats);
|
||||
drawTopComplication(dc, ti, getSetting("LeftTop", 6), true, info, weather, stats, ds);
|
||||
drawTopComplication(dc, ti, getSetting("RightTop", 8), false, info, weather, stats, ds);
|
||||
}
|
||||
|
||||
drawTime(dc, cx, cy, s, theme);
|
||||
drawTime(dc, ti, ds);
|
||||
|
||||
if (getBool("ShowBand", true)) {
|
||||
drawDateBand(dc, w, cx, s, theme, ds);
|
||||
drawDateBand(dc, ti, ds);
|
||||
}
|
||||
|
||||
if (getBool("ShowBottom", true)) {
|
||||
drawBottom(dc, cx, s, theme, info);
|
||||
drawBottom(dc, ti, info, weather, stats, ds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user