From 533faa58accfc4422e80ce2be6e18db684dca2c5 Mon Sep 17 00:00:00 2001 From: James Chang <120231221+jameszjwchang@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:56:07 +0800 Subject: [PATCH 1/2] Revamp UI and add spatial composer mode --- index.html | 259 +++++--- js/ComposerSpace.js | 694 +++++++++++++++++++++ styles.css | 1446 +++++++++++++++++++------------------------ 3 files changed, 1476 insertions(+), 923 deletions(-) create mode 100644 js/ComposerSpace.js diff --git a/index.html b/index.html index 5495b46..a6292bc 100644 --- a/index.html +++ b/index.html @@ -8,145 +8,152 @@ -
+

Dialogue de l'ombre double

- - (7/27) Explore Pierre Boulez's iconic composition from the perspectives
of the audio engineer, performer & audience. + (7/27) Explore Pierre Boulez's iconic composition from the perspectives
of the audio engineer, performer & an audience.

- - +

+ +
- - -
-
Mode:
-
- - + +
+
+
+ +
+
Mode
+
+ + +
+
+ + +
+ + +
+ + +
+
Movements
+ + + + +
+
+ +
+ + + +
+ +
+
+
Soundstage Map
+ +
+ +
-
- -
- - -
- - -
- - - - -
+
+
+
Transport
+
+ + +
+
- - - - - - - - - - - - + + + - + + - diff --git a/js/ComposerSpace.js b/js/ComposerSpace.js new file mode 100644 index 0000000..1a89dcd --- /dev/null +++ b/js/ComposerSpace.js @@ -0,0 +1,694 @@ +const DEG2RAD = Math.PI / 180; + +class SpatialComposer { + constructor() { + this.root = document.getElementById('composer-space'); + this.sceneContainer = document.getElementById('composer-scene'); + this.enterBtn = document.getElementById('enterComposerSpace'); + this.exitBtn = document.getElementById('exitComposerSpace'); + this.addNoteBtn = document.getElementById('addNoteBtn'); + this.playBtn = document.getElementById('playCompositionBtn'); + this.saveBtn = document.getElementById('saveCompositionBtn'); + this.autoSpinBtn = document.getElementById('autoSpinBtn'); + this.rotationSlider = document.getElementById('sphereRotationSlider'); + this.statusEl = document.getElementById('composer-status'); + this.noteListEl = document.getElementById('composer-noteList'); + this.coordInputs = { + x: document.getElementById('coordX'), + y: document.getElementById('coordY'), + z: document.getElementById('coordZ') + }; + + if (!this.root || !this.sceneContainer || !this.enterBtn || !this.exitBtn) { + return; + } + + this.sphereRadius = 6; + this.moveSpeed = 8; + this.lookSensitivity = 0.0025; + this.autoSpinSpeed = DEG2RAD * 12; + this.notes = []; + this.moveState = { forward: false, back: false, left: false, right: false, up: false, down: false }; + this.isPointerDown = false; + this.lastPointer = { x: 0, y: 0 }; + this.isActive = false; + this.isAdjustingRotation = false; + this.autoSpin = false; + this.liveContext = null; + this.activeTimeout = null; + + this.animate = this.animate.bind(this); + this.handleResize = this.handleResize.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + this.onKeyUp = this.onKeyUp.bind(this); + + this.setupThree(); + this.bindUI(); + this.setStatus('Drop coordinates to begin sculpting.'); + } + + setupThree() { + const width = this.sceneContainer.clientWidth; + const height = this.sceneContainer.clientHeight; + + this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.renderer.setSize(width, height); + this.sceneContainer.appendChild(this.renderer.domElement); + + this.scene = new THREE.Scene(); + this.scene.background = new THREE.Color(0x05070f); + this.scene.fog = new THREE.FogExp2(0x05070f, 0.018); + + this.camera = new THREE.PerspectiveCamera(70, width / height, 0.1, 200); + this.camera.position.set(0, 2, 14); + this.yaw = Math.PI; + this.pitch = 0; + + const ambient = new THREE.AmbientLight(0xf2d6c3, 0.35); + this.scene.add(ambient); + + const hemi = new THREE.HemisphereLight(0xffb37f, 0x16274f, 0.5); + this.scene.add(hemi); + + const dirLight = new THREE.DirectionalLight(0xffe8d1, 0.6); + dirLight.position.set(6, 10, 6); + dirLight.castShadow = false; + this.scene.add(dirLight); + + this.stageGroup = new THREE.Group(); + this.scene.add(this.stageGroup); + + const sphereGeometry = new THREE.SphereGeometry(this.sphereRadius, 64, 64); + const sphereMaterial = new THREE.MeshPhongMaterial({ + color: 0x1e2a52, + wireframe: true, + transparent: true, + opacity: 0.2 + }); + this.sonicSphere = new THREE.Mesh(sphereGeometry, sphereMaterial); + this.stageGroup.add(this.sonicSphere); + + const floorGeometry = new THREE.CircleGeometry(this.sphereRadius, 64); + const floorMaterial = new THREE.MeshBasicMaterial({ + color: 0x0d1324, + transparent: true, + opacity: 0.55 + }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + this.stageGroup.add(floor); + + const orbitRingGeometry = new THREE.RingGeometry(this.sphereRadius * 0.65, this.sphereRadius * 0.65 + 0.03, 128); + const orbitMaterial = new THREE.MeshBasicMaterial({ + color: 0xffb05a, + transparent: true, + opacity: 0.25, + side: THREE.DoubleSide + }); + const orbit = new THREE.Mesh(orbitRingGeometry, orbitMaterial); + orbit.rotation.x = -Math.PI / 2; + this.stageGroup.add(orbit); + + const verticalRing = orbit.clone(); + verticalRing.rotation.set(0, 0, Math.PI / 2); + this.stageGroup.add(verticalRing); + + const particles = this.createStarfield(850, 90); + this.scene.add(particles); + + this.noteGroup = new THREE.Group(); + this.stageGroup.add(this.noteGroup); + + this.clock = new THREE.Clock(); + this.updateCameraDirection(); + } + + createStarfield(count, spread) { + const positions = new Float32Array(count * 3); + for (let i = 0; i < count; i++) { + positions[i * 3] = (Math.random() - 0.5) * spread; + positions[i * 3 + 1] = (Math.random() - 0.5) * spread; + positions[i * 3 + 2] = (Math.random() - 0.5) * spread; + } + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + const material = new THREE.PointsMaterial({ + color: 0xffd5a6, + size: 0.35, + transparent: true, + opacity: 0.6 + }); + return new THREE.Points(geometry, material); + } + + bindUI() { + window.addEventListener('resize', this.handleResize); + window.addEventListener('keydown', this.onKeyDown); + window.addEventListener('keyup', this.onKeyUp); + + this.sceneContainer.addEventListener('mousedown', (event) => { + if (!this.isActive) return; + this.isPointerDown = true; + this.lastPointer.x = event.clientX; + this.lastPointer.y = event.clientY; + }); + + window.addEventListener('mouseup', () => { + this.isPointerDown = false; + }); + + window.addEventListener('mousemove', (event) => { + if (!this.isPointerDown || !this.isActive) return; + const dx = event.clientX - this.lastPointer.x; + const dy = event.clientY - this.lastPointer.y; + this.lastPointer.x = event.clientX; + this.lastPointer.y = event.clientY; + + this.yaw -= dx * this.lookSensitivity; + this.pitch -= dy * this.lookSensitivity; + const maxPitch = Math.PI / 2 - 0.2; + this.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.pitch)); + this.updateCameraDirection(); + }); + + this.enterBtn.addEventListener('click', () => this.enter()); + this.exitBtn.addEventListener('click', () => this.exit()); + + if (this.addNoteBtn) { + this.addNoteBtn.addEventListener('click', () => this.handleAddNote()); + } + + if (this.playBtn) { + this.playBtn.addEventListener('click', () => this.playComposition()); + } + + if (this.saveBtn) { + this.saveBtn.addEventListener('click', () => this.saveComposition()); + } + + if (this.autoSpinBtn) { + this.autoSpinBtn.addEventListener('click', () => { + this.autoSpin = !this.autoSpin; + this.autoSpinBtn.classList.toggle('active', this.autoSpin); + this.setStatus(this.autoSpin ? 'Sphere spinning — listen for orbital sweeps.' : 'Sphere rotation paused.'); + }); + } + + if (this.rotationSlider) { + this.rotationSlider.addEventListener('pointerdown', () => { + this.isAdjustingRotation = true; + this.autoSpin = false; + this.autoSpinBtn?.classList.remove('active'); + }); + this.rotationSlider.addEventListener('pointerup', () => { + this.isAdjustingRotation = false; + }); + this.rotationSlider.addEventListener('touchstart', () => { + this.isAdjustingRotation = true; + this.autoSpin = false; + this.autoSpinBtn?.classList.remove('active'); + }, { passive: true }); + this.rotationSlider.addEventListener('touchend', () => { + this.isAdjustingRotation = false; + }); + this.rotationSlider.addEventListener('input', (event) => { + const degrees = Number(event.target.value); + this.stageGroup.rotation.y = degrees * DEG2RAD; + this.setStatus(`Sphere rotated ${degrees.toFixed(0)}°.`); + }); + } + + if (this.noteListEl) { + this.noteListEl.addEventListener('click', (event) => { + const target = event.target; + if (target instanceof HTMLElement && target.dataset.index) { + const index = Number(target.dataset.index); + this.removeNote(index); + } + }); + } + + document.addEventListener('visibilitychange', () => { + if (document.hidden && this.isActive) { + this.exit(); + } + }); + } + + enter() { + if (this.isActive) return; + document.body.classList.add('composer-active'); + this.root?.setAttribute('aria-hidden', 'false'); + this.exitBtn?.setAttribute('aria-hidden', 'false'); + this.isActive = true; + this.clock.start(); + this.renderer.setAnimationLoop(this.animate); + this.handleResize(); + this.setStatus('Spatial atelier ready — sculpt your constellation.'); + } + + exit() { + if (!this.isActive) return; + document.body.classList.remove('composer-active'); + this.root?.setAttribute('aria-hidden', 'true'); + this.exitBtn?.setAttribute('aria-hidden', 'true'); + this.isActive = false; + this.renderer.setAnimationLoop(null); + this.stopPlayback(); + this.autoSpin = false; + this.autoSpinBtn?.classList.remove('active'); + if (this.rotationSlider) { + this.rotationSlider.value = '0'; + } + this.stageGroup.rotation.y = 0; + this.camera.position.set(0, 2, 14); + this.yaw = Math.PI; + this.pitch = 0; + this.updateCameraDirection(); + this.setStatus('Drop coordinates to begin sculpting.'); + } + + handleAddNote() { + const x = parseFloat(this.coordInputs.x?.value || '0'); + const y = parseFloat(this.coordInputs.y?.value || '0'); + const z = parseFloat(this.coordInputs.z?.value || '0'); + + if ([x, y, z].some((v) => Number.isNaN(v))) { + this.setStatus('Enter numeric values for X, Y, and Z.'); + return; + } + + const position = new THREE.Vector3(x, y, z); + const distance = position.length(); + if (distance > this.sphereRadius) { + position.setLength(this.sphereRadius - 0.2); + this.setStatus('Point nudged inside the sphere for resonance.', true); + } else { + this.setStatus('Resonance seeded inside the sphere.', true); + } + + const hue = THREE.MathUtils.mapLinear(position.y, -this.sphereRadius, this.sphereRadius, 0.58, 0.03); + const color = new THREE.Color().setHSL(THREE.MathUtils.euclideanModulo(hue, 1), 0.65, 0.55); + const emissive = color.clone().multiplyScalar(0.35); + + const noteMesh = new THREE.Mesh( + new THREE.SphereGeometry(0.25, 24, 24), + new THREE.MeshStandardMaterial({ + color, + emissive, + metalness: 0.45, + roughness: 0.35 + }) + ); + noteMesh.position.copy(position); + noteMesh.castShadow = false; + this.noteGroup.add(noteMesh); + + const noteData = { + position, + mesh: noteMesh + }; + this.notes.push(noteData); + this.updateNoteList(); + } + + removeNote(index) { + if (index < 0 || index >= this.notes.length) return; + const note = this.notes[index]; + if (note.mesh) { + this.noteGroup.remove(note.mesh); + note.mesh.geometry.dispose(); + note.mesh.material.dispose(); + } + this.notes.splice(index, 1); + this.updateNoteList(); + this.setStatus('Resonance removed.'); + } + + updateNoteList() { + if (!this.noteListEl) return; + this.noteListEl.innerHTML = ''; + + this.notes.forEach((note, index) => { + const li = document.createElement('li'); + const coords = note.position; + const label = `X ${coords.x.toFixed(2)} · Y ${coords.y.toFixed(2)} · Z ${coords.z.toFixed(2)}`; + li.innerHTML = `${label}`; + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.textContent = 'Remove'; + removeBtn.dataset.index = String(index); + li.appendChild(removeBtn); + this.noteListEl.appendChild(li); + }); + + if (this.notes.length === 0) { + const li = document.createElement('li'); + li.textContent = 'No resonances yet — drop a point to begin.'; + this.noteListEl.appendChild(li); + } + } + + playComposition() { + if (this.notes.length === 0) { + this.setStatus('Add at least one resonance point before playing.'); + return; + } + + this.stopPlayback(); + + const AudioCtx = window.AudioContext || window.webkitAudioContext; + if (!AudioCtx) { + this.setStatus('AudioContext not supported in this browser.'); + return; + } + + const context = new AudioCtx(); + this.liveContext = context; + const now = context.currentTime + 0.15; + const masterGain = context.createGain(); + masterGain.gain.setValueAtTime(0.9, now); + masterGain.connect(context.destination); + + this.notes.forEach((note, index) => { + const startOffset = index * 0.7 + this.mapToDelay(note.position); + const osc = context.createOscillator(); + const gain = context.createGain(); + const pan = context.createStereoPanner(); + + osc.type = this.mapToWave(note.position); + osc.frequency.setValueAtTime(this.mapToFrequency(note.position), now + startOffset); + + const gainValue = this.mapToGain(note.position); + gain.gain.setValueAtTime(0.0001, now + startOffset); + gain.gain.exponentialRampToValueAtTime(gainValue, now + startOffset + 0.05); + gain.gain.exponentialRampToValueAtTime(0.0001, now + startOffset + 0.6); + + pan.pan.setValueAtTime(this.mapToPan(note.position), now + startOffset); + + osc.connect(gain); + gain.connect(pan); + pan.connect(masterGain); + + osc.start(now + startOffset); + osc.stop(now + startOffset + 0.7); + }); + + const duration = this.notes.length * 0.7 + 1.5; + this.setStatus('Live mix playing — listen in motion.', true); + this.activeTimeout = window.setTimeout(() => { + this.setStatus('Playback complete. Ready for the next constellation.'); + this.stopPlayback(); + }, duration * 1000); + } + + stopPlayback() { + if (this.liveContext) { + try { + this.liveContext.close(); + } catch (error) { + console.warn('ComposerSpace: unable to close context', error); + } + this.liveContext = null; + } + if (this.activeTimeout) { + window.clearTimeout(this.activeTimeout); + this.activeTimeout = null; + } + } + + async saveComposition() { + if (this.notes.length === 0) { + this.setStatus('Add resonances before exporting a mix.'); + return; + } + + const sampleRate = 44100; + const duration = this.notes.length * 0.7 + 2; + const offline = new OfflineAudioContext(2, Math.ceil(sampleRate * duration), sampleRate); + + const masterGain = offline.createGain(); + masterGain.gain.setValueAtTime(0.9, 0); + masterGain.connect(offline.destination); + + this.notes.forEach((note, index) => { + const startOffset = index * 0.7 + this.mapToDelay(note.position) + 0.5; + const osc = offline.createOscillator(); + const gain = offline.createGain(); + const pan = offline.createStereoPanner(); + + osc.type = this.mapToWave(note.position); + osc.frequency.setValueAtTime(this.mapToFrequency(note.position), startOffset); + + const gainValue = this.mapToGain(note.position); + gain.gain.setValueAtTime(0.0001, startOffset); + gain.gain.exponentialRampToValueAtTime(gainValue, startOffset + 0.08); + gain.gain.exponentialRampToValueAtTime(0.0001, startOffset + 0.75); + + pan.pan.setValueAtTime(this.mapToPan(note.position), startOffset); + + osc.connect(gain); + gain.connect(pan); + pan.connect(masterGain); + + osc.start(startOffset); + osc.stop(startOffset + 0.9); + }); + + this.setStatus('Rendering mix for download…'); + try { + const buffer = await offline.startRendering(); + const blob = this.audioBufferToWav(buffer); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = `boulez-atlier-${Date.now()}.wav`; + anchor.click(); + URL.revokeObjectURL(url); + this.setStatus('Mix downloaded — continue sculpting!', true); + } catch (error) { + console.error('ComposerSpace: unable to export mix', error); + this.setStatus('Something interrupted the export. Please try again.'); + } + } + + mapToFrequency(vector) { + const min = 180; + const max = 840; + const normalized = THREE.MathUtils.clamp((vector.y + this.sphereRadius) / (this.sphereRadius * 2), 0, 1); + return min + (max - min) * normalized; + } + + mapToGain(vector) { + const normalized = THREE.MathUtils.clamp((vector.length() / this.sphereRadius), 0, 1); + return 0.18 + normalized * 0.55; + } + + mapToDelay(vector) { + return THREE.MathUtils.clamp((vector.z + this.sphereRadius) / (this.sphereRadius * 2), 0, 1) * 0.6; + } + + mapToPan(vector) { + return THREE.MathUtils.clamp(vector.x / this.sphereRadius, -1, 1); + } + + mapToWave(vector) { + const normalized = THREE.MathUtils.clamp((vector.x + this.sphereRadius) / (this.sphereRadius * 2), 0, 1); + if (normalized < 0.33) return 'sine'; + if (normalized < 0.66) return 'triangle'; + return 'sawtooth'; + } + + audioBufferToWav(buffer) { + const numOfChan = buffer.numberOfChannels; + const length = buffer.length * numOfChan * 2 + 44; + const bufferArray = new ArrayBuffer(length); + const view = new DataView(bufferArray); + let offset = 0; + + const writeString = (str) => { + for (let i = 0; i < str.length; i++) { + view.setUint8(offset + i, str.charCodeAt(i)); + } + }; + + // RIFF identifier + writeString('RIFF'); offset += 4; + view.setUint32(offset, 36 + buffer.length * numOfChan * 2, true); offset += 4; + writeString('WAVE'); offset += 4; + writeString('fmt '); offset += 4; + view.setUint32(offset, 16, true); offset += 4; // chunk size + view.setUint16(offset, 1, true); offset += 2; // PCM format + view.setUint16(offset, numOfChan, true); offset += 2; + view.setUint32(offset, buffer.sampleRate, true); offset += 4; + view.setUint32(offset, buffer.sampleRate * numOfChan * 2, true); offset += 4; + view.setUint16(offset, numOfChan * 2, true); offset += 2; + view.setUint16(offset, 16, true); offset += 2; + writeString('data'); offset += 4; + view.setUint32(offset, buffer.length * numOfChan * 2, true); offset += 4; + + const channels = []; + for (let i = 0; i < numOfChan; i++) { + channels.push(buffer.getChannelData(i)); + } + + let sampleIndex = 0; + while (sampleIndex < buffer.length) { + for (let channel = 0; channel < numOfChan; channel++) { + const sample = Math.max(-1, Math.min(1, channels[channel][sampleIndex])); + view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true); + offset += 2; + } + sampleIndex++; + } + + return new Blob([bufferArray], { type: 'audio/wav' }); + } + + updateCameraDirection() { + const direction = new THREE.Vector3( + Math.sin(this.yaw) * Math.cos(this.pitch), + Math.sin(this.pitch), + Math.cos(this.yaw) * Math.cos(this.pitch) + ); + const target = new THREE.Vector3().copy(this.camera.position).add(direction); + this.camera.lookAt(target); + } + + updateMovement(delta) { + const direction = new THREE.Vector3(); + if (this.moveState.forward) direction.z -= 1; + if (this.moveState.back) direction.z += 1; + if (this.moveState.left) direction.x -= 1; + if (this.moveState.right) direction.x += 1; + + if (direction.lengthSq() > 0) { + direction.normalize(); + direction.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.yaw); + this.camera.position.addScaledVector(direction, this.moveSpeed * delta); + } + + if (this.moveState.up) this.camera.position.y += this.moveSpeed * delta; + if (this.moveState.down) this.camera.position.y -= this.moveSpeed * delta; + + const distance = this.camera.position.length(); + const maxDistance = this.sphereRadius * 2.4; + if (distance > maxDistance) { + this.camera.position.setLength(maxDistance); + } + + this.updateCameraDirection(); + } + + animate() { + if (!this.isActive) return; + const delta = this.clock.getDelta(); + this.updateMovement(delta); + + if (this.autoSpin) { + this.stageGroup.rotation.y += this.autoSpinSpeed * delta; + if (!this.isAdjustingRotation && this.rotationSlider) { + const normalizedDeg = THREE.MathUtils.radToDeg(this.stageGroup.rotation.y); + const wrapped = THREE.MathUtils.euclideanModulo(normalizedDeg + 180, 360) - 180; + this.rotationSlider.value = wrapped.toFixed(0); + } + } + + this.renderer.render(this.scene, this.camera); + } + + handleResize() { + if (!this.renderer || !this.camera) return; + const width = this.sceneContainer.clientWidth; + const height = this.sceneContainer.clientHeight; + this.renderer.setSize(width, height); + this.camera.aspect = width / height; + this.camera.updateProjectionMatrix(); + } + + onKeyDown(event) { + if (!this.isActive) return; + switch (event.code) { + case 'KeyW': + case 'ArrowUp': + this.moveState.forward = true; + break; + case 'KeyS': + case 'ArrowDown': + this.moveState.back = true; + break; + case 'KeyA': + case 'ArrowLeft': + this.moveState.left = true; + break; + case 'KeyD': + case 'ArrowRight': + this.moveState.right = true; + break; + case 'Space': + this.moveState.up = true; + event.preventDefault(); + break; + case 'ShiftLeft': + case 'ShiftRight': + this.moveState.down = true; + break; + case 'KeyQ': + this.moveState.up = true; + break; + case 'KeyE': + this.moveState.down = true; + break; + case 'Escape': + this.exit(); + break; + default: + break; + } + } + + onKeyUp(event) { + switch (event.code) { + case 'KeyW': + case 'ArrowUp': + this.moveState.forward = false; + break; + case 'KeyS': + case 'ArrowDown': + this.moveState.back = false; + break; + case 'KeyA': + case 'ArrowLeft': + this.moveState.left = false; + break; + case 'KeyD': + case 'ArrowRight': + this.moveState.right = false; + break; + case 'Space': + this.moveState.up = false; + break; + case 'ShiftLeft': + case 'ShiftRight': + this.moveState.down = false; + break; + case 'KeyQ': + this.moveState.up = false; + break; + case 'KeyE': + this.moveState.down = false; + break; + default: + break; + } + } + + setStatus(message, highlight = false) { + if (!this.statusEl) return; + this.statusEl.textContent = message; + this.statusEl.classList.toggle('status-display--active', highlight); + } +} + +new SpatialComposer(); diff --git a/styles.css b/styles.css index 11fbc3c..f06a2e1 100644 --- a/styles.css +++ b/styles.css @@ -1,401 +1,395 @@ -/* Reset styles */ +/* Reset */ * { margin: 0; padding: 0; box-sizing: border-box; } +:root { + --bg-gradient: radial-gradient(circle at 20% 20%, rgba(34, 40, 64, 0.65), rgba(4, 7, 16, 0.9)), #020308; + --hud-surface: rgba(12, 16, 28, 0.78); + --hud-surface-strong: rgba(20, 26, 42, 0.9); + --hud-border: rgba(255, 173, 90, 0.45); + --hud-border-strong: rgba(255, 200, 140, 0.75); + --accent: #ffb05a; + --accent-strong: #ff8f3f; + --text-primary: #fef5e6; + --text-muted: rgba(254, 245, 230, 0.7); + --shadow-strong: 0 22px 48px rgba(0, 0, 0, 0.55); + --shadow-soft: 0 12px 28px rgba(0, 0, 0, 0.4); + --panel-radius: 18px; + --icon-size: 42px; + font-family: 'Inter', 'Segoe UI', sans-serif; +} + body, html { height: 100%; width: 100%; overflow: hidden; - background-color: #000; - font-family: 'Inter', sans-serif; - color: #f0f0f0; + background: var(--bg-gradient); + color: var(--text-primary); } -/* Immersive container - takes the full screen */ -#immersive-container { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #000; - overflow: hidden; +body { + position: relative; + letter-spacing: 0.01em; } -#scene-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - width: 100%; - height: 100%; +.title-card { + backdrop-filter: blur(14px); + border-radius: 28px; + border: 1px solid rgba(0, 255, 255, 0.35); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55); } -/* Mode Selector */ -.mode-selector { - position: absolute; - top: 30px; - left: 30px; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 10px; - padding: 10px; - z-index: 110; - border: 1px solid #e69138; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.5); +.global-nav { + position: fixed; + top: 24px; + left: 50%; + transform: translateX(-50%); display: flex; - flex-direction: column; - gap: 8px; + gap: 16px; + z-index: 120; + padding: 10px 18px; + border-radius: 999px; + background: rgba(8, 10, 18, 0.68); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(16px); + box-shadow: var(--shadow-soft); +} + +.nav-btn { + padding: 10px 22px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.18); + background: transparent; + color: var(--text-muted); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.25s ease; + text-transform: uppercase; + letter-spacing: 0.08em; } -.mode-label { - font-size: 14px; - color: #e69138; - margin-bottom: 5px; - text-align: center; +.nav-btn--primary { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); + color: #0f0f16; + border-color: transparent; + box-shadow: 0 12px 28px rgba(255, 146, 51, 0.45); } -.mode-options { - display: flex; - gap: 5px; +.nav-btn:hover { + transform: translateY(-2px); + color: var(--text-primary); + border-color: rgba(255, 255, 255, 0.3); } -.mode-btn { - padding: 5px 10px; - background-color: rgba(50, 50, 50, 0.7); - border: 1px solid #777; - border-radius: 5px; - color: #ddd; - cursor: pointer; - font-size: 12px; - /* transition: all 0.2s; */ +body.composer-active #title-container { + opacity: 0; + pointer-events: none; } -.mode-btn.active { - background-color: #e69138; - color: #000; - border-color: #e69138; - box-shadow: 0 0 10px rgba(230, 145, 56, 0.8); +body.composer-active .global-nav { + opacity: 1; + pointer-events: auto; } -/* Mix Mode Toggle */ -.mix-mode-toggle { - position: absolute; - top: 85px; - left: 30px; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 10px; - padding: 10px; - z-index: 110; - border: 1px solid #e69138; - display: flex; - align-items: center; - gap: 10px; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.5); +body.composer-active #enterComposerSpace { + display: none; } -.switch { - position: relative; - display: inline-block; - width: 45px; - height: 24px; +body:not(.composer-active) #exitComposerSpace { + display: none; } -.switch input { - opacity: 0; - width: 0; - height: 0; +#immersive-container { + position: fixed; + inset: 0; + overflow: hidden; + background: transparent; } -.slider { +#scene-container { position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #555; - transition: .4s; + inset: 0; } -.slider:before { +#joystick-container { position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background-color: white; - transition: .4s; -} - -input:checked + .slider { - background-color: #e69138; -} - -input:checked + .slider:before { - transform: translateX(21px); + bottom: 40px; + right: 40px; + width: 130px; + height: 130px; + background: rgba(12, 16, 28, 0.72); + border-radius: 50%; + border: 1px solid var(--hud-border); + box-shadow: var(--shadow-soft); + z-index: 90; } -.slider.round { - border-radius: 24px; +.ui-overlay { + position: absolute; + inset: 0; + padding: 30px 40px; + display: flex; + flex-direction: column; + justify-content: space-between; + pointer-events: none; + z-index: 80; } -.slider.round:before { - border-radius: 50%; +.ui-top, +.ui-bottom { + display: flex; + justify-content: space-between; + gap: 24px; + pointer-events: none; } -/* Scene Selector */ -.scene-selector { - position: absolute; - top: 180px; - left: 30px; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 10px; - padding: 10px; - z-index: 110; - border: 1px solid #e69138; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.5); +.hud-stack { display: flex; flex-direction: column; - gap: 8px; + gap: 18px; + min-width: 210px; + pointer-events: none; } -.scene-btn { - padding: 8px 12px; - background-color: rgba(50, 50, 50, 0.7); - border: 1px solid #777; - border-radius: 5px; - color: #ddd; - cursor: pointer; - font-size: 13px; - transition: all 0.2s; - text-align: left; +.hud-stack > .hud-button { + width: 100%; + justify-content: center; + display: inline-flex; + align-items: center; } -.scene-btn.active { - background-color: #e69138; - color: #000; - border-color: #e69138; - box-shadow: 0 0 10px rgba(230, 145, 56, 0.8); +.hud-stack--right { + align-items: flex-end; } -/* Joystick styles */ -#joystick-container { - position: absolute; - bottom: 290px; - right: 100px; - width: 120px; - height: 120px; - background-color: rgba(255, 255, 255, 0.1); - border-radius: 50%; - z-index: 100; - border: 2px solid rgba(230, 145, 56, 0.7); - box-shadow: 0 0 20px rgba(230, 145, 56, 0.3); +.hud-card { + background: var(--hud-surface); + border-radius: var(--panel-radius); + border: 1px solid var(--hud-border); + box-shadow: var(--shadow-soft); + padding: 18px 20px; + backdrop-filter: blur(18px); + pointer-events: auto; } -/* Rotation indicator */ -.rotation-indicator { - position: absolute; - top: 30px; - right: 30px; - background-color: rgba(0, 0, 0, 0.7); - padding: 10px 15px; - border-radius: 30px; - color: #e69138; - font-weight: bold; +.mode-selector, +.performer-dropdown, +.scene-selector { display: flex; flex-direction: column; - align-items: center; - z-index: 100; - border: 1px solid #e69138; - box-shadow: 0 0 10px rgba(230, 145, 56, 0.4); -} - -.indicator-label { - font-size: 14px; - margin-bottom: 4px; + gap: 12px; } -.indicator-value { - font-size: 20px; +.mode-label, +.scene-label, +.performer-dropdown label, +.control-label { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--text-muted); } -/* Audio controls */ -.audio-controls { - position: absolute; - bottom: 150px; - left: 30px; +.mode-options { display: flex; - gap: 20px; - z-index: 100; + gap: 10px; } -#playPauseButton, #resetButton { - width: 60px; - height: 60px; - border-radius: 50%; - border: none; - cursor: pointer; - background-color: rgba(0, 0, 0, 0.6); - color: #e69138; - font-size: 24px; - display: flex; - justify-content: center; +.mode-btn, +.scene-btn, +.trivia-nav { + display: inline-flex; align-items: center; - transition: all 0.3s ease; - border: 2px solid #e69138; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.3); -} - -/* Replace the current #toggleArabicBtn style with this */ -#toggleArabicBtn { - position: absolute; - top: 180px; - left: 180px; - width: 165px; - height: 40px; - background-color: rgba(51, 51, 51, 0.7); - color: white; - border: 1px solid #e69138; - border-radius: 8px; + justify-content: center; + padding: 10px 14px; + border-radius: 12px; + border: 1px solid transparent; + background: rgba(255, 255, 255, 0.04); + color: var(--text-muted); + font-size: 0.88rem; + font-weight: 600; cursor: pointer; - z-index: 9999; /* Increased to ensure always on top */ - font-size: 14px; - transition: background-color 0.3s ease; - padding: 8px 12px; - box-shadow: 0 0 10px rgba(230, 145, 56, 0.2); + transition: all 0.2s ease; } -#toggleArabicBtn:hover { - background-color: rgba(80, 80, 80, 0.9); +.scene-selector { + gap: 10px; } -#toggleArabicBtn.active { - background-color: #e69138; - color: #000; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.6); +.scene-selector .scene-btn { + justify-content: flex-start; } -#playPauseButton:hover, #resetButton:hover { - transform: scale(1.1); - box-shadow: 0 0 20px rgba(230, 145, 56, 0.6); - background-color: rgba(30, 30, 30, 0.7); +.mode-btn:hover, +.scene-btn:hover, +.mode-btn.active, +.scene-btn.active, +.trivia-nav:hover, +.trivia-nav.active { + color: #0c0d12; + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); + border-color: transparent; + box-shadow: 0 10px 24px rgba(255, 146, 51, 0.32); } -#playPauseButton::before { - content: var(--play-pause-icon, "\25B6"); /* Play triangle */ +.performer-dropdown select { + width: 100%; + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(5, 7, 16, 0.7); + color: var(--text-primary); + font-size: 0.95rem; + transition: border 0.2s ease; } -#resetButton::before { - content: "\21BA"; /* Curved arrow */ +.performer-dropdown select:focus { + outline: none; + border-color: var(--accent); } -/* Add tooltips for the buttons */ -#playPauseButton, #resetButton { - position: relative; +.hud-button { + padding: 12px 18px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(12, 16, 28, 0.72); + color: var(--text-muted); + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + pointer-events: auto; } -#playPauseButton::after, #resetButton::after { - position: absolute; - bottom: -30px; - left: 50%; - transform: translateX(-50%); - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 5px 10px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; - opacity: 0; - transition: opacity 0.3s; - pointer-events: none; +.hud-button.active { + border-color: var(--accent); + color: var(--accent); + box-shadow: 0 8px 18px rgba(255, 146, 51, 0.3); } -#playPauseButton::after { - content: attr(title); /* Use the title attribute */ +.hud-button--primary { + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); + color: #101116; + border: none; + box-shadow: 0 12px 26px rgba(255, 146, 51, 0.38); } -#resetButton::after { - content: attr(title); /* Use the title attribute */ +.hud-button--icon { + width: var(--icon-size); + height: var(--icon-size); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + padding: 0; } -#playPauseButton:hover::after, #resetButton:hover::after { - opacity: 1; +.hud-button:hover { + transform: translateY(-2px); + border-color: rgba(255, 255, 255, 0.24); + color: var(--text-primary); } -/* Sound wave visualization */ -.sound-wave { - position: absolute; - border-radius: 50%; - opacity: 0.7; - background: radial-gradient(circle, rgba(230, 145, 56, 0.5) 0%, rgba(230, 145, 56, 0) 70%); - pointer-events: none; - z-index: 10; +.hud-button--primary:hover { + transform: translateY(-3px); } -/* 2D Top-down visualization */ .topdown-view { - position: absolute; - top: 50px; - right: 30px; - width: 200px; - height: 200px; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 10px; - padding: 10px; - z-index: 100; - border: 1px solid #e69138; - box-shadow: 0 0 20px rgba(230, 145, 56, 0.6); + width: 220px; display: flex; flex-direction: column; + gap: 12px; + padding: 18px; } .view-label { - font-size: 13px; - color: #e69138; - margin-bottom: 5px; - text-align: center; - text-shadow: 0 0 5px rgba(230, 145, 56, 0.8); + font-size: 0.85rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-muted); } #topdown-canvas { - flex: 1; width: 100%; - height: calc(100% - 17px); - background-color: rgba(0, 0, 0, 0.7); - border-radius: 5px; + height: 180px; + border-radius: 14px; + background: rgba(4, 6, 12, 0.8); + border: 1px solid rgba(255, 255, 255, 0.05); } -/* Trivia Button and Container */ -#triviaButton { - position: absolute; - top: 30px; - left: 264px; - width: 40px; - height: 40px; +.audio-controls { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-width: 320px; +} + +.control-actions { + display: flex; + gap: 14px; +} + +#playPauseButton, +#resetButton { + width: 58px; + height: 58px; border-radius: 50%; - border: 2px solid #e69138; - background-color: rgba(0, 0, 0, 0.7); - color: #e69138; - font-size: 20px; - font-weight: bold; + border: 2px solid var(--hud-border); + background: rgba(10, 14, 24, 0.8); + color: var(--accent); + font-size: 22px; display: flex; - justify-content: center; align-items: center; + justify-content: center; cursor: pointer; - z-index: 110; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.5); - font-style: italic; - transition: all 0.2s; + transition: all 0.3s ease; } -#triviaButton:hover { - transform: scale(1.1); - box-shadow: 0 0 20px rgba(230, 145, 56, 0.8); +#playPauseButton:hover, +#resetButton:hover { + transform: scale(1.05); + box-shadow: 0 12px 26px rgba(255, 146, 51, 0.32); +} + +#playPauseButton::before { + content: var(--play-pause-icon, "\25B6"); +} + +#resetButton::before { + content: "\21BA"; +} + +#dryWetControl { + max-width: 320px; + gap: 14px; +} + +.slider-container { + display: flex; + flex-direction: column; + gap: 8px; +} + +.dry-wet-slider { + width: 100%; +} + +.slider-labels { + display: flex; + justify-content: space-between; + font-size: 0.8rem; + color: var(--text-muted); +} + +.control-value { + font-weight: 600; + font-size: 1rem; + color: var(--accent); } .trivia-container { @@ -403,701 +397,509 @@ input:checked + .slider:before { top: 50%; left: 50%; transform: translate(-50%, -50%); - width: 80%; - max-width: 700px; - height: 80%; - max-height: 600px; - background-color: rgba(0, 0, 0, 0.9); - border: 2px solid #e69138; - border-radius: 15px; - z-index: 1000; - box-shadow: 0 0 30px rgba(230, 145, 56, 0.7); + width: min(720px, 90vw); + height: min(620px, 90vh); + background: var(--hud-surface-strong); + border-radius: 24px; + border: 1px solid var(--hud-border-strong); + box-shadow: var(--shadow-strong); display: flex; flex-direction: column; overflow: hidden; + z-index: 200; } -.trivia-header { +.trivia-header, +.trivia-navigation { display: flex; - justify-content: space-between; align-items: center; - padding: 15px 20px; - background-color: rgba(230, 145, 56, 0.2); - border-bottom: 1px solid #e69138; + justify-content: space-between; + gap: 12px; + padding: 20px 24px; + background: rgba(255, 255, 255, 0.04); } .trivia-header h2 { - color: #e69138; - font-size: 24px; + color: var(--accent); + font-size: 1.4rem; + letter-spacing: 0.08em; + text-transform: uppercase; } .close-trivia { background: none; border: none; - color: #e69138; - font-size: 28px; + color: var(--accent); + font-size: 2rem; cursor: pointer; - padding: 0 10px; - transition: all 0.2s; + transition: transform 0.2s ease; } .close-trivia:hover { - transform: scale(1.2); - color: #ff9d45; + transform: scale(1.1); } .trivia-content { flex: 1; + padding: 24px; overflow-y: auto; - padding: 20px; line-height: 1.6; + color: var(--text-primary); } .trivia-content h3 { - color: #e69138; - margin-bottom: 15px; -} - -.trivia-content p { - margin-bottom: 15px; + color: var(--accent); + margin-bottom: 12px; } .trivia-content img { max-width: 100%; - border-radius: 8px; - margin: 15px 0; - box-shadow: 0 0 15px rgba(0, 0, 0, 0.6); + border-radius: 16px; + margin: 18px 0; + box-shadow: var(--shadow-soft); } .trivia-navigation { - display: flex; justify-content: center; - gap: 10px; - padding: 15px; - background-color: rgba(230, 145, 56, 0.1); - border-top: 1px solid #e69138; -} - -.trivia-nav { - padding: 8px 15px; - background-color: rgba(50, 50, 50, 0.7); - border: 1px solid #777; - border-radius: 5px; - color: #e69138; - cursor: pointer; - transition: all 0.2s; -} - -.trivia-nav:hover, .trivia-nav.active { - background-color: #e69138; - color: #000; - border-color: #e69138; -} - -/* Contour View Toggle */ -.toggle-contour-view { - position: absolute; - bottom: 210px; - left: 50%; - transform: translateX(-50%); - display: flex; - background-color: rgba(0, 0, 0, 0.7); - border-radius: 8px; - border: 1px solid #e69138; - overflow: hidden; - z-index: 110; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.5); -} - -.toggle-contour-view button { - padding: 8px 15px; - background-color: transparent; - border: none; - color: #e69138; - cursor: pointer; - font-size: 14px; - transition: all 0.2s; -} - -.toggle-contour-view button.active { - background-color: #e69138; - color: #000; + gap: 16px; } -/* Music Visualization Container */ -.music-visualization-container { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 200px; - background-color: rgba(0, 0, 0, 0.7); - border-top: 1px solid #e69138; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.8); - z-index: 100; -} - -/* Arabic Visualization Container */ #arabic-visualization-container { position: absolute; - bottom: 50px; /* Changed from 0 to 50px */ - left: 50%; /* Center horizontally */ - transform: translateX(-50%); /* Adjust for centering */ - width: 90%; /* Adjust width as needed, or use max-width */ - max-width: 1200px; /* Example max-width, adjust as needed */ - height: 150px; /* Adjusted height from 100px to 200px */ - background-color: rgba(0, 0, 0, 0.3); /* Optional background */ - border: 2px solid #e69138; /* Orange outline */ - border-radius: 20px; /* Corner radius */ - overflow: hidden; /* Hide overflow and constrain playhead visually */ - z-index: 105; /* Ensure it's above other bottom elements if necessary */ + inset: auto 40px 40px auto; + width: clamp(280px, 28vw, 420px); + background: var(--hud-surface); + border-radius: var(--panel-radius); + border: 1px solid var(--hud-border); + box-shadow: var(--shadow-soft); + padding: 18px; + display: flex; + flex-direction: column; + gap: 12px; + z-index: 95; } #arabic-visualization-image { - /* display: block; */ - display:flex; width: 100%; - height: 100%; - object-fit: fill; /* Changed from 'contain' to 'fill' to stretch vertically */ + border-radius: 12px; } #arabic-playhead { - position: absolute; - top: 0; - left: 0; /* Initial position, JS will update based on percentage */ - width: 3px; /* Width of the vertical bar */ - height: 100%; - background-color: rgba(255, 255, 255, 0.7); /* Translucent white */ - box-shadow: 0 0 5px rgba(255, 255, 255, 0.5); - pointer-events: none; /* Prevent interaction */ - z-index: 106; /* Above the image */ -} - -.contour-display, .notation-display { - width: 100%; - height: 100%; - display: none; -} - -.contour-display.active, .notation-display.active { - display: block; -} - -#contour-canvas { - width: 100%; - height: 100%; -} - -.notation-scroll { - width: 100%; - height: 100%; - display: flex; - overflow-x: auto; - overflow-y: hidden; - padding: 10px 0; - align-items: center; -} - -.notation-image { - height: 180px; - margin: 0 10px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.7); + height: 4px; + background: linear-gradient(90deg, var(--accent), var(--accent-strong)); + border-radius: 999px; + width: 0; } -/* Volume Display Panel Styles */ #volume-display-panel { position: absolute; - top: 50%; /* Center vertically */ - left: 50%; /* Center horizontally */ - transform: translate(-50%, -50%); /* Adjust for exact centering */ - width: 500px; /* Adjusted width for 6 horizontal bars */ - height: 300px; - background: rgba(0, 0, 0, 0.85); - color: #fff; - border: 2px solid #e69138; - border-radius: 12px; - padding: 15px; - z-index: 120; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - /* display: none; is handled by JS */ + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--hud-surface-strong); + border-radius: 24px; + border: 1px solid var(--hud-border-strong); + padding: 26px; + width: clamp(320px, 60vw, 680px); + box-shadow: var(--shadow-strong); + display: flex; + flex-direction: column; + gap: 20px; + z-index: 210; } .volume-panel-header { display: flex; - justify-content: space-between; align-items: center; - margin-bottom: 15px; - border-bottom: 1px solid #333; - padding-bottom: 8px; + justify-content: space-between; } .volume-panel-header h3 { - margin: 0; - font-size: 18px; - color: #e69138; - font-weight: bold; + font-size: 1.2rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent); } .close-panel { background: none; border: none; - color: #e69138; - font-size: 24px; + color: var(--accent); + font-size: 1.6rem; cursor: pointer; - transition: all 0.2s ease; -} - -.close-panel:hover { - transform: rotate(90deg); -} - -.performer-dropdown { - position: absolute; - top: 120px; - left: 30px; - background-color: rgba(0, 0, 0, 0.7); - border: 1px solid #e69138; - border-radius: 8px; - padding: 10px; - z-index: 110; - color: #f0f0f0; - display: block; /* Ensure it's visible */ } .volume-bars-container { - display: flex; /* Use flexbox for horizontal layout */ - flex-direction: row; /* Arrange items in a row */ - justify-content: space-around; /* Distribute space around items */ - align-items: flex-start; /* Align items to the top */ - gap: 10px; /* Space between speaker groups */ + display: grid; + grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); + gap: 18px; + justify-items: center; } - - .volume-bar-group { display: flex; flex-direction: column; align-items: center; - gap: 8px; + gap: 12px; } .volume-bar-wrapper { - width: 30px; /* Width of the vertical bar container */ - height: 120px; + width: 38px; + height: 150px; + background: rgba(4, 6, 12, 0.85); + border-radius: 20px; + overflow: hidden; position: relative; + border: 1px solid rgba(255, 255, 255, 0.08); } .volume-bar { - width: 100%; - height: 100%; - background: #2a2a2a; - border-radius: 15px; - overflow: hidden; - position: relative; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); + position: absolute; + inset: 0; + background: rgba(20, 26, 42, 0.6); } .volume-fill { - background: linear-gradient(to top, #e69138, #ffb84d); - width: 100%; - height: 50%; /* Default starting at 50% */ position: absolute; bottom: 0; - border-radius: 0 0 15px 15px; + width: 100%; + background: linear-gradient(180deg, var(--accent), var(--accent-strong)); + height: 50%; transition: height 0.3s ease; } .volume-bar-group[data-active="true"] .volume-fill { - background: linear-gradient(to top, #ff6b35, #ff8c42); - box-shadow: 0 0 15px rgba(255, 107, 53, 0.5); - animation: pulse 1s ease-in-out infinite alternate; -} - -@keyframes pulse { - from { box-shadow: 0 0 10px rgba(255, 107, 53, 0.5); } - to { box-shadow: 0 0 20px rgba(255, 107, 53, 0.8); } + box-shadow: 0 0 20px rgba(255, 146, 51, 0.6); } .volume-percentage { - position: absolute; - bottom: -25px; - left: 50%; - transform: translateX(-50%); - font-size: 12px; - color: #e69138; - font-weight: bold; + font-size: 0.85rem; + color: var(--text-primary); } .speaker-label { - font-size: 13px; - color: #e69138; - font-weight: bold; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-muted); } -/* Volume Display Toggle Button */ -#volumeDisplayBtn { +.sound-wave { position: absolute; - top: 240px; - left: 180px; - padding: 8px 12px; - background: rgba(0, 0, 0, 0.7); - color: #e69138; - border: 1px solid #e69138; - border-radius: 8px; - cursor: pointer; - font-size: 14px; - z-index: 110; - transition: all 0.2s ease; + border-radius: 50%; + opacity: 0.7; + background: radial-gradient(circle, rgba(255, 176, 90, 0.5) 0%, rgba(255, 176, 90, 0) 70%); + pointer-events: none; + z-index: 10; } -#volumeDisplayBtn:hover { - background: rgba(230, 145, 56, 0.2); +#score-panel { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--hud-surface-strong); + border-radius: 24px; + border: 1px solid var(--hud-border-strong); + box-shadow: var(--shadow-strong); + width: min(780px, 92vw); + height: min(640px, 90vh); + display: flex; + flex-direction: column; + z-index: 205; } -#volumeDisplayBtn.active { - background: #e69138; - color: #000; +.score-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 24px; + background: rgba(255, 255, 255, 0.04); } -/* Score Display Toggle Button */ -#scoreDisplayBtn { - position: absolute; - top: 390px; - left: 30px; - /* right: 252px; */ - padding: 8px 12px; - background: rgba(0, 0, 0, 0.7); - color: #e69138; - border: 1px solid #e69138; - border-radius: 8px; - cursor: pointer; - font-size: 14px; - z-index: 110; - transition: all 0.2s ease; +.score-panel-header h3 { + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--accent); } -#scoreDisplayBtn:hover { - background: rgba(230, 145, 56, 0.2); +.score-content { + flex: 1; + padding: 24px; + overflow: auto; } -#scoreDisplayBtn.active { - background: #e69138; - color: #000; +.score-image-container { + width: 100%; + height: 100%; + border-radius: 18px; + overflow: hidden; + background: rgba(4, 6, 12, 0.8); + display: flex; + align-items: center; + justify-content: center; } -/* Mobile optimization for volume panel */ -@media (max-width: 768px) { - #volume-display-panel { - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 90%; /* Use percentage for better responsiveness */ - max-width: 320px; /* Max width on small screens */ - padding: 10px; - } - - .volume-bars-container { - display: flex; - flex-direction: row; - flex-wrap: wrap; /* Allow bars to wrap to the next line */ - justify-content: space-around; - gap: 8px; /* Adjust gap for mobile */ - } - - .volume-bar-group { - /* Adjust basis for wrapping, e.g., fit 3 per row */ - flex-basis: calc(33.33% - 10px); /* Adjust based on gap and desired items per row */ - min-width: 60px; /* Ensure labels and bars are not too squished */ - } +.score-image-container img { + width: 100%; + height: auto; +} - .volume-bar-wrapper { - height: 100px; /* Slightly shorter bars on mobile */ - width: 25px; /* Slightly thinner bars on mobile */ - } - - .speaker-label { - font-size: 11px; /* Smaller font for labels */ - } +/* Composer Space */ +#composer-space { + position: fixed; + inset: 0; + display: none; + background: radial-gradient(circle at 30% 20%, rgba(18, 24, 54, 0.85), rgba(5, 8, 18, 0.95)); + color: var(--text-primary); + overflow: hidden; + z-index: 0; +} - .volume-percentage { - font-size: 10px; /* Smaller font for percentage */ - bottom: -20px; /* Adjust position */ - } +body.composer-active #immersive-container { + display: none; +} - #volumeDisplayBtn { - top: auto; /* Remove fixed top */ - bottom: 20px; /* Position near bottom controls */ - right: 20px; - font-size: 12px; - padding: 6px 10px; - } +body.composer-active #composer-space { + display: block; } -/* Toggle button for minimized 3D view in Engineer Mode */ -.minimized-view-toggle-btn { +#composer-scene { position: absolute; - top: 120px; - left: 30px; - padding: 10px 16px; - background-color: rgba(0, 0, 0, 0.7); - color: #e69138; - border: 1px solid #e69138; - border-radius: 10px; - cursor: pointer; - font-size: 16px; - z-index: 200; - transition: all 0.2s ease; + inset: 0; } -.minimized-view-toggle-btn:hover { - background-color: #e69138; - color: #000; +.composer-overlay { + position: relative; + z-index: 10; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: 40px 60px; + pointer-events: none; } -/* Class to hide the minimized scene container */ -.minimized-view-hidden { - opacity: 0 !important; - visibility: hidden !important; - transition: opacity 0.2s ease, visibility 0.2s ease; +.composer-status-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + background: rgba(12, 16, 28, 0.78); + padding: 20px 28px; + border-radius: var(--panel-radius); + border: 1px solid var(--hud-border); + box-shadow: var(--shadow-soft); + pointer-events: auto; +} + +.composer-status-bar h2 { + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--accent); + font-size: 1.2rem; + margin-bottom: 8px; } -/* Engineer mode: Make 3D view smaller */ -#scene-container.minimized { - position: absolute; - top: 150px; - left: 200px; - /* Adjusted to avoid overlap with score panel (400px + 20px margin + 20px gap = 440px from right) */ - right: 460px; - width: 200px; - height: 200px; - border: 2px solid #e69138; - border-radius: 5px; - z-index: 150; /* Higher z-index to appear above other elements */ - overflow: hidden; /* Add this line to clip the content */ - transition: opacity 0.2s ease, visibility 0.2s ease; /* Smooth transitions for visibility changes */ -} - -/* Engineer mode: Make 2D topdown view expanded */ -.topdown-view.expanded { - width: calc(100% - 40px); /* Adjust for padding */ - height: calc(100% - 40px); /* Adjust for padding */ - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - right: auto; /* Override default right positioning */ +.composer-status-bar p { + color: var(--text-muted); + font-size: 0.95rem; } -/* Dry/Wet Control for Strophe V */ -#dryWetControl { - position: absolute; - top: 240px; - left: 180px; - background: rgba(0, 0, 0, 0.8); - border: 1px solid #e69138; - border-radius: 10px; - padding: 15px; - z-index: 110; - min-width: 200px; - box-shadow: 0 0 15px rgba(230, 145, 56, 0.3); - transition: all 0.3s ease; +.composer-hint { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--text-muted); } -.control-label { - color: #e69138; - font-size: 14px; - font-weight: bold; - margin-bottom: 10px; +.composer-panel { + margin-top: 28px; + background: rgba(10, 12, 24, 0.82); + border-radius: 28px; + border: 1px solid var(--hud-border-strong); + box-shadow: 0 30px 60px rgba(0, 0, 0, 0.55); + padding: 32px 36px 40px; + pointer-events: auto; +} + +.deck-header { + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 0.28em; + color: var(--text-muted); text-align: center; + margin-bottom: 26px; } -.slider-container { - position: relative; - margin-bottom: 8px; +.deck-grid { + display: grid; + grid-template-columns: repeat(3, minmax(200px, 1fr)); + gap: 28px; } -.dry-wet-slider { - width: 100%; - height: 6px; - background: linear-gradient(to right, #4a4a4a 0%, #e69138 100%); - outline: none; - border-radius: 3px; - -webkit-appearance: none; - appearance: none; - cursor: pointer; - transition: background 0.3s ease; +.deck-column { + background: rgba(20, 24, 40, 0.82); + border-radius: 22px; + border: 1px solid rgba(255, 255, 255, 0.06); + padding: 22px 24px 26px; + display: flex; + flex-direction: column; + gap: 18px; } -.dry-wet-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - background: #e69138; - border-radius: 50%; - cursor: pointer; - box-shadow: 0 0 10px rgba(230, 145, 56, 0.5); - transition: all 0.2s ease; +.deck-column h3 { + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--accent); + font-size: 0.95rem; } -.dry-wet-slider::-webkit-slider-thumb:hover { - background: #ff9500; - box-shadow: 0 0 15px rgba(255, 149, 0, 0.8); - transform: scale(1.1); +.coordinate-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + align-items: center; } -.dry-wet-slider::-moz-range-thumb { - width: 20px; - height: 20px; - background: #e69138; - border-radius: 50%; - cursor: pointer; - border: none; - box-shadow: 0 0 10px rgba(230, 145, 56, 0.5); - transition: all 0.2s ease; +.coordinate-grid label { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--text-muted); } -.dry-wet-slider::-moz-range-thumb:hover { - background: #ff9500; - box-shadow: 0 0 15px rgba(255, 149, 0, 0.8); - transform: scale(1.1); +.coordinate-grid input { + width: 100%; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(4, 6, 12, 0.8); + color: var(--text-primary); } -.slider-labels { +.knob-bank, +.transport-bank { display: flex; - justify-content: space-between; - font-size: 12px; - color: #ccc; - margin-top: 5px; + flex-direction: column; + gap: 12px; } -.control-value { - text-align: center; - color: #e69138; - font-size: 13px; - font-weight: bold; -} - -/* Mobile optimization for dry/wet control */ -@media (max-width: 768px) { - #dryWetControl { - top: 150px; - right: 10px; - min-width: 150px; - padding: 10px; - } - - .control-label { - font-size: 12px; - } - - .slider-labels { - font-size: 10px; - } - - .control-value { - font-size: 11px; - } +.knob-bank input[type="range"] { + width: 100%; } -/* Score Panel Styles for Audio Engineer */ -#score-panel { - position: fixed; - top: 20px; - right: 20px; - width: 400px; - height: calc(100vh - 40px); - background: rgba(0, 0, 0, 0.9); - color: #fff; - border: 2px solid #e69138; - border-radius: 12px; - padding: 15px; - z-index: 110; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - backdrop-filter: blur(10px); - transform: translateX(100%); - opacity: 0; - transition: transform 0.3s ease, opacity 0.3s ease; - /* display: none; is handled by JS */ +.status-display { + min-height: 64px; + padding: 14px 16px; + border-radius: 14px; + background: rgba(4, 6, 12, 0.72); + border: 1px solid rgba(255, 255, 255, 0.08); + font-size: 0.9rem; + color: var(--text-muted); } -#score-panel.visible { - transform: translateX(0); - opacity: 1; +.status-display--active { + color: var(--accent); + border-color: rgba(255, 176, 90, 0.45); + background: rgba(20, 26, 42, 0.8); } -.score-panel-header { +.note-list { + list-style: none; display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; - border-bottom: 1px solid #333; - padding-bottom: 8px; -} - -.score-panel-header h3 { - margin: 0; - font-size: 18px; - color: #e69138; - font-weight: bold; -} - -.score-content { - height: calc(100% - 50px); + flex-direction: column; + gap: 10px; + max-height: 160px; overflow-y: auto; - overflow-x: hidden; + padding-right: 6px; } -.score-image-container { +.note-list li { display: flex; - justify-content: center; - align-items: flex-start; - min-height: 100%; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-radius: 12px; + background: rgba(12, 16, 28, 0.85); + border: 1px solid rgba(255, 255, 255, 0.08); + font-size: 0.85rem; } -#score-image { - max-width: 100%; - height: auto; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); - transition: transform 0.3s ease; +.note-list button { + border: none; + background: transparent; + color: var(--accent); + font-weight: 600; + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.1em; } -#score-image:hover { - transform: scale(1.02); -} +/* Responsive */ +@media (max-width: 1100px) { + .ui-overlay { + padding: 24px; + } -/* Custom scrollbar for score panel */ -.score-content::-webkit-scrollbar { - width: 8px; -} + .ui-top { + flex-direction: column; + align-items: stretch; + } -.score-content::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 4px; -} + .hud-stack--right { + align-items: stretch; + } -.score-content::-webkit-scrollbar-thumb { - background: #e69138; - border-radius: 4px; - transition: background 0.3s ease; -} + .ui-bottom { + flex-direction: column; + align-items: stretch; + } + + #joystick-container { + bottom: 28px; + right: 28px; + } + + .topdown-view { + width: 100%; + } -.score-content::-webkit-scrollbar-thumb:hover { - background: #ffb84d; + .global-nav { + top: auto; + bottom: 20px; + } + + .composer-overlay { + padding: 30px; + } + + .deck-grid { + grid-template-columns: 1fr; + } } -/* Mobile optimization for score panel */ -@media (max-width: 768px) { - #score-panel { - top: 10px; - right: 10px; - left: 10px; - width: auto; - height: 50vh; - max-height: 400px; +@media (max-width: 640px) { + .global-nav { + flex-direction: column; + align-items: center; + padding: 14px 18px; } - - .score-panel-header h3 { - font-size: 16px; + + .hud-stack { + min-width: auto; } -} \ No newline at end of file + + .ui-overlay { + padding: 18px; + } + + .composer-overlay { + padding: 20px; + } + + .composer-panel { + padding: 24px; + } +} From e4b928c6837c0a65121053ad9269b55007122fb1 Mon Sep 17 00:00:00 2001 From: James Chang <120231221+jameszjwchang@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:10:01 +0800 Subject: [PATCH 2/2] Refine intro UI and finalize composer workspace --- index.html | 290 +++++++++++++-- js/ComposerSpace.js | 887 +++++++++++++++++++++++++++++++++++++------- script.js | 51 ++- styles.css | 374 ++++++++++++++++++- 4 files changed, 1399 insertions(+), 203 deletions(-) diff --git a/index.html b/index.html index a6292bc..203d5de 100644 --- a/index.html +++ b/index.html @@ -10,23 +10,53 @@
+

Dialogue de l'ombre double

@@ -115,12 +166,6 @@

Dialogue de l'ombre double

-
- - - -
-
Soundstage Map
@@ -131,25 +176,53 @@

Dialogue de l'ombre double

-
-
Transport
-
- - +
+
+
+
+

Audio Engineer Console

+

Monitor phrase energy and live speaker activity.

+
+
+ + + +
+
+
+
+
+ + + +
+
+
+
- -