/*
 * A animação da arte (o "Animar" do Canva): cada peça pode ter uma entrada
 * (Surgir, Subir, Pop, Máquina de escrever...), um movimento enquanto está na
 * tela (Pulsar, Flutuar...) e uma saída, cada um com o tempo dele; a página
 * tem a duração e o movimento do fundo.
 *
 * A conta é uma só (estadoNoTempo): o editor toca com ela (palco.tsx) e o
 * vídeo é gravado com ela (video.ts), então o que se vê no editor é o que
 * sai no MP4. Aqui só há conta e conferência, nada de tela.
 */

import { contadorLimpo, type ContadorDaPeca } from "./contador";

export const EFEITOS_DE_ENTRADA = [
  { chave: "surgir", nome: "Fade", detalhe: "Aparece aos poucos (fade in); na saída, some aos poucos (fade out)" },
  { chave: "subir", nome: "Subir", detalhe: "Aparece subindo um pouco", direcao: true },
  { chave: "deslizar", nome: "Deslizar", detalhe: "Entra pelo lado", direcao: true },
  { chave: "revelar", nome: "Revelar", detalhe: "Uma cortina descobre a peça", direcao: true },
  { chave: "pop", nome: "Pop", detalhe: "Pula na tela" },
  { chave: "aproximar", nome: "Aproximar", detalhe: "Cresce até o tamanho certo" },
  { chave: "carimbo", nome: "Carimbo", detalhe: "Cai grande e bate no lugar" },
  { chave: "quicar", nome: "Quicar", detalhe: "Cai do alto e quica" },
  { chave: "girar", nome: "Girar", detalhe: "Chega girando" },
  { chave: "desfocar", nome: "Desfocar", detalhe: "Sai do borrado para o nítido" },
  { chave: "maquina", nome: "Máquina", detalhe: "Escreve linha por linha" },
  { chave: "neon", nome: "Neon", detalhe: "Pisca como letreiro ligando" },
  { chave: "chicote", nome: "Chicote", detalhe: "Chega rápido pelo lado, com rastro", direcao: true },
  /* As que descobrem a peça por uma máscara que anda. */
  { chave: "circulo", nome: "Círculo", detalhe: "Uma máscara redonda abre do meio" },
  { chave: "diagonal", nome: "Diagonal", detalhe: "A máscara corre na diagonal, de um canto ao outro" },
  { chave: "abrir", nome: "Abrir do meio", detalhe: "A máscara abre do meio para os lados" },
  { chave: "persiana", nome: "Persianas", detalhe: "Faixas que abrem juntas, como uma persiana" },
  { chave: "elastico", nome: "Elástico", detalhe: "Estica e volta, como uma mola" },
  /* Em 3D (com perspectiva): giram em volta do eixo do meio. */
  { chave: "virar", nome: "Virar 3D", detalhe: "Vira de lado em 3D, como uma carta" },
  { chave: "tombar", nome: "Tombar 3D", detalhe: "Cai para a frente em 3D, como uma placa" },
] as const;
export type EfeitoDaAnimacao = (typeof EFEITOS_DE_ENTRADA)[number]["chave"];

export const EFEITOS_DE_ENFASE = [
  { chave: "pulsar", nome: "Pulsar", detalhe: "Cresce e volta, como respirando" },
  { chave: "flutuar", nome: "Flutuar", detalhe: "Sobe e desce de leve" },
  { chave: "balancar", nome: "Balançar", detalhe: "Vai de um lado para o outro" },
  { chave: "girar", nome: "Girar", detalhe: "Gira sem parar" },
  { chave: "piscar", nome: "Piscar", detalhe: "Apaga e acende" },
  { chave: "tremer", nome: "Tremer", detalhe: "Dá umas tremidas de tempos em tempos" },
  { chave: "clicar", nome: "Clicar", detalhe: "Afunda e volta, como um clique" },
  { chave: "balancar3d", nome: "Balançar em 3D", detalhe: "Inclina para os lados em 3D, de leve" },
  { chave: "girar3d", nome: "Girar em 3D", detalhe: "Gira em volta do eixo, como uma moeda" },
] as const;
export type EnfaseDaAnimacao = (typeof EFEITOS_DE_ENFASE)[number]["chave"];

export const DIRECOES = ["cima", "baixo", "esquerda", "direita"] as const;
/** Para onde a peça anda (Subir para "cima" vem de baixo). */
export type DirecaoDaAnimacao = (typeof DIRECOES)[number];

export const MOVIMENTOS_DO_FUNDO = [
  { chave: "parado", nome: "Parado" },
  { chave: "aproximar", nome: "Aproximar" },
  { chave: "afastar", nome: "Afastar" },
  { chave: "deslizar", nome: "Deslizar" },
] as const;
export type MovimentoDoFundo = (typeof MOVIMENTOS_DO_FUNDO)[number]["chave"];

