Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 87 additions & 24 deletions tests/browser/backdrop.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ async function backdrops(page) {
return page.evaluate(() => window.__pcbaTest.counters().backdrops);
}

// A backdrop for the current view is drawn. It is displayed only where the
// buffer does not reach, so this is not the same as being on screen.
async function ready(page) {
return page.evaluate(() => allcanvas.front._backdropReady === true);
}

function shown(page) {
return page.evaluate(() => allcanvas.front.backdrop.style.display);
}

async function center(page) {
const box = await page.locator('#frontcanvas').boundingBox();
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
Expand Down Expand Up @@ -147,6 +157,58 @@ test('a fast zoom out shows the board at the edges', async ({ page }) => {
expect(agreement(without, fit)).toBeLessThan(0.9);
});

// The buffer is see-through outside the board outline. A backdrop under it
// showed there, scaled up and blurred, when the buffer covered the whole view.
test('the backdrop is not shown while the buffer covers the view', async ({ page }) => {
await load(page);
expect(await ready(page)).toBe(true);
expect(await shown(page)).toBe('none');

await zoomIn(page, 8);
expect(await shown(page)).toBe('none');

// A fast zoom out leaves the edges without buffer, and the backdrop shows.
expect(await page.evaluate(() => {
const t = allcanvas.front.transform;
t.zoom = 1; t.panx = 0; t.pany = 0;
updateCSSTransform(allcanvas.front);
return allcanvas.front.backdrop.style.display;
})).toBe('block');

// The renders catch up and it is hidden again.
await page.evaluate(() => redrawCanvas(allcanvas.front));
await waitIdle(page);
expect(await shown(page)).toBe('none');
});

// After a layer toggle the old backdrop showed outside the board outline
// until the new one came. The first render of the new state covers the view,
// so the old backdrop must be hidden from then on.
test('the old backdrop is not shown after a layer toggle', async ({ page }) => {
await load(page);
const first = await backdrops(page);
// Hold the worker's next backdrop, so only the renders come in.
await page.evaluate(() => {
window.__held = null;
_worker.onmessage = (e) => {
if (e.data.type === 'backdrop' && !window.__held) window.__held = e;
else handleWorkerMessage(e);
};
});
await page.keyboard.press('s');
await page.waitForFunction(() => window.__held !== null, null, { timeout: 30000 });
expect(await backdrops(page)).toBe(first);
expect(await shown(page)).toBe('none');

await page.evaluate(() => {
_worker.onmessage = handleWorkerMessage;
handleWorkerMessage(window.__held);
});
await waitIdle(page);
expect(await backdrops(page)).toBe(first + 1);
expect(await shown(page)).toBe('none');
});

// The worker keeps the backdrop's layer canvases between draws. Cleared under
// the last draw's transform, they kept part of the board at the old rotation.
// Only a board off the positive quadrant shows it. After a few turns the
Expand All @@ -157,6 +219,8 @@ test('the backdrop shows no old rotation after rotating', async ({ page }) => {
for (const c of document.querySelectorAll('#frontcanvas canvas')) {
if (c !== allcanvas.front.backdrop) c.style.visibility = 'hidden';
}
// The buffer covers the view, so the backdrop is not displayed.
allcanvas.front.backdrop.style.display = 'block';
});
return boardMask(page);
};
Expand Down Expand Up @@ -184,22 +248,21 @@ test('the backdrop shows no old rotation after rotating', async ({ page }) => {
// one, as a second board, until the new backdrop arrived.
test('the old backdrop is hidden while the new rotation is drawn', async ({ page }) => {
await load(page);
const shown = () => page.evaluate(() => allcanvas.front.backdrop.style.display);
expect(await shown()).toBe('block');
expect(await ready(page)).toBe(true);

const first = await backdrops(page);
expect(await page.evaluate(() => {
rotateBoard(1);
return allcanvas.front.backdrop.style.display;
})).toBe('none');
return [allcanvas.front.backdrop.style.display, allcanvas.front._backdropReady];
})).toEqual(['none', false]);
await waitIdle(page);
expect(await shown()).toBe('block');
expect(await ready(page)).toBe(true);
expect(await backdrops(page)).toBe(first + 1);

// A full turn brings back the old key, and it must still be drawn again.
for (let i = 0; i < 3; i++) await page.evaluate(() => rotateBoard(1));
await waitIdle(page);
expect(await shown()).toBe('block');
expect(await ready(page)).toBe(true);
});

