Swiss Federal Railway Clock modeled w/ Circular Gauges

Posted on
  • Motivated by this traed about how to show step counter graphically as a progress indicator in the shape of half of a circle, I ventured into building a JS prototype / 'class' of a Circular Gauge. The Gauge can start at any angle, end at any angle and can be a thin or fat arc in two segments of different colors, one representing the value and the other one a 'filler' up to the max value.

    To show the gauge alive, I animated the values. Since the arc can be fat to the extent of the outer radius and 360 degrees, it looks more like partial pie, and animated, the cuts look like moving hands of a clock. And that inspired me to do the Swiss Federal Railway Clock. Attached you see four snapshots that show the hand states in the last few and the first few seconds of a minute, and and two clips and an animated gif that show the behavior (Attribution: Uhrendesign Hans Hilfiker, Animation Hk kng, License: CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0) via Wikimedia Commons).

    The .mov clip is a screen recording of the clock in the BangleJS Emulator. The .mp4 is a 9s recording spanning the minute-change of the clock running on a BangleJS watch.

    The next post includes a brief description and the code.


    4 Attachments

  • What is so unique about this clock?

    Besides the modern, clean, very legible look - designed 70+ years ago by Hans Hilfiker - it also moves in a very unique way that makes it very noticeable when exactly a new minute brakes, almost like a shot that starts a race, but silent.

    The second hand is (was) run by a synchron electric motor by the AC frequency of the power grid, and geared the way that it runs the minute in about 59 seconds and then stops. A central clock then fired a pulse to all clocks to begin the next minute.In other words, the seconds in each minute are controlled by the AC power grid locally, where as the start of a new minute is centrally managed.

    • 1. In the first face of the picture the time looks like 35 minutes - purple arc - and :57 seconds - 'white pie'.
    • 2. When the second hand reaches :60 seconds - indicating the 36th minute is about to complete - it stops for about second - waiting for the minute pulse from the central clock.
    • 3. On the pulse, the mine hand jumps to the 36th minute and the second hand is at the begin of the race thru the next minute
    • 4. The second hand is about 4 seconds into the new minute... since it is the full round is done in 59 seconds, it is a tad less than 4 seconds... actually about 50ms less...

    Since at the time of the original implementation things were working mostly analog and continuously... In the BangleJS implementation the second hand is not continuously moving, nor does the hour hand. The second hand jumps by the second and the hour hand by every 12 minutes (the angle of a common clock face minute). Nicely shown is the jump form actual completion of the minute / 60 seconds, because the value of the 360 degrees Circular Gauge changes from 60 to 0. At this time also the minute hand jumps - and ever 12th minutes - the hours hand as well.

    And here is the code. When started, it takes the time from the current time. Since the start is usually during a minute and not at the begin, the rest of the minute is implemented as a timeout that that then sets the second hand value to 0 and starts starts the new minutes with an interval. The second hand circular gauge jumps every 984ms (983.3333 would be exact) as an interval and clears itself after reaching the value of 60.

    The other arcs are just residues from the development of the Circular Gauge... The code can already now be uploaded onto the BangleJS and it behaves nicely. At some time, the clock may make it into an actual, properly sized BangleJS clock face.

    // Swiss Federal Railway Clock implemented with
    // cgauge.js - circular gauge prototype / 'class'
    // allObjects - forum.espruino.com
    // 2020-11-28
    
    // left/top = x/y = 0/0
    
    // Circular Gauge cg0:
    // - visual: 270 degree clock-wise gauge starting mid left bottom quadrant
    // - graphics: ...starting mid 2nd quadrant ending mid 1st quadrant
    // - showing values from 0..300 clockwise with initial value of 100
    
    var cg0
      , cg1,cg1c,cg1f,cg1t,cg1h
      , cg2,cg2c,cg2f,cg2t,cg2h,cg2i,cg2j,cg2u,cg2k
      , cg3,cg3c,cg3f,cg3t,cg3h
      , cg4
      , cg5
      , cg6
      , b1w,b2w,b3w
      ;
    function run() {
      halt();
      cg0=new CGauge("cg0",0,0,300,[0,  1,  0],[1  ,0  ,0  ],135,270,null,120,140,100,96);
      cg1=new CGauge("cg1",0,0, 30,[1,  1,  0],[0  ,1  ,1  ],180,180,null, 80,212, 16,13);
      cg2=new CGauge("cg2",0,0, 60,[1,  1,  1],[0.5,0.5,0.5],270,360,null,120,142, 18, 1); // sec
      cg3=new CGauge("cg3",0,0, 30,[1,  0,  1],[0  ,0  ,1  ],180,180,null,160,212, 16, 0);
      cg4=new CGauge("cg4",0,0, 40,[1,  0,  1],[0.6,0.6,0.6],120,-60,null,120,  8, 70,64);
      cg5=new CGauge("cg5",0,0, 60,[1,0.5,  1],[0.5,0.5,0.5],270,360,null,120,142, 53,42); // min
      cg6=new CGauge("cg6",0,0, 60,[1,  1,0.5],[0  ,0  ,0  ],270,360,null,120,142, 28,22); // hour
      cg0.setVal(100,-1);
      cg1.setVal( 20,-1);
      cg2.setVal(  0,-1); cg5.setVal(  0,-1); cg6.setVal(  0,-1); // sec, min, hour
      cg2.tikL=0;         cg5.tikL=0;         cg6.tikL=0;         // nbo ticks
      cg3.setVal( 15,-1);
      cg4.setVal( 10,-1);
      drawAll();
      cg1c=2; cg1f=function(){ if (!cg1.setVal(cg1.val+cg1c)) cg1c=-cg1c; };
      cg1t=750;
      cg2c=1; cg2f=function(){ var v=cg2.val+cg2c;  // +1 - seconds
                       if (v>=60 && cg2h) cg2h=clearInterval(cg2h);
                       cg2.setVal(v); };
      cg2i=function(){ cg2j(); cg2k=setInterval(cg2j,cg2u); }; // finished min
      cg2j=function(){ cg2h=setInterval(cg2f,cg2t); cg2.setVal(0); // minutes
                       cg5.setVal((cg5.val+1)%60); 
                       if (cg5.val%12==0) cg6.setVal((cg6.val+1)%60); };
      cg2t=984;cg2u=60000;
      cg3c=5; cg3f=function(){ if (!cg3.setVal(cg3.val+cg3c)) cg3c=-cg3c; };
      cg3t=1700;
      if (!b1w) b1w=setWatch(cg1f,BTN1,{edge:"rising",repeat:true});
      if (!b2w) b2w=setWatch(cg2f,BTN2,{edge:"rising",repeat:true});
      if (!b3w) b3w=setWatch(cg3f,BTN3,{edge:"rising",repeat:true});
      cont();
    }
    function cont() { var d;
       cg1h=setInterval(cg1f,cg1t);
       d=new Date();
       cg2.setVal(d.getSeconds());
       cg5.setVal(d.getMinutes());
       cg6.setVal(d.getHours()%12*5+Math.floor(cg5.val/12));
       cg2k=(cg2.val!=0)?setTimeout(cg2i,cg2u-cg2.val*cg2t)
                        :setInterval(cg2j,cg2u);
       if (cg2.val<cg2.maxV) cg2h=setInterval(cg2f,cg2t);
       cg3h=setInterval(cg3f,cg3t);
    }
    function halt() {
      if (cg1h) cg1h=clearInterval(cg1h);
      if (cg2h) cg2h=clearInterval(cg2h);
      if (cg2k) cg2k=clearInterval(cg2k);
      if (cg3h) cg3h=clearInterval(cg3h);
    }
    
    function drawAll() { 
      g.clear(); setTimeout(function() {
          cg0.draw(1); cg1.draw(1);
          for (var i=1;i<=12;i++) { cg2.drawTick(cg2.x,cg2.y,cg5.rIn-5,
            (cg6.rOut+5)/(cg5.rIn-5),cg2.rad(270+i*30),[1,0.8,0.8]); }
          cg2.draw(1); cg5.draw(1); cg6.draw(1);
          cg3.draw(1); cg4.draw(1); }
        , 1000); }
    
    var p; // temp for prototype references
    function CGauge(id,val,minV,maxV,color,fColor,begDeg,degs,deg0,x,y,rOuter,rInner) {
      var _=0||this;
      _.mxXY=239;    // x, y max graph coord - defaults for BangleJS Graphics
      _.pps=2;       // 'pixel per segment'/jaggedness/graphical precision/resolution
      _.tikL=6;      // tick-length (set to 0 post construction for no ticks drawing)
      _.id=id;       // id of the circular gauge
      _.val=null;    // temporary, set at end of construction
      _.minV=minV;   // minimum value (arc all in fColor)
      _.maxV=maxV;   // maximum value (arc all in color)
      _.clr=color;   // color - as required by Graphics - for the value arc
      _.fClr=fColor; // color - as required by Graphics - for to complete the arc
      _.begD=begDeg; // 0 degrees: +x-Axis
      _.degs=degs;   // gauge full arc in degrees -/+ = counter/clockwise
      _.deg0=(deg0)?deg0:begDeg; // for 0/center value mark; falsy defaults to begDeg
      _.x=x;         // center x
      _.y=y;         // center y
      _.rOut=rOuter; // radius outer
      _.rIn=rInner;  // radius inner (optional)
      _.begR=_.rad(_.begD);                              // begin radian
      _.arcR=(_.degs==360)?Math.PI*2:_.rad(_.degs);      // arc rad used for sCnt only
      _.segR=(Math.PI/(4/_.pps)/_.rOut)*((degs>0)?1:-1); // segment radian
      _.sCnt=Math.round(Math.abs(_.arcR/_.segR));        // segment count in arc
      _.cUp=[];                                          // clean up vertices 
      _.setVal(val,-1); // set value only
    } p=CGauge.prototype;
    p.setVal=function(v,opt) { // --- set min/max adj'd val, draw != && opt=0 || opt>0; 
      var chd = (v=(v<this.minV)?this.minV:(v>this.maxV)?this.maxV:v)!=this.val; // ret
      if (opt<0) { this.val=v; // update value only, NO drawing
      } else if (v!=this.val||opt>0) { this.val=v; this.draw(opt); }
      return chd; };
    p.draw=function(o) { // --- draw circular gauge (o-pt w/ extras, such as 0-tick)
      var s=this.sCnt,v=Math.round(s/(this.maxV-this.minV)*this.val),h=!!this.rIn,vs;
      if (o) { if (this.tikL) this.drawTicks(h); } // console.log(this.id,this.val,v,s,this.cUp);
      g.setColor(0,0,0); while (this.cUp.length) g.drawLine.apply(g,this.cUp.pop());
      if (v<s) g.setColor.apply(g,this.fClr).drawPoly(this._pvs(v,s,0),h);
      vs=this._pvs(0,v,1);
      g.setColor.apply(g,this.clr).drawPoly(vs,h); };
    p.drawTicks=function(h) { // --- draw ticks, begin and end and 0-tick
      var x=this.x,y=this.y,rTO=(h)?this.rIn:this.rOut,rTI=rTO-this.tikL,bR=this.begR
        , eR=bR+this.sCnt*this.segR,rTS=((rTI<0)?0:rTI)/rTO; // console.log(this.id,rTO,rTI,rTS);
      this.drawTick(x,y,rTO,rTS,eR,this.fClr);this.drawTick(x,y,rTO,rTS,bR,this.clr);
      if (this.deg0!=this.begD) this.drawTick(x,y,rTO,rTS,bR,this.clr); };
    p.drawTick=function(x,y,t,s,r,c) { // --- draw tick;
      var vX=t*Math.cos(r),vY=t*Math.sin(r); g.setColor.apply(g,c); g.drawLine(
        Math.round(x+vX),Math.round(y+vY),Math.round(x+vX*s),Math.round(y+vY*s)); };
    p._pvs=function(f,t,c) { // --- calc polygon vertices from..to
      var x=this.x, y=this.y, rO=this.rOut, rI=this.rIn, bR=this.begR, sR=this.segR
        , l=(t-f+1)*2*((rI)?2:1) // len of array for vertices (double w/ inner radius
        , v=((this.mxXY<=355) ? new Uint8Array(l) : new Uint16Array(l)) // vertices array
        , s=f-1  // segment index 
        , i=-1,j // vertices array index (running and 'turn around'/last outer)
        , r      // radian
        ; // console.log(x,y,rO,rI,bR,sR,f,t,s,i);
      while(++s<=t) { r=bR+s*sR;
        v[++i]=Math.round(x+rO*Math.cos(r));
        v[++i]=Math.round(y+rO*Math.sin(r)); } // console.log(this.id,s,r,v[i-1],v[i]); }
      if (rI) { j=i;
        while (--s>=f) { r=bR+s*sR;
          v[++i]=Math.round(x+rI*Math.cos(r));
          v[++i]=Math.round(y+rI*Math.sin(r)); }
        this.cUp.push((c)?v.slice(j-1,j+3):v.slice(0,2).concat(v.slice(-2)));
      } // console.log(this.id,c,j,i,v.slice(0,4),this.cUp);
      return v; };
    p.rad=function(degrs) { return 2*Math.PI*(degrs%360)/360; }; // radian <-- degrees
    // p.v=function(x,y,r,rO,ri) { // <--- vertice
    //  return [
    
    function r() { run(); }
    function h() { halt(); }
    function c() { cont(); }
    
    setTimeout(run,999);
    
  • Optimized the CGauge to draw by default only delta-value of the arc, in other words, the part of the arc that changes color, from value color to filler color or background color, when the value changes (and options omitted). To always draw the full arc(s), set draw arc delta only .dado=0 (or fals) after construction.

    Optimized drawings are still almost perfect. A few missing pixels because of overlaps:

    • when CGauge's full arc > 270 degrees and 'smaller' radiuses
    • when drawing a single line of a polygon because it shares points of previous and next line when new value is less then previously drawn value. (could be fixed by clearing one more value segment and redrawing it, requiring 12 trigonometric calculations... and some additional funky looking code. :[ So I did let it go... at least for now... ;) ~~~

    Options allow to force a complete drawing, including customizable/overridable .drawXtras(); (extras include by default the begin, end and optional 0-tick. Setting the tick length .tikL=0 after construction spresses tick drawing).

    Below the test code and CGauge prototype/'class'. Main change in the CGauge is in .draw(...) method - lines 133..142- where it is determined whether the the:

    • complete arc has to be drawn (because of options or .dado (draw arc delta only) values)
    • value increased and value arc has to be extended (draw value arc over begin of filler arc)
    • value decreased and value arc has to be shrinked (draw filler arc over end of value arc)

    Furthermore, the calculation of the filler arc vertices and with that the its drawing begin and end are reversed in order to handle the traverse:

    • The filler arc begins at the highest possible value point on the outer radius and goes 'down' to the value position, then - when inner radius is defined - switches to inner radius and goes back up to maximum possible value position.
    • The value arc begins at the lowest value position on the outer radius and goes 'up' to the value position, then - when inner radius defined - switches to inner radius and goes back 'down' to lowest value position. This creates the interesting case for the optimized drawing when the changes to a lower value: The traversing value - or a dot - has to be drawn with the value color at the value position after the filler expansion just draw it with the filler color.

    PS: Buttons have been redefined:

    • BTN1 advances small, bottom left (yellow) CGauge by one change value step (cg1c)
    • BTN2 halts execution of timers / intervals
    • BTN3 continues - resumes - execution

      // Swiss Federal Railway Clock implemented with
      // cgauge.js - circular gauge prototype / 'class'
      // allObjects - forum.espruino.com
      // 2020-11-29
      
      // left/top = x/y = 0/0
      
      // Circular Gauge cg0:
      // - visual: 270 degree clock-wise gauge starting mid left bottom quadrant
      // - graphics: ...starting mid 2nd quadrant ending mid 1st quadrant
      // - showing values from 0..300 clockwise with initial value of 100
      
      var rng=0 // running
      , cg0,cg0c,cg0f,cg0t,cg0h // Circular Gauge 0 variables...
      , cg1,cg1c,cg1f,cg1t,cg1h
      , cg2,cg2c,cg2f,cg2t,cg2h,cg2i,cg2j,cg2u,cg2k
      , cg3,cg3c,cg3f,cg3t,cg3h
      , cg4,cg4c,cg4f,cg4t,cg4h
      , cg5
      , cg6
      , b1w,b2w,b3w // button watch handles
      ;
      function run() { if (rng) halt(); rng=1;
      cg0=new CGauge("cg0",0,0,300,[0,  1,  0],[1  ,0  ,0  ],135,270,null,120,140,100,96);
      cg1=new CGauge("cg1",0,0, 30,[1,  1,  0],[0  ,0  ,0  ],180,180,null, 80,212, 16,11);
      cg2=new CGauge("cg2",0,0, 60,[1,  1,  1],[0.5,0.5,0.5],270,360,null,120,142, 18, 1); // sec
      cg3=new CGauge("cg3",0,0, 30,[1,  0,  1],[0  ,0  ,1  ],180,180,null,160,212, 16, 0);
      cg4=new CGauge("cg4",0,0, 40,[1,  0,  1],[0.6,0.6,0.6],120,-60,null,120,  8, 70,64);
      cg5=new CGauge("cg5",0,0, 60,[0.4,0.7,1],[0.5,0.5,0.5],270,360,null,120,142, 53,42); // min
      cg6=new CGauge("cg6",0,0, 60,[1,  1,0.5],[0  ,0  ,0  ],270,360,null,120,142, 28,22); // hour
      cg0.setVal(100,-1);
      cg1.setVal( 20,-1);
      cg2.setVal(  0,-1); cg5.setVal(  0,-1); cg6.setVal(  0,-1); // sec, min, hour
      cg2.tikL=0;         cg5.tikL=0;         cg6.tikL=0;         // no ticks when opt2
      cg3.setVal( 15,-1);
      cg4.setVal( 30,-1);
      drawAll();
      cg0c= 12; cg0f=function(){ if (!cg0.setVal(cg0.val+cg0c)) cg0c=-cg0c; };
      cg0t=1200;
      cg1c=  3; cg1f=function(){ if (!cg1.setVal(cg1.val+cg1c)) cg1c=-cg1c; };
      cg1t=1900;
      cg2c=  1; cg2f=function(){ var v=cg2.val+cg2c; // +1 - seconds
                     if (v>=60 && cg2h) cg2h=clearInterval(cg2h);
                     cg2.setVal(v); };
      cg2i=function(){ cg2j(); cg2k=setInterval(cg2j,cg2u); }; // finished min
      cg2j=function(){ cg2h=setInterval(cg2f,cg2t); cg2.setVal(0); // minutes
                     cg5.setVal((cg5.val+1)%60); 
                     if (cg5.val%12==0) cg6.setVal((cg6.val+1)%60); };
      cg2t= 984; cg2u=60000;
      cg3c=5  ; cg3f=function(){ if (!cg3.setVal(cg3.val+cg3c)) cg3c=-cg3c; };
      cg3t=1700;
      cg4c=5  ; cg4f=function(){ if (!cg4.setVal(cg4.val+cg4c,1)) cg4c=-cg4c; };
      cg4t=3000;
      if (!b1w) b1w=setWatch(cg1f,BTN1,{edge:"rising",repeat:true});
      if (!b2w) b2w=setWatch(halt,BTN2,{edge:"rising",repeat:true});
      if (!b3w) b3w=setWatch(cont,BTN3,{edge:"rising",repeat:true});
      cont();
      }
      function cont() { var d; halt();
      cg0h=setInterval(cg0f,cg0t);
      cg1h=setInterval(cg1f,cg1t);
      d=new Date();
      cg2.setVal(d.getSeconds());
      cg5.setVal(d.getMinutes());
      cg6.setVal(d.getHours()%12*5+Math.floor(cg5.val/12));
      cg2k=(cg2.val!=0)?setTimeout(cg2i,cg2u-cg2.val*cg2t)
                      :setInterval(cg2j,cg2u);
      if (cg2.val<cg2.maxV) cg2h=setInterval(cg2f,cg2t);
      cg3h=setInterval(cg3f,cg3t);
      cg4h=setInterval(cg4f,cg4t);
      }
      function halt() {
      if (cg0h) cg0h=clearInterval(cg0h);
      if (cg1h) cg1h=clearInterval(cg1h);
      if (cg2h) cg2h=clearInterval(cg2h);
      if (cg2k) cg2k=clearInterval(cg2k);
      cg2.vLD=null; cg5.vLD=null; cg6.vLD=null;
      if (cg3h) cg3h=clearInterval(cg3h);
      if (cg4h) cg4h=clearInterval(cg4h);
      }
      
      function drawAll() {
      g.clear(); setTimeout(function() {
        cg0.draw(1,1);
        cg1.draw(1);
        for (var i=1;i<=12;i++) { cg2.drawTick(cg2.x,cg2.y,cg5.rIn-5,
          (cg6.rOut+5)/(cg5.rIn-5),cg2.rad(270+i*30),[1,0.8,0.8]); }
        cg2.draw(1); cg5.draw(1); cg6.draw(1);
        cg3.draw(1,1);
        cg4.draw(1,1); }
      , 1000); }
      
      var p; // temp for prototype references
      function CGauge(id,val,minV,maxV,color,fColor,begDeg,degs,deg0,x,y,rOuter,rInner) {
      var _=0||this;
      _.mxXY=239;     // x, y max graph coord - defaults for BangleJS Graphics
      _.pps=2;        // 'pixel per segment'/jaggedness/graphical precision/resolution
      _.tikL=6;       // tick-length (set to 0 post construction for no ticks drawing)
      _.dado=1;       // draw arc delta only  
      _.bClr=[0,0,0]; // background color
      _.id=id;        // id of the circular gauge
      _.val=null;     // temporary, set at end of construction
      _.minV=minV;    // minimum value (arc all in fColor)
      _.maxV=maxV;    // maximum value (arc all in color)
      _.clr=color;    // color - as required by Graphics - for the value arc
      _.fClr=fColor;  // color - as required by Graphics - for to complete the arc
      _.begD=begDeg;  // 0 degrees: +x-Axis
      _.degs=degs;    // gauge full arc in degrees -/+ = counter/clockwise
      _.deg0=(deg0)?deg0:begDeg; // for 0/center value mark; falsy defaults to begDeg
      _.x=x;          // center x
      _.y=y;          // center y
      _.rOut=rOuter;  // radius outer
      _.rIn=rInner;   // radius inner (optional)
      _.begR=_.rad(_.begD);                              // begin radian
      _.arcR=(_.degs==360)?Math.PI*2:_.rad(_.degs);      // arc rad used for sCnt only
      _.segR=(Math.PI/(4/_.pps)/_.rOut)*((degs>0)?1:-1); // segment radian
      _.sCnt=Math.round(Math.abs(_.arcR/_.segR));        // segment count in arc
      _.cUp=[];                                          // clean up vertices 
      _.vLD=null;       // (display/draw) value (v) last displayed/drawn
      _.setVal(val,-1); // set value only
      } p=CGauge.prototype;
      p.setVal=function(v,o1,o2) { // --- set min/max adj'd val, draw != && o1=0 || o1>0; 
      var chd = (v=(v<this.minV)?this.minV:(v>this.maxV)?this.maxV:v)!=this.val; // ret
      if (o1<0) { this.val=v; this.vLD=null; // update value only, NO drawing & never draw
      } else if (v!=this.val||o1>0||o2) { this.val=v; this.draw(o1,o2); }
      return chd; };
      p.draw=function(o1,o2) { // --- draw circular gauge (otp1:value, 2:ticks+extras)
      var s=this.sCnt,v=Math.round(s/(this.maxV-this.minV)*this.val),h=(this.rIn)?1:0
      , vL,vs,m;
      if (o2) { this.drawXtras(s,v,h,o1); } // console.log(this.id,this.val,v,s,o1,o2,this.cUp);
      if (o1!=-1) { if (h) { g.setColor.apply(g,this.bClr);
           while (this.cUp.length) g.drawLine.apply(g,this.cUp.pop()); }
      if (o1==1||!this.dado||(vL=this.vLD)==null) {
        if (v<s) g.setColor.apply(g,this.fClr).drawPoly(this._pvs(v,s,-1),h);
        g.setColor.apply(g,this.clr).drawPoly(this._pvs(0,v,1),h);
      } else if (v>vL) {
        g.setColor.apply(g,this.clr).drawPoly(this._pvs(vL,v,1),h&&vL==0);
      } else if (v<vL) {
        g.setColor.apply(g,this.fClr).drawPoly(vs=this._pvs(v,vL,-1),h&&vL==s);
        vs=(h)?vs.slice((m=vs.length/2-2),m+4):(m=vs.slice(-2)).concat(m);
        g.setColor.apply(g,this.clr);g.drawLine.apply(g,vs);
      } } this.vLD=v; };
      p.drawXtras=function(s,v,h,o1) { // --- draw extras... place holder for custom override
      if (this.tikL) this.drawTicks(h); }; // incl drawTicks() in custom override if needed 
      p.drawTicks=function(h) { // --- draw ticks, begin and end and 0-tick
      var x=this.x,y=this.y,rTO=(h)?this.rIn:this.rOut,rTI=rTO-this.tikL,bR=this.begR
      , eR=bR+this.sCnt*this.segR,rTS=((rTI<0)?0:rTI)/rTO; // console.log(this.id,rTO,rTI,rTS);
      this.drawTick(x,y,rTO,rTS,eR,this.fClr);this.drawTick(x,y,rTO,rTS,bR,this.clr);
      if (this.deg0!=this.begD) this.drawTick(x,y,rTO,rTS,bR,this.clr); };
      p.drawTick=function(x,y,t,s,r,c) { // --- draw tick x,y,radius,scale,radian,color
      var vX=t*Math.cos(r),vY=t*Math.sin(r); g.setColor.apply(g,c); g.drawLine(
      Math.round(x+vX),Math.round(y+vY),Math.round(x+vX*s),Math.round(y+vY*s)); };
      p._pvs=function(f,t,d) { // --- calc polygon vertices from..to in direction
      var x=this.x, y=this.y, rO=this.rOut, rI=this.rIn, bR=this.begR, sR=this.segR
      , l=(t-f+1)*2*((rI)?2:1) // len of array for vertices (double w/ inner radius
      , v=((this.mxXY<=355) ? new Uint8Array(l) : new Uint16Array(l)) // vertices array
      , s=f-1  // segment index 
      , i=-1,j // vertices array index (running and 'turn around'/last outer)
      , m=(d>0)?f:t,r // segmentRadian 'multiplier' (starting w/ f or t+1), radian
      ; // console.log(this.id,f,t,d,"|",x,y,rO,rI,bR,sR,m,s,i);
      while (++s<=t) { r=bR+m*sR; m+=d;
      v[++i]=Math.round(x+rO*Math.cos(r));
      v[++i]=Math.round(y+rO*Math.sin(r)); } // console.log(this.id,s,r,v[i-1],v[i]); }
      if (rI) { j=i;
      while (--s>=f) { m-=d; r=bR+m*sR;
        v[++i]=Math.round(x+rI*Math.cos(r));
        v[++i]=Math.round(y+rI*Math.sin(r)); }
      this.cUp.push(v.slice(j-1,j+3));
      } // console.log(this.id,d,j,i,v.slice(0,4),this.cUp);
      return v; };
      p.rad=function(degrs) { return 2*Math.PI*(degrs%360)/360; }; // radian <-- degrees
      
      function r() { run(); }
      function h() { halt(); }
      function c() { cont(); }
      
      setTimeout(run,999);
      
  • Nice, that looks really neat!

  • ...and here the eXtende Circular Gauges that allow mitch and match of filled and unfilled arcs... Fill is already extracted but I guess there may be more code streamlining possible. Attached is also a clip to show real behavior on the BanglJS watch. Since junk by by junk of 62 points - 31 segments - with 1 segment overlap is drawn, you can notice it in the large green-red 270 degree CGauge. Despite all gauges are animated, behavior seams time true. The clip shows the minute change.

    Drawing of gauges with mixed - filled and hallow - arcs require to first fill in background color, then outline and for the filled part lastly fill the segment - full or delta draw of the value and filler arg. Constructor has a few more argument to define the fill. Currently, the fill - other than background - has to be of the same color of the outline... adding more config on construction - in addition to the fill indicators - which could actually be the color object - and the optional background color, the constructor with flat argument becomes heavy. Providing a structured config object may be the answer here to reduce the number of arguments and get away from the constraint positional arguments pose.

    // Swiss Federal Railway Clock implemented with
    // CGaugeX.js - circular gauge prototype / 'class'
    // allObjects - forum.espruino.com
    // 2020-11-30
    
    // left/top = x/y = 0/0
    
    // Circular Gauge cg0:
    // - visual: 270 degree clock-wise gauge starting mid left bottom quadrant
    // - graphics: ...starting mid 2nd quadrant ending mid 1st quadrant
    // - showing values from 0..300 clockwise with initial value of 100
    
    var t=0   // test mode
      , rng=0 // running        // c:change/f:funcion/t:interval time/h:inteval handle 
      , cg0,cg0c,cg0f,cg0t,cg0h // Circular Gauge 0 variables... big one
      , cg1,cg1c,cg1f,cg1t,cg1h // small one lower left
      , cg2,cg2c,cg2f,cg2t,cg2h,cg2i,cg2j,cg2u,cg2k // center, seconds of clock
      , cg3,cg3c,cg3f,cg3t,cg3h // small on elower right
      , cg4,cg4c,cg4f,cg4t,cg4h // middle top inside bow of bit one
      , cg5                     // co-centric, outer around hr ticks, minutes of clock
      , cg6                     // co-centeric, around seconds insice hour ticks, hour of clock
      , b1w,b2w,b3w // button watch handles
      ;
    function run() { // --- run / startup application
      if (rng) halt(); rng=1; // prevent multiple timer instances for same function
      // --- construct gauges, no display/drawing yet - static setup
      cg0=new CGauge("cg0",0,0,300,[0,  1,  0],[1  ,0  ,0  ],135,270,null,120,140,100,96,0,1);
      cg1=new CGauge("cg1",0,0, 30,[1,  1,  0],[0  ,0  ,0  ],180,180,null, 80,212, 16,11,1,1);
      cg2=new CGauge("cg2",0,0, 60,[1,  1,  1],[0.5,0.5,0.5],270,360,null,120,142, 18, 1,1,0); // sec
      cg3=new CGauge("cg3",0,0, 30,[1,  0,  1],[0  ,0  ,1  ],180,180,null,160,212, 16, 0);
      cg4=new CGauge("cg4",0,0, 40,[1,  0,  1],[0.6,0.6,0.6],120,-60,null,120,  8, 70,64,1,1);
      cg5=new CGauge("cg5",0,0, 60,[0.4,0.7,1],[0.5,0.5,0.5],270,360,null,120,142, 53,42,1,0); // min
      cg6=new CGauge("cg6",0,0, 60,[1,  1,0.5],[0  ,0  ,0  ],270,360,null,120,142, 28,22,1,0); // hour
      cg0.setVal(100,-1);
      cg1.setVal( 20,-1);
      cg2.setVal(  0,-1); cg5.setVal(  0,-1); cg6.setVal(  0,-1); // sec, min, hour
      cg2.tikL=0;         cg5.tikL=0;         cg6.tikL=0;         // no ticks when opt2 on in draw
      cg3.setVal( 15,-1);
      cg4.setVal( 30,-1);
      drawAll(); // --- initial / first draw - not animated yet - static draw
      // --- setup anumiations with change value and function on timer and value events
      cg0c= 60; cg0f=function(){ if (!cg0.setVal(cg0.val+cg0c)) cg0c=-cg0c; };
      cg0t=1200;
      cg1c=  3; cg1f=function(){ if (!cg1.setVal(cg1.val+cg1c)) cg1c=-cg1c; };
      cg1t=1900;
      cg2c=  1; cg2f=function(){ var v=cg2.val+cg2c; // +1 - seconds
                       if (v>=60 && cg2h) cg2h=clearInterval(cg2h);
                       cg2.setVal(v); };
      cg2i=function(){ cg2j(); cg2k=setInterval(cg2j,cg2u); }; // finished min
      cg2j=function(){ cg2h=setInterval(cg2f,cg2t); cg2.setVal(0,1); // minutes
                       cg5.setVal((cg5.val+1)%60); 
                       if (cg5.val%12==0) cg6.setVal((cg6.val+1)%60,((cg6.val+1)%60==0)?1:0); };
      cg2t= 984; cg2u=60000; // milliseconds for second and for minute
      cg3c=5  ; cg3f=function(){ if (!cg3.setVal(cg3.val+cg3c)) cg3c=-cg3c; };
      cg3t=1700;
      cg4c=5  ; cg4f=function(){ if (!cg4.setVal(cg4.val+cg4c,1)) cg4c=-cg4c; };
      cg4t=3000;
      // hookup buttons
      if (!b1w) b1w=setWatch(cg1f,BTN1,{edge:"rising",repeat:true});
      if (!b2w) b2w=setWatch(halt,BTN2,{edge:"rising",repeat:true});
      if (!b3w) b3w=setWatch(cont,BTN3,{edge:"rising",repeat:true});
      cont(); // start animation
    }
    function cont() { var d; halt(); // --- continue, restart timers/...
       cg0h=setInterval(cg0f,cg0t);
       if (t) return;
       cg1h=setInterval(cg1f,cg1t);
       d=new Date();
       cg2.setVal(d.getSeconds());
       cg5.setVal(d.getMinutes());
       cg6.setVal(d.getHours()%12*5+Math.floor(cg5.val/12));
       cg2k=(cg2.val!=0)?setTimeout(cg2i,cg2u-cg2.val*cg2t)
                        :setInterval(cg2j,cg2u);
       if (cg2.val<cg2.maxV) cg2h=setInterval(cg2f,cg2t);
       cg3h=setInterval(cg3f,cg3t);
       cg4h=setInterval(cg4f,cg4t);
    }
    function halt() { // --- halt, clear timers/...
      if (cg0h) cg0h=clearInterval(cg0h);
      if (t) return;
      if (cg1h) cg1h=clearInterval(cg1h);
      if (cg2h) cg2h=clearInterval(cg2h);
      if (cg2k) cg2k=clearInterval(cg2k);
      // will enforce full draw on resume/continue after halt
      // to ensure correct watch display after break
      cg2.vLD=null; cg5.vLD=null; cg6.vLD=null;
      if (cg3h) cg3h=clearInterval(cg3h);
      if (cg4h) cg4h=clearInterval(cg4h);
    }
    
    function drawAll() { // --- draw all for initial draw
      g.clear(); setTimeout(function() {
          // g.setColor(0.3,0.3,0.3).fillRect(0,0,239,239);
          cg0.draw(1,1);
          if (t) return;
          cg1.draw(1);
          for (var i=1;i<=12;i++) { cg2.drawTick(cg2.x,cg2.y,cg5.rIn-5,
            (cg6.rOut+5)/(cg5.rIn-5),cg2.rad(270+i*30),[1,0.8,0.8]); }
          cg2.draw(1); cg5.draw(1); cg6.draw(1);
          cg3.draw(1,1);
          cg4.draw(1,1); }
        , 1000); }
    
    var p; // temp for prototype references
    function CGauge(id,val,minV,maxV,color,fColor,begDeg,degs,deg0
                   ,x,y,rOuter,rInner,fill,fFill,bgColor) {
      var _=0||this;
      _.mxXY=239;     // x, y max graph coord - defaults for BangleJS Graphics
      _.pps=2;        // 'pixel per segment'/jaggedness/graphical precision/resolution
      _.tikL=6;       // tick-length (set to 0 post construction for no ticks drawing)
      _.dado=1;       // draw arc delta only
      _.bClr=[0,0,0]; // background color (used as default)
      _.id=id;        // id of the circular gauge
      _.val=null;     // temporary, set at end of construction
      _.minV=minV;    // minimum value (arc all in fColor)
      _.maxV=maxV;    // maximum value (arc all in color)
      _.clr=color;    // color - as required by Graphics - for the value arc
      _.fClr=fColor;  // color - as required by Graphics - for to complete the arc
      _.begD=begDeg;  // 0 degrees: +x-Axis
      _.degs=degs;    // gauge full arc in degrees -/+ = counter/clockwise
      _.deg0=(deg0==null)?deg0:begDeg; // for 0/center value mark; null defaults to begDeg
      _.x=x;          // center x
      _.y=y;          // center y
      _.rOut=rOuter;  // radius outer
      _.rIn=rInner;   // radius inner (optional)
      _.fV=fill;      // fill value arc w/ color
      _.fF=fFill;     // fill filler arc w/ fColor
      _.bClr=(bgColor)?bgColor:this.bClr;                // opt bg color, defaults blk 
      _.begR=_.rad(_.begD);                              // begin radian
      _.arcR=(_.degs==360)?Math.PI*2:_.rad(_.degs);      // arc rad used for sCnt only
      _.segR=(Math.PI/(4/_.pps)/_.rOut)*((degs>0)?1:-1); // segment radian
      _.sCnt=Math.round(Math.abs(_.arcR/_.segR));        // segment count in arc
      _.cUp=[];                                          // clean up vertices 
      _.vLD=null;       // (display/draw) value (v) last displayed/drawn
      _.setVal(val,-1); // set value only
    } p=CGauge.prototype;
    p.setVal=function(v,o1,o2) { // --- set min/max adj'd val, draw != && o1=0 || o1>0; 
      var chd = (v=(v<this.minV)?this.minV:(v>this.maxV)?this.maxV:v)!=this.val; // ret
      if (o1<0) { this.val=v; this.vLD=null; // update value only, NO drawing & never draw
      } else if (v!=this.val||o1>0||o2) { this.val=v; this.draw(o1,o2); }
      return chd; };
    p.draw=function(o1,o2) { // --- draw circular gauge (otp1:value, 2:ticks+extras)
      var s=this.sCnt,v=Math.round(s/(this.maxV-this.minV)*this.val)
        , h=(this.rIn)?1:0,fV=!!this.fV,fF=!!this.fF,bC=this.bClr
        , vL,vs,m; // console.log(this.id,this.val,v,s,o1,o2,this.cUp);
      if (o2) { this.drawXtras(s,v,h,o1); }
      if (o1!=-1) { if (h&&(fV==fF)) { g.setColor.apply(g,bC);
             while (this.cUp.length) g.drawLine.apply(g,this.cUp.pop()); 
          } else { this.cUp=[]; }
        if (o1==1||!this.dado||(vL=this.vLD)==null) {
          if (v<s) { vs=this._pvs(v,s,-1,0);
            if (h&&!fF&&fV) { g.setColor.apply(g,bC); this.fSs(vs,vs.length); }
            g.setColor.apply(g,this.fClr).drawPoly(vs,h);
            if (h&&fF) this.fSs(vs,vs.length); vs=null; }
          vs=this._pvs(0,v,1,1);
          if (h&&!fV&&fF) { g.setColor.apply(g,bC); this.fSs(vs,vs.length); }
          g.setColor.apply(g,this.clr).drawPoly(vs,h);
          if (h&&fV) this.fSs(vs,vs.length); vs=null;
        } else if (v>vL) { vs=this._pvs(vL,v,1,1);
          if (h&&!fV&&fF) { g.setColor.apply(g,bC); this.fSs(vs,vs.length); }
          g.setColor.apply(g,this.clr).drawPoly(vs,h&&vL==0);
          if (h&&fV) this.fSs(vs,vs.length); vs=null;
        } else if (v<vL) { vs=this._pvs(v,vL,-1,1);
          if (h&&!fF&&fV) { g.setColor.apply(g,bC); this.fSs(vs,vs.length); }
          g.setColor.apply(g,this.fClr).drawPoly(vs,h&&vL==s);
          if (h&&fF) this.fSs(vs,vs.length);
          vs=(h)?vs.slice((m=vs.length/2-2),m+4):(m=vs.slice(-2)).concat(m);
          g.setColor.apply(g,this.clr);g.drawLine.apply(g,vs);
      } } this.vLD=v; };
    p.fSs=function(vs,vsL) { // --- fill
      if (vsL<64) { g.fillPoly(vs,1); // console.log("------",vsL/2);
      } else { var k=30,l=vsL/2,i=0,j=vsL-k,n; // console.log("start:",k,":",i,i+k,l,j,j+k);
        while (i<l) { // console.log(":",k,":",i,i+k,l,j,j+k);
          g.fillPoly(vs.slice(i,i+=k).concat(vs.slice(j,j+k)),1);
          if (i<l) { i-=2; if ((n=l-i)<30) k=n; j-=k-2;
            if (k==2) { k+=2; i-=2; j+=2; }
      } } } };
    p.drawXtras=function(s,v,h,o1) { // --- draw extras... place holder for custom override
      if (this.tikL) this.drawTicks(h); }; // incl drawTicks() in custom override if needed 
    p.drawTicks=function(h) { // --- draw ticks, begin and end and 0-tick
      var x=this.x,y=this.y,rTO=(h)?this.rIn:this.rOut,rTI=rTO-this.tikL,bR=this.begR
        , eR=bR+this.sCnt*this.segR,rTS=((rTI<0)?0:rTI)/rTO; // console.log(this.id,rTO,rTI,rTS);
      this.drawTick(x,y,rTO,rTS,eR,this.fClr);this.drawTick(x,y,rTO,rTS,bR,this.clr);
      if (this.deg0!=this.begD) this.drawTick(x,y,rTO,rTS,bR,this.clr); };
    p.drawTick=function(x,y,t,s,r,c) { // --- draw tick x,y,radius,scale,radian,color
      var vX=t*Math.cos(r),vY=t*Math.sin(r); g.setColor.apply(g,c); g.drawLine(
        Math.round(x+vX),Math.round(y+vY),Math.round(x+vX*s),Math.round(y+vY*s)); };
    p._pvs=function(f,t,d,c) { // --- calc polygon vertices from..to in direction
      var x=this.x, y=this.y, rO=this.rOut, rI=this.rIn, bR=this.begR, sR=this.segR
        , l=(t-f+1)*2*((rI)?2:1) // len of array for vertices (double w/ inner radius
        , v=((this.mxXY<=355) ? new Uint8Array(l) : new Uint16Array(l)) // vertices array
        , s=f-1  // segment index 
        , i=-1,j // vertices array index (running and 'turn around'/last outer)
        , m=(d>0)?f:t,r // segmentRadian 'multiplier' (starting w/ f or t+1), radian
        ; // console.log(this.id,f,t,d,"|",x,y,rO,rI,bR,sR,m,s,i);
      while (++s<=t) { r=bR+m*sR; m+=d;
        v[++i]=Math.round(x+rO*Math.cos(r));
        v[++i]=Math.round(y+rO*Math.sin(r)); } // console.log(this.id,s,r,v[i-1],v[i]); }
      if (rI) { j=i;
        while (--s>=f) { m-=d; r=bR+m*sR;
          v[++i]=Math.round(x+rI*Math.cos(r));
          v[++i]=Math.round(y+rI*Math.sin(r)); }
        if (c) this.cUp.push(v.slice(j-1,j+3));
      } // console.log(this.id,d,j,i,v.slice(0,4),this.cUp);
      return v; };
    p.rad=function(degrs) { return 2*Math.PI*(degrs%360)/360; }; // radian <-- degrees
    
    // convenient single char functions for control in console
    function r() { run(); }
    function h() { halt(); }
    function c() { cont(); }
    
    setTimeout(run,999);
    

    5 Attachments

  • This looks great. It would be nice to see a watchface based on that.

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

Swiss Federal Railway Clock modeled w/ Circular Gauges

Posted by Avatar for allObjects @allObjects

Actions