/**
 * A animação de uma peça. Tempos em segundos desde o começo da página.
 * `entrada` nula: a peça já está lá (ou aparece seco em `inicio`).
 * `fim` nulo: fica até o fim da página; com `fim` e sem `saida`, some seco.
 */
/*
 * Quadros-chave (o keyframe do CapCut): a pose da peça (lugar, tamanho, giro
 * e transparência, em px da arte) em instantes da cena. Entre um e outro a
 * peça anda sozinha; `curva` é o jeito de chegar no quadro (vindo do anterior).
 * Antes do primeiro e depois do último, fica na pose deles.
 */
export const CURVAS = [
  { chave: "suave", nome: "Suave", detalhe: "Começa e termina devagar" },
  { chave: "linear", nome: "Linear", detalhe: "Velocidade igual o tempo todo" },
  { chave: "acelera", nome: "Acelera", detalhe: "Sai devagar e chega rápido" },
  { chave: "freia", nome: "Freia", detalhe: "Sai rápido e chega devagar" },
  { chave: "mola", nome: "Mola", detalhe: "Passa do ponto e volta, como mola" },
  { chave: "segura", nome: "Segura", detalhe: "Fica parada e pula para o quadro no instante dele" },
] as const;
export type CurvaDoQuadro = (typeof CURVAS)[number]["chave"];
export type PoseDaPeca = { x: number; y: number; w: number; h: number; rot: number; opacidade: number };
export type QuadroChave = PoseDaPeca & { t: number; curva: CurvaDoQuadro };
export const MAXIMO_DE_QUADROS = 60;
/** A distância da "câmera" do 3D, em vezes o lado maior da peça (quanto menor, mais forte a perspectiva). */
export const PERSPECTIVA = 2.2;

/** A fração `p` (0 a 1) andada pela curva. */
export function curvar(curva: CurvaDoQuadro, p: number): number {
  const q = Math.max(0, Math.min(1, p));
  switch (curva) {
    case "linear":
      return q;
    case "acelera":
      return q * q * q;
    case "freia":
      return 1 - Math.pow(1 - q, 3);
    case "mola":
      return q >= 1 ? 1 : 1 - Math.exp(-6 * q) * Math.cos(q * 11);
    case "segura":
      return q >= 1 ? 1 : 0;
    default:
      return q < 0.5 ? 4 * q * q * q : 1 - Math.pow(-2 * q + 2, 3) / 2;
  }
}

/** A pose em `t` pelos quadros (em ordem de tempo). */
export function poseNoTempo(quadros: QuadroChave[], t: number): PoseDaPeca {
  const primeiro = quadros[0];
  if (t <= primeiro.t || quadros.length === 1) return primeiro;
  for (let i = 1; i < quadros.length; i += 1) {
    const b = quadros[i];
    if (t <= b.t) {
      const a = quadros[i - 1];
      const e = curvar(b.curva, (t - a.t) / Math.max(0.001, b.t - a.t));
      const lerp = (x: number, y: number) => x + (y - x) * e;
      return { x: lerp(a.x, b.x), y: lerp(a.y, b.y), w: lerp(a.w, b.w), h: lerp(a.h, b.h), rot: lerp(a.rot, b.rot), opacidade: lerp(a.opacidade, b.opacidade) };
    }
  }
  return quadros[quadros.length - 1];
}

/** A peça anda `dx`, `dy` (px da arte) de `inicio` a `inicio + duracao` e fica lá (o cursor indo até o botão). */
export type MovimentoDaPeca = { dx: number; dy: number; inicio: number; duracao: number };

export type AnimacaoDaPeca = {
  entrada: EfeitoDaAnimacao | null;
  /* Texto: a entrada acontece palavra por palavra (uma depois da outra, dentro da duração). */
  porPalavra?: boolean;
  mover?: MovimentoDaPeca | null;
  quadros?: QuadroChave[];
  /* O número que sobe sozinho (contador.ts): o primeiro número do texto conta de `de` até ele. */
  contador?: ContadorDaPeca | null;
  /* Palavra por palavra no tempo da fala (a legenda automática): quando cada palavra entra, em segundos depois de `inicio`. */
  tempoDasPalavras?: number[];
  /* Num grupo: a peça cuja pose os quadros guardam (as outras andam junto). */
  quadrosDe?: string;
  direcao: DirecaoDaAnimacao;
  inicio: number;
  duracao: number;
  enfase: EnfaseDaAnimacao | null;
  /* Segundos de cada volta da ênfase. */
  ritmo: number;
  saida: EfeitoDaAnimacao | null;
  direcaoDaSaida: DirecaoDaAnimacao;
  fim: number | null;
  duracaoDaSaida: number;
};

export type AnimacaoDaPagina = { duracao: number; fundo: MovimentoDoFundo };

