# Cenas sob medida

Quando nenhuma peça do catálogo serve, a cena é escrita à mão, como nos vídeos que ficaram melhores: um
arquivo por cena nesta pasta (`compositions/cenas/s03_orcamento.html`), cada um uma sub-composição do
HyperFrames. A pasta fica dentro de `compositions/` porque o `python3 build.py hf lint` só confere o que está
ali (uma cena em outra pasta passaria sem lint). O `build.py` encaixa a cena no tempo certo do corte, numa faixa
própria da linha do tempo do estúdio, tanto na `amostra` quanto no `completo`. Os arquivos soltos em
`compositions/` (fora de `cenas/`) são as peças do catálogo que o build gera e apaga a cada build: não escreva lá.

## No projeto.json

```json
"cenas_proprias": [
  {"arquivo": "compositions/cenas/s03_orcamento.html", "de": 12.3, "ate": "cortou", "nome": "Cena · orçamento",
   "faixa": 31, "esconde_legenda": true}
]
```

| campo | o que é |
|---|---|
| `arquivo` | o arquivo da cena, a partir da pasta do projeto |
| `de`, `ate` | segundos do `cut.mp4` ou a palavra (a cena começa ou termina quando a palavra começa); `de_depois_de` e `ate_depois_de` escolhem qual repetição da palavra |
| `nome` | nome da faixa no estúdio (padrão: "Cena · s03 orcamento", tirado do arquivo) |
| `faixa` | número da faixa no estúdio (padrão 30, 31, 32...; só organiza a tela, o render não lê) |
| `esconde_legenda` | `true` quando a cena já escreve a fala (a legenda some nesse trecho) |
| `sobre_a_pessoa` | `true` quando a cena é um cartão pequeno acima da cabeça, com a pessoa em tela cheia (`area: "acima_do_rosto"` nas cenas `k2d_medidor`, `k2d_comando`, `k2d_busca` e `k2d_notas`): a câmera viva fica em zoom 1,0 e não aproxima nesse trecho. O build avisa se um `tela_cheia` do trecho ainda aproxima |

Na `amostra`, só entram as cenas que cruzam o trecho. Se a amostra começa no meio de uma cena, o build põe
`data-playback-start` e a cena continua do ponto certo (não recomeça).

## O que a cena recebe

O `index.html` traz, antes de tudo, `window.__cenas["<id da cena>"]`:

| chave | o que é |
|---|---|
| `de`, `ate`, `duracao` | onde a cena fica no corte, em segundos. Com `segura_fim`, a cena cujo `ate` cai no fim do corte (até 0,25 s antes) vai até o fim do quadro parado, e o `ate` e a `duracao` já vêm com isso |
| `palavras` | a fala do trecho: `[{"texto": "cortou", "t": 3.68, "fim": 4.04}]`, com `t` e `fim` em segundos **da cena** (0 = o `de`). O `t` é o começo medido na onda quando a pasta `edit` tem o `palavras_onda.json` (`helpers/palavras_na_onda.py`); sem ele, o tempo do Whisper, que adianta até 0,3 s |
| `tempo` | de onde vem o `t` das palavras: `"onda"` ou `"whisper"`. Com `"whisper"`, uma batida que precisa cair na palavra pode chegar até 0,3 s cedo: rode o helper e o build de novo |
| `rosto` | a caixa do rosto na tela no começo da cena, em pixels: `{x0, y0, x1, y1, topo}` (`topo` = topo da cabeça estimado); `null` sem rosto |
| `moldura` | quando a cena cruza uma moldura: `{caixa, area_livre, fechada_em}` (`fechada_em` em segundos da cena: antes disso o quadro ainda está fechando e o que aparecer passa na frente do rosto); `null` sem moldura. Cruzar é dividir pelo menos um quadro do render, com os dois trechos meio-abertos, [de, ate): a cena que começa no quadro em que a moldura acaba não recebe `moldura` (senão fica transparente em cima do vídeo em tela cheia), nem quando o tempo foi escrito com 3 casas (`4.933` contra o corte de verdade, 4,93333) ou quando o bloco encostou no corte e a cena ficou no tempo que você escreveu |
| `seguro` | a zona segura do formato `[x0, y0, x1, y1]` (9:16: 140, 230, 940, 1530) |
| `largura`, `altura` | 1080 x 1920, 1920 x 1080, 1080 x 1350 (4:5) ou 1080 x 1080 (1:1) |
| `cores` | as cores do `estilo` do projeto: `papel`, `linha`, `base`, `destaque`, `tinta`, `verde`, `vermelho`. Em estilo escuro (`laranja`) a `tinta` é clara e o `papel` é escuro: letra sobre uma cor clara (o laranja) leva o `papel`, e não a `tinta` (as cenas do kit já escolhem pela luz do fundo) |
| `fundo_css` | o fundo do estilo em CSS (grade e brilho), só nos estilos escuros com fundo próprio (`laranja`); `null` em `papelaria` e `noite`. A cena que pinta o próprio fundo em tela cheia usa ele quando existe (as cenas do kit 2D já usam), para a grade e o brilho não mudarem de um trecho para o outro |

