Formas básicas
rect,circle,ellipse: primitivas con parámetros, no trazosline,polyline,polygon: listas de puntos- Todas aceptan
fill,strokey las mismas propiedades de pintura
svg-playground
Un taller de 3 horas donde no se aprende SVG. Se aprende ciencias de la computación, y SVG es el bisturí porque un dibujo vectorial no es un blob de píxeles: es un documento XML que se escribe con printf, se versiona con git y se lee como cualquier otro texto. No hace falta ninguna herramienta de dibujo. El renderer es el navegador que ya tenés abierto.
mano.svg: 116 bytes escritos con printf, sin ningún programa de dibujo. Es el archivo completo, no un recorte. El módulo 0 lo escribe de nuevo, a mano, en el editor de abajo.El primer taller de esta serie rompía video para ver a un decodificador alucinar píxeles que nunca existieron. El segundo le mentía a la cabecera de una imagen para ver la misma clase de mentira fallar fuerte en una dirección y en silencio en la otra. Los dos trabajaban sobre formatos binarios: para leerlos hacía falta un programa que supiera dónde termina un campo y empieza el siguiente.
Este taller cambia el material, no el método. SVG es XML: texto plano, sin ningún campo binario, sin ningún byte que solo un programa especializado pueda interpretar. Eso cambia todo lo que se puede hacer con un dibujo. Se escribe con printf. Se versiona con git línea por línea. Y se diffea, en serio, no como curiosidad: el módulo 5 mide cuánto cuesta exactamente perder esa propiedad.
El módulo 7 cierra algo que viene de los dos talleres anteriores. En el taller de video, un fotograma clave faltante hacía que el decodificador alucinara píxeles. En el taller de imágenes, una cabecera mentirosa corría la imagen en silencio en una dirección y fallaba fuerte en la otra. Acá vas a romper el mismo byte de dos maneras — o mejor dicho, la misma manera, leída por dos parsers distintos — y uno se va a negar a leerlo con número de línea y columna, mientras el otro te lo arregla sin decir nada y dibuja igual. Tres talleres, tres filosofías de falla, y la diferencia nunca estuvo en el archivo. Siempre estuvo en quién lo lee.
Antes de arrancar, cada persona necesita esto resuelto. No gastes tiempo del taller instalando cosas — de hecho, no hay nada que instalar.
Dos scripts, ya en el repo, hacen el trabajo mecánico para que el taller se dedique a leer y escribir, no a pelear con la terminal:
./scripts/gen-media.sh # genera todos los .svg de media/ desde cero con printf y bucles de shell
./scripts/romper.sh media/mano.svg # rompe un carácter y produce roto.svg + roto.html — el módulo 7
gen-media.sh no dibuja nada con un programa de dibujo: escribe cada .svg con printf o con un bucle for de shell, y al final imprime la tabla de bytes que este sitio cita. La única excepción es espiral-200.png, el raster de comparación del módulo 6: ese sí necesita un rasterizador, y como no hay ninguno instalado, el script prueba rsvg-convert, después inkscape, y si tampoco están usa un Chromium sin cabeza para sacarle una captura de pantalla al propio SVG. El renderer es el navegador incluso para generar el material del taller.
romper.sh le saca un solo carácter a un SVG con cierre propio (/>) y deja el mismo markup roto servido de dos maneras: como roto.svg y como roto.html. El módulo 7 abre los dos y compara cómo los lee cada parser.
Esta sección es material de referencia, no se recorre en vivo. El taller toca a fondo path, transform y el DOM vía scripting; todo lo demás queda catalogado acá para que sepas dónde volver a buscar.
rect, circle, ellipse: primitivas con parámetros, no trazosline, polyline, polygon: listas de puntosfill, stroke y las mismas propiedades de pinturadpath — M, L, C, Q, A, Z — se ve a fondo en el módulo 2A (arco elíptico) es el único comando que no aparece en el taller, por complejidad de parámetrospath; lo inverso no siempretranslate, rotate, scale, skewX/skewY, matrix — se ven a fondo en el módulo 3transform-origin en CSS permite fijar el pivote de rotación o escalalinearGradient y radialGradient, definidos en defs y referenciados por idstop con offset y stop-color: los puntos de control del degradégradientUnits="userSpaceOnUse" para que el degradé no se recalcule con cada instanciaclipPath: recorte binario, adentro o afuera, sin semitonosmask: recorte por luminancia o alfa, con semitonostextPath hace que una cadena de texto siga la curva de un path existentefeGaussianBlur, feColorMatrix, feTurbulence, feDisplacementMap, y varias másfilter con la misma lógica de grafo que ffmpeg-playground usa para sus filtros de videoanimate, animateTransform — declarado adentro del propio SVG@keyframes sobre propiedades SVG animables, igual que en HTMLcreateElementNS, getBBox, getCTM, getTotalLengthSVGElement hereda de Element: todo lo que sabés de manipular HTML aplica igualforeignObjectNueve módulos, 3 horas, terminal y un navegador. Cada módulo tiene la misma estructura: un concepto de ciencias de la computación, algo que hacés con las manos, y el momento en que las dos cosas encajan.
Módulo 0
15 min
Concepto Representación mínima de datos, XML como formato de dibujo
printf '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#4fd9e8"/></svg>\n' > mano.svg
wc -c mano.svg
116 mano.svg
mano.svg pesa 116 bytes y es un dibujo completo: un círculo cian de radio 40 dentro de un lienzo de 100 por 100 unidades. Ningún programa de dibujo estuvo involucrado. El navegador lo abre directo — probalo con doble clic o arrastrándolo a una pestaña.
El favicon de esta misma página es el mismo truco: favicon.svg pesa 192 bytes y se referencia así, sin conversión a ningún formato binario:
<link rel="icon" href="favicon.svg" type="image/svg+xml">
Ahora escribilo de nuevo, a mano, en el editor de abajo — es el mismo mano.svg, prellenado. Borrá algo, cambiá el radio, mirá qué pasa. La última línea de este módulo es el widget: escribís texto y aparece un dibujo, en vivo.
El momento Borrá el / antes del > de cierre del circle y mirá la línea de estado: el mismo parser estricto que va a protagonizar el módulo 7 ya está corriendo en este editor, en tiempo real, cada vez que tipeás.
Módulo 1
20 min
Concepto Sistemas de coordenadas anidados, unidades de usuario
viewBox="0 0 100 100" no es un tamaño en píxeles: es una declaración de unidades de usuario, un sistema de coordenadas propio del dibujo. Sin width ni height, el SVG escala para llenar el contenedor donde lo pongas — por eso todos los dibujos de este sitio están escritos así a propósito: para que se incrusten de forma responsiva, sin media queries por dibujo.
Eso significa que hay al menos dos sistemas de coordenadas superpuestos: el interno, en unidades de usuario (los números que escribís en cx, r, d), y el externo, en píxeles de pantalla. La matriz que traduce uno al otro se llama CTM — current transformation matrix — y se puede leer, no solo intuir.
matriz.svg (374 bytes), incrustado directo como markup en esta página en vez de como <img>, para que puedas abrir la consola y llamar getCTM() sobre él de verdad. Su viewBox es 0 0 200 200. Medido con este mismo dibujo en un contenedor de 400 px de ancho (2× el viewBox, porque 400/200 = 2): la matriz que escribiste en el grupo amano, en unidades de usuario, es [0,8660254, 0,5, -0,5, 0,8660254, 34,641016, 20]. Lo que devuelve getCTM() ahí es [1,7321, 1, -1, 1,7321, 69,282, 40] — cada componente al doble, redondeando a cuatro decimales. Lo que pasa más allá del cuarto decimal lo mide el módulo 3, y no es lo que esperarías. Si tu ventana es angosta, abrí las devtools y forzá el ancho a 400 px para reproducir estos números dígito por dígito.Ese factor 2 no es un redondeo ni una coincidencia de esta página: es la definición de viewBox funcionando. El navegador compone tu matriz con la suya — la que traduce las 200 unidades del viewBox al ancho real del contenedor — y getCTM() te devuelve el resultado ya multiplicado. Es la demostración más limpia que hay de que "sistema de coordenadas anidado" no es una frase abstracta: es un número que podés leer en la consola.
El momento Nunca hay un solo sistema de coordenadas en un SVG. Hay como mínimo dos, y getCTM() es la ventana a la matriz que los conecta. El módulo 3 va a mostrar que esa misma matriz también puede escribirse a mano, número por número.
Módulo 2
25 min
Concepto DSL embebido, parsing declarativo, interpretación de una cadena
cat > camino.svg <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 120">
<path d="M 20 100 L 60 30 L 100 100 Z" fill="none" stroke="#4fd9e8" stroke-width="3"/>
<path d="M 120 100 C 140 20, 180 20, 200 100" fill="none" stroke="#f26fc0" stroke-width="3"/>
<path d="M 220 100 Q 250 10, 280 100" fill="none" stroke="#6fd88a" stroke-width="3"/>
</svg>
EOF
camino.svg, 343 bytes, tres path.Cada letra de d es un comando de un lenguaje chico: M mueve el lápiz sin dibujar (moveto), L dibuja una recta (lineto), C dibuja una curva de Bézier cúbica con dos puntos de control, Q una cuadrática con uno solo, y Z cierra la figura volviendo al punto de partida. Medido con getTotalLength() sobre cada uno de los tres path de arriba:
| path | d | largo |
|---|---|---|
| recta | M 20 100 L 60 30 L 100 100 Z | 241 |
| cúbica | M 120 100 C 140 20, 180 20, 200 100 | 153 |
| cuadrática | M 220 100 Q 250 10, 280 100 | 113 |
Ese número no está adivinado ni interpolado a ojo: getTotalLength() hace que el navegador mida, en unidades de usuario, la longitud real de una curva que parseó de una cadena de caracteres. La cadena "M 120 100 C 140 20, 180 20, 200 100" es un programa chico, con su propia sintaxis y su propio conjunto de comandos, y el navegador tiene un intérprete de ese programa integrado en el motor de renderizado. Un DSL completo, viviendo adentro de un atributo.
El momento No hay una "forma curva" guardada en algún lado: hay una cadena de texto que un intérprete lee, comando por comando, y convierte en geometría. El atributo d es la prueba más chica y más legible que existe de qué es un lenguaje de dominio específico con su propio parser.
Módulo 3
25 min
Concepto Álgebra lineal como sintaxis de atributo, composición de matrices
La sintaxis amigable de transform —rotate, translate, scale— no es una capa por encima de algo más simple: es azúcar sintáctico sobre una matriz afín de 2×3, y el navegador la compila a esa matriz antes de dibujar nada. rotate(30) translate(40 0) compone, en unidades de usuario, exactamente a:
matrix(0.8660254 0.5 -0.5 0.8660254 34.641016 20)
La cuenta a mano: rotate(30) es la matriz [cos30, sin30, -sin30, cos30, 0, 0] = [0,866, 0,5, -0,5, 0,866, 0, 0]. Componer translate(40 0) encima multiplica esa traslación por la parte rotada de la matriz: e = 0,866 × 40 = 34,641 y f = 0,5 × 40 = 20. Nada de esto es magia del navegador — es la multiplicación de matrices de toda la vida, aplicada dos veces y sumada.
Volvé a mirar el dibujo incrustado en el módulo 1: el grupo compuesto usa la sintaxis amigable, el grupo amano usa los seis números crudos de arriba, y en pantalla se superponen sin que se vea un solo píxel de diferencia. Pero medilos con getCTM() y no son idénticos. Estos son los cuatro componentes de escala y rotación, medidos:
compuesto 1.7320508075688774 0.9999999999999999 -0.9999999999999999 1.7320508075688774
amano 1.732050895690918 1 -1 1.732050895690918
diferencia máxima: 0.000003524881620364795
Tres cosas para mirar de esa salida, y ninguna es un error del navegador:
1 que no es 1. rotate(30) debería dar sin 30 = 0,5 exacto. En aritmética de punto flotante sin(30°) vale 0,49999999999999994, y por eso el componente aparece como 0,9999999999999999 después de escalar. No es el SVG: probá Math.sin(30 * Math.PI / 180) en cualquier consola y en Python te da lo mismo.3,52 × 10⁻⁶ de unidad de usuario. Componer dos matrices y multiplicarlas en el momento no acumula el mismo redondeo que escribir el producto ya hecho. Son dos caminos aritméticos al mismo lugar.matrix(0.8660254 ...) y matrix(0.8660254037844387 ...) devuelven resultados byte a byte iguales. Los números de los atributos se guardan con precisión reducida, así que a partir del séptimo dígito estás escribiendo decoración.Y aun así: 3,5 millonésimas de unidad de usuario, en un dibujo de 200 unidades de lado, es una fracción invisible de un píxel. Se superponen perfectamente a la vista porque el error es del orden del redondeo, no de la geometría. La conclusión sigue en pie y ahora está medida en vez de afirmada.
Esta misma matriz de 2×3 ya la usaste en otra forma, en el taller anterior de esta serie. En pixel-playground escribiste un kernel de convolución de 3×3: una matriz chica que se desliza sobre una imagen. Son la misma clase de objeto matemático haciendo trabajos distintos. El kernel de convolución combina los valores de los píxeles vecinos — multiplica y suma intensidades. La matriz afín de transform reubica coordenadas — multiplica y suma posiciones. Una opera sobre qué tan brillante es un punto; la otra, sobre dónde está. Multiplicar-y-sumar es el núcleo de las dos, y es el núcleo de casi toda la computación gráfica.
El momento rotate(), translate() y scale() no son primitivas independientes: son nombres legibles para casos particulares de una única matriz de 2×3. Escribirla a mano, número por número, y verla caer encima de la versión legible con un error de una millonésima es la prueba de que no hay dos sistemas — hay uno solo, con dos formas de nombrarlo y dos caminos de redondeo para llegar.
Módulo 4
20 min
Concepto Generación procedural, programa como dibujo
for i in $(seq 1 60); do
printf ' <circle cx="100" cy="100" r="%d" fill="none" stroke="#4fd9e8" stroke-opacity="0.35" transform="rotate(%d 100 100) translate(%d 0)"/>\n' \
"$((100 - i))" "$((i * 6))" "$((i / 2))"
done >> espiral.svg
espiral.svg, 8.194 bytes, 60 elementos circle. Cada uno con un radio, una rotación y una traslación un poco distintas de la del anterior — el bucle de shell de arriba, ejecutado una vez por cada i de 1 a 60.Ningún programa de dibujo estuvo involucrado en ningún paso. El bucle no generó una imagen que después se convirtió a SVG: generó directamente el texto del SVG, línea por línea, con la misma herramienta con la que un script genera cualquier otro archivo de texto. path del módulo 2 y transform del módulo 3 son lo único que hizo falta aprender para que esto sea posible.
El momento Un dibujo generativo no necesita un lenguaje de shaders ni un motor de partículas. Necesita una forma, un atributo que varíe con el índice del bucle, y printf. La distancia entre "escribir un archivo de texto" y "escribir un programa que dibuja" es más chica de lo que parece.
Módulo 5
25 min — el módulo central
Concepto Control de versiones, granularidad de diff, formato como decisión funcional
El mismo dibujo, ocho círculos, escrito de dos formas. Un elemento por línea:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<circle cx="22" cy="100" r="12" fill="#4fd9e8"/>
<circle cx="44" cy="100" r="12" fill="#4fd9e8"/>
...
</svg>
Y la misma información, minificada a una sola línea. El contenido es idéntico; lo único que cambia es dónde caen los saltos:
Geométricamente son el mismo dibujo: los mismos ocho círculos, en los mismos cx, con el mismo r. Ahora cambiá el radio del cuarto círculo de 12 a 30. En multi.svg, ese es 1 línea cambiada de 10, y el diff muestra exactamente eso:
@@ -5 +5 @@
- <circle cx="88" cy="100" r="12" fill="#4fd9e8"/>
+ <circle cx="88" cy="100" r="30" fill="#4fd9e8"/>
El mismo cambio en la versión de una sola línea es 1 línea cambiada de 1 — porque solo hay una línea. git diff no tiene ninguna línea más chica en la que fijarse: reimprime el dibujo entero dos veces, una como borrado y una como agregado, y revisar ese diff a ojo es imposible. No hay forma de ver, mirando el diff, que el único cambio real fue un 12 que pasó a 30.
Minificar ahorró 17 bytes, un 3,5 %, y a cambio costó la capacidad entera de revisar un cambio. Ese no es un argumento de gusto ni de estilo de código: es un número medido, y dice que en este caso el ahorro de espacio no vale ni de cerca lo que cuesta.
El momento git diffea líneas, no dibujos ni significado. Si el formato pone un elemento por línea, un cambio quirúrgico se ve quirúrgico. Si lo aplasta todo a una línea, el mismo cambio se ve como si se hubiera reescrito todo. El salto de línea no es un detalle cosmético: es una decisión funcional sobre si el control de versiones te va a servir de algo. Ningún taller de este tipo sobre imágenes raster puede correr este módulo — un PPM o un PNG no tienen líneas que diffear, tienen bytes de una grilla. Esto es exclusivo de que el dibujo sea texto.
Módulo 6
20 min
Concepto Representación por regla vs. representación por muestra, independencia de resolución
El mismo dibujo de 60 círculos del módulo 4, exportado a raster a tres tamaños distintos, contra el peso del propio SVG:
| render | PNG | SVG | PNG/SVG |
|---|---|---|---|
| 200×200 | 49.794 | 8.194 | 6,08× |
| 800×800 | 415.116 | 8.194 | 50,66× |
| 2000×2000 | 1.390.522 | 8.194 | 169,70× |
El SVG pesa 8.194 bytes en las tres filas. No cambia. El PNG crece aproximadamente con el área — de 200×200 a 2000×2000 el área se multiplica por 100, y el PNG por casi 28. La razón de la diferencia no es un detalle de compresión: es que el SVG no guarda una imagen, guarda una receta — 60 elementos circle con sus atributos — y esa receta se ejecuta en el momento de dibujar, a cualquier resolución que pida el dispositivo. El PNG, en cambio, guarda el resultado de ejecutar esa receta una vez, congelado en una grilla de píxeles de un tamaño fijo.
Mostrados al mismo tamaño en CSS, escalá la página del navegador hacia arriba y mirá los dos: el SVG se mantiene nítido, el PNG empieza a mostrar sus píxeles.
Esto es la misma familia de idea que pixel-playground, módulo 5, donde cuantizar una imagen a 2 colores no le sacaba un solo byte al PPM crudo — el formato reserva espacio fijo por píxel sin importar cuánta información real hay adentro. Ahí la lección era que menos información no es menos bytes. Acá es la otra cara de la misma moneda: lo que un archivo guarda y lo que representa son preguntas distintas, y la independencia de resolución del SVG es la versión más extrema y más medible de esa distinción que vas a ver en todo el taller.
El momento Guardar una receta en vez de un resultado no es una optimización de espacio que a veces funciona: es una propiedad estructural que se sostiene siempre, y la tabla de arriba la vuelve un número concreto en vez de una promesa de marketing sobre "gráficos vectoriales".
Módulo 7
15 min — el cierre de la serie
Concepto Parsing estricto vs. permisivo, ley de Postel, modelos de amenaza distintos
./scripts/romper.sh media/mano.svg
El script le saca exactamente un carácter a mano.svg — el / del cierre propio de <circle ... />. 116 bytes se convierten en 115 bytes. Con eso escribe dos archivos, roto.svg y roto.html, con el mismo markup inválido adentro, servido de dos maneras distintas.
Abrí roto.svg. El parser de XML es estricto y lo rechaza: document.documentElement.tagName pasa a ser html — el documento SVG fue reemplazado por una página de error — aparece un elemento parsererror, y el mensaje dice, con línea y columna exactas:
This page contains the following errors: error on line 1 at column 114: Unexpected closing tag: {http://www.w3.org/2000/svg}svg != ...
Abrí roto.html, el mismo markup exacto, servido como HTML. Ahí no hay parsererror: el círculo está en el DOM, r="40" se lee de vuelta correctamente, y pinta 599 px de ancho. El parser de HTML cerró el tag por vos, en silencio, y dibujó.
Nótese la precisión de la afirmación: no es que la versión XML "no dibuje nada". La página de error de Chromium sí muestra un árbol con el código fuente. Lo verificado y defendible es esto: en un caso, el elemento raíz pasa a ser html, aparece parsererror, y se reporta línea y columna. En el otro, no hay error y el círculo se renderiza.
Esto es la ley de Postel en 115 bytes: sé liberal en lo que aceptás. HTML eligió ser liberal porque la primera web estaba llena de páginas escritas a mano, con errores, por gente que nunca leyó una especificación. XML eligió ser estricto porque se diseñó para que máquinas intercambiaran datos entre sí, sin un humano mirando en el medio. Ninguna de las dos decisiones está mal — tienen modelos de amenaza distintos. ffmpeg-playground hizo exactamente este argumento sobre decodificadores de video: un decodificador tolera un paquete perdido porque cortar una videollamada es peor que degradarla, mientras que un handshake de TLS aborta ante el primer byte inesperado porque ahí la permisividad es la vulnerabilidad. La pregunta es siempre la misma — cuánta permisividad tolerar — y la respuesta correcta depende de quién está leyendo y para qué.
El momento Esto cierra la serie. En ffmpeg-playground, un fotograma clave faltante hizo que el decodificador alucinara — dibujó algo, pero no lo que había. En pixel-playground, una cabecera mentirosa corrió la imagen en silencio en una dirección y falló fuerte en la otra. Acá, el mismo byte roto se rechaza con línea y columna por un parser y se repara en silencio por el otro. Tres talleres, tres formas distintas de fallar, y en ningún caso la diferencia estuvo en el archivo. Siempre estuvo en el programa que lo lee.
Módulo 8
5 min
Concepto Cierre y evaluación
Cada persona guarda el dibujo que más le gustó de todo el taller. No hace falta convertirlo a nada: ya es el archivo final, el mismo que se puede abrir, leer y editar con un editor de texto común.
Y antes de mostrarlo, dice una sola frase: qué parte de su dibujo es receta y no resultado — un transform, un bucle, un path que reutilizó. Esa frase es toda la evaluación del taller. Si puede decirla, entendió que el dibujo es texto y que eso no es un detalle técnico sino la razón por la que todo lo anterior funcionó. Si no puede, el dibujo queda igual de lindo — pero volvemos 10 minutos al módulo 3.
El momento El que aparece cuando alguien mira el archivo de otro y dice, sin que se lo pidan, "esto lo podés versionar". Ahí dejaron de ver una imagen y empezaron a ver un documento.
Ordenado por dificultad. El primero es el que más enseña.
Tomá cualquier dibujo del taller y animalo dos veces: una con animate/animateTransform declarados adentro del propio SVG (SMIL), y otra con @keyframes en una hoja de estilos aparte (CSS). Mismo resultado visual, mecanismos completamente distintos — uno vive en el documento del dibujo, el otro en la hoja de estilos que lo rodea.
potrace convierte un bitmap en curvas vectoriales — el camino inverso al de todo este taller, que escribió el vector directamente. Tomá una imagen tramada a un bit del taller de netpbm de esta serie y pasala por potrace: es el gancho de vuelta a la parada anterior.
Tomá una columna de números de cualquier archivo real y generá un path o una serie de rect con awk, calculando posiciones, y printf, escribiendo el SVG. Es el módulo 4 de este taller aplicado a datos que te importan, en vez de a una espiral decorativa.
Una cadena de feGaussianBlur, feColorMatrix y feDisplacementMap dentro de un filter es un grafo de operaciones sobre píxeles, muy parecido en estructura a un fragment shader. Escribir el mismo efecto en los dos lenguajes es la puerta de entrada a por qué existe WebGL.
Elegí una letra de tu nombre y escribí su contorno como un único path, a mano, con los comandos del módulo 2. Es el ejercicio más lento de esta lista y el que más deja: una fuente tipográfica es, literalmente, una colección de path con metadatos encima.
Estas son las traducciones que usamos, con el término original al lado para que puedan googlear.
viewBox. No son píxeles de pantalla hasta que algo las traduce.transform.getCTM().d.d: M moveto, L lineto, C cúbica, Q cuadrática, Z cerrar.C es cúbica (dos puntos de control), Q es cuadrática (uno).git, esa unidad es la línea.