export const TRANSICOES = [
  { chave: "corte", nome: "Corte seco", detalhe: "Troca direto" },
  { chave: "dissolver", nome: "Dissolver", detalhe: "Uma cena some enquanto a outra aparece" },
  { chave: "deslizar", nome: "Deslizar", detalhe: "A cena nova entra por cima, vinda do lado" },
  { chave: "empurrar", nome: "Empurrar", detalhe: "A cena nova empurra a antiga para fora" },
  { chave: "zoom", nome: "Zoom", detalhe: "A antiga cresce e some, a nova aparece" },
  { chave: "cortina", nome: "Cortina", detalhe: "A cena nova é descoberta de um lado ao outro" },
  { chave: "branco", nome: "Flash", detalhe: "Um clarão branco entre as cenas" },
  { chave: "circulo", nome: "Círculo", detalhe: "A cena nova abre num círculo do meio para fora" },
  { chave: "chicote", nome: "Chicote", detalhe: "As duas passam rápido de lado, com rastro" },
  { chave: "preto", nome: "Apagar", detalhe: "Escurece e acende na cena nova" },
  { chave: "diagonal", nome: "Diagonal", detalhe: "A cena nova corre na diagonal, de um canto ao outro" },
  { chave: "persianas", nome: "Persianas", detalhe: "A cena nova abre em faixas, como persiana" },
  { chave: "virar", nome: "Virar 3D", detalhe: "A cena gira em 3D e mostra a nova do outro lado" },
] as const;
export type TipoDeTransicao = (typeof TRANSICOES)[number]["chave"];
/** Como a página entra depois da anterior (vale a partir da 2ª). */
export type TransicaoDaPagina = { tipo: TipoDeTransicao; duracao: number };

export const DURACAO_PADRAO = 5;
export const DURACAO_MINIMA = 1;
export const DURACAO_MAXIMA = 60;

export const usaDirecao = (e: EfeitoDaAnimacao | null) => e === "subir" || e === "deslizar" || e === "revelar" || e === "chicote" || e === "diagonal";
const DIRECAO_DE: Partial<Record<EfeitoDaAnimacao, DirecaoDaAnimacao>> = { subir: "cima", deslizar: "direita", revelar: "direita", chicote: "esquerda", diagonal: "direita" };
export const direcaoPadrao = (e: EfeitoDaAnimacao | null): DirecaoDaAnimacao => (e && DIRECAO_DE[e]) || "cima";

/** A animação nova de uma peça (o que o painel põe no primeiro clique). */
export function animacaoNova(entrada: EfeitoDaAnimacao | null = "surgir", campos: Partial<AnimacaoDaPeca> = {}): AnimacaoDaPeca {
  return { entrada, direcao: direcaoPadrao(entrada), inicio: 0.2, duracao: entrada === "maquina" ? 1.4 : 0.7, enfase: null, ritmo: 1.6, saida: null, direcaoDaSaida: "cima", fim: null, duracaoDaSaida: 0.6, ...campos };
}

/* ------------------------------------------------------------ conferir */

const CHAVES_DE_ENTRADA = EFEITOS_DE_ENTRADA.map((e) => e.chave) as readonly string[];
const CHAVES_DE_ENFASE = EFEITOS_DE_ENFASE.map((e) => e.chave) as readonly string[];
const CHAVES_DO_FUNDO = MOVIMENTOS_DO_FUNDO.map((e) => e.chave) as readonly string[];
const num = (v: unknown, min: number, max: number, reserva: number) => {
  const n = Number(v);
  return Number.isFinite(n) ? Math.max(min, Math.min(max, Math.round(n * 100) / 100)) : reserva;
};
const direcaoLimpa = (v: unknown, reserva: DirecaoDaAnimacao): DirecaoDaAnimacao => ((DIRECOES as readonly string[]).includes(String(v)) ? (v as DirecaoDaAnimacao) : reserva);

