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
+
+
+
+
+
+
+
+
+
+
+
+
+
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