Files
ParkingRobot/Simulation/wwwroot/viewer.js
T

843 lines
24 KiB
JavaScript
Raw 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.
const canvas = document.getElementById("world-canvas");
const context = canvas.getContext("2d");
const vehicleSelect = document.getElementById("vehicle-select");
const commandMessage = document.getElementById("command-message");
const connectionDot = document.getElementById("connection-dot");
const connectionText = document.getElementById("connection-text");
const vehicleCards = document.getElementById("vehicle-cards");
const selectedVehicleDetail =
document.getElementById("selected-vehicle-detail");
const actionGroups = document.getElementById("action-groups");
const updateRate = document.getElementById("update-rate");
const vehicleCountInput = document.getElementById("vehicle-count");
const layoutRows = document.getElementById("layout-rows");
const remoteState = document.getElementById("remote-state");
const speedScaleInput = document.getElementById("speed-scale");
const steeringScaleInput =
document.getElementById("steering-scale");
const speedScaleValue =
document.getElementById("speed-scale-value");
const steeringScaleValue =
document.getElementById("steering-scale-value");
let vehicles = [];
let actions = [];
let configuration = {
vehicleCount: 1,
fleetCenter: { xMeters: 0, yMeters: 0, yawRadians: 0 },
vehicles: [
{ vehicleId: 1, xMeters: 0, yMeters: 0, yawRadians: 0 }
]
};
let lastUpdateTime = performance.now();
let successfulUpdates = 0;
let remoteThrottle = 0;
let remoteSteering = 0;
let remotePointerId = null;
let remoteVehicleId = null;
const pressedDriveKeys = new Set();
function resizeCanvas() {
const bounds = canvas.getBoundingClientRect();
const ratio = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(bounds.width * ratio));
canvas.height = Math.max(1, Math.floor(bounds.height * ratio));
context.setTransform(ratio, 0, 0, ratio, 0, 0);
}
function getView() {
const centerX = configuration.fleetCenter.xMeters;
const centerY = configuration.fleetCenter.yMeters;
let radiusX = 2.1;
let radiusY = 1.7;
for (const vehicle of vehicles) {
radiusX = Math.max(
radiusX,
Math.abs(vehicle.xMeters - centerX) + 1.2);
radiusY = Math.max(
radiusY,
Math.abs(vehicle.yMeters - centerY) + 1.0);
}
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const scale = Math.min(
width / (radiusX * 2),
height / (radiusY * 2),
155);
return { centerX, centerY, scale, width, height };
}
function worldToScreen(xMeters, yMeters, view = getView()) {
return {
x: view.width / 2 +
(xMeters - view.centerX) * view.scale,
y: view.height / 2 -
(yMeters - view.centerY) * view.scale,
scale: view.scale
};
}
function drawGrid(view) {
const width = view.width;
const height = view.height;
const worldOrigin = worldToScreen(0, 0, view);
const spacing = view.scale;
context.clearRect(0, 0, width, height);
context.fillStyle = "#071018";
context.fillRect(0, 0, width, height);
context.lineWidth = 1;
context.strokeStyle = "rgba(109, 143, 160, 0.12)";
for (let x = worldOrigin.x % spacing; x < width; x += spacing) {
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, height);
context.stroke();
}
for (let y = worldOrigin.y % spacing; y < height; y += spacing) {
context.beginPath();
context.moveTo(0, y);
context.lineTo(width, y);
context.stroke();
}
context.strokeStyle = "rgba(90, 125, 142, 0.35)";
context.lineWidth = 1.2;
context.beginPath();
context.moveTo(0, worldOrigin.y);
context.lineTo(width, worldOrigin.y);
context.moveTo(worldOrigin.x, 0);
context.lineTo(worldOrigin.x, height);
context.stroke();
}
function drawFleetCenter(view) {
const fleet = configuration.fleetCenter;
const point = worldToScreen(
fleet.xMeters,
fleet.yMeters,
view);
const yaw = fleet.yawRadians;
const axisLength = 55;
context.save();
context.translate(point.x, point.y);
context.rotate(-yaw);
context.fillStyle = "#b58cff";
context.beginPath();
context.moveTo(0, -8);
context.lineTo(8, 0);
context.lineTo(0, 8);
context.lineTo(-8, 0);
context.closePath();
context.fill();
drawArrow(0, 0, axisLength, 0, "#ff7285", 2);
drawArrow(0, 0, 0, -axisLength, "#66d99a", 2);
context.restore();
context.fillStyle = "#c7aaff";
context.font = "12px Inter, sans-serif";
context.fillText("FLEET CENTER", point.x + 12, point.y - 11);
}
function drawVehicle(vehicle, view) {
const point = worldToScreen(
vehicle.xMeters,
vehicle.yMeters,
view);
const selected = Number(vehicleSelect.value) === vehicle.vehicleId;
const bodyLength = vehicle.bodyLengthMeters * point.scale;
const bodyWidth = vehicle.bodyWidthMeters * point.scale;
context.save();
context.translate(point.x, point.y);
context.rotate(-vehicle.yawRadians);
context.fillStyle = selected
? "rgba(26, 180, 155, 0.2)"
: "rgba(50, 73, 88, 0.42)";
context.strokeStyle = selected ? "#39d8b7" : "#668392";
context.lineWidth = selected ? 2.5 : 1.5;
context.beginPath();
context.roundRect(
-bodyLength / 2,
-bodyWidth / 2,
bodyLength,
bodyWidth,
12);
context.fill();
context.stroke();
drawBodyAxes(bodyLength, bodyWidth);
for (const wheel of vehicle.wheels) {
drawWheel(
wheel.xMeters * point.scale,
-wheel.yMeters * point.scale,
wheel);
}
context.fillStyle = "rgba(202, 222, 230, 0.8)";
context.font = "10px Inter, sans-serif";
context.textAlign = "center";
context.fillText(
"1472 × 948 mm",
0,
bodyWidth / 2 - 8);
context.restore();
context.textAlign = "left";
context.fillStyle = "#dbe9ef";
context.font = "600 13px Inter, sans-serif";
context.fillText(
`CAR ${vehicle.vehicleId}`,
point.x - 24,
point.y - bodyWidth / 2 - 18);
context.fillStyle = vehicle.modeReady ? "#42dfb7" : "#ffb257";
context.font = "12px Inter, sans-serif";
context.fillText(
vehicle.modeReady ? "READY" : "ALIGNING",
point.x - 23,
point.y + bodyWidth / 2 + 25);
}
function drawBodyAxes(bodyLength, bodyWidth) {
const xLength = bodyLength / 2 + 30;
const yLength = bodyWidth / 2 + 27;
drawArrow(0, 0, xLength, 0, "#ff6477", 2.4);
drawArrow(0, 0, 0, -yLength, "#54d991", 2.4);
context.font = "700 11px Inter, sans-serif";
context.fillStyle = "#ff8b99";
context.fillText("+X", xLength - 2, -7);
context.fillStyle = "#7debab";
context.fillText("+Y", 6, -yLength + 2);
}
function drawWheel(x, y, wheel) {
const wheelLength = 33;
const limitRadius = 20;
context.save();
context.translate(x, y);
context.strokeStyle = "rgba(86, 167, 255, 0.72)";
context.lineWidth = 1.2;
context.setLineDash([3, 3]);
context.beginPath();
context.moveTo(-22, 0);
context.lineTo(24, 0);
context.stroke();
drawWheelLimitArc(limitRadius);
context.save();
context.rotate(-wheel.targetAngleDegrees * Math.PI / 180);
context.strokeStyle = "rgba(255, 178, 87, 0.9)";
context.lineWidth = 3;
context.setLineDash([5, 4]);
context.beginPath();
context.moveTo(-wheelLength / 2, 0);
context.lineTo(wheelLength / 2, 0);
context.stroke();
context.restore();
context.save();
context.rotate(-wheel.actualAngleDegrees * Math.PI / 180);
context.strokeStyle = wheel.isAligned ? "#42dfb7" : "#f2f6f8";
context.lineWidth = 7;
context.setLineDash([]);
context.lineCap = "round";
context.beginPath();
context.moveTo(-wheelLength / 2, 0);
context.lineTo(wheelLength / 2, 0);
context.stroke();
context.restore();
context.setLineDash([]);
context.fillStyle = wheel.isAligned ? "#7ef4dc" : "#ffca83";
context.font = "700 9px Inter, sans-serif";
context.textAlign = "center";
const sign = wheel.actualAngleDegrees >= 0 ? "+" : "";
context.fillText(
`${sign}${wheel.actualAngleDegrees.toFixed(0)}°`,
0,
-25);
context.restore();
}
function drawWheelLimitArc(radius) {
context.strokeStyle = "rgba(181, 140, 255, 0.5)";
context.lineWidth = 1;
context.setLineDash([]);
context.beginPath();
for (let angle = -120; angle <= 120; angle += 5) {
const radians = -angle * Math.PI / 180;
const x = Math.cos(radians) * radius;
const y = Math.sin(radians) * radius;
if (angle === -120) context.moveTo(x, y);
else context.lineTo(x, y);
}
context.stroke();
for (const angle of [-120, 0, 120]) {
const radians = -angle * Math.PI / 180;
context.beginPath();
context.moveTo(
Math.cos(radians) * (radius - 3),
Math.sin(radians) * (radius - 3));
context.lineTo(
Math.cos(radians) * (radius + 3),
Math.sin(radians) * (radius + 3));
context.stroke();
}
}
function drawArrow(x1, y1, x2, y2, color, width) {
const angle = Math.atan2(y2 - y1, x2 - x1);
context.strokeStyle = color;
context.fillStyle = color;
context.lineWidth = width;
context.setLineDash([]);
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.stroke();
context.beginPath();
context.moveTo(x2, y2);
context.lineTo(
x2 - 9 * Math.cos(angle - Math.PI / 6),
y2 - 9 * Math.sin(angle - Math.PI / 6));
context.lineTo(
x2 - 9 * Math.cos(angle + Math.PI / 6),
y2 - 9 * Math.sin(angle + Math.PI / 6));
context.closePath();
context.fill();
}
function draw() {
const view = getView();
drawGrid(view);
drawFleetCenter(view);
vehicles.forEach(vehicle => drawVehicle(vehicle, view));
requestAnimationFrame(draw);
}
function renderActions() {
const preferredGroups = ["舵轮模式", "运动测试", "维护"];
const groups = [...new Set(actions.map(action => action.group))]
.sort((left, right) => {
const leftIndex = preferredGroups.indexOf(left);
const rightIndex = preferredGroups.indexOf(right);
if (leftIndex < 0 && rightIndex < 0)
return left.localeCompare(right, "zh-CN");
if (leftIndex < 0) return 1;
if (rightIndex < 0) return -1;
return leftIndex - rightIndex;
});
actionGroups.innerHTML = groups.map(group => {
const buttons = actions
.filter(action => action.group === group)
.sort((a, b) => a.order - b.order)
.map(action => `
<button data-command="${action.key}"
class="${action.key === "stop" ? "danger-button" : ""}">
${action.displayName}
</button>`)
.join("");
return `
<section class="action-group">
<p class="group-label">${group}</p>
<div class="action-grid">${buttons}</div>
</section>`;
}).join("");
actionGroups.querySelectorAll("[data-command]")
.forEach(button => {
button.addEventListener("click", () =>
sendCommand(button.dataset.command));
});
}
function renderVehicleSelector() {
const previous = Number(vehicleSelect.value);
vehicleSelect.innerHTML = vehicles
.map(vehicle =>
`<option value="${vehicle.vehicleId}">车辆 ${vehicle.vehicleId}</option>`)
.join("");
if (vehicles.some(vehicle => vehicle.vehicleId === previous))
vehicleSelect.value = String(previous);
}
function renderCards() {
vehicleCards.innerHTML = vehicles.map(vehicle => {
const actual = vehicle.actualBodyTwist;
const selected =
Number(vehicleSelect.value) === vehicle.vehicleId;
return `
<button class="vehicle-card ${selected ? "selected" : ""}"
data-vehicle="${vehicle.vehicleId}">
<div class="card-topline">
<strong>CAR ${vehicle.vehicleId}</strong>
<span class="${vehicle.modeReady ? "status-ready" : "status-aligning"}">
${vehicle.modeReady ? "READY" : "ALIGNING"}
</span>
</div>
<div class="mode-name">${formatMode(vehicle.mode)}</div>
<dl>
<div><dt>X</dt><dd>${vehicle.xMeters.toFixed(2)} m</dd></div>
<div><dt>Y</dt><dd>${vehicle.yMeters.toFixed(2)} m</dd></div>
<div><dt>Yaw</dt><dd>${radToDeg(vehicle.yawRadians).toFixed(1)}°</dd></div>
<div><dt>Vx / Vy</dt><dd>${actual.vxMetersPerSecond.toFixed(2)} / ${actual.vyMetersPerSecond.toFixed(2)}</dd></div>
</dl>
</button>`;
}).join("");
vehicleCards.querySelectorAll("[data-vehicle]").forEach(card => {
card.addEventListener("click", () => {
vehicleSelect.value = card.dataset.vehicle;
renderCards();
renderSelectedVehicleDetail();
});
});
}
function renderSelectedVehicleDetail() {
const selectedId = Number(vehicleSelect.value);
const vehicle = vehicles.find(item => item.vehicleId === selectedId);
if (!vehicle) {
selectedVehicleDetail.innerHTML = "";
return;
}
selectedVehicleDetail.innerHTML = `
<section class="wheel-detail">
<div class="geometry-note">
车体 1472 × 948 mm · 实际DLL轮位 X±750 / Y±500 mm
</div>
${vehicle.wheels.map(wheel => {
const actualPercent =
(wheel.actualAngleDegrees + 120) / 240 * 100;
const targetPercent =
(wheel.targetAngleDegrees + 120) / 240 * 100;
return `
<div class="wheel-row">
<div class="wheel-row-title">
<strong>${wheel.name}</strong>
<span>实际 ${signed(wheel.actualAngleDegrees)}° · 目标 ${signed(wheel.targetAngleDegrees)}°</span>
</div>
<div class="angle-scale">
<span class="zero-mark"></span>
<span class="target-mark" style="left:${targetPercent}%"></span>
<span class="actual-mark" style="left:${actualPercent}%"></span>
</div>
<div class="wheel-speed">
轮速 ${wheel.actualSpeedMetersPerSecond.toFixed(2)} m/s
</div>
</div>`;
}).join("")}
</section>`;
}
function formatMode(mode) {
return {
Normal: "正常模式",
CrabLeft: "左蟹行",
CrabRight: "右蟹行",
Spin: "自转模式"
}[mode] || mode;
}
async function loadActions() {
const response = await fetch("/api/actions", { cache: "no-store" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
actions = await response.json();
renderActions();
}
async function loadConfiguration() {
const response = await fetch("/api/configuration", {
cache: "no-store"
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
configuration = await response.json();
populateConfigurationForm();
}
function populateConfigurationForm() {
vehicleCountInput.value = String(configuration.vehicleCount);
document.getElementById("fleet-x").value =
configuration.fleetCenter.xMeters;
document.getElementById("fleet-y").value =
configuration.fleetCenter.yMeters;
document.getElementById("fleet-yaw").value =
radToDeg(configuration.fleetCenter.yawRadians).toFixed(1);
renderLayoutRows(configuration.vehicleCount, configuration.vehicles);
}
function readExistingLayouts() {
return [...layoutRows.querySelectorAll(".layout-row")].map(row => ({
vehicleId: Number(row.dataset.vehicleId),
xMeters: Number(row.querySelector("[data-field='x']").value),
yMeters: Number(row.querySelector("[data-field='y']").value),
yawRadians: degToRad(
Number(row.querySelector("[data-field='yaw']").value))
}));
}
function renderLayoutRows(count, sourceLayouts = readExistingLayouts()) {
const byId = new Map(
sourceLayouts.map(layout => [layout.vehicleId, layout]));
const fallbackSpacing = 2.0;
layoutRows.innerHTML = Array.from({ length: count }, (_, index) => {
const id = index + 1;
const fallbackX =
(index - (count - 1) / 2) * fallbackSpacing;
const layout = byId.get(id) || {
vehicleId: id,
xMeters: count === 1 ? 0 : fallbackX,
yMeters: 0,
yawRadians: 0
};
return `
<div class="layout-row" data-vehicle-id="${id}">
<strong>CAR ${id}</strong>
<label>X<input data-field="x" type="number" step="0.1"
value="${layout.xMeters}"></label>
<label>Y<input data-field="y" type="number" step="0.1"
value="${layout.yMeters}"></label>
<label>Yaw°<input data-field="yaw" type="number" step="1"
value="${radToDeg(layout.yawRadians).toFixed(1)}"></label>
</div>`;
}).join("");
}
async function applyConfiguration() {
const count = Number(vehicleCountInput.value);
const layouts = readExistingLayouts().slice(0, count);
const payload = {
vehicleCount: count,
fleetCenter: {
xMeters: Number(document.getElementById("fleet-x").value),
yMeters: Number(document.getElementById("fleet-y").value),
yawRadians: degToRad(
Number(document.getElementById("fleet-yaw").value))
},
vehicles: layouts
};
const response = await fetch("/api/configuration", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok) {
commandMessage.textContent =
result.message || "布局配置失败。";
commandMessage.classList.add("error");
return;
}
configuration = result;
commandMessage.textContent =
`已应用 ${configuration.vehicleCount} 辆车的车队布局。`;
commandMessage.classList.remove("error");
await updateVehicles();
}
async function updateVehicles() {
try {
const response = await fetch("/api/vehicles", {
cache: "no-store"
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
vehicles = await response.json();
renderVehicleSelector();
renderCards();
renderSelectedVehicleDetail();
connectionDot.classList.add("online");
connectionText.textContent = "离线仿真运行中";
successfulUpdates += 1;
} catch {
connectionDot.classList.remove("online");
connectionText.textContent = "仿真后端未连接";
}
}
async function sendCommand(command) {
const vehicleId = vehicleSelect.value;
try {
const response = await fetch(
`/api/vehicles/${vehicleId}/commands/${command}`,
{ method: "POST" });
const result = await response.json();
commandMessage.textContent = result.message;
commandMessage.classList.toggle("error", !result.success);
await updateVehicles();
} catch {
commandMessage.textContent =
"命令发送失败,请检查仿真后端。";
commandMessage.classList.add("error");
}
}
function getRemoteScale(input) {
return Number(input.value) / 100;
}
function updateRemoteLabels() {
speedScaleValue.textContent = `${speedScaleInput.value}%`;
steeringScaleValue.textContent =
`${steeringScaleInput.value}%`;
}
async function sendManualControl(
throttle,
steering,
showMessage = false,
vehicleIdOverride = null) {
remoteThrottle = throttle;
remoteSteering = steering;
const active =
Math.abs(throttle) > 0.001 ||
Math.abs(steering) > 0.001;
remoteState.textContent = active
? `油门 ${signed(throttle)} · 转向 ${signed(steering)}`
: "已松开";
remoteState.classList.toggle("active", active);
try {
const vehicleId = vehicleIdOverride ??
(active
? vehicleSelect.value
: remoteVehicleId ?? vehicleSelect.value);
if (active)
remoteVehicleId = String(vehicleId);
const response = await fetch(
`/api/vehicles/${vehicleId}/manual-control`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
throttle,
steering,
speedScale: getRemoteScale(speedScaleInput),
steeringScale: getRemoteScale(steeringScaleInput)
})
});
const result = await response.json();
if (!response.ok || showMessage) {
commandMessage.textContent = result.message;
commandMessage.classList.toggle(
"error",
!result.success);
}
if (!active &&
String(vehicleId) === remoteVehicleId) {
remoteVehicleId = null;
}
} catch {
commandMessage.textContent =
"虚拟遥控命令发送失败,请检查仿真后端。";
commandMessage.classList.add("error");
}
}
function stopRemote(showMessage = false) {
const vehicleId = remoteVehicleId;
pressedDriveKeys.clear();
document.querySelectorAll(".remote-pad .pressed")
.forEach(button => button.classList.remove("pressed"));
return sendManualControl(
0,
0,
showMessage,
vehicleId);
}
function driveFromKeyboard() {
const forward =
pressedDriveKeys.has("KeyW") ||
pressedDriveKeys.has("ArrowUp");
const backward =
pressedDriveKeys.has("KeyS") ||
pressedDriveKeys.has("ArrowDown");
const left =
pressedDriveKeys.has("KeyA") ||
pressedDriveKeys.has("ArrowLeft");
const right =
pressedDriveKeys.has("KeyD") ||
pressedDriveKeys.has("ArrowRight");
const throttle = Number(forward) - Number(backward);
const steering = Number(left) - Number(right);
sendManualControl(throttle, steering);
}
function isTypingTarget(target) {
return target instanceof HTMLInputElement ||
target instanceof HTMLSelectElement ||
target instanceof HTMLTextAreaElement;
}
document.querySelectorAll(
".remote-pad [data-throttle][data-steering]")
.forEach(button => {
button.addEventListener("pointerdown", event => {
event.preventDefault();
remotePointerId = event.pointerId;
button.setPointerCapture(event.pointerId);
button.classList.add("pressed");
sendManualControl(
Number(button.dataset.throttle),
Number(button.dataset.steering));
});
const release = event => {
if (remotePointerId !== event.pointerId)
return;
remotePointerId = null;
button.classList.remove("pressed");
stopRemote();
};
button.addEventListener("pointerup", release);
button.addEventListener("pointercancel", release);
button.addEventListener("lostpointercapture", release);
});
document.getElementById("remote-stop").addEventListener(
"click",
() => stopRemote(true));
window.addEventListener("keydown", event => {
if (isTypingTarget(event.target))
return;
const driveKeys = [
"KeyW", "KeyS", "KeyA", "KeyD",
"ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"
];
if (!driveKeys.includes(event.code))
return;
event.preventDefault();
if (event.repeat)
return;
pressedDriveKeys.add(event.code);
driveFromKeyboard();
});
window.addEventListener("keyup", event => {
if (!pressedDriveKeys.has(event.code))
return;
event.preventDefault();
pressedDriveKeys.delete(event.code);
driveFromKeyboard();
});
window.addEventListener("blur", () => stopRemote());
document.addEventListener("visibilitychange", () => {
if (document.hidden)
stopRemote();
});
[speedScaleInput, steeringScaleInput].forEach(input => {
input.addEventListener("input", () => {
updateRemoteLabels();
if (remoteThrottle !== 0 || remoteSteering !== 0) {
sendManualControl(
remoteThrottle,
remoteSteering);
}
});
});
document.getElementById("reset-all").addEventListener(
"click",
async () => {
await fetch("/api/reset", { method: "POST" });
commandMessage.textContent = "全部仿真车已复位。";
commandMessage.classList.remove("error");
await updateVehicles();
});
document.getElementById("apply-configuration")
.addEventListener("click", applyConfiguration);
vehicleCountInput.addEventListener("change", () =>
renderLayoutRows(Number(vehicleCountInput.value)));
vehicleSelect.addEventListener("change", () => {
stopRemote();
renderCards();
renderSelectedVehicleDetail();
});
window.addEventListener("resize", resizeCanvas);
setInterval(updateVehicles, 100);
setInterval(() => {
const now = performance.now();
const seconds = (now - lastUpdateTime) / 1000;
updateRate.textContent =
`${(successfulUpdates / seconds).toFixed(0)} Hz`;
successfulUpdates = 0;
lastUpdateTime = now;
}, 1000);
function signed(value) {
return `${value >= 0 ? "+" : ""}${value.toFixed(1)}`;
}
function degToRad(value) {
return value * Math.PI / 180;
}
function radToDeg(value) {
return value * 180 / Math.PI;
}
async function initialize() {
try {
await Promise.all([
loadActions(),
loadConfiguration(),
updateVehicles()
]);
} catch {
connectionText.textContent = "仿真初始化失败";
commandMessage.textContent =
"无法读取命令或布局配置,请检查后端。";
commandMessage.classList.add("error");
}
}
resizeCanvas();
updateRemoteLabels();
initialize();
requestAnimationFrame(draw);