672 lines
19 KiB
HTML
672 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Scientific Calculator</title>
|
||
<link rel="icon" type="image/png" href="images/gigachad.png" />
|
||
<link rel="apple-touch-icon" href="images/gigachad.png" />
|
||
<style>
|
||
:root {
|
||
--bg: #0f1419;
|
||
--surface: #1a2332;
|
||
--key: #243044;
|
||
--key-hover: #2d3d54;
|
||
--accent: #3d8bfd;
|
||
--accent-hover: #5a9dff;
|
||
--text: #e8eef7;
|
||
--muted: #8a9bb0;
|
||
--danger: #e85d5d;
|
||
--sci: #1e2a3d;
|
||
--sci-hover: #263548;
|
||
--radius: 10px;
|
||
font-family: "Segoe UI", system-ui, sans-serif;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: radial-gradient(ellipse 120% 80% at 50% -20%, #1e3a5f 0%, var(--bg) 55%);
|
||
color: var(--text);
|
||
}
|
||
|
||
.calc {
|
||
width: min(100% - 2rem, 680px);
|
||
padding: 1.25rem;
|
||
background: var(--surface);
|
||
border-radius: calc(var(--radius) + 4px);
|
||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.hero-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
width: 100%;
|
||
max-width: 320px;
|
||
margin: 0 auto 0.75rem;
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
border: 3px solid var(--accent);
|
||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.hero-img {
|
||
display: block;
|
||
width: 100%;
|
||
height: auto;
|
||
max-height: 250px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.calc-panel {
|
||
width: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0 0 0.65rem;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
text-align: center;
|
||
}
|
||
|
||
.display-wrap {
|
||
background: var(--bg);
|
||
border-radius: var(--radius);
|
||
padding: 0.65rem 0.85rem;
|
||
margin-bottom: 0.75rem;
|
||
min-height: 3.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
}
|
||
|
||
.expression {
|
||
font-size: 0.7rem;
|
||
color: var(--muted);
|
||
min-height: 1.15em;
|
||
word-break: break-all;
|
||
text-align: right;
|
||
width: 100%;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.result {
|
||
font-size: 1.5rem;
|
||
font-variant-numeric: tabular-nums;
|
||
font-weight: 500;
|
||
word-break: break-all;
|
||
text-align: right;
|
||
width: 100%;
|
||
}
|
||
|
||
.mode-row {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-bottom: 0.45rem;
|
||
}
|
||
|
||
.mode-btn {
|
||
font: inherit;
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
padding: 0.35rem 0.65rem;
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
background: var(--sci);
|
||
color: var(--accent);
|
||
}
|
||
|
||
.mode-btn:hover {
|
||
background: var(--sci-hover);
|
||
}
|
||
|
||
.keys {
|
||
display: grid;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.keys.sci {
|
||
grid-template-columns: repeat(5, 1fr);
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.keys.main {
|
||
grid-template-columns: repeat(4, 1fr);
|
||
}
|
||
|
||
button {
|
||
font: inherit;
|
||
font-size: 1rem;
|
||
font-variant-numeric: tabular-nums;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
padding: 0.72rem 0.2rem;
|
||
cursor: pointer;
|
||
background: var(--key);
|
||
color: var(--text);
|
||
transition: background 0.12s ease, transform 0.08s ease;
|
||
}
|
||
|
||
button.sci {
|
||
font-size: 0.78rem;
|
||
padding: 0.55rem 0.15rem;
|
||
background: var(--sci);
|
||
}
|
||
|
||
button.sci:hover {
|
||
background: var(--sci-hover);
|
||
}
|
||
|
||
button:hover {
|
||
background: var(--key-hover);
|
||
}
|
||
|
||
button:active {
|
||
transform: scale(0.97);
|
||
}
|
||
|
||
button.op {
|
||
background: #2a3548;
|
||
color: var(--accent);
|
||
}
|
||
|
||
button.op:hover {
|
||
background: #33425a;
|
||
}
|
||
|
||
button.equals {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
grid-row: span 2;
|
||
}
|
||
|
||
button.equals:hover {
|
||
background: var(--accent-hover);
|
||
}
|
||
|
||
button.danger {
|
||
color: var(--danger);
|
||
}
|
||
|
||
button.zero {
|
||
grid-column: span 2;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="calc" role="application" aria-label="Scientific calculator">
|
||
<div class="hero-wrap">
|
||
<img
|
||
class="hero-img"
|
||
src="images/gigachad.png"
|
||
alt="GigaChad meme portrait"
|
||
/>
|
||
</div>
|
||
<div class="calc-panel">
|
||
<h1>Scientific calculator</h1>
|
||
<div class="mode-row">
|
||
<button type="button" class="mode-btn" id="degBtn" title="Toggle angle units for sin/cos/tan and inverse trig">DEG</button>
|
||
</div>
|
||
<div class="display-wrap">
|
||
<div class="expression" id="expr" aria-live="polite"></div>
|
||
<div class="result" id="out">0</div>
|
||
</div>
|
||
<div class="keys sci">
|
||
<button type="button" class="sci danger" data-action="clear">C</button>
|
||
<button type="button" class="sci" data-action="back">⌫</button>
|
||
<button type="button" class="sci op" data-append="(">(</button>
|
||
<button type="button" class="sci op" data-append=")">)</button>
|
||
<button type="button" class="sci op" data-action="ans">ANS</button>
|
||
<button type="button" class="sci" data-append="sin(">sin</button>
|
||
<button type="button" class="sci" data-append="cos(">cos</button>
|
||
<button type="button" class="sci" data-append="tan(">tan</button>
|
||
<button type="button" class="sci" data-append="asin(">asin</button>
|
||
<button type="button" class="sci" data-append="acos(">acos</button>
|
||
<button type="button" class="sci" data-append="atan(">atan</button>
|
||
<button type="button" class="sci" data-append="log(">log</button>
|
||
<button type="button" class="sci" data-append="ln(">ln</button>
|
||
<button type="button" class="sci" data-append="sqrt(">√</button>
|
||
<button type="button" class="sci op" data-append="^">x^y</button>
|
||
<button type="button" class="sci op" data-append="^2">x²</button>
|
||
<button type="button" class="sci op" data-append="^(-1)">1/x</button>
|
||
<button type="button" class="sci op" data-append="pi">π</button>
|
||
<button type="button" class="sci op" data-append="e">e</button>
|
||
<button type="button" class="sci op" data-append="!">!</button>
|
||
</div>
|
||
<div class="keys main">
|
||
<button type="button" class="op" data-append="/">÷</button>
|
||
<button type="button" class="op" data-append="*">×</button>
|
||
<button type="button" class="op" data-append="-">−</button>
|
||
<button type="button" class="op" data-append="+">+</button>
|
||
<button type="button" data-digit="7">7</button>
|
||
<button type="button" data-digit="8">8</button>
|
||
<button type="button" data-digit="9">9</button>
|
||
<button type="button" class="op equals" data-action="equals">=</button>
|
||
<button type="button" data-digit="4">4</button>
|
||
<button type="button" data-digit="5">5</button>
|
||
<button type="button" data-digit="6">6</button>
|
||
<button type="button" data-digit="1">1</button>
|
||
<button type="button" data-digit="2">2</button>
|
||
<button type="button" data-digit="3">3</button>
|
||
<button type="button" class="zero" data-digit="0">0</button>
|
||
<button type="button" data-action="dot">.</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
const exprEl = document.getElementById("expr");
|
||
const outEl = document.getElementById("out");
|
||
const degBtn = document.getElementById("degBtn");
|
||
|
||
let exprStr = "";
|
||
let lastResult = 0;
|
||
let degMode = true;
|
||
|
||
function formatDisplay(n) {
|
||
if (!Number.isFinite(n)) return "Error";
|
||
const s = String(n);
|
||
if (s.length > 16) return n.toPrecision(12).replace(/\.?0+e/, "e");
|
||
return s;
|
||
}
|
||
|
||
function prettyExpr(s) {
|
||
if (!s) return "";
|
||
return s
|
||
.replace(/\*/g, "×")
|
||
.replace(/\//g, "÷")
|
||
.replace(/pi/g, "π");
|
||
}
|
||
|
||
function updateView() {
|
||
exprEl.textContent = prettyExpr(exprStr);
|
||
outEl.textContent = formatDisplay(lastResult);
|
||
}
|
||
|
||
function factorial(n) {
|
||
if (!Number.isFinite(n)) throw new Error("domain");
|
||
if (n < 0 || Math.abs(n - Math.round(n)) > 1e-12) throw new Error("domain");
|
||
const k = Math.round(n);
|
||
if (k > 170) throw new Error("overflow");
|
||
let r = 1;
|
||
for (let i = 2; i <= k; i++) r *= i;
|
||
return r;
|
||
}
|
||
|
||
function applyFunc(name, x) {
|
||
const rad = (d) => (d * Math.PI) / 180;
|
||
const deg = (r) => (r * 180) / Math.PI;
|
||
switch (name) {
|
||
case "sin":
|
||
return degMode ? Math.sin(rad(x)) : Math.sin(x);
|
||
case "cos":
|
||
return degMode ? Math.cos(rad(x)) : Math.cos(x);
|
||
case "tan":
|
||
return degMode ? Math.tan(rad(x)) : Math.tan(x);
|
||
case "asin":
|
||
if (degMode) return deg(Math.asin(x));
|
||
return Math.asin(x);
|
||
case "acos":
|
||
if (degMode) return deg(Math.acos(x));
|
||
return Math.acos(x);
|
||
case "atan":
|
||
if (degMode) return deg(Math.atan(x));
|
||
return Math.atan(x);
|
||
case "log":
|
||
return Math.log10(x);
|
||
case "ln":
|
||
return Math.log(x);
|
||
case "sqrt":
|
||
return Math.sqrt(x);
|
||
case "abs":
|
||
return Math.abs(x);
|
||
default:
|
||
throw new Error("fn");
|
||
}
|
||
}
|
||
|
||
function tokenize(str) {
|
||
const tokens = [];
|
||
let i = 0;
|
||
const peek = () => str[i];
|
||
const readNum = () => {
|
||
let start = i;
|
||
if (peek() === ".") {
|
||
i++;
|
||
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||
} else {
|
||
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||
if (peek() === ".") {
|
||
i++;
|
||
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||
}
|
||
}
|
||
const raw = str.slice(start, i);
|
||
if (raw === "." || raw === "") throw new Error("num");
|
||
const v = parseFloat(raw);
|
||
if (!Number.isFinite(v)) throw new Error("num");
|
||
return { type: "number", value: v };
|
||
};
|
||
|
||
while (i < str.length) {
|
||
const c = peek();
|
||
if (/\s/.test(c)) {
|
||
i++;
|
||
continue;
|
||
}
|
||
if (/[0-9.]/.test(c)) {
|
||
tokens.push(readNum());
|
||
continue;
|
||
}
|
||
if (/[a-zA-Z]/.test(c)) {
|
||
let j = i;
|
||
while (j < str.length && /[a-zA-Z]/.test(str[j])) j++;
|
||
const id = str.slice(i, j);
|
||
i = j;
|
||
if (id === "pi") tokens.push({ type: "const", name: "pi" });
|
||
else if (id === "e") tokens.push({ type: "const", name: "e" });
|
||
else if (
|
||
["sin", "cos", "tan", "asin", "acos", "atan", "log", "ln", "sqrt", "abs"].includes(id)
|
||
) {
|
||
tokens.push({ type: "func", name: id });
|
||
} else throw new Error("id");
|
||
continue;
|
||
}
|
||
if (c === "(") {
|
||
tokens.push({ type: "lparen" });
|
||
i++;
|
||
continue;
|
||
}
|
||
if (c === ")") {
|
||
tokens.push({ type: "rparen" });
|
||
i++;
|
||
continue;
|
||
}
|
||
if (c === "!") {
|
||
tokens.push({ type: "fact" });
|
||
i++;
|
||
continue;
|
||
}
|
||
if ("+-*/^".includes(c)) {
|
||
tokens.push({ type: "op", v: c });
|
||
i++;
|
||
continue;
|
||
}
|
||
throw new Error("tok");
|
||
}
|
||
return tokens;
|
||
}
|
||
|
||
function insertImplicitMul(tokens) {
|
||
const out = [];
|
||
const needs = (a, b) => {
|
||
if (!a || !b) return false;
|
||
const end = ["number", "rparen", "fact"];
|
||
const start = ["number", "lparen", "func", "const"];
|
||
return end.includes(a.type) && start.includes(b.type);
|
||
};
|
||
for (let k = 0; k < tokens.length; k++) {
|
||
const t = tokens[k];
|
||
if (k > 0 && needs(out[out.length - 1], t)) {
|
||
out.push({ type: "op", v: "*" });
|
||
}
|
||
out.push(t);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function parseAndEval(tokens) {
|
||
let p = 0;
|
||
const peek = () => tokens[p];
|
||
const take = () => tokens[p++];
|
||
|
||
function parseAdditive() {
|
||
let left = parseMultiplicative();
|
||
while (peek()?.type === "op" && (peek().v === "+" || peek().v === "-")) {
|
||
const op = take().v;
|
||
const right = parseMultiplicative();
|
||
left = op === "+" ? left + right : left - right;
|
||
}
|
||
return left;
|
||
}
|
||
|
||
function parseMultiplicative() {
|
||
let left = parsePower();
|
||
while (peek()?.type === "op" && (peek().v === "*" || peek().v === "/")) {
|
||
const op = take().v;
|
||
const right = parsePower();
|
||
left = op === "*" ? left * right : left / right;
|
||
}
|
||
return left;
|
||
}
|
||
|
||
function parsePower() {
|
||
let left = parseUnary();
|
||
if (peek()?.type === "op" && peek().v === "^") {
|
||
take();
|
||
const right = parsePower();
|
||
left = Math.pow(left, right);
|
||
}
|
||
return left;
|
||
}
|
||
|
||
function parseUnary() {
|
||
if (peek()?.type === "op" && peek().v === "-") {
|
||
take();
|
||
return -parseUnary();
|
||
}
|
||
if (peek()?.type === "op" && peek().v === "+") {
|
||
take();
|
||
return parseUnary();
|
||
}
|
||
return parsePostfix();
|
||
}
|
||
|
||
function parsePostfix() {
|
||
let val = parsePrimary();
|
||
while (peek()?.type === "fact") {
|
||
take();
|
||
val = factorial(val);
|
||
}
|
||
return val;
|
||
}
|
||
|
||
function parsePrimary() {
|
||
const t = peek();
|
||
if (!t) throw new Error("exp");
|
||
if (t.type === "number") {
|
||
take();
|
||
return t.value;
|
||
}
|
||
if (t.type === "const") {
|
||
take();
|
||
return t.name === "pi" ? Math.PI : Math.E;
|
||
}
|
||
if (t.type === "func") {
|
||
const name = take().name;
|
||
if (peek()?.type !== "lparen") throw new Error("(");
|
||
take();
|
||
const inner = parseAdditive();
|
||
if (peek()?.type !== "rparen") throw new Error(")");
|
||
take();
|
||
return applyFunc(name, inner);
|
||
}
|
||
if (t.type === "lparen") {
|
||
take();
|
||
const inner = parseAdditive();
|
||
if (peek()?.type !== "rparen") throw new Error(")");
|
||
take();
|
||
return inner;
|
||
}
|
||
throw new Error("prim");
|
||
}
|
||
|
||
const result = parseAdditive();
|
||
if (peek()) throw new Error("trail");
|
||
if (!Number.isFinite(result)) throw new Error("nan");
|
||
return result;
|
||
}
|
||
|
||
function evaluate(raw) {
|
||
const sub = String(raw).trim();
|
||
if (!sub) throw new Error("empty");
|
||
let s = sub.replace(/\bANS\b/g, "(" + String(lastResult) + ")");
|
||
const tokens = insertImplicitMul(tokenize(s));
|
||
return parseAndEval(tokens);
|
||
}
|
||
|
||
function lastChar() {
|
||
return exprStr.slice(-1);
|
||
}
|
||
|
||
function shouldInsertMul(next) {
|
||
if (!exprStr) return false;
|
||
const a = lastChar();
|
||
const b = next[0];
|
||
if (/[0-9]/.test(a) && /[0-9]/.test(b)) return false;
|
||
if (a === "^") return false;
|
||
const aEnd = /[0-9\)\!]/.test(a);
|
||
const bStart = /[\(0-9a-zA-Z]/.test(b) && b !== "!";
|
||
return aEnd && bStart;
|
||
}
|
||
|
||
function append(s) {
|
||
if (shouldInsertMul(s)) exprStr += "*";
|
||
exprStr += s;
|
||
updateView();
|
||
}
|
||
|
||
function inputDigit(d) {
|
||
append(d);
|
||
}
|
||
|
||
function inputDot() {
|
||
append(".");
|
||
}
|
||
|
||
function doEval() {
|
||
try {
|
||
const v = evaluate(exprStr);
|
||
lastResult = v;
|
||
exprStr = "";
|
||
updateView();
|
||
} catch {
|
||
lastResult = NaN;
|
||
updateView();
|
||
}
|
||
}
|
||
|
||
function clear() {
|
||
exprStr = "";
|
||
lastResult = 0;
|
||
updateView();
|
||
}
|
||
|
||
function back() {
|
||
exprStr = exprStr.slice(0, -1);
|
||
updateView();
|
||
}
|
||
|
||
function insertAns() {
|
||
append("ANS");
|
||
}
|
||
|
||
degBtn.addEventListener("click", function () {
|
||
degMode = !degMode;
|
||
degBtn.textContent = degMode ? "DEG" : "RAD";
|
||
degBtn.title = degMode
|
||
? "Angles in degrees (click for radians)"
|
||
: "Angles in radians (click for degrees)";
|
||
});
|
||
|
||
document.querySelector(".calc").addEventListener("click", function (e) {
|
||
const btn = e.target.closest("button");
|
||
if (!btn) return;
|
||
if (btn === degBtn) return;
|
||
|
||
const ap = btn.getAttribute("data-append");
|
||
const d = btn.getAttribute("data-digit");
|
||
const action = btn.getAttribute("data-action");
|
||
|
||
if (action === "equals") {
|
||
doEval();
|
||
return;
|
||
}
|
||
if (d !== null) {
|
||
inputDigit(d);
|
||
return;
|
||
}
|
||
if (ap !== null) {
|
||
append(ap);
|
||
return;
|
||
}
|
||
if (action === "clear") clear();
|
||
else if (action === "back") back();
|
||
else if (action === "dot") inputDot();
|
||
else if (action === "ans") insertAns();
|
||
});
|
||
|
||
document.addEventListener("keydown", function (e) {
|
||
if (e.key >= "0" && e.key <= "9") {
|
||
e.preventDefault();
|
||
inputDigit(e.key);
|
||
} else if (e.key === ".") {
|
||
e.preventDefault();
|
||
inputDot();
|
||
} else if (e.key === "+") {
|
||
e.preventDefault();
|
||
append("+");
|
||
} else if (e.key === "-") {
|
||
e.preventDefault();
|
||
append("-");
|
||
} else if (e.key === "*") {
|
||
e.preventDefault();
|
||
append("*");
|
||
} else if (e.key === "/") {
|
||
e.preventDefault();
|
||
append("/");
|
||
} else if (e.key === "^") {
|
||
e.preventDefault();
|
||
append("^");
|
||
} else if (e.key === "(") {
|
||
e.preventDefault();
|
||
append("(");
|
||
} else if (e.key === ")") {
|
||
e.preventDefault();
|
||
append(")");
|
||
} else if (e.key === "Enter" || (e.key === "=" && !e.shiftKey)) {
|
||
e.preventDefault();
|
||
doEval();
|
||
} else if (e.key === "Escape") {
|
||
e.preventDefault();
|
||
clear();
|
||
} else if (e.key === "Backspace") {
|
||
e.preventDefault();
|
||
back();
|
||
}
|
||
});
|
||
|
||
updateView();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|