mirror of https://github.com/espruino/BangleApps
476 lines
12 KiB
JavaScript
476 lines
12 KiB
JavaScript
/**
|
|
* Adrian Kirk 2021-02
|
|
* Sliding text clock inspired by the Pebble
|
|
* clock with the same name
|
|
*/
|
|
|
|
|
|
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){
|
|
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.colour = color;
|
|
this.finished_callback=null;
|
|
this.timeoutId = null;
|
|
}
|
|
reset(){
|
|
this.hide();
|
|
this.x = this.init_x;
|
|
this.y = this.init_y;
|
|
this.txt = this.init_txt;
|
|
this.show();
|
|
if(this.timeoutId != null){
|
|
clearTimeout(this.timeoutId);
|
|
}
|
|
}
|
|
show() {
|
|
g.setFont(this.font_name,this.font_size);
|
|
g.setColor(this.colour[0],this.colour[1],this.colour[2]);
|
|
g.drawString(this.txt, this.x, this.y);
|
|
}
|
|
hide(){
|
|
g.setFont(this.font_name,this.font_size);
|
|
g.setColor(0,0,0);
|
|
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
|
|
diff_x = this.tgt_x - this.x;
|
|
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
|
|
diff_y = this.tgt_y - this.y;
|
|
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;
|
|
finished = finished_x & finished_y;
|
|
if(!finished){
|
|
this.timeoutId = setTimeout(this._doMove.bind(this), this.freq_millis);
|
|
} else if(this.finished_callback != null){
|
|
this.finished_callback.call();
|
|
this.finished_callback = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class DateFormatter {
|
|
/**
|
|
* A pure virtual class which all the other date formatters will
|
|
* inherit from.
|
|
* The name will be used to declare the date format when selected
|
|
* and the date formatDate methid will return the time formated
|
|
* to the lines of text on the screen
|
|
*/
|
|
name(){"no name";}
|
|
formatDate(date){
|
|
return ["","",""];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* English date formatting
|
|
*/
|
|
|
|
// English String Numbers
|
|
const numberStr = ["ZERO","ONE", "TWO", "THREE", "FOUR", "FIVE",
|
|
"SIX", "SEVEN","EIGHT", "NINE", "TEN",
|
|
"ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN",
|
|
"FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN",
|
|
"NINETEEN", "TWENTY"];
|
|
const tensStr = ["ZERO", "TEN", "TWENTY", "THIRTY", "FOURTY",
|
|
"FIFTY"];
|
|
|
|
function hoursToText(hours){
|
|
hours = hours % 12;
|
|
if(hours == 0){
|
|
hours = 12;
|
|
}
|
|
return numberStr[hours];
|
|
}
|
|
|
|
function numberToText(value){
|
|
word1 = '';
|
|
word2 = '';
|
|
if(value > 20){
|
|
tens = (value / 10 | 0);
|
|
word1 = tensStr[tens];
|
|
remainder = value - tens * 10;
|
|
if(remainder > 0){
|
|
word2 = numberStr[remainder];
|
|
}
|
|
} else if(value > 0) {
|
|
word1 = numberStr[value];
|
|
}
|
|
return [word1,word2];
|
|
}
|
|
|
|
class EnglishDateFormatter extends DateFormatter{
|
|
name(){return "English";}
|
|
formatDate(date){
|
|
hours_txt = hoursToText(date.getHours());
|
|
mins_txt = numberToText(date.getMinutes());
|
|
return [hours_txt,mins_txt[0],mins_txt[1]];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* French date formatting
|
|
*/
|
|
const frenchNumberStr = [ "ZERO", "UNE", "DEUX", "TROIS", "QUATRE",
|
|
"CINQ", "SIX", "SEPT", "HUIT", "NEUF", "DIX",
|
|
"ONZE", "DOUZE", "TREIZE", "QUATORZE","QUINZE",
|
|
"SEIZE", "DIX SEPT", "DIX HUIT","DIX NEUF", "VINGT",
|
|
"VINGT ET UN", "VINGT DEUX", "VINGT TROIS",
|
|
"VINGT QUATRE", "VINGT CINQ", "VINGT SIX",
|
|
"VINGT SEPT", "VINGT HUIT", "VINGT NEUF"
|
|
];
|
|
|
|
function frenchHoursToText(hours){
|
|
hours = hours % 12;
|
|
if(hours == 0){
|
|
hours = 12;
|
|
}
|
|
return frenchNumberStr[hours];
|
|
}
|
|
|
|
function frenchHeures(hours){
|
|
if(hours % 12 == 1){
|
|
return 'HEURE';
|
|
} else {
|
|
return 'HEURES';
|
|
}
|
|
}
|
|
|
|
class FrenchDateFormatter extends DateFormatter {
|
|
constructor() {
|
|
super();
|
|
}
|
|
name(){return "French";}
|
|
formatDate(date){
|
|
hours = frenchHoursToText(date.getHours());
|
|
heures = frenchHeures(date.getHours());
|
|
mins = date.getMinutes();
|
|
if(mins == 0){
|
|
if(hours == 0){
|
|
return ["MINUIT", "",""];
|
|
} else if(hours == 12){
|
|
return ["MIDI", "",""];
|
|
} else {
|
|
return [hours, heures,""];
|
|
}
|
|
} else if(mins == 30){
|
|
return [hours, heures,'ET DEMIE'];
|
|
} else if(mins == 15){
|
|
return [hours, heures,'ET QUERT'];
|
|
} else if(mins == 45){
|
|
next_hour = date.getHours() + 1;
|
|
hours = frenchHoursToText(next_hour);
|
|
heures = frenchHeures(next_hour);
|
|
return [hours, heures,"MOINS",'LET QUERT'];
|
|
}
|
|
if(mins > 30){
|
|
to_mins = 60-mins;
|
|
mins_txt = frenchNumberStr[to_mins];
|
|
next_hour = date.getHours() + 1;
|
|
hours = frenchHoursToText(next_hour);
|
|
heures = frenchHeures(next_hour);
|
|
return [ hours, heures , "MOINS", mins_txt ];
|
|
} else {
|
|
mins_txt = frenchNumberStr[mins];
|
|
return [ hours, heures , mins_txt ];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Japanese date formatting
|
|
*/
|
|
const japaneseHourStr = [ "ZERO", "ICHII", "NI", "SAN", "YO",
|
|
"GO", "ROKU", "SHICHI", "HACHI", "KU", "JUU",
|
|
'JUU ICHI', 'JUU NI'];
|
|
const tensPrefixStr = [ "",
|
|
"JUU",
|
|
'NIJUU',
|
|
'SAN JUU',
|
|
'YON JUU',
|
|
'GO JUU'];
|
|
|
|
const japaneseMinuteStr = [ ["", "PUN"],
|
|
["IP","PUN" ],
|
|
["NI", "FUN"],
|
|
["SAN", "PUN"],
|
|
["YON","FUN"],
|
|
["GO", "HUN"],
|
|
["RO", "PUN"],
|
|
["NANA", "FUN"],
|
|
["HAP", "PUN"],
|
|
["KYU","FUN"],
|
|
["JUP", "PUN"]
|
|
];
|
|
|
|
function japaneseHoursToText(hours){
|
|
hours = hours % 12;
|
|
if(hours == 0){
|
|
hours = 12;
|
|
}
|
|
return japaneseHourStr[hours];
|
|
}
|
|
|
|
function japaneseMinsToText(mins){
|
|
if(mins == 0){
|
|
return ["",""];
|
|
} else if(mins == 30)
|
|
return ["HAN",""];
|
|
else {
|
|
units = mins % 10;
|
|
mins_txt = japaneseMinuteStr[units];
|
|
tens = mins /10 | 0;
|
|
if(tens > 0){
|
|
tens_txt = tensPrefixStr[tens];
|
|
return [tens_txt + ' ' + mins_txt[0], mins_txt[1]];
|
|
} else {
|
|
return [mins_txt[0], mins_txt[1]];
|
|
}
|
|
}
|
|
}
|
|
|
|
class JapaneseDateFormatter extends DateFormatter {
|
|
constructor() {
|
|
super();
|
|
}
|
|
name(){return "Japanese (Romanji)";}
|
|
formatDate(date){
|
|
hours_txt = japaneseHoursToText(date.getHours());
|
|
mins_txt = japaneseMinsToText(date.getMinutes());
|
|
return [hours_txt,"JI", mins_txt[0], mins_txt[1] ];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The Watch Display
|
|
*/
|
|
|
|
// a list of display rows
|
|
let row_displays = [
|
|
new ShiftText(240,60,'',"Vector",40,10,10,40,[1,1,1]),
|
|
new ShiftText(240,100,'',"Vector",20,10,10,50,[0.85,0.85,0.85]),
|
|
new ShiftText(240,120,'',"Vector",20,10,10,60,[0.85,0.85,0.85]),
|
|
new ShiftText(240,140,'',"Vector",20,10,10,70,[0.85,0.85,0.85])
|
|
];
|
|
|
|
// a list of the formatters to cycle through
|
|
let date_formatters = [
|
|
new EnglishDateFormatter(),
|
|
new FrenchDateFormatter(),
|
|
new JapaneseDateFormatter()
|
|
];
|
|
|
|
// current index of the date formatter to display
|
|
let date_formatter_idx = 0;
|
|
let date_formatter = date_formatters[date_formatter_idx];
|
|
|
|
// The small display at the top which announces the date format
|
|
let format_name_display = new ShiftText(55,0,'',"Vector",10,1,1,50,[1,1,1]);
|
|
|
|
function changeFormatter(){
|
|
date_formatter_idx += 1;
|
|
if(date_formatter_idx >= date_formatters.length){
|
|
date_formatter_idx = 0;
|
|
}
|
|
console.log("changing to formatter " + date_formatter_idx);
|
|
date_formatter = date_formatters[date_formatter_idx];
|
|
reset_clock();
|
|
draw_clock();
|
|
// now announce the formatter by name
|
|
format_name_display.setTextYPosition(date_formatter.name(),-10);
|
|
format_name_display.moveToY(15);
|
|
// and then move back
|
|
format_name_display.onFinished(
|
|
function(){
|
|
format_name_display.moveToY(-10);
|
|
}
|
|
);
|
|
}
|
|
|
|
function reset_clock(){
|
|
//console.log("reset_clock");
|
|
var i;
|
|
for (i = 0; i < row_displays.length; i++) {
|
|
row_displays[i].reset();
|
|
}
|
|
}
|
|
|
|
function draw_clock(){
|
|
//console.log("draw_clock");
|
|
date = new Date();
|
|
rows = date_formatter.formatDate(date);
|
|
var i;
|
|
for (i = 0; i < rows.length; i++) {
|
|
display = row_displays[i];
|
|
txt = rows[i];
|
|
display_row(display,txt);
|
|
}
|
|
// If the dateformatter has not returned enough
|
|
// rows then treat the reamining rows as empty
|
|
for (j = i; j < row_displays.length; j++) {
|
|
display = row_displays[j];
|
|
display_row(display,'');
|
|
}
|
|
//console.log(date);
|
|
}
|
|
|
|
function display_row(display,txt){
|
|
if(display.txt == ''){
|
|
display.setTextXPosition(txt,240);
|
|
display.moveToX(20);
|
|
} else if(txt != display.txt){
|
|
display.moveToX(-100);
|
|
display.onFinished(
|
|
function(){
|
|
display.setTextXPosition(txt,240);
|
|
display.moveToX(20);
|
|
}
|
|
);
|
|
} else {
|
|
display.setTextXPosition(txt,20);
|
|
}
|
|
}
|
|
|
|
// The interval reference for updating the clock
|
|
let intervalRef = null;
|
|
|
|
function clearTimers(){
|
|
if(intervalRef) {
|
|
clearInterval(intervalRef);
|
|
intervalRef = null;
|
|
}
|
|
}
|
|
|
|
function startTimers(){
|
|
let date = new Date();
|
|
let secs = date.getSeconds();
|
|
let nextMinuteStart = 60 - secs;
|
|
//console.log("scheduling clock draw in " + nextMinuteStart + " seconds");
|
|
setTimeout(scheduleDrawClock,nextMinuteStart * 1000);
|
|
draw_clock();
|
|
}
|
|
|
|
function scheduleDrawClock(){
|
|
//console.log("scheduleDrawClock");
|
|
if(intervalRef) clearTimers();
|
|
intervalRef = setInterval(draw_clock, 60*1000);
|
|
draw_clock();
|
|
}
|
|
|
|
Bangle.on('lcdPower', (on) => {
|
|
if (on) {
|
|
console.log("lcdPower: on");
|
|
Bangle.drawWidgets();
|
|
reset_clock();
|
|
startTimers();
|
|
} else {
|
|
console.log("lcdPower: off");
|
|
reset_clock();
|
|
clearTimers();
|
|
}
|
|
});
|
|
Bangle.on('faceUp',function(up){
|
|
//console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn());
|
|
if (up && !Bangle.isLCDOn()) {
|
|
//console.log("faceUp and LCD off");
|
|
clearTimers();
|
|
Bangle.setLCDPower(true);
|
|
}
|
|
});
|
|
|
|
g.clear();
|
|
Bangle.loadWidgets();
|
|
Bangle.drawWidgets();
|
|
startTimers();
|
|
// Show launcher when middle button pressed
|
|
setWatch(Bangle.showLauncher, BTN2,{repeat:false,edge:"falling"});
|
|
setWatch(changeFormatter, BTN1,{repeat:true,edge:"falling"});
|