Skip to content
8 changes: 7 additions & 1 deletion glue/crumble/draw/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ const rad = 10;
const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5;
const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5;

export {pitch, rad, OFFSET_X, OFFSET_Y};
const MIN_ZOOM = 0.25;
const MAX_ZOOM = 4;

const MAX_QUBIT_COORDINATE = 100;
const LABEL_GAP = 20;

export {pitch, rad, OFFSET_X, OFFSET_Y, MIN_ZOOM, MAX_ZOOM, MAX_QUBIT_COORDINATE, LABEL_GAP};
79 changes: 57 additions & 22 deletions glue/crumble/draw/main_draw.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {pitch, rad, OFFSET_X, OFFSET_Y} from "./config.js"
import {pitch, rad, OFFSET_X, OFFSET_Y, MAX_QUBIT_COORDINATE, LABEL_GAP} from "./config.js"
import {marker_placement} from "../gates/gateset_markers.js";
import {drawTimeline} from "./timeline_viewer.js";
import {PropagatedPauliFrames} from "../circuit/propagated_pauli_frames.js";
Expand Down Expand Up @@ -192,6 +192,15 @@ function defensiveDraw(ctx, body) {
}
}

function switchToScreenCoordinates(ctx) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
}

function switchToTransformationCoordinates(ctx, snap) {
const zoom = snap.viewportZoom;
ctx.setTransform(zoom, 0, 0, zoom, snap.viewportX, snap.viewportY);
}

