diff --git a/samples/dev/index.css b/samples/dev/index.css new file mode 100644 index 0000000..abf3c72 --- /dev/null +++ b/samples/dev/index.css @@ -0,0 +1,204 @@ +:root { + --body: #d8d8d8; + --panel: #202020; + --panel-2: #2b2b2b; + --screen: #d8e7b8; + --screen-text: #1b2a12; + --screen-dim: #5b6f45; + --key: #3a3a3a; + --key-text: #f2f2f2; + --accent: #8cff6d; + --border: #111; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: Arial, sans-serif; + background: linear-gradient(180deg, #efefef, var(--body)); + color: #111; +} + +.wrap { + max-width: 980px; + margin: 0 auto; + padding: 24px; +} + +.calc { + background: linear-gradient(180deg, #2f2f2f, #1f1f1f); + border: 1px solid #111; + border-radius: 20px; + padding: 18px; + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.25); +} + +.brand { + color: #fafafa; + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 12px; + gap: 12px; +} + +.brand h1 { + margin: 0; + font-size: 18px; + letter-spacing: 0.06em; +} + +.brand small { + color: #c9c9c9; +} + +.screen { + background: linear-gradient(180deg, #dbe8b8, var(--screen)); + color: var(--screen-text); + border: 2px inset #8aa36b; + border-radius: 10px; + padding: 14px; + min-height: 190px; + font-family: "Courier New", monospace; + display: grid; + grid-template-rows: auto auto 1fr; + gap: 10px; +} + +.screen-top { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 12px; + color: var(--screen-dim); +} + +.stack { + border-top: 1px solid rgba(27, 42, 18, 0.35); + 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: 1px 4px; + border-radius: 4px; +} + +.stack-line.selected { + background: rgba(27, 42, 18, 0.14); + outline: 1px dashed rgba(27, 42, 18, 0.45); +} + +.stack-line.moving { + background: rgba(140, 255, 109, 0.18); + outline: 1px solid rgba(27, 42, 18, 0.55); +} + +.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; +} + +.hidden-input { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.input-row { + display: grid; + grid-template-columns: 1fr 150px; + gap: 12px; + margin-top: 14px; +} + +input, select, button { + border-radius: 10px; + border: 1px solid #000; + font: inherit; +} + +input, select { + padding: 12px 14px; + background: #f7f7f7; + color: #111; +} + +.panel { + margin-top: 14px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 14px; + padding: 14px; +} + +.title { + color: #fff; + margin: 0 0 10px; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.buttons { + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); +} + +button { + padding: 12px 10px; + background: linear-gradient(180deg, #4a4a4a, var(--key)); + color: var(--key-text); + cursor: pointer; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +button:hover { filter: brightness(1.08); } +button:active { transform: translateY(1px); } + +.status { + margin-top: 12px; + display: flex; + flex-wrap: wrap; + gap: 10px; + color: #ececec; + font-size: 13px; +} + +.pill { + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 999px; + padding: 5px 10px; + background: rgba(255, 255, 255, 0.05); +} + +.error { + margin-top: 10px; + min-height: 20px; + color: #ff8a8a; + font-family: "Courier New", monospace; + font-size: 13px; +} + +.hint { + color: #ddd; + margin-top: 10px; + font-size: 13px; + line-height: 1.5; +} diff --git a/samples/dev/index.html b/samples/dev/index.html index 813546f..9c89336 100644 --- a/samples/dev/index.html +++ b/samples/dev/index.html @@ -4,212 +4,7 @@ HP48-style RPN Calculator - +
@@ -270,443 +65,6 @@
- + \ No newline at end of file diff --git a/samples/dev/index.js b/samples/dev/index.js new file mode 100644 index 0000000..11019a2 --- /dev/null +++ b/samples/dev/index.js @@ -0,0 +1,436 @@ +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 groups = { + stack: ['enter', 'dup', 'drop', 'swap', 'clear'], + arithmetic: ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'sqr', 'neg', 'sqrt', 'recip', 'log', 'ln'], + trig: ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'], + const: ['pi', 'e'], +}; + +let stackCursor = null; +let isMovingStackItem = false; +let stackSnapshotBeforeMove = null; + +function labelFor(command) { + return ({ add: '+', sub: '−', mul: '×', div: '÷', pow: 'y^x', recip: '1/x', sqr: 'x²' }[command] || command); +} + +function addButtons(container, commands) { + container.innerHTML = ''; + commands.forEach((commandName) => { + const button = document.createElement('button'); + button.textContent = labelFor(commandName); + button.addEventListener('click', () => execute(commandName)); + container.appendChild(button); + }); +} + +function getStackValue(index) { + return calc.isValidIndex(index) ? calc.stack[index] : undefined; +} + +function getLineValue(line) { + if (calc.isEditing) { + if (line === 0) { + return calc.inputValue; + } + return getStackValue(line - 1); + } + return getStackValue(line); +} + +function hasStackSelection() { + return stackCursor !== null && calc.isValidIndex(stackCursor); +} + +function clearStackSelection() { + stackCursor = null; + isMovingStackItem = false; + stackSnapshotBeforeMove = null; +} + +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; + } else { + calc.inputValue = ''; + calc.isEditing = true; + } + syncInputFromState(); +} + +function moveStackSelection(direction) { + if (!hasStackSelection()) { + if (direction === 'up') { + ensureValidSelection(); + } else { + reactivateEditOnX(); + } + return; + } + + const nextIndex = direction === 'up' ? stackCursor + 1 : stackCursor - 1; + if (calc.isValidIndex(nextIndex)) { + stackCursor = nextIndex; + return; + } + + if (direction === 'down' && stackCursor === 0) { + reactivateEditOnX(); + } +} + +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 render() { + const names = ['T', 'Z', 'Y', 'X']; + const lines = []; + for (let line = 3; line >= 0; line -= 1) { + const value = getLineValue(line); + const isSelected = stackCursor === line; + const classes = ['stack-line']; + if (isSelected) { + classes.push(isMovingStackItem ? 'moving' : 'selected'); + } + lines.push(`
${names[3 - line]}
${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: ${['X', 'Y', 'Z', 'T'][stackCursor] || '?'}`; + } else if (hasStackSelection()) { + displayEl.textContent = `SELECTED: ${['X', 'Y', 'Z', 'T'][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; + 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 = ''; + } + if (key === 'Backspace') { + calc.inputValue = calc.inputValue.slice(0, -1); + } else { + calc.inputValue += key; + } + if (calc.inputValue === '') { + calc.isEditing = false; + } + syncInputFromState(); +} + +function handleKeydown(event) { + if (shouldIgnoreKeyboardEvent(event)) { + return; + } + + const action = getKeyboardAction(event); + if (!action) { + return; + } + + try { + if (action.type === 'escapeKey') { + if (calc.isEditing) { + event.preventDefault(); + calc.inputValue = ''; + calc.isEditing = false; + syncInputFromState(); + render(); + return; + } + if (isMovingStackItem) { + event.preventDefault(); + cancelMoveMode(); + render(); + } + 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 (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(); +}); + +addButtons(document.getElementById('stackButtons'), groups.stack); +addButtons(document.getElementById('arithButtons'), groups.arithmetic); +addButtons(document.getElementById('trigButtons'), groups.trig); +addButtons(document.getElementById('constButtons'), groups.const); + +render(); +focusScreen(); diff --git a/samples/hp48/visual.txt b/samples/dev/visual.txt similarity index 100% rename from samples/hp48/visual.txt rename to samples/dev/visual.txt