generated from Labyricorn/labyricorn-project-template
Post Step 4 Completion
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* XZBT Control Bus
|
||||
*
|
||||
* XZBTControlBus is a small registry of named, settable console targets
|
||||
* (ranges, toggles and triggers). Anything that wants to drive the console
|
||||
* from outside the DOM — contract adapter, hotkeys, future transports —
|
||||
* talks to the bus rather than reaching into app.js internals.
|
||||
*
|
||||
* MIDI NOTE: Web MIDI control belonged to standalone SciFi-XZBT until
|
||||
* Step 3.5. External MIDI integration will be handled by XZBT-NGN, which
|
||||
* will send canonical contract commands (set / invoke) to this exhibit.
|
||||
* The control bus itself is transport-agnostic and is intentionally kept here.
|
||||
*/
|
||||
|
||||
class XZBTControlBus {
|
||||
constructor() {
|
||||
this.targets = new Map();
|
||||
this.listeners = new Set();
|
||||
}
|
||||
register(id, spec) {
|
||||
this.targets.set(id, { id, ...spec });
|
||||
}
|
||||
set(id, value, source = 'external') {
|
||||
const t = this.targets.get(id);
|
||||
if (!t) return false;
|
||||
let v = value;
|
||||
if (t.type === 'range') {
|
||||
v = Math.max(t.min, Math.min(t.max, Number(value)));
|
||||
if (t.step) v = Math.round(v / t.step) * t.step;
|
||||
}
|
||||
if (t.apply) t.apply(v, source);
|
||||
this.listeners.forEach(fn => fn({ id, value: v, source, target: t }));
|
||||
return true;
|
||||
}
|
||||
trigger(id, source = 'external') {
|
||||
const t = this.targets.get(id);
|
||||
if (!t) return false;
|
||||
if (t.apply) t.apply(true, source);
|
||||
this.listeners.forEach(fn => fn({ id, value: true, source, target: t }));
|
||||
return true;
|
||||
}
|
||||
list() {
|
||||
return [...this.targets.values()];
|
||||
}
|
||||
}
|
||||
window.XZBTControlBus = XZBTControlBus;
|
||||
@@ -0,0 +1,216 @@
|
||||
// Approved settings-panel core animations. Procedural, offline, and dependency-free.
|
||||
(() => {
|
||||
'use strict';
|
||||
const TAU = Math.PI * 2;
|
||||
function line(c, points, color, width = 1) {
|
||||
c.beginPath(); points.forEach(([x, y], i) => i ? c.lineTo(x, y) : c.moveTo(x, y));
|
||||
c.strokeStyle = color; c.lineWidth = width; c.stroke();
|
||||
}
|
||||
function ring(c, x, y, r, color, width = 1, start = 0, end = TAU) {
|
||||
c.beginPath(); c.arc(x, y, r, start, end); c.strokeStyle = color; c.lineWidth = width; c.stroke();
|
||||
}
|
||||
function dot(c, x, y, r, color) {
|
||||
c.fillStyle = color; c.beginPath(); c.arc(x, y, r, 0, TAU); c.fill();
|
||||
}
|
||||
function glow(c, x, y, r, color, alpha = 1) {
|
||||
c.save(); c.globalAlpha = alpha;
|
||||
const g = c.createRadialGradient(x, y, 0, x, y, r);
|
||||
g.addColorStop(0, color); g.addColorStop(1, color + '00');
|
||||
c.fillStyle = g; c.fillRect(x - r, y - r, r * 2, r * 2); c.restore();
|
||||
}
|
||||
function rect(c, x, y, w, h, fill, stroke) {
|
||||
c.fillStyle = fill; c.fillRect(x, y, w, h);
|
||||
if (stroke) { c.strokeStyle = stroke; c.lineWidth = 1; c.strokeRect(x, y, w, h); }
|
||||
}
|
||||
function polygon(c, r, sides, color, rotation = 0, width = 1) {
|
||||
const p = Array.from({ length: sides + 1 }, (_, i) => [Math.cos(i * TAU / sides + rotation) * r, Math.sin(i * TAU / sides + rotation) * r]);
|
||||
line(c, p, color, width);
|
||||
}
|
||||
function base(c) {
|
||||
for (let x = -160; x <= 160; x += 20) for (let y = -120; y <= 120; y += 20) dot(c, x, y, 0.6, '#253545');
|
||||
line(c, [[-158, -93], [-158, -110], [-141, -110]], '#344556');
|
||||
line(c, [[158, 93], [158, 110], [141, 110]], '#344556');
|
||||
}
|
||||
|
||||
const renderers = {
|
||||
starfleet(c, t, load) {
|
||||
const beat = Math.pow((Math.sin(t * 3.4) + 1) / 2, 5);
|
||||
glow(c, 0, 0, 92, '#167eca', 0.17 + load * 0.2);
|
||||
rect(c, -39, -105, 78, 210, '#0b1b2b', '#335770');
|
||||
for (const side of [-1, 1]) {
|
||||
line(c, [[side * 34, -105], [side * 48, -78], [side * 48, 78], [side * 34, 105]], '#6f8d9c', 3);
|
||||
rect(c, -37, side < 0 ? -110 : 102, 74, 8, '#b89976');
|
||||
line(c, [[side * 28, 0], [side * 87, 0], [side * 106, 20], [side * 146, 20]], '#234253', 8);
|
||||
line(c, [[side * 28, 0], [side * 87, 0], [side * 106, 20], [side * 146, 20]], '#80b4c4', 1);
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const travel = (i / 18 + t * 0.55) % 1;
|
||||
const y = side * (100 - travel * 97);
|
||||
const x = Math.sin(travel * 13 + t * 2) * 12;
|
||||
line(c, [[x, y + side * 9], [x, y]], side < 0 ? '#75baff' : '#c8f5ff', 1 + load * 2);
|
||||
}
|
||||
}
|
||||
for (let i = -6; i <= 6; i++) {
|
||||
const wave = Math.pow(Math.max(0, Math.cos(t * 3.4 - Math.abs(i) * 0.62)), 6);
|
||||
c.save(); c.globalAlpha = 0.2 + wave * (0.4 + load * 0.4);
|
||||
line(c, [[-32, i * 15], [-22, i * 15 + 3], [22, i * 15 + 3], [32, i * 15]], '#89eaff', 3); c.restore();
|
||||
}
|
||||
glow(c, 0, 0, 28 + beat * 18 * load, '#81ddff', 0.8);
|
||||
line(c, [[0, -20], [14, 0], [0, 20], [-14, 0], [0, -20]], '#e8fdff', 2);
|
||||
line(c, [[0, -20], [0, 20], [14, 0], [-14, 0]], '#b0eeff');
|
||||
},
|
||||
military(c, t, load) {
|
||||
polygon(c, 104, 6, '#3f4a50', Math.PI / 6, 12);
|
||||
polygon(c, 110, 6, '#a7987e', Math.PI / 6);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const a = i * TAU / 6 + Math.PI / 6;
|
||||
dot(c, Math.cos(a) * 104, Math.sin(a) * 104, 3, '#d6c6ad');
|
||||
c.save(); c.rotate(a);
|
||||
rect(c, 38, -10, 38, 20, '#171f25', '#655e4d');
|
||||
for (let j = 0; j < 5; j++) rect(c, 42 + j * 6, -6, 4, 12, j < 1 + load * 4 && (t * 2 + i) % 6 > j * 0.65 ? '#e8b468' : '#433e30');
|
||||
c.restore();
|
||||
}
|
||||
for (const [r, direction, color] of [[86, 1, '#d3b285'], [31, -1, '#8ea8a6']]) {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const a = i * TAU / 12 + direction * t * 0.65;
|
||||
ring(c, 0, 0, r, color, 4, a, a + 0.3);
|
||||
}
|
||||
}
|
||||
glow(c, 0, 0, 29, '#eab164', 0.25 + load * 0.4);
|
||||
polygon(c, 18, 6, '#f8d09d', 0, 2);
|
||||
line(c, [[-10, 0], [10, 0]], '#e7d8bf', 3);
|
||||
for (const x of [-136, 126]) for (let j = 0; j < 7; j++) rect(c, x, -38 + j * 12, 10, 7, j < load * 7 ? '#c7a16c' : '#263039');
|
||||
},
|
||||
outlaw(c, t, load) {
|
||||
c.save(); c.translate(Math.sin(t * 19) * load * 1.4, Math.sin(t * 23) * load * 1.5);
|
||||
line(c, [[-126, 50], [-93, 50], [-93, -40], [26, -40], [26, -19]], '#a26c4f', 5);
|
||||
line(c, [[-77, 48], [-54, 66], [47, 66], [66, 29]], '#746554', 4);
|
||||
rect(c, -103, -23, 40, 70, '#302d29', '#81705c');
|
||||
rect(c, -74, -38, 100, 85, '#34383b', '#8b8c7b');
|
||||
rect(c, -53, -45, 29, 17, '#674938', '#c28b58');
|
||||
rect(c, -19, -21, 40, 48, '#233c39', '#609a87');
|
||||
for (let i = 0; i < 4; i++) rect(c, -14, -15 + i * 9, 28, 4, '#79b49e');
|
||||
rect(c, 27, -22, 35, 51, '#494139', '#a08c6e');
|
||||
for (let i = 0; i < 4; i++) rect(c, 30 + i * 8, -27, 4, 61, '#80745f');
|
||||
const flicker = 0.8 + Math.sin(t * 17) * 0.12 + Math.sin(t * 29) * 0.08;
|
||||
c.save(); c.translate(68, 3); c.scale((0.2 + load * 0.85) * flicker, 0.4 + load * 0.5);
|
||||
glow(c, 25, 0, 55, '#e88250', 0.5);
|
||||
for (let j = 0; j < 7; j++) {
|
||||
const len = 42 + 35 * Math.sin(t * 13 + j) ** 2;
|
||||
line(c, [[0, (j - 3) * 4], [len * 0.5, (j - 3) * 3], [len, Math.sin(t * 8 + j) * 5]], j % 2 ? '#ffc07c' : '#a3e5e7', 2);
|
||||
}
|
||||
c.restore();
|
||||
dot(c, -67, 2, 27, '#0b1015'); ring(c, -67, 2, 28, '#b0967a', 3);
|
||||
c.save(); c.translate(-67, 2); c.rotate(t * 4);
|
||||
for (let i = 0; i < 5; i++) { c.rotate(TAU / 5); line(c, [[5, 0], [20, -8], [22, 3], [5, 0]], '#a49c87', 3); }
|
||||
c.restore(); dot(c, -67, 2, 6, '#d8ae7a');
|
||||
c.font = '9px Consolas, monospace'; c.fillStyle = '#dcc1a1'; c.fillText('NO. 07', -15, 43);
|
||||
c.restore();
|
||||
},
|
||||
comedy(c, t, load) {
|
||||
c.translate(0, Math.sin(t * 1.2) * 8);
|
||||
glow(c, 0, 0, 100, '#6b49af', 0.17);
|
||||
const colors = ['#d0acff', '#81ded2', '#f0c78e'];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
c.save(); c.rotate(i * Math.PI / 3 + Math.sin(t * 0.5 + i) * 0.3);
|
||||
const r = 74 + load * 15;
|
||||
const squeeze = 0.22 + Math.abs(Math.sin(t * 0.65 + i)) * 0.55;
|
||||
c.scale(1, squeeze); ring(c, 0, 0, r, colors[i], 2);
|
||||
const a = t * (0.7 + i * 0.2) + i * 2;
|
||||
dot(c, Math.cos(a) * r, Math.sin(a) * r, 5, colors[i]); c.restore();
|
||||
}
|
||||
glow(c, 0, 0, 45, '#a686f4', 0.4);
|
||||
const vertices = Array.from({ length: 6 }, (_, i) => {
|
||||
const a = i * TAU / 6 + t * 0.3;
|
||||
const r = 24 + Math.sin(t * 1.7 + i * 2) * (4 + load * 10);
|
||||
return [Math.cos(a) * r, Math.sin(a) * r];
|
||||
});
|
||||
line(c, [...vertices, vertices[0]], '#ece0ff', 2);
|
||||
for (let i = 0; i < 6; i++) { line(c, [vertices[i], vertices[(i + 2) % 6]], '#b6a4d9'); dot(c, ...vertices[i], 2, '#ffffff'); }
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const a = i * 2.4 + t * 0.18;
|
||||
const x = Math.cos(a) * 121, y = Math.sin(a) * 86;
|
||||
line(c, [[x - 3, y], [x + 3, y]], colors[i % 3]); line(c, [[x, y - 3], [x, y + 3]], colors[i % 3]);
|
||||
}
|
||||
},
|
||||
industrial(c, t, load) {
|
||||
const compression = (Math.sin(t * 2) + 1) / 2;
|
||||
rect(c, -70, -93, 140, 186, '#27241e', '#877653');
|
||||
for (const y of [-94, 85]) for (let i = 0; i < 14; i++) rect(c, -70 + i * 10, y, 10, 9, i % 2 ? '#bd943f' : '#30291a');
|
||||
rect(c, -54, -76, 108, 152, '#0d1011', '#5e543d');
|
||||
glow(c, 0, 0, 60 + compression * load * 18, '#ec8c26', 0.6 + load * 0.3);
|
||||
c.save(); c.scale(1 - compression * 0.25, 1 + compression * 0.13);
|
||||
glow(c, 0, 0, 31 + load * 8, '#ffda87'); c.restore();
|
||||
for (const side of [-1, 1]) {
|
||||
const x = side < 0 ? -133 : 78;
|
||||
rect(c, x, -21, 55, 42, '#2e3332', '#6e7770');
|
||||
const reach = 25 + compression * (12 + load * 16);
|
||||
const inner = side * (77 - reach);
|
||||
rect(c, side < 0 ? -78 : inner, -8, reach, 16, '#a6a092', '#d1c6ac');
|
||||
rect(c, inner - (side < 0 ? 0 : 8), -34, 8, 68, '#746447', '#d9b875');
|
||||
}
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const travel = (i / 16 + t * 0.24) % 1;
|
||||
dot(c, Math.sin(i * 8 + travel * 4) * 36, 60 - travel * 125, 0.8 + (i % 3) * 0.5, '#e4ad61');
|
||||
}
|
||||
for (const y of [-65, 59]) for (let i = 0; i < 8; i++) rect(c, -45 + i * 12, y, 6, 6, i < 2 + load * 6 ? '#e4a751' : '#55422b');
|
||||
for (const x of [-63, 63]) for (const y of [-80, 80]) dot(c, x, y, 3, '#ad9b77');
|
||||
},
|
||||
station(c, t, load) {
|
||||
ring(c, 0, 0, 109, '#34545c');
|
||||
for (let i = 0; i < 4; i++) {
|
||||
c.save(); c.rotate(i * Math.PI / 2);
|
||||
line(c, [[-15, -119], [-15, -106], [15, -106], [15, -119]], '#8ba9a9', 3); c.restore();
|
||||
}
|
||||
c.save(); c.rotate(t * 0.24);
|
||||
ring(c, 0, 0, 89, '#263f44', 19); ring(c, 0, 0, 100, '#93bcb8', 2); ring(c, 0, 0, 78, '#537c7e', 2);
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const a = i * TAU / 24;
|
||||
ring(c, 0, 0, 90, i % 4 === 0 ? '#c4d5bd' : '#568d86', 8, a + 0.025, a + 0.16);
|
||||
}
|
||||
for (let i = 0; i < 6; i++) {
|
||||
c.save(); c.rotate(i * TAU / 6);
|
||||
line(c, [[19, -4], [78, -4]], '#5b797c', 2); line(c, [[19, 4], [78, 4]], '#5b797c', 2);
|
||||
for (let j = 0; j < 1 + Math.floor(load * 3); j++) {
|
||||
const travel = (t * 0.55 + j / 3 + i / 6) % 1;
|
||||
dot(c, 22 + travel * 55, 0, 2, '#b4ffe0');
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
c.restore();
|
||||
glow(c, 0, 0, 30, '#73e4c2', 0.3 + load * 0.3);
|
||||
dot(c, 0, 0, 21, '#142d32'); ring(c, 0, 0, 21, '#a3d9cd', 2);
|
||||
line(c, [[-10, -5], [10, -5], [10, 5], [-10, 5], [-10, -5]], '#c5e7dc', 2);
|
||||
for (let i = 0; i < 6; i++) ring(c, 0, 0, 115, '#3b625f', 2, i * TAU / 6 + 0.3, i * TAU / 6 + 0.6);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const modes = {
|
||||
'intermix-cascade': 'starfleet',
|
||||
'tactical-flywheel': 'military',
|
||||
'patchwork-drive': 'outlaw',
|
||||
'improbability-engine': 'comedy',
|
||||
'compression-furnace': 'industrial',
|
||||
'station-hub': 'station'
|
||||
};
|
||||
const CoreAnimations = {
|
||||
has(mode) { return Object.prototype.hasOwnProperty.call(modes, mode); },
|
||||
render(ctx, mode, width, height, time, load) {
|
||||
if (!this.has(mode) || width <= 20 || height <= 42) return;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.save();
|
||||
// Preserve the existing label and the full motion envelope at sidebar size.
|
||||
const labelSpace = 32;
|
||||
const bottomSpace = 10;
|
||||
ctx.translate(width / 2, labelSpace + (height - labelSpace - bottomSpace) / 2);
|
||||
const scale = Math.min((width - 20) / 330, (height - labelSpace - bottomSpace) / 250);
|
||||
ctx.scale(scale, scale);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
base(ctx);
|
||||
renderers[modes[mode]](ctx, time, Math.max(0, Math.min(1, load)));
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
window.CoreAnimations = CoreAnimations;
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Procedural SVG Viewport Frames & Bezels for Observation Mode
|
||||
*/
|
||||
window.ObservationBezels = {
|
||||
getViewportFrameSvg(universeId, presetId, engine) {
|
||||
if (universeId === 'spacestations') {
|
||||
// Space Stations has its own authentic native station viewport in observationStage!
|
||||
return '';
|
||||
}
|
||||
|
||||
if (universeId === 'starfleet') {
|
||||
let era = 'tng';
|
||||
const preset = (typeof StarshipPresets !== 'undefined' && presetId) ? StarshipPresets[presetId] : null;
|
||||
if (preset && preset.era) era = preset.era;
|
||||
|
||||
if (era === 'tos') {
|
||||
// TOS Original Series - hexagonal bridge viewscreen bezel
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="tosMetal" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#2b2118"/>
|
||||
<stop offset="50%" stop-color="#171310"/>
|
||||
<stop offset="100%" stop-color="#0a0806"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V70 L1350,190 H250 L0,70 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V830 L1350,710 H250 L0,830 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H130 L170,710 V190 L130,0 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1470 L1430,710 V190 L1470,0 Z" fill="url(#tosMetal)" stroke="#f59e0b" stroke-width="3"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<rect x="530" y="195" width="30" height="515" fill="#171310" stroke="#dc2626" stroke-width="2.5" opacity="0.9"/>
|
||||
<rect x="1040" y="195" width="30" height="515" fill="#171310" stroke="#dc2626" stroke-width="2.5" opacity="0.9"/>
|
||||
` : ''}
|
||||
|
||||
<text x="800" y="865" text-anchor="middle" fill="#f59e0b" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">U.S.S. INTERPRISE NCC-1701.5 // BRIDGE VIEWSCREEN</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'voyager') {
|
||||
// Wayfarer Intrepid Class - sleeker rounded modern arch, cyan accent
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="voyMetal" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#0f2027"/>
|
||||
<stop offset="50%" stop-color="#0a1418"/>
|
||||
<stop offset="100%" stop-color="#050a0c"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V70 Q800,130 0,70 Z" fill="url(#voyMetal)" stroke="#22d3ee" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V830 Q800,770 0,830 Z" fill="url(#voyMetal)" stroke="#22d3ee" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H80 Q110,450 80,0 Z" fill="url(#voyMetal)"/>
|
||||
<path d="M1600,0 V900 H1520 Q1490,450 1520,0 Z" fill="url(#voyMetal)"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<path d="M525,90 L535,800 L515,800 L505,90 Z" fill="#0a1418" stroke="#22d3ee" stroke-width="2" opacity="0.9"/>
|
||||
<path d="M1075,90 L1065,800 L1085,800 L1095,90 Z" fill="#0a1418" stroke="#22d3ee" stroke-width="2" opacity="0.9"/>
|
||||
` : ''}
|
||||
|
||||
<g transform="translate(500, 838)">
|
||||
<rect x="0" y="0" width="600" height="20" rx="10" fill="#22d3ee" opacity="0.85"/>
|
||||
<text x="300" y="15" text-anchor="middle" fill="#000" font-family="'Antonio', sans-serif" font-weight="700" font-size="12" letter-spacing="3">USS WAYFARER // BRIDGE VIEWPORT</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'ds9') {
|
||||
// Defiance Class / DS9 Ops - angular, aggressive warship framing
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="defMetal" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1c1410"/>
|
||||
<stop offset="50%" stop-color="#0c0806"/>
|
||||
<stop offset="100%" stop-color="#1c1410"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V60 L1300,170 H300 L0,60 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V840 L1300,730 H300 L0,840 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H150 L190,730 V170 L150,0 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1450 L1410,730 V170 L1450,0 Z" fill="url(#defMetal)" stroke="#f97316" stroke-width="3"/>
|
||||
<text x="800" y="865" text-anchor="middle" fill="#f97316" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">USS DEFIANCE // TACTICAL BRIDGE VIEWPORT</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (era === 'nx') {
|
||||
// Interprise NS-01 - early, industrial, submarine-like bulkhead
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="nxMetal" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1c1917"/>
|
||||
<stop offset="50%" stop-color="#0c0a09"/>
|
||||
<stop offset="100%" stop-color="#1c1917"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V110 L1420,175 H180 L0,110 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V790 L1420,725 H180 L0,790 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H120 L150,725 V175 L120,0 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1480 L1450,725 V175 L1480,0 Z" fill="url(#nxMetal)" stroke="#a16207" stroke-width="3"/>
|
||||
<text x="800" y="865" text-anchor="middle" fill="#a16207" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">INTERPRISE NS-01 // COMMAND BRIDGE VIEWPORT</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
// era === 'tng' (or unrecognized) falls through to the default Ten Forward arch below.
|
||||
}
|
||||
|
||||
if (universeId === 'industrial' || universeId === 'outlaw') {
|
||||
// Heavy Industrial Reinforced Bulkhead & Riveted Blast Shutter Framing
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="indMetal" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#1c1917"/>
|
||||
<stop offset="50%" stop-color="#0c0a09"/>
|
||||
<stop offset="100%" stop-color="#1c1917"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Outer Reinforced Rim -->
|
||||
<path d="M0,0 H1600 V100 L1460,160 H140 L0,100 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V800 L1460,740 H140 L0,800 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H110 L140,740 V160 L110,0 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1490 L1460,740 V160 L1490,0 Z" fill="url(#indMetal)" stroke="#b45309" stroke-width="3"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<!-- Vertical Heavy Structural Mullions -->
|
||||
<rect x="530" y="160" width="34" height="580" fill="#292524" stroke="#78350f" stroke-width="3"/>
|
||||
<rect x="1036" y="160" width="34" height="580" fill="#292524" stroke="#78350f" stroke-width="3"/>
|
||||
` : ''}
|
||||
|
||||
<!-- Hazard Stripes on Lower Sill -->
|
||||
<g opacity="0.6">
|
||||
<rect x="220" y="755" width="1160" height="14" fill="#f59e0b"/>
|
||||
<path d="M230,755 L250,769 M270,755 L290,769 M310,755 L330,769 M350,755 L370,769 M390,755 L410,769 M430,755 L450,769 M470,755 L490,769 M510,755 L530,769" stroke="#000" stroke-width="6"/>
|
||||
</g>
|
||||
<text x="800" y="865" text-anchor="middle" fill="#d97706" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="4">HEAVY INDUSTRIAL VIEWPORT // DECK 04 CARGO GANTRY</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (universeId === 'whataverse') {
|
||||
// Gallifreyan Observatory Roundel Viewing Portal / TARDIX Frame
|
||||
// Console era (classic/revival/modern) tints the rings and label.
|
||||
let era = 'revival';
|
||||
const wUniverse = (typeof UniverseRegistry !== 'undefined') ? UniverseRegistry['whataverse'] : null;
|
||||
const preset = (wUniverse && wUniverse.presets && presetId) ? wUniverse.presets[presetId] : null;
|
||||
if (preset && preset.era) era = preset.era;
|
||||
|
||||
let ringA = '#d4af37', ringB = '#00e5ff', dash = '16 12', labelColor = '#d4af37';
|
||||
let label = 'T.A.R.D.I.X. TEMPORAL VORTEX OBSERVATION PORTAL';
|
||||
if (era === 'classic') {
|
||||
ringA = '#b45309'; ringB = '#f5deb3'; dash = '22 10'; labelColor = '#f5deb3';
|
||||
label = 'TYPE 4D CLASSIC CONSOLE // TEMPORAL VIEWPORT';
|
||||
} else if (era === 'modern') {
|
||||
ringA = '#e2e8f0'; ringB = '#94a3b8'; dash = '4 6'; labelColor = '#e2e8f0';
|
||||
label = 'INFINITE WHITE // TEMPORAL OBSERVATION PORTAL';
|
||||
}
|
||||
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Circular Astrolabe Arch Frame -->
|
||||
<path d="M0,0 H1600 V900 H0 Z M800,450 m-680,0 a680,410 0 1,0 1360,0 a680,410 0 1,0 -1360,0" fill="#030712" fill-rule="evenodd" stroke="${ringB}" stroke-width="4"/>
|
||||
<!-- Gallifreyan Circular Inscriptions -->
|
||||
<ellipse cx="800" cy="450" rx="690" ry="420" fill="none" stroke="${ringA}" stroke-width="3" stroke-dasharray="${dash}"/>
|
||||
<ellipse cx="800" cy="450" rx="715" ry="440" fill="none" stroke="${ringB}" stroke-width="1.5" opacity="0.6"/>
|
||||
<text x="800" y="875" text-anchor="middle" fill="${labelColor}" font-family="'Share Tech Mono', monospace" font-size="17" letter-spacing="4">${label}</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (universeId === 'deepspace') {
|
||||
// Terok Nor / DS9 Arched Station Viewport overlooking Docking Pylons
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0,0 H1600 V120 Q800,190 0,120 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V780 Q800,720 0,780 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H140 L160,780 Q180,450 160,120 L140,0 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
<path d="M1600,0 V900 H1460 L1440,780 Q1420,450 1440,120 L1460,0 Z" fill="#0d1117" stroke="#38bdf8" stroke-width="3"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<!-- Exterior Docking Ring Structure visible outside -->
|
||||
<path d="M180,520 L420,410 L440,430 L180,560 Z" fill="#1e293b" opacity="0.6" stroke="#38bdf8" stroke-width="2"/>
|
||||
<path d="M1420,520 L1180,410 L1160,430 L1420,560 Z" fill="#1e293b" opacity="0.6" stroke="#38bdf8" stroke-width="2"/>
|
||||
` : ''}
|
||||
|
||||
<text x="800" y="860" text-anchor="middle" fill="#38bdf8" font-family="'Share Tech Mono', monospace" font-size="16" letter-spacing="3">DEEP SPACE STATION // PROMENADE OBSERVATION DECK</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
if (universeId === 'bioships' || universeId === 'retrofuture' || universeId === 'military' || universeId === 'comedy') {
|
||||
// These four universes previously had no case here at all and silently fell through
|
||||
// to the Starflight default below, which hardcodes "USS INTERPRISE-G" into the plaque --
|
||||
// showing the wrong ship name in every one of these themes' Observation view.
|
||||
// Fix: reuse the same bulkhead shape (it already themes correctly via the CSS
|
||||
// accent variables set per universe), but pull the REAL ship/deck name for the
|
||||
// plaque text from this universe's own preset data, same source the header uses.
|
||||
const universe = UniverseRegistry[universeId];
|
||||
const preset = (universe && universe.presets && presetId) ? universe.presets[presetId] : null;
|
||||
const plaqueText = preset
|
||||
? `${preset.name.toUpperCase()} // OBSERVATION DECK`
|
||||
: `${universe ? universe.name : universeId.toUpperCase()} // OBSERVATION DECK`;
|
||||
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="genBulkhead" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1e222d"/>
|
||||
<stop offset="50%" stop-color="#10141e"/>
|
||||
<stop offset="100%" stop-color="#0a0d14"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0,0 H1600 V80 Q800,150 0,80 Z" fill="url(#genBulkhead)" stroke="var(--primary-accent, #ff9900)" stroke-width="3"/>
|
||||
<path d="M0,900 H1600 V810 Q800,740 0,810 Z" fill="url(#genBulkhead)" stroke="var(--secondary-accent, #cc99cc)" stroke-width="3"/>
|
||||
<path d="M0,0 V900 H90 Q120,450 90,0 Z" fill="url(#genBulkhead)"/>
|
||||
<path d="M1600,0 V900 H1510 Q1480,450 1510,0 Z" fill="url(#genBulkhead)"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<path d="M510,95 L535,95 L545,765 L500,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
<path d="M1090,95 L1065,95 L1055,765 L1100,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
` : ''}
|
||||
|
||||
<g transform="translate(480, 830)">
|
||||
<rect x="0" y="0" width="640" height="24" rx="12" fill="var(--primary-accent, #ff9900)" opacity="0.85"/>
|
||||
<text x="320" y="17" text-anchor="middle" fill="#000" font-family="'Antonio', sans-serif" font-weight="700" font-size="14" letter-spacing="3">${plaqueText}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
// Default: Iconic Starflight Ten Forward / Galaxy-Class Observation Lounge
|
||||
return `
|
||||
<svg viewBox="0 0 1600 900" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="sfBulkhead" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1e222d"/>
|
||||
<stop offset="50%" stop-color="#10141e"/>
|
||||
<stop offset="100%" stop-color="#0a0d14"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Top Arched Bulkhead Canopy -->
|
||||
<path d="M0,0 H1600 V80 Q800,150 0,80 Z" fill="url(#sfBulkhead)" stroke="var(--primary-accent, #ff9900)" stroke-width="3"/>
|
||||
|
||||
<!-- Bottom Curved Padded Console Sill -->
|
||||
<path d="M0,900 H1600 V810 Q800,740 0,810 Z" fill="url(#sfBulkhead)" stroke="var(--secondary-accent, #cc99cc)" stroke-width="3"/>
|
||||
|
||||
<!-- Side Bulkheads -->
|
||||
<path d="M0,0 V900 H90 Q120,450 90,0 Z" fill="url(#sfBulkhead)"/>
|
||||
<path d="M1600,0 V900 H1510 Q1480,450 1510,0 Z" fill="url(#sfBulkhead)"/>
|
||||
|
||||
${(engine ? engine.showPillars : false) ? `
|
||||
<!-- Vertical Architectural Tapered Mullions dividing into 3 bays -->
|
||||
<path d="M510,95 L535,95 L545,765 L500,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
<path d="M1090,95 L1065,95 L1055,765 L1100,765 Z" fill="#151922" stroke="var(--tertiary-accent, #99ccff)" stroke-width="2.5" opacity="0.95"/>
|
||||
` : ''}
|
||||
|
||||
<!-- Authentic LCARD Display Bar along lower sill -->
|
||||
<g transform="translate(480, 830)">
|
||||
<rect x="0" y="0" width="640" height="24" rx="12" fill="var(--primary-accent, #ff9900)" opacity="0.85"/>
|
||||
<text x="320" y="17" text-anchor="middle" fill="#000" font-family="'Antonio', sans-serif" font-weight="700" font-size="14" letter-spacing="3">USS INTERPRISE-G // OBSERVATION LOUNGE // DECK 10 FORWARD</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,673 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user