Vídeo pictórico (painterly video)
Você pinta com código da mesma forma que um pintor pinta com um pincel: decida primeiro a imagem (composição, valores, paleta), depois aplique pinceladas reais de trás para frente, olhe o resultado e ajuste. O motor transforma essas pinceladas em quadros; nada é um filtro sobre um render — cada marca é uma pincelada que você colocou.
O método foi aprendido a partir de quatro demonstrações de pintura (processo, pinheiro, árvores pictóricas, nuvens de pôr do sol) e da medição de 13 paisagens mestras em domínio público. As lições destiladas estão em references/; o código funcional está em engine/ e examples/.
Configuração (uma vez por projeto)
cp -R /caminho/para/painterly-video/engine ./painter-engine
cd painter-engine
npm ci
node bin/paint.mjs still ../caminho/para/cena.js --t 8 # → PNG ao lado da cena (stills/<cena>/)
Substitua /caminho/para/painterly-video pela pasta que contém este SKILL.md (por exemplo ~/.claude/skills/painterly-video). Precisa de Node ≥ 22.12, Google Chrome com WebGPU (ou CHROME_PATH), ffmpeg e ffprobe. Estudos de referência adicionalmente precisam de Python com tools/requirements.txt. Nenhuma etapa de build ou chaves de API de renderização; a renderização roda no hardware local. Inicie uma nova cena copiando o arquivo mais próximo em examples/ (sunset-river.js céu/nuvens/água · lone-pine.js vertical, sujeito único · lake-path.js árvores/profundidade/caminho · sunset-study.js céu dramático de massas escuras de nuvens com luz de contorno (um estudo da estrutura de uma demonstração) · mountain-hamlet.js montanhas, cabana, rochas, figura caminhando, fumaça · brush-chart.js pincéis e misturas).
Fluxo de trabalho — não pule as etapas de observação
- Briefing. Sujeito, clima, hora do dia, formato (16:9, 9:16, 1:1 — qualquer
--size), duração e como o tempo é dividido: revelação da pintura, pintura viva, ou ambos (padrão: ~60 % revelação, ~40 % viva). - Estude uma referência quando existir (
tools/study.py, vejareferences/composition.md). Ela relata massas de valor, key, croma, paleta e ponto focal — copie a estrutura, nunca a imagem. - Componha antes da cor. Escreva a composição como um comentário no topo da cena: altura do horizonte (nunca no meio), ponto focal (em um terço, onde vão o contraste de valor mais forte e o croma mais alto), a entrada, o que emoldura, e um plano de valores — um valor (0–1) para cada forma grande, em três massas desiguais.
references/composition.md. - Paleta a partir de pigmentos, colocada no plano de valores.
tone(mix({ ultramarine: 1, transYellowOxide: 1 }), 0.42). Mantenha o croma baixo em todos os lugares, exceto no acento focal (mestres: croma OKLab médio 0.02–0.06, área de alto croma abaixo de ~8 %). - Pinte de trás para frente com os pintores de sujeito (
paintSky,paintCumulus,paintHills,paintMountains,paintWater,paintTreeline,paintPine,paintFir,paintTree,paintGrass,paintRocks,paintHouse,paintGlow,paintMoon,walker,boat,smoke…) elayer.scatter/stroke/pathbruto para qualquer outra coisa. Umap.phase('nome')por estágio, camadas criadas de longe → perto.references/api.md. - Olhe, critique, ajuste — pelo menos duas rodadas. Renderize um still, abra o PNG e julgue como uma pintura: aperte os olhos (as três massas de valor são legíveis?), o ponto focal está onde planejado, algo está igualmente espaçado / igualmente dimensionado / igualmente saturado, alguma marca parece mecânica? Corrija a causa no código. Execute
study.pyno seu próprio still e compare com o plano. Este loop é a habilidade; primeiros renders nunca estão certos. - Tempo.
p.timelapse()para a revelação, depois movimento:sway,drift,shimmer,p.wind,p.camera(paralaxe por profundidade da camada),p.dynamicpara coisas que viajam.references/animation.md. - Folha de contato, depois renderize.
still --n 12 --sheet→ verifique cada estágio da linha do tempo; depoisrender→qa.references/pipeline.md. - Relate honestamente: o que foi verificado automaticamente, o que você olhou, o que você não olhou.
Esqueleto da cena
import { Painting, mix, tone, mixOk, skyColor, paintSky, ridge, paintHills, paintGrass, key, ease } from 'painter';
export const meta = { duration: 16, fps: 30 };
export default function ({ aspect, seed }) { // aspect = largura / altura do render
const p = new Painting({ aspect, seed, ground: '#c9b89a' }); // tela tingida, nunca branca
p.duration = 16;
const A = aspect; // unidades da cena: x 0…A, y 0 (topo) … 1 (fundo)
const skyTop = tone(mix({ ultramarine: 2, titaniumWhite: 2 }), 0.72), skyLow = tone(mix({ cadmiumOrange: 1, titaniumWhite: 3 }), 0.88);
const hillCol = tone(mix({ ultramarine: 1, burntUmber: 1 }), 0.3);
const hill = ridge({ y: 0.68, amp: 0.05, freq: 2, seed });
p.phase('sky');
paintSky(p.layer('sky', { depth: 0 }), { color: skyColor({ top: 0, bottom: 0.7, stops: [[0, skyTop], [1, skyLow]] }), bottom: 0.72 });
p.phase('land');
paintHills(p.layer('hill', { depth: 0.6 }), { top: hill, color: (x, y, k) => hillCol, size: 0.03 });
paintGrass(p.layer('grass', { depth: 1 }), { bounds: [0, 0.8, A, 1.05], dark: hillCol, light: tone(hillCol, 0.5), blade: 0.05 });
p.timelapse({ start: 0.3, end: 10 }); // imagem finalizada aos 10 s
p.camera = (t) => ({ zoom: key(t, [[10, 1], [16, 1.07]], ease.inOut) });
p.wind = (t) => ({ strength: key(t, [[0, 0], [10, 0.15], [12, 1]]) });
return p;
}
Regras que fazem parecer pintado (a lista resumida)
- O valor faz o trabalho, a cor leva o crédito. Planeje três massas de valor desiguais; teste de apertar os olhos em cada still.
- Tela tingida, nunca branca. Escolha a imprimatura para harmonizar a imagem (siena quente sob verdes, creme claro sob um pôr do sol).
- Distância primeiro, depois para frente. Longe = mais claro, mais frio, mais cinza, mais macio, tinta mais fina, marcas menores. Perto = escuros mais escuros, maior contraste e croma, tinta mais grossa, marcas maiores. Use
atmosphere(cor, d, ar). - Escuros primeiro e finos, claros por último e grossos.
thick0.1–0.3 para sombras e distância, 0.8–1 para realces; realces são pequenos e poucos. - Claro contra escuro, escuro contra claro. Quebre cada massa com marcas de um pequeno passo de valor (±0.05) do que está embaixo;
p.valueAt(x, y)/p.sample(x, y)dizem o que está lá. - Pinceladas seguem a forma: ao redor de um aglomerado de nuvem, ao longo de uma encosta, plano na água, para cima em uma lâmina de grama, para baixo em um tronco.
- Misture verdes e os amarronze (
earth(mix({ ultramarine: 1, cadmiumYellow: 1 }), 0.3)); reserve croma puro para um acento. - Varie tudo: tamanhos, espaçamentos, alturas, intervalos. Igual = mecânico. Grupos ímpares, formas sobrepostas, um dominante.
- Arestas perdidas e encontradas: suaves onde as formas se viram ou recuam (
soft0.5+), nítidas no ponto focal. - Função pura do tempo. Uma cena é determinística a partir de
seed; um quadro depende apenas det. Nunca useMath.random()ou tempo de relógio — uselayer.rand/rng(seed).
Onde as coisas dão errado (aprendido construindo isso)
- O branco engole as misturas. O misturador espectral pondera pigmentos pela luminância; uma mistura 1:9 cor:branco é quase branca. Misture o matiz com pouco ou nenhum branco, depois coloque com
tone(c, valor). Corte resultados berrantes comchroma(c, 0.5–0.8). - Nuvens como bolas com gravetos → muitos lóbulos pequenos em uma base plana (
cumulusShapelóbulos 40–60), luz apenas no lado voltado para o sol, pinceladas suaves redondas,thickmodesto. - Pinheiro como palmeira → agulhas são aglomerados arredondados (explosões de pinceladas curtas em leque) sobre os galhos com buracos de céu entre eles, escuro embaixo, claros em cima; galhos curvam para baixo e alcançam para cima.
- Árvores com galhos de guarda-chuva / troncos pretos → os ramos devem terminar dentro da copa; escuros do tronco são valor ~0.22, não preto, e recebem ar (
atmosphere) com a distância. - Uma área inteira em cor saturada (chão laranja, céu turquesa) → amarronze; espalhe a cor pura como algumas poucas marcas pequenas.
- Caminho parece rio → cinza claro quente, pinceladas ao longo da direção do movimento, violeta frio apenas em suas sombras.
- Água feita de traços curtos parece digital → água são longos arrastos horizontais mesclados entre si, com apenas algumas marcas de ondulação perto do observador (o que
paintWaterfaz); não adicione fileiras de traços manualmente. - Mar parece espelho vítreo →
paintWater({ chop: 1 })para dorso de ondas e cristas brancas; mantenhachop: 0para lagos e rios. - Luz de lâmpada / janela é uma mancha plana →
paintGlowsobre um entorno escuro, depois o pequeno núcleo quente; uma longa pincelada suave não é um brilho. - Raios como espinhos brancos → poucos, largos, muito suaves, feixes de baixa opacidade (padrões de
paintRays). - Trama da tela se transforma em grade em tinta fina e clara → mantenha
look.weave≤ 0.2. - Montanhas como parede escamosa ou listras de zebra → construa o horizonte com
peaks(), mantenha a cordilheira distante (tinta fina,size≈ 0.02, faixa de valor estreita) e deixepaintMountainsdividi-la em alguns poucos planos grandes iluminados e sombreados. - Uma faixa plana não é um banco de nuvens / colina / linha de árvores → varie a borda superior, os valores internos e deixe lacunas.
- A cena falha com um erro vago do navegador → a CLI verifica a sintaxe primeiro; um erro em tempo de execução imprime a linha da cena. Uma variável local sombreando uma cor (
leaf,sky,hill) é a causa usual dec.map is not a function. - Lendo um still desatualizado: sempre verifique a saída do comando em busca de um erro antes de abrir o PNG.
- WebGPU e WebCodecs só existem em uma origem segura — a CLI serve
http://127.0.0.1; nunca abra cenas viafile://.
Arquivos de referência
references/painting-lessons.md— o que cada demonstração ensina e como mapeia para chamadas do motor.references/composition.md— compondo uma cena, o plano de valores, números dos mestres,study.py.references/api.md— referência de Painting / camada / pincel / cor / pintor de sujeito, escrevendo um novo pintor de sujeito.references/animation.md— ritmo de revelação, movimento vivo, câmera e paralaxe, camadas dinâmicas, ferver, áudio.references/pipeline.md— CLI, configurações de render, desempenho, QA, solução de problemas, entregáveis.