From 6a28aaaac6e0ec02728d9598215108d79a046e70 Mon Sep 17 00:00:00 2001 From: MatMoul Date: Sat, 25 Apr 2026 01:43:05 +0200 Subject: [PATCH] feat: add HP48-style RPN calculator sample --- samples/calc-01/index.css | 338 +++++++++++++++++++++ samples/calc-01/index.html | 69 +++++ samples/calc-01/index.js | 594 +++++++++++++++++++++++++++++++++++++ samples/calc-01/visual.txt | 7 + 4 files changed, 1008 insertions(+) create mode 100644 samples/calc-01/index.css create mode 100644 samples/calc-01/index.html create mode 100644 samples/calc-01/index.js create mode 100644 samples/calc-01/visual.txt diff --git a/samples/calc-01/index.css b/samples/calc-01/index.css new file mode 100644 index 0000000..6c885dd --- /dev/null +++ b/samples/calc-01/index.css @@ -0,0 +1,338 @@ +:root { + --body-top: #1d2430; + --body-bottom: #0c1017; + --shell-top: #4b5567; + --shell-bottom: #252d39; + --shell-edge: #141922; + --screen: #dce7b3; + --screen-top: #e8f0c3; + --screen-text: #1c2910; + --screen-dim: #60714a; + --key-text: #f4f7fb; + --key-fn-top: #677287; + --key-fn-bottom: #424b5d; + --key-num-top: #5d6470; + --key-num-bottom: #383f49; + --key-op-top: #4f6b95; + --key-op-bottom: #304661; + --key-danger-top: #7a5050; + --key-danger-bottom: #553636; + --key-enter-top: #7d9079; + --key-enter-bottom: #4d614b; + --border: #11151c; + --shadow: rgba(0, 0, 0, 0.35); +} + +* { box-sizing: border-box; } + +body { + margin: 0; + min-height: 100vh; + font-family: Arial, sans-serif; + background: + radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 30%), + linear-gradient(180deg, var(--body-top), var(--body-bottom)); + color: #f3f6fb; +} + +.wrap { + max-width: 760px; + margin: 0 auto; + padding: 28px 18px 40px; +} + +.calc { + background: linear-gradient(180deg, var(--shell-top), var(--shell-bottom)); + border: 1px solid var(--shell-edge); + border-radius: 28px; + padding: 18px; + box-shadow: + 0 24px 60px var(--shadow), + inset 0 1px 0 rgba(255, 255, 255, 0.12), + inset 0 -2px 0 rgba(0, 0, 0, 0.28); +} + +.brand { + color: #f7f8fc; + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 14px; + gap: 12px; +} + +.brand h1 { + margin: 0; + font-size: 20px; + letter-spacing: 0.08em; +} + +.brand small { + color: #cfd6e2; +} + +.screen { + background: linear-gradient(180deg, var(--screen-top), var(--screen)); + color: var(--screen-text); + border: 3px solid #829366; + border-radius: 14px; + padding: 14px 16px; + min-height: 196px; + font-family: "Courier New", monospace; + display: grid; + grid-template-rows: auto auto 1fr; + gap: 10px; + box-shadow: + inset 0 2px 8px rgba(60, 80, 28, 0.18), + 0 8px 16px rgba(0, 0, 0, 0.18); +} + +.screen-top { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 12px; + color: var(--screen-dim); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.stack { + border-top: 1px solid rgba(28, 41, 16, 0.24); + padding-top: 10px; + line-height: 1.5; + font-size: 18px; + white-space: pre-wrap; +} + +.stack-line { + display: grid; + grid-template-columns: 26px 1fr; + gap: 8px; + padding: 2px 4px; + border-radius: 4px; +} + +.stack-line.selected { + background: rgba(28, 41, 16, 0.12); + outline: 1px dashed rgba(28, 41, 16, 0.4); +} + +.stack-line.moving { + background: rgba(117, 160, 90, 0.2); + outline: 1px solid rgba(28, 41, 16, 0.5); +} + +.stack-line .label { + text-align: right; + color: var(--screen-dim); +} + +.stack-line.selected .label, +.stack-line.moving .label { + color: var(--screen-text); + font-weight: bold; +} + +#display { + font-size: 18px; + font-weight: bold; + letter-spacing: 0.04em; +} + +.hidden-input { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.toolbar { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 14px; + margin-top: 14px; +} + +.status { + display: flex; + flex-wrap: wrap; + gap: 8px; + color: #edf2fa; + font-size: 12px; +} + +.pill { + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 999px; + padding: 6px 10px; + background: rgba(255, 255, 255, 0.06); +} + +.mode-select { + display: grid; + gap: 6px; + min-width: 148px; + font-size: 12px; + color: #d9e1ec; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +select, +button { + border-radius: 12px; + border: 1px solid var(--border); + font: inherit; +} + +select { + padding: 10px 12px; + background: #edf1f7; + color: #111; +} + +.keyboard-layout { + margin-top: 18px; + display: grid; + grid-template-columns: 4fr 3fr 2fr; + gap: 12px; +} + +.key-group { + background: rgba(6, 10, 16, 0.18); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 18px; + padding: 12px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +.group-title { + margin: 0 0 10px; + color: #dce4f0; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.14em; +} + +.key-grid { + display: grid; + gap: 10px; +} + +.functions-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-rows: repeat(5, 56px); +} + +.numbers-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-rows: repeat(5, 56px); +} + +.operators-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(5, 56px); +} + +.key-spacer { + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px dashed rgba(255, 255, 255, 0.04); +} + +button { + padding: 10px 8px; + color: var(--key-text); + cursor: pointer; + font-weight: bold; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.12), + 0 3px 0 rgba(0, 0, 0, 0.25); + transition: filter 120ms ease, transform 120ms ease; +} + +button:hover { filter: brightness(1.08); } +button:active { + transform: translateY(2px); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.key-function { + background: linear-gradient(180deg, var(--key-fn-top), var(--key-fn-bottom)); +} + +.key-number { + background: linear-gradient(180deg, var(--key-num-top), var(--key-num-bottom)); +} + +.key-operator { + background: linear-gradient(180deg, var(--key-op-top), var(--key-op-bottom)); +} + +.key-danger { + background: linear-gradient(180deg, var(--key-danger-top), var(--key-danger-bottom)); +} + +.key-enter { + background: linear-gradient(180deg, var(--key-enter-top), var(--key-enter-bottom)); + grid-row: span 2; +} + +.error { + margin-top: 12px; + min-height: 20px; + color: #ff9f9f; + font-family: "Courier New", monospace; + font-size: 13px; +} + +.hint { + color: #cfd7e3; + margin-top: 10px; + font-size: 12px; + line-height: 1.5; +} + +@media (max-width: 860px) { + .keyboard-layout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .wrap { + padding: 14px; + } + + .calc { + padding: 14px; + border-radius: 22px; + } + + .brand { + flex-direction: column; + align-items: flex-start; + } + + .toolbar { + flex-direction: column; + } + + .mode-select { + min-width: 0; + width: 100%; + } + + .functions-grid, + .numbers-grid, + .operators-grid { + grid-template-rows: repeat(5, 52px); + } +} + diff --git a/samples/calc-01/index.html b/samples/calc-01/index.html new file mode 100644 index 0000000..34a9e6e --- /dev/null +++ b/samples/calc-01/index.html @@ -0,0 +1,69 @@ + + + + + + HP48-style RPN Calculator + + + +
+
+
+

HP48-style RPN

+ powered by src/rpn-calculator.js +
+ +
+
+
RPN stack
+
deg
+
+
+
+
+ + + +
+
+
inputValue:
+
isEditing:
+
+ + +
+ +
+
+
Functions
+
+
+ +
+
Numbers
+
+
+ +
+
Operators
+
+
+
+ +
+
Keyboard: digits, numpad, Enter, Backspace, Delete, Esc, ↑, ↓, →, +, -, *, /, %, ^, q, n, r, i, g, l, s, c, S, C, x, y, z, t
+
+
+ + + + + \ No newline at end of file diff --git a/samples/calc-01/index.js b/samples/calc-01/index.js new file mode 100644 index 0000000..302a5f9 --- /dev/null +++ b/samples/calc-01/index.js @@ -0,0 +1,594 @@ +const calc = new RpnCalculator({ angleMode: 'deg' }); +const input = document.getElementById('input'); +const screen = document.getElementById('screen'); +const stackEl = document.getElementById('stack'); + +const displayEl = document.getElementById('display'); +const errorEl = document.getElementById('error'); +const inputValueLabel = document.getElementById('inputValueLabel'); +const editingLabel = document.getElementById('editingLabel'); +const modeLabel = document.getElementById('modeLabel'); +const angleMode = document.getElementById('angleMode'); + +const keyLayouts = { + functions: [ + [ + { type: 'command', value: 'pi', label: 'π', className: 'key-function' }, + { type: 'command', value: 'e', label: 'e', className: 'key-function' }, + null, + null, + ], + [ + { type: 'command', value: 'sqrt', label: 'sqrt', className: 'key-function' }, + { type: 'command', value: 'pow', label: 'y^x', className: 'key-function' }, + { type: 'command', value: 'sqr', label: 'x²', className: 'key-function' }, + { type: 'command', value: 'recip', label: '1/x', className: 'key-function' }, + ], + [ + { type: 'command', value: 'log', label: 'log', className: 'key-function' }, + { type: 'command', value: 'ln', label: 'ln', className: 'key-function' }, + null, + { type: 'command', value: 'mod', label: '%', className: 'key-function' }, + ], + [ + { type: 'command', value: 'sin', label: 'sin', className: 'key-function' }, + { type: 'command', value: 'cos', label: 'cos', className: 'key-function' }, + { type: 'command', value: 'tan', label: 'tan', className: 'key-function' }, + null, + ], + [ + { type: 'command', value: 'asin', label: 'asin', className: 'key-function' }, + { type: 'command', value: 'acos', label: 'acos', className: 'key-function' }, + { type: 'command', value: 'atan', label: 'atan', className: 'key-function' }, + null, + ], + ], + numbers: [ + [null, null, null], + [ + { type: 'input', value: '7', label: '7', className: 'key-number' }, + { type: 'input', value: '8', label: '8', className: 'key-number' }, + { type: 'input', value: '9', label: '9', className: 'key-number' }, + ], + [ + { type: 'input', value: '4', label: '4', className: 'key-number' }, + { type: 'input', value: '5', label: '5', className: 'key-number' }, + { type: 'input', value: '6', label: '6', className: 'key-number' }, + ], + [ + { type: 'input', value: '1', label: '1', className: 'key-number' }, + { type: 'input', value: '2', label: '2', className: 'key-number' }, + { type: 'input', value: '3', label: '3', className: 'key-number' }, + ], + [ + { type: 'input', value: '0', label: '0', className: 'key-number' }, + { type: 'input', value: '.', label: '.', className: 'key-number' }, + { type: 'command', value: 'neg', label: '+/−', className: 'key-number' }, + ], + ], + operators: [ + [ + { type: 'command', value: 'clear', label: 'del', className: 'key-danger' }, + { type: 'action', value: 'escape', label: 'esc', className: 'key-danger' }, + ], + [ + { type: 'command', value: 'div', label: '/', className: 'key-operator' }, + { type: 'command', value: 'drop', label: 'back', className: 'key-operator' }, + ], + [ + { type: 'command', value: 'mul', label: '*', className: 'key-operator' }, + null, + ], + [ + { type: 'command', value: 'sub', label: '-', className: 'key-operator' }, + { type: 'command', value: 'enter', label: 'Enter', className: 'key-enter' }, + ], + [ + { type: 'command', value: 'add', label: '+', className: 'key-operator' }, + null, + ], + ], +}; + +let stackCursor = null; +let isMovingStackItem = false; +let stackSnapshotBeforeMove = null; +let stackViewOffset = 0; +let editRestoreValue = null; + +function handleEscapeAction() { + if (calc.isEditing) { + if (editRestoreValue !== null) { + calc.push(editRestoreValue); + editRestoreValue = null; + } + calc.inputValue = ''; + calc.isEditing = false; + syncInputFromState(); + render(); + return; + } + + if (isMovingStackItem) { + cancelMoveMode(); + clearStackSelection(); + render(); + return; + } + + if (hasStackSelection()) { + clearStackSelection(); + render(); + } +} + +function pressKey(key) { + clearStackSelection(); + editXWithKey(key); + render(); +} + +function createButton(cell) { + if (!cell) { + const spacer = document.createElement('div'); + spacer.className = 'key-spacer'; + spacer.setAttribute('aria-hidden', 'true'); + return spacer; + } + + const button = document.createElement('button'); + button.type = 'button'; + button.textContent = cell.label; + button.className = cell.className; + button.addEventListener('click', () => { + focusScreen(); + if (cell.type === 'input') { + pressKey(cell.value); + return; + } + if (cell.type === 'action' && cell.value === 'escape') { + handleEscapeAction(); + return; + } + execute(cell.value); + }); + return button; +} + +function renderKeyLayout(container, rows) { + container.innerHTML = ''; + rows.flat().forEach((cell) => { + container.appendChild(createButton(cell)); + }); +} + +function getStackValue(index) { + return calc.isValidIndex(index) ? calc.stack[index] : undefined; +} + +function getDisplayValue(index) { + if (calc.isEditing) { + if (index === 0) { + return calc.inputValue; + } + return getStackValue(index - 1); + } + return getStackValue(index); +} + +function hasStackSelection() { + return stackCursor !== null && calc.isValidIndex(stackCursor); +} + +function clearStackSelection() { + stackCursor = null; + isMovingStackItem = false; + stackSnapshotBeforeMove = null; + stackViewOffset = 0; +} + +function ensureValidSelection() { + if (hasStackSelection()) { + return; + } + stackCursor = calc.isValidIndex(0) ? 0 : null; +} + +function beginMoveMode() { + if (!hasStackSelection()) { + return; + } + isMovingStackItem = true; + stackSnapshotBeforeMove = calc.stack.slice(); +} + +function commitMoveMode() { + isMovingStackItem = false; + stackSnapshotBeforeMove = null; +} + +function cancelMoveMode() { + if (!isMovingStackItem || !stackSnapshotBeforeMove) { + return; + } + + const snapshot = stackSnapshotBeforeMove.slice(); + calc.clear(); + for (let index = snapshot.length - 1; index >= 0; index -= 1) { + calc.push(snapshot[index]); + } + + isMovingStackItem = false; + stackSnapshotBeforeMove = null; + stackCursor = calc.isValidIndex(stackCursor) ? stackCursor : (calc.isValidIndex(0) ? 0 : null); + syncInputFromState(); +} + +function reactivateEditOnX() { + clearStackSelection(); + if (calc.isValidIndex(0)) { + const value = getStackValue(0); + calc.remove(0); + calc.inputValue = calc.formatNumber(value); + calc.isEditing = true; + editRestoreValue = value; + } else { + calc.inputValue = ''; + calc.isEditing = true; + editRestoreValue = null; + } + syncInputFromState(); +} + +function moveStackSelection(direction) { + if (!hasStackSelection()) { + if (direction === 'up') { + ensureValidSelection(); + } else if (direction === 'down') { + reactivateEditOnX(); + } + return; + } + + const nextIndex = direction === 'up' ? stackCursor + 1 : stackCursor - 1; + if (calc.isValidIndex(nextIndex)) { + stackCursor = nextIndex; + } +} + +function moveStackItem(direction) { + if (!hasStackSelection()) { + return; + } + + const targetIndex = direction === 'up' ? stackCursor + 1 : stackCursor - 1; + if (!calc.isValidIndex(targetIndex)) { + return; + } + + calc.swap(stackCursor, targetIndex); + stackCursor = targetIndex; +} + +function getVisibleStackIndex(visualLine) { + return stackViewOffset + visualLine; +} + +function clampStackViewOffset() { + const maxOffset = Math.max(0, calc.stack.length - 4); + if (stackViewOffset < 0) { + stackViewOffset = 0; + } else if (stackViewOffset > maxOffset) { + stackViewOffset = maxOffset; + } +} + +function ensureSelectionVisible() { + if (!hasStackSelection()) { + stackViewOffset = 0; + return; + } + + if (stackCursor < stackViewOffset) { + stackViewOffset = stackCursor; + } else if (stackCursor > stackViewOffset + 3) { + stackViewOffset = stackCursor - 3; + } + + clampStackViewOffset(); +} + +function render() { + const names = ['T', 'Z', 'Y', 'X']; + const lines = []; + const showStackIndexes = hasStackSelection() || isMovingStackItem; + + clampStackViewOffset(); + ensureSelectionVisible(); + + for (let visualLine = 3; visualLine >= 0; visualLine -= 1) { + const stackIndex = getVisibleStackIndex(visualLine); + const value = getDisplayValue(stackIndex); + const isSelected = stackCursor === stackIndex; + const classes = ['stack-line']; + const label = showStackIndexes ? String(stackIndex) : names[3 - visualLine]; + + if (isSelected) { + classes.push(isMovingStackItem ? 'moving' : 'selected'); + } + + lines.push(`
${label}
${value !== undefined && value !== '' ? calc.formatNumber(value) : ''}
`); + } + + stackEl.innerHTML = lines.join(''); + if (calc.isEditing) { + displayEl.textContent = `ENTERING: ${calc.inputValue}`; + } else if (isMovingStackItem && hasStackSelection()) { + displayEl.textContent = `MOVING: ${stackCursor}`; + } else if (hasStackSelection()) { + displayEl.textContent = `SELECTED: ${stackCursor}`; + } else { + displayEl.textContent = 'READY'; + } + inputValueLabel.textContent = calc.inputValue || '∅'; + editingLabel.textContent = String(calc.isEditing); + modeLabel.textContent = calc.angleMode; + angleMode.value = calc.angleMode; + errorEl.textContent = ''; +} + +function pushEditingValueIfNeeded() { + if (!calc.isEditing) return; + if (calc.inputValue !== '') { + const value = calc.parseInputValue(calc.inputValue); + calc.push(value); + } + calc.inputValue = ''; + calc.isEditing = false; + editRestoreValue = null; + syncInputFromState(); +} + +function execute(name) { + try { + if (name === 'enter') { + if (calc.isEditing) { + pushEditingValueIfNeeded(); + } else if (calc.isValidIndex(0)) { + calc.push(getStackValue(0)); + } + } else if (name === 'swap') { + pushEditingValueIfNeeded(); + clearStackSelection(); + if (calc.isValidIndex(1)) calc.swap(0, 1); + } else if (name === 'drop') { + pushEditingValueIfNeeded(); + if (hasStackSelection()) { + calc.remove(stackCursor); + stackCursor = calc.isValidIndex(stackCursor) ? stackCursor : (calc.isValidIndex(stackCursor - 1) ? stackCursor - 1 : null); + } else if (calc.isValidIndex(0)) { + calc.remove(0); + } + commitMoveMode(); + } else if (name === 'clear') { + calc.clear(); + clearStackSelection(); + } else { + pushEditingValueIfNeeded(); + clearStackSelection(); + calc.command(name); + } + syncInputFromState(); + render(); + } catch (error) { + errorEl.textContent = error.message; + } +} + +function isInputChar(key) { + return /^[0-9a-fA-F.]$/.test(key); +} + +function shouldIgnoreKeyboardEvent(event) { + const target = event.target; + if (!target) return false; + + const tagName = target.tagName; + return ( + tagName === 'INPUT' || + tagName === 'TEXTAREA' || + tagName === 'SELECT' || + target.isContentEditable + ); +} + +function getKeyboardAction(event) { + const numpadMap = { + Numpad0: { type: 'input', value: '0' }, + Numpad1: { type: 'input', value: '1' }, + Numpad2: { type: 'input', value: '2' }, + Numpad3: { type: 'input', value: '3' }, + Numpad4: { type: 'input', value: '4' }, + Numpad5: { type: 'input', value: '5' }, + Numpad6: { type: 'input', value: '6' }, + Numpad7: { type: 'input', value: '7' }, + Numpad8: { type: 'input', value: '8' }, + Numpad9: { type: 'input', value: '9' }, + NumpadDecimal: { type: 'input', value: '.' }, + NumpadAdd: { type: 'command', value: 'add' }, + NumpadSubtract: { type: 'command', value: 'sub' }, + NumpadMultiply: { type: 'command', value: 'mul' }, + NumpadDivide: { type: 'command', value: 'div' }, + NumpadEnter: { type: 'command', value: 'enter' }, + }; + + if (numpadMap[event.code]) { + return numpadMap[event.code]; + } + + if (isInputChar(event.key)) { + return { type: 'input', value: event.key }; + } + + const keyMap = { + Enter: { type: 'enterKey' }, + Backspace: { type: 'stackOrEdit', value: 'drop' }, + Delete: { type: 'command', value: 'clear' }, + Escape: { type: 'escapeKey' }, + ArrowUp: { type: 'stackArrow', value: 'up' }, + ArrowDown: { type: 'stackArrow', value: 'down' }, + ArrowRight: { type: 'command', value: 'swap' }, + '+': { type: 'command', value: 'add' }, + '-': { type: 'command', value: 'sub' }, + '*': { type: 'command', value: 'mul' }, + '/': { type: 'command', value: 'div' }, + '%': { type: 'command', value: 'mod' }, + '^': { type: 'command', value: 'pow' }, + q: { type: 'command', value: 'sqr' }, + n: { type: 'command', value: 'neg' }, + r: { type: 'command', value: 'sqrt' }, + i: { type: 'command', value: 'recip' }, + g: { type: 'command', value: 'log' }, + l: { type: 'command', value: 'ln' }, + s: { type: 'command', value: 'sin' }, + c: { type: 'command', value: 'cos' }, + S: { type: 'command', value: 'asin' }, + C: { type: 'command', value: 'acos' }, + x: { type: 'stackSelect', value: 0 }, + y: { type: 'stackSelect', value: 1 }, + z: { type: 'stackSelect', value: 2 }, + t: { type: 'stackSelect', value: 3 }, + X: { type: 'stackSelect', value: 0 }, + Y: { type: 'stackSelect', value: 1 }, + Z: { type: 'stackSelect', value: 2 }, + T: { type: 'stackSelect', value: 3 }, + }; + + return keyMap[event.key] || null; +} + +function focusScreen() { + screen.focus(); +} + +function syncInputFromState() { + input.value = calc.inputValue; +} + +function editXWithKey(key) { + if (!calc.isEditing) { + pushEditingValueIfNeeded(); + calc.isEditing = true; + calc.inputValue = ''; + editRestoreValue = null; + } + if (key === 'Backspace') { + calc.inputValue = calc.inputValue.slice(0, -1); + } else { + calc.inputValue += key; + } + if (calc.inputValue === '') { + calc.isEditing = false; + editRestoreValue = null; + } + syncInputFromState(); +} + +function handleKeydown(event) { + if (shouldIgnoreKeyboardEvent(event)) { + return; + } + + const action = getKeyboardAction(event); + if (!action) { + return; + } + + try { + if (action.type === 'escapeKey') { + event.preventDefault(); + handleEscapeAction(); + return; + } + + event.preventDefault(); + + if (action.type === 'input') { + clearStackSelection(); + editXWithKey(action.value); + render(); + return; + } + + if (action.type === 'stackSelect') { + if (calc.isEditing || isMovingStackItem) { + return; + } + stackCursor = calc.isValidIndex(action.value) ? action.value : null; + render(); + return; + } + + if (action.type === 'stackArrow') { + if (calc.isEditing) { + render(); + return; + } + + if (isMovingStackItem) { + moveStackItem(action.value); + } else { + moveStackSelection(action.value); + } + render(); + return; + } + + if (action.type === 'stackOrEdit') { + if (calc.isEditing) { + editXWithKey('Backspace'); + render(); + } else { + execute(action.value); + } + return; + } + + if (action.type === 'enterKey') { + if (hasStackSelection()) { + if (isMovingStackItem) { + commitMoveMode(); + } else { + beginMoveMode(); + } + render(); + return; + } + execute('enter'); + return; + } + + if (action.type === 'command') { + execute(action.value); + } + } catch (error) { + errorEl.textContent = error.message; + } +} + +window.addEventListener('keydown', handleKeydown); + +screen.addEventListener('click', focusScreen); +window.addEventListener('load', focusScreen); + +angleMode.addEventListener('change', (event) => { + calc.angleMode = event.target.value; + render(); +}); + +renderKeyLayout(document.getElementById('functionsButtons'), keyLayouts.functions); +renderKeyLayout(document.getElementById('numbersButtons'), keyLayouts.numbers); +renderKeyLayout(document.getElementById('operatorsButtons'), keyLayouts.operators); + +render(); +focusScreen(); diff --git a/samples/calc-01/visual.txt b/samples/calc-01/visual.txt new file mode 100644 index 0000000..4732b1a --- /dev/null +++ b/samples/calc-01/visual.txt @@ -0,0 +1,7 @@ +┌───────────── Functions ────────────┬──── Numbers ────┬─── Operators ───┐ +| consts | | | | | | | del | esc | +| sqrt | y^x | x² | 1/x | 7 | 8 | 9 | / | backspace | +| log | ln | | % | 4 | 5 | 6 | * | | +| sin | cos | tan | | 1 | 2 | 3 | - | Enter | +| asin | acos | atan | | 0 | . | +/- | + | Enter | +└────────────────────────────────────┴─────────────────┴─────────────────┘