mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
✨ Coalesce playground wheel zoom per animation frame
Apply at most one _set_view + _render_from_cache per animation frame in the wasm playground, accumulating the wheel zoom factor and the last cursor point instead of rendering synchronously on every wheel event. This mirrors schedule-zoom! in app.main.ui.workspace.viewport.actions so playground interactions match the application. A comment records that the playground should call the real function once it migrates to ClojureScript. Extend the adapter tests with the coalesced flush, factor reset across frames, scale clamps, and per-move pointer pan. AI-assisted-by: deepseek-flash
This commit is contained in:
parent
f81696886a
commit
34e4dc1674
@ -104,19 +104,35 @@ export function createRendererAdapter(module, scheduler = globalThis) {
|
||||
scheduler.clearTimeout(timer);
|
||||
timer = scheduler.setTimeout(finish, 100);
|
||||
};
|
||||
// Mirrors schedule-zoom! in app.main.ui.workspace.viewport.actions:
|
||||
// accumulate wheel zoom and flush at most one _set_view + _render_from_cache
|
||||
// per animation frame. When the playground migrates to ClojureScript, call
|
||||
// that function directly instead of duplicating this scheduling.
|
||||
const zoom = { factor: 1, pt: null, frame: undefined };
|
||||
const flushZoom = () => {
|
||||
zoom.frame = undefined;
|
||||
if (disposed) return;
|
||||
stopRender();
|
||||
const factor = zoom.factor;
|
||||
const [px, py] = zoom.pt;
|
||||
zoom.factor = 1;
|
||||
zoom.pt = null;
|
||||
const nextScale = Math.max(0.01, Math.min(100, view.scale * factor));
|
||||
view.x += px / nextScale - px / view.scale;
|
||||
view.y += py / nextScale - py / view.scale;
|
||||
view.scale = nextScale;
|
||||
update();
|
||||
};
|
||||
listen(
|
||||
"wheel",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
begin();
|
||||
const nextScale = Math.max(
|
||||
0.01,
|
||||
Math.min(100, view.scale * (event.deltaY < 0 ? 1.1 : 0.9)),
|
||||
);
|
||||
view.x += event.offsetX / nextScale - event.offsetX / view.scale;
|
||||
view.y += event.offsetY / nextScale - event.offsetY / view.scale;
|
||||
view.scale = nextScale;
|
||||
update();
|
||||
zoom.factor *= event.deltaY < 0 ? 1.1 : 0.9;
|
||||
zoom.pt = [event.offsetX, event.offsetY];
|
||||
if (zoom.frame === undefined) {
|
||||
zoom.frame = scheduler.requestAnimationFrame(flushZoom);
|
||||
}
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
@ -143,6 +159,8 @@ export function createRendererAdapter(module, scheduler = globalThis) {
|
||||
detach = () => {
|
||||
for (const remove of listeners) remove();
|
||||
scheduler.clearTimeout(timer);
|
||||
if (zoom.frame !== undefined) scheduler.cancelAnimationFrame(zoom.frame);
|
||||
zoom.frame = undefined;
|
||||
panning = false;
|
||||
active = false;
|
||||
};
|
||||
|
||||
@ -23,6 +23,7 @@ function fixture() {
|
||||
},
|
||||
};
|
||||
const state = { rendered: 0, cleaned: false, deleted: false, lost: false };
|
||||
const calls = { views: [], cached: 0, start: 0, end: 0 };
|
||||
const gl = {
|
||||
getExtension: (name) =>
|
||||
name === "WEBGL_lose_context"
|
||||
@ -47,10 +48,18 @@ function fixture() {
|
||||
_init() {},
|
||||
_set_render_options() {},
|
||||
_set_browser() {},
|
||||
_set_view_start() {},
|
||||
_set_view() {},
|
||||
_set_view_end() {},
|
||||
_render_from_cache() {},
|
||||
_set_view_start() {
|
||||
calls.start++;
|
||||
},
|
||||
_set_view(...args) {
|
||||
calls.views.push(args);
|
||||
},
|
||||
_set_view_end() {
|
||||
calls.end++;
|
||||
},
|
||||
_render_from_cache() {
|
||||
calls.cached++;
|
||||
},
|
||||
_render() {
|
||||
state.rendered++;
|
||||
return 2;
|
||||
@ -62,22 +71,122 @@ function fixture() {
|
||||
const adapter = createRendererAdapter(module, scheduler);
|
||||
adapter.assignCanvas(canvas);
|
||||
adapter.setupInteraction(canvas);
|
||||
return { adapter, state, canvas, frames, timers, module };
|
||||
return { adapter, state, calls, canvas, frames, timers, module };
|
||||
}
|
||||
|
||||
test("wheel zoom flushes once per animation frame", () => {
|
||||
const f = fixture();
|
||||
const wheel = (deltaY, offsetX, offsetY) =>
|
||||
f.canvas.dispatchEvent(
|
||||
Object.assign(new Event("wheel"), { deltaY, offsetX, offsetY }),
|
||||
);
|
||||
const runFrame = () => {
|
||||
const [id] = f.frames.keys();
|
||||
const callback = f.frames.get(id);
|
||||
f.frames.delete(id);
|
||||
callback(16);
|
||||
};
|
||||
|
||||
wheel(-1, 10, 20);
|
||||
wheel(-1, 30, 40);
|
||||
wheel(-1, 50, 60);
|
||||
|
||||
assert.equal(f.frames.size, 1);
|
||||
assert.equal(f.calls.start, 1);
|
||||
assert.equal(f.calls.views.length, 0);
|
||||
assert.equal(f.calls.cached, 0);
|
||||
|
||||
runFrame();
|
||||
|
||||
assert.equal(f.calls.views.length, 1);
|
||||
assert.equal(f.calls.cached, 1);
|
||||
const [scale, x, y] = f.calls.views[0];
|
||||
assert.ok(Math.abs(scale - 1.1 ** 3) < 1e-9, `unexpected scale ${scale}`);
|
||||
assert.ok(
|
||||
Math.abs(x - (50 / 1.1 ** 3 - 50)) < 1e-9,
|
||||
`unexpected x anchor ${x}`,
|
||||
);
|
||||
assert.ok(
|
||||
Math.abs(y - (60 / 1.1 ** 3 - 60)) < 1e-9,
|
||||
`unexpected y anchor ${y}`,
|
||||
);
|
||||
assert.equal(f.timers.size, 1);
|
||||
|
||||
// The flush releases the frame slot and resets the accumulated factor.
|
||||
wheel(-1, 10, 20);
|
||||
assert.equal(f.frames.size, 1);
|
||||
assert.equal(f.calls.cached, 1);
|
||||
|
||||
runFrame();
|
||||
|
||||
assert.equal(f.calls.views.length, 2);
|
||||
assert.equal(f.calls.cached, 2);
|
||||
assert.ok(
|
||||
Math.abs(f.calls.views[1][0] - 1.1 ** 4) < 1e-9,
|
||||
`unexpected scale ${f.calls.views[1][0]}`,
|
||||
);
|
||||
f.adapter.dispose();
|
||||
});
|
||||
|
||||
test("wheel zoom clamps the accumulated factor to the scale bounds", () => {
|
||||
const f = fixture();
|
||||
const wheel = (deltaY) =>
|
||||
f.canvas.dispatchEvent(
|
||||
Object.assign(new Event("wheel"), { deltaY, offsetX: 10, offsetY: 20 }),
|
||||
);
|
||||
const runFrame = () => {
|
||||
const [id] = f.frames.keys();
|
||||
const callback = f.frames.get(id);
|
||||
f.frames.delete(id);
|
||||
callback(16);
|
||||
};
|
||||
|
||||
for (let index = 0; index < 100; index++) wheel(-1);
|
||||
runFrame();
|
||||
assert.equal(f.calls.views[0][0], 100);
|
||||
|
||||
for (let index = 0; index < 200; index++) wheel(1);
|
||||
runFrame();
|
||||
assert.equal(f.calls.views[1][0], 0.01);
|
||||
f.adapter.dispose();
|
||||
});
|
||||
|
||||
test("pointer pan still renders on every move", () => {
|
||||
const f = fixture();
|
||||
f.canvas.dispatchEvent(
|
||||
Object.assign(new Event("pointerdown"), {
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
pointerId: 1,
|
||||
}),
|
||||
);
|
||||
f.canvas.dispatchEvent(
|
||||
Object.assign(new Event("pointermove"), { clientX: 10, clientY: 20 }),
|
||||
);
|
||||
f.canvas.dispatchEvent(
|
||||
Object.assign(new Event("pointermove"), { clientX: 30, clientY: 40 }),
|
||||
);
|
||||
|
||||
assert.equal(f.calls.views.length, 2);
|
||||
assert.equal(f.calls.cached, 2);
|
||||
assert.equal(f.frames.size, 0);
|
||||
f.adapter.dispose();
|
||||
});
|
||||
|
||||
test("disposing an adapter settles pending work and isolates late callbacks", async () => {
|
||||
const first = fixture();
|
||||
const second = fixture();
|
||||
const pending = first.adapter.renderFull();
|
||||
const lateFrame = [...first.frames.values()][0];
|
||||
const lateRender = [...first.frames.values()][0];
|
||||
first.canvas.dispatchEvent(
|
||||
Object.assign(new Event("wheel"), { deltaY: 1, offsetX: 10, offsetY: 20 }),
|
||||
);
|
||||
const lateTimer = [...first.timers.values()][0];
|
||||
assert.equal(first.frames.size, 1);
|
||||
const lateZoom = [...first.frames.values()][0];
|
||||
first.adapter.dispose();
|
||||
first.adapter.dispose();
|
||||
lateFrame(16);
|
||||
lateTimer();
|
||||
lateRender(16);
|
||||
lateZoom(16);
|
||||
assert.equal(await pending, false);
|
||||
assert.equal(first.frames.size + first.timers.size, 0);
|
||||
assert.deepEqual(first.state, {
|
||||
@ -86,8 +195,9 @@ test("disposing an adapter settles pending work and isolates late callbacks", as
|
||||
deleted: true,
|
||||
lost: true,
|
||||
});
|
||||
assert.equal(first.calls.cached, 0);
|
||||
first.canvas.dispatchEvent(new Event("wheel"));
|
||||
assert.equal(first.timers.size, 0);
|
||||
assert.equal(first.frames.size + first.timers.size, 0);
|
||||
const completed = second.adapter.renderFull();
|
||||
[...second.frames.values()][0](32);
|
||||
assert.equal(await completed, true);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user