Skip to content
Tórculo1.0 · working draft

Part 5 · plates · version 1

Plates: drawings that reveal themselves

A plate is a hand drawing ready for any engine to make it appear in pen without recomputing the hand: the image is rasterised once and a time atlas says when the pen reaches each pixel.

A hand with its index finger stretched out, like the ones old readers drew in the margins of their books, comes out of a sleeve with a lace cuff and points to the right. Underneath, the pencil lines that set out the drawing are still visible and, beside it, a handwritten note: “it points; it doesn't answer”.
A manicule drawn in pen.lamina · v1
3.96 s · 3 boil poses

A manicule drawn in pen

A hand with its index finger stretched out, like the ones old readers drew in the margins of their books, comes out of a sleeve with a lace cuff and points to the right. Underneath, the pencil lines that set out the drawing are still visible and, beside it, a handwritten note: “it points; it doesn't answer”.

manicula · 640 × 300 · scale 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 boil poses

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 · scale 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 boil poses

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 · scale 2 · lupa.lamina

How they reveal

Each plate carries, per layer (pencil, flats, ink, colour, notes) and per boil pose, a colour atlas and a time atlas. The viewer, in WebGL2 with no dependencies, composes the layers on the paper and lets the ink through wherever the pen has already arrived: reveal = clamp((progress · 1.03 − t) · 60, 0, 1). Once the drawing is whole, the boil starts: three poses at 8 frames per second. With “reduce motion” on, the finished drawing is shown, without animation.

All three come from the references in pluma/pruebas/referencia/ through the Python binding (torculo_pluma), byte for byte the same as in Rust and wasm.

The format (LAMINA.md)

This text is in Spanish.

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).