309 lines
8.0 KiB
HTML
309 lines
8.0 KiB
HTML
<!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">x²</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>
|