交接0
车辆0
车站0
故障无
车队行为示意图——非实时数据
/* ---------- palette + sizing (edit these) ---------- */
.magv{
--magv-height: 300px;
--magv-deck: #CFD0CC; /* floor */
--magv-grid: #B4B6B1; /* floor markers, shadows */
--magv-accent: #E9A70B; /* the vehicles */
--magv-accent-d: #A0700A; /* vehicle detail + live figure */
--magv-ink: #16181A;
--magv-ink-2: #5B6165;
--magv-ink-3: #878C8F;
--magv-line: #C4C6C1; /* borders */
--magv-surface: #F1F1EF; /* stats bar, pick-up station */
--magv-load: #C4B08B; /* the carried item */
--magv-load-2: #E2D6BC; /* its label */
--magv-station: #9EA3A0;
--magv-station-2: #7E8481;
--magv-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--magv-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Courier New", monospace;
}
/* dark palette — applied when the visitor prefers dark and scheme is auto */
@media (prefers-color-scheme: dark){
.magv[data-scheme="auto"]{
--magv-deck: #1F2326;
--magv-grid: #333A3E;
--magv-accent: #F5B92B;
--magv-accent-d: #F5B92B;
--magv-ink: #ECEDEA;
--magv-ink-2: #9BA1A5;
--magv-ink-3: #6E7579;
--magv-line: #2E3335;
--magv-surface: #1B1E21;
--magv-load: #9C8A68;
--magv-load-2: #C4B593;
--magv-station: #41474A;
--magv-station-2: #2C3134;
}
}
/* ...or when you force it */
.magv[data-scheme="dark"]{
--magv-deck: #1F2326;
--magv-grid: #333A3E;
--magv-accent: #F5B92B;
--magv-accent-d: #F5B92B;
--magv-ink: #ECEDEA;
--magv-ink-2: #9BA1A5;
--magv-ink-3: #6E7579;
--magv-line: #2E3335;
--magv-surface: #1B1E21;
--magv-load: #9C8A68;
--magv-load-2: #C4B593;
--magv-station: #41474A;
--magv-station-2: #2C3134;
}
/* ---------- structure ---------- */
.magv, .magv *{ box-sizing: border-box; }
.magv{
width: 100%;
font-family: var(--magv-font);
}
.magv__frame{
border: 1px solid var(--magv-line);
border-radius: 14px;
background: var(--magv-deck);
overflow: hidden;
}
.magv__canvas{
display: block;
width: 100%;
height: var(--magv-height);
max-width: 100%;
}
.magv__bar{
display: flex;
flex-wrap: wrap;
align-items: center;
border-top: 1px solid var(--magv-line);
background: var(--magv-surface);
}
.magv__stat{
display: flex;
align-items: baseline;
gap: 9px;
padding: 11px 20px;
border-right: 1px solid var(--magv-line);
font-family: var(--magv-mono);
font-size: 11px;
line-height: 1.4;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--magv-ink-3);
}
.magv__stat b{
font-size: 14px;
font-weight: 600;
letter-spacing: .02em;
color: var(--magv-ink);
font-variant-numeric: tabular-nums;
}
.magv__stat--live b{ color: var(--magv-accent-d); }
.magv__note{
margin-left: auto;
padding: 11px 20px;
font-family: var(--magv-mono);
font-size: 10px;
line-height: 1.4;
letter-spacing: .09em;
text-transform: uppercase;
color: var(--magv-ink-3);
}
@media (max-width: 760px){
.magv__note{
margin-left: 0;
width: 100%;
border-top: 1px solid var(--magv-line);
}
.magv__stat{ padding: 10px 14px; }
}
(function () {
'use strict';
function initAll() {
var nodes = document.querySelectorAll('.magv');
for (var i = 0; i < nodes.length; i++) init(nodes[i]);
}
function init(root) {
if (root.getAttribute('data-magv-ready') === '1') return; // don't double-init
root.setAttribute('data-magv-ready', '1');
var cv = root.querySelector('.magv__canvas');
if (!cv || !cv.getContext) return;
var ctx = cv.getContext('2d');
var elCount = root.querySelector('[data-magv="count"]');
var elBots = root.querySelector('[data-magv="bots"]');
var elStations = root.querySelector('[data-magv="stations"]');
var motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
var W = 0, H = 0, dpr = 1;
var deckTop = 0, deckBot = 0, rw = 30, rh = 20;
var lanes = [], stations = [], pickup = { x: 0, y: 0, w: 0, h: 0 };
var bots = [], done = 0;
var rafId = null, last = 0, running = false;
var C = {};
function readTokens() {
var s = getComputedStyle(root);
function g(n) { return s.getPropertyValue(n).trim(); }
C = {
deck: g('--magv-deck'), grid: g('--magv-grid'),
accent: g('--magv-accent'), accentD: g('--magv-accent-d'),
ink: g('--magv-ink'), ink2: g('--magv-ink-2'), ink3: g('--magv-ink-3'),
line: g('--magv-line'), surface: g('--magv-surface'),
load: g('--magv-load'), load2: g('--magv-load-2'),
station: g('--magv-station'), station2: g('--magv-station-2'),
mono: g('--magv-mono') || 'monospace'
};
}
function rr(x, y, w, h, r) {
r = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function layout() {
var bandH = Math.max(38, Math.min(58, H * 0.19));
var pad = 9;
var pickW = Math.max(58, Math.min(92, W * 0.085));
deckTop = bandH + pad;
deckBot = H - bandH - pad;
rw = Math.max(22, Math.min(34, W * 0.031));
rh = rw * 0.66;
lanes = [];
var n = 4, usable = deckBot - deckTop;
for (var i = 0; i < n; i++) lanes.push(deckTop + usable * (i + 0.5) / n);
var startX = pickW + 16;
var avail = W - startX - 12;
var gap = Math.max(9, W * 0.012);
var target = Math.max(50, Math.min(76, W * 0.062));
var count = Math.max(3, Math.round((avail + gap) / (target + gap)));
var cw = (avail - gap * (count - 1)) / count;
stations = [];
for (var s = 0; s < 2; s++) {
for (var j = 0; j < count; j++) {
stations.push({
x: startX + j * (cw + gap), w: cw,
y: s === 0 ? pad : H - bandH - pad, h: bandH - pad,
side: s === 0 ? 'top' : 'bot',
id: (s === 0 ? 'A' : 'B') + (j + 1),
fill: Math.random() * 0.55, flash: 0, swap: 0
});
}
}
pickup = { x: 7, y: deckTop, w: pickW - 14, h: deckBot - deckTop };
if (elStations) elStations.textContent = String(stations.length);
}
function assign(b) {
var ahead = [];
for (var i = 0; i b.x + rw) ahead.push(stations[i]);
}
var pool = ahead.length ? ahead : stations;
var c = pool[(Math.random() * pool.length) | 0];
b.station = c;
b.targetX = c.x + c.w / 2;
b.edgeY = c.side === 'top' ? deckTop + rh * 0.62 : deckBot - rh * 0.62;
return c;
}
function respawn(b, seed) {
b.lane = (Math.random() * lanes.length) | 0;
b.homeY = lanes[b.lane];
b.y = b.homeY;
b.speed = 50 + Math.random() * 32;
b.tipT = 0;
b.state = 'carry';
if (seed) {
b.x = pickup.x + pickup.w + Math.random() * Math.max(40, W - pickup.w - 60);
b.alpha = 1;
} else {
b.x = pickup.x + pickup.w * 0.55;
b.alpha = 0;
}
assign(b);
if (seed && b.x > b.targetX) b.state = 'return';
}
function build() {
var want = Math.max(6, Math.min(14, Math.round(W / 82)));
bots = [];
for (var i = 0; i = b.targetX) { b.x = b.targetX; b.state = 'approach'; }
} else if (b.state === 'approach') {
var dy = b.edgeY - b.y;
b.y += (dy > 0 ? 1 : -1) * Math.min(Math.abs(dy), 78 * dt);
if (Math.abs(dy) = 1) {
b.tipT = 1;
b.state = 'return';
done++;
b.station.flash = 1;
b.station.fill += 0.11;
if (b.station.fill >= 1) { b.station.fill = 0; b.station.swap = 1; }
}
} else {
b.x += b.speed * 1.3 * dt;
var d2 = b.homeY - b.y;
b.y += (d2 > 0 ? 1 : -1) * Math.min(Math.abs(d2), 64 * dt);
if (b.x > W + 40) respawn(b, false);
}
if (b.state === 'return' && b.x > W - 56) b.alpha = Math.max(0, (W + 24 - b.x) / 80);
else b.alpha = Math.min(1, b.alpha + dt * 2.4);
}
function drawStation(c) {
var openH = 6, top = c.y;
ctx.fillStyle = C.station;
rr(c.x, top, c.w, c.h, 4); ctx.fill();
if (c.fill > 0.02) {
ctx.fillStyle = C.station2;
var fh = (c.h - openH - 4) * c.fill;
rr(c.x + 3, top + c.h - 3 - fh, c.w - 6, fh, 3); ctx.fill();
}
ctx.fillStyle = C.station2;
if (c.side === 'top') rr(c.x, top + c.h - openH, c.w, openH, 3);
else rr(c.x, top, c.w, openH, 3);
ctx.fill();
if (c.flash > 0.01) {
ctx.strokeStyle = C.accent; ctx.globalAlpha = c.flash; ctx.lineWidth = 2;
rr(c.x - 1, top - 1, c.w + 2, c.h + 2, 5); ctx.stroke(); ctx.globalAlpha = 1;
}
if (c.swap > 0.01) {
ctx.fillStyle = C.accent; ctx.globalAlpha = c.swap * 0.35;
rr(c.x, top, c.w, c.h, 4); ctx.fill(); ctx.globalAlpha = 1;
}
if (c.w > 40) {
ctx.fillStyle = C.ink3;
ctx.font = '600 9px ' + C.mono;
ctx.textAlign = 'center';
ctx.textBaseline = c.side === 'top' ? 'top' : 'bottom';
ctx.fillText(c.id, c.x + c.w / 2, c.side === 'top' ? top + 5 : top + c.h - 5);
}
}
function drawBot(b) {
var t = b.state === 'tip' ? b.tipT : 0;
var dir = b.station && b.station.side === 'top' ? -1 : 1;
var tilt = t > 0 ? Math.sin(Math.PI * Math.min(1, t * 1.25)) * 0.34 * dir : 0;
var carrying = b.state === 'carry' || b.state === 'approach' || b.state === 'tip';
var pw = rw * 0.6, ph = rh * 0.52, pOff = 0, pAlpha = 1;
if (b.state === 'tip') {
var e = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
pOff = e * (rh * 0.5 + 30) * dir;
pAlpha = 1 - Math.max(0, (t - 0.5) / 0.5);
}
ctx.save();
// shadow
ctx.fillStyle = C.grid;
ctx.globalAlpha = b.alpha * 0.35;
ctx.beginPath();
ctx.ellipse(b.x, b.y + rh * 0.62, rw * 0.5, rh * 0.2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = b.alpha;
// chassis (tilts when handing off)
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(tilt);
ctx.fillStyle = C.accent;
rr(-rw / 2, -rh / 2, rw, rh, rh * 0.34); ctx.fill();
ctx.fillStyle = C.accentD;
ctx.globalAlpha = b.alpha * 0.32;
rr(-rw / 2 + 2.5, -rh / 2 + 2, rw - 5, rh - 4, rh * 0.26); ctx.fill();
ctx.globalAlpha = b.alpha;
ctx.fillStyle = C.ink;
ctx.globalAlpha = b.alpha * 0.7;
rr(rw / 2 - 6, -1.4, 3.4, 2.8, 1.4); ctx.fill();
ctx.globalAlpha = b.alpha;
ctx.restore();
// the load
if (carrying) {
ctx.globalAlpha = b.alpha * pAlpha;
ctx.fillStyle = C.load;
rr(b.x - pw / 2, b.y - ph / 2 + pOff, pw, ph, 2); ctx.fill();
ctx.fillStyle = C.load2;
rr(b.x - pw / 2 + 2, b.y - ph / 2 + pOff + 1.5, pw * 0.42, ph * 0.4, 1); ctx.fill();
}
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = C.deck;
ctx.fillRect(0, 0, W, H);
// floor markers
ctx.strokeStyle = C.grid;
ctx.lineWidth = 1;
ctx.globalAlpha = 0.75;
ctx.beginPath();
for (var gx = pickup.x + pickup.w + 15; gx < W; gx += 30) {
for (var gy = deckTop + 4; gy < deckBot; gy += 30) {
ctx.moveTo(gx - 2.5, gy); ctx.lineTo(gx + 2.5, gy);
ctx.moveTo(gx, gy - 2.5); ctx.lineTo(gx, gy + 2.5);
}
}
ctx.stroke();
ctx.globalAlpha = 1;
// lane edges
ctx.strokeStyle = C.grid;
ctx.beginPath();
ctx.moveTo(0, deckTop - 3); ctx.lineTo(W, deckTop - 3);
ctx.moveTo(0, deckBot + 3); ctx.lineTo(W, deckBot + 3);
ctx.stroke();
// pick-up station
ctx.fillStyle = C.surface;
rr(pickup.x, pickup.y, pickup.w, pickup.h, 6); ctx.fill();
ctx.strokeStyle = C.line; ctx.stroke();
ctx.fillStyle = C.accent;
rr(pickup.x + pickup.w - 4, pickup.y + 6, 3, pickup.h - 12, 1.5); ctx.fill();
ctx.save();
ctx.translate(pickup.x + pickup.w / 2 - 2, pickup.y + pickup.h / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillStyle = C.ink2;
ctx.font = '600 9px ' + C.mono;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('PICK-UP', 0, 0);
ctx.restore();
for (var i = 0; i < stations.length; i++) drawStation(stations[i]);
for (var j = 0; j < bots.length; j++) drawBot(bots[j]);
}
function frame(now) {
if (!running) return;
var dt = Math.min(0.05, (now - last) / 1000 || 0);
last = now;
for (var i = 0; i < bots.length; i++) step(bots[i], dt);
for (var j = 0; j 0) stations[j].flash = Math.max(0, stations[j].flash - dt * 2.2);
if (stations[j].swap > 0) stations[j].swap = Math.max(0, stations[j].swap - dt * 1.6);
}
draw();
if (elCount) {
var txt = done.toLocaleString('en-US');
if (elCount.textContent !== txt) elCount.textContent = txt;
}
rafId = requestAnimationFrame(frame);
}
function start() {
if (running || motionQuery.matches || !W) return;
running = true;
last = performance.now();
rafId = requestAnimationFrame(frame);
}
function stop() {
running = false;
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
}
function staticFrame() {
for (var i = 0; i < bots.length; i++) {
var b = bots[i];
b.x = pickup.x + pickup.w + 30 + (i / bots.length) * (W - pickup.w - 90);
b.alpha = 1;
if (i === 2) { b.state = 'tip'; b.tipT = 0.62; b.y = b.edgeY; }
}
done = 1284;
draw();
if (elCount) elCount.textContent = done.toLocaleString('en-US');
}
function resize() {
var r = cv.getBoundingClientRect();
if (!r.width) return;
dpr = Math.min(window.devicePixelRatio || 1, 2);
cv.width = Math.round(r.width * dpr);
cv.height = Math.round(r.height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
W = r.width; H = r.height;
readTokens();
layout();
build();
if (motionQuery.matches) staticFrame(); else draw();
}
var rt;
window.addEventListener('resize', function () {
clearTimeout(rt);
rt = setTimeout(resize, 140);
});
// keep canvas colours in step with the palette
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
function repaint() { readTokens(); if (!running) draw(); }
if (darkQuery.addEventListener) darkQuery.addEventListener('change', repaint);
if (window.MutationObserver) {
new MutationObserver(repaint).observe(root, { attributes: true, attributeFilter: ['data-scheme'] });
}
if (motionQuery.addEventListener) {
motionQuery.addEventListener('change', function () {
if (motionQuery.matches) { stop(); staticFrame(); } else { start(); }
});
}
document.addEventListener('visibilitychange', function () {
if (document.hidden) stop(); else start();
});
// only animate while it is on screen
if ('IntersectionObserver' in window) {
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) start(); else stop();
}, { threshold: 0.02 }).observe(cv);
}
function boot() { resize(); if (!motionQuery.matches) start(); }
if (document.fonts && document.fonts.ready) document.fonts.ready.then(boot).catch(boot);
else boot();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
})();