microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
logo

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/npm/qsharp/ux/atoms/layout.ts

730lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import { appendChildren, createSvgElements, setAttributes } from "./utils.js";
5
6// Zones will be rendered from top to bottom in array order
7type ZoneData = {
8 title: string;
9 rows: number;
10 kind: "register" | "interaction" | "measurement";
11};
12
13export type ZoneLayout = {
14 cols: number;
15 zones: ZoneData[];
16};
17
18export type TraceData = {
19 metadata: any;
20 qubits: Array<[number, number]>;
21 steps: Array<{
22 id: string | number;
23 ops: Array<string>;
24 }>;
25};
26
27// const exampleLayout: ZoneLayout = {
28// "cols": 36,
29// "zones": [
30// { "title": "Register 1", "rows": 17, "kind": "register" },
31// { "title": "Interaction Zone", "rows": 4, "kind": "interaction" },
32// { "title": "Register 2", "rows": 17, "kind": "register" },
33// { "title": "Measurement Zone", "rows": 4, "kind": "measurement" },
34// ],
35// };
36
37type Location = [number, number, SVGElement?];
38
39const qubitSize = 10;
40const zoneSpacing = 8;
41const colPadding = 20;
42const initialScale = 1.5;
43const scaleStep = 0.15;
44const speedStep = 0.75;
45const zoneBoxCornerRadius = 3;
46const doublonCornerRadius = 5;
47
48// Used when no trace data is provided to fill the qubit mappings, assuming all register
49// zones are populated with sequentially numbered qubit ids from the top left to the bottom right.
50export function fillQubitLocations(
51 layout: ZoneLayout,
52): Array<[number, number]> {
53 const qubits: Array<[number, number]> = [];
54 let currRow = 0;
55 layout.zones.forEach((zone) => {
56 for (let row = 0; row < zone.rows; ++row) {
57 if (zone.kind === "register") {
58 for (let col = 0; col < layout.cols; ++col) {
59 qubits.push([currRow, col]);
60 }
61 }
62 ++currRow;
63 }
64 });
65
66 return qubits;
67}
68
69function parseMove(op: string): { qubit: number; to: Location } | undefined {
70 const match = op.match(/move\((\d+), (\d+)\) (\d+)/);
71 if (match) {
72 const to: Location = [parseInt(match[1]), parseInt(match[2])];
73 return { qubit: parseInt(match[3]), to };
74 }
75 return undefined;
76}
77
78function parseGate(
79 op: string,
80): { gate: string; qubit: number; arg?: string } | undefined {
81 const match = op.match(/(\w+)\s*(\(.*\))? (\d+)/);
82 if (match) {
83 const gate = match[1];
84 const qubit = parseInt(match[3]);
85 const arg = match[2]
86 ? match[2].substring(1, match[2].length - 2)
87 : undefined;
88 return { gate, qubit, arg };
89 }
90}
91
92/*
93We want to build up a cache of the qubit location at each 'n' steps so that scrubbing is fast.
94Storing for each step is too large for the number of steps we want to handle. Also, the building
95of the cache can take a long time, so we want to chunk it up to avoid blocking the UI thread.
96*/
97function TraceToGetLayoutFn(trace: TraceData) {
98 const STEP_SIZE = 100; // How many steps between each cache entry
99
100 const cacheEntries = Math.ceil(trace.steps.length / STEP_SIZE);
101 const entrySize = trace.qubits.length * 2; // row and col for each qubit
102 const cache = new Uint16Array(cacheEntries * entrySize);
103
104 // Fill initial locations
105 trace.qubits.forEach((loc, idx) => {
106 cache[idx * 2] = loc[0];
107 cache[idx * 2 + 1] = loc[1];
108 });
109
110 let lastIndexProcessed = 0;
111
112 // Update the layout (which should be the prior step layout)
113 function getNextStepLayout(stepIndex: number, layout: Uint16Array) {
114 if (stepIndex <= 0 || stepIndex >= trace.steps.length) {
115 throw "Step out of range";
116 }
117 // Extract the move operations in the prior step, to apply to the prior layout
118 const moves = trace.steps[stepIndex - 1].ops
119 .map(parseMove)
120 .filter((x) => x != undefined);
121
122 // Then apply them to the layout
123 moves.forEach((move) => {
124 layout[move.qubit * 2] = move.to[0];
125 layout[move.qubit * 2 + 1] = move.to[1];
126 });
127 }
128
129 // syncRunToIndex is used to force processing up to a certain index synchronously, such as
130 // when the user is scrubbing to a point not yet processed asynchronously.
131 function processChunk(syncRunToIndex = 0) {
132 if (lastIndexProcessed >= cacheEntries - 1) {
133 return; // Done
134 }
135
136 const startPriorIndex = lastIndexProcessed * entrySize;
137 const startIndex = (lastIndexProcessed + 1) * entrySize;
138
139 // Copy to the next entry as the starting point
140 cache.copyWithin(startIndex, startPriorIndex, startPriorIndex + entrySize);
141 const targetSlice = cache.subarray(startIndex, startIndex + entrySize);
142
143 // Run each step in the chunk and apply the moves to the cache
144 for (let stepOffset = 1; stepOffset <= STEP_SIZE; ++stepOffset) {
145 const stepIndex = lastIndexProcessed * STEP_SIZE + stepOffset;
146 if (stepIndex >= trace.steps.length) break;
147 getNextStepLayout(stepIndex, targetSlice);
148 }
149
150 // Queue up the next chunk
151 ++lastIndexProcessed;
152 if (syncRunToIndex > 0) {
153 // Process synchronously up to the requested index if not done yet
154 if (lastIndexProcessed < syncRunToIndex) processChunk(syncRunToIndex);
155 } else {
156 // Process the next chunk asynchronously
157 setTimeout(processChunk, 0);
158 }
159 }
160 // Kick off the async processing
161 processChunk();
162
163 // When a layout is requested for a step, cache the response. Most of the time the following
164 // step will be requested next, so we can easily just apply one set of moves to the prior layout.
165 let lastRequestedStep = -1;
166 const lastLayout = new Uint16Array(entrySize);
167
168 function getLayoutAtStep(step: number): Uint16Array {
169 if (step < 0 || step >= trace.steps.length) {
170 throw "Step out of range";
171 }
172
173 // If the cache hasn't processed up to the required step, do so now
174 if (step >= (lastIndexProcessed + 1) * STEP_SIZE) {
175 const entryIndex = Math.floor(step / STEP_SIZE);
176 processChunk(entryIndex);
177 }
178
179 // If the step exactly matches a cache entry, return that
180 if (step % STEP_SIZE === 0) {
181 const entryIndex = step / STEP_SIZE;
182 const startIndex = entryIndex * entrySize;
183 lastLayout.set(cache.subarray(startIndex, startIndex + entrySize));
184 lastRequestedStep = step;
185 return lastLayout;
186 }
187
188 // If the same as the last requested step, just return the cached layout
189 if (step === lastRequestedStep) {
190 return lastLayout;
191 }
192
193 // Otherwise, if the last requested step was the prior step, just apply the moves
194 if (step === lastRequestedStep + 1) {
195 getNextStepLayout(step, lastLayout);
196 ++lastRequestedStep;
197 return lastLayout;
198 }
199
200 // Otherwise, find the nearest prior cache entry and apply moves from there
201 const entryIndex = Math.floor(step / STEP_SIZE);
202 const startIndex = entryIndex * entrySize;
203 lastLayout.set(cache.subarray(startIndex, startIndex + entrySize));
204 for (
205 let stepIndex = entryIndex * STEP_SIZE + 1;
206 stepIndex <= step;
207 ++stepIndex
208 ) {
209 getNextStepLayout(stepIndex, lastLayout);
210 }
211 lastRequestedStep = step;
212 return lastLayout;
213 }
214
215 return getLayoutAtStep;
216}
217
218export class Layout {
219 container: SVGSVGElement;
220 width: number;
221 height: number;
222 scale: number = initialScale;
223 qubits: Location[];
224 rowOffset: number[];
225 currentStep = 0;
226 trackParent: SVGGElement;
227 activeGates: SVGElement[] = [];
228 trace: TraceData;
229 getStepLayout: (step: number) => Uint16Array;
230 showTracks = true;
231 showDottedPath = true;
232 stepInterval = 500; // Used for playing and animations
233
234 constructor(
235 public layout: ZoneLayout,
236 trace: TraceData,
237 ) {
238 if (!trace.qubits?.length) {
239 trace.qubits = fillQubitLocations(layout);
240 }
241 this.trace = trace;
242 this.getStepLayout = TraceToGetLayoutFn(trace);
243
244 this.qubits = structuredClone(trace.qubits);
245
246 this.container = document.createElementNS(
247 "http://www.w3.org/2000/svg",
248 "svg",
249 );
250
251 const totalRows = layout.zones.reduce((prev, curr) => prev + curr.rows, 0);
252
253 this.height =
254 totalRows * qubitSize + zoneSpacing * (layout.zones.length + 1);
255 this.width = layout.cols * qubitSize + colPadding;
256
257 setAttributes(this.container, {
258 viewBox: `-5 0 ${this.width} ${this.height}`,
259 width: `${this.width * this.scale}px`,
260 height: `${this.height * this.scale}px`,
261 });
262
263 // Loop through the zones, calculating the row offsets, and rendering the zones
264 this.rowOffset = [];
265 let nextOffset = zoneSpacing;
266 let nextRowNum = 0;
267 layout.zones.forEach((zone, index) => {
268 this.renderZone(index, nextOffset, nextRowNum);
269 for (let i = 0; i < zone.rows; ++i) {
270 this.rowOffset.push(nextOffset);
271 nextOffset += qubitSize;
272 ++nextRowNum;
273 }
274 nextOffset += zoneSpacing; // Add spacing after each zone
275 });
276
277 const colNumOffset = nextOffset - 8;
278 this.renderColNums(layout.cols, colNumOffset);
279
280 // Put the track parent before the qubits, so the qubits render on top
281 this.trackParent = createSvgElements("g")[0] as SVGGElement;
282
283 this.trackParent.addEventListener("mouseover", (e) => {
284 const t = e.target as SVGElement;
285 if (t && t.dataset.qubitid) {
286 const qubitId = parseInt(t.dataset.qubitid);
287 this.highlightQubit(qubitId);
288 }
289 });
290 this.trackParent.addEventListener("mouseout", () => {
291 this.highlightQubit(null);
292 });
293
294 appendChildren(this.container, [this.trackParent]);
295
296 this.renderQubits();
297 }
298
299 private highlightQubit(qubit: number | null) {
300 if (qubit === null || qubit < 0 || qubit >= this.qubits.length) {
301 this.container
302 .querySelectorAll(".qs-atoms-qubit-highlight")
303 .forEach((elem) => {
304 elem.classList.remove("qs-atoms-qubit-highlight");
305 });
306 } else {
307 this.container
308 .querySelectorAll(`[data-qubitId="${qubit}"]`)
309 .forEach((elem) => {
310 elem.classList.add("qs-atoms-qubit-highlight");
311 });
312 }
313 }
314
315 private getOpsAtStep(step: number) {
316 if (step < 0 || step >= this.trace.steps.length) {
317 throw "Step out of range";
318 }
319 return this.trace.steps[step].ops;
320 }
321
322 private renderZone(zoneIndex: number, offset: number, firstRowNum = 0) {
323 const zoneData = this.layout.zones[zoneIndex];
324 const g = createSvgElements("g")[0];
325 setAttributes(g, {
326 transform: `translate(0 ${offset})`,
327 class: "qs-atoms-zonebox",
328 });
329
330 if (zoneData.kind !== "interaction") {
331 // For non-interaction zones we draw one big rounded rectangle with lines between qubit rows & cols
332 const rect = createSvgElements("rect")[0];
333 setAttributes(rect, {
334 x: "0",
335 y: "0",
336 width: `${this.layout.cols * qubitSize}`,
337 height: `${zoneData.rows * qubitSize}`,
338 rx: `${zoneBoxCornerRadius}`,
339 });
340 appendChildren(g, [rect]);
341
342 // Draw the lines between the rows
343 for (let i = 1; i < zoneData.rows; i++) {
344 const path = createSvgElements("path")[0];
345 setAttributes(path, {
346 d: `M 0,${i * qubitSize} h${this.layout.cols * qubitSize}`,
347 });
348 appendChildren(g, [path]);
349 }
350 // Draw the lines between the columns
351 for (let i = 1; i < this.layout.cols; i++) {
352 const path = createSvgElements("path")[0];
353 setAttributes(path, {
354 d: `M ${i * qubitSize},0 v${zoneData.rows * qubitSize}`,
355 });
356 appendChildren(g, [path]);
357 }
358 } else {
359 // For the interaction zone draw each doublon
360 for (let row = 0; row < zoneData.rows; ++row) {
361 for (let i = 0; i < this.layout.cols; i += 2) {
362 const rect = createSvgElements("rect")[0];
363 setAttributes(rect, {
364 x: `${i * qubitSize}`,
365 y: `${row * qubitSize}`,
366 width: `${qubitSize * 2}`,
367 height: `${qubitSize}`,
368 rx: `${doublonCornerRadius}`,
369 });
370 const path = createSvgElements("path")[0];
371 setAttributes(path, {
372 d: `M ${(i + 1) * qubitSize},${row * qubitSize} v${qubitSize}`,
373 });
374 appendChildren(g, [rect, path]);
375 }
376 }
377 }
378
379 // Number the rows
380 for (let i = 0; i < zoneData.rows; ++i) {
381 const rowNum = firstRowNum + i;
382 const label = createSvgElements("text")[0];
383 setAttributes(label, {
384 x: `${this.layout.cols * qubitSize + 5}`,
385 y: `${i * qubitSize + 5}`,
386 class: "qs-atoms-label",
387 });
388 label.textContent = `${rowNum}`;
389 appendChildren(g, [label]);
390 }
391
392 // Draw the title
393 const text = createSvgElements("text")[0];
394 setAttributes(text, {
395 x: "1",
396 y: "-1",
397 class: "qs-atoms-zone-text",
398 });
399 text.textContent = zoneData.title;
400
401 appendChildren(g, [text]);
402 appendChildren(this.container, [g]);
403 }
404
405 private renderQubits() {
406 const elems = this.qubits.map((location, index) => {
407 const [x, y] = this.getQubitCenter(index);
408
409 // Safari has an issue animating multiple attributes concurrently, which we need
410 // to do to move the qubit (animate 'cx' and 'cy'), so instead set cx and cy to 0
411 // and position the qubit with a transform (see https://stackoverflow.com/a/72022385/1674945)
412
413 const circle = createSvgElements("circle")[0];
414 setAttributes(circle, {
415 cx: `0`,
416 cy: `0`,
417 r: `2`,
418 class: "qs-atoms-qubit",
419 "data-qubitid": `${index}`,
420 });
421 circle.addEventListener("mouseover", () => this.highlightQubit(index));
422 circle.addEventListener("mouseout", () => this.highlightQubit(null));
423 // Animation sets the transform as a style attribute, not an element attribute.
424 // Also note, when animating the CSS it requires the 'px' length type (unlike the attribute).
425 circle.style.transform = `translate(${x}px, ${y}px)`;
426 location[2] = circle;
427 return circle;
428 });
429
430 appendChildren(this.container, elems);
431 }
432
433 private renderColNums(cols: number, offset: number) {
434 const g = createSvgElements("g")[0];
435 setAttributes(g, {
436 transform: `translate(0 ${offset})`,
437 });
438 // Number the columns
439 for (let i = 0; i < cols; ++i) {
440 const label = createSvgElements("text")[0];
441 setAttributes(label, {
442 x: `${i * qubitSize + 5}`,
443 y: `5`,
444 class: "qs-atoms-label",
445 });
446 label.textContent = `${i}`;
447 appendChildren(g, [label]);
448 }
449 appendChildren(this.container, [g]);
450 }
451
452 renderGateOnQubit(qubit: number, gate: string, arg?: string) {
453 if (gate == "RESET") gate = "R";
454 const [x, y] = this.getQubitCenter(qubit);
455
456 const gateClass =
457 gate === "MZ"
458 ? "qs-atoms-gate qs-atoms-gate-mz"
459 : gate === "R"
460 ? "qs-atoms-gate qs-atoms-gate-reset"
461 : "qs-atoms-gate";
462
463 const g = createSvgElements("g")[0];
464 setAttributes(g, {
465 transform: `translate(${x - qubitSize / 2} ${y - qubitSize / 2})`,
466 class: "qs-atoms-gate",
467 });
468
469 if (gate === "CZ") {
470 // Render the rounded doublon box in a bright color and x--x inside
471 const [rect, path, leftDot, rightDot] = createSvgElements(
472 "rect",
473 "path",
474 "circle",
475 "circle",
476 );
477 setAttributes(rect, {
478 x: `0`,
479 y: "0",
480 width: `${qubitSize * 2}`,
481 height: `${qubitSize}`,
482 rx: `${doublonCornerRadius}`,
483 class: "qs-atoms-zonebox qs-atoms-gate-cz",
484 });
485 // <path d= "M45,5 h10" stroke-width="1.5" stroke="black"/>
486 // <circle cx="45" cy="5" r="2" stroke-width="0" fill="#123" />
487 // <circle cx="55" cy="5" r="2" stroke-width="0" fill="#123" />
488 setAttributes(path, {
489 fill: "none",
490 stroke: "black",
491 "stroke-width": "1.5",
492 d: "M5,5 h10",
493 });
494 setAttributes(leftDot, {
495 cx: "5",
496 cy: "5",
497 r: "2",
498 "stroke-width": "0",
499 fill: "#123",
500 });
501 setAttributes(rightDot, {
502 cx: "15",
503 cy: "5",
504 r: "2",
505 "stroke-width": "0",
506 fill: "#123",
507 });
508 appendChildren(g, [rect, path, leftDot, rightDot]);
509 } else {
510 const [rect, text] = createSvgElements("rect", "text");
511 setAttributes(rect, {
512 x: "0.5",
513 y: "0.5",
514 width: `${qubitSize - 1}`,
515 height: `${qubitSize - 1}`,
516 class: gateClass,
517 });
518 setAttributes(text, {
519 x: "5",
520 y: arg ? "2.75" : "5",
521 class: "qs-atoms-gate-text",
522 });
523 text.textContent = gate;
524
525 appendChildren(g, [rect, text]);
526
527 if (arg) {
528 const argText = createSvgElements("text")[0];
529 setAttributes(argText, {
530 x: "5",
531 y: "7",
532 class: "qs-atoms-gate-text qs-atoms-gate-text-small",
533 textLength: "8",
534 });
535 text.classList.add("qs-atoms-gate-text-small");
536 argText.textContent = arg;
537 appendChildren(g, [argText]);
538 }
539 }
540
541 appendChildren(this.container, [g]);
542 this.activeGates.push(g);
543 }
544
545 clearGates() {
546 this.activeGates.forEach((gate) => {
547 gate.parentElement?.removeChild(gate);
548 });
549 // TODO: Clear doublons too
550 this.activeGates = [];
551 }
552
553 zoomIn() {
554 this.scale += scaleStep * this.scale;
555 setAttributes(this.container, {
556 width: `${this.width * this.scale}px`,
557 height: `${this.height * this.scale}px`,
558 });
559 }
560
561 zoomOut() {
562 this.scale -= scaleStep * this.scale;
563 setAttributes(this.container, {
564 width: `${this.width * this.scale}px`,
565 height: `${this.height * this.scale}px`,
566 });
567 }
568
569 faster() {
570 this.stepInterval = this.stepInterval * speedStep;
571 }
572
573 slower() {
574 this.stepInterval = this.stepInterval / speedStep;
575 }
576
577 cycleAnimation() {
578 if (!this.showTracks) {
579 this.showTracks = true;
580 this.showDottedPath = true;
581 } else if (this.showTracks && this.showDottedPath) {
582 this.showDottedPath = false;
583 } else {
584 this.showTracks = false;
585 this.showDottedPath = false;
586 }
587 this.gotoStep(this.currentStep);
588 }
589
590 getQubitRowOffset(row: number) {
591 return this.rowOffset[row];
592 }
593
594 getLocationCenter(row: number, col: number): [number, number] {
595 const x = col * qubitSize + qubitSize / 2;
596 const y = this.getQubitRowOffset(row) + qubitSize / 2;
597 return [x, y];
598 }
599
600 getQubitCenter(qubit: number): [number, number] {
601 if (this.qubits[qubit] == undefined) {
602 throw "Qubit not found";
603 }
604
605 const [row, col] = this.qubits[qubit];
606 return this.getLocationCenter(row, col);
607 }
608
609 gotoStep(step: number) {
610 // Remove prior rendering's gates, trails, or remaining animations
611 this.clearGates();
612 this.trackParent.replaceChildren();
613 this.container.getAnimations({ subtree: true }).forEach((anim) => {
614 anim.cancel();
615 });
616
617 const forwards = step > this.currentStep;
618 this.currentStep = step;
619
620 // When on step 0, just layout the qubits per index 0
621 // When on step 1, layout per index 0 then apply the gates/moves per index 0
622 // When on step 2, layout per index 1 then apply the gates/moves per index 1
623 // etc. until when on step n + 1, layout per index n and apply per index n
624 const qubitLocationIndex = step === 0 ? 0 : step - 1;
625
626 // Update all qubit locations
627 const qubitLayout = this.getStepLayout(qubitLocationIndex);
628 const qubitCount = qubitLayout.length / 2;
629 for (let idx = 0; idx < qubitCount; ++idx) {
630 const elem = this.qubits[idx][2];
631 if (elem === undefined) {
632 throw "Invalid qubit index in step";
633 }
634 const loc: Location = [qubitLayout[idx * 2], qubitLayout[idx * 2 + 1]];
635 this.qubits[idx] = [loc[0], loc[1], elem]; // Update the location
636
637 // Get the offset for the location and move it there
638 const [x, y] = this.getQubitCenter(idx);
639 elem.style.transform = `translate(${x}px, ${y}px)`;
640 }
641
642 // Now apply the ops
643 if (step > 0) {
644 const duration = forwards ? this.stepInterval / 2 : 0;
645 const ops = this.getOpsAtStep(qubitLocationIndex);
646 let trailId = 0;
647 ops.forEach((op) => {
648 const move = parseMove(op);
649 if (move) {
650 // Apply the move animation
651 const [oldX, oldY] = this.getQubitCenter(move.qubit);
652 const [newX, newY] = this.getLocationCenter(move.to[0], move.to[1]);
653 const qubit = this.qubits[move.qubit][2];
654 if (!qubit) throw "Invalid qubit index";
655 if (this.showTracks) {
656 if (this.showDottedPath) {
657 // Render a hollow circle at the start position
658 const [startCircle, trail] = createSvgElements("circle", "line");
659 setAttributes(startCircle, {
660 cx: `${oldX}`,
661 cy: `${oldY}`,
662 class: `qs-atoms-qubit-trail-start`,
663 "data-qubitid": `${move.qubit}`,
664 });
665
666 const id = `dotted-trail-${trailId++}`;
667 setAttributes(trail, {
668 id,
669 x1: `${oldX}`,
670 y1: `${oldY}`,
671 x2: `${newX}`,
672 y2: `${newY}`,
673 class: "qs-atoms-qubit-trail",
674 "data-qubitid": `${move.qubit}`,
675 });
676 appendChildren(this.trackParent, [trail, startCircle]);
677 } else {
678 const id = `gradient-${trailId++}`;
679 // Draw the path of any qubit movements
680 const [gradient, trail] = createSvgElements(
681 "linearGradient",
682 "line",
683 );
684 setAttributes(gradient, {
685 id,
686 gradientUnits: "userSpaceOnUse",
687 x1: `${oldX}`,
688 y1: `${oldY}`,
689 x2: `${newX}`,
690 y2: `${newY}`,
691 });
692 gradient.innerHTML = `<stop offset="0%" stop-color="gray" stop-opacity="0.2"/><stop offset="100%" stop-color="gray" stop-opacity="0.8"/>`;
693 setAttributes(trail, {
694 x1: `${oldX}`,
695 y1: `${oldY}`,
696 x2: `${newX}`,
697 y2: `${newY}`,
698 style: `stroke-width: 2px; stroke: url(#${id})`,
699 });
700 appendChildren(this.trackParent, [gradient, trail]);
701 }
702 }
703 qubit
704 .animate(
705 [
706 { transform: `translate(${oldX}px, ${oldY}px)` },
707 { transform: `translate(${newX}px, ${newY}px)` },
708 ],
709 {
710 duration: this.showDottedPath ? 50 : duration,
711 fill: "forwards",
712 easing: "ease",
713 },
714 )
715 .finished.then((anim) => {
716 anim.commitStyles();
717 anim.cancel();
718 });
719 // TODO: Check if you can/should cancel when scrubbing
720 } else {
721 // Wasn't a move, so render the gate
722 const gate = parseGate(op);
723 if (!gate) throw `Invalid gate: ${op}`;
724 const arg = gate.arg ? gate.arg.substring(0, 4) : undefined;
725 this.renderGateOnQubit(gate.qubit, gate.gate.toUpperCase(), arg);
726 }
727 });
728 }
729 }
730}
731