/** * Adrian Kirk 2021-02 * Sliding text clock inspired by the Pebble * clock with the same name */ const color_schemes = [ { name: "white", background : [1.0,1.0,1.0], main_bar: [0.0,0.0,0.0], other_bars: [0.1,0.1,0.1], }, { name: "black", background : [0.0,0.0,0.0], main_bar: [1.0,1.0,1.0], other_bars: [0.9,0.9,0.9], }, { name: "red", background : [1.0,0.0,0.0], main_bar: [1.0,1.0,0.0], other_bars: [0.85,0.85,0.85] }, { name: "grey", background : [0.5,0.5,0.5], main_bar: [1.0,1.0,1.0], other_bars: [0.0,0.0,0.0], }, { name: "purple", background : [1.0,0.0,1.0], main_bar: [1.0,1.0,0.0], other_bars: [0.85,0.85,0.85] }, { name: "blue", background : [0.4,0.7,1.0], main_bar: [1.0,1.0,1.0], other_bars: [0.9,0.9,0.9] } ]; let color_scheme_index = 0; /** * The Watch Display */ function bg_color(){ return color_schemes[color_scheme_index].background; } function main_color(){ return color_schemes[color_scheme_index].main_bar; } function other_color(){ return color_schemes[color_scheme_index].other_bars; } let command_stack_high_priority = []; let command_stack_low_priority = []; function next_command(){ command = command_stack_high_priority.pop(); if(command == null){ //console.log("Low priority command"); command = command_stack_low_priority.pop(); } else { //console.log("High priority command"); } if(command != null){ command.call(); } else { //console.log("no command"); } } function reset_commands(){ command_stack_high_priority = []; command_stack_low_priority = []; } function has_commands(){ return command_stack_high_priority.length > 0 || command_stack_low_priority.length > 0; } class ShiftText { /** * Class Responsible for shifting text around the screen * * This is a object that initializes itself with a position and * text after which you can tell it where you want to move to * using the moveTo method and it will smoothly move the text across * at the selected frame rate and speed */ constructor(x,y,txt,font_name, font_size,speed_x,speed_y,freq_millis, color, bg_color){ this.x = x; this.tgt_x = x; this.init_x = x; this.y = y; this.tgt_y = y; this.init_y = y; this.txt = txt; this.init_txt = txt; this.font_name = font_name; this.font_size = font_size; this.speed_x = Math.abs(speed_x); this.speed_y = Math.abs(speed_y); this.freq_millis = freq_millis; this.color = color; this.bg_color = bg_color; this.finished_callback=null; this.timeoutId = null; } setColor(color){ this.color = color; } setBgColor(bg_color){ this.bg_color = bg_color; } reset(hard_reset) { //console.log("reset"); this.hide(); this.x = this.init_x; this.y = this.init_y; if (hard_reset) { this.txt = this.init_txt; } this.show(); if(this.timeoutId != null){ clearTimeout(this.timeoutId); } } show() { g.setFontAlign(-1,-1,0); g.setFont(this.font_name,this.font_size); g.setColor(this.color[0],this.color[1],this.color[2]); g.drawString(this.txt, this.x, this.y); } hide(){ g.setFontAlign(-1,-1,0); g.setFont(this.font_name,this.font_size); //console.log("bgcolor:" + this.bg_color); g.setColor(this.bg_color[0],this.bg_color[1],this.bg_color[2]); g.drawString(this.txt, this.x, this.y); } setText(txt){ this.txt = txt; } setTextPosition(txt,x,y){ this.hide(); this.x = x; this.y = y; this.txt = txt; this.show(); } setTextXPosition(txt,x){ this.hide(); this.x = x; this.txt = txt; this.show(); } setTextYPosition(txt,y){ this.hide(); this.y = y; this.txt = txt; this.show(); } moveTo(new_x,new_y){ this.tgt_x = new_x; this.tgt_y = new_y; this._doMove(); } moveToX(new_x){ this.tgt_x = new_x; this._doMove(); } moveToY(new_y){ this.tgt_y = new_y; this._doMove(); } onFinished(finished_callback){ this.finished_callback = finished_callback; } /** * private internal method for directing the text move. * It will see how far away we are from the target coords * and move towards the target at the defined speed. */ _doMove(){ this.hide(); // move closer to the target in the x direction var diff_x = this.tgt_x - this.x; var finished_x = false; if(Math.abs(diff_x) <= this.speed_x){ this.x = this.tgt_x; finished_x = true; } else { if(diff_x > 0){ this.x += this.speed_x; } else { this.x -= this.speed_x; } } // move closer to the target in the y direction var diff_y = this.tgt_y - this.y; var finished_y = false; if(Math.abs(diff_y) <= this.speed_y){ this.y = this.tgt_y; finished_y = true; } else { if(diff_y > 0){ this.y += this.speed_y; } else { this.y -= this.speed_y; } } this.show(); this.timeoutId = null; var finished = finished_x & finished_y; if(!finished){ this.timeoutId = setTimeout(this._doMove.bind(this), this.freq_millis); } else if(this.finished_callback != null){ //console.log("finished - calling:" + this.finished_callback); this.finished_callback.call(); this.finished_callback = null; } } } const CLOCK_TEXT_SPEED_X = 10; // a list of display rows let row_displays; function setRowDisplays(y, heights) { var cols = [ main_color(), other_color(), other_color(), other_color(), main_color() ]; row_displays = []; for (var i=0;i200)? 1 : 2; } if (bangleVersion()<2) setRowDisplays(50, [40,30,30,30,40]); else setRowDisplays(34, [35,25,25,25,35]); function nextColorTheme(){ color_scheme_index += 1; if(color_scheme_index > row_displays.length){ color_scheme_index = 0; } setColorScheme(color_schemes[color_scheme_index]); reset_clock(true); draw_clock(); } function setColorScheme(color_scheme){ setColor(color_scheme.main_bar, color_scheme.other_bars, color_scheme.background); } function setColor(main_color,other_color,bg_color){ row_displays[0].setColor(main_color); row_displays[0].setBgColor(bg_color); for(var i=1; i= date_formatters.length){ date_formatter_idx = 0; } console.log("changing to formatter " + date_formatter_idx); date_formatter = date_formatters[date_formatter_idx]; reset_clock(true); draw_clock(); command_stack_high_priority.unshift( function() { //console.log("move in new:" + txt); // first select the top or bottom to display the formatter name // We choose the first spare row without text var format_name_display = row_displays[row_displays.length - 1]; if (format_name_display.txt != '') { format_name_display = row_displays[0]; } if (format_name_display.txt != ''){ return; } format_name_display.speed_x = 3; format_name_display.onFinished(function(){ format_name_display.speed_x = CLOCK_TEXT_SPEED_X; console.log("return speed to:" + format_name_display.speed_x) next_command(); }); format_name_display.setTextXPosition(date_formatter.name(),220); format_name_display.moveToX(-date_formatter.name().length * format_name_display.font_size); } ); } var DISPLAY_TEXT_X = 20; function reset_clock(hard_reset){ console.log("reset_clock hard_reset:" + hard_reset); setColorScheme(color_schemes[color_scheme_index]); if(!hard_reset && last_draw_time != null){ // If its not a hard reset then we want to reset the // rows set to the last time. If the last time is too long // ago then we fast forward to 1 min ago. // In this way the watch wakes by scrolling // off the last time and scroll on the new time var reset_time = last_draw_time; var last_minute_millis = Date.now() - 60000; if(reset_time.getTime() < last_minute_millis){ reset_time = display_time(new Date(last_minute_millis)); } var rows = date_formatter.formatDate(reset_time); for (var i = 0; i < rows.length; i++) { row_displays[i].hide(); row_displays[i].speed_x = CLOCK_TEXT_SPEED_X; row_displays[i].x = DISPLAY_TEXT_X; row_displays[i].y = row_displays[i].init_y; if(row_displays[i].timeoutId != null){ clearTimeout(row_displays[i].timeoutId); } row_displays[i].setText(rows[i]); row_displays[i].show(); } } else { // do a hard reset and clear everything out for (var i = 0; i < row_displays.length; i++) { row_displays[i].speed_x = CLOCK_TEXT_SPEED_X; row_displays[i].reset(hard_reset); } } reset_commands(); } let last_draw_time = null; const next_minute_boundary_secs = 10; function display_time(date){ if(date.getSeconds() > 60 - next_minute_boundary_secs){ console.log("forwarding to next minute"); return new Date(date.getTime() + next_minute_boundary_secs * 1000); } else { return date; } } function draw_clock(){ var date = new Date(); // we don't want the time to be displayed // and then immediately be trigger another time if(last_draw_time != null && Date.now() - last_draw_time.getTime() < next_minute_boundary_secs * 1000 && has_commands() ){ console.log("skipping draw clock"); return; } else { last_draw_time = date; } reset_commands(); date = display_time(date); console.log("draw_clock:" + last_draw_time.toISOString() + " display:" + date.toISOString()); var rows = date_formatter.formatDate(date); var display; for (var i = 0; i < rows.length; i++) { display = row_displays[i]; var txt = rows[i]; //console.log(i + "->" + txt); display_row(display,txt); } // If the dateformatter has not returned enough // rows then treat the reamining rows as empty for (var j = i; j < row_displays.length; j++) { display = row_displays[j]; //console.log(i + "->''(empty)"); display_row(display,''); } next_command(); //console.log(date); } function display_row(display,txt){ if(display == null) { console.log("no display for text:" + txt) return; } if(display.txt == null || display.txt == ''){ if(txt != '') { command_stack_high_priority.unshift( function () { //console.log("move in new:" + txt); display.onFinished(next_command); display.setTextXPosition(txt, 240); display.moveToX(DISPLAY_TEXT_X); } ); } } else if(txt != display.txt && display.txt != null){ command_stack_high_priority.push( function(){ //console.log("move out:" + txt); display.onFinished(next_command); display.moveToX(-display.txt.length * display.font_size); } ); command_stack_low_priority.push( function(){ //console.log("move in:" + txt); display.onFinished(next_command); display.setTextXPosition(txt,240); display.moveToX(DISPLAY_TEXT_X); } ); } else { command_stack_high_priority.push( function(){ //console.log("move in2:" + txt); display.setTextXPosition(txt,DISPLAY_TEXT_X); next_command(); } ); } } /** * called from load_settings on startup to * set the color scheme to named value */ function set_colorscheme(colorscheme_name){ console.log("setting color scheme:" + colorscheme_name); for (var i=0; i < color_schemes.length; i++) { if(color_schemes[i].name == colorscheme_name){ color_scheme_index = i; console.log("match"); setColorScheme(color_schemes[color_scheme_index]); break; } } } function set_dateformat(dateformat_name){ console.log("setting date format:" + dateformat_name); for (var i=0; i < date_formatters.length; i++) { if(date_formatters[i].shortName() == dateformat_name){ date_formatter_idx = i; date_formatter = date_formatters[date_formatter_idx]; console.log("match"); } } } var enable_live_controls = false; const PREFERENCE_FILE = "slidingtext.settings.json"; /** * Called on startup to set the watch to the last preference settings */ function load_settings(){ var setScheme = false; try{ var settings = require("Storage").readJSON(PREFERENCE_FILE); if(settings != null){ console.log("loaded:" + JSON.stringify(settings)); if(settings.color_scheme != null){ set_colorscheme(settings.color_scheme); setScheme = true; } if(settings.date_format != null){ set_dateformat(settings.date_format); } if(settings.enable_live_controls == null){ settings.enable_live_controls = (bangleVersion() <= 1); } enable_live_controls = settings.enable_live_controls; } else { console.log("no settings to load"); enable_live_controls = (bangleVersion() <= 1); } console.log("enable_live_controls=" + enable_live_controls); } catch(e){ console.log("failed to load settings:" + e); } // just set up as default if (!setScheme) setColorScheme(color_schemes[color_scheme_index]); } /** * Called on button press to save down the last preference settings */ function save_settings(){ var settings = { date_format : date_formatter.shortName(), color_scheme : color_schemes[color_scheme_index].name, enable_live_controls: enable_live_controls }; console.log("saving:" + JSON.stringify(settings)); require("Storage").writeJSON(PREFERENCE_FILE,settings); } function button1pressed() { console.log("button1pressed"); if (enable_live_controls) { changeFormatter(); save_settings(); } } function button3pressed() { console.log("button3pressed"); if (enable_live_controls) { nextColorTheme(); reset_clock(true); draw_clock(); save_settings(); } } // The interval reference for updating the clock let intervalRef = null; function clearTimers(){ if(intervalRef != null) { clearInterval(intervalRef); intervalRef = null; } } function startTimers(){ var date = new Date(); var secs = date.getSeconds(); var nextMinuteStart = 60 - secs; //console.log("scheduling clock draw in " + nextMinuteStart + " seconds"); setTimeout(scheduleDrawClock,nextMinuteStart * 1000); draw_clock(); } /** * confirms that a redraw is needed by checking the last redraw time and * the lcd state of the UI * @returns {boolean|*} */ function shouldRedraw(){ return last_draw_time != null && Date.now() - last_draw_time.getTime() > next_minute_boundary_secs * 1000 && Bangle.isLCDOn(); } function scheduleDrawClock(){ clearTimers(); if (Bangle.isLCDOn()) { console.log("schedule draw of clock"); intervalRef = setInterval(() => { if (!shouldRedraw()) { console.log("draw clock callback - skipped redraw"); } else { console.log("draw clock callback"); draw_clock() } }, 60 * 1000 ); if (shouldRedraw()) { draw_clock(); } else { console.log("scheduleDrawClock - skipped redraw"); } } else { console.log("scheduleDrawClock - skipped not visible"); } } Bangle.on('lcdPower', (on) => { if (on) { console.log("lcdPower: on"); Bangle.drawWidgets(); reset_clock(false); startTimers(); } else { console.log("lcdPower: off"); reset_clock(false); clearTimers(); } }); g.clear(); load_settings(); Bangle.loadWidgets(); Bangle.drawWidgets(); startTimers(); // Show launcher when button pressed Bangle.setUI("clockupdown", d=>{ if (d<0) button1pressed(); if (d>0) button3pressed(); });