From 69330e03cb8791c245ec5c88b7a17ed52c3c7758 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Fri, 13 Mar 2026 15:09:13 -0600 Subject: [PATCH] feat: Add calculator app --- calculator/calculator.app.js | 420 +++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 calculator/calculator.app.js diff --git a/calculator/calculator.app.js b/calculator/calculator.app.js new file mode 100644 index 0000000..5f4e77a --- /dev/null +++ b/calculator/calculator.app.js @@ -0,0 +1,420 @@ +/** + * BangleJS Calculator + * + * Original Author: Frederic Rousseau https://github.com/fredericrous + * Created: April 2020 + * + * Contributors: thyttan https://github.com/thyttan + */ + +g.clear(); +require("Font7x11Numeric7Seg").add(Graphics); + +var DEFAULT_SELECTION_NUMBERS = '5'; +var RESULT_HEIGHT = 40; +var RESULT_MAX_LEN = Math.floor((g.getWidth() - 20) / 14); +var COLORS = { + // [normal, selected] + DEFAULT: ['#7F8183', '#A6A6A7'], + OPERATOR: ['#F99D1C', '#CA7F2A'], + SPECIAL: ['#65686C', '#7F8183'] +}; + +var KEY_AREA = [0, RESULT_HEIGHT, g.getWidth(), g.getHeight()]; + +var screen, screenColor; +var globalGrid = [4, 5]; +var swipeEnabled; + +var numbersGrid = [3, 4]; +var numbers = { + '0': {grid: [1, 3], globalGrid: [1, 4], trbl: '2.00'}, + '.': {grid: [2, 3], globalGrid: [2, 4], trbl: '3=.0'}, + '1': {grid: [0, 2], globalGrid: [0, 3], trbl: '4201'}, + '2': {grid: [1, 2], globalGrid: [1, 3], trbl: '5301'}, + '3': {grid: [2, 2], globalGrid: [2, 3], trbl: '6+.2'}, + '4': {grid: [0, 1], globalGrid: [0, 2], trbl: '7514'}, + '5': {grid: [1, 1], globalGrid: [1, 2], trbl: '8624'}, + '6': {grid: [2, 1], globalGrid: [2, 2], trbl: '9-35'}, + '7': {grid: [0, 0], globalGrid: [0, 1], trbl: 'R847'}, + '8': {grid: [1, 0], globalGrid: [1, 1], trbl: 'N957'}, + '9': {grid: [2, 0], globalGrid: [2, 1], trbl: '%*68'}, +}; + +var operatorsGrid = [2, 3]; +var operators = { + '+': {grid: [0, 0], globalGrid: [3, 3], trbl: '-+=3'}, + '-': {grid: [1, 0], globalGrid: [3, 2], trbl: '*-+6'}, + '*': {grid: [0, 1], globalGrid: [3, 1], trbl: '/*-9'}, + '/': {grid: [1, 1], globalGrid: [3, 0], trbl: '//*%'}, + '=': {grid: [1, 2], globalGrid: [3, 4], trbl: '+==.'}, +}; + +var specialsGrid = [2, 2]; +var specials = { + 'R': {grid: [0, 0], globalGrid: [0, 0], trbl: 'RN7R', val: 'AC'}, + 'N': {grid: [1, 0], globalGrid: [1, 0], trbl: 'N%8R', val: '+/-'}, + '%': {grid: [0, 1], globalGrid: [2, 0], trbl: '%/9N'}, +}; + +var selected = DEFAULT_SELECTION_NUMBERS; +var prevSelected = DEFAULT_SELECTION_NUMBERS; +var prevNumber = null; +var currNumber = null; +var operator = null; +var results = null; +var isDecimal = false; +var hasPressedEquals = false; + +function prepareScreen(screen, grid, defaultColor) { + for (var k in screen) { + if (screen.hasOwnProperty(k)) { + screen[k].color = screen[k].color || defaultColor; + var position = []; + var xGrid = (KEY_AREA[2]-KEY_AREA[0])/grid[0]; + var yGrid = (KEY_AREA[3]-KEY_AREA[1])/grid[1]; + if (swipeEnabled) { + position[0] = KEY_AREA[0]+xGrid*screen[k].grid[0]; + position[1] = KEY_AREA[1]+yGrid*screen[k].grid[1]; + } else { + position[0] = KEY_AREA[0]+xGrid*screen[k].globalGrid[0]; + position[1] = KEY_AREA[1]+yGrid*screen[k].globalGrid[1]; + } + position[2] = position[0]+xGrid-1; + position[3] = position[1]+yGrid-1; + screen[k].xy = position; + } + } +} + +function drawKey(name, k, selected) { + var color = k.color || COLORS.DEFAULT; + g.setColor(color[selected ? 1 : 0]); + g.setFont('Vector', 20).setFontAlign(0,0); + g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); + g.setColor(-1); + g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2); +} + +function drawKeys() { + g.setColor(screenColor[0]); + g.fillRect(KEY_AREA[0], KEY_AREA[1], KEY_AREA[2], KEY_AREA[3]); + for (var k in screen) { + if (screen.hasOwnProperty(k)) { + drawKey(k, screen[k], k == selected); + } + } +} +function drawGlobal() { + screen = {}; + screenColor = COLORS.DEFAULT; + prepareScreen(numbers, globalGrid, COLORS.DEFAULT); + for (var k in numbers) { + screen[k] = numbers[k]; + } + prepareScreen(operators, globalGrid, COLORS.OPERATOR); + for (var k in operators) { + screen[k] = operators[k]; + } + prepareScreen(specials, globalGrid, COLORS.SPECIAL); + for (var k in specials) { + screen[k] = specials[k]; + } + drawKeys(); +} +function drawNumbers() { + screen = numbers; + screenColor = COLORS.DEFAULT; + drawKeys(); +} +function drawOperators() { + screen = operators; + screenColor =COLORS.OPERATOR; + drawKeys(); +} +function drawSpecials() { + screen = specials; + screenColor = COLORS.SPECIAL; + drawKeys(); +} + +function getIntWithPrecision(x) { + var xStr = x.toString(); + var xRadix = xStr.indexOf('.'); + var xPrecision = xRadix === -1 ? 0 : xStr.length - xRadix - 1; + return { + num: Number(xStr.replace('.', '')), + p: xPrecision + }; +} + +function multiply(x, y) { + var xNum = getIntWithPrecision(x); + var yNum = getIntWithPrecision(y); + return xNum.num * yNum.num / Math.pow(10, xNum.p + yNum.p); +} + +function divide(x, y) { + var xNum = getIntWithPrecision(x); + var yNum = getIntWithPrecision(y); + return xNum.num / yNum.num / Math.pow(10, xNum.p - yNum.p); +} + +function sum(x, y) { + let xNum = getIntWithPrecision(x); + let yNum = getIntWithPrecision(y); + + let diffPrecision = Math.abs(xNum.p - yNum.p); + if (diffPrecision > 0) { + if (xNum.p > yNum.p) { + yNum.num = yNum.num * Math.pow(10, diffPrecision); + } else { + xNum.num = xNum.num * Math.pow(10, diffPrecision); + } + } + return (xNum.num + yNum.num) / Math.pow(10, Math.max(xNum.p, yNum.p)); +} + +function subtract(x, y) { + return sum(x, -y); +} + +function doMath(x, y, operator) { + switch (operator) { + case '/': + return divide(x, y); + case '*': + return multiply(x, y); + case '+': + return sum(x, y); + case '-': + return subtract(x, y); + } +} + +function displayOutput(num) { + g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1); + g.setColor(-1); + if (num === Infinity || num === -Infinity || isNaN(num)) { + // handle division by 0 + if (num === Infinity) { + num = 'INFINITY'; + } else if (num === -Infinity) { + num = '-INFINITY'; + } else { + num = 'NOT A NUMBER'; + } + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + prevNumber = null; + operator = null; + specials.R.val = 'AC'; + if (!swipeEnabled) drawKey('R', specials.R); + g.setFont('Vector', 22); + } else { + // might not be a number due to display of dot "." + var numNumeric = Number(num); + + if (typeof num === 'string') { + if (num.indexOf('.') !== -1) { + // display a 0 before a lonely dot + if (numNumeric == 0) { + num = '0.'; + } + } else { + // remove preceding 0 + while (num.length > 1 && num[0] === '0') + num = num.substr(1); + } + } + num = num.toString(); + num = num.replace("-","- "); // fix padding for '-' + g.setFont('7x11Numeric7Seg', 2); + if (num.length > RESULT_MAX_LEN) { + num = num.substr(0, RESULT_MAX_LEN - 1)+'...'; + } + } + g.setFontAlign(1,0); + g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); + if (operator) { + g.setFont('Vector', 22).setFontAlign(1,0); + g.drawString(operator, g.getWidth()-1, RESULT_HEIGHT/2); + } +} +var wasPressedEquals = false; +var hasPressedNumber = false; +function calculatorLogic(x) { + if (wasPressedEquals && hasPressedNumber !== false) { + prevNumber = null; + currNumber = hasPressedNumber; + wasPressedEquals = false; + hasPressedNumber = false; + return; + } + if (hasPressedEquals) { + if (hasPressedNumber) { + prevNumber = null; + hasPressedNumber = false; + operator = null; + } else { + currNumber = null; + prevNumber = results; + } + hasPressedEquals = false; + wasPressedEquals = true; + } + + if (currNumber == null && operator != null && '/*-+'.indexOf(x) !== -1) { + operator = x; + displayOutput(prevNumber); + } else if (prevNumber != null && currNumber != null && operator != null) { + // we execute the calculus only when there was a previous number entered before and an operator + results = doMath(prevNumber, currNumber, operator); + operator = x; + prevNumber = results; + currNumber = null; + displayOutput(results); + } else if (prevNumber == null && currNumber != null && operator == null) { + // no operator yet, save the current number for later use when an operator is pressed + operator = x; + prevNumber = currNumber; + currNumber = null; + displayOutput(prevNumber); + } else if (prevNumber == null && currNumber == null && operator == null) { + displayOutput(0); + } +} + +function buttonPress(val) { + switch (val) { + case 'R': + currNumber = null; + results = null; + isDecimal = false; + hasPressedEquals = false; + if (specials.R.val == 'AC') { + prevNumber = null; + operator = null; + } else { + specials.R.val = 'AC'; + drawKey('R', specials.R, true); + } + wasPressedEquals = false; + hasPressedNumber = false; + displayOutput(0); + break; + case '%': + if (results != null) { + displayOutput(results /= 100); + } else if (currNumber != null) { + displayOutput(currNumber /= 100); + } + hasPressedNumber = false; + break; + case 'N': + if (results != null) { + displayOutput(results *= -1); + } else { + displayOutput(currNumber *= -1); + } + break; + case '/': + case '*': + case '-': + case '+': + calculatorLogic(val); + hasPressedNumber = false; + if (swipeEnabled) drawNumbers(); + break; + case '.': + specials.R.val = 'C'; + if (!swipeEnabled) drawKey('R', specials.R); + isDecimal = true; + displayOutput(currNumber == null ? 0 + '.' : currNumber + '.'); + break; + case '=': + if (prevNumber != null && currNumber != null && operator != null) { + results = doMath(prevNumber, currNumber, operator); + prevNumber = results; + displayOutput(results); + hasPressedEquals = 1; + } + hasPressedNumber = false; + break; + default: { + specials.R.val = 'C'; + if (!swipeEnabled) drawKey('R', specials.R); + const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity); + if (isDecimal) { + currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val; + isDecimal = false; + } else { + currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val); + } + if (hasPressedEquals === 1) { + hasPressedEquals = 2; + } + hasPressedNumber = currNumber; + displayOutput(currNumber); + break; + } + } +} + +function moveDirection(d) { + drawKey(selected, screen[selected]); + prevSelected = selected; + selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : screen[selected].trbl[d]; + drawKey(selected, screen[selected], true); +} + +if (process.env.HWVERSION==1) { + setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100}); + setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100}); + setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100}); + setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100}); + setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100}); + swipeEnabled = false; + drawGlobal(); +} else { // touchscreen? + selected = "NONE"; + swipeEnabled = true; + prepareScreen(numbers, numbersGrid, COLORS.DEFAULT); + prepareScreen(operators, operatorsGrid, COLORS.OPERATOR); + prepareScreen(specials, specialsGrid, COLORS.SPECIAL); + drawNumbers(); + + Bangle.setUI({ + mode : 'custom', + back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be) + touch : (n,e)=>{ + for (var key in screen) { + if (typeof screen[key] == "undefined") break; + var r = screen[key].xy; + if (e.x>=r[0] && e.y>=r[1] && e.x { + if (LR == 1) { // right + drawSpecials(); + } + if (LR == -1) { // left + drawOperators(); + } + if (UD == 1) { // down + drawNumbers(); + } + if (UD == -1) { // up + drawNumbers(); + } + } + }); + +} + +displayOutput(0);