Files
SciFi-XZBT/artifacts/core_improvement_examples.html

347 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>