perf(movimento): sem tela vazia, passagens em CSS e vitrine leve
Projetos sobem sobre o palco do hero e o celular tem hero de uma tela sem mergulho; passagens com animation-timeline em CSS, sem GSAP nem motion; troca da vitrine sem texto duplicado; moldura de celular só com pôster e scrub reencodado em 960 px (9,4 para 5,2 MB); 3D monta na primeira interação e para quando parado; transição entre páginas do mesmo segmento; foco em anel duplo; texto de 15 px no celular; código morto removido.
This commit is contained in:
1 parent
10f170abf4
commit
318a764190
77 files changed
+4293
-4951
No files matched your search
@@ -1,6 +1,68 @@
|
||||
import { act, render } from "@testing-library/react";
|
||||
import FilmGrain from "./FilmGrain";
|
||||
|
||||
/* IntersectionObserver controlado: o teste decide quando cada tela de projeto entra ou sai da janela. */
|
||||
type Observer = {
|
||||
callback: IntersectionObserverCallback;
|
||||
observed: Set<Element>;
|
||||
disconnect: jest.Mock;
|
||||
};
|
||||
let observers: Observer[] = [];
|
||||
const original = global.IntersectionObserver;
|
||||
|
||||
beforeEach(() => {
|
||||
observers = [];
|
||||
global.IntersectionObserver = class {
|
||||
private readonly record: Observer;
|
||||
constructor(callback: IntersectionObserverCallback) {
|
||||
this.record = { callback, observed: new Set(), disconnect: jest.fn() };
|
||||
observers.push(this.record);
|
||||
}
|
||||
observe(target: Element) {
|
||||
this.record.observed.add(target);
|
||||
}
|
||||
unobserve(target: Element) {
|
||||
this.record.observed.delete(target);
|
||||
}
|
||||
disconnect() {
|
||||
this.record.disconnect();
|
||||
}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
} as unknown as typeof IntersectionObserver;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.IntersectionObserver = original;
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function addScreen(className: string) {
|
||||
const element = document.createElement("div");
|
||||
element.className = className;
|
||||
document.body.append(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
/** O navegador avisa que `target` entrou (ou saiu) da janela; `height` 0 é uma tela escondida por CSS. */
|
||||
function intersect(target: Element, isIntersecting: boolean, height = 600) {
|
||||
act(() => {
|
||||
for (const observer of observers)
|
||||
if (observer.observed.has(target))
|
||||
observer.callback(
|
||||
[
|
||||
{
|
||||
target,
|
||||
isIntersecting,
|
||||
boundingClientRect: { height },
|
||||
} as unknown as IntersectionObserverEntry,
|
||||
],
|
||||
{} as IntersectionObserver,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe("static cinematic effects", () => {
|
||||
it("renders grain without a canvas", () => {
|
||||
const grain = render(<FilmGrain />);
|
||||
@@ -13,62 +75,36 @@ describe("static cinematic effects", () => {
|
||||
});
|
||||
|
||||
describe("film grain over the project screens", () => {
|
||||
let screenTop = 5000;
|
||||
|
||||
function addScreen(className: string) {
|
||||
const element = document.createElement("div");
|
||||
element.className = className;
|
||||
element.getBoundingClientRect = () =>
|
||||
({ top: screenTop, bottom: screenTop + 600, height: 600 }) as DOMRect;
|
||||
document.body.append(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
function scroll(to: number) {
|
||||
screenTop = to;
|
||||
act(() => {
|
||||
window.dispatchEvent(new Event("scroll"));
|
||||
jest.advanceTimersByTime(40);
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
screenTop = 5000;
|
||||
Object.defineProperty(window, "innerHeight", {
|
||||
configurable: true,
|
||||
value: 800,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
it("steps aside while a project screen is in view and comes back when it leaves", () => {
|
||||
addScreen("ds-reel");
|
||||
const reel = addScreen("ds-reel");
|
||||
const { container } = render(<FilmGrain />);
|
||||
const grain = container.firstElementChild as HTMLElement;
|
||||
expect(grain).not.toHaveAttribute("data-over-projects");
|
||||
|
||||
scroll(300); // a vitrine entrou na janela
|
||||
intersect(reel, true); // a vitrine entrou na janela
|
||||
expect(grain).toHaveAttribute("data-over-projects");
|
||||
|
||||
scroll(-900); // passou: a cena acabou 300 px acima do topo
|
||||
intersect(reel, false); // passou
|
||||
expect(grain).not.toHaveAttribute("data-over-projects");
|
||||
});
|
||||
|
||||
it("also covers the browser and phone frames", () => {
|
||||
addScreen("ds-bframe");
|
||||
const { container } = render(<FilmGrain />);
|
||||
scroll(100);
|
||||
expect(container.firstElementChild).toHaveAttribute("data-over-projects");
|
||||
it("also covers the browser and phone frames and the card screens", () => {
|
||||
for (const className of ["ds-bframe", "ds-phone", "ds-frame"]) {
|
||||
const screen = addScreen(className);
|
||||
const { container, unmount } = render(<FilmGrain />);
|
||||
|
||||
intersect(screen, true);
|
||||
|
||||
expect(container.firstElementChild).toHaveAttribute("data-over-projects");
|
||||
unmount();
|
||||
document.body.innerHTML = "";
|
||||
}
|
||||
});
|
||||
|
||||
it("stays on when there is no project screen on the page", () => {
|
||||
const { container } = render(<FilmGrain />);
|
||||
scroll(100);
|
||||
|
||||
expect(observers[0].observed.size).toBe(0);
|
||||
expect(container.firstElementChild).not.toHaveAttribute(
|
||||
"data-over-projects",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user