/** A animação da peça conferida (nada fora da lista, tempos dentro dos limites); sem nada que mexa, volta null. */
export function animacaoLimpa(bruto: unknown): AnimacaoDaPeca | null {
  if (!bruto || typeof bruto !== "object") return null;
  const a = bruto as Record<string, unknown>;
  const entrada = CHAVES_DE_ENTRADA.includes(String(a.entrada)) ? (a.entrada as EfeitoDaAnimacao) : null;
  const saida = CHAVES_DE_ENTRADA.includes(String(a.saida)) ? (a.saida as EfeitoDaAnimacao) : null;
  const enfase = CHAVES_DE_ENFASE.includes(String(a.enfase)) ? (a.enfase as EnfaseDaAnimacao) : null;
  const inicio = num(a.inicio, 0, DURACAO_MAXIMA, 0);
  const fim = a.fim === null || a.fim === undefined ? null : num(a.fim, 0, DURACAO_MAXIMA, 0);
  const m = a.mover && typeof a.mover === "object" ? (a.mover as Record<string, unknown>) : null;
  const mover: MovimentoDaPeca | null = m ? { dx: num(m.dx, -20000, 20000, 0), dy: num(m.dy, -20000, 20000, 0), inicio: num(m.inicio, 0, DURACAO_MAXIMA, 0), duracao: num(m.duracao, 0.1, 10, 0.8) } : null;
  const quadros = quadrosLimpos(a.quadros);
  const limpa: AnimacaoDaPeca = {
    entrada,
    ...(quadros.length ? { quadros } : {}),
    ...(() => {
      const contador = contadorLimpo(a.contador, num, CHAVES_DAS_CURVAS);
      return contador ? { contador } : {};
    })(),
    ...(Array.isArray(a.tempoDasPalavras) && a.tempoDasPalavras.length ? { tempoDasPalavras: a.tempoDasPalavras.slice(0, 80).map((v) => num(v, 0, DURACAO_MAXIMA, 0)) } : {}),
    ...(quadros.length && typeof a.quadrosDe === "string" && a.quadrosDe ? { quadrosDe: a.quadrosDe.replace(/[^a-z0-9_-]/gi, "").slice(0, 40) } : {}),
    ...(a.porPalavra === true && entrada && entrada !== "maquina" ? { porPalavra: true } : {}),
    ...(mover && (mover.dx || mover.dy) ? { mover } : {}),
    direcao: direcaoLimpa(a.direcao, direcaoPadrao(entrada)),
    inicio,
    duracao: num(a.duracao, 0.1, 6, 0.7),
    enfase,
    ritmo: num(a.ritmo, 0.4, 8, 1.6),
    saida,
    direcaoDaSaida: direcaoLimpa(a.direcaoDaSaida, "cima"),
    fim: fim !== null && fim > inicio ? fim : null,
    duracaoDaSaida: num(a.duracaoDaSaida, 0.1, 6, 0.6),
  };
  return limpa.entrada || limpa.enfase || limpa.mover || limpa.quadros || limpa.contador || limpa.inicio > 0 || limpa.fim !== null ? limpa : null;
}

const CHAVES_DAS_CURVAS = CURVAS.map((c) => c.chave) as readonly string[];
/** Os quadros conferidos: em ordem, um por instante, números dentro dos limites. */
export function quadrosLimpos(bruto: unknown): QuadroChave[] {
  if (!Array.isArray(bruto)) return [];
  const lista = bruto.slice(0, MAXIMO_DE_QUADROS * 2).flatMap((item): QuadroChave[] => {
    if (!item || typeof item !== "object") return [];
    const q = item as Record<string, unknown>;
    if (![q.t, q.x, q.y, q.w, q.h].every((v) => Number.isFinite(Number(v)))) return [];
    return [
      {
        t: num(q.t, 0, DURACAO_MAXIMA, 0),
        x: num(q.x, -40000, 40000, 0),
        y: num(q.y, -40000, 40000, 0),
        w: num(q.w, 1, 40000, 100),
        h: num(q.h, 1, 40000, 100),
        rot: num(q.rot, -3600, 3600, 0),
        opacidade: num(q.opacidade, 0, 1, 1),
        curva: CHAVES_DAS_CURVAS.includes(String(q.curva)) ? (q.curva as CurvaDoQuadro) : "suave",
      },
    ];
  });
  lista.sort((a, b) => a.t - b.t);
  return lista.filter((q, i) => i === 0 || q.t - lista[i - 1].t > 0.02).slice(0, MAXIMO_DE_QUADROS);
}

const CHAVES_DE_TRANSICAO = TRANSICOES.map((e) => e.chave) as readonly string[];
export function transicaoLimpa(bruto: unknown): TransicaoDaPagina | null {
  if (!bruto || typeof bruto !== "object") return null;
  const a = bruto as Record<string, unknown>;
  if (!CHAVES_DE_TRANSICAO.includes(String(a.tipo)) || a.tipo === "corte") return null;
  return { tipo: a.tipo as TipoDeTransicao, duracao: num(a.duracao, 0.2, 2, 0.6) };
}

export function animacaoDaPaginaLimpa(bruto: unknown): AnimacaoDaPagina | null {
  if (!bruto || typeof bruto !== "object") return null;
  const a = bruto as Record<string, unknown>;
  return { duracao: num(a.duracao, DURACAO_MINIMA, DURACAO_MAXIMA, DURACAO_PADRAO), fundo: CHAVES_DO_FUNDO.includes(String(a.fundo)) ? (a.fundo as MovimentoDoFundo) : "parado" };
}

/* --------------------------------------------------------------- conta */

/**
 * Um recorte que descobre a peça: a caixa (frações da caixa da peça) ou as
 * linhas da Máquina de escrever (`feitas` linhas inteiras, a parte quebrada
 * é a linha sendo escrita).
 */
