Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Lumina Calc | Glassmorphism Scientific Calculator</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@300;400;600&display=swap" rel="stylesheet"> | |
| <style> | |
| /* Custom Glassmorphism & Animations */ | |
| body { | |
| background: radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%), | |
| radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%), | |
| radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%); | |
| background-color: #0f172a; | |
| font-family: 'Inter', sans-serif; | |
| overflow: hidden; /* Prevent scroll on body, handle in app */ | |
| } | |
| .glass-panel { | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); | |
| } | |
| .calc-btn { | |
| transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .calc-btn::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 0; | |
| height: 0; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| transition: width 0.3s ease-out, height 0.3s ease-out; | |
| } | |
| .calc-btn:active::after { | |
| width: 200%; | |
| height: 200%; | |
| } | |
| .calc-btn:active { | |
| transform: scale(0.95); | |
| } | |
| .display-font { | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| /* Scrollbar for history */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.02); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| /* Floating shapes for background animation */ | |
| .blob { | |
| position: absolute; | |
| filter: blur(80px); | |
| z-index: -1; | |
| opacity: 0.6; | |
| animation: float 10s infinite ease-in-out; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translate(0, 0); } | |
| 50% { transform: translate(20px, -20px); } | |
| } | |
| </style> | |
| </head> | |
| <body class="h-screen w-screen flex items-center justify-center text-white selection:bg-pink-500 selection:text-white"> | |
| <!-- Background Blobs --> | |
| <div class="blob bg-purple-600 w-96 h-96 rounded-full top-0 left-0 mix-blend-multiply filter blur-3xl opacity-30 animate-blob"></div> | |
| <div class="blob bg-cyan-600 w-96 h-96 rounded-full top-0 right-0 mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-2000"></div> | |
| <div class="blob bg-pink-600 w-96 h-96 rounded-full -bottom-32 left-20 mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-4000"></div> | |
| <!-- Main Application Container --> | |
| <main class="relative w-full max-w-5xl h-full max-h-[90vh] flex flex-col md:flex-row gap-6 p-4 md:p-8"> | |
| <!-- Header / Branding --> | |
| <div class="absolute top-4 left-6 z-50 flex items-center gap-2 opacity-80 hover:opacity-100 transition-opacity"> | |
| <div class="w-3 h-3 rounded-full bg-red-500"></div> | |
| <div class="w-3 h-3 rounded-full bg-yellow-500"></div> | |
| <div class="w-3 h-3 rounded-full bg-green-500"></div> | |
| <span class="ml-2 text-xs font-bold tracking-widest text-gray-400 uppercase">Lumina Calc v1.0</span> | |
| </div> | |
| <div class="absolute top-4 right-6 z-50"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-xs font-semibold text-cyan-400 hover:text-cyan-300 transition-colors border border-cyan-500/30 px-3 py-1 rounded-full bg-cyan-900/20 backdrop-blur-sm"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| <!-- Calculator Unit --> | |
| <section class="flex-1 glass-panel rounded-3xl flex flex-col p-6 shadow-2xl relative overflow-hidden"> | |
| <!-- Display Screen --> | |
| <div class="flex flex-col justify-end items-end h-40 mb-6 px-4 py-2 bg-black/20 rounded-2xl border border-white/5 shadow-inner"> | |
| <div id="previous-operand" class="display-font text-gray-400 text-sm h-6 overflow-hidden"></div> | |
| <div id="current-operand" class="display-font text-5xl font-bold tracking-tight break-all text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-300">0</div> | |
| </div> | |
| <!-- Keypad --> | |
| <div class="grid grid-cols-4 gap-3 md:gap-4 flex-1"> | |
| <!-- Row 1 --> | |
| <button data-action="clear" class="calc-btn col-span-1 bg-red-500/20 text-red-300 border border-red-500/30 rounded-2xl text-lg font-semibold hover:bg-red-500/30">AC</button> | |
| <button data-action="delete" class="calc-btn col-span-1 bg-white/5 text-white border border-white/10 rounded-2xl text-lg font-semibold hover:bg-white/10">DEL</button> | |
| <button data-action="percent" class="calc-btn bg-white/5 text-cyan-300 border border-white/10 rounded-2xl text-xl font-semibold hover:bg-white/10">%</button> | |
| <button data-operation="÷" class="calc-btn bg-indigo-600/40 text-white border border-indigo-500/30 rounded-2xl text-2xl font-semibold hover:bg-indigo-600/60">÷</button> | |
| <!-- Row 2 --> | |
| <button data-number="7" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">7</button> | |
| <button data-number="8" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">8</button> | |
| <button data-number="9" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">9</button> | |
| <button data-operation="×" class="calc-btn bg-indigo-600/40 text-white border border-indigo-500/30 rounded-2xl text-2xl font-semibold hover:bg-indigo-600/60">×</button> | |
| <!-- Row 3 --> | |
| <button data-number="4" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">4</button> | |
| <button data-number="5" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">5</button> | |
| <button data-number="6" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">6</button> | |
| <button data-operation="-" class="calc-btn bg-indigo-600/40 text-white border border-indigo-500/30 rounded-2xl text-2xl font-semibold hover:bg-indigo-600/60">-</button> | |
| <!-- Row 4 --> | |
| <button data-number="1" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">1</button> | |
| <button data-number="2" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">2</button> | |
| <button data-number="3" class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">3</button> | |
| <button data-operation="+" class="calc-btn bg-indigo-600/40 text-white border border-indigo-500/30 rounded-2xl text-2xl font-semibold hover:bg-indigo-600/60">+</button> | |
| <!-- Row 5 --> | |
| <button data-number="0" class="calc-btn col-span-2 bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">0</button> | |
| <button data-number="." class="calc-btn bg-white/5 text-white border border-white/10 rounded-2xl text-2xl font-medium hover:bg-white/10">.</button> | |
| <button data-action="equals" class="calc-btn bg-gradient-to-br from-cyan-500 to-blue-600 text-white border border-cyan-400/50 rounded-2xl text-2xl font-bold shadow-lg shadow-cyan-500/20 hover:shadow-cyan-500/40 hover:scale-[1.02]">=</button> | |
| </div> | |
| </section> | |
| <!-- History Sidebar (Collapsible on mobile, visible on desktop) --> | |
| <aside class="w-full md:w-80 glass-panel rounded-3xl p-6 flex flex-col hidden md:flex"> | |
| <div class="flex justify-between items-center mb-4 border-b border-white/10 pb-4"> | |
| <h2 class="text-lg font-semibold text-gray-200 flex items-center gap-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-cyan-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| History Tape | |
| </h2> | |
| <button id="clear-history" class="text-xs text-red-400 hover:text-red-300 uppercase tracking-wider font-bold">Clear</button> | |
| </div> | |
| <div id="history-list" class="flex-1 overflow-y-auto space-y-3 pr-2"> | |
| <!-- History items injected here --> | |
| <div class="text-center text-gray-500 text-sm mt-10 italic">No calculations yet</div> | |
| </div> | |
| </aside> | |
| </main> | |
| <script> | |
| // --- Audio Engine (Tone.js) --- | |
| let synth; | |
| let audioInitialized = false; | |
| async function initAudio() { | |
| if (audioInitialized) return; | |
| await Tone.start(); | |
| // Create a simple membrane synth for a pleasant "click" sound | |
| synth = new Tone.MembraneSynth({ | |
| pitchDecay: 0.008, | |
| octaves: 2, | |
| oscillator: { type: "sine" }, | |
| envelope: { | |
| attack: 0.001, | |
| decay: 0.1, | |
| sustain: 0, | |
| release: 0.1 | |
| } | |
| }).toDestination(); | |
| // Lower volume | |
| synth.volume.value = -10; | |
| audioInitialized = true; | |
| } | |
| function playClickSound(type) { | |
| if (!audioInitialized) initAudio(); | |
| if (!synth) return; | |
| try { | |
| if (type === 'number') synth.triggerAttackRelease("C2", "32n"); | |
| else if (type === 'operator') synth.triggerAttackRelease("E2", "32n"); | |
| else if (type === 'action') synth.triggerAttackRelease("A1", "16n"); | |
| else if (type === 'equals') synth.triggerAttackRelease("C3", "16n"); | |
| } catch (e) { | |
| console.log("Audio context not ready yet"); | |
| } | |
| } | |
| // --- Calculator Logic --- | |
| class Calculator { | |
| constructor(previousOperandTextElement, currentOperandTextElement, historyListElement) { | |
| this.previousOperandTextElement = previousOperandTextElement; | |
| this.currentOperandTextElement = currentOperandTextElement; | |
| this.historyListElement = historyListElement; | |
| this.clear(); | |
| this.history = []; | |
| } | |
| clear() { | |
| this.currentOperand = '0'; | |
| this.previousOperand = ''; | |
| this.operation = undefined; | |
| this.shouldResetScreen = false; | |
| } | |
| delete() { | |
| if (this.shouldResetScreen) return; | |
| if (this.currentOperand === '0') return; | |
| this.currentOperand = this.currentOperand.toString().slice(0, -1); | |
| if (this.currentOperand === '' || this.currentOperand === '-') { | |
| this.currentOperand = '0'; | |
| } | |
| } | |
| appendNumber(number) { | |
| if (this.shouldResetScreen) { | |
| this.currentOperand = ''; | |
| this.shouldResetScreen = false; | |
| } | |
| // Prevent multiple decimals | |
| if (number === '.' && this.currentOperand.includes('.')) return; | |
| // Prevent multiple leading zeros | |
| if (number === '0' && this.currentOperand === '0') return; | |
| // Replace initial 0 unless adding decimal | |
| if (this.currentOperand === '0' && number !== '.') { | |
| this.currentOperand = number.toString(); | |
| } else { | |
| this.currentOperand = this.currentOperand.toString() + number.toString(); | |
| } | |
| } | |
| chooseOperation(operation) { | |
| if (this.currentOperand === '') return; | |
| if (this.previousOperand !== '') { | |
| this.compute(); | |
| } | |
| this.operation = operation; | |
| this.previousOperand = this.currentOperand; | |
| this.currentOperand = ''; // Clear current for next input | |
| } | |
| compute() { | |
| let computation; | |
| const prev = parseFloat(this.previousOperand); | |
| const current = parseFloat(this.currentOperand); | |
| if (isNaN(prev) || isNaN(current)) return; | |
| switch (this.operation) { | |
| case '+': | |
| computation = prev + current; | |
| break; | |
| case '-': | |
| computation = prev - current; | |
| break; | |
| case '×': | |
| computation = prev * current; | |
| break; | |
| case '÷': | |
| if (current === 0) { | |
| alert("Cannot divide by zero!"); | |
| this.clear(); | |
| return; | |
| } | |
| computation = prev / current; | |
| break; | |
| default: | |
| return; | |
| } | |
| // Handle floating point precision issues roughly | |
| computation = Math.round(computation * 100000000) / 100000000; | |
| this.addToHistory(prev, this.operation, current, computation); | |
| this.currentOperand = computation; | |
| this.operation = undefined; | |
| this.previousOperand = ''; | |
| this.shouldResetScreen = true; | |
| } | |
| percent() { | |
| const current = parseFloat(this.currentOperand); | |
| if (isNaN(current)) return; | |
| this.currentOperand = current / 100; | |
| } | |
| getDisplayNumber(number) { | |
| const stringNumber = number.toString(); | |
| const integerDigits = parseFloat(stringNumber.split('.')[0]); | |
| const decimalDigits = stringNumber.split('.')[1]; | |
| let integerDisplay; | |
| if (isNaN(integerDigits)) { | |
| integerDisplay = ''; | |
| } else { | |
| integerDisplay = integerDigits.toLocaleString('en', { maximumFractionDigits: 0 }); | |
| } | |
| if (decimalDigits != null) { | |
| return `${integerDisplay}.${decimalDigits}`; | |
| } else { | |
| return integerDisplay; | |
| } | |
| } | |
| updateDisplay() { | |
| this.currentOperandTextElement.innerText = this.getDisplayNumber(this.currentOperand); | |
| if (this.operation != null) { | |
| this.previousOperandTextElement.innerText = | |
| `${this.getDisplayNumber(this.previousOperand)} ${this.operation}`; | |
| } else { | |
| this.previousOperandTextElement.innerText = ''; | |
| } | |
| } | |
| addToHistory(prev, op, current, result) { | |
| const entry = { prev, op, current, result, timestamp: new Date() }; | |
| this.history.unshift(entry); | |
| this.renderHistory(); | |
| } | |
| renderHistory() { | |
| if (this.history.length === 0) { | |
| this.historyListElement.innerHTML = '<div class="text-center text-gray-500 text-sm mt-10 italic">No calculations yet</div>'; | |
| return; | |
| } | |
| this.historyListElement.innerHTML = this.history.map((item, index) => ` | |
| <div class="bg-white/5 p-3 rounded-xl border border-white/5 hover:bg-white/10 transition-colors cursor-pointer group animate-fade-in" onclick="calculator.loadResult(${item.result})"> | |
| <div class="text-xs text-gray-400 text-right mb-1 font-mono">${this.getDisplayNumber(item.prev)} ${item.op} ${this.getDisplayNumber(item.current)} =</div> | |
| <div class="text-xl text-cyan-300 text-right font-bold font-mono group-hover:text-cyan-200">${this.getDisplayNumber(item.result)}</div> | |
| </div> | |
| `).join(''); | |
| } | |
| loadResult(value) { | |
| this.currentOperand = value; | |
| this.shouldResetScreen = true; | |
| this.updateDisplay(); | |
| playClickSound('number'); | |
| } | |
| clearHistory() { | |
| this.history = []; | |
| this.renderHistory(); | |
| } | |
| } | |
| // --- Initialization & Event Listeners --- | |
| const previousOperandTextElement = document.getElementById('previous-operand'); | |
| const currentOperandTextElement = document.getElementById('current-operand'); | |
| const historyListElement = document.getElementById('history-list'); | |
| const calculator = new Calculator(previousOperandTextElement, currentOperandTextElement, historyListElement); | |
| // Button Selection | |
| const numberButtons = document.querySelectorAll('[data-number]'); | |
| const operationButtons = document.querySelectorAll('[data-operation]'); | |
| const equalsButton = document.querySelector('[data-action="equals"]'); | |
| const deleteButton = document.querySelector('[data-action="delete"]'); | |
| const allClearButton = document.querySelector('[data-action="clear"]'); | |
| const percentButton = document.querySelector('[data-action="percent"]'); | |
| const clearHistoryButton = document.getElementById('clear-history'); | |
| // Event Handlers | |
| numberButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| calculator.appendNumber(button.getAttribute('data-number')); | |
| calculator.updateDisplay(); | |
| playClickSound('number'); | |
| }); | |
| }); | |
| operationButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| calculator.chooseOperation(button.getAttribute('data-operation')); | |
| calculator.updateDisplay(); | |
| playClickSound('operator'); | |
| }); | |
| }); | |
| equalsButton.addEventListener('click', () => { | |
| calculator.compute(); | |
| calculator.updateDisplay(); | |
| playClickSound('equals'); | |
| }); | |
| allClearButton.addEventListener('click', () => { | |
| calculator.clear(); | |
| calculator.updateDisplay(); | |
| playClickSound('action'); | |
| }); | |
| deleteButton.addEventListener('click', () => { | |
| calculator.delete(); | |
| calculator.updateDisplay(); | |
| playClickSound('action'); | |
| }); | |
| percentButton.addEventListener('click', () => { | |
| calculator.percent(); | |
| calculator.updateDisplay(); | |
| playClickSound('operator'); | |
| }); | |
| clearHistoryButton.addEventListener('click', () => { | |
| calculator.clearHistory(); | |
| playClickSound('action'); | |
| }); | |
| // Keyboard Support | |
| document.addEventListener('keydown', (e) => { | |
| // Initialize audio on first interaction if not already | |
| if(!audioInitialized) initAudio(); | |
| if ((e.key >= 0 && e.key <= 9) || e.key === '.') { | |
| calculator.appendNumber(e.key); | |
| calculator.updateDisplay(); | |
| playClickSound('number'); | |
| highlightButton(e.key); | |
| } | |
| if (e.key === '+' || e.key === '-') { | |
| calculator.chooseOperation(e.key); | |
| calculator.updateDisplay(); | |
| playClickSound('operator'); | |
| highlightButton(e.key, 'operation'); | |
| } | |
| if (e.key === '*') { | |
| calculator.chooseOperation('×'); | |
| calculator.updateDisplay(); | |
| playClickSound('operator'); | |
| highlightButton('×', 'operation'); | |
| } | |
| if (e.key === '/') { | |
| calculator.chooseOperation('÷'); | |
| calculator.updateDisplay(); | |
| playClickSound('operator'); | |
| highlightButton('÷', 'operation'); | |
| } | |
| if (e.key === 'Enter' || e.key === '=') { | |
| e.preventDefault(); // Prevent form submission if any | |
| calculator.compute(); | |
| calculator.updateDisplay(); | |
| playClickSound('equals'); | |
| highlightButton('equals', 'action'); | |
| } | |
| if (e.key === 'Backspace') { | |
| calculator.delete(); | |
| calculator.updateDisplay(); | |
| playClickSound('action'); | |
| highlightButton('delete', 'action'); | |
| } | |
| if (e.key === 'Escape') { | |
| calculator.clear(); | |
| calculator.updateDisplay(); | |
| playClickSound('action'); | |
| highlightButton('clear', 'action'); | |
| } | |
| }); | |
| // Visual feedback for keyboard presses | |
| function highlightButton(key, type = 'number') { | |
| let selector; | |
| if (type === 'number') selector = `[data-number="${key}"]`; | |
| else if (type === 'operation') selector = `[data-operation="${key}"]`; | |
| else if (key === 'equals') selector = `[data-action="equals"]`; | |
| else if (key === 'delete') selector = `[data-action="delete"]`; | |
| else if (key === 'clear') selector = `[data-action="clear"]`; | |
| const btn = document.querySelector(selector); | |
| if (btn) { | |
| btn.classList.add('scale-95', 'brightness-125'); | |
| setTimeout(() => { | |
| btn.classList.remove('scale-95', 'brightness-125'); | |
| }, 100); | |
| } | |
| } | |
| // Initial display | |
| calculator.updateDisplay(); | |
| </script> | |
| </body> | |
| </html> |