347 lines
22 KiB
HTML
347 lines
22 KiB
HTML
<!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>
|