Aberta sozinha no painel Compositions do estúdio, a cena não tem esses dados: use sempre um valor padrão (o
exemplo abaixo faz isso).

A lista `palavras` vem da transcrição do corte e pode mudar quando o corte é conferido de novo (o Whisper junta
ou separa palavras, "o tudo" vira "o" e "tudo", "ééé" vira "éééé"). Nunca conte com um número fixo de palavras:
procure pela palavra (a função `em` do exemplo) e deixe o desenho aguentar uma a mais (linha que quebra sozinha,
`flex-wrap`), senão a cena estoura o painel e o `check` reprova.

## O contrato (o que não pode faltar)

1. Tudo dentro de `<template id="<id>-template">`: o `<style>`, a raiz e o `<script>`. O que fica fora do
   template é jogado fora.
2. A raiz é `<div id="root" data-composition-id="<id>" data-width="1080" data-height="1920" data-duration="...">`
   no tamanho do vídeo do projeto (1920x1080 no 16:9, 1080x1350 no 4:5, 1080x1080 no 1:1; veja "O tamanho da
   raiz") e o estilo dela vai em `#root {...}` (nunca numa classe da raiz). O `<id>` é o nome do arquivo sem
   `.html` (`s03_orcamento`); `main` e `peca-...` são do build.
3. Uma timeline só, pausada, registrada com o mesmo id escrito por extenso:
   `window.__timelines["s03_orcamento"] = tl;` dentro de uma função `(function () { ... })();`.
4. Tempo da cena: 0 é o `de`. Nunca tempo negativo.
5. Determinística e sem depender do seek: `fromTo` com os dois lados, `immediateRender: false` a partir do
   segundo tween do mesmo elemento, nada de `Math.random`, `Date.now`, `repeat: -1` ou timer. Valor que muda a
   cada quadro vai como lista de dados + laço de `tl.set`.
6. Todo elemento nasce com `opacity: 0` no CSS e entra por tween. Anime `opacity`, `x`, `y`, `rotation` e
   `clip-path`; nunca `left/top/width/height`, e nada de `transform` no CSS de quem o GSAP move. Nada de escala
   (`scale`, `scaleX`): depois de um tween de escala o Chrome guarda o desenho numa escala que depende do que a
   página já tocou e da máquina, e dois renders do mesmo projeto saem diferentes. Para "crescer", use `clip-path`
   (`inset(...)` ou `circle(...)`) ou um deslocamento maior. Imagem do tamanho em que aparece: maior que a
   caixa, reduzida pelo navegador, também muda a borda de um render para outro.
7. Texto que muda nunca nasce vazio (escreva o primeiro valor no HTML); número que começa com zero leva o
   espaço de largura zero na frente (`"​01"`).
8. Ids com o prefixo da cena (`#s03-card`), para não bater com as peças do build.
9. Caminho de arquivo sempre a partir da pasta do projeto (`assets/fontes/Montserrat.ttf`,
   `assets/print.png`), nunca `../`. Fonte com `@font-face` dentro do `<style>` da cena.
10. `<video>` dentro da cena: `muted playsinline`, com `id`, `data-start` em tempo da cena, sem `crossorigin`.
    Som vai num `<audio id>` à parte.
11. Espaço entre palavras em elementos separados vai como margem em px, nunca como texto: o HyperFrames
    descarta o texto que é só espaço entre dois elementos (`<span>Não</span> <span>fui</span>` sai "NÃOFUI").
    Use `margin-left: 14px` (em px: `.3em` num span sem `font-size` próprio mede pelos 16 px da raiz e some).
12. Risco em cima de texto reprova o `check` (`text_occluded`), mesmo com `data-layout-allow-occlusion`. Ponha o
    risco por baixo das letras: filho do item, com o item em `position:relative; z-index:0` e o risco em
    `z-index:-1`. Continua parecendo riscado e o check passa.
13. Troca de estado com os dois textos no mesmo lugar ao mesmo tempo reprova (`content_overlap`): o texto velho
    termina de sair antes do novo entrar (ou um sai por um lado e o outro entra pelo outro, sem cruzar).
14. Na moldura, o primeiro elemento entra depois do quadro fechar (`C.moldura.fechada_em`, uns 0,7 s depois
    da troca). O build avisa quando a cena começa antes disso. A cena que começa quando a moldura acaba não é da
    moldura (`moldura: null`): desenhe com fundo próprio, porque o vídeo está em tela cheia.

## O tamanho da raiz

O HyperFrames monta a cena com o tamanho que a raiz declara, não com o do vídeo: uma raiz 1080x1920 num projeto
16:9, 4:5 ou 1:1 sai cortada. O build compara a raiz de cada cena com o vídeo:

- a cena se desenha pelo `largura` e `altura` que recebe (lê os dois do `window.__cenas`, como as cenas do kit 2D
  e 3D): o build põe a raiz no tamanho do vídeo no `index.html` (uma regra com `!important` no host e no filho
  dele, que vence o tamanho que o HyperFrames escreve, no estúdio e no render) e diz numa linha só quais cenas
  ajustou. O arquivo da cena não muda; aberta sozinha no painel Compositions, ela fica no tamanho que declara.
- a cena tem posições fixas (não lê `largura` nem `altura`): o build não mexe e avisa `a raiz declara 1080x1920 e o
  vídeo é 1920x1080: a cena sai cortada`. Troque `data-width` e `data-height` da raiz e refaça as posições para o
  formato (melhor: calcule pelo `largura`, `altura` e `seguro` que a cena recebe).

## Exemplo mínimo (passa no `python3 build.py hf lint` e no `hf check`)

`compositions/cenas/s01_titulo.html`, com `{"arquivo": "compositions/cenas/s01_titulo.html", "de": 0.7, "ate": 5.5}` no
projeto:

```html
<!doctype html>
<html lang="pt-BR"><head><meta charset="UTF-8"/><title>s01 · título</title></head>
<body>
<template id="s01_titulo-template">
<style>
@font-face{font-family:Montserrat;src:url("assets/fontes/Montserrat.ttf") format("truetype");font-weight:100 900;font-display:block}
#root{position:absolute;inset:0;font-family:Montserrat}
#s01-card{position:absolute;left:190px;top:300px;width:700px;height:200px;border-radius:32px;background:#fffdfd;
  display:flex;align-items:center;justify-content:center;opacity:0;box-shadow:0 12px 0 -2px #c7b7ef,0 22px 44px rgba(53,44,64,.35)}
#s01-txt{position:relative;z-index:1;font-weight:900;font-size:64px;color:#352c40;white-space:nowrap}
#s01-risco{position:absolute;left:60px;right:60px;top:50%;height:10px;margin-top:-5px;border-radius:5px;background:#e5484d;opacity:0}
</style>
<div id="root" data-composition-id="s01_titulo" data-width="1080" data-height="1920" data-duration="4.8">
  <div id="s01-card"><span id="s01-txt">UMA VEZ SÓ</span><i id="s01-risco"></i></div>  <!-- risco por baixo das letras -->
</div>
<script>
(function () {
  // palavras do trecho em segundos da cena; aberta sozinha no estúdio a cena usa os valores padrão
  const C = (window.__cenas || {})["s01_titulo"] || { palavras: [], duracao: 4.8 };
  const sem = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
  const em = (texto, depois = 0, padrao = depois) => {
    const p = C.palavras.find((w) => w.t >= depois - 1e-3 && sem(w.texto).startsWith(sem(texto)));
    return p ? p.t : padrao;
  };
  const entra = em("vez", 0, 0.8), risca = em("errei", entra, 1.5);
  const tl = gsap.timeline({ paused: true });
  tl.fromTo("#s01-card", { opacity: 0, y: 60 }, { opacity: 1, y: 0, duration: 0.35, ease: "back.out(2)" }, Math.max(0, entra - 0.1));
  // o risco cresce da esquerda com clip-path (escala deixaria dois renders diferentes)
  tl.fromTo("#s01-risco", { opacity: 1, clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.25, ease: "power2.out", immediateRender: false }, risca);
  tl.to("#s01-card", { opacity: 0, y: -20, duration: 0.25, ease: "power3.in", immediateRender: false }, Math.max(risca + 0.8, (C.duracao || 4.8) - 0.3));
  window.__timelines["s01_titulo"] = tl;
})();
</script>
</template>
</body></html>
```

## Como conferir

1. `python3 build.py amostra <de> <ate>` com o trecho da cena (uns 15 s com motion).
2. `python3 build.py hf lint` e `python3 build.py hf check` sem erro (o `hf` usa a versão do `package.json`).
3. `python3 build.py estudio`: a cena aparece numa faixa com o nome dela; arraste a agulha pela entrada e pela
   saída.
4. Render só no fim (`python3 build.py hf render --quality draft --output renders/amostra.mp4`) e olhe o
   arquivo: ele é a referência, não a prévia.
5. Anote em `eventos.json` (na pasta do projeto) o quadro em que cada mudança de estado pousa, com o tipo do
   som (`[{"t": 12.4, "tipo": "pop"}]`): o `plano_sfx.py --projeto` junta com os sons das peças.

A cena é sua: o build nunca reescreve os arquivos desta pasta. O estúdio, sim: quando abre o projeto, carimba
um `data-hf-id` em cada elemento das cenas (e o arquivo muda no disco). É normal. Ao editar a cena depois disso,
procure o trecho sem contar com o texto exato das tags (uma troca de texto exata falha calada e deixa o elemento
velho): leia o arquivo de novo antes de mudar, ou troque por expressão que ignora o `data-hf-id`.

## Outras cenas prontas para adaptar

- **Kit 2D**: `compositions/cenas/2d/` com oito cenas `k2d_*.html`: o arquivo de vídeo que muda de estado sem
  sair do lugar, a conversa no celular que vira pedido confirmado, o esquema que cresce com a fala (etapas,
  grupos ou causa e efeito), o antes e depois desenhado, a barra que digita o pedido para a IA, o medidor com
  agulha, a barra de busca com o resultado e a chamada e o bloco de notas com a lista que se conclui. Parâmetros no topo de cada uma; quando usar: `references/animacao.md` da skill, seção Kit 2D.
- **3D de verdade**: `compositions/cenas/3d/` (o motor `motor-three.js` e quatro cenas `k3d_*.html`, com o
  three.js r181 em `assets/three`). Como usar e o teste de 5 s: `references/3d.md` da skill.
- **Legenda à mão (papelaria), manchete, legenda que se escreve**: não são estilos da legenda do modelo (que
  tem `padrao` e `discreta`). Escreva como cena própria com `"esconde_legenda": true`, usando as `palavras`.
- **Moldura com o vídeo desfocado atrás**: não precisa de cena, é `"fundo": "desfocado"` no bloco `moldura`.
  Cena própria só quando o arranjo é outro (vidro fosco nos painéis, dois quadros).
