Skip to content

Commit 1d5f03b

Browse files
committed
Added Zoom+pan for qubit grid (left view)
+ Added vertical scroll for timeline (right) view + changed some "magic numbers" to consts (QUBIT_HIGHLIGHT_SIZE, MAX_QUBIT_COORDINATE)
1 parent 75d9cb1 commit 1d5f03b

6 files changed

Lines changed: 167 additions & 37 deletions

File tree

glue/crumble/draw/config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ const rad = 10;
33
const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5;
44
const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5;
55

6-
export {pitch, rad, OFFSET_X, OFFSET_Y};
6+
const MIN_ZOOM = 0.25;
7+
const MAX_ZOOM = 4;
8+
9+
const MAX_QUBIT_COORDINATE = 100;
10+
11+
export {pitch, rad, OFFSET_X, OFFSET_Y, MIN_ZOOM, MAX_ZOOM, MAX_QUBIT_COORDINATE};

glue/crumble/draw/main_draw.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {pitch, rad, OFFSET_X, OFFSET_Y} from "./config.js"
1+
import {pitch, rad, OFFSET_X, OFFSET_Y, MAX_QUBIT_COORDINATE} from "./config.js"
22
import {marker_placement} from "../gates/gateset_markers.js";
33
import {drawTimeline} from "./timeline_viewer.js";
44
import {PropagatedPauliFrames} from "../circuit/propagated_pauli_frames.js";
@@ -192,6 +192,15 @@ function defensiveDraw(ctx, body) {
192192
}
193193
}
194194

