Files
ClaudeandClaude Sonnet 5 977d348c4f Rename in-app IP references and rewrite README for end users
Apply the ship/vessel and terminology rename mappings throughout the
live app (index.html, css/style.css, js/*.js) and reference docs, per
Ship_IP_Changes.md and Theme_Terminology_IP_Changes.md, including:
- Ship/theme names and generic terminology (Starfleet, LCARS, Warp
  Core, TARDIS, etc.) in both code strings and visible UI text, while
  leaving internal code identifiers (theme keys, CSS classes, preset
  IDs) untouched.
- Made "Species 8675309" canonical and fixed the dotted T.A.R.D.I.X.
  acronym on the Whataverse theme.
- Replaced the per-preset era/pulseShape franchise tag in the preset
  list with the existing safe universe category name, since those
  codes are also used functionally by the audio and observation
  engines and weren't covered by either mapping document.

Also rewrite README.md to lead with end-user ambience usage (how to
run it, universe/vessel overview, mixer channels, sleep timer,
observation mode, hotkeys) with the developer/build notes moved into
a collapsed section.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jt872nc9Fi2WMaJhAKGyDq
2026-09-04 21:16:11 +00:00

674 lines
25 KiB
JavaScript

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 worp 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 {
// Starflight / Classic LCARD
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 TARDIX 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. TARDIX 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 Leviathon Bio-Heart (Moya, Lexx, Species 8675309)
* 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, TARDIX, Freighters)
* - Viewport Window Framing Architecture per Universe (Starflight, Industrial, Station, Whataverse, 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.