Add distinct theme core animations and fix retro redraw

This commit is contained in:
Claude
2026-09-03 23:54:27 -07:00
parent 97b0faa4a6
commit 198081706c
7 changed files with 680 additions and 46 deletions
+346
View File
@@ -0,0 +1,346 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Core Studies — Six Distinct Machines</title>
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #080d14; color: #e5edf5; }
* { box-sizing: border-box; }
body { margin: 0; }
main { max-width: 1440px; margin: auto; padding: 38px 28px 28px; }
.eyebrow, .number, .tag, .readout, footer { font-family: Consolas, monospace; }
.eyebrow { color: #8ed5de; font-size: 11px; letter-spacing: .2em; text-transform: uppercase; }
header { display: flex; justify-content: space-between; align-items: end; gap: 28px; margin-bottom: 26px; }
h1 { font-size: clamp(30px, 4vw, 48px); font-weight: 550; letter-spacing: -.045em; margin: 10px 0; }
header p { color: #9aaabb; max-width: 660px; line-height: 1.65; margin: 0; font-size: 14px; }
.stamp { border-left: 2px solid #80ccd3; padding: 8px 0 8px 18px; color: #b7c7d4; white-space: nowrap; line-height: 1.8; font-size: 12px; }
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 18px 28px; padding: 17px 20px; border: 1px solid #293645; border-radius: 8px; background: #101923; margin-bottom: 26px; }
button, select { font: inherit; font-size: 12px; border: 1px solid #435369; border-radius: 5px; background: #192636; color: #e5edf5; padding: 9px 13px; cursor: pointer; }
button:hover { background: #2b4054; }
button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid #bdefff; outline-offset: 4px; }
label { display: flex; align-items: center; gap: 12px; font-size: 12px; color: #b5c3d2; }
input { width: 140px; accent-color: #8bdde4; }
output { min-width: 35px; color: #e1f5fa; font-family: Consolas, monospace; }
.state { margin-left: auto; color: #8ed5de; font: 11px Consolas, monospace; }
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; }
article { --accent: #8bdde4; background: #0e1621; border: 1px solid #283341; border-radius: 10px; overflow: hidden; }
.card-head { padding: 18px 20px 13px; border-top: 2px solid var(--accent); }
.number { font-size: 10px; color: var(--accent); letter-spacing: .11em; }
h2 { font-size: 18px; font-weight: 550; margin: 7px 0 5px; }
.tag { font-size: 10px; color: #8b9db0; }
/* Match the 300px sidebar minus panel padding and borders in style.css. */
.viewport { position: relative; width: 266px; max-width: calc(100% - 32px); height: 240px; margin: 16px auto; background: #080b10; border: 1px solid #34414b; border-radius: 6px; overflow: hidden; }
article:nth-child(2) .viewport, article:nth-child(5) .viewport { width: 263px; }
canvas { width: 100%; height: 100%; display: block; }
.readout { position: absolute; top: 6px; left: 8px; right: 8px; color: var(--accent); font: 11.2px Consolas, monospace; letter-spacing: .1em; pointer-events: none; }
.readout span:first-child { display: inline-block; padding: 2px 6px; border-radius: 3px; background: rgba(0, 0, 0, .6); }
.readout span:last-child { display: none; }
.size-note { margin: -6px 0 14px; text-align: center; color: #8b9db0; font: 10px Consolas, monospace; }
.copy { padding: 17px 20px 20px; }
h3 { font-size: 13px; margin: 0 0 8px; font-weight: 600; }
.copy p { font-size: 12px; line-height: 1.65; color: #a7b6c6; margin: 0; }
.change { color: var(--accent); font: 10px Consolas, monospace; margin-top: 14px; display: block; }
footer { display: flex; justify-content: space-between; gap: 20px; margin-top: 24px; color: #8d9cad; font-size: 10px; line-height: 1.7; }
@media (max-width: 1100px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .stamp { display: none; } }
@media (max-width: 680px) { main { padding: 24px 15px; } .grid { grid-template-columns: 1fr; } .toolbar { gap: 16px; } .state { margin-left: 0; } footer { display: block; } }
</style>
</head>
<body>
<main>
<header>
<div><div class="eyebrow">SciFi-XZBT / Motion design laboratory / 01</div>
<h1>Six themes. Six machines.</h1>
<p>New core concepts shown at the existing settings-panel dimensions. Each animation fits inside the current 240px-high frame, with clear space for the overlay label.</p>
</div>
<div class="stamp">06 ANIMATED STUDIES<br>Procedural · Silent · Offline</div>
</header>
<div class="toolbar" aria-label="Animation controls">
<button id="pause" type="button">Pause motion</button>
<label for="speed">Speed <select id="speed"><option value="0.5">0.5×</option><option value="1" selected>1×</option><option value="2">2×</option></select></label>
<label for="load">Core load <input id="load" type="range" min="0" max="100" value="60"><output id="load-value" for="load">60%</output></label>
<span class="tag">ACTUAL PANEL SIZE · 266 × 240px / 263 × 240px</span>
<span class="state" id="state" role="status">RUNNING / SIMULATED LOAD</span>
</div>
<section class="grid" aria-label="Core animation concepts">
<article style="--accent:#8bddff">
<div class="card-head"><div class="number">01 / STARFLEET COMMAND</div><h2>Intermix cascade</h2><div class="tag">CURRENTLY SHARES: WARP-CORE</div></div>
<div class="viewport"><canvas id="starfleet" role="img" aria-label="Twin plasma streams converge on a central crystal, sending sequential light through containment coils."></canvas><div class="readout"><span>M / A INJECTION</span><span>FIELD BALANCED</span></div></div>
<div class="copy"><h3>Order. Symmetry. Controlled energy.</h3><p>Opposing plasma streams meet inside a suspended crystal. Containment bands illuminate outward in sequence, then send energy through the side conduits.</p><span class="change">LOAD → BRIGHTER FLOW + STRONGER CASCADE</span></div>
</article>
<article style="--accent:#e7b975">
<div class="card-head"><div class="number">02 / MILITARY SPACE</div><h2>Tactical flywheel</h2><div class="tag">CURRENTLY SHARES: WARP-CORE</div></div>
<div class="viewport"><canvas id="military" role="img" aria-label="An armored hexagonal chamber contains counter-rotating segmented flywheels and sequential capacitor cells."></canvas><div class="readout"><span>CAPACITOR BANK</span><span>COMBAT READY</span></div></div>
<div class="copy"><h3>Armored mass. Stored power.</h3><p>Two segmented flywheels counter-rotate inside a bolted hexagonal cage. Six capacitor banks charge in a deliberate sequence around the central power bus.</p><span class="change">LOAD → FASTER ROTORS + MORE CHARGED CELLS</span></div>
</article>
<article style="--accent:#efa788">
<div class="card-head"><div class="number">03 / OUTLAW / FRONTIER</div><h2>Patchwork impulse drive</h2><div class="tag">CURRENTLY SHARES: WARP-CORE</div></div>
<div class="viewport"><canvas id="outlaw" role="img" aria-label="A horizontal salvaged turbine shakes unevenly, with exposed cables, a spinning fan, and a flickering exhaust jet."></canvas><div class="readout"><span>BYPASS ENGAGED</span><span>STILL FLYING</span></div></div>
<div class="copy"><h3>Asymmetry. Improvisation. Character.</h3><p>A salvaged horizontal drive shudders on mismatched mounts. An exposed fan spins beside copper bypass lines, while an uneven exhaust plume trails from its nozzle.</p><span class="change">LOAD → LONGER EXHAUST + HEAVIER VIBRATION</span></div>
</article>
<article style="--accent:#d0acff">
<div class="card-head"><div class="number">04 / COMEDY / ADVENTURE</div><h2>Improbability engine</h2><div class="tag">CURRENTLY SHARES: WARP-CORE</div></div>
<div class="viewport"><canvas id="comedy" role="img" aria-label="Pastel orbital hoops fold around a morphing polyhedron, with small satellites moving on impossible intersecting paths."></canvas><div class="readout"><span>PROBABILITY: OPTIONAL</span><span>MOSTLY HARMLESS</span></div></div>
<div class="copy"><h3>Playful geometry. Impossible physics.</h3><p>Three luminous hoops fold through each other around a shape-changing crystal. Satellites wander across tilted orbits as the whole assembly gently bobs in place.</p><span class="change">LOAD → WIDER ORBITS + MORE SHAPE DISTORTION</span></div>
</article>
<article style="--accent:#f3be62">
<div class="card-head"><div class="number">05 / INDUSTRIAL SPACE</div><h2>Compression furnace</h2><div class="tag">CURRENTLY SHARES: INDUSTRIAL-REACTOR</div></div>
<div class="viewport"><canvas id="industrial" role="img" aria-label="Opposed hydraulic pistons compress an amber plasma furnace with hazard bands, heat vents, and rising embers."></canvas><div class="readout"><span>FUSION PRESSURE</span><span>CONTAINMENT SEALED</span></div></div>
<div class="copy"><h3>Weight. Pressure. Physical work.</h3><p>Opposed hydraulic pistons compress a caged furnace in a slow mechanical cycle. The central plasma flares under pressure, lighting heat vents while embers rise.</p><span class="change">LOAD → DEEPER COMPRESSION + HOTTER FURNACE</span></div>
</article>
<article style="--accent:#91e4ce">
<div class="card-head"><div class="number">06 / SPACE STATIONS</div><h2>Centrifugal distribution hub</h2><div class="tag">CURRENTLY SHARES: INDUSTRIAL-REACTOR</div></div>
<div class="viewport"><canvas id="station" role="img" aria-label="A six-spoke station rotor turns around a fixed hub, delivering traveling energy packets to a ring of habitat modules."></canvas><div class="readout"><span>HABITAT POWER GRID</span><span>GRAVITY STABLE</span></div></div>
<div class="copy"><h3>Continuous rotation. Shared infrastructure.</h3><p>A broad habitat rotor turns around a fixed service hub. Energy packets travel along six spokes to the outer modules while docking brackets remain stationary.</p><span class="change">LOAD → DENSER DISTRIBUTION + BRIGHTER MODULES</span></div>
</article>
</section>
<footer><span>STUDY SCOPE / Six themes from the two shared-renderer groups.<br>Whoniverse, Bioships, Retro Future, and Deep Space already have dedicated renderers.</span><span>Standalone concepts · No audio connection<br>Open this file directly in a browser. All assets are inline.</span></footer>
</main>
<script>
(() => {
'use strict';
const TAU = Math.PI * 2;
const controls = { speed: 1, load: 0.6, paused: matchMedia('(prefers-reduced-motion: reduce)').matches };
const pauseButton = document.getElementById('pause');
const status = document.getElementById('state');
let time = 0;
let previous = null;
function updatePause() {
pauseButton.textContent = controls.paused ? 'Resume motion' : 'Pause motion';
pauseButton.setAttribute('aria-pressed', String(controls.paused));
status.textContent = (controls.paused ? 'PAUSED' : 'RUNNING') + ' / SIMULATED LOAD';
}
pauseButton.addEventListener('click', () => { controls.paused = !controls.paused; updatePause(); });
document.getElementById('speed').addEventListener('change', event => { controls.speed = Number(event.target.value); });
document.getElementById('load').addEventListener('input', event => {
controls.load = Number(event.target.value) / 100;
document.getElementById('load-value').textContent = event.target.value + '%';
});
document.querySelectorAll('.viewport').forEach((viewport, index) => {
const note = document.createElement('div');
note.className = 'size-note';
note.textContent = (index === 1 || index === 4 ? '263' : '266') + ' × 240 px · existing core frame';
viewport.after(note);
});
updatePause();
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 views = Object.entries(renderers).map(([id, draw]) => {
const canvas = document.getElementById(id);
return { canvas, draw, context: canvas.getContext('2d'), width: 0, height: 0 };
});
const resize = new ResizeObserver(entries => {
for (const entry of entries) {
const view = views.find(item => item.canvas === entry.target);
view.width = entry.contentRect.width; view.height = entry.contentRect.height;
const dpr = Math.min(devicePixelRatio || 1, 2);
view.canvas.width = Math.round(view.width * dpr); view.canvas.height = Math.round(view.height * dpr);
}
});
views.forEach(view => resize.observe(view.canvas));
function frame(now) {
const elapsed = previous === null ? 0 : Math.min((now - previous) / 1000, 0.05);
previous = now;
if (!controls.paused && !document.hidden) time += elapsed * controls.speed;
for (const view of views) {
const { context: c, canvas, width, height } = view;
if (!width || !height) continue;
c.setTransform(1, 0, 0, 1, 0, 0); c.clearRect(0, 0, canvas.width, canvas.height);
c.setTransform(canvas.width / width, 0, 0, canvas.height / height, 0, 0);
// Keep the full motion envelope inside the frame, below the overlay.
const labelSpace = 32;
const bottomSpace = 10;
c.translate(width / 2, labelSpace + (height - labelSpace - bottomSpace) / 2);
const scale = Math.min((width - 20) / 330, (height - labelSpace - bottomSpace) / 250);
c.scale(scale, scale); c.lineCap = 'round'; c.lineJoin = 'round';
c.save(); base(c); c.restore();
c.save(); view.draw(c, time * (view.draw === renderers.military ? 0.6 + controls.load * 0.8 : 1), controls.load); c.restore();
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
+41
View File
@@ -0,0 +1,41 @@
# Existing Settings-Page Core Animations
This document summarizes the current implementation of the core visualizer card in the settings/control screen's right sidebar (`warp-core-canvas`). It covers all ten universe themes. Findings come from reading the source; they have not been visually verified in a browser. Observation-mode scenes and the separate audio spectrum display are outside this inventory.
## Theme inventory
| Theme | Settings card title / overlay | Existing renderer and appearance |
| --- | --- | --- |
| Starfleet Command | **MATTER / ANTIMATTER CORE** / `INTERMIX CHAMBER // ACTIVE` | `warp-core`: a vertical blue glass intermix chamber in a dark housing, orange injectors at the top and bottom, a white/cyan central reaction glow, 14 horizontal cyan coils, and pale blue plasma particles traveling vertically along a sinusoidal path. Pulses enlarge and brighten the central glow and particles, strengthen the coils, and launch extra particles from the center in both directions. |
| The Whoniverse | **CENTRAL TIME ROTOR COLUMN** / `VORTEX ROTOR // OPERATIONAL` | `time-rotor`: a cyan glass cylinder with brass/gold end collars. An inner assembly of three illuminated rods and four gold elliptical rings rises and falls continuously. A white/cyan glow follows the moving assembly, while cyan particles drift through a winding path around it. Audio pulses increase the glow and particle emphasis; the vertical travel runs on its own animation phase. All seven TARDIS console presets share this drawing. |
| Industrial Space | **HEAVY FUSION REACTOR** / `CONTAINMENT CORE // ACTIVE` | `industrial-reactor`: a dark brown cast-iron-style vertical housing, top/bottom warning-band rectangles, a white/yellow/amber central plasma glow, and nine horizontal coil clamps alternating amber and brown. Pulses expand the glow and increase the clamps' shadow glow. There are no moving mechanical parts or rendered plasma particles in this mode. |
| Bioships | **ORGANIC BIO-HEART SAC** / `VASCULAR NEXUS // PULSING` | `bio-heart`: a soft mint/emerald central aura fading into violet, with six curved green neural veins radiating outward. Audio pulses expand the aura and vein reach and increase its brightness. The visible form is an abstract radial glow and veins. Its additional phase-based breathing and vein rotation do not advance while this mode is selected; see the behavior notes below. |
| Retro Future | **ASTROGATOR OSCILLOSCOPE** / `ANALOG BEAM // ROTATING` | `retro-oscilloscope`: a circular dark green CRT face with a green rim, crosshairs, and a glowing pale green closed waveform. Forty-eight points form a ring whose radius is distorted by live audio time-domain samples; pulse energy increases the distortion scale. Despite the overlay, its angular phase does not advance in this mode. Drawing requires an initialized audio analyser. |
| Military Space | **DRADIS TACTICAL COMBAT CORE** / `CONDITION ONE // ARMED` | `warp-core`: the same blue/cyan chamber, orange injectors, central glow, 14 coils, and traveling particles used by Starfleet. The title and overlay provide the military identity; the core renderer contains no dedicated DRADIS or tactical geometry. |
| Deep Space | **GRAVITY SINGULARITY CORE** / `EVENT HORIZON // LOCKED` | `singularity-core`: a flattened elliptical indigo/blue accretion glow behind a small black circular event horizon, outlined in indigo. Pulses change disk opacity and the event horizon's halo strength. Disk and sphere sizes remain fixed. The disk has a phase-based rotation transform, but its phase does not advance in this mode, so it does not continuously rotate. |
| Outlaw / Frontier | **MODIFIED SUB-LIGHT REACTOR** / `KINETIC THRUSTERS // PRIMED` | `warp-core`: the same Starfleet-style blue/cyan chamber animation, including orange injectors, pulsing central glow, 14 coils, and plasma particles. There is no separate sub-light reactor or thruster animation. |
| Space Stations | **CENTRIFUGAL ROTATION CORE** / `STATION ROTOR // DOCKED` | `industrial-reactor`: the same amber glow, dark housing, warning-band rectangles, and nine coil clamps as Industrial Space. Pulses change glow size and coil illumination. Despite its title, no centrifugal rotor or station rotation is drawn. |
| Comedy / Adventure | **IMPROBABILITY CORE** / `REALITY FIELD // SHIFTING` | `warp-core`: the same Starfleet-style blue/cyan chamber, orange injectors, 14 coils, central glow, and traveling particles. There is no dedicated improbability distortion or shifting reality animation. |
## Shared behavior and implementation details
- **Ten themes use six renderers.** Starfleet, Military, Outlaw, and Comedy share `warp-core`. Industrial and Space Stations share `industrial-reactor`. Whoniverse, Bioships, Retro Future, and Deep Space each select one of the other four renderers.
- **Universe selection chooses the drawing.** `switchUniverse` applies the universe's theme class, updates the card text, and calls `visualizer.setMode(...)`. Selecting a ship/room preset changes sound parameters but does not select another core renderer. Starfleet's TNG, Voyager, Defiant, DS9, TOS, and NX preset theme values do not produce distinct core drawings in the current settings-page wiring.
- **Most pulse responses come from a synth callback.** Each `WarpCoreSynth` pulse resets visual `pulseEnergy` to `1.0` and adds six particles at the reaction center. Energy decays by a factor of `0.94` per animation frame, down to a floor of `0.15`. The callback receives pulse phase and duration, but the core visualizer does not use either value to shape the flash. BPM affects callback timing; the visual flash does not reproduce each preset's detailed audio envelope.
- **Only the retro waveform directly samples the audio signal.** Its ring deformation uses the analyser's time-domain data. The other core drawings use pulse energy and/or procedural particle movement rather than measured audio amplitude.
- **The TARDIS owns the only advancing phase.** `rotorPhase` starts at zero and advances by `0.038` per frame only inside `renderTimeRotor()`. Bio-heart breathing/vein rotation, retro waveform orientation, and singularity disk rotation read that same value without advancing it. Switching away from the TARDIS preserves its last phase, leaving those other renderers at a fixed phase until TARDIS mode advances it again.
- **Animation continues independently of master playback.** The visualizer starts a recurring `requestAnimationFrame` loop at initialization. When synth pulses stop, energy settles to its minimum. Warp particles and TARDIS travel continue, while the pulse-only reactor/heart/singularity effects settle. The retro drawing depends on analyser availability and data.
- **Particles are shared, but only two modes draw them.** The initial pool contains 36 particles. Warp-core and time-rotor modes move, recycle, and trim them. Other modes still receive pulse-created particles through the shared callback but do not render or trim that pool. `setMode` only changes the mode; it does not reset particles, energy, or phase.
- **Core colors are defined in JavaScript.** The canvas renderers use literal color values rather than reading each theme's CSS palette. Themes sharing a renderer therefore share its internal colors and geometry, even though their surrounding interface styling and labels differ.
- **Motion and decay are frame-based.** Particle movement, TARDIS phase advancement, and pulse decay use per-frame increments rather than elapsed time, so their real-time speed can vary with rendering frame rate.
## Source references
- `index.html`: core visualizer card, title, label, and canvas.
- `js/config.js`: `UniverseRegistry` theme-to-renderer assignments and ship/room presets.
- `js/app.js`: visualizer construction, `switchUniverse`, title map, `selectPreset`, and visualizer initialization.
- `js/visualizer.js`: pulse callback, mode selection, render loop, particle handling, and all six core renderers.
- `js/audio.js`: `WarpCoreSynth.startPulseLoop()` and `scheduleNextPulse()` supply pulse timing.
- `css/style.css`: shared canvas wrapper and 240px core wrapper height.
This is an inventory of existing behavior. No application code changes or proposed redesigns are included.
+1
View File
@@ -359,6 +359,7 @@
<!-- All-in-One Procedural Audio Engine & UI -->
<script src="js/audio.js"></script>
<script src="js/config.js"></script>
<script src="js/core-animations.js"></script>
<script src="js/visualizer.js"></script>
<script src="js/observation-bezels.js"></script>
<script src="js/observation-engine.js"></script>
+6 -6
View File
@@ -135,7 +135,7 @@ document.addEventListener('DOMContentLoaded', () => {
const titlesMap = {
'starfleet': {
sidebar: 'STARSHIP PRESETS',
visTitle: 'MATTER / ANTIMATTER CORE',
visTitle: 'INTERMIX CASCADE',
visLabel: 'INTERMIX CHAMBER // ACTIVE',
soundboard: 'LCARS KEYPAD'
},
@@ -147,7 +147,7 @@ document.addEventListener('DOMContentLoaded', () => {
},
'industrial': {
sidebar: 'INDUSTRIAL FREIGHTERS',
visTitle: 'HEAVY FUSION REACTOR',
visTitle: 'COMPRESSION FURNACE',
visLabel: 'CONTAINMENT CORE // ACTIVE',
soundboard: 'INDUSTRIAL INSTRUMENTATION'
},
@@ -165,7 +165,7 @@ document.addEventListener('DOMContentLoaded', () => {
},
'military': {
sidebar: 'BATTLEFLEET WARSHIPS',
visTitle: 'DRADIS TACTICAL COMBAT CORE',
visTitle: 'TACTICAL FLYWHEEL',
visLabel: 'CONDITION ONE // ARMED',
soundboard: 'CIC TACTICAL KEYPAD'
},
@@ -177,19 +177,19 @@ document.addEventListener('DOMContentLoaded', () => {
},
'outlaw': {
sidebar: 'FRONTIER & SALVAGE SHIPS',
visTitle: 'MODIFIED SUB-LIGHT REACTOR',
visTitle: 'PATCHWORK IMPULSE DRIVE',
visLabel: 'KINETIC THRUSTERS // PRIMED',
soundboard: 'OUTLAW COCKPIT CONTROLS'
},
'spacestations': {
sidebar: 'ORBITAL STATIONS & DOCKS',
visTitle: 'CENTRIFUGAL ROTATION CORE',
visTitle: 'CENTRIFUGAL DISTRIBUTION HUB',
visLabel: 'STATION ROTOR // DOCKED',
soundboard: 'STATION OPS CONSOLE'
},
'comedy': {
sidebar: 'WHIMSICAL & ADVENTURE FLEET',
visTitle: 'IMPROBABILITY CORE',
visTitle: 'IMPROBABILITY ENGINE',
visLabel: 'REALITY FIELD // SHIFTING',
soundboard: 'CHEERFUL CONSOLE KEYPAD'
}
+6 -6
View File
@@ -155,7 +155,7 @@ const UniverseRegistry = {
icon: '★',
themeClass: 'theme-lcars-tng',
headerTitle: 'STARFLEET SOUND MATRIX v2.5',
visualizer: 'warp-core',
visualizer: 'intermix-cascade',
events: [
{ id: 'btn-warp-jump', label: 'WARP JUMP', type: 'warp' },
{ id: 'btn-red-alert', label: 'RED ALERT', type: 'alert-red' },
@@ -292,7 +292,7 @@ const UniverseRegistry = {
icon: '⚙',
themeClass: 'theme-industrial',
headerTitle: 'HEAVY INDUSTRIAL REFINERY & FREIGHT',
visualizer: 'industrial-reactor',
visualizer: 'compression-furnace',
events: [
{ id: 'btn-epstein', label: 'EPSTEIN BURN', type: 'epstein' },
{ id: 'btn-airlock-purge', label: 'PURGE AIRLOCK', type: 'purge' },
@@ -593,7 +593,7 @@ const UniverseRegistry = {
icon: '⚔',
themeClass: 'theme-military',
headerTitle: 'TACTICAL BATTLESHIP & COMBAT COMMAND',
visualizer: 'warp-core',
visualizer: 'tactical-flywheel',
events: [
{ id: 'btn-action-stations', label: 'ACTION STATIONS', type: 'action-stations' },
{ id: 'btn-ftl-jump', label: 'FTL JUMP', type: 'ftl-jump' },
@@ -787,7 +787,7 @@ const UniverseRegistry = {
icon: '🚀',
themeClass: 'theme-outlaw',
headerTitle: 'BOUNTY HUNTERS, SMUGGLERS & SALVAGERS',
visualizer: 'warp-core',
visualizer: 'patchwork-drive',
events: [
{ id: 'btn-afterburner', label: 'AFTERBURNER ENGAGE', type: 'afterburner' },
{ id: 'btn-pirate-scramble', label: 'PIRATE SCRAMBLE', type: 'scramble' },
@@ -874,7 +874,7 @@ const UniverseRegistry = {
icon: '🛰',
themeClass: 'theme-spacestations',
headerTitle: 'ORBITAL HABITATS & COLONIAL OUTPOSTS',
visualizer: 'industrial-reactor',
visualizer: 'station-hub',
events: [
{ id: 'btn-station-rotation', label: 'STATION ROTATION DWELL', type: 'rotation' },
{ id: 'btn-docking-seq', label: 'DOCKING SEQUENCE', type: 'docking' },
@@ -961,7 +961,7 @@ const UniverseRegistry = {
icon: '✨',
themeClass: 'theme-comedy',
headerTitle: 'WHIMSICAL & RETRO SCI-FI EXPLORATION',
visualizer: 'warp-core',
visualizer: 'improbability-engine',
events: [
{ id: 'btn-ludicrous', label: 'LUDICROUS SPEED', type: 'ludicrous' },
{ id: 'btn-improbability', label: 'IMPROBABILITY FLIP', type: 'improbability' },
+216
View File
@@ -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;
})();
+64 -34
View File
@@ -13,6 +13,9 @@ class StarshipVisualizer {
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) {
@@ -72,6 +75,8 @@ class StarshipVisualizer {
}
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
@@ -83,32 +88,48 @@ class StarshipVisualizer {
}
startRenderLoop() {
const render = () => {
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();
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;
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 * 0.94);
this.pulseEnergy = Math.max(0.15, this.pulseEnergy * Math.pow(0.94, elapsed * 60));
this.animationFrameId = requestAnimationFrame(render);
};
@@ -135,7 +156,7 @@ class StarshipVisualizer {
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') gridCol = 'rgba(245, 158, 11, 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;
@@ -171,7 +192,7 @@ class StarshipVisualizer {
else col = '#c084fc';
} else if (this.mode === 'retro-oscilloscope') {
col = i < 28 ? '#22c55e' : '#86efac';
} else if (this.mode === 'industrial-reactor') {
} 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';
@@ -502,8 +523,8 @@ class StarshipVisualizer {
* 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 || !this.am.analyser) return;
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;
@@ -532,10 +553,12 @@ class StarshipVisualizer {
ctx.lineTo(centerX, centerY + crtRadius);
ctx.stroke();
// Lissajous audio waveform
const bufferLength = this.am.analyser.fftSize;
const dataArray = new Uint8Array(bufferLength);
this.am.analyser.getByteTimeDomainData(dataArray);
// 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';
@@ -547,16 +570,23 @@ class StarshipVisualizer {
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.rotorPhase;
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();
ctx.shadowBlur = 0;
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;
}
/**