195+
function switchToScreenCoordinates(ctx) {
196+
ctx.setTransform(1, 0, 0, 1, 0, 0);
197+
}
198+
199+
function switchToTransformationCoordinates(ctx, snap) {
200+
const zoom = snap.viewportZoom;
201+
ctx.setTransform(zoom, 0, 0, zoom, snap.viewportX, snap.viewportY);
202+
}
203+
195204
/**
196205
* @param {!CanvasRenderingContext2D} ctx
197206
* @param {!StateSnapshot} snap
@@ -254,8 +263,10 @@ function draw(ctx, snap) {
254263
}
255264

256265
defensiveDraw(ctx, () => {
257-
ctx.fillStyle = 'white';
266+
switchToScreenCoordinates(ctx);
258267
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
268+
switchToTransformationCoordinates(ctx, snap);
269+
259270
let [focusX, focusY] = xyToPos(snap.curMouseX, snap.curMouseY);
260271

261272
// Draw the background polygons.
@@ -278,26 +289,26 @@ function draw(ctx, snap) {
278289

279290
// Draw the grid of qubits.
280291
defensiveDraw(ctx, () => {
281-
for (let qx = 0; qx < 100; qx += 0.5) {
292+
for (let qx = 0; qx < MAX_QUBIT_COORDINATE; qx += 0.5) {
282293
let [x, _] = c2dCoordTransform(qx, 0);
283294
let s = `${qx}`;
284295
ctx.fillStyle = 'black';
285296
ctx.fillText(s, x - ctx.measureText(s).width / 2, 15);
286297
}
287-
for (let qy = 0; qy < 100; qy += 0.5) {
298+
for (let qy = 0; qy < MAX_QUBIT_COORDINATE; qy += 0.5) {
288299
let [_, y] = c2dCoordTransform(0, qy);
289300
let s = `${qy}`;
290301
ctx.fillStyle = 'black';
291302
ctx.fillText(s, 18 - ctx.measureText(s).width, y);
292303
}
293304

294305
ctx.strokeStyle = 'black';
295-
for (let qx = 0; qx < 100; qx += 0.5) {
306+
for (let qx = 0; qx < MAX_QUBIT_COORDINATE; qx += 0.5) {
296307
let [x, _] = c2dCoordTransform(qx, 0);
297308
let s = `${qx}`;
298309
ctx.fillStyle = 'black';
299310
ctx.fillText(s, x - ctx.measureText(s).width / 2, 15);
300-
for (let qy = qx % 1; qy < 100; qy += 1) {
311+
for (let qy = qx % 1; qy < MAX_QUBIT_COORDINATE; qy += 1) {
301312
let [x, y] = c2dCoordTransform(qx, qy);
302313
ctx.fillStyle = 'white';
303314
let isUnused = !usedQubitCoordSet.has(`${qx},${qy}`);
@@ -384,7 +395,8 @@ function draw(ctx, snap) {
384395
});
385396
});
386397

387-
drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length);
398+
switchToScreenCoordinates(ctx);
399+
const maxTimelineScrollY = drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length);
388400

389401
// Draw scrubber.
390402
ctx.save();
@@ -485,6 +497,7 @@ function draw(ctx, snap) {
485497
} finally {
486498
ctx.restore();
487499
}
500+
return maxTimelineScrollY;
488501
}
489502

490503
export {xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y}

glue/crumble/draw/state_snapshot.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ class StateSnapshot {
1717
* @param {!number} mouseDownX
1818
* @param {!number} mouseDownY
1919
* @param {!Array<![!number, !number]>} boxHighlightPreview
20+
* @param {!number} viewportX
21+
* @param {!number} viewportY
22+
* @param {!number} viewportZoom
23+
* @param {!number} timelineScrollY
24+
* @param {!number} curMouseScreenX
25+
* @param {!number} curMouseScreenY
2026
*/
21-
constructor(circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, boxHighlightPreview) {
27+
constructor(circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, boxHighlightPreview, viewportX=0, viewportY=0, viewportZoom=1, timelineScrollY=0, curMouseScreenX=undefined, curMouseScreenY=undefined) {
2228
this.circuit = circuit.copy();
2329
this.curLayer = curLayer;
2430
this.focusedSet = new Map(focusedSet.entries());
@@ -28,6 +34,12 @@ class StateSnapshot {
2834
this.mouseDownX = mouseDownX;
2935
this.mouseDownY = mouseDownY;
3036
this.boxHighlightPreview = [...boxHighlightPreview];
37+
this.viewportX = viewportX;
38+
this.viewportY = viewportY;
39+
this.viewportZoom = viewportZoom;
40+
this.timelineScrollY = timelineScrollY;
41+
this.curMouseScreenX = curMouseScreenX;
42+
this.curMouseScreenY = curMouseScreenY;
3143

3244
while (this.circuit.layers.length <= this.curLayer) {
3345
this.circuit.layers.push(new Layer());

glue/crumble/draw/timeline_viewer.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {OFFSET_Y, rad} from "./config.js";
1+
import {rad} from "./config.js";
22
import {stroke_connector_to} from "../gates/gate_draw_util.js"
33
import {marker_placement} from '../gates/gateset_markers.js';
44

5-
let TIMELINE_PITCH = 32;
5+
const TIMELINE_PITCH = 32;
6+
const QUBIT_HIGHLIGHT_SIZE = 40;
67

78
/**
89
* @param {!CanvasRenderingContext2D} ctx
@@ -110,6 +111,7 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
110111
return x1 - x2;
111112
});
112113

114+
// Calculate base coordinates.
113115
let base_y2xy = new Map();
114116
let prev_y = undefined;
115117
let cur_x = 0;
@@ -132,6 +134,17 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
132134
base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y) + 0.5]);
133135
}
134136

137+
138+
// Apply vertical scroll offset.
139+
const maxScrollY = Math.max(0, cur_y - ctx.canvas.height + TIMELINE_PITCH); // Restrict scroll based on qubits drawn
140+
const scrollY = Math.max(0, Math.min(snap.timelineScrollY, maxScrollY));
141+
142+
if (scrollY !== 0) {
143+
for (let [key, [x, y]] of base_y2xy) {
144+
base_y2xy.set(key, [x, y - scrollY]);
145+
}
146+
}
147+
135148
let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25);
136149
let num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch);
137150
let min_t_free = snap.curLayer - num_cols_half + 1;
@@ -211,22 +224,30 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
211224

212225
// Draw links to timeslice viewer.
213226
ctx.globalAlpha = 0.5;
227+
const mouseScreenX = snap.curMouseScreenX;
228+
const mouseScreenY = snap.curMouseScreenY;
229+
const zoom = snap.viewportZoom;
230+
214231
for (let q of qubits) {
215232
let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1);
216-
let [x1, y1] = timesliceQubitCoordsFunc(q);
217-
if (snap.curMouseX > ctx.canvas.width / 2 && snap.curMouseY >= y0 + OFFSET_Y - TIMELINE_PITCH * 0.55 && snap.curMouseY <= y0 + TIMELINE_PITCH * 0.55 + OFFSET_Y) {
233+
const [wx1, wy1] = timesliceQubitCoordsFunc(q);
234+
// Convert from world to screen coordinates for qubit highlight.
235+
const x1 = wx1 * zoom + snap.viewportX;
236+
const y1 = wy1 * zoom + snap.viewportY;
237+
if (mouseScreenX > ctx.canvas.width / 2 && mouseScreenY >= y0 - TIMELINE_PITCH * 0.55 && mouseScreenY <= y0 + TIMELINE_PITCH * 0.55) {
218238
ctx.beginPath();
219239
ctx.moveTo(x0, y0);
220240
ctx.lineTo(x1, y1);
221241
ctx.stroke();
222242
ctx.fillStyle = 'black';
223-
ctx.fillRect(x1 - 20, y1 - 20, 40, 40);
243+
ctx.fillRect(x1 - (QUBIT_HIGHLIGHT_SIZE/2) * zoom, y1 - (QUBIT_HIGHLIGHT_SIZE/2) * zoom, QUBIT_HIGHLIGHT_SIZE * zoom, QUBIT_HIGHLIGHT_SIZE * zoom);
224244
ctx.fillRect(ctx.canvas.width / 2, y0 - TIMELINE_PITCH / 3, ctx.canvas.width / 2, TIMELINE_PITCH * 2 / 3);
225245
}
226246
}
227247
} finally {
228248
ctx.restore();
229249
}
250+
return maxScrollY;
230251
}
231252

232-
export {drawTimeline}
253+
export {drawTimeline}

glue/crumble/editor/editor_state.js

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class EditorState {
4747
this.mouseDownX = /** @type {undefined|!number} */ undefined;
4848
this.mouseDownY = /** @type {undefined|!number} */ undefined;
4949
this.obs_val_draw_state = /** @type {!ObservableValue<StateSnapshot>} */ new ObservableValue(this.toSnapshot(undefined));
50+
this.curMouseScreenX = /** @type {undefined|!number} */ undefined;
51+
this.curMouseScreenY = /** @type {undefined|!number} */ undefined;
52+
this.viewportX = 0;
53+
this.viewportY = 0;
54+
this.viewportZoom = 1;
55+
this.timelineScrollY = 0;
5056
}
5157

5258
flipTwoQubitGateOrderAtFocus(preview) {
@@ -201,17 +207,7 @@ class EditorState {
201207
if (previewCircuit === undefined) {
202208
previewCircuit = this.copyOfCurCircuit();
203209
}
204-
return new StateSnapshot(
205-
previewCircuit,
206-
this.curLayer,
207-
this.focusedSet,
208-
this.timelineSet,
209-
this.curMouseX,
210-
this.curMouseY,
211-
this.mouseDownX,
212-
this.mouseDownY,
213-
this.currentPositionsBoxesByMouseDrag(this.chorder.curModifiers.has("alt")),
214-
);
210+
return new StateSnapshot(previewCircuit, this.curLayer, this.focusedSet, this.timelineSet, this.curMouseX, this.curMouseY, this.mouseDownX, this.mouseDownY, this.currentPositionsBoxesByMouseDrag(this.chorder.curModifiers.has("alt")), this.viewportX, this.viewportY, this.viewportZoom, this.timelineScrollY, this.curMouseScreenX, this.curMouseScreenY);
215211
}
216212

217213
force_redraw() {

glue/crumble/main.js

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {Circuit} from "./circuit/circuit.js"
22
import {minXY} from "./circuit/layer.js"
3-
import {pitch} from "./draw/config.js"
3+
import {MAX_QUBIT_COORDINATE, MAX_ZOOM, MIN_ZOOM, pitch} from "./draw/config.js"
44
import {GATE_MAP} from "./gates/gateset.js"
55
import {EditorState} from "./editor/editor_state.js";
66
import {initUrlCircuitSync} from "./editor/sync_url_to_state.js";
77
import {draw} from "./draw/main_draw.js";
88
import {drawToolbox} from "./keyboard/toolbox.js";
99
import {Operation} from "./circuit/operation.js";
1010
import {make_mpp_gate} from './gates/gateset_mpp.js';
11-
import {PropagatedPauliFrames} from './circuit/propagated_pauli_frames.js';
1211

1312
const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5;
1413
const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5;
@@ -39,6 +38,14 @@ txtStimCircuit.addEventListener('keydown', ev => ev.stopPropagation());
3938

4039
let editorState = /** @type {!EditorState} */ new EditorState(document.getElementById('cvn'));
4140

41+
function toWorldMouseX(screenX) {
42+
return (screenX - editorState.viewportX) / editorState.viewportZoom + OFFSET_X;
43+
}
44+
45+
function toWorldMouseY(screenY) {
46+
return (screenY - editorState.viewportY) / editorState.viewportZoom + OFFSET_Y;
47+
}
48+
4249
btnExport.addEventListener('click', _ev => {
4350
exportCurrentState();
4451
});
@@ -144,8 +151,10 @@ function exportCurrentState() {
144151
}
145152

146153
editorState.canvas.addEventListener('mousemove', ev => {
147-
editorState.curMouseX = ev.offsetX + OFFSET_X;
148-
editorState.curMouseY = ev.offsetY + OFFSET_Y;
154+
editorState.curMouseScreenX = ev.offsetX;
155+
editorState.curMouseScreenY = ev.offsetY;
156+
editorState.curMouseX = toWorldMouseX(ev.offsetX);
157+
editorState.curMouseY = toWorldMouseY(ev.offsetY);
149158

150159
// Scrubber.
151160
let w = editorState.canvas.width / 2;
@@ -159,10 +168,12 @@ editorState.canvas.addEventListener('mousemove', ev => {
159168

160169
let isInScrubber = false;
161170
editorState.canvas.addEventListener('mousedown', ev => {
162-
editorState.curMouseX = ev.offsetX + OFFSET_X;
163-
editorState.curMouseY = ev.offsetY + OFFSET_Y;
164-
editorState.mouseDownX = ev.offsetX + OFFSET_X;
165-
editorState.mouseDownY = ev.offsetY + OFFSET_Y;
171+
editorState.curMouseScreenX = ev.offsetX;
172+
editorState.curMouseScreenY = ev.offsetY;
173+
editorState.curMouseX = toWorldMouseX(ev.offsetX);
174+
editorState.curMouseY = toWorldMouseY(ev.offsetY);
175+
editorState.mouseDownX = toWorldMouseX(ev.offsetX);
176+
editorState.mouseDownY = toWorldMouseY(ev.offsetY);
166177

167178
// Scrubber.
168179
let w = editorState.canvas.width / 2;
@@ -179,14 +190,80 @@ editorState.canvas.addEventListener('mouseup', ev => {
179190
let highlightedArea = editorState.currentPositionsBoxesByMouseDrag(ev.altKey);
180191
editorState.mouseDownX = undefined;
181192
editorState.mouseDownY = undefined;
182-
editorState.curMouseX = ev.offsetX + OFFSET_X;
183-
editorState.curMouseY = ev.offsetY + OFFSET_Y;
193+
editorState.curMouseScreenX = ev.offsetX;
194+
editorState.curMouseScreenY = ev.offsetY;
195+
editorState.curMouseX = toWorldMouseX(ev.offsetX);
196+
editorState.curMouseY = toWorldMouseY(ev.offsetY);
184197
editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey);
185198
if (ev.buttons === 1) {
186199
isInScrubber = false;
187200
}
188201
});
189202

203+
// Make sure qubit grid and timeline don't deviate from the area of interest.
204+
function restrictQubitGridAndTimeline() {
205+
const width = editorState.canvas.width / 2;
206+
const height = editorState.canvas.height;
207+
const zoom = editorState.viewportZoom;
208+
const gridMin = -1 * pitch - OFFSET_X;
209+
const gridMax = MAX_QUBIT_COORDINATE * pitch - OFFSET_X;
210+
211+
editorState.viewportX = Math.max(
212+
width - gridMax * zoom,
213+
Math.min(-gridMin * zoom, editorState.viewportX)
214+
);
215+
editorState.viewportY = Math.max(
216+
height - gridMax * zoom,
217+
Math.min(-gridMin * zoom, editorState.viewportY)
218+
);
219+
220+
editorState.timelineScrollY = Math.max(
221+
0,
222+
editorState.timelineScrollY
223+
);
224+
}
225+
226+
function handleTimelineVerticalScroll(ev) {
227+
editorState.timelineScrollY += ev.deltaY;
228+
restrictQubitGridAndTimeline();
229+
editorState.force_redraw();
230+
return;
231+
}
232+
233+
function handleQubitGridZoomPan(ev) {
234+
if (ev.ctrlKey || ev.metaKey) {
235+
// Handle zoom.
236+
const zoomMultiplier = ev.deltaY < 0 ? 1.05 : (1 / 1.05);
237+
const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, editorState.viewportZoom * zoomMultiplier));
238+
const ratio = newZoom / editorState.viewportZoom;
239+
editorState.viewportZoom = newZoom;
240+
241+
// Center zoom around mouse.
242+
editorState.viewportX = ev.offsetX - (ev.offsetX - editorState.viewportX) * ratio;
243+
editorState.viewportY = ev.offsetY - (ev.offsetY - editorState.viewportY) * ratio;
244+
} else {
245+
// Handle pan.
246+
editorState.viewportX -= ev.deltaX;
247+
editorState.viewportY -= ev.deltaY;
248+
}
249+
250+
editorState.curMouseX = toWorldMouseX(ev.offsetX);
251+
editorState.curMouseY = toWorldMouseY(ev.offsetY);
252+
restrictQubitGridAndTimeline();
253+
editorState.force_redraw();
254+
}
255+
256+
editorState.canvas.addEventListener('wheel', ev => {
257+
ev.preventDefault();
258+
const width = editorState.canvas.width / 2;
259+
260+
if (ev.offsetX > width) {
261+
handleTimelineVerticalScroll(ev);
262+
} else {
263+
handleQubitGridZoomPan(ev);
264+
}
265+
}, { passive: false });
266+
190267
/**
191268
* @return {!Map<!string, !function(preview: !boolean) : void>}
192269
*/
@@ -504,7 +581,13 @@ editorState.rev.changes().subscribe(() => {
504581
drawToolbox(editorState.chorder.toEvent(false));
505582
});
506583
initUrlCircuitSync(editorState.rev);
507-
editorState.obs_val_draw_state.observable().subscribe(ds => requestAnimationFrame(() => draw(editorState.canvas.getContext('2d'), ds)));
584+
editorState.obs_val_draw_state.observable().subscribe(ds => requestAnimationFrame(() => {
585+
const maxTimelineScrollY = draw(editorState.canvas.getContext('2d'), ds);
586+
// Prevent over-scrolling.
587+
if (editorState.timelineScrollY > maxTimelineScrollY) {
588+
editorState.timelineScrollY = maxTimelineScrollY;
589+
}
590+
}));
508591
window.addEventListener('focus', () => {
509592
editorState.chorder.handleFocusChanged();
510593
});

0 commit comments

Comments
 (0)