diff --git a/apps/dwm-clock/app.js b/apps/dwm-clock/app.js new file mode 100644 index 000000000..773777ca5 --- /dev/null +++ b/apps/dwm-clock/app.js @@ -0,0 +1,224 @@ +// daylight world map clock +// equirectangular projected map and approximated daylight graph + +// load font for timezone, weekday and day in month +require("FontDennis8").add(Graphics); + +const W = g.getWidth(); +const H = g.getHeight(); + +const TZOFFSET = new Date().getTimezoneOffset(); + +const UTCSTRING = ((TZOFFSET > 0 ? "-" : "+") + + ("0" + Math.floor(Math.abs(TZOFFSET) / 60)).slice(-2)) + + (TZOFFSET % 60 ? Math.abs(TZOFFSET) % 60 : ""); + +function getMap() { + return { + width: 176, height: 88, bpp: 1, + transparent: 1, + buffer: require("heatshrink").decompress(atob("/4A/AA0Av+Ag4UQwBhDn//1//8///AUI3MAhAUBgIQBh4LC/kfCg34rmAngVD/1/CYICBA4IAF8EOwF/+AVCAAXj//AA4PjDQIVDgkQj/4gBtEx+EGgXwCoJ8Bv+8geQgIVE4P/553Egf/nwFCgUE4H8gBqB/0AhLxHggFE+E8gJoBDIIAI5wFE4F8h/4v5FBABA2BAAUf7n+VYXgoAVNn/Dv+fCoPACo8MEQPHHAUf4DuB//58FgCgsHeoWfMgUDConw4AVFh/wXIRDDwBWC8jfBFY3xaAa5DYYXkKw8D+YVDHAcXAwKuIgIUDSIIJCsYVKeAIVHj5fGNogVHgN/AwPyEgPhCokZCo40D8E0wcwTYhsECoY0D8H2hEACocBCoqnCKwQVB/nICokJ+4VL/RGBQQkdw4VESQTwCDgIVBNgkeEQaSEQQReC4QrEhwUECoUECooAFVwoABgF+CoY+DAYZAFAAOgv4VGoFgCpXwGIoABkEHDQUvCo9zD4YVE4EIgIUGCoNnZwYVCiEP8E8hYVH/kHII0Qj/wvkP94WH4IVGhE/MQMH54VH+IVGKYIJBgfnCo/98IVFcYP5/9HMYbdGn7FFv/4/9vCpH/4DmC4AVCD4P/n4VKUoXgCwQ2Cz42CCpX//BtCCoMeCpJTBZgcAgYFCjElCpA7BEIQVBZoeYp4sICoIQCIIJzC/+Mp+DCpJSC/kAj4KC5/f4GfK5AVIeYPgNpIVEIIf/6f/v6ZHPwYVG//7V5BtDCoMOEof+jYVH8AVFhgLD/EZCo6UBCokYBYa2BCp04G4oVJNAX+gF4XYqDHCoKqCCoIrDAoL9DCowfCB4N9CorMDCooPEfowVMB4IVPeAQABwIVPeAQABw4LEg/ANo/wTAQAI8E//YVS+F//IIGGg4AFCo7OHAAf+v/jCowqM//HAwvhCpuPOwwVNAAwrOAA3xCqhtOAH4AfW4wAN/0/A4sP//AgFygYVH/V/AwlwgE8gAACDYIAF9ArC+uACAUgCocAHIn8k/gj4FBCgYAGBoXwgEYDof+ChMAJ4PmAwcBDgIUKgANBJIkZ/0cCpYrBIAIADzkwChQ5B/tgBAh7FNpANMAGg=")) + }; +} + +const YOFFSET = H - getMap().height; + +// map offset in degree +// -180 to 180 / default: 0 +function getLongitudeOffset() { + return require("Storage").readJSON("dwm-clock.json", 1) || {"lon": 0}; +} + +function drawMap() { + g.setBgColor(0, 0, 0); + + // does not flip on it's own, but there is a draw function after that does + g.drawImages([{ + x: -lonOffset * W / 360, + y: YOFFSET, + image: getMap(), + scale: 1, + rotate: 0, + center: false, + repeat: true, + nobounds: false + }], { + x: 0, + y: YOFFSET, + width: getMap().width, + height: getMap().height + }); +} + +function drawDaylightMap() { + // number of xy points, < 40 looks very skewed around solstice + const STEPS = 40; + const YFACTOR = getMap().height / 2; + const YOFF = H / 2 + YFACTOR; + var graph = []; + + // progress of day, float 0 to 1 + var dayOffset = (now.getHours() + (now.getMinutes() + TZOFFSET) / 60) / 24; + + // sun position modifier + var sunPosMod; + + var solarNoon = require("suncalc").getTimes(now, 0, 0, 0).solarNoon; + + var altitude = require("suncalc").getPosition(solarNoon, 0, 0).altitude; + + // this is trial and error. no thought went into this + sunPosMod = Math.pow(altitude - 0.08, 8); + + // switch sign on equinox + // this is an approximation + if (require("suncalc").getPosition(solarNoon, 0, 0).azimuth < -1) { + sunPosMod = -sunPosMod; + } + + for (var x = 0; x < (STEPS + 1) / STEPS; x += 1 / STEPS) { + // this is an approximation instead of projecting a circle onto a sphere + // y = arctan(sin(x) * n) + var y = Math.atan(Math.sin(2 * Math.PI * x + dayOffset * 2 * Math.PI + // user defined map offset fixed offset + // v v + + 2 * Math.PI * lonOffset / 360 - Math.PI / 2) * sunPosMod) + * (2 / Math.PI); + // ^ + // factor keeps y <= 1 + + graph.push(x * W, y * YFACTOR + YOFF); + } + + // day area, yellow + g.setColor(0.8, 0.8, 0.3); + g.fillRect(0, YOFFSET, W, H); + + // night area, blue + g.setColor(0, 0, 0.5); + // switch on equinox + if (sunPosMod < 0) { + g.fillPoly([0, H - 1].concat(graph, W - 1, H - 1)); + } else { + g.fillPoly([0, YOFFSET].concat(graph, W, YOFFSET)); + } + + drawMap(); + + // day-night line, white + g.setColor(1, 1, 1); + g.drawPoly(graph, false); +} + +function drawClock() { + // clock area + g.clearRect(0, YOFFSET, W, 24); + + // clock text + g.setColor(1, 1, 1); + g.setFontAlign(0, -1); + g.setFont("Vector", 58); + // with the vector font this leaves 26px above the text + g.drawString(require("locale").time(now, 1), W / 2, 24 - 2); + + + // timezone text + g.setFontAlign(-1, 1); + g.setFont("6x8", 2); + g.drawString("UTC" + UTCSTRING, 3, YOFFSET); + + + // day text + g.setFontAlign(1, 1); + g.setFont("Dennis8", 2); + g.drawString(require("locale").dow(now, 1) + " " + now.getDate(), + W - 1, YOFFSET); +} + +function renderScreen() { + now = new Date(); + + drawClock(); + drawDaylightMap(); +} + +function renderAndQueue() { + timeoutID = setTimeout(renderAndQueue, 60000 - (Date.now() % 60000)); + renderScreen(); +} + +g.reset().clearRect(Bangle.appRect); + +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +g.setBgColor(0, 0, 0); + +var now = new Date(); + +// map offsets +var defLonOffset = getLongitudeOffset().lon; +var lonOffset = defLonOffset; + +var timeoutID; +var timeoutIDTouch; + +Bangle.on('drag', function(touch) { + + if (timeoutIDTouch) { + clearTimeout(timeoutIDTouch); + } + + // return after not touching for 5 seconds + timeoutIDTouch = setTimeout(renderAndQueue, 5 * 1000); + + // touch map + if (touch.y >= YOFFSET) { + lonOffset -= touch.dx * 360 / W; + + // wrap map offset + if (lonOffset < -180) { + lonOffset += 360; + } else if (lonOffset >= 180) { + lonOffset -= 360; + } + + // snap to 0° longitude + if (lonOffset > -5 && lonOffset < 5) { + lonOffset = 0; + } + + lonOffset = Math.round(lonOffset); + + // clock area + g.clearRect(0, YOFFSET, W, 24); + + // text + g.setColor(1, 1, 1); + g.setFontAlign(0, -1); + g.setFont("Dennis8", 2); + // could not get ° (degree sign) to render + g.drawString("select lon offset\n< tap: save\nreset: tap >\n" + + lonOffset + " degree", W / 2, 24); + + drawDaylightMap(); + + // touch clock, left side, save offset + } else if (touch.x < W / 2) { + if (defLonOffset != lonOffset) { + require("Storage").writeJSON("dwm-clock.json", {"lon": lonOffset}); + defLonOffset = lonOffset; + } + + renderScreen(); + + // touch clock, right side, reset offset + } else { + lonOffset = defLonOffset; + renderScreen(); + } +}); + +renderAndQueue();