- cards com moldura de navegador (endereco real) e brilho na cor do site no card que toca - um video por vez: o do card sob o mouse/foco, ou o mais perto do centro da tela; nada baixa antes de tocar, e um botao pausa os videos que se mexem sozinhos - janela ampliada em <dialog> nativo: troca computador/celular mantendo o ponto do site, capitulos clicaveis, controles proprios, setas entre projetos e foco devolvido ao fechar - dados e capitulos de cada projeto em lib/project-showcase.ts (substitui project-videos.ts) Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
105 lines
3.1 KiB
TypeScript
105 lines
3.1 KiB
TypeScript
import {
|
|
easeToward,
|
|
locateInReel,
|
|
pickActiveCard,
|
|
reelStarts,
|
|
videoProgress,
|
|
} from "./showcase-math";
|
|
|
|
const card = (id: string, visible: boolean, distance: number) => ({
|
|
id,
|
|
visible,
|
|
distance,
|
|
});
|
|
|
|
describe("pickActiveCard", () => {
|
|
it("plays the visible card closest to the middle of the screen", () => {
|
|
expect(
|
|
pickActiveCard(
|
|
[card("a", true, 300), card("b", true, 80), card("c", false, 5)],
|
|
null,
|
|
),
|
|
).toBe("b");
|
|
});
|
|
|
|
it("lets the hovered card win over a closer one, but only while it is visible", () => {
|
|
const cards = [card("a", true, 10), card("b", true, 400)];
|
|
expect(pickActiveCard(cards, "b")).toBe("b");
|
|
expect(
|
|
pickActiveCard([card("a", true, 10), card("b", false, 0)], "b"),
|
|
).toBe("a");
|
|
});
|
|
|
|
it("keeps the first card on a tie, so the left one of a row wins", () => {
|
|
expect(
|
|
pickActiveCard(
|
|
[card("left", true, 120), card("right", true, 120.3)],
|
|
null,
|
|
),
|
|
).toBe("left");
|
|
});
|
|
|
|
it("plays nothing when no card is on screen", () => {
|
|
expect(pickActiveCard([card("a", false, 0)], null)).toBeNull();
|
|
expect(pickActiveCard([], null)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("locateInReel", () => {
|
|
const weights = [1, 2, 1];
|
|
|
|
it("maps overall progress to a project and a position inside it", () => {
|
|
expect(locateInReel(weights, 0)).toEqual({ index: 0, local: 0 });
|
|
expect(locateInReel(weights, 0.125)).toEqual({ index: 0, local: 0.5 });
|
|
const middle = locateInReel(weights, 0.5);
|
|
expect(middle.index).toBe(1);
|
|
expect(middle.local).toBeCloseTo(0.5);
|
|
expect(locateInReel(weights, 1)).toEqual({ index: 2, local: 1 });
|
|
});
|
|
|
|
it("clamps progress outside the pinned range", () => {
|
|
expect(locateInReel(weights, -3)).toEqual({ index: 0, local: 0 });
|
|
expect(locateInReel(weights, 7)).toEqual({ index: 2, local: 1 });
|
|
});
|
|
|
|
it("copes with an empty reel", () => {
|
|
expect(locateInReel([], 0.4)).toEqual({ index: 0, local: 0 });
|
|
});
|
|
|
|
it("gives each project the share of the scroll that its weight asks for", () => {
|
|
expect(reelStarts(weights)).toEqual([0, 0.25, 0.75]);
|
|
});
|
|
});
|
|
|
|
describe("videoProgress", () => {
|
|
it("holds still at the start and at the end of each project's stretch", () => {
|
|
expect(videoProgress(0)).toBe(0);
|
|
expect(videoProgress(0.1)).toBe(0);
|
|
expect(videoProgress(0.5)).toBeCloseTo(0.5);
|
|
expect(videoProgress(0.9)).toBe(1);
|
|
expect(videoProgress(1)).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("easeToward", () => {
|
|
it("moves toward the target and never overshoots", () => {
|
|
const first = easeToward(0, 1, 1 / 60);
|
|
expect(first).toBeGreaterThan(0);
|
|
expect(first).toBeLessThan(1);
|
|
expect(easeToward(0.5, 0.5, 1)).toBe(0.5);
|
|
expect(easeToward(0, 1, 10)).toBeCloseTo(1, 5);
|
|
});
|
|
|
|
it("does not depend on the frame rate", () => {
|
|
let sixty = 0;
|
|
for (let i = 0; i < 60; i += 1) sixty = easeToward(sixty, 1, 1 / 60);
|
|
let thirty = 0;
|
|
for (let i = 0; i < 30; i += 1) thirty = easeToward(thirty, 1, 1 / 30);
|
|
expect(sixty).toBeCloseTo(thirty, 6);
|
|
});
|
|
|
|
it("stands still when no time has passed", () => {
|
|
expect(easeToward(0.2, 1, 0)).toBe(0.2);
|
|
});
|
|
});
|