// A new size of the view gives a new fit. The backdrop at the old fit showed
Expand All @@ -208,45 +271,45 @@ test('the old backdrop is hidden when the view changes size', async ({ page }) =
// Both sides are drawn from FB on, which takes several seconds a step.
test.setTimeout(60000);
await load(page);
const shown = () => page.evaluate(() => allcanvas.front.backdrop.style.display);

// The window. The resize event comes later, so record whether it was hidden.
await page.evaluate(() => {
window.__hidden = false;
const c = allcanvas.front.backdrop;
new MutationObserver(() => {
if (c.style.display === 'none') window.__hidden = true;
}).observe(c, { attributes: true, attributeFilter: ['style'] });
const hide = window.hideBackdrop;
window.hideBackdrop = (ld) => {
if (ld === allcanvas.front) window.__hidden = true;
hide(ld);
};
});
await page.setViewportSize({ width: 1000, height: 650 });
await page.waitForFunction(() => window.__hidden, null, { timeout: 10000 });
await waitIdle(page);
expect(await shown()).toBe('block');
expect(await ready(page)).toBe(true);

// F to FB, as the G key does.
expect(await page.evaluate(() => {
showSide('FB');
return allcanvas.front.backdrop.style.display;
})).toBe('none');
return allcanvas.front._backdropReady;
})).toBe(false);
await waitIdle(page);
expect(await shown()).toBe('block');
expect(await ready(page)).toBe(true);

// Row to column.
expect(await page.evaluate(() => {
setCanvasDirection('column');
return allcanvas.front.backdrop.style.display;
})).toBe('none');
return allcanvas.front._backdropReady;
})).toBe(false);
await waitIdle(page);
expect(await shown()).toBe('block');
expect(await ready(page)).toBe(true);
});

// The same fit keeps the backdrop. A resize to the same size must not hide it.
test('a resize with the same fit keeps the backdrop shown', async ({ page }) => {
test('a resize with the same fit keeps the backdrop', async ({ page }) => {
await load(page);
expect(await page.evaluate(() => {
resizeAll();
return allcanvas.front.backdrop.style.display;
})).toBe('block');
return allcanvas.front._backdropReady;
})).toBe(true);
});

// A backdrop the worker is still drawing when the board turns is for the old
Expand All @@ -269,12 +332,12 @@ test('a backdrop drawn for the old rotation is dropped', async ({ page }) => {
rotateBoard(1);
_worker.onmessage = handleWorkerMessage;
handleWorkerMessage(window.__held);
return allcanvas.front.backdrop.style.display;
})).toBe('none');
return allcanvas.front._backdropReady;
})).toBe(false);
expect(await backdrops(page)).toBe(first);

await waitIdle(page);
expect(await page.evaluate(() => allcanvas.front.backdrop.style.display)).toBe('block');
expect(await ready(page)).toBe(true);
expect(await backdrops(page)).toBe(first + 1);
});

