Implementing separate locked/unlocked faces on js2

Posted on
Page
of 2
/ 2
Next
  • As per the title, I'm working on a clock that has different color schemes based on whether the bangle.js2 is locked or unlocked. I'll be drawing strings, filled polygons, and circles, as well as loading a background image from heatshrink, all depending on lock state.

    For the sake of best practices, ram usage, etc. I'm wondering if I should have two different conditions within the same function, and using if(Bangle.isLocked()) within that function to decide at each updateinterval what background image to load, FG and BG colors, and what to draw... or should I have two separate functions that are called/killed depending on state? If the latter, how do I kill a function that is currently running?

  • I did a test using the first method, and it works, although it waits until the setInterval rate, so the backlight comes on, but it takes up to a second to switch to the other face. I'll have to figure out how to have the change in lock/unlock state trigger an immediate reset of the interval so the change is instantaneous.

  • To have immediate update you can e.g. use code like this:

    Bangle.on('lock', on => {
      // ... draw using isLocked ...
    });
    
  • Also I prefer using setTimeout instead of setInterval so that I can draw exactly when second changes and not e.g. 700 ms "too late". (This isn't that significant with 1 second interval, but this same code can be used also with 1 minute interval and there I definitely don't want to draw e.g. 40 seconds "too late".)

    For example in following example you can call draw whenever you want and it will first redraw and then (re)queue next draw call. If there is already draw queued it's first cleared with clearTimeout.

    function draw() {
      // ... draw using isLocked ...
    
      queueDraw();
    }
    
    var drawTimeout;
    function queueDraw() {
      if (drawTimeout) clearTimeout(drawTimeout);
      drawTimeout = setTimeout(() => {
        drawTimeout = undefined;
        draw();
      }, 1000 - (Date.now() % 1000));
    }
    
    Bangle.on('lock', on => {
      draw();
    });
    
    draw();
    
  • I think it might be safer to just have two draw functions rather than trying to decide which one to queue (which I think is what you're asking?). So use @malaire's code but with:

    function draw() {
      if (unlocked) draw1() else draw2();
      queueDraw();
    }
    

    as well as loading a background image from heatshrink, all depending on lock state.

    Not quite sure what you have in mind here, but if you store the background image as a file (uncompressed), drawImage can draw that file directly without ever loading it into RAM - it's likely to be faster and use less memory than trying to use heatshrink.

  • Great ideas, thanks for the tips folks!
    @Gordon that did answer my question about choosing two separate draw functions, and good to know more about how the background image files load. Is there a preferred file type? Gif? Png?

  • Is there a preferred file type? Gif? Png?

    You have to convert to its preferred (uncompressed) image format with the Image Converter first since Bangle doesn't include gif/png decoders. Easiest to just upload using the Web IDE's Storage menu first, then when you're ready to put it on the app store, download the files to a fileimage.img file.

  • Thanks, I did upload them through the IDE, and since they're showing up, I'm assuming that's the uncompressed format, even though my code is still calling for glowface.gif and so on?

    Here's what I'm working on for that end segment:

    var drawTimeout;
    function queueDraw() {
      if (drawTimeout) clearTimeout(drawTimeout);
      drawTimeout = setTimeout(() => {
        drawTimeout = undefined;
        if(Bangle.isLocked()){
        daylight();
        } else {
        nightglow(); 
        }
      }, 250 - (Date.now() % 250));
    }
    queueDraw();
    Bangle.on('lock', on => {
      daylight();
    });
    Bangle.on('lock', off => {
      nightglow();
    });
    

    So my goal was to do a face that switches to a "glow in the dark" theme when the screen is unlocked, and back to a daylight theme when locked. I'm still seeing about a half second delay between switching modes, so when I'm in the dark and unlock the screen, I see the daylight style before the glow theme kicks in, which I'd like to eliminate if possible.

    Also I'm trying out a second hand that actually moves every quarter second in smaller increments, just for cosmetic effect really. It's working fine, but I'm wondering what the ramifications of the more frequent refresh rate would be in terms of power usage, RAM, and wear on the LCD and processor.

  • As Gordon said, don't use Bangle.isLocked() in queueDraw but in draw (btw that was also what I meant in my post with "draw using isLocked" comment, but I guess I should've spelled that more clearly).

    Also having Bangle.on('lock', off => { makes no sense - just use single event.

  • Ah, ok I see now where I went wrong. I misunderstood that Gordon's code was meant to be placed in the main draw function, I was confused whether it was meant to replace of part of yours, malaire. One thing I don't fully understand is why we need Bangle.on('lock', on => { draw(); }); if we're going to call the draw function right afterward anyway.
    I tried removing the former, and it seems to work just the same.

    Anyway, here's what I have now:

    function draw(){
      if(Bangle.isLocked()){
        daylight();
        } else {
        nightglow();
        }
    }
    
    var drawTimeout;
    function queueDraw() {
      if (drawTimeout) clearTimeout(drawTimeout);
      drawTimeout = setTimeout(() => {
        drawTimeout = undefined;
        draw();
      }, 250 - (Date.now() % 250));
    }
    
    Bangle.on('lock', on => {
      draw();
    });
    
    draw();
    

    Calling queuedraw() at the end of the draw() function resulted in a frozen clock face, so I moved it to the end of my dalight() and nightglow() functions and that got it ticking again. Hopefully I didn't miss anything else and have implemented everything correctly. I'm still seeing the lag when changing images, so perhaps I'll rethink using the lock as the method of changing modes. Either way, I'm learning a lot.

  • One thing I don't fully understand is why we need Bangle.on('lock', on => { draw(); });

    That creates an event which calls draw every time lock state changes, i.e. when watch is locked or unlocked. This is used so that draw is done immediately after state change. Without this draw would happen at next queued draw, i.e. with up to 250 ms delay with your current 250ms interval.

    ... if we're going to call the draw function right afterward anyway.

    That draw(); at end is so that first draw when app starts is immediate. This also starts queued draws when queueDraw is called during that draw.

    I'm still seeing the lag when changing images

    I don't understand why that would happen. Can you show your daylight() and nightglow() functions?

  • Of course! Here is my entire code:

    const xc=88, yc=88;
    var twoPi  = 2*Math.PI;
      var Pi     = Math.PI;
      var halfPi = Math.PI/2;
    
    function daylight() {
      var d = new Date();
      var h = d.getHours() % 12 || 12;
      //var time = (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
      var HoursAngle   = (d.getHours()+(d.getMinutes()/60))/12 * twoPi - Pi;
      var MinutesAngle = (d.getMinutes()/60) * twoPi - Pi;
      //var SecondsAngle = (d.getSeconds()/60) * twoPi - Pi;
      var SecondsAngle = (d.getSeconds()/60+d.getMilliseconds()/6­0000) * twoPi - Pi;
      g.reset();
      g.clear();
      g.drawImage(require("Storage").read("Fac­eDaylight.gif"));
      //DateWindow
      g.setFont("6x8");
      g.setFontAlign(0,0);
      g.setColor(0,0,0);
      g.drawString(d.toISOString().substr(8,2)­, 138, 88, 0);
      g.drawString(d.toISOString().substr(8,2)­, 139, 88, 0);
      g.drawImage(require("Storage").read("Hou­rHandDay.gif"),xc,yc,{scale:1,rotate:Hou­rsAngle});
      g.drawImage(require("Storage").read("Min­HandDay.gif"),xc,yc,{scale:1,rotate:Minu­tesAngle});
      g.drawImage(require("Storage").read("Sec­HandDay.gif"),xc,yc,{scale:1,rotate:Seco­ndsAngle});
      queueDraw();
    }
    
    function nightglow() {
      var d = new Date();
      var h = d.getHours() % 12 || 12, m = d.getMinutes(), yyyy = d.getFullYear(), mm = d.getMonth(), dd = d.getDate();
      //var time = (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
      var HoursAngle   = (d.getHours()+(d.getMinutes()/60))/12 * twoPi - Pi;
      var MinutesAngle = (d.getMinutes()/60) * twoPi - Pi;
      //var SecondsAngle = (d.getSeconds()/60) * twoPi - Pi;
      var SecondsAngle = (d.getSeconds()/60+d.getMilliseconds()/6­0000) * twoPi - Pi;
      g.reset();
      g.clear();
      g.drawImage(require("Storage").read("Fac­eGlow.gif"));
      //DateWindow
      g.setFont("6x8");
      g.setFontAlign(0,0);
      g.setColor(0,0,0);
      g.drawString(dd, 137, 88, 0);
      g.drawString(dd, 138, 88, 0);
      g.drawImage(require("Storage").read("Hou­rHandGlow.gif"),xc,yc,{scale:1,rotate:Ho­ursAngle});
      g.drawImage(require("Storage").read("Min­HandGlow.gif"),xc,yc,{scale:1,rotate:Min­utesAngle});
      g.drawImage(require("Storage").read("Sec­HandGlow.gif"),xc,yc,{scale:1,rotate:Sec­ondsAngle});
      queueDraw();
    }
    
    function draw(){
      if(Bangle.isLocked()){
        daylight();
        } else {
        nightglow();
        }
    }
    
    var drawTimeout;
    function queueDraw() {
      if (drawTimeout) clearTimeout(drawTimeout);
      drawTimeout = setTimeout(() => {
        drawTimeout = undefined;
        draw();
      }, 250 - (Date.now() % 250));
    }
    
    Bangle.on('lock', on => {
      setTimeout(0);
      draw();
    });
    
    draw();
    
  • ... deleted ...

  • I tried adding the '''setTimeout(0);''' to the '''Bangle.on('lock', on=> {''' section to speed things up, not sure if that makes sense, but it didn't seem to make a difference...

  • Sorry, I accidentally posted yesterday's code, then deleted. The above post shows the entirety of today's code

  • For testing purposes, here are the nightglow images...


    4 Attachments

    • SecHandGlow.gif
    • MinHandGlow.gif
    • HourHandGlow.gif
    • FaceGlow.gif
  • ...and the daylight images


    4 Attachments

    • SecHandDay.gif
    • MinHandDay.gif
    • HourHandDay.gif
    • FaceDaylight.gif
  • That code looks correct except for setTimeout(0); which does nothing I think.

    I havn't used images so I wonder if reading them is too slow here. You could try code like this instead so you read images once when app starts and not every time it's drawn (this code is replacement for your daylight function):

    var faceDaylight = require("Storage").read("FaceDaylight.gi­f");
    var hourHandDay = require("Storage").read("HourHandDay.gif­");
    var minHandDay = require("Storage").read("MinHandDay.gif"­);
    var secHandDay = require("Storage").read("SecHandDay.gif"­);
    
    function daylight() {
      var d = new Date();
      var h = d.getHours() % 12 || 12;
      //var time = (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
      var HoursAngle   = (d.getHours()+(d.getMinutes()/60))/12 * twoPi - Pi;
      var MinutesAngle = (d.getMinutes()/60) * twoPi - Pi;
      //var SecondsAngle = (d.getSeconds()/60) * twoPi - Pi;
      var SecondsAngle = (d.getSeconds()/60+d.getMilliseconds()/6­0000) * twoPi - Pi;
      g.reset();
      g.clear();
      g.drawImage(faceDaylight);
      //DateWindow
      g.setFont("6x8");
      g.setFontAlign(0,0);
      g.setColor(0,0,0);
      g.drawString(d.toISOString().substr(8,2)­, 138, 88, 0);
      g.drawString(d.toISOString().substr(8,2)­, 139, 88, 0);
      g.drawImage(hourHandDay,xc,yc,{scale:1,r­otate:HoursAngle});
      g.drawImage(minHandDay,xc,yc,{scale:1,ro­tate:MinutesAngle});
      g.drawImage(secHandDay,xc,yc,{scale:1,ro­tate:SecondsAngle});
      queueDraw();
    }
    

    And similar change for nightglow function.

  • Ah, so does naming the variables at the beginning only load them once? I wasn't sure if it was just using the variable as a shortcut to calling it out frame by frame anyway. I'll give this a try asap...

  • Would I be better off naming '''var d = new Date();''' and such at the beginning, outside of my draw functions? I figured they needed to be inside the function so they'd update every time the function is repeated

  • Okay.... here's what I have:

    const xc=88, yc=88;
    var twoPi  = 2*Math.PI;
    var Pi     = Math.PI;
    var halfPi = Math.PI/2;
    //daylight_images
    var faceDaylight = require("Storage").read("FaceDaylight.gi­f");
    var hourHandDay = require("Storage").read("HourHandDay.gif­");
    var minHandDay = require("Storage").read("MinHandDay.gif"­);
    var secHandDay = require("Storage").read("SecHandDay.gif"­);
    //nightglow_images
    var faceGlow = require("Storage").read("FaceGlow.gif");­
    var hourHandGlow = require("Storage").read("HourHandGlow.gi­f");
    var minHandGlow = require("Storage").read("MinHandGlow.gif­");
    var secHandGlow = require("Storage").read("SecHandGlow.gif­");
    //
    var drawTimeout;
    
    function daylight() {
      var d = new Date();
      var h = d.getHours() % 12 || 12;
      var HoursAngle   = (d.getHours()+(d.getMinutes()/60))/12 * twoPi - Pi;
      var MinutesAngle = (d.getMinutes()/60) * twoPi - Pi;
      var SecondsAngle = (d.getSeconds()/60+d.getMilliseconds()/6­0000) * twoPi - Pi;
      g.reset();
      g.clear();
      g.drawImage(faceDaylight);
      //DateWindow
      g.setFont("6x8");
      g.setFontAlign(0,0);
      g.setColor(0,0,0);
      g.drawString(d.toISOString().substr(8,2)­, 138, 88, 0);
      g.drawString(d.toISOString().substr(8,2)­, 139, 88, 0);
      g.drawImage(hourHandDay,xc,yc,{scale:1,r­otate:HoursAngle});
      g.drawImage(minHandDay,xc,yc,{scale:1,ro­tate:MinutesAngle});
      g.drawImage(secHandDay,xc,yc,{scale:1,ro­tate:SecondsAngle});
      queueDraw();
    }
    
    function nightglow() {
      var d = new Date();
      var h = d.getHours() % 12 || 12;
      var HoursAngle   = (d.getHours()+(d.getMinutes()/60))/12 * twoPi - Pi;
      var MinutesAngle = (d.getMinutes()/60) * twoPi - Pi;
      var SecondsAngle = (d.getSeconds()/60+d.getMilliseconds()/6­0000) * twoPi - Pi;
      g.reset();
      g.clear();
      g.drawImage(faceGlow);
      g.drawImage(hourHandGlow,xc,yc,{scale:1,­rotate:HoursAngle});
      g.drawImage(minHandGlow,xc,yc,{scale:1,r­otate:MinutesAngle});
      g.drawImage(secHandGlow,xc,yc,{scale:1,r­otate:SecondsAngle});
      queueDraw();
    }
    
    function draw(){
      if(Bangle.isLocked()){
        daylight();
        } else {
        nightglow();
        }
    }
    
    function queueDraw() {
      if (drawTimeout) clearTimeout(drawTimeout);
      drawTimeout = setTimeout(() => {
        drawTimeout = undefined;
        draw();
      }, 250 - (Date.now() % 250));
    }
    Bangle.on('lock', on => {
      draw();
    });
    draw();
    

    Still getting the tiniest delay, though I think it is better than it was.

  • Ah, so does naming the variables at the beginning only load them once?

    Any code outside functions is only executed once - when app starts.

    Would I be better off naming '''var d = new Date();''' and such at the beginning, outside of my draw functions?

    Usually it's better to keep variable inside function if that is possible, i.e. var d = new Date(); should be inside function.

    But those images might be an exception if loading them from storage is too slow.

  • I suppose an image loaded when app starts is held in RAM, but then I suppose holding that image isn't as heavy as repeatedly loading it AND holding it

  • Actually documentation of require("Storage").read says that it does NOT use RAM. It returns some kind of reference to the file data.

    https://www.espruino.com/Reference#l_Sto­rage_read

    (But in general using more RAM can be a downside of creating variables outside functions.)

  • Oh riiight, that's what Gordon was saying about the uncompressed files... If they were compressed it would require RAM to decompress and present them via heatshrink. I'm reminded of how illustrator files are smaller on disk because they're a collection of vectors, but can get pretty heavy when open because of having to be interpreted by gpu in real time, whereas a photoshop file that's larger on disk might run more quickly. So much to learn... Banglejs is a great intro to javascript for me, there's nothing better than a practical application!

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Implementing separate locked/unlocked faces on js2

Posted by Avatar for NoMusicTuesdays @NoMusicTuesdays

Actions