diff --git a/index.html b/index.html index 85196e4..f05e742 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,64 @@ - + - Audio Harmonograph + Retro Harmonograph Live + + + + +
+ +
+ + Harmonograph Control Unit +
+ + + +
+ +
OSC PARAMETERS
+ + + + + + + +
+ +
VISUAL PRESETS
+ + +
+ STATUS: Ready. Press Live Signal to start. +
+ +
+ - + \ No newline at end of file diff --git a/main.js b/main.js index f2c1fa0..96d28b3 100644 --- a/main.js +++ b/main.js @@ -1,49 +1,114 @@ +// main.js - Application Orchestrator + +// ================================ +// 1. IMPORTS +// ================================ + +// Imports esențiale din ambele ramuri: +import { initializeControls } from './ui/controls.js'; import { initAudio, updateAudio, audioEngine, audioData, ensureAudioRunning } from "./audio/audioEngine.js"; import { generateHarmonographPoints } from "./harmonograph/harmonograph.js"; import { renderFrame } from "./render/renderer.js"; +// Global variables let canvas, ctx; +let started = false; // Necessar pentru controlul audio (din HEAD) + +// Time variable for Harmonograph animation let t = 0; -let started = false; -function resize() { + +// ================================ +// 2. CANVAS SETUP & RESIZE +// ================================ + +// Păstrăm funcția de setup din a7e96c4 pentru a inițializa canvas-ul +function setupCanvas() { + canvas = document.getElementById("screen"); + ctx = canvas.getContext("2d"); + resizeCanvas(); // Folosim resizeCanvas + window.addEventListener("resize", resizeCanvas); +} + +// Renunțăm la resize() și păstrăm resizeCanvas() din a7e96c4 +function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } -// 🔥 Browserul NU permite audio până la un click/tap/tastă + +// ================================ +// 3. AUDIO UNLOCK & STARTUP (Logică preluată din HEAD) +// ================================ + +// 🔥 Browserul NU permite audio până la un click/tap/tastă. const unlockAudio = async () => { if (started) return; started = true; await start(); }; +// Ascultători pentru deblocarea audio window.addEventListener("pointerdown", unlockAudio, { once: true }); window.addEventListener("keydown", unlockAudio, { once: true }); async function start() { - canvas = document.getElementById("screen"); + // Reinițializăm canvas-ul și contextul, deoarece setupCanvas() din a7e96c4 făcea asta + canvas = document.getElementById("screen"); ctx = canvas.getContext("2d"); + resizeCanvas(); // Folosim resizeCanvas + window.addEventListener("resize", resizeCanvas); - resize(); - window.addEventListener("resize", resize); - + // 1. Initialize Audio await initAudio(); await ensureAudioRunning(); + + // 2. Initialize UI Controls (din a7e96c4) + initializeControls(); + + // 3. Start the animation loop loop(); + + // Mesaj de status (din a7e96c4) + console.log("Application initialized. Ready to render."); } + +// ================================ +// 4. MAIN ANIMATION LOOP (Combinată) +// ================================ + +/** + * Main animation loop (runs at ~60 FPS). + */ function loop() { requestAnimationFrame(loop); + // 1. Audio Analysis (Din HEAD) updateAudio(); const vol = audioData.volume || 0; + // 2. Update Time (Controls the slow, organic rotation of the figure) (Din HEAD) // accelerăm timpul în funcție de audio const speed = 0.5 + vol * 2.0 + audioData.energy * 1.2; t += speed; + // 3. Drawing (Din HEAD) const points = generateHarmonographPoints(t, canvas); + // 4. Render (Folosim renderFrame din HEAD/Renderer, care include efectul CRT) renderFrame(ctx, points); } + + +// ================================ +// 5. INITIALIZATION FALLBACK (Modificat) +// ================================ + +// Păstrăm listenerul 'DOMContentLoaded' doar pentru logica UI care nu necesită audio, +// dar pornim bucla de animație în 'start()' (după click) +document.addEventListener('DOMContentLoaded', () => { + // setupCanvas și initializeControls sunt mutate în start() + // pentru a se asigura că funcționează după ce canvas-ul este gata. + // Lăsăm această secțiune goală, deoarece 'start' este punctul de intrare real. +}); \ No newline at end of file diff --git a/style.css b/style.css index f9d1e5f..6b6bdc1 100644 --- a/style.css +++ b/style.css @@ -1,14 +1,339 @@ +/* Utilizam fontul importat in index.html */ +:root { + --color-primary: #00ff66; /* Phosphor Green */ + --color-secondary: #00e0e0; /* Cyan */ + --color-error: #ff3333; /* Red for Off/Error */ + --bg-dark: #020202; /* Deep Black */ +} + +/* Stiluri de bază combinate */ html, body { margin: 0; - padding: 0; + padding: 0; /* Păstrat din HEAD */ + height: 100%; /* Păstrat din HEAD */ + width: 100%; /* Păstrat din HEAD */ +} + +body { + /* Preluat din a7e96c4, care definește stilul retro */ overflow: hidden; - background: #002b2f; - height: 100%; - width: 100%; + background: var(--bg-dark); + font-family: 'VT323', monospace; + color: var(--color-primary); + user-select: none; } #screen { position: absolute; top: 0; left: 0; + z-index: 1; + border-radius: 1%; + box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.4); +} + +/* -------------------------------------- */ +/* CONTROL PANEL (Side UI) */ +/* -------------------------------------- */ + +#control-panel { + position: absolute; + top: 20px; + right: 20px; + width: 280px; + z-index: 10; + padding: 15px; + background: rgba(0, 0, 0, 0.85); + border: 2px solid var(--color-primary); + box-shadow: 0 0 15px var(--color-primary); + transition: all 0.4s ease-in-out; +} + +.panel-header { + position: relative; + font-size: 1.2em; + margin-bottom: 10px; + /* Linia punctată */ + border-bottom: 1px dashed var(--color-secondary); + padding-bottom: 5px; + + /* Ridicăm header-ul puțin pentru a-l depărta de marginea de sus a panel-ului */ + padding-top: 5px; + + /* NOU: Spațiu ajustat pentru butonul mai mic (30px) */ + padding-left: 30px; +} + +.section-title { + color: var(--color-secondary); + margin: 15px 0 5px 0; + font-size: 0.9em; +} + +.divider { + border: none; + border-top: 1px dotted rgba(0, 255, 102, 0.5); + margin: 10px 0; +} + +/* -------------------------------------- */ +/* BUTON MINIMIZARE (PĂTRĂȚEL) */ +/* -------------------------------------- */ + +.control-icon { + position: absolute; + left: 0; + right: auto; + + /* Rămâne ridicat de linia albastră */ + top: -1px; + + background: transparent; + border: 1px solid var(--color-primary); + color: var(--color-primary); + + display: block; + + /* Padding ajustat */ + padding: 0px; + /* Dimensiune NOUĂ */ + width: 22px; + height: 22px; + box-sizing: border-box; + + /* Font size ajustat */ + font-size: 0.9em; + /* Ajustăm line-height pentru a ridica simbolul */ + line-height: 1; + + cursor: pointer; + text-align: center; + transition: color 0.2s, background 0.2s; +} + +.control-icon:hover { + color: var(--color-secondary); + background: rgba(0, 255, 102, 0.1); +} + +/* -------------------------------------- */ +/* SLIDERE & ALINIERE (STIL ANALOG) */ +/* -------------------------------------- */ + +/* Containerul (Label) pentru aliniere Text <-> Slider */ +.control-label { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 10px; + font-size: 1.1em; +} + +/* -------------------------------------- */ +/* SLIDERE & ALINIERE (STIL RETRO BAR SEGMENTAT) */ +/* -------------------------------------- */ + +/* Bara slider-ului (INPUT TYPE=RANGE) */ +input[type="range"] { + -webkit-appearance: none; + width: 150px; + max-width: 150px; + height: 12px; + border: 1px solid var(--color-secondary); + background: var(--bg-dark); + cursor: pointer; + margin-top: 0; + margin-left: 10px; + border-radius: 0; +} + +/* 1A. WEB-KIT TRACK (Fundal, Grilă și Progres - TOTUL AICI) */ +input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 100%; + cursor: pointer; + border-radius: 0; + + background: + repeating-linear-gradient( + to right, + var(--color-primary), + var(--color-primary) 3px, + transparent 3px, + transparent 7px + ) 0 0 / var(--progress-percent, 50%) 100% no-repeat, + + repeating-linear-gradient( + to right, + rgba(255, 255, 255, 0.05), + rgba(255, 255, 255, 0.05) 2px, + transparent 2px, + transparent 7px + ), + #111; + + box-shadow: + 0 0 3px var(--color-primary) inset; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + height: 12px; + width: 1px; /* Facem Thumb-ul subțire */ + border-radius: 0; + background: var(--color-primary); + cursor: pointer; + margin-top: 0; +} + +input[type="range"]::-moz-range-track { + width: 100%; + height: 100%; + cursor: pointer; + border-radius: 0; + + background: repeating-linear-gradient( + to right, + rgba(255, 255, 255, 0.05), + rgba(255, 255, 255, 0.05) 2px, + transparent 2px, + transparent 7px + ), #111; + + box-shadow: + 0 0 3px var(--color-primary) inset; +} + +input[type="range"]::-moz-range-progress { + height: 100%; + background: repeating-linear-gradient( + to right, + var(--color-primary), + var(--color-primary) 3px, + transparent 3px, + transparent 7px + ); + border-radius: 0; +} + +input[type="range"]::-moz-range-thumb { + height: 12px; + width: 1px; + border: none; + background: var(--color-primary); + cursor: pointer; +} + +/* -------------------------------------- */ +/* RESTUL ELEMENTELOR UI (Nemodificat) */ +/* -------------------------------------- */ + +.retro-btn { + width: 100%; + padding: 15px 10px; + border: 3px solid; + font-family: 'VT323', monospace; + font-size: 1.2em; + cursor: pointer; + transition: all 0.15s ease; +} + +.retro-btn.off { + background: var(--bg-dark); + border-color: var(--color-error); + color: var(--color-error); + box-shadow: 0 0 8px var(--color-error); +} + +.retro-btn.on { + background: var(--bg-dark); + border-color: var(--color-primary); + color: var(--color-primary); + box-shadow: 0 0 15px var(--color-primary); +} + +.retro-btn:active { + transform: scale(0.98); +} + +.retro-select { + width: 100%; + padding: 8px; + background: var(--bg-dark); + border: 1px solid var(--color-secondary); + color: var(--color-primary); + font-family: 'VT323', monospace; + font-size: 1em; + margin-top: 5px; +} + +/* -------------------------------------- */ +/* STATUS / TERMINAL DISPLAY */ +/* -------------------------------------- */ + +#status-display { + margin-top: 20px; + padding: 10px; + min-height: 40px; + background: rgba(0, 0, 0, 0.7); + border: 1px dashed var(--color-secondary); + color: var(--color-primary); + + white-space: pre-wrap; + font-size: 1.1em; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + line-height: 1.2; + + padding-left: 10px; + text-indent: 0; +} + +/* -------------------------------------- */ +/* MINIMIZARE STATE */ +/* -------------------------------------- */ + +#control-panel.minimized { + width: 25px; + height: 25px; + padding: 3px; + + top: 20px; + right: 20px; + bottom: auto; + + overflow: hidden; + box-shadow: 0 0 5px var(--color-primary); + border-radius: 5px; +} + +#control-panel.minimized .panel-header { + border-bottom: none; + padding-bottom: 0; + font-size: 0; + height: 100%; + padding-left: 0; +} + +#control-panel.minimized .control-icon { + position: static; + width: 100%; + height: 100%; + font-size: 1.2em; + padding: 0; + line-height: 1; + + border: none; +} + +#control-panel.minimized .retro-btn, +#control-panel.minimized .section-title, +#control-panel.minimized .divider, +#control-panel.minimized .control-label, +#control-panel.minimized .retro-select, +#control-panel.minimized #status-display { + display: none; } \ No newline at end of file diff --git a/ui/controls.js b/ui/controls.js index e69de29..82a1e68 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -0,0 +1,150 @@ +// ui/controls.js - Gestioneaza evenimentele si controalele UI + +// ====================================== +// STATE MANAGEMENT (Mutati in ui/state.js daca e cazul) +// ====================================== +const state = { + audioActive: false, + sensitivity: 0.5, + damping: 0.003, + glowIntensity: 12, + visualStyle: 'CRT_GREEN', +}; + +export const getState = () => state; + +export const updateStatusDisplay = (message) => { + const display = document.getElementById('status-display'); + if (display) { + // Limitam mesajul pentru a nu fi prea lung pe ecranul terminal + const shortMessage = message.length > 50 ? message.substring(0, 47) + '...' : message; + display.textContent = `STATUS: ${shortMessage}`; + } +}; + +export const updateState = (key, value) => { + state[key] = value; + // Afisam valoarea in status display + const formattedValue = typeof value === 'number' ? value.toFixed(4).replace(/\.?0+$/, '') : value; + updateStatusDisplay(`${key.charAt(0).toUpperCase() + key.slice(1)} set to ${formattedValue}`); +}; +// ====================================== + + +export const initializeControls = () => { + // 1. Identifica elementele + const controlPanel = document.getElementById('control-panel'); + const minimizeBtn = document.getElementById('minimizeBtn'); + const audioBtn = document.getElementById('audioBtn'); + + const sensSlider = document.getElementById('sensSlider'); + const dampSlider = document.getElementById('dampSlider'); + const glowSlider = document.getElementById('glowSlider'); + const styleSelect = document.getElementById('styleSelect'); + + // Salvăm simbolul inițial (care ar trebui să fie '::') + const initialMinimizeSymbol = minimizeBtn ? minimizeBtn.textContent.trim() : '::'; + + + // 2. Functia de Minimizare/Maximizare + const toggleMinimize = () => { + controlPanel.classList.toggle('minimized'); + + // Schimba simbolul pentru feedback vizual: '::' pentru Maximizat, '[+]' pentru Minimizat + if (controlPanel.classList.contains('minimized')) { + // Când este minimizat, arătăm simbolul de maximizare + minimizeBtn.textContent = ' _ '; + } else { + // Când este maximizat, revenim la simbolul inițial + minimizeBtn.textContent = ` ${initialMinimizeSymbol} `; + } + }; + + // Asigurăm că simbolul inițial este setat corect la început (folosind initialMinimizeSymbol) + if (minimizeBtn) { + minimizeBtn.textContent = ` ${initialMinimizeSymbol} `; + } + + + // 3. Ataseaza functia de Minimizare + if (minimizeBtn && controlPanel) { + minimizeBtn.addEventListener('click', toggleMinimize); + } + + // 4. Logica pentru butonul LIVE SIGNAL + audioBtn.addEventListener('click', () => { + const isActive = !getState().audioActive; + updateState('audioActive', isActive); + + audioBtn.classList.toggle('off', !isActive); + audioBtn.classList.toggle('on', isActive); + + if (isActive) { + updateStatusDisplay("Live Signal ON. Listening for audio input..."); + // TODO: initializeAudio() + } else { + updateStatusDisplay("Live Signal OFF. Animation frozen."); + // TODO: stopAudio() + } + }); + + + // 5. Functia care calculeaza si aplica progresul vizual retro (BAR SEGMENTAT) + const handleSliderChange = (event) => { + const slider = event.target; + const value = parseFloat(slider.value); + const min = parseFloat(slider.min); + const max = parseFloat(slider.max); + + // Calculează progresul ca procent (0% la 100%) + const percent = ((value - min) / (max - min)) * 100; + + // Injectează progresul ca variabilă CSS (--progress-percent) + slider.style.setProperty('--progress-percent', `${percent}%`); + + // Logica de state management + let stateKey; + if (slider.id === 'sensSlider') stateKey = 'sensitivity'; + else if (slider.id === 'dampSlider') stateKey = 'damping'; + else if (slider.id === 'glowSlider') stateKey = 'glowIntensity'; + + if (stateKey) { + updateState(stateKey, value); + } + }; + + // Functie de initializare a progresului vizual (necesara la incarcarea paginii) + const setInitialProgress = (slider) => { + if (!slider) return; + const value = parseFloat(slider.value); + const min = parseFloat(slider.min); + const max = parseFloat(slider.max); + const percent = ((value - min) / (max - min)) * 100; + slider.style.setProperty('--progress-percent', `${percent}%`); + } + + // 6. Ataseaza functiile la slidere + if (sensSlider) { + sensSlider.addEventListener('input', handleSliderChange); + setInitialProgress(sensSlider); + } + if (dampSlider) { + dampSlider.addEventListener('input', handleSliderChange); + setInitialProgress(dampSlider); + } + if (glowSlider) { + glowSlider.addEventListener('input', handleSliderChange); + setInitialProgress(glowSlider); + } + + // 7. Select Presets + if (styleSelect) { + styleSelect.addEventListener('change', (event) => { + const newStyle = event.target.value; + updateState('visualStyle', newStyle); + }); + } + + // Seteaza mesajul initial + updateStatusDisplay("System Initialized. Ready to render."); +}; \ No newline at end of file diff --git a/ui/presets.js b/ui/presets.js index 20d643d..bd69421 100644 --- a/ui/presets.js +++ b/ui/presets.js @@ -1,23 +1,56 @@ -export const presets = { +// ui/presets.js - Definiții pentru stilurile vizuale (preset-uri) + +export const PRESETS = { + // CRT_GREEN (Combinat) CRT_GREEN: { - color: '#00ff66', - glow: 14, + color: '#00FF66', lineWidth: 1.5, + + // Din a7e96c4 (Control Unit) - Damping + damping: 0.003, + scanlineIntensity: 0.8, + + // Din HEAD - Render Effects + glow: 14, scanlines: 0.12, noise: 0.04 }, + + // NEON_BLUE (Doar din HEAD) NEON_BLUE: { color: '#00d4ff', - glow: 18, lineWidth: 1.7, + glow: 18, scanlines: 0.08, - noise: 0.03 + noise: 0.03, + // Adăugăm proprietăți din a7e96c4 cu valori implicite rezonabile + damping: 0.002, + scanlineIntensity: 0.4 }, + + // VAPORWAVE (Combinat) VAPORWAVE: { - color: '#ff66cc', + color: '#ff66cc', // Păstrat din HEAD + lineWidth: 1.8, // Păstrat din HEAD + + // Din a7e96c4 + damping: 0.001, + scanlineIntensity: 0.2, + + // Din HEAD glow: 20, - lineWidth: 1.8, scanlines: 0.10, noise: 0.05 + }, + + // ANALOG_RED (Doar din HEAD) + ANALOG_RED: { + color: "#ff3333", + lineWidth: 2.0, + damping: 0.004, + scanlineIntensity: 0.9, + glow: 15, + scanlines: 0.15, + noise: 0.06 } -}; +}; \ No newline at end of file