diff --git a/apps.json b/apps.json index 50a628676..f5c751ccc 100644 --- a/apps.json +++ b/apps.json @@ -2591,5 +2591,19 @@ {"name":"astral.app.js","url":"app.js"}, {"name":"astral.img","url":"app-icon.js","evaluate":true} ] +}, +{ "id": "lifeclk", + "name": "Game of Life Clock", + "shortName":"Conway's Clock", + "icon": "app.png", + "version":"0.05", + "description": "Modification and clockification of Conway's Game of Life", + "tags": "clock", + "type" : "clock", + "readme": "README.md", + "storage": [ + {"name":"lifeclk.app.js","url":"app.js"}, + {"name":"lifeclk.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/lifeclk/ChangeLog b/apps/lifeclk/ChangeLog new file mode 100644 index 000000000..dfd8b8775 --- /dev/null +++ b/apps/lifeclk/ChangeLog @@ -0,0 +1,5 @@ +0.01: New App! 2021-01-07 +0.02: Faster algorithm, hours and minutes are now displayable whenever, using the upper button 2021-01-14 +0.03: Ah yes. Some people prefer the 12 hour system 2021-01-14 +0.04: Fixed a bug, doesn't run while display's on now 2021-01-18 +0.05: Fixed a bug, doesn't count the time it was asleep when calculating the update time 2021-01-19 \ No newline at end of file diff --git a/apps/lifeclk/README.md b/apps/lifeclk/README.md new file mode 100644 index 000000000..e799cf72c --- /dev/null +++ b/apps/lifeclk/README.md @@ -0,0 +1,12 @@ +# Game of Life Clock + +Modification of the "Game of Life" ("life") app that also displays the current time, including seconds. + +## Usage + +Upon launch, the clock loads a randomly generated 27x23 grid of "cells", with a block in the middle showing the time. +Hours and minute are only displayed for 10 generations after launch, reset or after pressing the top button. After 10 generations, the cells that showed the hours and minutes become part of the game of life. + +* Top button: Immediately displays hours and minutes, killing any cells underneath the time block. +* Middle button: Shows the launcher. The state of the game is not saved. +* Lower button: Resets the cells, randomly generates new ones. diff --git a/apps/lifeclk/app-icon.js b/apps/lifeclk/app-icon.js new file mode 100644 index 000000000..8127abd79 --- /dev/null +++ b/apps/lifeclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AFkLC6+wC6u73YuVC7JIUCwIZBLywwTIwYwTC4QwUC4OwGCgXCCoQwRXwYwTO4wwQO4wwQO44wPO44wPO5G7olACY9EAAVLO44LCCosECwYXDGAm0BQIvFCwoABCwIcCCoQuHCwwXEAAguFBQgFBoEEC451GIwQVDAgIXBIhQwECoYEBYAS5NCogEBC6BGFVAQXPGAoXRGAoXSO6owGC6Z3VGAoXUd4gWRGAYXVIwQXUIwReSC7gWUgELFyoXBCyoA/ACwA==")) \ No newline at end of file diff --git a/apps/lifeclk/app.js b/apps/lifeclk/app.js new file mode 100644 index 000000000..d0288ad2f --- /dev/null +++ b/apps/lifeclk/app.js @@ -0,0 +1,475 @@ +Bangle.setLCDTimeout(30); + +const is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; + +const squaresize = 5; +const padding = 2; +const bigSquareSize = squaresize+padding; +const noOfSquX = 27; +const noOfSquY = 23; +const screenWidth = noOfSquX * bigSquareSize - padding; +const screenHeight = noOfSquY * bigSquareSize - padding; +const ulCornerX = parseInt((220-screenWidth)/2); +const ulCornerY = parseInt(40 + (160-screenHeight)/2); +const textWidth = 27; +const textHeight = 7; +const sectextWidth = 9; +const textWidthScreen = textWidth*bigSquareSize; +const textHeightScreen = textHeight*bigSquareSize; +const textWidthScreenMin = (textWidth-sectextWidth)*bigSquareSize; +const textWidthScreenSec = sectextWidth*bigSquareSize; +const ulCornerTextX = parseInt((noOfSquX-textWidth)/2); +const ulCornerTextY = parseInt((noOfSquY-textHeight)/2); +const ulCornerTextXScreen = ulCornerX+parseInt((noOfSquX-textWidth) + *bigSquareSize/2); +const ulCornerTextYScreen = ulCornerY+parseInt((noOfSquY-textHeight) + *bigSquareSize/2); +const seculCornerTextX = ulCornerTextX+textWidth-sectextWidth; + +let buf = Graphics.createArrayBuffer(screenWidth,screenHeight,1,{msb:true}); +let textbufMin = Graphics.createArrayBuffer((textWidth-sectextWidth)*bigSquareSize, + textHeight*bigSquareSize, + 1,{msb:true}); +let textPreBufMin = Graphics.createArrayBuffer(textWidth-sectextWidth, + textHeight, + 8,{msb:true}); +let textbufSec = Graphics.createArrayBuffer(sectextWidth*bigSquareSize, + textHeight*bigSquareSize, + 1,{msb:true}); +let textPreBufSec = Graphics.createArrayBuffer(sectextWidth, + textHeight, + 8,{msb:true}); + +let genNow = new Uint8Array((noOfSquX+2)*(noOfSquY+2)); +let genFut = new Uint8Array((noOfSquX+2)*(noOfSquY+2)); +let generation=0; +const minuteGens = 10; +let genEnd = minuteGens; +let lastupdatetime=null; + +function fillLinesFirstFewGens(){ + let fade = (genEnd-generation)/minuteGens; + g.setColor(fade,fade,fade); + // vertical lines + g.fillRect(ulCornerTextXScreen, + ulCornerTextYScreen, + ulCornerTextXScreen, + ulCornerTextYScreen+textHeightScreen-padding); + // horizontal lines + g.fillRect(ulCornerTextXScreen, + ulCornerTextYScreen, + ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen); + g.fillRect(ulCornerTextXScreen, + ulCornerTextYScreen+textHeightScreen-padding, + ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen+textHeightScreen-padding); + g.setColor(1,1,1); +} + +function clearLinesFirstFewGens(){ + // vertical lines + g.clearRect(ulCornerTextXScreen, + ulCornerTextYScreen, + ulCornerTextXScreen, + ulCornerTextYScreen+textHeightScreen-padding); + // horizontal lines + g.clearRect(ulCornerTextXScreen, + ulCornerTextYScreen, + ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen); + g.clearRect(ulCornerTextXScreen, + ulCornerTextYScreen+textHeightScreen-padding, + ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen+textHeightScreen-padding); +} + +function fillLinesLaterGens(){ + g.setColor(1,1,1); + // horizontal lines + g.fillRect(ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen, + ulCornerTextXScreen+textWidthScreenMin+textWidthScreenSec, + ulCornerTextYScreen); + g.fillRect(ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen+textHeightScreen-padding, + ulCornerTextXScreen+textWidthScreenMin+textWidthScreenSec, + ulCornerTextYScreen+textHeightScreen-padding); + // vertical lines + g.fillRect(ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen, + ulCornerTextXScreen+textWidthScreenMin, + ulCornerTextYScreen+textHeightScreen-padding); + g.fillRect(ulCornerTextXScreen+textWidthScreenMin+textWidthScreenSec, + ulCornerTextYScreen, + ulCornerTextXScreen+textWidthScreenMin+textWidthScreenSec, + ulCornerTextYScreen+textHeightScreen-padding); +} + +function renderSecText(){ + g.clearRect(ulCornerTextXScreen+18*bigSquareSize+padding, + ulCornerTextYScreen, + ulCornerTextXScreen+textWidthScreen, + ulCornerTextYScreen+textHeightScreen-1); + fillLinesLaterGens(); + g.drawImage({width:textWidthScreenSec, + height:textHeightScreen, + bpp:1,transparent:0,buffer:textbufSec.buffer}, + ulCornerTextXScreen+textWidthScreenMin,ulCornerTextYScreen); +} + +function flip(dontclear) { + g.setColor(1,1,1); + g.drawImage({width:screenWidth,height:screenHeight,bpp:1,buffer:buf.buffer}, + ulCornerX,ulCornerY); + if(generation=seculCornerTextX && x=ulCornerTextY && y=ulCornerTextX && x=ulCornerTextY && y4 && genNow[ind]<8){ + genFut[ind]+=1; + addNeighbors(ind); + let Xr=bigSquareSize*(x-1); + let Yr=bigSquareSize*(y-1); + buf.fillRect(Xr,Yr, Xr+squaresize,Yr+squaresize); + } + } + if(y == noOfSquY){ + if(generation == genEnd-1){writeMinuteText();} + flip(); + let tmp = genFut; genFut = genNow; genNow = tmp; // reference swapping + genFut.fill(0); + howlong(); + currentY = 1; + } + else{ + currentY++; + } + if(runOn){setTimeout(nextLineComp, 50);} +} + +function stopdraw() { + runOn = false; +} + +function startdraw(init) { + if (init===undefined) init=false; + Bangle.drawWidgets(); + g.reset(); + g.setColor(1,1,1); + g.setFont("6x8",1); + g.setFontAlign(0,0,3); + g.drawString("RESET",230,200); + g.drawString("LAUNCH",230,130); + g.drawString("CLOCK",230,60); + if(!init){ + updateSecondText(); + // stopdraw(); + runOn = true; + sleeptime = 0; + nextLineComp(); + } +} + +function regen(){ + g.setColor(1,1,1); + generation = 0; + genEnd = minuteGens; + currentY=1; + g.setFont("6x8",2); + g.setFontAlign(-1,-1,0); + g.clearRect(20,220,220,240); + g.drawString('Gen:'+generation+' 0ms ',20,220,true); + lastupdatetime=Date.now(); + updateSecondText(); + genNow.fill(0); + genFut.fill(0); + initDraw(); + // stopdraw(); + runOn = true; + sleeptime = 0; + nextLineComp(); +} + +function showMinAgain(){ + if(genEnd != generation+minuteGens){ + genEnd = generation+minuteGens; + lastDate = null; + } +} + +function setButtons(){ + setWatch(showMinAgain, BTN1, {repeat:true,edge:"falling"}); + setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"}); + setWatch(regen, BTN3, {repeat:true,edge:"falling"}); +} + +let wentToSleepAt; + +var SCREENACCESS = { + withApp:true, + request:function(){ + this.withApp=false; + stopdraw(); + wentToSleepAt = Date.now(); + }, + release:function(){ + this.withApp=true; + sleeptime = Date.now()-wentToSleepAt; + startdraw(); + } +}; + +Bangle.on('lcdPower',function(on) { + if (!SCREENACCESS.withApp) return; + if (on) { + startdraw(); + sleeptime = Date.now()-wentToSleepAt; + } else { + stopdraw(); + wentToSleepAt = Date.now(); + } +}); + +g.clear(); +Bangle.loadWidgets(); +setButtons(); +regen(); +startdraw(true); diff --git a/apps/lifeclk/app.png b/apps/lifeclk/app.png new file mode 100644 index 000000000..a88157f5c Binary files /dev/null and b/apps/lifeclk/app.png differ