blob: 1e08359af010cefceebe2d57b63e29f9e9a79e88 [file] [log] [blame]
Maxwell Hendersonad312342023-01-10 12:07:47 -08001import {ByteBuffer} from 'flatbuffers';
James Kuszmaulf7b5d622023-03-11 15:14:53 -08002
3import {ClientStatistics} from '../../aos/network/message_bridge_client_generated'
4import {ServerStatistics, State as ConnectionState} from '../../aos/network/message_bridge_server_generated'
Maxwell Hendersonad312342023-01-10 12:07:47 -08005import {Connection} from '../../aos/network/www/proxy';
Milo Lin72fb9012023-03-10 19:53:19 -08006import {ZeroingError} from '../../frc971/control_loops/control_loops_generated';
James Kuszmaulf7b5d622023-03-11 15:14:53 -08007import {Status as DrivetrainStatus} from '../../frc971/control_loops/drivetrain/drivetrain_status_generated';
8import {LocalizerOutput} from '../../frc971/control_loops/drivetrain/localization/localizer_output_generated';
9import {ArmState, ArmStatus, EndEffectorState, Status as SuperstructureStatus} from '../control_loops/superstructure/superstructure_status_generated'
10import {RejectionReason} from '../localizer/status_generated';
11import {TargetEstimateDebug, Visualization} from '../localizer/visualization_generated';
12import {Class} from '../vision/game_pieces_generated'
Maxwell Hendersonad312342023-01-10 12:07:47 -080013
14import {FIELD_LENGTH, FIELD_WIDTH, FT_TO_M, IN_TO_M} from './constants';
15
16// (0,0) is field center, +X is toward red DS
17const FIELD_SIDE_Y = FIELD_WIDTH / 2;
18const FIELD_EDGE_X = FIELD_LENGTH / 2;
19
James Kuszmaulfb894572023-02-23 17:25:06 -080020const ROBOT_WIDTH = 25 * IN_TO_M;
21const ROBOT_LENGTH = 32 * IN_TO_M;
Maxwell Hendersonad312342023-01-10 12:07:47 -080022
23const PI_COLORS = ['#ff00ff', '#ffff00', '#00ffff', '#ffa500'];
James Kuszmaulfb894572023-02-23 17:25:06 -080024const PIS = ['pi1', 'pi2', 'pi3', 'pi4'];
Maxwell Hendersonad312342023-01-10 12:07:47 -080025
26export class FieldHandler {
27 private canvas = document.createElement('canvas');
James Kuszmaulfb894572023-02-23 17:25:06 -080028 private localizerOutput: LocalizerOutput|null = null;
Maxwell Hendersonad312342023-01-10 12:07:47 -080029 private drivetrainStatus: DrivetrainStatus|null = null;
Milo Line6571c02023-03-04 21:08:20 -080030 private superstructureStatus: SuperstructureStatus|null = null;
Maxwell Hendersonad312342023-01-10 12:07:47 -080031
32 // Image information indexed by timestamp (seconds since the epoch), so that
33 // we can stop displaying images after a certain amount of time.
James Kuszmaulfb894572023-02-23 17:25:06 -080034 private localizerImageMatches = new Map<number, Visualization>();
35 private x: HTMLElement = (document.getElementById('x') as HTMLElement);
Maxwell Hendersonad312342023-01-10 12:07:47 -080036 private y: HTMLElement = (document.getElementById('y') as HTMLElement);
37 private theta: HTMLElement =
38 (document.getElementById('theta') as HTMLElement);
Maxwell Hendersonad312342023-01-10 12:07:47 -080039 private imagesAcceptedCounter: HTMLElement =
40 (document.getElementById('images_accepted') as HTMLElement);
James Kuszmaulfb894572023-02-23 17:25:06 -080041 private rejectionReasonCells: HTMLElement[] = [];
James Kuszmaulf7b5d622023-03-11 15:14:53 -080042 private messageBridgeDiv: HTMLElement =
43 (document.getElementById('message_bridge_status') as HTMLElement);
44 private clientStatuses = new Map<string, HTMLElement>();
45 private serverStatuses = new Map<string, HTMLElement>();
Maxwell Hendersonad312342023-01-10 12:07:47 -080046 private fieldImage: HTMLImageElement = new Image();
Milo Line6571c02023-03-04 21:08:20 -080047 private endEffectorState: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080048 (document.getElementById('end_effector_state') as HTMLElement);
Milo Line6571c02023-03-04 21:08:20 -080049 private wrist: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080050 (document.getElementById('wrist') as HTMLElement);
Milo Line6571c02023-03-04 21:08:20 -080051 private armState: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080052 (document.getElementById('arm_state') as HTMLElement);
Milo Line6571c02023-03-04 21:08:20 -080053 private gamePiece: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080054 (document.getElementById('game_piece') as HTMLElement);
55 private armX: HTMLElement = (document.getElementById('arm_x') as HTMLElement);
56 private armY: HTMLElement = (document.getElementById('arm_y') as HTMLElement);
Milo Line6571c02023-03-04 21:08:20 -080057 private circularIndex: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080058 (document.getElementById('arm_circular_index') as HTMLElement);
Milo Line6571c02023-03-04 21:08:20 -080059 private roll: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080060 (document.getElementById('arm_roll') as HTMLElement);
Milo Line6571c02023-03-04 21:08:20 -080061 private proximal: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080062 (document.getElementById('arm_proximal') as HTMLElement);
Milo Line6571c02023-03-04 21:08:20 -080063 private distal: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080064 (document.getElementById('arm_distal') as HTMLElement);
Milo Lin72fb9012023-03-10 19:53:19 -080065 private zeroingFaults: HTMLElement =
James Kuszmaulf7b5d622023-03-11 15:14:53 -080066 (document.getElementById('zeroing_faults') as HTMLElement);
67 _
Maxwell Hendersonad312342023-01-10 12:07:47 -080068
69 constructor(private readonly connection: Connection) {
70 (document.getElementById('field') as HTMLElement).appendChild(this.canvas);
71
James Kuszmaulf7b5d622023-03-11 15:14:53 -080072 this.fieldImage.src = '2023.png';
James Kuszmaulfb894572023-02-23 17:25:06 -080073
74 for (const value in RejectionReason) {
75 // Typescript generates an iterator that produces both numbers and
76 // strings... don't do anything on the string iterations.
77 if (isNaN(Number(value))) {
78 continue;
79 }
80 const row = document.createElement('div');
81 const nameCell = document.createElement('div');
82 nameCell.innerHTML = RejectionReason[value];
83 row.appendChild(nameCell);
84 const valueCell = document.createElement('div');
85 valueCell.innerHTML = 'NA';
86 this.rejectionReasonCells.push(valueCell);
87 row.appendChild(valueCell);
88 document.getElementById('vision_readouts').appendChild(row);
89 }
Maxwell Hendersonad312342023-01-10 12:07:47 -080090
91 for (let ii = 0; ii < PI_COLORS.length; ++ii) {
92 const legendEntry = document.createElement('div');
93 legendEntry.style.color = PI_COLORS[ii];
94 legendEntry.innerHTML = 'PI' + (ii + 1).toString()
95 document.getElementById('legend').appendChild(legendEntry);
96 }
97
98 this.connection.addConfigHandler(() => {
99 // Visualization message is reliable so that we can see *all* the vision
100 // matches.
James Kuszmaulfb894572023-02-23 17:25:06 -0800101 for (const pi in PIS) {
102 this.connection.addReliableHandler(
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800103 '/' + PIS[pi] + '/camera', 'y2023.localizer.Visualization',
James Kuszmaulfb894572023-02-23 17:25:06 -0800104 (data) => {
105 this.handleLocalizerDebug(pi, data);
106 });
107 }
Maxwell Hendersonad312342023-01-10 12:07:47 -0800108 this.connection.addHandler(
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800109 '/drivetrain', 'frc971.control_loops.drivetrain.Status', (data) => {
Maxwell Hendersonad312342023-01-10 12:07:47 -0800110 this.handleDrivetrainStatus(data);
111 });
112 this.connection.addHandler(
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800113 '/localizer', 'frc971.controls.LocalizerOutput', (data) => {
James Kuszmaulfb894572023-02-23 17:25:06 -0800114 this.handleLocalizerOutput(data);
Maxwell Hendersonad312342023-01-10 12:07:47 -0800115 });
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800116 this.connection.addHandler(
117 '/superstructure', 'y2023.control_loops.superstructure.Status',
118 (data) => {this.handleSuperstructureStatus(data)});
119 this.connection.addHandler(
120 '/aos', 'aos.message_bridge.ServerStatistics',
121 (data) => {this.handleServerStatistics(data)});
122 this.connection.addHandler(
123 '/aos', 'aos.message_bridge.ClientStatistics',
124 (data) => {this.handleClientStatistics(data)});
Maxwell Hendersonad312342023-01-10 12:07:47 -0800125 });
126 }
127
James Kuszmaulfb894572023-02-23 17:25:06 -0800128 private handleLocalizerDebug(pi: string, data: Uint8Array): void {
129 const now = Date.now() / 1000.0;
130
131 const fbBuffer = new ByteBuffer(data);
132 this.localizerImageMatches.set(
133 now, Visualization.getRootAsVisualization(fbBuffer));
134
135 const debug = this.localizerImageMatches.get(now);
136
137 if (debug.statistics()) {
138 if (debug.statistics().rejectionReasonsLength() ==
139 this.rejectionReasonCells.length) {
140 for (let ii = 0; ii < debug.statistics().rejectionReasonsLength();
141 ++ii) {
142 this.rejectionReasonCells[ii].innerHTML =
143 debug.statistics().rejectionReasons(ii).count().toString();
144 }
145 } else {
146 console.error('Unexpected number of rejection reasons in counter.');
147 }
148 }
149 }
150
151 private handleLocalizerOutput(data: Uint8Array): void {
152 const fbBuffer = new ByteBuffer(data);
153 this.localizerOutput = LocalizerOutput.getRootAsLocalizerOutput(fbBuffer);
154 }
155
Maxwell Hendersonad312342023-01-10 12:07:47 -0800156 private handleDrivetrainStatus(data: Uint8Array): void {
157 const fbBuffer = new ByteBuffer(data);
158 this.drivetrainStatus = DrivetrainStatus.getRootAsStatus(fbBuffer);
159 }
160
Milo Line6571c02023-03-04 21:08:20 -0800161 private handleSuperstructureStatus(data: Uint8Array): void {
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800162 const fbBuffer = new ByteBuffer(data);
163 this.superstructureStatus = SuperstructureStatus.getRootAsStatus(fbBuffer);
164 }
165
166 private populateNodeConnections(nodeName: string): void {
167 const row = document.createElement('div');
168 this.messageBridgeDiv.appendChild(row);
169 const nodeDiv = document.createElement('div');
170 nodeDiv.innerHTML = nodeName;
171 row.appendChild(nodeDiv);
172 const clientDiv = document.createElement('div');
173 clientDiv.innerHTML = 'N/A';
174 row.appendChild(clientDiv);
175 const serverDiv = document.createElement('div');
176 serverDiv.innerHTML = 'N/A';
177 row.appendChild(serverDiv);
178 this.serverStatuses.set(nodeName, serverDiv);
179 this.clientStatuses.set(nodeName, clientDiv);
180 }
181
182 private setCurrentNodeState(element: HTMLElement, state: ConnectionState):
183 void {
184 if (state === ConnectionState.CONNECTED) {
185 element.innerHTML = ConnectionState[state];
186 element.classList.remove('faulted');
187 element.classList.add('connected');
188 } else {
189 element.innerHTML = ConnectionState[state];
190 element.classList.remove('connected');
191 element.classList.add('faulted');
192 }
193 }
194
195 private handleServerStatistics(data: Uint8Array): void {
196 const fbBuffer = new ByteBuffer(data);
197 const serverStatistics =
198 ServerStatistics.getRootAsServerStatistics(fbBuffer);
199
200 for (let ii = 0; ii < serverStatistics.connectionsLength(); ++ii) {
201 const connection = serverStatistics.connections(ii);
202 const nodeName = connection.node().name();
203 if (!this.serverStatuses.has(nodeName)) {
204 this.populateNodeConnections(nodeName);
205 }
206 this.setCurrentNodeState(
207 this.serverStatuses.get(nodeName), connection.state());
208 }
209 }
210
211 private handleClientStatistics(data: Uint8Array): void {
212 const fbBuffer = new ByteBuffer(data);
213 const clientStatistics =
214 ClientStatistics.getRootAsClientStatistics(fbBuffer);
215
216 for (let ii = 0; ii < clientStatistics.connectionsLength(); ++ii) {
217 const connection = clientStatistics.connections(ii);
218 const nodeName = connection.node().name();
219 if (!this.clientStatuses.has(nodeName)) {
220 this.populateNodeConnections(nodeName);
221 }
222 this.setCurrentNodeState(
223 this.clientStatuses.get(nodeName), connection.state());
224 }
Milo Line6571c02023-03-04 21:08:20 -0800225 }
226
Maxwell Hendersonad312342023-01-10 12:07:47 -0800227 drawField(): void {
228 const ctx = this.canvas.getContext('2d');
229 ctx.save();
James Kuszmaulfb894572023-02-23 17:25:06 -0800230 ctx.scale(1.0, -1.0);
Maxwell Hendersonad312342023-01-10 12:07:47 -0800231 ctx.drawImage(
232 this.fieldImage, 0, 0, this.fieldImage.width, this.fieldImage.height,
233 -FIELD_EDGE_X, -FIELD_SIDE_Y, FIELD_LENGTH, FIELD_WIDTH);
234 ctx.restore();
235 }
236
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800237 drawCamera(x: number, y: number, theta: number, color: string = 'blue'):
238 void {
Maxwell Hendersonad312342023-01-10 12:07:47 -0800239 const ctx = this.canvas.getContext('2d');
240 ctx.save();
241 ctx.translate(x, y);
242 ctx.rotate(theta);
243 ctx.strokeStyle = color;
244 ctx.beginPath();
245 ctx.moveTo(0.5, 0.5);
246 ctx.lineTo(0, 0);
Maxwell Hendersonad312342023-01-10 12:07:47 -0800247 ctx.lineTo(0.5, -0.5);
248 ctx.stroke();
249 ctx.beginPath();
250 ctx.arc(0, 0, 0.25, -Math.PI / 4, Math.PI / 4);
251 ctx.stroke();
252 ctx.restore();
253 }
254
255 drawRobot(
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800256 x: number, y: number, theta: number, color: string = 'blue',
257 dashed: boolean = false): void {
Maxwell Hendersonad312342023-01-10 12:07:47 -0800258 const ctx = this.canvas.getContext('2d');
259 ctx.save();
260 ctx.translate(x, y);
261 ctx.rotate(theta);
262 ctx.strokeStyle = color;
263 ctx.lineWidth = ROBOT_WIDTH / 10.0;
264 if (dashed) {
265 ctx.setLineDash([0.05, 0.05]);
266 } else {
267 // Empty array = solid line.
268 ctx.setLineDash([]);
269 }
270 ctx.rect(-ROBOT_LENGTH / 2, -ROBOT_WIDTH / 2, ROBOT_LENGTH, ROBOT_WIDTH);
271 ctx.stroke();
272
273 // Draw line indicating which direction is forwards on the robot.
274 ctx.beginPath();
275 ctx.moveTo(0, 0);
James Kuszmaulfb894572023-02-23 17:25:06 -0800276 ctx.lineTo(ROBOT_LENGTH / 2.0, 0);
Maxwell Hendersonad312342023-01-10 12:07:47 -0800277 ctx.stroke();
278
279 ctx.restore();
280 }
281
282 setZeroing(div: HTMLElement): void {
283 div.innerHTML = 'zeroing';
284 div.classList.remove('faulted');
285 div.classList.add('zeroing');
286 div.classList.remove('near');
287 }
288
Milo Line6571c02023-03-04 21:08:20 -0800289 setEstopped(div: HTMLElement): void {
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800290 div.innerHTML = 'estopped';
291 div.classList.add('faulted');
292 div.classList.remove('zeroing');
293 div.classList.remove('near');
Milo Line6571c02023-03-04 21:08:20 -0800294 }
295
296 setTargetValue(
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800297 div: HTMLElement, target: number, val: number, tolerance: number): void {
298 div.innerHTML = val.toFixed(4);
299 div.classList.remove('faulted');
300 div.classList.remove('zeroing');
301 if (Math.abs(target - val) < tolerance) {
302 div.classList.add('near');
303 } else {
304 div.classList.remove('near');
305 }
Milo Line6571c02023-03-04 21:08:20 -0800306 }
307
Maxwell Hendersonad312342023-01-10 12:07:47 -0800308 setValue(div: HTMLElement, val: number): void {
309 div.innerHTML = val.toFixed(4);
310 div.classList.remove('faulted');
311 div.classList.remove('zeroing');
312 div.classList.remove('near');
313 }
314
315 draw(): void {
316 this.reset();
317 this.drawField();
318
319 // Draw the matches with debugging information from the localizer.
320 const now = Date.now() / 1000.0;
James Kuszmaulfb894572023-02-23 17:25:06 -0800321
Milo Line6571c02023-03-04 21:08:20 -0800322 if (this.superstructureStatus) {
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800323 this.endEffectorState.innerHTML =
324 EndEffectorState[this.superstructureStatus.endEffectorState()];
325 if (!this.superstructureStatus.wrist() ||
326 !this.superstructureStatus.wrist().zeroed()) {
327 this.setZeroing(this.wrist);
328 } else if (this.superstructureStatus.wrist().estopped()) {
329 this.setEstopped(this.wrist);
330 } else {
331 this.setTargetValue(
332 this.wrist,
333 this.superstructureStatus.wrist().unprofiledGoalPosition(),
334 this.superstructureStatus.wrist().estimatorState().position(),
335 1e-3);
336 }
337 this.armState.innerHTML =
338 ArmState[this.superstructureStatus.arm().state()];
339 this.gamePiece.innerHTML = Class[this.superstructureStatus.gamePiece()];
340 this.armX.innerHTML = this.superstructureStatus.arm().armX().toFixed(2);
341 this.armY.innerHTML = this.superstructureStatus.arm().armY().toFixed(2);
342 this.circularIndex.innerHTML =
343 this.superstructureStatus.arm().armCircularIndex().toFixed(0);
344 this.roll.innerHTML = this.superstructureStatus.arm()
345 .rollJointEstimatorState()
346 .position()
347 .toFixed(2);
348 this.proximal.innerHTML = this.superstructureStatus.arm()
349 .proximalEstimatorState()
350 .position()
351 .toFixed(2);
352 this.distal.innerHTML = this.superstructureStatus.arm()
353 .distalEstimatorState()
354 .position()
355 .toFixed(2);
356 let zeroingErrors: string = 'Roll Joint Errors:' +
357 '<br/>';
358 for (let i = 0; i < this.superstructureStatus.arm()
359 .rollJointEstimatorState()
360 .errors.length;
361 i++) {
362 zeroingErrors += ZeroingError[this.superstructureStatus.arm()
363 .rollJointEstimatorState()
364 .errors(i)] +
365 '<br/>';
366 }
367 zeroingErrors += '<br/>' +
368 'Proximal Joint Errors:' +
369 '<br/>';
370 for (let i = 0; i < this.superstructureStatus.arm()
371 .proximalEstimatorState()
372 .errors.length;
373 i++) {
374 zeroingErrors += ZeroingError[this.superstructureStatus.arm()
375 .proximalEstimatorState()
376 .errors(i)] +
377 '<br/>';
378 }
379 zeroingErrors += '<br/>' +
380 'Distal Joint Errors:' +
381 '<br/>';
382 for (let i = 0; i <
383 this.superstructureStatus.arm().distalEstimatorState().errors.length;
384 i++) {
385 zeroingErrors += ZeroingError[this.superstructureStatus.arm()
386 .distalEstimatorState()
387 .errors(i)] +
388 '<br/>';
389 }
390 zeroingErrors += '<br/>' +
391 'Wrist Errors:' +
392 '<br/>';
393 for (let i = 0;
394 i < this.superstructureStatus.wrist().estimatorState().errors.length;
395 i++) {
396 zeroingErrors += ZeroingError[this.superstructureStatus.wrist()
397 .estimatorState()
398 .errors(i)] +
399 '<br/>';
400 }
401 this.zeroingFaults.innerHTML = zeroingErrors;
Milo Line6571c02023-03-04 21:08:20 -0800402 }
403
Maxwell Hendersonad312342023-01-10 12:07:47 -0800404 if (this.drivetrainStatus && this.drivetrainStatus.trajectoryLogging()) {
405 this.drawRobot(
406 this.drivetrainStatus.trajectoryLogging().x(),
407 this.drivetrainStatus.trajectoryLogging().y(),
James Kuszmaulf7b5d622023-03-11 15:14:53 -0800408 this.drivetrainStatus.trajectoryLogging().theta(), '#000000FF',
Maxwell Hendersonad312342023-01-10 12:07:47 -0800409 false);
410 }
411
James Kuszmaulfb894572023-02-23 17:25:06 -0800412 if (this.localizerOutput) {
413 if (!this.localizerOutput.zeroed()) {
414 this.setZeroing(this.x);
415 this.setZeroing(this.y);
416 this.setZeroing(this.theta);
417 } else {
418 this.setValue(this.x, this.localizerOutput.x());
419 this.setValue(this.y, this.localizerOutput.y());
420 this.setValue(this.theta, this.localizerOutput.theta());
421 }
422
423 this.drawRobot(
424 this.localizerOutput.x(), this.localizerOutput.y(),
425 this.localizerOutput.theta());
426
427 this.imagesAcceptedCounter.innerHTML =
428 this.localizerOutput.imageAcceptedCount().toString();
429 }
430
431 for (const [time, value] of this.localizerImageMatches) {
432 const age = now - time;
433 const kRemovalAge = 1.0;
434 if (age > kRemovalAge) {
435 this.localizerImageMatches.delete(time);
436 continue;
437 }
438 const kMaxImageAlpha = 0.5;
439 const ageAlpha = kMaxImageAlpha * (kRemovalAge - age) / kRemovalAge
440 for (let i = 0; i < value.targetsLength(); i++) {
441 const imageDebug = value.targets(i);
442 const x = imageDebug.impliedRobotX();
443 const y = imageDebug.impliedRobotY();
444 const theta = imageDebug.impliedRobotTheta();
445 const cameraX = imageDebug.cameraX();
446 const cameraY = imageDebug.cameraY();
447 const cameraTheta = imageDebug.cameraTheta();
448 const accepted = imageDebug.accepted();
449 // Make camera readings fade over time.
450 const alpha = Math.round(255 * ageAlpha).toString(16).padStart(2, '0');
451 const dashed = false;
James Kuszmaulfb894572023-02-23 17:25:06 -0800452 const cameraRgb = PI_COLORS[imageDebug.camera()];
453 const cameraRgba = cameraRgb + alpha;
James Kuszmaul122a22b2023-02-25 18:14:15 -0800454 this.drawRobot(x, y, theta, cameraRgba, dashed);
James Kuszmaulfb894572023-02-23 17:25:06 -0800455 this.drawCamera(cameraX, cameraY, cameraTheta, cameraRgba);
456 }
457 }
458
Maxwell Hendersonad312342023-01-10 12:07:47 -0800459 window.requestAnimationFrame(() => this.draw());
460 }
461
462 reset(): void {
463 const ctx = this.canvas.getContext('2d');
464 ctx.setTransform(1, 0, 0, 1, 0, 0);
465 const size = window.innerHeight * 0.9;
466 ctx.canvas.height = size;
467 const width = size / 2 + 20;
468 ctx.canvas.width = width;
469 ctx.clearRect(0, 0, size, width);
470
471 // Translate to center of display.
472 ctx.translate(width / 2, size / 2);
473 // Coordinate system is:
474 // x -> forward.
475 // y -> to the left.
476 ctx.rotate(-Math.PI / 2);
477 ctx.scale(1, -1);
478
479 const M_TO_PX = (size - 10) / FIELD_LENGTH;
480 ctx.scale(M_TO_PX, M_TO_PX);
481 ctx.lineWidth = 1 / M_TO_PX;
482 }
483}