/* "circulo": centro em frações da caixa, raio em fração da distância até o canto mais longe (1 cobre tudo). */
/* "poligono": pontos em frações da caixa (as persianas são um polígono só, em zigue-zague). */
export type RecorteDaCena = { tipo: "caixa"; x0: number; y0: number; x1: number; y1: number } | { tipo: "linhas"; linhas: number; feitas: number } | { tipo: "circulo"; cx: number; cy: number; r: number } | { tipo: "poligono"; pontos: Array<[number, number]> };

/**
 * Como a peça está num instante: transparência, deslocamento e giro (em px da
 * arte), tamanho, desfoque, recorte e o rastro do movimento (o borrão para
 * trás, em px). Na entrada palavra por palavra, `partes`: cada palavra (a
 * caixa dela na página) desenhada à parte, no estado dela.
 */
/* `rx`, `ry`: giro em 3D (graus) em volta dos eixos do meio, com perspectiva (perspectiva.ts desenha no MP4). */
export type EstadoNaCena = { alfa: number; dx: number; dy: number; escala: number; giro: number; desfoque: number; recorte: RecorteDaCena | null; rastro: [number, number]; partes: ParteDaCena[] | null; rx: number; ry: number };
export type ParteDaCena = { caixa: Caixa; estado: EstadoNaCena };

export const PARADO: EstadoNaCena = { alfa: 1, dx: 0, dy: 0, escala: 1, giro: 0, desfoque: 0, recorte: null, rastro: [0, 0], partes: null, rx: 0, ry: 0 };
const OCULTO: EstadoNaCena = { ...PARADO, alfa: 0 };

export type Caixa = { x: number; y: number; w: number; h: number };
/** O que a conta precisa saber da peça: a caixa dela, o tamanho da página, quantas linhas tem e onde estão as palavras (texto, medidas no navegador). */
/* `base`: a pose guardada da peça (a camada é desenhada nela); os quadros-chave andam a partir dela. */
export type ContextoDaCena = { caixa: Caixa; largura: number; altura: number; linhas: number; palavras?: Caixa[]; base?: PoseDaPeca };

const limitar = (v: number) => Math.max(0, Math.min(1, v));
const suave = (t: number) => 1 - Math.pow(1 - t, 3);
const forte = (t: number) => 1 - Math.pow(1 - t, 4);
const comPulo = (t: number) => {
  const c1 = 1.70158;
  const c3 = c1 + 1;
  return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
};
const quique = (t: number) => {
  const n = 7.5625;
  const d = 2.75;
  if (t < 1 / d) return n * t * t;
  if (t < 2 / d) return n * (t -= 1.5 / d) * t + 0.75;
  if (t < 2.5 / d) return n * (t -= 2.25 / d) * t + 0.9375;
  return n * (t -= 2.625 / d) * t + 0.984375;
};
/* O letreiro ligando: apaga e acende algumas vezes e firma. */
const NEON: Array<[number, number]> = [[0, 0], [0.1, 0], [0.12, 1], [0.2, 1], [0.22, 0.1], [0.3, 0.1], [0.32, 1], [0.42, 1], [0.44, 0.35], [0.52, 0.35], [0.54, 1], [1, 1]];
function neon(p: number) {
  for (let i = 1; i < NEON.length; i += 1) {
    const [x1, y1] = NEON[i];
    const [x0, y0] = NEON[i - 1];
    if (p <= x1) return y0 + ((y1 - y0) * (p - x0)) / Math.max(1e-6, x1 - x0);
  }
  return 1;
}
const VETOR: Record<DirecaoDaAnimacao, [number, number]> = { cima: [0, -1], baixo: [0, 1], esquerda: [-1, 0], direita: [1, 0] };
const OPOSTA: Record<DirecaoDaAnimacao, DirecaoDaAnimacao> = { cima: "baixo", baixo: "cima", esquerda: "direita", direita: "esquerda" };