Expand Down
36 changes: 34 additions & 2 deletions web/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ function handleWorkerMessage(e) {
// faster than the renders arrive, or past what the buffer holds, leaves the
// edges empty. The backdrop shows there instead, blurred. It is moved by
// CSS for pan and zoom, and drawn again only when what is drawn changes.
//
// It is shown only where the buffer does not reach. The buffer is see-through
// outside the board outline, and a backdrop under it shows there: blurred at
// a high zoom, and in the old state for a moment after a layer toggle.

var _backdropTimers = {}; // side -> timeout
var _backdropPending = {}; // side -> true while the worker draws it
Expand All @@ -211,6 +215,7 @@ var _backdropStale = {}; // side -> drop the one the worker is drawing
function hideBackdrop(layerdict) {
if (!layerdict.backdrop) return;
layerdict.backdrop.style.display = "none";
layerdict._backdropReady = false;
layerdict._backdropKey = null;
if (_backdropPending[layerdict.layer]) _backdropStale[layerdict.layer] = true;
}
Expand Down Expand Up @@ -277,7 +282,7 @@ function blitBackdrop(msg) {
ctx.clearRect(0, 0, c.width, c.height);
ctx.drawImage(bitmap, 0, 0);
bitmap.close();
c.style.display = "block";
layerdict._backdropReady = true;
updateBackdropTransform(layerdict);

if (!_stats[msg.side]) _stats[msg.side] = makeStatsTracker();
Expand All @@ -294,6 +299,28 @@ function updateBackdropTransform(layerdict) {
var tx = t.zoom * t.panx / devicePixelRatio;
var ty = t.zoom * t.pany / devicePixelRatio;
c.style.transform = "translate(" + tx + "px," + ty + "px) scale(" + t.zoom + ")";
c.style.display = layerdict._backdropReady && !bufferCoversView(layerdict) ? "block" : "none";
}

// Whether the drawn part of the buffer covers the whole view. Placed on
// screen as updateCSSTransform() places it. Until the overscan pieces are
// all in, only the viewport of the render is drawn. Compared in device
// pixels, as the worker sizes it: back in CSS pixels it can come out a hair
// short of the view.
function bufferCoversView(layerdict) {
var buf = layerdict._bufferState, r = layerdict._drawnRect;
if (!buf || !r) return false;
if (!_piecesPending[layerdict.layer]) r = { x: 0, y: 0, w: layerdict._bufW, h: layerdict._bufH };
var div = document.getElementById(layerdict.layer === "B" ? "backcanvas" : "frontcanvas");
if (!div) return false;
var t = layerdict.transform;
var os = layerdict._overscan || { x: 0, y: 0 };
var k = t.zoom / buf.zoom;
var tx = t.zoom * (t.panx - buf.panx) - k * os.x;
var ty = t.zoom * (t.pany - buf.pany) - k * os.y;
return tx + k * r.x <= 0 && ty + k * r.y <= 0 &&
tx + k * (r.x + r.w) >= div.clientWidth * devicePixelRatio &&
ty + k * (r.y + r.h) >= div.clientHeight * devicePixelRatio;
}

// ---- Render performance stats ----
Expand Down Expand Up @@ -485,6 +512,7 @@ function blitBitmaps(msg) {
// The canvases no longer match _bufferState, so the next render must not
// carry them.
layerdict._content = null;
layerdict._drawnRect = null;
return;
}

Expand All @@ -505,6 +533,7 @@ function blitBitmaps(msg) {
layerdict._bufW = msg.bufW;
layerdict._bufH = msg.bufH;
layerdict._content = content;
layerdict._drawnRect = msg.rect;

// A layer with no bitmap has no content and is cleared.
var canvases = pieceCanvases(layerdict, msg);
Expand Down Expand Up @@ -632,7 +661,10 @@ function blitPiece(msg) {
return;
}
drawPieceBitmaps(pieceCanvases(layerdict, msg), msg.rect);
if (msg.done) _piecesPending[msg.side] = false;
if (msg.done) {
_piecesPending[msg.side] = false;
updateBackdropTransform(layerdict);
}

if (!_stats[msg.side]) _stats[msg.side] = makeStatsTracker();
_stats[msg.side].drawCalls += msg.drawCalls || 0;
Expand Down
Loading