perf(movimento): vitrine e 3D mais leves, WebGL por software lembrado na sessão e suporte ao WebKit
This commit is contained in:
1 parent
9fd1570426
commit
8903f3c57e
28 files changed
+854
-406
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
import {
|
||||
createField,
|
||||
type FieldEngine,
|
||||
needsFrame,
|
||||
isSettling,
|
||||
prepareField,
|
||||
type Rig,
|
||||
type Tick,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
* - onReady no 2º quadro, amortecimento da rolagem (limite de dt, progresso ausente), entrada, escurecimento e abertura;
|
||||
* - retrato x paisagem (posição do grupo e distância da câmera) e o foco da lente;
|
||||
* - mouse ativo x parado x toque, raio que acerta ou não o plano, e a quantidade de pontos (denso x leve);
|
||||
* - quando pedir o próximo quadro (entrada, rolagem assentando, cursor) e quando não pedir;
|
||||
* - quando pedir o próximo quadro (entrada e rolagem assentando) e quando deixar para a batida de 30 por segundo (cursor);
|
||||
* - perda do contexto WebGL, falha ao desenhar e limpeza.
|
||||
* O WebGLRenderer é um dublê (o jsdom não tem WebGL); o `three` é o de verdade, com ShaderMaterial, BufferGeometry,
|
||||
* PerspectiveCamera e Group registrando as instâncias criadas. O quadro é chamado direto por `renderAt`, com o relógio
|
||||
@@ -48,6 +48,8 @@ class MockRenderer {
|
||||
forceContextLoss = jest.fn();
|
||||
setSize = jest.fn();
|
||||
setPixelRatio = jest.fn();
|
||||
// um WebGL que não conta o nome do renderizador (sem WEBGL_debug_renderer_info): não é por software
|
||||
getContext = () => ({ getExtension: () => null });
|
||||
getPixelRatio = () => this.pixelRatio;
|
||||
constructor() {
|
||||
mockRenderers.push(this);
|
||||
@@ -512,13 +514,16 @@ describe("createField: quando pedir o próximo quadro", () => {
|
||||
expect(scene.frame(50)).toBe(0);
|
||||
});
|
||||
|
||||
it("path: settled, visible, and the mouse is moving -> frames are requested", () => {
|
||||
it("path: settled, visible, and the mouse is moving -> the frame draws but asks for no other (the pulse brings the next at 30 Hz)", () => {
|
||||
const scene = setup({ visible: true });
|
||||
scene.settle(200);
|
||||
(performance.now as jest.Mock).mockReturnValue(scene.now());
|
||||
pointerMove("mouse");
|
||||
const drawn = scene.renderer.render.mock.calls.length;
|
||||
|
||||
expect(scene.frame(50)).toBeGreaterThan(0);
|
||||
expect(scene.frame(50)).toBe(0);
|
||||
|
||||
expect(scene.renderer.render.mock.calls.length).toBe(drawn + 1);
|
||||
});
|
||||
|
||||
it("path: settled, mouse active, but the hero is not visible -> no frame (cursor only counts on screen)", () => {
|
||||
@@ -538,16 +543,17 @@ describe("createField: quando pedir o próximo quadro", () => {
|
||||
expect(scene.frame()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("path: becoming visible later -> the next frame sees the new flag", () => {
|
||||
it("path: becoming visible later -> the pulse starts and asks for a frame right away", () => {
|
||||
const scene = setup({ visible: false });
|
||||
scene.settle(200);
|
||||
(performance.now as jest.Mock).mockReturnValue(scene.now());
|
||||
pointerMove("mouse");
|
||||
expect(scene.frame(50)).toBe(0);
|
||||
frameRequests.mockClear();
|
||||
|
||||
scene.engine.setVisible(true);
|
||||
|
||||
expect(scene.frame(50)).toBeGreaterThan(0);
|
||||
expect(frameRequests).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("path: several requests before a frame become ONE animation frame", () => {
|
||||
@@ -563,7 +569,7 @@ describe("createField: quando pedir o próximo quadro", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("needsFrame / updatePointer / updateRig (contas puras)", () => {
|
||||
describe("isSettling / updatePointer / updateRig (contas puras)", () => {
|
||||
function rig(): Rig {
|
||||
const scene = setup();
|
||||
scene.frame();
|
||||
@@ -604,23 +610,22 @@ describe("needsFrame / updatePointer / updateRig (contas puras)", () => {
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it("needsFrame: settling wins over everything, interaction only counts on screen", () => {
|
||||
it("isSettling: the intro still running keeps asking, the settled scene does not", () => {
|
||||
const r = rig();
|
||||
r.uniforms.uIntro.value = 1;
|
||||
r.uniforms.uMouseAmt.value = 0.4;
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.4, false, true)).toBe(false);
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.4, true, true)).toBe(true);
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.4, true, false)).toBe(false);
|
||||
expect(needsFrame(r.uniforms, r.state, 0.5, 0.4, false, false)).toBe(true);
|
||||
expect(isSettling(r.uniforms, r.state, 0)).toBe(false);
|
||||
r.uniforms.uIntro.value = 0.5;
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.4, false, false)).toBe(true);
|
||||
expect(isSettling(r.uniforms, r.state, 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("needsFrame: the first frames always ask for another (the cursor strength still has to settle)", () => {
|
||||
it("isSettling: the scroll target far from the shown progress keeps asking, the cursor does not (the pulse does that)", () => {
|
||||
const r = rig();
|
||||
r.uniforms.uIntro.value = 1;
|
||||
r.uniforms.uMouseAmt.value = 0;
|
||||
r.state.frames = 1;
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.4, false, true)).toBe(true);
|
||||
r.state.lastMove = 1;
|
||||
expect(isSettling(r.uniforms, r.state, 0)).toBe(false);
|
||||
expect(isSettling(r.uniforms, r.state, 0.5)).toBe(true);
|
||||
});
|
||||
|
||||
it("updatePointer: returns the strength it is heading for (0.8 with the mouse, 0.4 without, none in the dive)", () => {
|
||||
@@ -648,30 +653,12 @@ describe("needsFrame / updatePointer / updateRig (contas puras)", () => {
|
||||
expect(r.group.rotation.y).toBeCloseTo(0, 9);
|
||||
});
|
||||
|
||||
it("needsFrame: the cursor strength alone still far from its target asks for a frame, but only on screen", () => {
|
||||
it("isSettling: the scroll damping counts as settling only beyond 0.0004 of the target", () => {
|
||||
const r = rig();
|
||||
r.uniforms.uIntro.value = 1;
|
||||
r.uniforms.uMouseAmt.value = 0;
|
||||
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.8, false, true)).toBe(true);
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.8, false, false)).toBe(false);
|
||||
// dentro da tolerância de 0,01 já está assentado
|
||||
r.uniforms.uMouseAmt.value = 0.795;
|
||||
expect(needsFrame(r.uniforms, r.state, 0, 0.8, false, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("needsFrame: the scroll damping counts as settling only beyond 0.0004 of the target", () => {
|
||||
const r = rig();
|
||||
r.uniforms.uIntro.value = 1;
|
||||
r.uniforms.uMouseAmt.value = 0.4;
|
||||
r.state.progress = 0.5;
|
||||
|
||||
expect(needsFrame(r.uniforms, r.state, 0.50039, 0.4, false, false)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(needsFrame(r.uniforms, r.state, 0.5005, 0.4, false, false)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isSettling(r.uniforms, r.state, 0.50039)).toBe(false);
|
||||
expect(isSettling(r.uniforms, r.state, 0.5005)).toBe(true);
|
||||
});
|
||||
|
||||
it("updateRig: the camera follows the choreography -> the first frame before it starts, a smoothed midpoint, the last frame after it ends", () => {
|
||||
@@ -856,12 +843,45 @@ describe("createField: o cursor sozinho não passa de ~30 quadros por segundo",
|
||||
return scene.renderer.render.mock.calls.length - before;
|
||||
}
|
||||
|
||||
function withMovingCursor() {
|
||||
/**
|
||||
* Uma tela de `refreshMs` entre quadros, com tudo assentado e o cursor mexendo: o único relógio é a batida (o
|
||||
* setInterval de 33 ms do FramePulse, aqui tocado à mão), e um quadro da tela só roda se alguém o pediu.
|
||||
*/
|
||||
function pulsedScreen(refreshMs: number) {
|
||||
let beat = () => {};
|
||||
jest.spyOn(window, "setInterval").mockImplementation(((
|
||||
callback: () => void,
|
||||
) => {
|
||||
beat = callback;
|
||||
return 7;
|
||||
}) as never);
|
||||
jest.spyOn(window, "clearInterval").mockImplementation(() => {});
|
||||
let pending: FrameRequestCallback | undefined;
|
||||
frameRequests.mockImplementation(((callback: FrameRequestCallback) => {
|
||||
pending = callback;
|
||||
return 1;
|
||||
}) as never);
|
||||
const scene = setup({ visible: true });
|
||||
scene.settle(200);
|
||||
(performance.now as jest.Mock).mockReturnValue(scene.now());
|
||||
let clock = 0;
|
||||
let nextBeat = 33;
|
||||
const run = (seconds: number) => {
|
||||
const drawnBefore = scene.renderer.render.mock.calls.length;
|
||||
frameRequests.mockClear();
|
||||
const end = clock + seconds * 1000;
|
||||
for (clock += refreshMs; clock <= end; clock += refreshMs) {
|
||||
for (; nextBeat <= clock; nextBeat += 33) beat();
|
||||
const frame = pending;
|
||||
pending = undefined;
|
||||
frame?.(1000 + clock);
|
||||
}
|
||||
return {
|
||||
drawn: scene.renderer.render.mock.calls.length - drawnBefore,
|
||||
requested: frameRequests.mock.calls.length,
|
||||
};
|
||||
};
|
||||
run(6); // a entrada corre a cada quadro da tela e assenta
|
||||
pointerMove("mouse");
|
||||
return scene;
|
||||
return run;
|
||||
}
|
||||
|
||||
it.each([
|
||||
@@ -869,32 +889,25 @@ describe("createField: o cursor sozinho não passa de ~30 quadros por segundo",
|
||||
[120, 1000 / 120],
|
||||
[60, 1000 / 60],
|
||||
])(
|
||||
"path: a %i Hz screen with the cursor moving -> about 30 frames a second, not one per screen frame",
|
||||
"path: a %i Hz screen with the cursor moving -> about 30 frames a second drawn and asked for, never a wake-up per screen frame",
|
||||
(_hz, refreshMs) => {
|
||||
const scene = withMovingCursor();
|
||||
const run = pulsedScreen(refreshMs);
|
||||
|
||||
const drawn = drawnOn(scene, refreshMs, 2);
|
||||
const { drawn, requested } = run(2);
|
||||
|
||||
expect(drawn).toBeGreaterThanOrEqual(2 * 25);
|
||||
expect(drawn).toBeLessThanOrEqual(2 * 31);
|
||||
// quem pede é a batida: nenhum requestAnimationFrame para pular o desenho e pedir outro
|
||||
expect(requested).toBeLessThanOrEqual(drawn + 1);
|
||||
},
|
||||
);
|
||||
|
||||
it("path: a skipped frame draws nothing, keeps the clock of the last drawn one and asks for the next", () => {
|
||||
const scene = withMovingCursor();
|
||||
scene.frame(50); // o último desenhado
|
||||
const drawn = scene.renderer.render.mock.calls.length;
|
||||
|
||||
expect(scene.frame(10)).toBe(1); // 10 ms depois: pula e pede outro
|
||||
expect(scene.renderer.render.mock.calls.length).toBe(drawn);
|
||||
|
||||
expect(scene.frame(10)).toBe(1);
|
||||
expect(scene.frame(10)).toBe(1);
|
||||
expect(scene.renderer.render.mock.calls.length).toBe(drawn);
|
||||
|
||||
// 40 ms depois do último desenhado: sai (o relógio do quadro pulado não conta)
|
||||
scene.frame(10);
|
||||
expect(scene.renderer.render.mock.calls.length).toBe(drawn + 1);
|
||||
it("path: a frame the pulse asks for soon after another is drawn, not skipped (nothing throttles it any more)", () => {
|
||||
const run = pulsedScreen(1000 / 144);
|
||||
const drawn = run(0.5).drawn;
|
||||
expect(drawn).toBeGreaterThanOrEqual(14);
|
||||
// 27,8 ms (4 quadros de 144 Hz) entre duas batidas que se alinharam mal: o desenho sai do mesmo jeito
|
||||
expect(run(0.1).drawn).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("path: the intro and the scroll are not throttled (one frame per screen frame while they settle)", () => {
|
||||
|
||||
Reference in new issue
Block a user