Files
mtm-rpn-js/samples/hp48/index.html
T

444 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HP48-style RPN Calculator</title>
<style>
: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;
}
.stack-line .label {
text-align: right;
color: var(--screen-dim);
}
.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;
}
</style>
</head>
<body>
<div class="wrap">
<div class="calc">
<div class="brand">
<h1>HP48-style RPN</h1>
<small>powered by src/rpn-calculator.js</small>
</div>
<div class="screen" id="screen" tabindex="0" role="application" aria-label="HP48 style calculator screen">
<div class="screen-top">
<div>RPN stack</div>
<div id="modeLabel">deg</div>
</div>
<div id="stack" class="stack"></div>
<div id="display"></div>
</div>
<input id="input" class="hidden-input" type="text" autocomplete="off" aria-hidden="true" tabindex="-1">
<div class="input-row">
<div class="hint">Keyboard input is captured directly by the screen</div>
<select id="angleMode">
<option value="deg">Degrees</option>
<option value="rad">Radians</option>
<option value="grad">Grads</option>
</select>
</div>
<div class="status">
<div class="pill">inputValue: <span id="inputValueLabel"></span></div>
<div class="pill">isEditing: <span id="editingLabel"></span></div>
</div>
<div class="panel">
<div class="title">Stack</div>
<div class="buttons" id="stackButtons"></div>
</div>
<div class="panel">
<div class="title">Arithmetic</div>
<div class="buttons" id="arithButtons"></div>
</div>
<div class="panel">
<div class="title">Trigonometry</div>
<div class="buttons" id="trigButtons"></div>
</div>
<div class="panel">
<div class="title">Constants</div>
<div class="buttons" id="constButtons"></div>
</div>
<div id="error" class="error"></div>
<div class="hint">Use Enter to commit the current value. Buttons call <code>command(...)</code> directly, like a real RPN demo.</div>
</div>
</div>
<script src="../../src/rpn-calculator.js"></script>
<script>
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'],
};
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 getLineValue(line) {
if (line === 0 && calc.isEditing) {
return calc.inputValue;
}
return calc.stack[line];
}
function render() {
const names = ['T', 'Z', 'Y', 'X'];
const lines = [];
for (let line = 3; line >= 0; line -= 1) {
const value = getLineValue(line);
lines.push(`<div class="stack-line"><div class="label">${names[3 - line]}</div><div>${value !== undefined && value !== '' ? calc.formatNumber(value) : ''}</div></div>`);
}
stackEl.innerHTML = lines.join('');
displayEl.textContent = calc.isEditing ? `ENTERING: ${calc.inputValue}` : '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);
if (calc.stack.length >= calc.maxSize) {
throw new Error('Stack overflow');
}
calc.stack.unshift(value);
if (calc.stack.length > 4) calc.stack.length = 4;
}
calc.inputValue = '';
calc.isEditing = false;
syncInputFromState();
}
function execute(name) {
try {
if (name === 'swap') {
pushEditingValueIfNeeded();
if (calc.stack.length >= 2) calc.swap(0, 1);
} else if (name === 'drop') {
pushEditingValueIfNeeded();
if (calc.stack.length >= 1) calc.remove(0);
} else if (name === 'clear') {
calc.clear();
} else if (name === 'enter') {
if (calc.isEditing) {
pushEditingValueIfNeeded();
}
} else {
pushEditingValueIfNeeded();
calc.command(name);
}
syncInputFromState();
render();
} catch (error) {
errorEl.textContent = error.message;
}
}
function isInputChar(key) {
return /^[0-9a-fA-F.+\-]$/.test(key);
}
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();
}
screen.addEventListener('keydown', (event) => {
try {
if (event.key === 'Enter') {
event.preventDefault();
if (calc.isEditing) {
calc.command('enter');
}
render();
return;
}
if (event.key === 'Backspace') {
event.preventDefault();
if (calc.isEditing) {
editXWithKey('Backspace');
render();
}
return;
}
if (isInputChar(event.key)) {
event.preventDefault();
editXWithKey(event.key);
render();
return;
}
const keyMap = {
'+': 'add',
'-': 'sub',
'*': 'mul',
'/': 'div',
'%': 'mod',
'^': 'pow',
};
if (keyMap[event.key]) {
event.preventDefault();
execute(keyMap[event.key]);
}
} catch (error) {
errorEl.textContent = error.message;
}
});
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();
</script>
</body>
</html>