/** * BangleJS Calculator * * Original Author: Frederic Rousseau https://github.com/fredericrous * Created: April 2020 * * Contributors: thyttan https://github.com/thyttan */ g.clear(); require("FontDylex7x13").add(Graphics); var DEFAULT_SELECTION_NUMBERS = '5'; var RESULT_HEIGHT = 40; 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.0B'}, '.': {grid: [2, 3], globalGrid: [2, 4], trbl: '3=.0'}, '1': {grid: [0, 2], globalGrid: [0, 3], trbl: '42B1'}, '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'}, 'B': {grid: [0, 3], globalGrid: [0, 4], trbl: '10BB', val: '<-', color: COLORS.SPECIAL}, }; 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: '+==.'}, }; if (process.env.HWVERSION!=1) { operatorsGrid = [2, 4]; operators['='].grid = [1, 3]; operators.r = {grid: [0, 2], val: 'sqrt'}; operators.s = {grid: [1, 2], val: 'x^2'}; operators.i = {grid: [0, 3], val: '1/x'}; } var scientificOperatorsGrid = [2, 4]; var scientificOperators = { 'sin': {grid: [0, 0], val: 'sin'}, 'cos': {grid: [1, 0], val: 'cos'}, 'tan': {grid: [0, 1], val: 'tan'}, 'angleMode': {grid: [1, 1], val: 'deg'}, 'log': {grid: [0, 2], val: 'log'}, 'tenpow': {grid: [1, 2], val: '10^x'}, 'ln': {grid: [0, 3], val: 'ln'}, 'epow': {grid: [1, 3], val: 'e^x'}, }; 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; var angleMode = 'deg'; 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('Dylex7x13', 2).setFontAlign(0,0); g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); g.setColor(0); 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 drawScientificOperators() { screen = scientificOperators; screenColor =COLORS.OPERATOR; drawKeys(); } function drawSpecials() { screen = specials; screenColor = COLORS.SPECIAL; drawKeys(); } function toExponential(num, precision) { if (num === 0) { let z = "0"; if (precision > 0) z += "." + "0".repeat(precision); return z + "e+0"; } var sign = ""; if (num < 0) { sign = "-"; num = -num; } var exp = Math.floor(Math.log(num) / Math.LN10); var mantissa = num / Math.pow(10, exp); var mantissaFixed = mantissa.toFixed(precision); if (parseFloat(mantissaFixed) >= 10) { mantissaFixed = (mantissa/10).toFixed(precision); exp++; } return sign + mantissaFixed + "e" + (exp >= 0 ? "+" : "") + exp; } function fixFloat(n) { if (Math.abs(n) < 1e-10) return 0; return n; } function addSeparators(s) { var parts = s.split("."); var intPart = parts[0]; var sign = ""; if (intPart[0] === "-") { sign = "-"; intPart = intPart.slice(1); } var result = ""; while (intPart.length > 3) { result = "," + intPart.slice(-3) + result; intPart = intPart.slice(0, -3); } result = intPart + result; parts[0] = sign + result; return parts.join("."); } function doMath(x, y, operator) { x = parseFloat(x); y = parseFloat(y); switch (operator) { case '/': return x / y; case '*': return x * y; case '+': return x + y; case '-': return x - y; } } function displayOutput(num) { g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1); g.setColor(-1); g.setFont('Dylex7x13', 2); if (num === Infinity || num === -Infinity || isNaN(num)) { // handle division by 0 if (num === Infinity) { num = 'INF'; } else if (num === -Infinity) { num = '-INF'; } else { num = 'NaN'; } currNumber = null; results = null; isDecimal = false; hasPressedEquals = false; prevNumber = null; operator = null; specials.R.val = 'AC'; if (!swipeEnabled) drawKey('R', specials.R); } 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); } } var numStr = num.toString(); var displayStr; if (typeof num === 'number' && (g.stringWidth(numStr) > g.getWidth() - 20 || (num !== 0 && Math.abs(num) < 1e-4))) { // try to format as scientific notation let precision = 10; // start with high precision while (precision >= 0) { let scientificStr = toExponential(num, precision).replace("e", "E"); if (g.stringWidth(scientificStr) <= g.getWidth() - 20) { displayStr = scientificStr; break; } precision--; } if (precision < 0) { // if it still doesn't fit displayStr = toExponential(num, 0).replace("e", "E"); } } else { displayStr = addSeparators(numStr); if (g.stringWidth(displayStr) > g.getWidth() - 20) { displayStr = numStr; } } num = displayStr; // final check for truncation if (g.stringWidth(num) > g.getWidth() - 20) { while(g.stringWidth(num+'...') > g.getWidth() - 20 && num.length > 1) { num = num.slice(0,-1); } num += '...'; } if (num.charAt(0) === '-') { num = '- ' + num.substr(1); } } g.setFontAlign(1,0); g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); if (operator) { g.setFont('Dylex7x13', 2).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 'r': if (results != null) { results = Math.sqrt(results); displayOutput(results); } else if (currNumber != null) { currNumber = Math.sqrt(currNumber); displayOutput(currNumber); } hasPressedNumber = false; break; case 's': if (results != null) { results = results * results; displayOutput(results); } else if (currNumber != null) { var num = parseFloat(currNumber); currNumber = num * num; displayOutput(currNumber); } hasPressedNumber = false; break; case 'sin': if (results != null) { let angle = results; if (angleMode === 'deg') { angle = angle * Math.PI / 180; } results = fixFloat(Math.sin(angle)); displayOutput(results); } else if (currNumber != null) { let angle = currNumber; if (angleMode === 'deg') { angle = angle * Math.PI / 180; } currNumber = fixFloat(Math.sin(angle)); displayOutput(currNumber); } hasPressedNumber = false; break; case 'cos': if (results != null) { let angle = results; if (angleMode === 'deg') { angle = angle * Math.PI / 180; } results = fixFloat(Math.cos(angle)); displayOutput(results); } else if (currNumber != null) { let angle = currNumber; if (angleMode === 'deg') { angle = angle * Math.PI / 180; } currNumber = fixFloat(Math.cos(angle)); displayOutput(currNumber); } hasPressedNumber = false; break; case 'tan': if (results != null) { let angle = results; if (angleMode === 'deg') { angle = angle * Math.PI / 180; } results = fixFloat(Math.tan(angle)); displayOutput(results); } else if (currNumber != null) { let angle = currNumber; if (angleMode === 'deg') { angle = angle * Math.PI / 180; } currNumber = fixFloat(Math.tan(angle)); displayOutput(currNumber); } hasPressedNumber = false; break; case 'log': if (results != null) { results = Math.log(results) / Math.LN10; displayOutput(results); } else if (currNumber != null) { currNumber = Math.log(currNumber) / Math.LN10; displayOutput(currNumber); } hasPressedNumber = false; break; case 'tenpow': if (results != null) { results = Math.pow(10, results); displayOutput(results); } else if (currNumber != null) { currNumber = Math.pow(10, currNumber); displayOutput(currNumber); } hasPressedNumber = false; break; case 'ln': if (results != null) { results = Math.log(results); displayOutput(results); } else if (currNumber != null) { currNumber = Math.log(currNumber); displayOutput(currNumber); } hasPressedNumber = false; break; case 'epow': if (results != null) { results = Math.exp(results); displayOutput(results); } else if (currNumber != null) { currNumber = Math.exp(currNumber); displayOutput(currNumber); } hasPressedNumber = false; break; case 'angleMode': if (angleMode === 'rad') { angleMode = 'deg'; } else { angleMode = 'rad'; } scientificOperators.angleMode.val = angleMode; drawKey('angleMode', scientificOperators.angleMode); break; case 'i': if (results != null) { results = 1 / results; displayOutput(results); } else if (currNumber != null) { currNumber = 1 / parseFloat(currNumber); displayOutput(currNumber); } hasPressedNumber = false; break; case 'N': if (results != null) { displayOutput(results *= -1); } else { displayOutput(currNumber *= -1); } break; case 'B': if (isDecimal) { isDecimal = false; displayOutput(currNumber); break; } if (currNumber != null) { currNumber = currNumber.toString(); if (currNumber.length > 1) { currNumber = currNumber.slice(0, -1); } else { currNumber = '0'; } // if we removed a decimal point if (currNumber.indexOf('.') === -1) { isDecimal = false; } hasPressedNumber = currNumber; displayOutput(currNumber); } 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 : parseInt(currNumber, 10) + '.' + 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(scientificOperators, scientificOperatorsGrid, 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 (UD !== 0) { drawNumbers(); return; } if (LR === 1) { // right if (screen === scientificOperators) drawOperators(); else if (screen === operators) drawNumbers(); else if (screen === numbers) drawSpecials(); else if (screen === specials) drawNumbers(); } if (LR === -1) { // left if (screen === numbers) drawOperators(); else if (screen === operators) drawScientificOperators(); else if (screen === specials) drawNumbers(); else if (screen === scientificOperators) drawNumbers(); } } }); } displayOutput(0);