From 70a750fdf6410249074a2e1ee26970dfdbdc7063 Mon Sep 17 00:00:00 2001 From: Amos Blanton Date: Sun, 19 Feb 2023 15:32:27 +0100 Subject: [PATCH] Rings watchface. --- apps/rings/ChangeLog | 1 + apps/rings/README.md | 11 ++ apps/rings/app-icon.js | 1 + apps/rings/app.js | 210 +++++++++++++++++++++++++++++++++++++ apps/rings/app.png | Bin 0 -> 4217 bytes apps/rings/metadata.json | 16 +++ apps/rings/screenshot1.png | Bin 0 -> 2950 bytes apps/rings/screenshot2.png | Bin 0 -> 5230 bytes 8 files changed, 239 insertions(+) create mode 100644 apps/rings/ChangeLog create mode 100644 apps/rings/README.md create mode 100644 apps/rings/app-icon.js create mode 100644 apps/rings/app.js create mode 100644 apps/rings/app.png create mode 100644 apps/rings/metadata.json create mode 100644 apps/rings/screenshot1.png create mode 100644 apps/rings/screenshot2.png diff --git a/apps/rings/ChangeLog b/apps/rings/ChangeLog new file mode 100644 index 000000000..ba635f847 --- /dev/null +++ b/apps/rings/ChangeLog @@ -0,0 +1 @@ +0.01: First rev. Could use a little optimization love. diff --git a/apps/rings/README.md b/apps/rings/README.md new file mode 100644 index 000000000..1f689623f --- /dev/null +++ b/apps/rings/README.md @@ -0,0 +1,11 @@ +# Rings watchface + +Ring based watchface, read from the outside in. When the watch is unlocked the circles shrink to show the date ring. + +By Amos Blanton, inspired by and remixed from Rinkulainen by Jukio Kallio. + +![](screenshot1.png) +View when watch is locked. + +![](screenshot2.png) +Watch unlocked, showing the date. \ No newline at end of file diff --git a/apps/rings/app-icon.js b/apps/rings/app-icon.js new file mode 100644 index 000000000..3e70c5e3a --- /dev/null +++ b/apps/rings/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+CR0GAokMiUMAQQLFAooAYg0SjEYjwAHjA0BGLQXChgrEGIIGCG4wyBLbItFFQYnDFgYREGIRiRCISHIFQgEDMYgCCSoYuPhi1IXg4vFMYI5CSh4uBJwYAEj5bDAAjGCjIzDTwguWFoMShkOgIVFgMOLQMYHwYCDGBZdKiUHisckcOwNWgGBxEkikjkcWhCmBj4sBMJ67GDIMAiojBAANd1lWq2BAAMOo4LCjkBiQWBLwQ0CK4RfGFw0SgJbBAAc51mBles1lPhwMENoMMGAJgDj0MGAoEBBgYuDhAgFkeIFgOBAQN6F40jisGYgQADRpguJL4LtEgE4B48VMIRiDSIxeFjEBDo1HWQMAqgACGIKdFAAUPYYUSSIjsJgAdHh1O1HSAAmovEICY8Id4RTDMAK9GRoMVDIscgGo6YuFAAPT1FOitHCosGFoYpBdwZeFg4uRGIYwHh4sBYIsAS4ZeJFwItKGAiSFjjyBL4aQBdo0MCwlHhxdMYgkAYJApDF48OIotOFw2ov1+NA3TvyRFisYMAJXDgCOEjCOEo8VLwuoqjXCAAIxF1EIJQsSF4RbBhkMXxcO5giEFwMqAAQwBqhkB6ZABqkdYArsBdQUSF4q+FkdUWQwrCAAQFFvzxGg5eCeARfGCYkdF4t+FI0Aq1VleBwNWgAvFFAgvHL4ovOq2swIvB1msgFAF7yPHlesLYNWwIHBR5AvJiUWd4ovE6S6EAQJfB1gyDF4rvCAAcSgwvEjEVCYdHh+o6ZgFAAksq1WlcrGwMHJQkciUfF4kAGwsOCgt4F4jCCqgABv1+SwcBgBKEkcVjAvEhkAjA0DAAIUEkcI1AvFAA+oHAIZGh0YAAQqBFAMSFodWkhFFjlOMAwAH6d+DA0MjESGIYrD1kAwOswMyIwsVGBouBhAXGXwRfCLYYsBAQIDBvUcC4lHGALzFFojHBFw0jcwq+CL4eBgGs1lWgwYFo8cgF+5gpBAAQvB0SlBo4VFh5bBRwIuBRwMGL4QABleBrgHBhxKGPYMIql+AAVUg8VjouGi0Mj7uELwYAGhkZhjZFMQTeCAAYIDdgqNCFoMMLwoABlYFBmJsCj8GGA4yCAAQMIFwJaCdoZeIBwQACj8Mh4jIABUWFwS8Dj0SLwovIGAMShCEBAB8PQ4JbCLoSNBABIwHjEGh4xMjkVDQMZRggEBFxQwIDIUMhEVdQQrDFgMOg0SCIQYFRY4wPj4gBiUMg8MAQS0BCgINBLYguDGBxJCGI4kCA5S7GFxwABa4QAXiQrPAAQ/CSZAANjBaQGA8MGKUYhiLSGJEGcgIsMfQItYGIxlChg0BAAUSFYYtQ")) \ No newline at end of file diff --git a/apps/rings/app.js b/apps/rings/app.js new file mode 100644 index 000000000..0fb768ea8 --- /dev/null +++ b/apps/rings/app.js @@ -0,0 +1,210 @@ +// Rings watch face +// for Bangle.js 2 +// by Amos Blanton +// Remixed from / inspired by Rinkulainen watch face by Jukio Kallio + +// To Do: +// Make Month / year text buffer 1/2 size +// Optimize text positioning transforms + +const watch = { + x:0, y:0, w:0, h:0, + color:"#000000", + dateRing : { size:109, weight:20, color:"#00FF00", cursor:14, numbers: true }, + hourRing : { size:85, weight:20, color:"#00FFFF", cursor:14, numbers: true }, + minuteRing : { size:61, weight:20, color:"#FFFF00", cursor:14, numbers: true }, + screen : { width:g.getWidth(), height:g.getHeight(), centerX: g.getWidth() *0.5, centerY: g.getHeight() * 0.5, cursor: 14, font:"6x8:2" }, +}; + +const month= ["JANUARY","FEBRUARY","MARCH","APRIL","MAY","JUNE","JULY", + "AUGUST","SEPTEMBER","OCTOBER","NOVEMBER","DECEMBER"]; + +var wait = 60000; // wait time, normally a minute +// timeout used to update every minute +var drawTimeout; +// Global for use in shrink / unshrink animations +var counter = 1; + +// Buffer for month circle text, 1/2 screen size (will be scaled up) +var monthCircleTextBuffer= Graphics.createArrayBuffer(watch.screen.width,watch.screen.height,1,{msb:true}); +var monthCircleTextImg = monthCircleTextBuffer.asImage(); +monthCircleTextImg.transparent = 1; +var lastMonthCircleImageText = ""; + +// Calculate number of days in this month / year for date ring +const getDays = (year, thisMonth) => { + return new Date(year, thisMonth, 0).getDate(); // getMonth() Jan = 0. +}; + +// Schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, wait - (Date.now() % wait)); +} + +// Draws a time circle (date, hours, minutes) +function drawTimeCircle(color, size, weight, range, value ) { + // variables for vertex transformations and positioning time + var tver, tobj, tran; + var ttime = (value / range) * (Math.PI * 2); + + // draw circle and line + g.setColor(color).fillCircle(watch.screen.centerX, watch.screen.centerY, size); + g.setColor("#000000").fillCircle(watch.screen.centerX, watch.screen.centerY, size - weight); + + tver = [-watch.screen.cursor, 0, watch.screen.cursor, 0, watch.screen.cursor, -size*1.01, -watch.screen.cursor, -size*1.05]; + + tobj = { x:watch.screen.centerX, y:watch.screen.centerY, scale:1, rotate:ttime }; + tran = g.transformVertices(tver, tobj); + g.fillPoly(tran); + + // Draw numbers + g.setFontAlign(0,0).setFont(watch.screen.font, 2).setColor(1,1,1); + + // size - 21 is the right offset to get the numbers aligned in the circle. + tver = [-1, 0, 1, 0, 1, -size, -1, -(size -21)]; + tran = g.transformVertices(tver, tobj); + g.setColor(1,1,1); + g.drawString(value, (tran[4]+tran[6]) / 2 , (tran[5]+tran[7]) / 2 ); + +} + +// Draws text for month and year in date circle +function drawMonthCircleText( text, circleSize, range, value){ + + // If the text isn't the same as last time, write it into a graphic object. + if(text != lastMonthCircleImageText){ + + monthCircleTextBuffer.clear(); + monthCircleTextBuffer.fillRect(0,0,watch.screen.width,watch.screen.height); + + var tver, tobj, tran; + + // From here: https://forum.espruino.com/comments/16781795/ + var gr = Graphics.createArrayBuffer(24,16,1,{msb:true}); + var grimg = gr.asImage(); + grimg.transparent = 1; + monthCircleTextBuffer.setColor(0,0,0); + + for(z=0; z < text.length; z++){ + tobj = { x:watch.screen.centerX, y:watch.screen.centerY, scale:1, rotate: ((z + 1) / range) * (Math.PI * 2) }; + tver = [-1, 0, 1, 0, 1, -circleSize, -1, -(circleSize -21)]; + tran = monthCircleTextBuffer.transformVertices(tver, tobj); + gr.clear().setColor(1,1,1).fillRect(0,0,24,16).setColor(0,0,0).setFont(watch.screen.font).setFontAlign(0,0).drawString(text[z],12,8); + + monthCircleTextBuffer.drawImage(grimg, + (tran[4]+tran[6]) / 2, + (tran[5]+tran[7]) / 2, + {rotate:((z+1) / range) * (Math.PI * 2) }); + } + + lastMonthCircleImageText = text; + } + + // Determine correct rotation for text in ring ( opposite the date position ) + var offset = value + (range / 2) - (text.length / 2); + if(offset > range) + offset = offset - range; + var rotation = (offset / range) * (Math.PI * 2); + + // Draw the image of text to the screen at that rotation + g.drawImage(monthCircleTextImg, watch.screen.centerX, watch.screen.centerY, {scale:1, rotate:rotation }); + +} + +// Animate by shrinking or expanding circles +function shrinkCircles(toggle){ + // If there's a queued draw operation,removeit so animation isn't interrupted. + if (drawTimeout) clearTimeout(drawTimeout); + + var date = new Date(); + var delta = 1; + + if(counter > 12) + { + counter = 1; + // We're finished, so queue next draw. + queueDraw(); + return; + } + + if(toggle) // We are shrinking + delta = counter * 2 * -1; + else // We are expanding + delta = counter *2 - 24; + + // Clear space on screen. + g.setColor(watch.color); + g.fillRect(0, 0, watch.screen.width, watch.screen.height); + + // Draw the date ring (unless it's the last run of an expansion). + if(counter < 11 || toggle){ + drawTimeCircle(watch.dateRing.color, watch.dateRing.size + delta, watch.dateRing.weight, getDays(date.getFullYear(), date.getMonth()+1), date.getDate() ); + // Draw month and year in date ring + drawMonthCircleText( month[date.getMonth()]+" "+date.getFullYear(), watch.dateRing.size - 24, getDays(date.getFullYear(), date.getMonth()+1), date.getDate()) ; + } + + drawTimeCircle(watch.hourRing.color, watch.hourRing.size + delta, watch.hourRing.weight, 12, date.getHours() ); + + drawTimeCircle(watch.minuteRing.color, watch.minuteRing.size + delta, watch.minuteRing.weight, 60, date.getMinutes() ); + + counter += 1; + setTimeout(shrinkCircles, 10, toggle); +} + + +// main draw function +function draw() { + // make date object + var date = new Date(); + var unLockedOffset = 0; + + // Reset the state of the graphics library + g.reset(); + + // Clear the area where we want to draw the time + g.setColor(watch.color); + g.fillRect(0, 0, watch.screen.width, watch.screen.height); + + // If unlocked, draw date ring and text and make hour and minute rings smaller + if(!Bangle.isLocked()){ + unLockedOffset = 24; + drawTimeCircle(watch.dateRing.color, watch.dateRing.size - unLockedOffset, watch.dateRing.weight, getDays(date.getFullYear(), date.getMonth()+1), date.getDate() ); + drawMonthCircleText( month[date.getMonth()]+" "+date.getFullYear(), watch.dateRing.size - unLockedOffset, getDays(date.getFullYear(), date.getMonth()+1), date.getDate()) ; + } + + drawTimeCircle(watch.hourRing.color, watch.hourRing.size - unLockedOffset, watch.hourRing.weight, 12, date.getHours() ); + drawTimeCircle(watch.minuteRing.color, watch.minuteRing.size -unLockedOffset , watch.minuteRing.weight, 60, date.getMinutes() ); + + queueDraw(); +} + +// Trigger shrink / expand animation on unlock / lock events +Bangle.on('lock', on=>{ + if (on) { // locked, expand circles + counter = 1; + shrinkCircles(false); + } else + { // unlocked, shrink circles and show date ring + counter = 1; + shrinkCircles(true); + } +}); + + +// End function definitions / start of initial execution. + +// Clear the screen once, at startup +g.clear(); + +// draw immediately at first +draw(); + + +// console.log("Whatevs"); + +// Show launcher when middle button pressed +Bangle.setUI("clock"); \ No newline at end of file diff --git a/apps/rings/app.png b/apps/rings/app.png new file mode 100644 index 0000000000000000000000000000000000000000..e4bfd6bac74147bbc2db8f653b2158a4359f4bcf GIT binary patch literal 4217 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 wVy~k*f@-b?0X9p<8!{c|UQbJ;0~F80t* zzLXb+#8pLK1ps)R2s&{jy>#EvM(P6qf&2hKSR?>&bm0nH0Ra5O0Dv_I06-xP0ARvo zwHU)M1|nT7bWr;Gfa@2S0ze9+1rT2#;6(!fSpk=77YJ|*$o4Nr0tNozTukASJAmXL zj@5;|+_W$HWz64}I1l)rVjj`I?3z5{|L|oPk87acg`vb~TlxY3AiB#A1Y~3}U#MBS zn_6Hk^z{_(pglz#ozYG%A~;XXWfTC8Q@DVhE?7q>&hws^uL4eq`!9yV1-^8Pazp>3 zU_F$$E%c3`YG@x9sEml1h!{753JQh7eVkDWNOjGB!Y_`LxZSW=jDo1BzrVkTzoZD- z$5m8ZUS3{QOhQybLimCq>>J>Pb;JpK`SScN^1nLjF1~ks+%Z^pv={VJ*U<^>hgIU{ zzD)G*_V+xo?x_DVdHMcR)Na3NHHZX+u!4*nKAl0IvSlQCBg=0XJ;pQ)Zf|hGwSD&wtvK z#JUgaYO8upZ&XguXkz5pYf4}`8)t_*Cp!XL|Fk6JJdD9FF|WhdGw6}&{)UgwN&3&` zLY(Z{Y8Z7);}1qFySK54qT!9)sh{>Ty3KzB%ZSYIcq?Zd+x%fQ|5owA$914goMi9! z0Jqn?b}t$sKN1}U*T;K9bNQ3Xm1W43h&Kppi)#+H@84;O$}Pf5^|qFswyA<+c-G`> zvW^M^q=4eZEc^nostcxYWX)FYJfUJDaQV9q<>{VkGH+7MeX{V{FiQSEr~qO1U(3Pl z)|2HgMn?x2LJ$UbrFvhA(J;KcS(F|{F*GBWJucTxVlCZW zB-{)u+j2;td-{W7=zS0H<)Kl3vCYKJ&Dbh>iY3rQ=7WU#yl-|=ktFw0F_dp@wZC{! zrMD5Dy(>p*5xen2$?fl*qs2o&Id42jIRbM+^#y2I^X>3DXK9TrnsVNo8RNskB<2`> zv`MLysV|B|I2x8goA@kBGmITZl{MBEX6k+o$d9Fe8I}0ezTKwcQ7>VOQtPFIKRqDP z4$MxdQMC*z?E;Yv9SglQsOkCMl0?(Y5wlF*o92e>6AJ#3yt^R%?#(hDiT~t*Yk5Ke zW!8K4LUHkkusJfU#GY6y7RJF}A#Ue2RZUr9@-%!1@l=k_v1??)r5EarZaE|n2uz}n;IU-1&-cD9maV+Ci?$FwHPy8t?lr|;pTZVzVa!OQ}D57=P8 zovYUS1=7)UvFQYi^U;&QOb`7`52J8j^aJb62>aB%(LUG;D@z~{vv!RX%!q9R(6TbG z2)ijFtj1Mr>b$3>5e*R60P#;7=8NH`-gl^l$NNArt{&&7$ICEtgdpid(oXN23A(kd zNU>5D>~rS{um>_ad{nu=-#Dmu{s1y3)VadE)$izf6TItXzB>0PM#taRgpzPQM*h{h z8BN(QPffAy`OdJIPYqf^Fp7=nbCN60*dB->tkKmfQwoIV>H7lq+VSDbHw(6)vCkZ-hvvOP^Hj>)RIBQLF5MIWcz(gDpxB60H|7AAE%Mgy=;JWltPh3|{ zr$p<>>BC1M-cu_iSKf2n;LP>Wb1|HO(3vqj8Or}8t}*jY(tZcs(XPDR%);LxK5N$5 zqi(I6TuF!{PU4DeW-EwyV%PgM+*Q&T9+Cb2b8z|w@g4T-%0J8N@`^{<3SKZa2v3cd z#fG(Loyix{NUJ~G+#N2?Bc_Zet0MX;a5X#n>p6|!4Z62IU}1Hp%F2{uGjvyY`FyRy zJYbsMqtl;A4)dxxyXZ>?T_9^Ll6VkqYTI$2EhJUEo3v)AGTCg|95mS4ylHXLlRagyl5I^vtu!wm!nV8o2RNlb>cGUc zV?{79a(3#a&lIQaFtN0iQ1g&k3JZ6n6mPJcRXY@e;wDq>ymRPCz6gGi%0bqU&d02-qXF1{ZACzffHf$L2%|)M_L0zqJpiWH?5-cyN_%dt)4% zL+4t97A&_bnjwc0R7V^0;9LAV(IJ+q68BpQSn{)HG+7@`tP zgD;3Ckm=rh09^=RuFH;s(laV}v2s&~3%ZntJ^I?DLw2KQMnB;Sc*UHEQ$_@606CqN z67Hyjh*vHNE`I?VkO}B3d8goXyTD#?p+Mjmix%j-0X1XhHl&K7ik3OSjo3vfX!q(_ z_l$HsF$WsDhwJr=ZY1JaWsVzQjzK?Gua$dnwoRhQLfj^PkBua~&xGwUyf)Zsp2 zZX#)tYX0=fF2=^JBVxJ89#|=lTVb02-jmeC0lh7(E+X8c(D9yQ=FBCdZ)dIIkM7cO zyJnegH#PCT!-K$3>fRP*B#6)}m-Bk}@~S$No$Mo2a6hszUG2Rk%TeK2p4H81QH z?dM}r^K*l@=3QKNk4L?bi3*Hut9YLCfK@T4&nPs^7w zqvOi0DeMY2HA+=8vVJ)$q_&k6+Uq6s>@Dv#QOg1&qmunVouJ&os=zDd7=+-J5Pyd` z9+LE9QS}XakI`;1M#Oe}=NuUn(;nrJvcu-BCqyPg=HLum;$5$*))9)Be-?lGl||&E zX|f`Jy!0(iXn}sJAW;-^ zKyaP-lE4)}PT`&B5Yx)i_D7ARgNShkLM$9?Ld7|kAG~rpH9}IZ;jcq=X{e@GzuLVCB?a=ICtts1KuW9^e)8#&quL z!3>1Qm!p+;-geU&gweJGpby7x&De$>oGlX@~kn#)#zMzo#7 zhC$z`1-g~cG3m9@+n}nI0I5BN`a+kM_B#U6yUx@XZdt_Z|k`#TyeBs^5Xg`hYrr| zS<{(mV+?1u%I4QFr?Puw?To@CLA;ZQ=isCy!2wPGmU@fNubxSr4u*UP%L?a|qT0uE zQCF)}dd2k4iPY6vzDajc&HmOO-DDplvH&i=A<6(Ax2R~7|III(EZt-5AnmbC>?Un^r;3tJ4+XkMV>~*OJ9AYc?^>P6+B>B;89&)?BVTgi|-n za(`i zi%Gh=({3W*?X@;yPTrkhdmeS(1&#=N%451kcV+K-$TRV`s$kO zT`YLfU0D75_3rpVT+`C!GwcfzR!qv;zxL$W^B6WwxmDqq%J z*D4QJ9^Po!PbDdekRb2y3HaDMaU0{Qzt8Dt5#{_tEK=Zw2P%R zsOB;78aF`+kg2T7VaoyTI(-2|%byVElC3F2YPP?XAREjJHsi7>$vAtO>BcCT#{CLH zQlTt|su-qsU7WjY3e?=Izv;qkDU@&hN{vt{#IS*P9c#S`j#;>U#T~#lydozB#HFPZ zz&?U{sj|khUv0w3+aI6zLC_5ml6ouDgaNpIb{LrZS-5ayUV`C%{U-i*(B z^~ap`2m;Of8J0Ar9?oLZfb2Hj4#UmIA3Wl3VU^Ox3r&X7;HZUPyQRYr_2L=K1UYro zG`O!W2!_cR& zve$~BMi(Z_(;5SHhwIpirL$heT9CMHJ7ng}^IXHIn{E}e^yM6^66`~~^ag8c^ec?^ zEzfvw@_1N3BqT>ye5QZ>vplF$)_nM}q)4+QLffKaQ%vD|x8t@a1PImkn6LD$T764> zqTb2F$A4x!;7t}CSQYf<=oy;KZ^)#{7af1Neg%{(=We{i6UWqWn}+Ytb6wco

