feat(showcase): moldura de navegador, um video por vez e video ampliado nos cards
- 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]>
This commit is contained in:
1 parent
60dc2f1878
commit
a1a1e91135
18 files changed
+3564
-155
No files matched your search
@@ -0,0 +1,91 @@
|
||||
/** Contas puras da vitrine (sem DOM), separadas para serem testadas uma a uma. */
|
||||
|
||||
export interface CardCandidate {
|
||||
id: string;
|
||||
visible: boolean;
|
||||
/** Distância do centro do card ao centro da tela, em pixels. */
|
||||
distance: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Qual card toca: o que está sob o mouse (se visível); senão o visível mais perto do centro da tela.
|
||||
* Empate fica com o primeiro da lista (ordem do DOM: esquerda antes de direita).
|
||||
*/
|
||||
export function pickActiveCard(
|
||||
candidates: CardCandidate[],
|
||||
hoveredId: string | null,
|
||||
): string | null {
|
||||
const visible = candidates.filter((candidate) => candidate.visible);
|
||||
if (hoveredId && visible.some((candidate) => candidate.id === hoveredId))
|
||||
return hoveredId;
|
||||
let best: CardCandidate | null = null;
|
||||
for (const candidate of visible) {
|
||||
if (!best || candidate.distance < best.distance - 0.5) best = candidate;
|
||||
}
|
||||
return best?.id ?? null;
|
||||
}
|
||||
|
||||
export interface ReelPosition {
|
||||
/** Projeto em cena. */
|
||||
index: number;
|
||||
/** Quanto do trecho do projeto já passou (0 a 1). */
|
||||
local: number;
|
||||
}
|
||||
|
||||
/** Em que projeto e em que ponto dele a rolagem está, dado o avanço total `t` (0 a 1) e o peso de cada trecho. */
|
||||
export function locateInReel(weights: number[], t: number): ReelPosition {
|
||||
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
||||
if (weights.length === 0 || total <= 0) return { index: 0, local: 0 };
|
||||
const position = Math.min(1, Math.max(0, t)) * total;
|
||||
let start = 0;
|
||||
for (let index = 0; index < weights.length; index += 1) {
|
||||
const end = start + weights[index];
|
||||
if (position < end || index === weights.length - 1) {
|
||||
const local = (position - start) / weights[index];
|
||||
return { index, local: Math.min(1, Math.max(0, local)) };
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
return { index: weights.length - 1, local: 1 };
|
||||
}
|
||||
|
||||
/** Início do trecho de cada projeto, como fração (0 a 1) do avanço total. */
|
||||
export function reelStarts(weights: number[]): number[] {
|
||||
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
||||
let acc = 0;
|
||||
return weights.map((weight) => {
|
||||
const start = total > 0 ? acc / total : 0;
|
||||
acc += weight;
|
||||
return start;
|
||||
});
|
||||
}
|
||||
|
||||
/** Fração do trecho de cada projeto em que o vídeo fica parado, no começo e no fim. */
|
||||
export const REEL_HOLD = 0.1;
|
||||
|
||||
/** Avanço total (0 a 1) em que o projeto `index` está em `local` (0 a 1) do trecho dele. */
|
||||
export function reelOffset(weights: number[], index: number, local: number) {
|
||||
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
||||
if (total <= 0 || index < 0 || index >= weights.length) return 0;
|
||||
return reelStarts(weights)[index] + (local * weights[index]) / total;
|
||||
}
|
||||
|
||||
/**
|
||||
* O trecho de cada projeto tem uma pausa no começo e outra no fim: o vídeo só anda no meio.
|
||||
* Isso dá tempo de ler o título antes de o site começar a rolar e de descansar antes da troca.
|
||||
*/
|
||||
export function videoProgress(local: number, hold = REEL_HOLD): number {
|
||||
const span = 1 - hold * 2;
|
||||
return Math.min(1, Math.max(0, (local - hold) / span));
|
||||
}
|
||||
|
||||
/** Aproxima `current` de `target` de forma suave, independente da taxa de quadros (`tau` em segundos). */
|
||||
export function easeToward(
|
||||
current: number,
|
||||
target: number,
|
||||
deltaSeconds: number,
|
||||
tau = 0.07,
|
||||
): number {
|
||||
const factor = 1 - Math.exp(-Math.max(0, deltaSeconds) / tau);
|
||||
return current + (target - current) * factor;
|
||||
}
|
||||
Reference in new issue
Block a user