/** O efeito em `p` (0 = ainda fora, 1 = no lugar). */
function noEfeito(efeito: EfeitoDaAnimacao, direcao: DirecaoDaAnimacao, p: number, c: ContextoDaCena): EstadoNaCena {
  const e = suave(p);
  const lado = Math.max(c.largura, c.altura);
  const [vx, vy] = VETOR[direcao];
  switch (efeito) {
    case "surgir":
      return { ...PARADO, alfa: e };
    case "subir": {
      const d = lado * 0.05 * (1 - e);
      return { ...PARADO, alfa: e, dx: -vx * d, dy: -vy * d };
    }
    case "deslizar": {
      const d = (vx ? c.largura * 0.35 : c.altura * 0.3) * (1 - forte(p));
      return { ...PARADO, alfa: limitar(p * 3), dx: -vx * d, dy: -vy * d };
    }
    case "revelar": {
      const r = forte(p);
      const recorte: RecorteDaCena =
        direcao === "direita" ? { tipo: "caixa", x0: 0, y0: 0, x1: r, y1: 1 } : direcao === "esquerda" ? { tipo: "caixa", x0: 1 - r, y0: 0, x1: 1, y1: 1 } : direcao === "baixo" ? { tipo: "caixa", x0: 0, y0: 0, x1: 1, y1: r } : { tipo: "caixa", x0: 0, y0: 1 - r, x1: 1, y1: 1 };
      return { ...PARADO, recorte: r >= 1 ? null : recorte };
    }
    case "pop":
      return { ...PARADO, alfa: limitar(p * 3), escala: 0.3 + 0.7 * comPulo(p) };
    case "aproximar":
      return { ...PARADO, alfa: e, escala: 0.6 + 0.4 * e };
    case "carimbo":
      return { ...PARADO, alfa: limitar(p * 4), escala: 1 + 1.6 * (1 - forte(p)) };
    case "quicar":
      return { ...PARADO, alfa: limitar(p * 5), dy: -(1 - quique(p)) * c.altura * 0.3 };
    case "girar":
      return { ...PARADO, alfa: limitar(p * 2), giro: -(1 - e) * 180, escala: 0.4 + 0.6 * e };
    case "desfocar":
      return { ...PARADO, alfa: e, desfoque: (1 - e) * Math.max(8, lado * 0.02), escala: 1.06 - 0.06 * e };
    case "maquina": {
      if (p >= 1) return PARADO;
      /* Cada linha em 14 batidas (as letras aparecem aos saltos, como digitando). */
      const total = Math.max(1, c.linhas);
      const feitas = Math.floor(p * total * 14) / 14;
      return { ...PARADO, recorte: { tipo: "linhas", linhas: total, feitas } };
    }
    case "neon":
      return { ...PARADO, alfa: neon(p) };
    case "circulo": {
      const r = forte(p);
      return { ...PARADO, recorte: r >= 1 ? null : { tipo: "circulo", cx: 0.5, cy: 0.5, r } };
    }
    case "abrir": {
      const r = forte(p);
      return { ...PARADO, recorte: r >= 1 ? null : { tipo: "caixa", x0: 0.5 - r / 2, y0: 0, x1: 0.5 + r / 2, y1: 1 } };
    }
    case "diagonal": {
      const r = forte(p);
      return { ...PARADO, recorte: r >= 1 ? null : { tipo: "poligono", pontos: diagonal(r, direcao) } };
    }
    case "persiana": {
      const r = forte(p);
      if (r >= 1) return PARADO;
      return { ...PARADO, recorte: { tipo: "poligono", pontos: persianas(r) } };
    }
    case "virar":
      return { ...PARADO, alfa: limitar(p * 4), ry: -(1 - comPulo(p)) * 90 };
    case "tombar":
      return { ...PARADO, alfa: limitar(p * 4), rx: (1 - comPulo(p)) * 90 };
    case "elastico": {
      const q = Math.max(0, Math.min(1, p));
      return { ...PARADO, alfa: limitar(p * 5), escala: q >= 1 ? 1 : 1 - Math.exp(-6 * q) * Math.cos(q * 12) * 0.9 };
    }
    case "chicote": {
      /* Chega rápido e freia; o rastro é do tamanho do que falta andar (some ao chegar). */
      const longe = vx ? c.largura * 0.6 : c.altura * 0.45;
      const falta = longe * Math.pow(2, -10 * p) * (1 - p);
      const rastro = Math.min(falta * 0.8, lado * 0.22);
      return { ...PARADO, alfa: limitar(p * 6), dx: -vx * falta, dy: -vy * falta, rastro: [-vx * rastro, -vy * rastro] };
    }
  }
}

/** O efeito contínuo, `tau` segundos depois de a peça chegar; `forca` sobe e desce nas pontas (sem tranco). */
function comEnfase(estado: EstadoNaCena, enfase: EnfaseDaAnimacao, tau: number, ritmo: number, forca: number, c: ContextoDaCena): EstadoNaCena {
  const lado = Math.max(c.largura, c.altura);
  const volta = (2 * Math.PI * tau) / ritmo;
  switch (enfase) {
    case "pulsar":
      return { ...estado, escala: estado.escala * (1 + 0.07 * forca * (0.5 - 0.5 * Math.cos(volta))) };
    case "flutuar":
      return { ...estado, dy: estado.dy - forca * lado * 0.012 * Math.sin(volta) };
    case "balancar":
      return { ...estado, giro: estado.giro + forca * 6 * Math.sin(volta) };
    case "girar":
      return { ...estado, giro: estado.giro + (360 * tau) / ritmo };
    case "piscar":
      return { ...estado, alfa: estado.alfa * (1 - forca * 0.6 * (0.5 - 0.5 * Math.cos(volta))) };
    case "balancar3d":
      return { ...estado, ry: estado.ry + forca * 18 * Math.sin(volta), rx: estado.rx + forca * 6 * Math.sin(volta * 0.5) };
    case "girar3d":
      return { ...estado, ry: estado.ry + (360 * tau) / ritmo };
    case "clicar": {
      const fase = (tau % ritmo) / ritmo;
      return fase < 0.2 ? { ...estado, escala: estado.escala * (1 - forca * 0.14 * Math.sin((Math.PI * fase) / 0.2)) } : estado;
    }
    case "tremer": {
      const fase = (tau % ritmo) / ritmo;
      return fase < 0.22 ? { ...estado, dx: estado.dx + forca * lado * 0.006 * Math.sin(fase * 2 * Math.PI * 7), giro: estado.giro + forca * 2 * Math.sin(fase * 2 * Math.PI * 7) } : estado;
    }
  }
}

