-
• #2
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);
- 1. In the first face of the picture the time looks like 35 minutes - purple arc - and :57 seconds - 'white pie'.
-
• #3
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);
- when CGauge's full arc > 270 degrees and 'smaller' radiuses
-
• #4
Nice, that looks really neat!
-
• #5
...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
-
• #6
This looks great. It would be nice to see a watchface based on that.
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