JL zL}q^4o>yU098Bj1#q)2>z6D2}_b10%p=-@sesNX>EP-(2aF&X}RFB^i80G{dc7`D8@b}v z8L}X|>q5ibhAeG^726#M{M91k!s+TLJ5tjwy1RJMNPQ@M@PM{yoa*2=iM==@5kJ&@ z7#8pSknXf?eB(*zs((Th;QE9NXZb7ED@~=9$Fv(=LhfueT1vJXuZ-@{FvvDO>|^V4 z#%7{90~zvsO*ALM6Bha!gECz{G+iUJ*V>^-VSeg5E2i_!0k%Djm*$n;Mw(Fd^-t2h zGKCsGLrO&JY!rBiH&EH|8L6#2KB9mXrL}mMj^qqzx(x{mO?t1qZXK~oD+kbGW62b@ za4AuWI=2=03=_pu^`GmZunO)tcEff?zH-2r@iA}hpQLwK^&EpctYK?cZMW%J#fMBh zqZ`Rfqm3w!iC`=_ZuB>i6av~sJXd(XSL5>}GPtijXBMAKtnR^Ir?4af=KI6>)PD3X z#5kP4rii8@5jGwy8Z8RiLUlg?4Bgc)B@LdSC-?DV1j>wC1O2p3b;gSeRe5xRMt!7G z0gBW`H_udV+Lag4%j1z^*Dc3j`8}Fwa^f2)?8*eV@3H!rc;HX0!<%Dm+`Hs8 zzX2K`+M-#X(GCxm3$5PQ2)^e12{pAdC9;3zS5qnI2M@kYgG)NP6-qK^5vy;e(e)z~ zoDb0FF?#Hz?lrpTg!?Grmhvb#@yS%7kP}VGZEn;Pqet{yYp7i8ktdn^mD;>(D{7tb zW(s9A^;dWG#O>Yi>_jd!S!3bF2Wb&Y0blQMB67PxV~;QHazXeaUDu(y{_J@jqYZVf zvh0gtvy!FDfsun1J%m*jh)_XV>iy6?E9_kz