/** Como a peça está em `t` segundos. Sem animação, parada no lugar. */
export function estadoNoTempo(a: AnimacaoDaPeca | null | undefined, t: number, c: ContextoDaCena): EstadoNaCena {
  if (!a) return PARADO;
  if (t < a.inicio && (a.entrada || a.inicio > 0)) return OCULTO;
  const chegou = a.entrada ? a.inicio + a.duracao : a.inicio;
  let estado = PARADO;
  if (a.entrada && t < chegou) estado = a.porPalavra && c.palavras && c.palavras.length > 1 ? porPalavras(a, t, c, c.palavras) : noEfeito(a.entrada, a.direcao, limitar((t - a.inicio) / a.duracao), c);
  if (a.fim !== null && t >= a.fim) {
    if (!a.saida) return OCULTO;
    const q = limitar((t - a.fim) / a.duracaoDaSaida);
    if (q >= 1) return OCULTO;
    /* A saída é a entrada ao contrário, andando para o lado escolhido. */
    estado = noEfeito(a.saida, OPOSTA[a.direcaoDaSaida], 1 - q, c);
  }
  if (a.enfase && t >= chegou) {
    const ate = a.fim ?? Infinity;
    const forca = a.enfase === "girar" ? 1 : Math.min(limitar((t - chegou) / 0.3), limitar((ate - t) / 0.3));
    estado = comEnfase(estado, a.enfase, t - chegou, a.ritmo, forca, c);
  }
  if (a.quadros?.length && c.base) estado = comQuadros(estado, poseNoTempo(a.quadros, t), c.base, c.caixa);
  if (a.mover && t > a.mover.inicio) {
    const q = limitar((t - a.mover.inicio) / a.mover.duracao);
    const e = q < 0.5 ? 4 * q * q * q : 1 - Math.pow(-2 * q + 2, 3) / 2;
    estado = { ...estado, dx: estado.dx + a.mover.dx * e, dy: estado.dy + a.mover.dy * e };
  }
  return estado;
}

/*
 * A pose dos quadros-chave por cima do estado do efeito: a camada (desenhada
 * na pose guardada) gira e cresce em volta do centro dela e anda até o centro
 * da pose do instante. O efeito (entrada, ênfase) vale por cima disso.
 */
function comQuadros(e: EstadoNaCena, k: PoseDaPeca, b: PoseDaPeca, caixa: Caixa): EstadoNaCena {
  const s = Math.sqrt((k.w * k.h) / Math.max(1, b.w * b.h));
  const g = k.rot - b.rot;
  const [cx, cy] = [caixa.x + caixa.w / 2, caixa.y + caixa.h / 2];
  const giro = (ang: number, x: number, y: number): [number, number] => {
    const r = (ang * Math.PI) / 180;
    return [x * Math.cos(r) - y * Math.sin(r), x * Math.sin(r) + y * Math.cos(r)];
  };
  /* A translação da pose: o centro guardado cai no centro do instante. */
  const [bx, by] = giro(g, (b.x + b.w / 2 - cx) * s, (b.y + b.h / 2 - cy) * s);
  const tx = k.x + k.w / 2 - cx - bx;
  const ty = k.y + k.h / 2 - cy - by;
  /* Composta com a do efeito: d = d_efeito + R(giro_efeito)·escala_efeito·t_pose. */
  const [ex, ey] = giro(e.giro, tx * e.escala, ty * e.escala);
  return { ...e, dx: e.dx + ex, dy: e.dy + ey, giro: e.giro + g, escala: e.escala * s, alfa: e.alfa * Math.min(1 / Math.max(0.01, b.opacidade), k.opacidade / Math.max(0.01, b.opacidade)) };
}

/* A cortina diagonal: a parte da caixa (em frações) já descoberta, de um canto ao outro. */
export function diagonal(r: number, direcao: DirecaoDaAnimacao): Array<[number, number]> {
  /* Linha x + y = 2r (de cima à esquerda para baixo à direita), e as outras direções espelhadas. */
  const k = 2 * r;
  const base: Array<[number, number]> = k <= 1 ? [[0, 0], [k, 0], [0, k]] : [[0, 0], [1, 0], [1, k - 1], [k - 1, 1], [0, 1]];
  const espelho = (x: number, y: number): [number, number] => (direcao === "esquerda" ? [1 - x, y] : direcao === "cima" ? [x, 1 - y] : direcao === "baixo" ? [1 - x, 1 - y] : [x, y]);
  return base.map(([x, y]) => espelho(x, y));
}

