Video pictórico (óleo animado)
Pintas con código como un pintor con el pincel: decide primero el cuadro (composición, valores, paleta), luego coloca pinceladas reales de atrás hacia adelante, observa el resultado y ajusta. El motor convierte esas pinceladas en fotogramas; nada es un filtro sobre un render — cada marca es una pincelada que tú colocaste.
El método se aprendió de cuatro demostraciones de pintura (proceso, pino, árboles pictóricos, nubes al atardecer) y de medir 13 paisajes maestros de dominio público. Las lecciones destiladas están en references/; el código funcional en engine/ y examples/.
Configuración (una vez por proyecto)
cp -R /ruta/a/painterly-video/engine ./painter-engine
cd painter-engine
npm ci
node bin/paint.mjs still ../ruta/a/escena.js --t 8 # → PNG junto a la escena (stills/<escena>/)
Reemplaza /ruta/a/painterly-video con la carpeta que contiene este SKILL.md (por ejemplo ~/.claude/skills/painterly-video). Necesita Node ≥ 22.12, Google Chrome con WebGPU (o CHROME_PATH), ffmpeg y ffprobe. Los estudios de referencia además necesitan Python con tools/requirements.txt. Sin proceso de compilación ni claves de API de renderizado; el renderizado se ejecuta en hardware local. Inicia una nueva escena copiando el archivo más cercano en examples/ (sunset-river.js cielo/nubes/agua · lone-pine.js vertical, sujeto único · lake-path.js árboles/profundidad/camino · sunset-study.js cielo dramático de masas de nubes oscuras con luz de borde (un estudio de la estructura de una demostración) · mountain-hamlet.js montañas, cabaña, rocas, figura caminando, humo · brush-chart.js pinceles y mezclas).
Flujo de trabajo — no omitas los pasos de observación
- Briefing. Sujeto, estado de ánimo, hora del día, formato (16:9, 9:16, 1:1 — cualquier
--size), duración y cómo se divide el tiempo: revelado de pintura, cuadro vivo o ambos (por defecto: ~60 % revelado, ~40 % vivo). - Estudia una referencia cuando exista (
tools/study.py, verreferences/composition.md). Reporta masas de valor, clave, croma, paleta y punto focal — copia la estructura, nunca el cuadro. - Componer antes que color. Escribe la composición como comentario al inicio de la escena: altura del horizonte (nunca en el medio), punto focal (en un tercio, donde van el contraste de valor más fuerte y el croma más alto), la entrada, qué lo enmarca y un plan de valores — un valor (0–1) para cada forma grande, en tres masas desiguales.
references/composition.md. - Paleta a partir de pigmentos, colocada sobre el plan de valores.
tone(mix({ ultramarine: 1, transYellowOxide: 1 }), 0.42). Mantén el croma bajo en todas partes excepto en el acento focal (maestros: croma medio OKLab 0.02–0.06, área de alto croma bajo ~8 %). - Pintar de atrás hacia adelante con los pinceles de sujeto (
paintSky,paintCumulus,paintHills,paintMountains,paintWater,paintTreeline,paintPine,paintFir,paintTree,paintGrass,paintRocks,paintHouse,paintGlow,paintMoon,walker,boat,smoke…) ylayer.scatter/stroke/pathen bruto para cualquier otra cosa. Unap.phase('nombre')por etapa, capas creadas de lejos a cerca.references/api.md. - Observa, critica, ajusta — al menos dos rondas. Renderiza un fotograma fijo, abre el PNG y evalúalo como una pintura: entrecierra los ojos (¿se leen las tres masas de valor?), ¿está el punto focal donde se planeó?, ¿hay algo igualmente espaciado / del mismo tamaño / igualmente saturado?, ¿alguna marca se ve mecánica? Corrige la causa en el código. Ejecuta
study.pysobre tu propio fotograma y compáralo con el plan. Este bucle es la habilidad; los primeros renders nunca son correctos. - Tiempo.
p.timelapse()para el revelado, luego movimiento:sway,drift,shimmer,p.wind,p.camera(paralaje por profundidad de capa),p.dynamicpara cosas que se desplazan.references/animation.md. - Hoja de contactos, luego render.
still --n 12 --sheet→ revisa cada etapa de la línea de tiempo; luegorender→qa.references/pipeline.md. - Informa con honestidad: qué se revisó automáticamente, qué observaste, qué no.
Esqueleto de escena
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 = ancho / alto del render
const p = new Painting({ aspect, seed, ground: '#c9b89a' }); // lienzo entonado, nunca blanco
p.duration = 16;
const A = aspect; // unidades de escena: x 0…A, y 0 (arriba) … 1 (abajo)
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 }); // cuadro terminado a los 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;
}
Reglas que lo hacen lucir pintado (la lista breve)
- El valor hace el trabajo, el color recibe el crédito. Planifica tres masas de valor desiguales; prueba de entrecerrar los ojos en cada fotograma.
- Fondo entonado, nunca blanco. Elige la imprimatura para armonizar el cuadro (siena cálida bajo verdes, crema pálido bajo un atardecer).
- Lejos primero, luego adelante. Lejos = más claro, más frío, más gris, más suave, pintura más delgada, marcas más pequeñas. Cerca = oscuros más profundos, mayor contraste y croma, pintura más espesa, marcas más grandes. Usa
atmosphere(color, d, air). - Oscuros primero y delgados, claros al final y espesos.
thick0.1–0.3 para sombras y lejanía, 0.8–1 para brillos; los brillos son pequeños y escasos. - Claro contra oscuro, oscuro contra claro. Rompe cada masa con marcas de un pequeño paso de valor (±0.05) respecto a lo que hay debajo;
p.valueAt(x, y)/p.sample(x, y)te indican lo que hay. - Las pinceladas siguen la forma: alrededor de un cúmulo de nubes, a lo largo de una pendiente, planas sobre el agua, hacia arriba en una brizna de hierba, hacia abajo en un tronco.
- Mezcla verdes y tierrálos (
earth(mix({ ultramarine: 1, cadmiumYellow: 1 }), 0.3)); guarda el croma puro para un solo acento. - Varía todo: tamaños, espaciados, alturas, intervalos. Igual = mecánico. Grupos impares, formas superpuestas, un dominante.
- Bordes perdidos y encontrados: suaves donde las formas se alejan o retroceden (
soft0.5+), nítidos en el punto focal. - Función pura del tiempo. Una escena es determinista a partir de
seed; un fotograma depende solo det. Nunca usesMath.random()ni el tiempo real — usalayer.rand/rng(seed).
Dónde suelen fallar las cosas (aprendido al construir esto)
- El blanco se traga las mezclas. El mezclador espectral pondera los pigmentos por luminancia; una mezcla 1:9 color:blanco es casi blanca. Mezcla el tono con poco o nada de blanco, luego colócalo con
tone(c, value). Atenúa resultados chillones conchroma(c, 0.5–0.8). - Nubes como bolas con palos → muchos lóbulos pequeños sobre una base plana (
cumulusShapelóbulos 40–60), luz solo en el lado que da al sol, toques redondos suaves,thickmoderado. - Pino como palmera → las agujas son grupos redondeados (ráfagas de pinceladas cortas en abanico) posados sobre las ramas con huecos de cielo entre ellos, oscuros debajo, claros arriba; las ramas caen bajas y se elevan altas.
- Árboles con ramas en paraguas / troncos negros → las ramas deben terminar dentro de la copa; los oscuros del tronco tienen valor ~0.22, no negro, y reciben
atmospherecon la distancia. - Un área entera de color saturado (suelo naranja, cielo turquesa) → tierrálalo; dispersa el color puro como unas pocas marcas pequeñas.
- El camino parece un río → gris claro cálido, pinceladas en la dirección del recorrido, violeta frío solo en sus sombras.
- Agua hecha de guiones cortos parece digital → el agua son arrastres largos y nivelados fundidos entre sí, con solo unas pocas marcas de ondas cerca del espectador (lo que hace
paintWater); no añadas filas de guiones a mano. - El mar se lee como un espejo →
paintWater({ chop: 1 })para lomos de olas y crestas blancas; manténchop: 0para lagos y ríos. - Luz de lámpara/ventana como un parche plano →
paintGlowsobre un entorno oscuro, luego el pequeño núcleo caliente; una pincelada larga y suave no es un resplandor. - Rayos como púas blancas → pocos, amplios, muy suaves, de baja opacidad (valores predeterminados de
paintRays). - La trama del lienzo se convierte en una rejilla sobre pintura pálida y delgada → mantén
look.weave≤ 0.2. - Montañas como un muro escamoso o rayas de cebra → construye el horizonte con
peaks(), mantén el rango lejano (pintura delgada,size≈ 0.02, rango de valor estrecho) y deja quepaintMountainslo divida en unos pocos planos grandes iluminados y en sombra. - Una banda plana no es un banco de nubes / colina / línea de árboles → varía el borde superior, los valores internos y deja espacios.
- La escena falla con un error vago del navegador → la CLI verifica la sintaxis primero; un error en tiempo de ejecución imprime la línea de la escena. Una variable local que sombrea un color (
leaf,sky,hill) es la causa habitual dec.map is not a function. - Leer un fotograma obsoleto: verifica siempre la salida del comando para detectar errores antes de abrir el PNG.
- WebGPU y WebCodecs solo existen en un origen seguro — la CLI sirve
http://127.0.0.1; nunca abras escenas mediantefile://.
Archivos de referencia
references/painting-lessons.md— qué enseña cada demostración y cómo se asigna a las llamadas del motor.references/composition.md— composición de una escena, el plan de valores, números de los maestros,study.py.references/api.md— Referencia de Painting / capa / pincel / color / pincel de sujeto, cómo escribir un nuevo pincel de sujeto.references/animation.md— ritmo de revelado, movimiento vivo, cámara y paralaje, capas dinámicas, ebullición, audio.references/pipeline.md— CLI, configuración de render, rendimiento, QA, solución de problemas, entregables.