feat: add browser RPN calculator engine and demo

This commit is contained in:
2026-04-22 22:08:01 +02:00
parent 707c6f3f73
commit e23373fee0
4 changed files with 851 additions and 1 deletions
+308
View File
@@ -0,0 +1,308 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RPN Calculator Demo</title>
<style>
:root {
color-scheme: light dark;
--bg: #f4f4f4;
--panel: #ffffff;
--text: #111;
--muted: #666;
--button: #e9e9e9;
--button-text: #111;
--border: #d0d0d0;
--accent: #0a7;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--panel: #1a1a1a;
--text: #f3f3f3;
--muted: #aaa;
--button: #2a2a2a;
--button-text: #f3f3f3;
--border: #333;
--accent: #3dc;
}
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.app {
max-width: 860px;
margin: 24px auto;
padding: 16px;
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
}
.display {
background: #000;
color: #0f0;
border-radius: 10px;
padding: 12px;
font-family: monospace;
min-height: 72px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(92px, 1fr));
gap: 8px;
margin-top: 12px;
}
button, select, input {
border: 1px solid var(--border);
background: var(--button);
color: var(--button-text);
border-radius: 8px;
padding: 10px 12px;
font-size: 15px;
}
button {
cursor: pointer;
}
input, select {
width: 100%;
box-sizing: border-box;
background: var(--panel);
color: var(--text);
font-size: 16px;
}
.row {
display: grid;
grid-template-columns: 1fr 180px;
gap: 12px;
margin-top: 12px;
align-items: end;
}
.stack {
margin-top: 12px;
font-family: monospace;
line-height: 1.5;
}
.muted {
color: var(--muted);
font-size: 14px;
}
.section-title {
margin: 16px 0 8px;
font-size: 14px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.status {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 12px;
color: var(--muted);
font-size: 14px;
}
.badge {
border: 1px solid var(--border);
border-radius: 999px;
padding: 4px 10px;
background: rgba(0, 0, 0, 0.04);
}
.accent {
color: var(--accent);
}
</style>
</head>
<body>
<div class="app">
<div class="card">
<h1>RPN Calculator Demo</h1>
<div id="display" class="display"></div>
<div id="stack" class="stack"></div>
<div class="row">
<label>
<div class="section-title">Input</div>
<input id="input" type="text" placeholder="Type a number, pi, e, or a command, then press Enter" autocomplete="off">
</label>
<label>
<div class="section-title">Angle mode</div>
<select id="angleMode">
<option value="deg">Degrees</option>
<option value="rad">Radians</option>
<option value="grad">Grads</option>
</select>
</label>
</div>
<div class="status">
<div class="badge">Mode: <span id="angleModeLabel" class="accent"></span></div>
<div class="badge">Base: <span id="baseLabel" class="accent"></span></div>
</div>
<div class="section-title">Constants</div>
<div class="grid">
<button data-const="pi">pi</button>
<button data-const="e">e</button>
</div>
<div class="section-title">Stack</div>
<div class="grid">
<button data-cmd="enter">Enter</button>
<button data-cmd="dup">Dup</button>
<button data-cmd="swap">Swap top 2</button>
<button data-cmd="drop">Drop</button>
<button data-cmd="clear">Clear</button>
</div>
<div class="section-title">Arithmetic</div>
<div class="grid">
<button data-cmd="add">+</button>
<button data-cmd="sub"></button>
<button data-cmd="mul">×</button>
<button data-cmd="div">÷</button>
<button data-cmd="mod">%</button>
<button data-cmd="pow">y^x</button>
<button data-cmd="sqr"></button>
<button data-cmd="neg">±</button>
<button data-cmd="sqrt">sqrt</button>
<button data-cmd="recip">1/x</button>
<button data-cmd="log">log</button>
<button data-cmd="ln">ln</button>
</div>
<div class="section-title">Trigonometry</div>
<div class="grid">
<button data-cmd="sin">sin</button>
<button data-cmd="cos">cos</button>
<button data-cmd="tan">tan</button>
<button data-cmd="asin">asin</button>
<button data-cmd="acos">acos</button>
<button data-cmd="atan">atan</button>
</div>
<p class="muted">Tip: trig functions follow the selected angle mode. Domain errors are reported with clear messages. sqrt computes the square root of the top stack value.</p>
</div>
</div>
<script src="../../src/rpn-calculator.js"></script>
<script>
const enabledCommands = ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'sqr', 'neg', 'sqrt', 'recip', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'log', 'ln', 'dup', 'swap', 'drop', 'clear', 'enter'];
const calc = new RpnCalculator({ angleMode: 'deg', enabledCommands });
const display = document.getElementById('display');
const stack = document.getElementById('stack');
const input = document.getElementById('input');
const angleMode = document.getElementById('angleMode');
const angleModeLabel = document.getElementById('angleModeLabel');
const baseLabel = document.getElementById('baseLabel');
function render() {
display.textContent = calc.isEditing ? `Editing: ${calc.inputValue}` : 'Ready';
stack.innerHTML = calc.stack.length
? calc.stack.map((value, index) => `<div>${index}: ${value}</div>`).join('')
: '<span class="muted">Stack empty</span>';
angleMode.value = calc.angleMode;
angleModeLabel.textContent = calc.angleMode;
baseLabel.textContent = String(calc.base);
}
function commitInput() {
if (input.value.trim() !== '') {
calc.inputValue = input.value;
calc.isEditing = true;
calc.command('enter');
input.value = '';
}
}
function runCommand(name) {
if (name === 'swap') {
if (calc.stack.length >= 2) calc.swap(0, 1);
return;
}
if (name === 'enter') {
commitInput();
return;
}
calc.command(name);
}
input.addEventListener('input', (event) => {
calc.inputValue = event.target.value;
calc.isEditing = event.target.value.length > 0;
render();
});
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
try {
commitInput();
render();
} catch (error) {
alert(error.message);
}
}
});
angleMode.addEventListener('change', (event) => {
calc.angleMode = ['deg', 'rad', 'grad'].includes(event.target.value) ? event.target.value : 'deg';
render();
});
document.querySelectorAll('button[data-cmd]').forEach((button) => {
button.addEventListener('click', () => {
try {
runCommand(button.dataset.cmd);
render();
} catch (error) {
alert(error.message);
}
});
});
document.querySelectorAll('button[data-const]').forEach((button) => {
button.addEventListener('click', () => {
try {
calc.inputValue = '';
calc.isEditing = false;
calc.command(button.dataset.const);
input.value = '';
render();
} catch (error) {
alert(error.message);
}
});
});
render();
</script>
</body>
</html>