Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Oscilloscope Wave Studio</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| @property --gradient-angle { | |
| syntax: '<angle>'; | |
| initial-value: 0deg; | |
| inherits: false; | |
| } | |
| @property --wave-hue { | |
| syntax: '<number>'; | |
| initial-value: 180; | |
| inherits: false; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-primary: #050505; | |
| --bg-secondary: #0a0a0f; | |
| --bg-tertiary: #13131f; | |
| --surface: rgba(255, 255, 255, 0.03); | |
| --surface-hover: rgba(255, 255, 255, 0.06); | |
| --border: rgba(255, 255, 255, 0.08); | |
| --border-hover: rgba(255, 255, 255, 0.15); | |
| --text-primary: #f0f0f5; | |
| --text-secondary: #8a8a9a; | |
| --text-muted: #5a5a6a; | |
| --accent: oklch(70% 0.2 var(--wave-hue)); | |
| --accent-glow: oklch(70% 0.2 var(--wave-hue) / 0.4); | |
| --danger: #ff4466; | |
| --success: #00ffaa; | |
| --font-sans: 'Inter', system-ui, -apple-system, sans-serif; | |
| --font-mono: 'JetBrains Mono', monospace; | |
| --radius-sm: 8px; | |
| --radius-md: 12px; | |
| --radius-lg: 20px; | |
| --radius-xl: 28px; | |
| } | |
| @media (prefers-color-scheme: light) { | |
| :root { | |
| --bg-primary: #f5f5fa; | |
| --bg-secondary: #eaeaf2; | |
| --bg-tertiary: #e0e0ea; | |
| --surface: rgba(0, 0, 0, 0.03); | |
| --surface-hover: rgba(0, 0, 0, 0.06); | |
| --border: rgba(0, 0, 0, 0.08); | |
| --border-hover: rgba(0, 0, 0, 0.15); | |
| --text-primary: #1a1a2e; | |
| --text-secondary: #5a5a6a; | |
| --text-muted: #8a8a9a; | |
| } | |
| } | |
| html { | |
| color-scheme: dark light; | |
| } | |
| body { | |
| font-family: var(--font-sans); | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| min-height: 100dvh; | |
| overflow-x: hidden; | |
| display: grid; | |
| grid-template-rows: auto 1fr auto; | |
| } | |
| /* Header */ | |
| .app-header { | |
| position: relative; | |
| padding: 1.25rem 2rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: linear-gradient(180deg, var(--bg-secondary), transparent); | |
| border-bottom: 1px solid var(--border); | |
| z-index: 10; | |
| container-type: inline-size; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.875rem; | |
| } | |
| .brand-icon { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: var(--radius-md); | |
| background: linear-gradient(var(--gradient-angle), var(--accent), oklch(80% 0.3 calc(var(--wave-hue) + 60))); | |
| animation: gradient-rotate 8s linear infinite, hue-shift 12s linear infinite; | |
| display: grid; | |
| place-items: center; | |
| box-shadow: 0 0 20px var(--accent-glow); | |
| } | |
| @keyframes gradient-rotate { | |
| to { --gradient-angle: 360deg; } | |
| } | |
| @keyframes hue-shift { | |
| to { --wave-hue: 540; } | |
| } | |
| .brand-icon svg { | |
| width: 22px; | |
| height: 22px; | |
| color: white; | |
| } | |
| .brand-text { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.125rem; | |
| } | |
| .brand-title { | |
| font-size: 1.125rem; | |
| font-weight: 700; | |
| letter-spacing: -0.025em; | |
| line-height: 1.2; | |
| } | |
| .brand-subtitle { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| font-weight: 500; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| } | |
| .header-links { | |
| display: flex; | |
| align-items: center; | |
| gap: 1.5rem; | |
| } | |
| .anycoder-badge { | |
| font-size: 0.8125rem; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| border-radius: var(--radius-xl); | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| font-weight: 500; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .anycoder-badge::before { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(90deg, transparent, var(--accent-glow), transparent); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .anycoder-badge:hover { | |
| border-color: var(--border-hover); | |
| color: var(--text-primary); | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 20px -4px var(--accent-glow); | |
| } | |
| .anycoder-badge:hover::before { | |
| opacity: 1; | |
| } | |
| /* Main Layout */ | |
| .app-main { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| grid-template-rows: 1fr auto; | |
| gap: 1.5rem; | |
| padding: 1.5rem 2rem; | |
| max-width: 1600px; | |
| width: 100%; | |
| margin: 0 auto; | |
| container-type: inline-size; | |
| } | |
| @container (min-width: 900px) { | |
| .app-main { | |
| grid-template-columns: 1fr 340px; | |
| grid-template-rows: 1fr; | |
| } | |
| } | |
| /* Canvas Stage */ | |
| .stage { | |
| position: relative; | |
| border-radius: var(--radius-lg); | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 400px; | |
| } | |
| .stage-grid { | |
| position: absolute; | |
| inset: 0; | |
| opacity: 0.4; | |
| background-image: | |
| linear-gradient(var(--border) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--border) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; | |
| } | |
| .canvas-container { | |
| position: relative; | |
| flex: 1; | |
| display: grid; | |
| place-items: center; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| .stage-overlay { | |
| position: absolute; | |
| inset: 1.25rem; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| pointer-events: none; | |
| } | |
| .metrics { | |
| display: flex; | |
| gap: 1.5rem; | |
| font-family: var(--font-mono); | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| } | |
| .metric { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .metric-value { | |
| font-size: 0.9375rem; | |
| font-weight: 700; | |
| color: var(--accent); | |
| text-shadow: 0 0 10px var(--accent-glow); | |
| transition: color 0.3s; | |
| } | |
| .status-pill { | |
| align-self: flex-end; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.375rem 0.875rem; | |
| border-radius: var(--radius-xl); | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-secondary); | |
| transition: all 0.3s; | |
| } | |
| .status-pill[data-active="true"] { | |
| background: rgba(0, 255, 170, 0.1); | |
| border-color: var(--success); | |
| color: var(--success); | |
| box-shadow: 0 0 20px rgba(0, 255, 170, 0.1); | |
| } | |
| .status-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| box-shadow: 0 0 8px currentColor; | |
| } | |
| /* Controls Panel */ | |
| .controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .panel { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 1.25rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.25rem; | |
| transition: border-color 0.3s; | |
| } | |
| .panel:hover { | |
| border-color: var(--border-hover); | |
| } | |
| .panel-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 0.75rem; | |
| } | |
| .panel-title { | |
| font-size: 0.8125rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--text-secondary); | |
| } | |
| /* Power Button */ | |
| .power-btn { | |
| width: 100%; | |
| padding: 1rem 1.5rem; | |
| border-radius: var(--radius-md); | |
| border: none; | |
| font-family: var(--font-sans); | |
| font-size: 0.9375rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.625rem; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| overflow: hidden; | |
| background: linear-gradient(135deg, var(--accent), oklch(60% 0.2 calc(var(--wave-hue) + 40))); | |
| color: white; | |
| box-shadow: 0 4px 20px -4px var(--accent-glow); | |
| } | |
| .power-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 30px -4px var(--accent-glow); | |
| } | |
| .power-btn:active { | |
| transform: translateY(0); | |
| } | |
| .power-btn[data-active="true"] { | |
| background: linear-gradient(135deg, var(--danger), #ff2244); | |
| box-shadow: 0 4px 20px -4px rgba(255, 68, 102, 0.4); | |
| } | |
| .power-btn[data-active="true"]:hover { | |
| box-shadow: 0 8px 30px -4px rgba(255, 68, 102, 0.5); | |
| } | |
| /* Waveform Selector */ | |
| .wave-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 0.625rem; | |
| } | |
| .wave-option { | |
| position: relative; | |
| padding: 0.875rem 0.5rem; | |
| border-radius: var(--radius-sm); | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| cursor: pointer; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.5rem; | |
| transition: all 0.2s; | |
| } | |
| .wave-option:hover { | |
| background: var(--surface-hover); | |
| border-color: var(--border-hover); | |
| } | |
| .wave-option[data-selected="true"] { | |
| background: rgba(255, 255, 255, 0.06); | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 1px var(--accent), 0 4px 12px -4px var(--accent-glow); | |
| } | |
| .wave-preview { | |
| width: 48px; | |
| height: 24px; | |
| color: var(--text-muted); | |
| } | |
| .wave-option[data-selected="true"] .wave-preview { | |
| color: var(--accent); | |
| } | |
| .wave-label { | |
| font-size: 0.6875rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-muted); | |
| } | |
| .wave-option[data-selected="true"] .wave-label { | |
| color: var(--text-primary); | |
| } | |
| /* Sliders */ | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.625rem; | |
| } | |
| .control-label { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.8125rem; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| } | |
| .control-value { | |
| font-family: var(--font-mono); | |
| font-size: 0.75rem; | |
| color: var(--accent); | |
| font-weight: 700; | |
| padding: 0.125rem 0.5rem; | |
| border-radius: 4px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 100%; | |
| height: 6px; | |
| border-radius: 999px; | |
| background: var(--bg-tertiary); | |
| outline: none; | |
| cursor: pointer; | |
| border: 1px solid var(--border); | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--text-primary); | |
| cursor: pointer; | |
| border: 2px solid var(--bg-primary); | |
| box-shadow: 0 0 0 2px var(--accent); | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| box-shadow: 0 0 0 3px var(--accent), 0 0 20px var(--accent-glow); | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--text-primary); | |
| cursor: pointer; | |
| border: 2px solid var(--bg-primary); | |
| box-shadow: 0 0 0 2px var(--accent); | |
| } | |
| /* Color Theme */ | |
| .theme-grid { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 0.5rem; | |
| } | |
| .theme-option { | |
| aspect-ratio: 1; | |
| border-radius: 50%; | |
| border: 2px solid transparent; | |
| cursor: pointer; | |
| position: relative; | |
| transition: transform 0.2s; | |
| } | |
| .theme-option:hover { | |
| transform: scale(1.1); | |
| } | |
| .theme-option[data-selected="true"] { | |
| border-color: var(--text-primary); | |
| box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--text-secondary); | |
| } | |
| /* Text Visualization Mode */ | |
| .text-viz { | |
| position: absolute; | |
| inset: 0; | |
| display: grid; | |
| place-items: center; | |
| font-family: var(--font-mono); | |
| font-size: clamp(1rem, 3vw, 1.5rem); | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| overflow: hidden; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.5s; | |
| } | |
| .text-viz[data-active="true"] { | |
| opacity: 0.15; | |
| } | |
| .text-viz span { | |
| display: inline-block; | |
| transition: transform 0.1s; | |
| } | |
| /* Footer */ | |
| .app-footer { | |
| padding: 1rem 2rem; | |
| text-align: center; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| border-top: 1px solid var(--border); | |
| } | |
| /* Responsive Adjustments */ | |
| @media (max-width: 640px) { | |
| .app-header { | |
| padding: 1rem; | |
| } | |
| .brand-title { | |
| font-size: 1rem; | |
| } | |
| .header-links { | |
| gap: 0.75rem; | |
| } | |
| .anycoder-badge { | |
| padding: 0.4rem 0.75rem; | |
| font-size: 0.75rem; | |
| } | |
| .app-main { | |
| padding: 1rem; | |
| gap: 1rem; | |
| } | |
| .panel { | |
| padding: 1rem; | |
| } | |
| } | |
| /* Reduced Motion */ | |
| @media (prefers-reduced-motion: reduce) { | |
| * { | |
| animation-duration: 0.01ms ; | |
| animation-iteration-count: 1 ; | |
| transition-duration: 0.01ms ; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="app-header"> | |
| <div class="brand"> | |
| <div class="brand-icon"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M2 10v3"/> | |
| <path d="M6 6v11"/> | |
| <path d="M10 3v18"/> | |
| <path d="M14 8v7"/> | |
| <path d="M18 5v13"/> | |
| <path d="M22 10v3"/> | |
| </svg> | |
| </div> | |
| <div class="brand-text"> | |
| <span class="brand-title">Oscilloscope Studio</span> | |
| <span class="brand-subtitle">Audio Waveform Visualizer</span> | |
| </div> | |
| </div> | |
| <div class="header-links"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-badge"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| </header> | |
| <main class="app-main"> | |
| <section class="stage" id="stage"> | |
| <div class="stage-grid"></div> | |
| <div class="canvas-container"> | |
| <canvas id="waveCanvas"></canvas> | |
| <div class="text-viz" id="textViz" data-active="false"></div> | |
| </div> | |
| <div class="stage-overlay"> | |
| <div class="metrics"> | |
| <div class="metric"> | |
| <span>Frequency</span> | |
| <span class="metric-value" id="freqDisplay">440 Hz</span> | |
| </div> | |
| <div class="metric"> | |
| <span>Amplitude</span> | |
| <span class="metric-value" id="ampDisplay">50%</span> | |
| </div> | |
| <div class="metric"> | |
| <span>Waveform</span> | |
| <span class="metric-value" id="waveDisplay">Sine</span> | |
| </div> | |
| </div> | |
| <div class="status-pill" id="statusPill" data-active="false"> | |
| <span class="status-dot"></span> | |
| <span id="statusText">Standby</span> | |
| </div> | |
| </div> | |
| </section> | |
| <aside class="controls"> | |
| <div class="panel"> | |
| <button class="power-btn" id="powerBtn" data-active="false"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M18.36 6.64a9 9 0 1 1-12.73 0"/> | |
| <line x1="12" y1="2" x2="12" y2="12"/> | |
| </svg> | |
| <span id="powerText">Initialize Oscillator</span> | |
| </button> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title">Waveform</span> | |
| </div> | |
| <div class="wave-grid" id="waveGrid"> | |
| <div class="wave-option" data-wave="sine" data-selected="true"> | |
| <svg class="wave-preview" viewBox="0 0 48 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M2 12 Q 12 2, 24 12 T 46 12"/> | |
| </svg> | |
| <span class="wave-label">Sine</span> | |
| </div> | |
| <div class="wave-option" data-wave="square" data-selected="false"> | |
| <svg class="wave-preview" viewBox="0 0 48 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M2 18 L14 18 L14 6 L34 6 L34 18 L46 18"/> | |
| </svg> | |
| <span class="wave-label">Square</span> | |
| </div> | |
| <div class="wave-option" data-wave="sawtooth" data-selected="false"> | |
| <svg class="wave-preview" viewBox="0 0 48 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M2 18 L24 6 L24 18 L46 6"/> | |
| </svg> | |
| <span class="wave-label">Sawtooth</span> | |
| </div> | |
| <div class="wave-option" data-wave="triangle" data-selected="false"> | |
| <svg class="wave-preview" viewBox="0 0 48 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M2 18 L14 6 L34 18 L46 6"/> | |
| </svg> | |
| <span class="wave-label">Triangle</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title">Parameters</span> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label"> | |
| <span>Frequency</span> | |
| <span class="control-value" id="freqValue">440 Hz</span> | |
| </div> | |
| <input type="range" id="freqSlider" min="20" max="2000" value="440" step="1"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label"> | |
| <span>Amplitude / Volume</span> | |
| <span class="control-value" id="volValue">50%</span> | |
| </div> | |
| <input type="range" id="volSlider" min="0" max="100" value="50" step="1"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label"> | |
| <span>Wave Speed</span> | |
| <span class="control-value" id="speedValue">1.0x</span> | |
| </div> | |
| <input type="range" id="speedSlider" min="0.1" max="3" value="1" step="0.1"> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title">Color Theme</span> | |
| </div> | |
| <div class="theme-grid" id="themeGrid"> | |
| <div class="theme-option" data-hue="180" data-selected="true" style="background: oklch(70% 0.2 180)"></div> | |
| <div class="theme-option" data-hue="280" data-selected="false" style="background: oklch(70% 0.2 280)"></div> | |
| <div class="theme-option" data-hue="30" data-selected="false" style="background: oklch(70% 0.2 30)"></div> | |
| <div class="theme-option" data-hue="120" data-selected="false" style="background: oklch(70% 0.2 120)"></div> | |
| <div class="theme-option" data-hue="0" data-selected="false" style="background: oklch(70% 0.2 0)"></div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <span class="panel-title">Visual Mode</span> | |
| </div> | |
| <div class="control-group"> | |
| <div class="control-label"> | |
| <span>Show Character Wave</span> | |
| <span class="control-value" id="textModeValue">Off</span> | |
| </div> | |
| <input type="range" id="textModeSlider" min="0" max="1" value="0" step="1"> | |
| </div> | |
| </div> | |
| </aside> | |
| </main> | |
| <footer class="app-footer"> | |
| <p>Oscilloscope Studio — Interactive Web Audio Component</p> | |
| </footer> | |
| <script> | |
| // Application State | |
| const state = { | |
| isPlaying: false, | |
| waveType: 'sine', | |
| frequency: 440, | |
| volume: 0.5, | |
| speed: 1, | |
| hue: 180, | |
| textMode: false, | |
| analyserData: new Float32Array(2048), | |
| time: 0 | |
| }; | |
| // DOM Elements | |
| const canvas = document.getElementById('waveCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const powerBtn = document.getElementById('powerBtn'); | |
| const powerText = document.getElementById('powerText'); | |
| const statusPill = document.getElementById('statusPill'); | |
| const statusText = document.getElementById('statusText'); | |
| const freqSlider = document.getElementById('freqSlider'); | |
| const volSlider = document.getElementById('volSlider'); | |
| const speedSlider = document.getElementById('speedSlider'); | |
| const textModeSlider = document.getElementById('textModeSlider'); | |
| const freqValue = document.getElementById('freqValue'); | |
| const volValue = document.getElementById('volValue'); | |
| const speedValue = document.getElementById('speedValue'); | |
| const textModeValue = document.getElementById('textModeValue'); | |
| const freqDisplay = document.getElementById('freqDisplay'); | |
| const ampDisplay = document.getElementById('ampDisplay'); | |
| const waveDisplay = document.getElementById('waveDisplay'); | |
| const waveOptions = document.querySelectorAll('.wave-option'); | |
| const themeOptions = document.querySelectorAll('.theme-option'); | |
| const textViz = document.getElementById('textViz'); | |
| const root = document.documentElement; | |
| // Audio Context | |
| let audioCtx = null; | |
| let oscillator = null; | |
| let gainNode = null; | |
| let analyser = null; | |
| // Resize Canvas | |
| function resizeCanvas() { | |
| const rect = canvas.parentElement.getBoundingClientRect(); | |
| const dpr = window.devicePixelRatio || 1; | |
| canvas.width = rect.width * dpr; | |
| canvas.height = rect.height * dpr; | |
| ctx.scale(dpr, dpr); | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| resizeCanvas(); | |
| // Initialize Audio | |
| function initAudio() { | |
| if (!audioCtx) { | |
| audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioCtx.createAnalyser(); | |
| analyser.fftSize = 4096; | |
| analyser.smoothingTimeConstant = 0.85; | |
| } | |
| } | |
| function startOscillator() { | |
| initAudio(); | |
| if (audioCtx.state === 'suspended') { | |
| audioCtx.resume(); | |
| } | |
| oscillator = audioCtx.createOscillator(); | |
| gainNode = audioCtx.createGain(); | |
| oscillator.type = state.waveType; | |
| oscillator.frequency.setValueAtTime(state.frequency, audioCtx.currentTime); | |
| gainNode.gain.setValueAtTime(state.volume * 0.3, audioCtx.currentTime); | |
| oscillator.connect(gainNode); | |
| gainNode.connect(analyser); | |
| analyser.connect(audioCtx.destination); | |
| oscillator.start(); | |
| state.isPlaying = true; | |
| updateUI(); | |
| } | |
| function stopOscillator() { | |
| if (oscillator) { | |
| oscillator.stop(); | |
| oscillator.disconnect(); | |
| oscillator = null; | |
| } | |
| if (gainNode) { | |
| gainNode.disconnect(); | |
| gainNode = null; | |
| } | |
| state.isPlaying = false; | |
| updateUI(); | |
| } | |
| function updateUI() { | |
| powerBtn.setAttribute('data-active', state.isPlaying); | |
| powerText.textContent = state.isPlaying ? 'Stop Oscillator' : 'Initialize Oscillator'; | |
| statusPill.setAttribute('data-active', state.isPlaying); | |
| statusText.textContent = state.isPlaying ? 'Live Output' : 'Standby'; | |
| } | |
| function updateParams() { | |
| if (oscillator && audioCtx) { | |
| oscillator.type = state.waveType; | |
| oscillator.frequency.setTargetAtTime(state.frequency, audioCtx.currentTime, 0.1); | |
| } | |
| if (gainNode && audioCtx) { | |
| gainNode.gain.setTargetAtTime(state.volume * 0.3, audioCtx.currentTime, 0.1); | |
| } | |
| freqValue.textContent = `${state.frequency} Hz`; | |
| volValue.textContent = `${Math.round(state.volume * 100)}%`; | |
| speedValue.textContent = `${state.speed}x`; | |
| textModeValue.textContent = state.textMode ? 'On' : 'Off'; | |
| freqDisplay.textContent = `${state.frequency} Hz`; | |
| ampDisplay.textContent = `${Math.round(state.volume * 100)}%`; | |
| waveDisplay.textContent = state.waveType.charAt(0).toUpperCase() + state.waveType.slice(1); | |
| root.style.setProperty('--wave-hue', state.hue); | |
| textViz.setAttribute('data-active', state.textMode); | |
| } | |
| // Event Listeners | |
| powerBtn.addEventListener('click', () => { | |
| if (state.isPlaying) { | |
| stopOscillator(); | |
| } else { | |
| startOscillator(); | |
| } | |
| }); | |
| freqSlider.addEventListener('input', (e) => { | |
| state.frequency = parseInt(e.target.value); | |
| updateParams(); | |
| }); | |
| volSlider.addEventListener('input', (e) => { | |
| state.volume = parseInt(e.target.value) / 100; | |
| updateParams(); | |
| }); | |
| speedSlider.addEventListener('input', (e) => { | |
| state.speed = parseFloat(e.target.value); | |
| updateParams(); | |
| }); | |
| textModeSlider.addEventListener('input', (e) => { | |
| state.textMode = parseInt(e.target.value) === 1; | |
| updateParams(); | |
| }); | |
| waveOptions.forEach(opt => { | |
| opt.addEventListener('click', () => { | |
| waveOptions.forEach(o => o.setAttribute('data-selected', 'false')); | |
| opt.setAttribute('data-selected', 'true'); | |
| state.waveType = opt.getAttribute('data-wave'); | |
| updateParams(); | |
| }); | |
| }); | |
| themeOptions.forEach(opt => { | |
| opt.addEventListener('click', () => { | |
| themeOptions.forEach(o => o.setAttribute('data-selected', 'false')); | |
| opt.setAttribute('data-selected', 'true'); | |
| state.hue = parseInt(opt.getAttribute('data-hue')); | |
| updateParams(); | |
| }); | |
| }); | |
| // Text Visualization Setup | |
| const chars = 'ooooooooooooooooooooooooossssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss |