class StarshipVisualizer { constructor(audioManager, warpSynth) { this.am = audioManager; this.warpSynth = warpSynth; this.spectrumCanvas = null; this.spectrumCtx = null; this.warpCoreCanvas = null; this.warpCoreCtx = null; this.animationFrameId = null; this.pulseEnergy = 0.2; this.warpParticles = []; this.mode = 'warp-core'; // 'warp-core' or 'time-rotor' this.rotorPhase = 0; this.coreTime = 0; this.coreLoad = 0.6; this.lastFrameTime = null; // Hook into warp core pulse callback if (this.warpSynth) { this.warpSynth.onPulse = (phase, duration) => { this.pulseEnergy = 1.0; this.spawnWarpPulses(); }; } } setMode(mode) { this.mode = mode || 'warp-core'; } init(spectrumCanvasId, warpCoreCanvasId) { this.spectrumCanvas = document.getElementById(spectrumCanvasId); if (this.spectrumCanvas) { this.spectrumCtx = this.spectrumCanvas.getContext('2d'); } this.warpCoreCanvas = document.getElementById(warpCoreCanvasId); if (this.warpCoreCanvas) { this.warpCoreCtx = this.warpCoreCanvas.getContext('2d'); this.initWarpParticles(); } window.addEventListener('resize', () => this.resizeCanvases()); this.resizeCanvases(); this.startRenderLoop(); } resizeCanvases() { if (this.spectrumCanvas) { const rect = this.spectrumCanvas.parentElement.getBoundingClientRect(); this.spectrumCanvas.width = rect.width * window.devicePixelRatio; this.spectrumCanvas.height = (rect.height || 160) * window.devicePixelRatio; this.spectrumCtx.scale(window.devicePixelRatio, window.devicePixelRatio); } if (this.warpCoreCanvas) { const rect = this.warpCoreCanvas.parentElement.getBoundingClientRect(); this.warpCoreCanvas.width = rect.width * window.devicePixelRatio; this.warpCoreCanvas.height = (rect.height || 260) * window.devicePixelRatio; this.warpCoreCtx.scale(window.devicePixelRatio, window.devicePixelRatio); } } initWarpParticles() { this.warpParticles = []; for (let i = 0; i < 36; i++) { this.warpParticles.push({ y: Math.random(), speed: (Math.random() * 0.008 + 0.004) * (Math.random() > 0.5 ? 1 : -1), size: Math.random() * 4 + 2, opacity: Math.random() * 0.7 + 0.3 }); } } spawnWarpPulses() { // The new machines render their own flow; only legacy particle modes need a pool. if (this.mode !== 'warp-core' && this.mode !== 'time-rotor') return; for (let i = 0; i < 6; i++) { this.warpParticles.push({ y: 0.5, // Center matter/antimatter reaction plane speed: (Math.random() * 0.018 + 0.01) * (i % 2 === 0 ? 1 : -1), size: Math.random() * 6 + 3, opacity: 1.0 }); } } startRenderLoop() { this.lastFrameTime = null; const render = (now) => { const elapsed = this.lastFrameTime === null ? 0 : Math.min((now - this.lastFrameTime) / 1000, 0.05); this.lastFrameTime = now; const volume = this.warpSynth && !this.warpSynth.isMuted ? this.warpSynth.params.volume : 0; const targetLoad = Math.max(0, Math.min(1, volume * (0.65 + this.pulseEnergy * 0.35))); this.coreLoad += (targetLoad - this.coreLoad) * (1 - Math.exp(-elapsed * 6)); this.coreTime += elapsed * (this.mode === 'tactical-flywheel' ? 0.6 + this.coreLoad * 0.8 : 1); this.renderSpectrum(); if (window.CoreAnimations && CoreAnimations.has(this.mode)) { if (this.warpCoreCanvas && this.warpCoreCtx) { CoreAnimations.render(this.warpCoreCtx, this.mode, this.warpCoreCanvas.width / window.devicePixelRatio, this.warpCoreCanvas.height / window.devicePixelRatio, this.coreTime, this.coreLoad); } } else { switch (this.mode) { case 'time-rotor': this.renderTimeRotor(); break; case 'industrial-reactor': this.renderIndustrialReactor(); break; case 'bio-heart': this.renderBioHeart(); break; case 'retro-oscilloscope': this.renderRetroOscilloscope(); break; case 'singularity-core': this.renderSingularityCore(); break; case 'warp-core': default: this.renderWarpCore(); break; } } // Decay pulse energy smoothly this.pulseEnergy = Math.max(0.15, this.pulseEnergy * Math.pow(0.94, elapsed * 60)); this.animationFrameId = requestAnimationFrame(render); }; if (this.animationFrameId) cancelAnimationFrame(this.animationFrameId); this.animationFrameId = requestAnimationFrame(render); } renderSpectrum() { if (!this.spectrumCanvas || !this.spectrumCtx || !this.am.analyser) return; const ctx = this.spectrumCtx; const w = this.spectrumCanvas.width / window.devicePixelRatio; const h = this.spectrumCanvas.height / window.devicePixelRatio; const bufferLength = this.am.analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); this.am.analyser.getByteFrequencyData(dataArray); ctx.clearRect(0, 0, w, h); // Dynamic grid color per visualizer mode let gridCol = 'rgba(255, 153, 0, 0.12)'; if (this.mode === 'time-rotor' || this.mode === 'singularity-core') gridCol = 'rgba(0, 229, 255, 0.12)'; else if (this.mode === 'bio-heart') gridCol = 'rgba(16, 185, 129, 0.12)'; else if (this.mode === 'retro-oscilloscope') gridCol = 'rgba(34, 197, 94, 0.15)'; else if (this.mode === 'industrial-reactor' || this.mode === 'compression-furnace' || this.mode === 'station-hub') gridCol = 'rgba(245, 158, 11, 0.15)'; ctx.strokeStyle = gridCol; ctx.lineWidth = 1; for (let y = 20; y < h; y += 30) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } // Draw segmented frequency bars const numBars = 32; const barWidth = (w / numBars) - 3; for (let i = 0; i < numBars; i++) { const binIdx = Math.floor(Math.pow(i / numBars, 1.8) * (bufferLength * 0.6)); const val = dataArray[binIdx] || 0; const barHeight = Math.max(4, (val / 255) * (h - 20)); const x = i * (barWidth + 3); const y = h - barHeight; let col; if (this.mode === 'time-rotor') { if (i < 8) col = '#00e5ff'; else if (i < 20) col = '#38bdf8'; else if (i < 28) col = '#d4af37'; else col = '#ffffff'; } else if (this.mode === 'bio-heart') { if (i < 8) col = '#10b981'; else if (i < 20) col = '#34d399'; else if (i < 28) col = '#a855f7'; else col = '#c084fc'; } else if (this.mode === 'retro-oscilloscope') { col = i < 28 ? '#22c55e' : '#86efac'; } else if (this.mode === 'industrial-reactor' || this.mode === 'compression-furnace' || this.mode === 'station-hub') { if (i < 8) col = '#d97706'; else if (i < 20) col = '#f59e0b'; else if (i < 28) col = '#fbbf24'; else col = '#fef08a'; } else if (this.mode === 'singularity-core') { if (i < 8) col = '#4f46e5'; else if (i < 20) col = '#6366f1'; else if (i < 28) col = '#38bdf8'; else col = '#ffffff'; } else { // Starfleet / Classic LCARS if (i < 8) col = '#ff6600'; else if (i < 20) col = '#ff9933'; else if (i < 28) col = '#cc99cc'; else col = '#99ccff'; } ctx.fillStyle = col; ctx.shadowColor = col; ctx.shadowBlur = val > 120 ? 8 : 0; ctx.fillRect(x, y, barWidth, barHeight); ctx.fillStyle = '#ffffff'; ctx.fillRect(x, y - 2, barWidth, 2); } ctx.shadowBlur = 0; } renderWarpCore() { if (!this.warpCoreCanvas || !this.warpCoreCtx) return; const ctx = this.warpCoreCtx; const w = this.warpCoreCanvas.width / window.devicePixelRatio; const h = this.warpCoreCanvas.height / window.devicePixelRatio; ctx.clearRect(0, 0, w, h); const centerX = w / 2; const chamberWidth = Math.min(70, w * 0.4); // 1. Draw outer intermix chamber housing ctx.fillStyle = '#111625'; ctx.fillRect(centerX - chamberWidth / 2 - 8, 0, chamberWidth + 16, h); // Chamber glass gradient const glassGrad = ctx.createLinearGradient(centerX - chamberWidth / 2, 0, centerX + chamberWidth / 2, 0); glassGrad.addColorStop(0, 'rgba(0, 50, 100, 0.4)'); glassGrad.addColorStop(0.5, 'rgba(0, 180, 255, 0.15)'); glassGrad.addColorStop(1, 'rgba(0, 50, 100, 0.4)'); ctx.fillStyle = glassGrad; ctx.fillRect(centerX - chamberWidth / 2, 0, chamberWidth, h); // 2. Matter / Antimatter injectors (Top and Bottom) ctx.fillStyle = '#ff9900'; ctx.fillRect(centerX - chamberWidth / 2 - 4, 0, chamberWidth + 8, 12); ctx.fillRect(centerX - chamberWidth / 2 - 4, h - 12, chamberWidth + 8, 12); // 3. Central Reaction Intermix Chamber (Center glowing disc) const centerY = h / 2; const glowRadius = 24 + this.pulseEnergy * 28; const coreGlow = ctx.createRadialGradient(centerX, centerY, 2, centerX, centerY, glowRadius); coreGlow.addColorStop(0, '#ffffff'); coreGlow.addColorStop(0.3, `rgba(0, 210, 255, ${0.7 + this.pulseEnergy * 0.3})`); coreGlow.addColorStop(0.7, `rgba(0, 100, 255, ${0.4 + this.pulseEnergy * 0.4})`); coreGlow.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = coreGlow; ctx.beginPath(); ctx.arc(centerX, centerY, glowRadius, 0, Math.PI * 2); ctx.fill(); // 4. Segmented Magnetic Constriction Coils (horizontal pulsing rings) const numCoils = 14; for (let i = 0; i < numCoils; i++) { const coilY = (i / (numCoils - 1)) * (h - 30) + 15; const distFromCenter = Math.abs(coilY - centerY) / (h / 2); const coilIntensity = Math.max(0.2, (1.0 - distFromCenter * 0.6) * (0.4 + this.pulseEnergy * 0.6)); ctx.fillStyle = `rgba(0, 230, 255, ${coilIntensity})`; ctx.shadowColor = '#00e6ff'; ctx.shadowBlur = this.pulseEnergy > 0.6 ? 12 : 3; // Draw coil bar ctx.fillRect(centerX - chamberWidth / 2 + 4, coilY - 2, chamberWidth - 8, 4); } ctx.shadowBlur = 0; // 5. Plasma stream particles for (let i = this.warpParticles.length - 1; i >= 0; i--) { const p = this.warpParticles[i]; p.y += p.speed; if (p.y < 0 || p.y > 1) { if (this.warpParticles.length > 36) { this.warpParticles.splice(i, 1); continue; } else { p.y = p.speed > 0 ? 0 : 1; } } const py = p.y * h; const px = centerX + (Math.sin(p.y * 12) * (chamberWidth * 0.25)); ctx.fillStyle = `rgba(180, 240, 255, ${p.opacity * (0.4 + this.pulseEnergy * 0.6)})`; ctx.beginPath(); ctx.arc(px, py, p.size * (0.8 + this.pulseEnergy * 0.4), 0, Math.PI * 2); ctx.fill(); } } /** * Renders the canonical TARDIS Central Time Rotor * A glass cylinder containing an interior mechanical column physically rising and falling * in sync with the pulse cycle, illuminated with glowing Gallifreyan cyan/emerald light. */ renderTimeRotor() { if (!this.warpCoreCanvas || !this.warpCoreCtx) return; const ctx = this.warpCoreCtx; const w = this.warpCoreCanvas.width / window.devicePixelRatio; const h = this.warpCoreCanvas.height / window.devicePixelRatio; ctx.clearRect(0, 0, w, h); const centerX = w / 2; const columnWidth = Math.min(84, w * 0.45); // 1. TARDIS Console Plinth & Ceiling Collar (Victorian Brass / Gallifreyan Bronze) const collarGrad = ctx.createLinearGradient(centerX - columnWidth / 2, 0, centerX + columnWidth / 2, 0); collarGrad.addColorStop(0, '#593e10'); collarGrad.addColorStop(0.3, '#d4af37'); collarGrad.addColorStop(0.7, '#fef08a'); collarGrad.addColorStop(1, '#593e10'); ctx.fillStyle = collarGrad; ctx.fillRect(centerX - columnWidth / 2 - 8, 0, columnWidth + 16, 14); ctx.fillRect(centerX - columnWidth / 2 - 8, h - 14, columnWidth + 16, 14); // 2. Outer Glass Column Tube const glassGrad = ctx.createLinearGradient(centerX - columnWidth / 2, 0, centerX + columnWidth / 2, 0); glassGrad.addColorStop(0, 'rgba(0, 40, 80, 0.45)'); glassGrad.addColorStop(0.15, 'rgba(0, 229, 255, 0.25)'); glassGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0.12)'); glassGrad.addColorStop(0.85, 'rgba(0, 229, 255, 0.25)'); glassGrad.addColorStop(1, 'rgba(0, 40, 80, 0.45)'); ctx.fillStyle = glassGrad; ctx.fillRect(centerX - columnWidth / 2, 14, columnWidth, h - 28); // Glass edge highlights ctx.strokeStyle = 'rgba(0, 229, 255, 0.6)'; ctx.lineWidth = 1.5; ctx.strokeRect(centerX - columnWidth / 2, 14, columnWidth, h - 28); // 3. Central Bobbing Time Rotor Column // Physical oscillation: rises and falls smoothly this.rotorPhase += 0.038; const maxTravel = (h - 90) * 0.35; const rotorOffset = Math.sin(this.rotorPhase) * maxTravel; const rotorCenterY = (h / 2) + rotorOffset; const rotorHeight = (h - 28) * 0.48; // Moving Inner Rotor Rod & Glass Tubes const innerWidth = columnWidth * 0.58; // Glowing core glow const coreGlow = ctx.createRadialGradient(centerX, rotorCenterY, 4, centerX, rotorCenterY, 36 + this.pulseEnergy * 30); coreGlow.addColorStop(0, '#ffffff'); coreGlow.addColorStop(0.4, `rgba(0, 229, 255, ${0.7 + this.pulseEnergy * 0.3})`); coreGlow.addColorStop(0.8, `rgba(0, 100, 200, ${0.3 + this.pulseEnergy * 0.4})`); coreGlow.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = coreGlow; ctx.beginPath(); ctx.arc(centerX, rotorCenterY, 36 + this.pulseEnergy * 30, 0, Math.PI * 2); ctx.fill(); // Inner mechanical tubes ctx.fillStyle = '#00e5ff'; ctx.shadowColor = '#00e5ff'; ctx.shadowBlur = 10 + this.pulseEnergy * 10; ctx.fillRect(centerX - 4, rotorCenterY - rotorHeight / 2, 8, rotorHeight); // Left and right secondary crystal tubes ctx.fillStyle = 'rgba(180, 240, 255, 0.85)'; ctx.fillRect(centerX - innerWidth / 2 + 2, rotorCenterY - rotorHeight / 2 + 10, 5, rotorHeight - 20); ctx.fillRect(centerX + innerWidth / 2 - 7, rotorCenterY - rotorHeight / 2 + 10, 5, rotorHeight - 20); // Gallifreyan Circular Rotor Rings for (let r = 0; r < 4; r++) { const ringY = rotorCenterY - rotorHeight / 2 + (r * (rotorHeight / 3)); ctx.strokeStyle = '#d4af37'; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(centerX, ringY, innerWidth / 2 + 2, 5, 0, 0, Math.PI * 2); ctx.stroke(); } ctx.shadowBlur = 0; // 4. Sparkling Vortex Time Energy Particles for (let i = this.warpParticles.length - 1; i >= 0; i--) { const p = this.warpParticles[i]; p.y += p.speed * 0.8; if (p.y < 0.05 || p.y > 0.95) { if (this.warpParticles.length > 36) { this.warpParticles.splice(i, 1); continue; } else { p.y = p.speed > 0 ? 0.05 : 0.95; } } const py = p.y * h; const px = centerX + (Math.sin(p.y * 16 + this.rotorPhase) * (columnWidth * 0.32)); ctx.fillStyle = `rgba(0, 229, 255, ${p.opacity * (0.5 + this.pulseEnergy * 0.5)})`; ctx.shadowColor = '#00e5ff'; ctx.shadowBlur = 6; ctx.beginPath(); ctx.arc(px, py, p.size * (0.7 + this.pulseEnergy * 0.5), 0, Math.PI * 2); ctx.fill(); } ctx.shadowBlur = 0; } /** * Industrial Fusion Reactor (Nostromo, Serenity, Rocinante) * Heavy containment walls, incandescent glowing amber plasma core, heat radiating coils */ renderIndustrialReactor() { if (!this.warpCoreCanvas || !this.warpCoreCtx) return; const ctx = this.warpCoreCtx; const w = this.warpCoreCanvas.width / window.devicePixelRatio; const h = this.warpCoreCanvas.height / window.devicePixelRatio; ctx.clearRect(0, 0, w, h); const centerX = w / 2; const centerY = h / 2; const chamberW = Math.min(80, w * 0.42); // Cast iron frame ctx.fillStyle = '#1c150c'; ctx.fillRect(centerX - chamberW / 2 - 10, 0, chamberW + 20, h); // Hazard warning bands at top and bottom for (let x = centerX - chamberW / 2 - 10; x < centerX + chamberW / 2 + 10; x += 12) { ctx.fillStyle = (x % 24 === 0) ? '#d97706' : '#1a1106'; ctx.fillRect(x, 0, 12, 10); ctx.fillRect(x, h - 10, 12, 10); } // Incandescent molten amber core const radius = 22 + this.pulseEnergy * 32; const glow = ctx.createRadialGradient(centerX, centerY, 2, centerX, centerY, radius); glow.addColorStop(0, '#ffffff'); glow.addColorStop(0.2, '#fef08a'); glow.addColorStop(0.5, `rgba(245, 158, 11, ${0.7 + this.pulseEnergy * 0.3})`); glow.addColorStop(1, 'rgba(180, 83, 9, 0)'); ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.fill(); // Heat induction coil clamps for (let i = 0; i < 9; i++) { const cy = 20 + i * ((h - 40) / 8); ctx.fillStyle = (i % 2 === 0) ? '#f59e0b' : '#78350f'; ctx.shadowColor = '#f59e0b'; ctx.shadowBlur = this.pulseEnergy > 0.6 ? 10 : 2; ctx.fillRect(centerX - chamberW / 2, cy - 3, chamberW, 6); } ctx.shadowBlur = 0; } /** * Living Leviathan Bio-Heart (Moya, Lexx, Species 8472) * Pulsing vascular heart sac with bioluminescent emerald/violet energy and neural veins */ renderBioHeart() { if (!this.warpCoreCanvas || !this.warpCoreCtx) return; const ctx = this.warpCoreCtx; const w = this.warpCoreCanvas.width / window.devicePixelRatio; const h = this.warpCoreCanvas.height / window.devicePixelRatio; ctx.clearRect(0, 0, w, h); const centerX = w / 2; const centerY = h / 2; // Organic vascular expansion const bioScale = 1.0 + Math.sin(this.rotorPhase * 1.2) * 0.12 + this.pulseEnergy * 0.18; const baseR = 35 * bioScale; // Outer bioluminescent aura const aura = ctx.createRadialGradient(centerX, centerY, 4, centerX, centerY, baseR * 1.8); aura.addColorStop(0, '#a7f3d0'); aura.addColorStop(0.3, `rgba(16, 185, 129, ${0.7 + this.pulseEnergy * 0.3})`); aura.addColorStop(0.7, `rgba(139, 92, 246, ${0.3 + this.pulseEnergy * 0.3})`); aura.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = aura; ctx.beginPath(); ctx.arc(centerX, centerY, baseR * 1.8, 0, Math.PI * 2); ctx.fill(); // Pulsing neural veins ctx.strokeStyle = '#34d399'; ctx.lineWidth = 2.5; ctx.shadowColor = '#10b981'; ctx.shadowBlur = 8; for (let v = 0; v < 6; v++) { const angle = (v / 6) * Math.PI * 2 + this.rotorPhase * 0.2; ctx.beginPath(); ctx.moveTo(centerX, centerY); const cpX = centerX + Math.cos(angle + 0.5) * (baseR * 0.8); const cpY = centerY + Math.sin(angle + 0.5) * (baseR * 0.8); const endX = centerX + Math.cos(angle) * (baseR * 1.5); const endY = centerY + Math.sin(angle) * (baseR * 1.5); ctx.quadraticCurveTo(cpX, cpY, endX, endY); ctx.stroke(); } ctx.shadowBlur = 0; } /** * Retro Oscilloscope & Analog Astrogator (Jupiter 2, Discovery One) * 1950s/60s green phosphor CRT screen with glowing Lissajous audio wave rings */ renderRetroOscilloscope() { if (!this.warpCoreCanvas || !this.warpCoreCtx) return; const ctx = this.warpCoreCtx; const w = this.warpCoreCanvas.width / window.devicePixelRatio; const h = this.warpCoreCanvas.height / window.devicePixelRatio; ctx.clearRect(0, 0, w, h); const centerX = w / 2; const centerY = h / 2; const crtRadius = Math.min(w, h) * 0.42; // Circular CRT bezel ctx.fillStyle = '#052e16'; ctx.beginPath(); ctx.arc(centerX, centerY, crtRadius, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#22c55e'; ctx.lineWidth = 2; ctx.stroke(); // Crosshairs ctx.strokeStyle = 'rgba(34, 197, 94, 0.25)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(centerX - crtRadius, centerY); ctx.lineTo(centerX + crtRadius, centerY); ctx.moveTo(centerX, centerY - crtRadius); ctx.lineTo(centerX, centerY + crtRadius); ctx.stroke(); // Draw a neutral trace before audio initialization, replacing the previous theme. const analyser = this.am.analyser; const bufferLength = analyser ? analyser.fftSize : 128; const dataArray = new Uint8Array(bufferLength); if (analyser) analyser.getByteTimeDomainData(dataArray); else dataArray.fill(128); ctx.strokeStyle = '#86efac'; ctx.shadowColor = '#22c55e'; ctx.shadowBlur = 8; ctx.lineWidth = 2; ctx.beginPath(); const points = 48; for (let i = 0; i < points; i++) { const idx = Math.floor((i / points) * (bufferLength / 2)); const v = (dataArray[idx] / 128.0) - 1.0; const angle = (i / points) * Math.PI * 2 + this.coreTime; const r = (crtRadius * 0.65) + (v * 28 * (0.8 + this.pulseEnergy)); const x = centerX + Math.cos(angle) * r; const y = centerY + Math.sin(angle) * r; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.closePath(); ctx.stroke(); // A sweep marker makes rotation legible even when the audio trace is silent. const sweepRadius = (crtRadius * 0.65) + ((dataArray[0] / 128.0) - 1.0) * 28 * (0.8 + this.pulseEnergy); ctx.fillStyle = '#d1fae5'; ctx.beginPath(); ctx.arc(centerX + Math.cos(this.coreTime) * sweepRadius, centerY + Math.sin(this.coreTime) * sweepRadius, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } /** * Gravity Singularity Core (Event Horizon, Deep Space) * Black hole event horizon with warping gravitational accretion disk */ renderSingularityCore() { if (!this.warpCoreCanvas || !this.warpCoreCtx) return; const ctx = this.warpCoreCtx; const w = this.warpCoreCanvas.width / window.devicePixelRatio; const h = this.warpCoreCanvas.height / window.devicePixelRatio; ctx.clearRect(0, 0, w, h); const centerX = w / 2; const centerY = h / 2; const diskR = Math.min(w, h) * 0.44; // Glowing gravitational accretion disk ctx.save(); ctx.translate(centerX, centerY); ctx.rotate(this.rotorPhase * 0.6); const grad = ctx.createRadialGradient(0, 0, 12, 0, 0, diskR); grad.addColorStop(0, '#000000'); grad.addColorStop(0.35, '#000000'); grad.addColorStop(0.45, `rgba(99, 102, 241, ${0.8 + this.pulseEnergy * 0.2})`); grad.addColorStop(0.7, `rgba(56, 189, 248, ${0.4 + this.pulseEnergy * 0.3})`); grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.ellipse(0, 0, diskR, diskR * 0.35, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); // Pure black event horizon sphere at center ctx.fillStyle = '#000000'; ctx.strokeStyle = 'rgba(99, 102, 241, 0.8)'; ctx.lineWidth = 2; ctx.shadowColor = '#6366f1'; ctx.shadowBlur = 12 + this.pulseEnergy * 10; ctx.beginPath(); ctx.arc(centerX, centerY, 18, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.shadowBlur = 0; } } window.StarshipVisualizer = StarshipVisualizer; // Helper: Hex color to RGBA function hexToRgba(hex, alpha = 1) { if (!hex || hex.charAt(0) !== '#') return `rgba(56, 189, 248, ${alpha})`; let c = hex.substring(1); if (c.length === 3) c = c.split('').map(x => x + x).join(''); const num = parseInt(c, 16); return `rgba(${(num >> 16) & 255}, ${(num >> 8) & 255}, ${num & 255}, ${alpha})`; } /** * ============================================================================ * CINEMATIC OBSERVATION LOUNGE ENGINE (v9g) * ============================================================================ * Features: * - 60fps DPI-Aware Deep Celestial Canvas (Parallax 3D Starfield & Warp Tunnel) * - Relativistic Warp Flight vs. Orbital Cruise Impulse Modes * - Procedural Celestial Bodies (Class-M Planet with Atmospheric Glow, Time Vortex, Gas Giants) * - Living Traffic & Encounters (Shuttles, Cruisers, Decloaking Klingon BOP, TARDIS, Freighters) * - Viewport Window Framing Architecture per Universe (Starfleet, Industrial, Station, Whoniverse, Military) * - Emergency Alert Synchronization (Red/Yellow Alert Klaxon Strobes & Shield Grids) * - Subspace Audio Harmonics Waveform Sill * - Auto-Hiding Interactive Glass Control Dock */ // ========================================================================= // OBSERVATION CANVAS MANIFEST (v3co) // ========================================================================= // Each universe declares exactly which canvas layers it draws. Default-deny: // anything not listed is OFF. A universe missing from this table gets no canvas at all. // This table is the contract that keeps each theme's OBSERVATION its own experience -- // do not add a layer here to "fill space"; give the theme its own bespoke content instead. // // `starfield` entries may carry a per-universe profile so that two universes drawing stars // are still drawing THEIR OWN stars (density, palette, scale), not one shared layer.