Parte 5 · láminas · versión 1
Láminas: dibujos que se revelan
Una lámina es un dibujo a mano listo para que cualquier motor lo haga aparecer a pluma sin volver a calcular la mano: la imagen se rasteriza una vez y un atlas de tiempo dice cuándo llega la pluma a cada píxel.

Una manícula dibujada a pluma
Una mano con el índice extendido, como las que los lectores antiguos dibujaban en el margen de los libros, sale de una manga con puños de encaje y señala hacia la derecha. Debajo se adivinan las líneas de lápiz con que se encajó el dibujo y, al lado, una nota a mano: «señala; no contesta».
manicula · 640 × 300 · escala 2 · manicula.lamina

Un tintero con una pluma de ave
Un tintero bajo y ancho dibujado a pluma, con una pluma de ave metida en el cuello. Junto a él cae una gota y hay un pequeño charco de tinta azul. Al margen, a mano: «empieza por una frase».
tintero · 340 × 240 · escala 2 · tintero.lamina

Una lupa sobre una página
Una lupa de mango torneado, dibujada a pluma, sobre una página de renglones garabateados con una esquina doblada. Dentro de la lente, dos renglones se ven más grandes. Al margen, a mano: «nada por aquí… todavía».
lupa · 360 × 240 · escala 2 · lupa.lamina
Cómo se revelan
Cada lámina lleva, por capa (lápiz, planos, tinta, color, notas) y por pose de hervor, un atlas de color y un atlas de tiempo. El visor, en WebGL2 y sin dependencias, compone las capas sobre el papel y deja pasar la tinta allí donde la pluma ya ha llegado: reveal = clamp((progreso · 1,03 − t) · 60, 0, 1). Cuando el dibujo está entero, empieza el hervor: tres poses a 8 fotogramas por segundo. Con «reducir movimiento» activado se ve el dibujo terminado, sin animación.
Las tres salen de las referencias de pluma/pruebas/referencia/ con el binding de Python (torculo_pluma), byte a byte iguales que en Rust y en wasm.
El formato (LAMINA.md)
Una lámina es un dibujo a mano listo para que cualquier motor (la web, una app, un juego, el vídeo) lo haga aparecer a pluma sin volver a calcular la mano. Sigue la idea de Ballpoint Gambit: la imagen se rasteriza una vez y, junto a ella, un atlas de tiempo dice en qué instante llega la pluma a cada píxel; un shader de cuatro líneas hace el resto.
La produce torculo_pluma::lamina::hacer (Rust), torculo_pluma.lamina()
(Python), lamina() del wasm y, en la skill, scripts/lamina.ts. La lee el
visor de pluma/visor/ y lamina::fotograma (para sacar fotogramas sin GPU).
1. Dos envoltorios, el mismo contenido
Directorio
nombre.lamina/, con los archivos de la sección 2 tal cual.Empaquetada, un solo archivo
nombre.lamina:bytes contenido 0 a 7 la firma LAMINA\0\x01(4C 41 4D 49 4E 41 00 01)8 a 11 n, largo del índice, u32 little-endian12 a 12+n el índice, JSON UTF-8: {"archivos":[{"ruta","inicio","largo"}…]}desde 12+n los archivos uno tras otro; iniciocuenta desde aquíLas rutas del índice son las mismas que las del directorio. Un lector distingue los dos casos por la firma.
2. Archivos
manifiesto.json lo que hay y cómo se compone (siempre el primero)
geometria.json la geometría vectorial de todas las poses (opcional)
vista.png el dibujo terminado sobre el papel, pose 0 (vista previa)
capas/<capa>-<pose>.png atlas de color de una capa en una pose
capas/<capa>-<pose>-tiempo.png atlas de tiempo de esa capa en esa poseTodas las imágenes de una lámina miden lo mismo: tamano.ancho ×
tamano.alto píxeles, que son la caja del dibujo por la escala
(2 por defecto: retina).
2.1 Atlas de color
PNG RGBA de 8 bits con la alfa premultiplicada (el PNG la declara recta; el
lector debe cargarlo sin tocar los valores: en WebGL,
createImageBitmap(blob, {premultiplyAlpha: 'none', colorSpaceConversion: 'none'})
y UNPACK_PREMULTIPLY_ALPHA_WEBGL = false). Sin papel: lo que no lleva tinta
es transparente. Colores sRGB del canon (canon.toml, papel claro).
2.2 Atlas de tiempo
PNG RGB de 8 bits. Por píxel:
R·256 + Ges el instante normalizado en 16 bits:t = (R·256 + G) / 65535, donde 0 es el arranque de la lámina y 1 suduracion;Bvale 255 si el píxel tiene tinta en esa capa y 0 si no (y entonces R y G no significan nada).
Se muestrea sin filtrar (NEAREST o texelFetch): interpolar mezclaría el
byte alto y el bajo. Si en un píxel se cruzan dos piezas de la misma capa, vale
el instante más temprano.
Cómo se calcula el instante de cada píxel, según la pieza que lo cubre:
| pieza | instante |
|---|---|
| tinta y color (pluma) | el del punto más cercano de su línea central; a lo largo de la línea, inicio + duracion · B⁻¹(arco/largo), con B = cubic-bezier(0.55, 0.1, 0.45, 0.95), la curva --ease-tinta de dibujos.css |
| lápiz | igual, pero lineal: inicio + duracion · arco/largo |
| plano | un pincel en diagonal: inicio + 0,7 s · f, con f la posición en la diagonal de su caja (el SVG lo funde en 0,7 s) |
| nota | se escribe de izquierda a derecha, renglón tras renglón, en 0,9 s (el SVG la funde en 0,9 s) |
inicio y duracion de cada pieza son exactamente los de aSvg (--d y
--t): el lápiz primero y deprisa (28 % del tiempo), luego la tinta y el color
en el orden en que se escribieron, los planos escalonados y las notas al 85 %.
2.3 El revelado
Por cada capa, de abajo arriba, con progreso de 0 a 1:
float t = (R * 255.0 * 256.0 + G * 255.0) / 65535.0;
float reveal = B > 0.5 ? clamp((progreso * 1.03 - t) * 60.0, 0.0, 1.0) : 1.0;
vec4 c = color * reveal; // premultiplicado
dst = mezcla == "multiplicar" ? dst * (1.0 - c.a) + dst * c.rgb
: c.rgb + dst * (1.0 - c.a);dst empieza con el papel. El adelanto de 1,03 garantiza que con
progreso = 1 todo está dibujado; la pendiente de 60 da un frente de tinta de
1/60 de la duración. Los dos números están en revelado del manifiesto.
3. Capas
Las del SVG de boceto.ts, en el mismo orden; solo aparecen las que tienen algo.
| capa | mezcla | desplazamiento | hierve |
|---|---|---|---|
lapiz | normal | ninguno | sí |
planos | multiplicar | ninguno | sí (el borde) |
tinta | normal | ninguno | sí |
color | multiplicar | el desregistro | sí |
encima | multiplicar | el desregistro | sí |
notas | normal | ninguno | no: todas las poses apuntan a la misma imagen |
El desregistro ya está aplicado en los píxeles; desplazamiento se da para
quien dibuje con la geometría.
4. El hervor
hervor.variantes poses (3 por defecto, hervor_poses del canon). La pose 0
es el dibujo de siempre, idéntico al SVG; las demás tienen la misma geometría y
el mismo ritmo de mano y solo cambian el temblor (las tres ondas de temblar
se re-siembran con semilla_de_pose; el desborde de los extremos no cambia,
así que cada trazo tiene los mismos puntos en todas las poses).
En reproducción:
pose = floor(segundos_en_reposo * fps + desfase) mod variantescon fps = 8 (hervor_fps del canon) y desfase = hash32(semilla(id)) · variantes, que viene escrito en el manifiesto. Regla de la casa
(al_reposar: true): mientras la pluma dibuja se ve la pose 0; el hervor
empieza cuando el dibujo está entero.
5. manifiesto.json
{
"formato": "lamina", "version": 1,
"id": "manicula", // el id del dibujo (y su semilla)
"titulo": "…", "descripcion": "…", // para lectores de pantalla, en la lengua de la lámina
"lengua": "es",
"tamano": { "ancho": 1280, "alto": 600, "escala": 2, "caja": [0, 0, 640, 300] },
"duracion": 3.96, // segundos hasta que acaba la última pieza
"papel": "#F5F0E8",
"alfa": "premultiplicada",
"revelado": { "formula": "…", "adelanto": 1.03, "pendiente": 60 },
"hervor": { "variantes": 3, "fps": 8, "desfase": 1.62, "semilla": 3294479249, "al_reposar": true },
"capas": [
{ "nombre": "lapiz", "mezcla": "normal", "desplazamiento": [0, 0], "hierve": true,
"color": ["capas/lapiz-0.png", "capas/lapiz-1.png", "capas/lapiz-2.png"],
"tiempo": ["capas/lapiz-0-tiempo.png", "…"] }
],
"trazos": [ // el orden de la mano: una entrada por pieza
{ "indice": 0, "capa": "lapiz", "elemento": 0, "pasada": 0, "modo": "lineal",
"inicio": 0, "duracion": 0.28, "t0": 0, "t1": 0.0707 }
],
"trayectoria": { // la punta: [ms, x, y, abajo]
"lapiz": [[0, 0.1, 98.2, 0], [0, 0.1, 98.2, 1], …],
"pluma": […]
},
"geometria": "geometria.json", // o null
"vista": "vista.png",
"canon": { "version": 1, "semilla": "fnv1a32-utf8", "serie": "mulberry32" }
}trazos[].modo:lineal(lápiz),pluma(tinta y color),pincel(planos) oescritura(notas).elementoes el índice enDibujo.elementos;pasada, la pasada de la pluma o, en un sombreado, la raya.trayectorialleva dos herramientas, porque el lápiz y la pluma se solapan en el tiempo (la tinta arranca al 60 % del lápiz). Cada punto es[ms, x, y, abajo]en unidades del dibujo (no en píxeles), conabajoa 1 mientras la punta toca el papel y a 0 en el arranque y el final de cada trazo; los milisegundos nunca retroceden (si dos trazos cortos se solapan, el segundo espera). Sirve para un cursor de pluma, para el sonido del plumín (foleyrotulador) o para un robot de dibujo.
6. geometria.json
{ "poses": [ { "id", "pose": 0, "caja", "duracion", "trazos": [ … ] }, … ] }Cada trazo, en el orden de la mano:
| campo | qué es |
|---|---|
tipo | cinta (pluma con presión), linea (lápiz, grosor fijo, remates redondos), relleno (plano) o nota |
capa, elemento, pasada, indice | como en trazos del manifiesto |
tinta, color, opacidad, mezcla, desplazamiento | cómo se pinta |
inicio, duracion, modo | cuándo, en segundos |
puntos | la línea central temblada, sin simplificar |
arco | longitud de arco acumulada en cada punto |
ancho | ancho de la pluma en cada punto, de lado a lado, con la presión (en linea, constante) |
llegada | segundo en que la pluma llega a cada punto |
contorno | en cinta, el polígono simplificado que rellena el SVG; en relleno, el polígono del plano |
nota | en nota: texto, x, y, tam, giro, ancla (letra: fuente/mano.ttf) |
Con puntos y ancho un motor dibuja el trazo nativo de grosor variable
(media anchura a cada lado de la normal, remates redondos en los extremos de
las cintas abiertas); con llegada lo revela al ritmo de la pluma. Los números
van redondeados a milésimas.
7. Garantías
- Determinismo. La misma lámina sale idéntica, byte a byte, en Rust nativo,
en Python y en wasm: el azar es el del canon (FNV-1a de 32 bits sobre UTF-8 y
Mulberry32) y nada toca el reloj ni
Math.random. - Fidelidad. La pose 0 es el mismo dibujo que
aSvg(verpruebas/equivalencia.ts):vista.pngy la captura de Chrome del SVG difieren en menos de medio nivel de gris de media (el suavizado de bordes). - Ids.
azar.tsde la skill siembra por unidades UTF-16 y el canon por bytes UTF-8: coinciden en ASCII. Unidcon «ñ» o tildes daría otro pulso en la lámina que en el SVG; por eso losidvan en ASCII.
8. Memoria
Cada capa en cada pose son dos texturas de ancho × alto × 4 bytes. Una lámina
de 340 × 240 a escala 2 con cinco capas y tres poses ocupa unos 40 MB de GPU;
la constelación (900 × 680) a escala 2, unos 300 MB. Para láminas grandes,
escala 1 o 1,5, o menos variantes (variantes: 1 quita el hervor).