Saltar al contenido
Tórculo1.0 · borrador de trabajo

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 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».
Una manícula dibujada a pluma.lamina · v1
3,96 s · 3 poses de hervor

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 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».
Un tintero con una pluma de ave.lamina · v1
2,60 s · 3 poses de hervor

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 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».
Una lupa sobre una página.lamina · v1
2,60 s · 3 poses de hervor

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:

    bytescontenido
    0 a 7la firma LAMINA\0\x01 (4C 41 4D 49 4E 41 00 01)
    8 a 11n, largo del índice, u32 little-endian
    12 a 12+nel índice, JSON UTF-8: {"archivos":[{"ruta","inicio","largo"}…]}
    desde 12+nlos archivos uno tras otro; inicio cuenta 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 pose

Todas 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 + G es el instante normalizado en 16 bits: t = (R·256 + G) / 65535, donde 0 es el arranque de la lámina y 1 su duracion;
  • B vale 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:

piezainstante
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ápizigual, pero lineal: inicio + duracion · arco/largo
planoun 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)
notase 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.

capamezcladesplazamientohierve
lapiznormalningunosí
planosmultiplicarningunosí (el borde)
tintanormalningunosí
colormultiplicarel desregistrosí
encimamultiplicarel desregistrosí
notasnormalningunono: 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 variantes

con 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) o escritura (notas). elemento es el índice en Dibujo.elementos; pasada, la pasada de la pluma o, en un sombreado, la raya.
  • trayectoria lleva 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), con abajo a 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 (foley rotulador) o para un robot de dibujo.

6. geometria.json

{ "poses": [ { "id", "pose": 0, "caja", "duracion", "trazos": [ … ] }, … ] }

Cada trazo, en el orden de la mano:

campoqué es
tipocinta (pluma con presión), linea (lápiz, grosor fijo, remates redondos), relleno (plano) o nota
capa, elemento, pasada, indicecomo en trazos del manifiesto
tinta, color, opacidad, mezcla, desplazamientocómo se pinta
inicio, duracion, modocuándo, en segundos
puntosla línea central temblada, sin simplificar
arcolongitud de arco acumulada en cada punto
anchoancho de la pluma en cada punto, de lado a lado, con la presión (en linea, constante)
llegadasegundo en que la pluma llega a cada punto
contornoen cinta, el polígono simplificado que rellena el SVG; en relleno, el polígono del plano
notaen 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 (ver pruebas/equivalencia.ts): vista.png y la captura de Chrome del SVG difieren en menos de medio nivel de gris de media (el suavizado de bordes).
  • Ids. azar.ts de la skill siembra por unidades UTF-16 y el canon por bytes UTF-8: coinciden en ASCII. Un id con «ñ» o tildes daría otro pulso en la lámina que en el SVG; por eso los id van 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).