/**
* @param {!CanvasRenderingContext2D} ctx
* @param {!StateSnapshot} snap
Expand Down Expand Up @@ -254,8 +263,49 @@ function draw(ctx, snap) {
}

defensiveDraw(ctx, () => {
ctx.fillStyle = 'white';
switchToScreenCoordinates(ctx);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

// Draw grid tick-mark labels.
defensiveDraw(ctx, () => {
ctx.fillStyle = 'black';

let tickMarkInterval = 0.5;
if (snap.viewportZoom < 0.85) tickMarkInterval = 1;
if (snap.viewportZoom < 0.3) tickMarkInterval = 2;

ctx.save();
ctx.beginPath();
ctx.rect(LABEL_GAP, 0, ctx.canvas.width - LABEL_GAP, ctx.canvas.height);
ctx.clip();
for (let qx = 0; qx < MAX_QUBIT_COORDINATE; qx += tickMarkInterval) {
let [x, _] = c2dCoordTransform(qx, 0);
const screenX = x * snap.viewportZoom + snap.viewportX;
let s = `${qx}`;
ctx.fillText(s, screenX - ctx.measureText(s).width / 2, 15);
}
ctx.restore();

ctx.save();
ctx.beginPath();
ctx.rect(0, LABEL_GAP, ctx.canvas.width, ctx.canvas.height - LABEL_GAP);
ctx.clip();
for (let qy = 0; qy < MAX_QUBIT_COORDINATE; qy += tickMarkInterval) {
let [_, y] = c2dCoordTransform(0, qy);
const screenY = y * snap.viewportZoom + snap.viewportY;
let s = `${qy}`;
ctx.fillText(s, 18 - ctx.measureText(s).width, screenY);
}
ctx.restore();
});

// Apply clipping on all content so it doesn't overlap on tick labels.
ctx.save();
ctx.beginPath();
ctx.rect(LABEL_GAP, LABEL_GAP, ctx.canvas.width, ctx.canvas.height - LABEL_GAP);
ctx.clip();
switchToTransformationCoordinates(ctx, snap);

let [focusX, focusY] = xyToPos(snap.curMouseX, snap.curMouseY);

// Draw the background polygons.
Expand All @@ -278,26 +328,9 @@ function draw(ctx, snap) {

// Draw the grid of qubits.
defensiveDraw(ctx, () => {
for (let qx = 0; qx < 100; qx += 0.5) {
let [x, _] = c2dCoordTransform(qx, 0);
let s = `${qx}`;
ctx.fillStyle = 'black';
ctx.fillText(s, x - ctx.measureText(s).width / 2, 15);
}
for (let qy = 0; qy < 100; qy += 0.5) {
let [_, y] = c2dCoordTransform(0, qy);
let s = `${qy}`;
ctx.fillStyle = 'black';
ctx.fillText(s, 18 - ctx.measureText(s).width, y);
}

ctx.strokeStyle = 'black';
for (let qx = 0; qx < 100; qx += 0.5) {
let [x, _] = c2dCoordTransform(qx, 0);
let s = `${qx}`;
ctx.fillStyle = 'black';
ctx.fillText(s, x - ctx.measureText(s).width / 2, 15);
for (let qy = qx % 1; qy < 100; qy += 1) {
for (let qx = 0; qx < MAX_QUBIT_COORDINATE; qx += 0.5) {
for (let qy = qx % 1; qy < MAX_QUBIT_COORDINATE; qy += 1) {
let [x, y] = c2dCoordTransform(qx, qy);
ctx.fillStyle = 'white';
let isUnused = !usedQubitCoordSet.has(`${qx},${qy}`);
Expand Down Expand Up @@ -384,7 +417,8 @@ function draw(ctx, snap) {
});
});

drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length);
switchToScreenCoordinates(ctx);
const timelineDrawSummary = drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length);

// Draw scrubber.
ctx.save();
Expand Down Expand Up @@ -485,6 +519,7 @@ function draw(ctx, snap) {
} finally {
ctx.restore();
}
return timelineDrawSummary;
}

export {xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y}
16 changes: 15 additions & 1 deletion glue/crumble/draw/state_snapshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,15 @@ class StateSnapshot {
* @param {!number} mouseDownX
* @param {!number} mouseDownY
* @param {!Array<![!number, !number]>} boxHighlightPreview
* @param {!number} viewportX
* @param {!number} viewportY
* @param {!number} viewportZoom
* @param {!number} timelineOffsetX
* @param {!number} timelineOffsetY
* @param {!number} curMouseScreenX
* @param {!number} curMouseScreenY
*/
constructor(circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, boxHighlightPreview) {
constructor(circuit, curLayer, focusedSet, timelineSet, curMouseX, curMouseY, mouseDownX, mouseDownY, boxHighlightPreview, viewportX, viewportY, viewportZoom, timelineOffsetX, timelineOffsetY, curMouseScreenX, curMouseScreenY) {
this.circuit = circuit.copy();
this.curLayer = curLayer;
this.focusedSet = new Map(focusedSet.entries());
Expand All @@ -28,6 +35,13 @@ class StateSnapshot {
this.mouseDownX = mouseDownX;
this.mouseDownY = mouseDownY;
this.boxHighlightPreview = [...boxHighlightPreview];
this.viewportX = viewportX;
this.viewportY = viewportY;
this.viewportZoom = viewportZoom;
this.timelineOffsetX = timelineOffsetX;
this.timelineOffsetY = timelineOffsetY;
this.curMouseScreenX = curMouseScreenX;
this.curMouseScreenY = curMouseScreenY;

while (this.circuit.layers.length <= this.curLayer) {
this.circuit.layers.push(new Layer());
Expand Down
105 changes: 81 additions & 24 deletions glue/crumble/draw/timeline_viewer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import {OFFSET_Y, rad} from "./config.js";
import {rad} from "./config.js";
import {stroke_connector_to} from "../gates/gate_draw_util.js"
import {marker_placement} from '../gates/gateset_markers.js';

let TIMELINE_PITCH = 32;
const TIMELINE_PITCH = 32;
const QUBIT_HIGHLIGHT_SIZE = 40;

// Timeline panel changes size dynamically. These values are collected during draw and are used for restricting panning outside the relevant bounds.
class DrawSummary {
/**
* @param {!number} minOffsetX - Minimum allowed offsetX for timeline panel
* @param {!number} maxOffsetX - Maximum allowed offsetX for timeline panel
* @param {!number} maxOffsetY - Maximum Y offset for timeline panel
*/
constructor(minOffsetX, maxOffsetX, maxOffsetY) {
this.minOffsetX = minOffsetX;
this.maxOffsetX = maxOffsetX;
this.maxOffsetY = maxOffsetY;
}
}

/**
* @param {!CanvasRenderingContext2D} ctx
Expand Down Expand Up @@ -110,6 +125,7 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
return x1 - x2;
});

// Calculate base coordinates.
let base_y2xy = new Map();
let prev_y = undefined;
let cur_x = 0;
Expand All @@ -132,11 +148,31 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y) + 0.5]);
}

let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25);
let num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch);

const x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25);
const num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch);

let min_t_free = snap.curLayer - num_cols_half + 1;
let min_t_clamp = Math.max(0, Math.min(min_t_free, numLayers - num_cols_half*2 + 1));
let max_t = Math.min(min_t_clamp + num_cols_half*2 + 2, numLayers);


const maxOffsetY = Math.max(0, cur_y - ctx.canvas.height + TIMELINE_PITCH);
const offsetY = Math.max(-maxOffsetY, Math.min(0, snap.timelineOffsetY ?? 0));

const lastLayerOffset = (numLayers - 1 - snap.curLayer - (min_t_clamp - min_t_free)) * x_pitch;
const minOffsetX = -Math.max(0, lastLayerOffset - 0.5 * w + 2*x_pitch);
const maxOffsetX = Math.max(0, (min_t_clamp - 1) * x_pitch);
const offsetX = Math.max(minOffsetX, Math.min(maxOffsetX, snap.timelineOffsetX ?? 0));

// Apply x/y offset to base coordinates
if (offsetY !== 0 || offsetX !== 0) {
for (let [key, [x, y]] of base_y2xy) {
base_y2xy.set(key, [x + offsetX, y + offsetY]);
}
}

const label_col_x = w * 1.5 + (min_t_free - 1 - snap.curLayer) * x_pitch;

let t2t = t => {
let dt = t - snap.curLayer;
dt -= min_t_clamp - min_t_free;
Expand All @@ -159,17 +195,23 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
try {
ctx.clearRect(w, 0, w, ctx.canvas.height);

// Apply clipping to prevent content from overlapping on labels and outside timeline panel.
ctx.save();
ctx.beginPath();
ctx.rect(label_col_x, 0, ctx.canvas.width - label_col_x, ctx.canvas.height);
ctx.clip();

// Draw colored indicators showing Pauli propagation.
let hitCounts = new Map();
for (let [mi, p] of propagatedMarkerLayers.entries()) {
drawTimelineMarkers(ctx, snap, qubitTimeCoords, p, mi, min_t_clamp, max_t, x_pitch, hitCounts);
drawTimelineMarkers(ctx, snap, qubitTimeCoords, p, mi, 0, numLayers, x_pitch, hitCounts);
}

// Draw highlight of current layer.
ctx.globalAlpha *= 0.5;
ctx.fillStyle = 'black';
{
let x1 = t2t(snap.curLayer) + w * 1.5 - x_pitch / 2;
let x1 = t2t(snap.curLayer) + w * 1.5 - x_pitch / 2 + offsetX;
ctx.fillRect(x1, 0, x_pitch, ctx.canvas.height);
}
ctx.globalAlpha *= 2;
Expand All @@ -179,26 +221,16 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun

// Draw wire lines.
for (let q of qubits) {
let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1);
let [x1, y1] = qubitTimeCoords(q, max_t + 1);
let [x0, y0] = qubitTimeCoords(q, -1);
let [x1, y1] = qubitTimeCoords(q, numLayers);
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.stroke();
}

// Draw wire labels.
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let q of qubits) {
let [x, y] = qubitTimeCoords(q, min_t_clamp - 1);
let qx = snap.circuit.qubitCoordData[q * 2];
let qy = snap.circuit.qubitCoordData[q * 2 + 1];
ctx.fillText(`${qx},${qy}:`, x, y);
}

// Draw layers of gates.
for (let time = min_t_clamp; time <= max_t; time++) {
for (let time = 0; time < numLayers; time++) {
let qubitsCoordsFuncForLayer = q => qubitTimeCoords(q, time);
let layer = snap.circuit.layers[time];
if (layer === undefined) {
Expand All @@ -209,24 +241,49 @@ function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFun
}
}

ctx.restore(); // Stop clipping since labels and links to timeslice should be outside clipping area.

// Draw wire labels.
ctx.strokeStyle = 'black';
ctx.fillStyle = 'black';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let q of qubits) {
let [x, y] = qubitTimeCoords(q, min_t_clamp - 1);
x -= offsetX; // Labels are frozen on horizontal axis.
let qx = snap.circuit.qubitCoordData[q * 2];
let qy = snap.circuit.qubitCoordData[q * 2 + 1];
ctx.fillText(`${qx},${qy}:`, x, y);
}

// Draw links to timeslice viewer.
ctx.globalAlpha = 0.5;
const mouseScreenX = snap.curMouseScreenX;
const mouseScreenY = snap.curMouseScreenY;
const zoom = snap.viewportZoom;

for (let q of qubits) {
let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1);
let [x1, y1] = timesliceQubitCoordsFunc(q);
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) {
x0 -= offsetX; // Lines start at frozen position to match labels.
const [wx1, wy1] = timesliceQubitCoordsFunc(q);
// Convert from world to screen coordinates for qubit highlight.
const x1 = wx1 * zoom + snap.viewportX;
const y1 = wy1 * zoom + snap.viewportY;
if (mouseScreenX > ctx.canvas.width / 2 && mouseScreenY >= y0 - TIMELINE_PITCH * 0.55 && mouseScreenY <= y0 + TIMELINE_PITCH * 0.55) {
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.stroke();
ctx.fillStyle = 'black';
ctx.fillRect(x1 - 20, y1 - 20, 40, 40);
ctx.fillRect(x1 - (QUBIT_HIGHLIGHT_SIZE/2) * zoom, y1 - (QUBIT_HIGHLIGHT_SIZE/2) * zoom, QUBIT_HIGHLIGHT_SIZE * zoom, QUBIT_HIGHLIGHT_SIZE * zoom);
ctx.fillRect(ctx.canvas.width / 2, y0 - TIMELINE_PITCH / 3, ctx.canvas.width / 2, TIMELINE_PITCH * 2 / 3);
}
}
} finally {
ctx.restore();
}

return new DrawSummary(minOffsetX, maxOffsetX, maxOffsetY);
}

export {drawTimeline}
export {drawTimeline, DrawSummary}
Loading
Loading