/* As persianas: faixas abertas `r` (0 a 1) cada uma, num polígono só em zigue-zague. */
export function persianas(r: number, faixas = 7): Array<[number, number]> {
  const pontos: Array<[number, number]> = [];
  for (let i = 0; i < faixas; i += 1) {
    const y0 = i / faixas;
    const y1 = y0 + r / faixas;
    pontos.push([0, y0], [1, y0], [1, y1], [0, y1]);
  }
  return pontos;
}

/*
 * A entrada palavra por palavra: cada palavra faz o efeito inteiro num pedaço
 * da duração, a seguinte começando antes de a anterior terminar (a última
 * chega junto com o fim da duração).
 */
function porPalavras(a: AnimacaoDaPeca, t: number, c: ContextoDaCena, palavras: Caixa[]): EstadoNaCena {
  const n = palavras.length;
  /* No tempo da fala: cada palavra entra no segundo dela (e a entrada é curta). */
  const daFala = a.tempoDasPalavras && a.tempoDasPalavras.length === n ? a.tempoDasPalavras : null;
  const cada = daFala ? 0.22 : Math.min(a.duracao, Math.max(0.15, Math.min(0.6, a.duracao * 0.45)));
  const passo = (a.duracao - cada) / Math.max(1, n - 1);
  const partes = palavras.map((caixa, i) => {
    const p = limitar((t - a.inicio - (daFala ? daFala[i] : i * passo)) / cada);
    return { caixa, estado: p <= 0 ? OCULTO : noEfeito(a.entrada!, a.direcao, p, { ...c, caixa, linhas: 1 }) };
  });
  return { ...PARADO, partes };
}

/** O fundo durante a página: um zoom lento ou um passeio de lado (no centro da página). */
export function estadoDoFundo(movimento: MovimentoDoFundo | undefined, t: number, duracao: number, largura: number): EstadoNaCena {
  const p = limitar(t / Math.max(0.1, duracao));
  const ida = p * p * (3 - 2 * p);
  if (movimento === "aproximar") return { ...PARADO, escala: 1 + 0.08 * ida };
  if (movimento === "afastar") return { ...PARADO, escala: 1.08 - 0.08 * ida };
  if (movimento === "deslizar") return { ...PARADO, escala: 1.1, dx: (ida - 0.5) * largura * 0.07 };
  return PARADO;
}

/** O recorte em pontos da arte (um polígono), dentro da caixa da peça. */
export function poligonoDoRecorte(r: RecorteDaCena, caixa: Caixa): Array<[number, number]> {
  const { x, y, w, h } = caixa;
  if (r.tipo === "poligono") return r.pontos.map(([px, py]) => [x + w * px, y + h * py]);
  if (r.tipo === "circulo") {
    const cx = x + w * r.cx;
    const cy = y + h * r.cy;
    const raio = Math.max(0, r.r) * Math.hypot(Math.max(r.cx, 1 - r.cx) * w, Math.max(r.cy, 1 - r.cy) * h);
    return Array.from({ length: 72 }, (_, i) => [cx + raio * Math.cos((i / 72) * Math.PI * 2), cy + raio * Math.sin((i / 72) * Math.PI * 2)] as [number, number]);
  }
  if (r.tipo === "caixa") {
    const a = x + w * r.x0;
    const b = x + w * r.x1;
    const c = y + h * r.y0;
    const d = y + h * r.y1;
    return [[a, c], [b, c], [b, d], [a, d]];
  }
  const alto = h / r.linhas;
  const inteiras = Math.min(r.linhas, Math.floor(r.feitas));
  const parte = x + w * (r.feitas - inteiras);
  const meio = y + alto * inteiras;
  if (inteiras >= r.linhas) return [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
  if (inteiras === 0) return [[x, y], [parte, y], [parte, y + alto], [x, y + alto]];
  return [[x, y], [x + w, y], [x + w, meio], [parte, meio], [parte, meio + alto], [x, meio + alto]];
}

/* ------------------------------------------------- textos para a tela */

export const nomeDoEfeito = (e: EfeitoDaAnimacao | null) => (e ? EFEITOS_DE_ENTRADA.find((x) => x.chave === e)?.nome ?? e : "Nenhuma");
export const nomeDaEnfase = (e: EnfaseDaAnimacao | null) => (e ? EFEITOS_DE_ENFASE.find((x) => x.chave === e)?.nome ?? e : "Nenhum");
/** "1,5 s" */
export const segundos = (s: number) => `${(Math.round(s * 10) / 10).toFixed(1).replace(".", ",")} s`;
