Cairo

Cairo es una potente biblioteca para la manipulación de imágenes bidimensionales (2D) basadas en gráficos vectoriales. Podemos usarlo para dibujar nuestros propios widgets, gráficos, efectos o animaciones.

Se trata de software libre (bajo licencia GNU LGPL) que ofrece soporte multiplataforma (escrito en C) y está disponible para distintos lenguajes de programación, incluido, por supuesto, Genie.

Ya que Cairo es una biblioteca de dibujo, puede ser muy útil integrarla con otras herramientas de interfaz gráfica, como Gtk+, Pango y SDL.

Para su instalación en Debian y derivadas incluyendo Ubuntu (para otros sistemas operativos, como Windows y Mac OS X, visita Download):

$ sudo apt install libcairo2 libcairo2-dev

Al principio del código especificamos: uses Cairo y en el compilador: valac --pkg cairo nombre_archivo.gs

Modelo gráfico: conceptos básicos

Para no andar muy perdido al principio, es conveniente repasar algunos conceptos básicos involucrados en el modelo gráfico de Cairo. Dentro de estos conceptos, podemos distinguir entre sustantivos y verbos. Los sustantivos son objetos abstractos mientras que los verbos ofrecen formas de manipular los sustantivos para crear gráficos.

Sustantivos

Los sustantivos son entidades abstractas que interactúan entre sí a distintos niveles o capas. Los distintos tipos de sustantivos son destino (destination o surface), fuente (source), máscara (mask), ruta (path) y contexto (context).

  • Destino. El destino es el “lienzo” o superficie (surface) sobre la que se dibuja. Puede ser una matriz de píxeles, un archivo SVG o PDF, o cualquier tipo de gráfico. Esta superficie va recogiendo los elementos de tu gráfico a medida que los aplicas, y su acumulación permite construir un trabajo gráfico completo.
  • Fuente. Es la “pintura” con la que se trabaja sobre el lienzo. Puede ser ser un color o un patrón de colores, y puede contener información sobre su grado de transparencia, el canal Alpha.
  • Máscara. La máscara actúa como un filtro o un molde, controlando dónde se aplica o "estampa" la fuente al destino. Gestiona los puntos de acceso de la fuente al destino permitiendo que la fuente se copie o no en determinados lugares del destino.
  • Ruta. Media entre la máscara y el contexto.
  • Contexto. Rastrea al resto de sustantivos para controlar y registrar las operaciones de los verbos sobre ellos. Antes de empezar a dibujar, se necesita crear un contexto, que debe estar vinculado a una superficie.

Cuando se crea un contexto se debe especificar el tipo de superficie a la que está vinculado. Por ejemplo, para inicializar un contexto con una superficie de imagen escribimos:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen de 256x256) y un contexto vinculado
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 256, 256)
	context: Cairo.Context = new Cairo.Context (surface)

El contexto en este ejemplo está ligado a una superficie de imagen con dimensiones de 256 x 256 y 32 bits por píxel para almacenar información RGB y Alpha. Más adelante veremos otros tipos de superficie.

Verbos

EL método de dibujo de Cairo consiste en colocar la fuente y la máscara en alguna parte sobre el destino y a continuación comprimir todas las capas juntas de manera que la pintura de la fuente se transfiere al destino donde la máscara lo permite. En este sentido, los siguientes cinco verbos u operaciones de dibujo son similares, y solo difieren en la forma que construyen la máscara que filtra esa transferencia.

Los verbos son trazar (stroke), rellenar (fill), mostrar texto (show text), pintar (paint) y enmascarar (mask).

  • Trazar. La operación Cairo.Context.stroke() recorre la ruta con un lápiz o bolígrafo virtual. Permite que la fuente (o parte de ella) se transfiera a través de la máscara de acuerdo a una línea y sus características (grosor, estilo y límites).
  • Rellenar. La operación Cairo.Context.fill() utiliza la ruta como las líneas de un libro de colorear que limitan el objeto a pintar. Permite que la fuente (o parte de ella) se transfiera a través de la máscara según las áreas limitadas por la ruta.
  • Mostrar texto. La operación Cairo.Context.show_text() forma la máscara desde cierto texto.
  • Pintar. La operación Cairo.Context.paint() usa una máscara que transfiere la fuente completa al destino (quizá por eso se podría pensar que ni siquiera es una máscara). Cairo.Context.paint_with_alpha() también permite transferir la fuente completa al destino, pero sólo transfiere cierto porcentaje de color.
  • Enmascarar. Las operaciones Cairo.Context.mask() y Context.mask_surface() permiten la transferencia de acuerdo con el nivel de transparencia / opacidad de un segundo patrón de fuente o una superficie: cuando es opaco, la fuente actual se transfiere al destino y cuando es transparente, no se transfiere nada.

Preparando el material de dibujo

Para poder crear una imagen, se tiene que seguir un proceso ordenado.

  1. Seleccionar una fuente: colores, gradientes, imágenes.
  2. Posicionar puntos de inserción y crear rutas para dibujar y pintar y para escribir texto.
  3. Seleccionar la superficie destino: archivo de imagen png, svg, pdf, ventana Gtk+.

Seleccionar fuente

Existen tres tipos principales de fuentes en Cairo: colores, gradientes e imágenes.

Colores

Los colores son los más simples; utilizan una tonalidad uniforme y opacidad para toda la fuente. Puede seleccionarlos con Cairo.Context.set_source_rgb (rojo:double, verde:double, azul:double) y Cairo.Context.set_source_rgba (rojo:double, verde:double, azul:double, alpha:double).

Un ejemplo que utiliza colores para pintar rectángulos:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen de 175x175) y un contexto vinculado
	lienzo: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 175, 175)
	pincel: Cairo.Context = new Cairo.Context (lienzo)
 
	// rectángulo rojo
	pincel.set_source_rgba (1, 0, 0, 1)
	pincel.rectangle (25, 25, 75, 75)
	pincel.fill ()
 
	// rectángulo azul
	pincel.set_source_rgba (0, 0, 1, 1)
	pincel.rectangle (75, 75, 75, 75)
	pincel.fill ()
 
	// guarda la imagen
	lienzo.write_to_png ("colores.png")

Gradientes

Los gradientes (que pueden ser lineales y radiales) describen una progresión de colores estableciendo unas referencias de inicio y fin y una serie de “paradas” (Cairo.Pattern.add_color_stop_rgb() y Cairo.Pattern.add_color_stop_rgba()) entre ellas.

Ejemplo de gradiente lineal aplicado a un rectángulo:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen de 120x120) y un contexto vinculado
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 120, 120)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// escala de coordenadas 1,0 x 1,0
	context.scale (120, 120)
 
	// gradiente y paradas
	gradlineal: Cairo.Pattern = new Cairo.Pattern.linear (0.25, 0.35, 0.75, 0.65)
	gradlineal.add_color_stop_rgba (0.00, 1, 1, 1, 0)
	gradlineal.add_color_stop_rgba (0.25, 0, 1, 0, 0.5)
	gradlineal.add_color_stop_rgba (0.50, 1, 1, 1, 0)
	gradlineal.add_color_stop_rgba (0.75, 0, 0, 1, 0.5)
	gradlineal.add_color_stop_rgba (1.00, 1, 1, 1, 0)
 
	// rectángulo
	context.rectangle (0.0, 0.0, 1, 1)	
	context.set_source (gradlineal)
	context.fill ()
 
	surface.write_to_png ("gradientelineal.png")

Ejemplo de gradiente radial aplicado a rectángulos creados por bucles for:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init	
	// crea una superficie (una imagen de 120x120) y un contexto vinculado
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 120, 120)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// escala de coordenadas 1,0 x 1,0
	context.scale (120, 120)
 
	// gradiente y paradas
	grad: Cairo.Pattern = new Cairo.Pattern.radial (0.25, 0.25, 0.1, 0.5, 0.5, 0.5)
	grad.add_color_stop_rgb(0, 1.0, 0.8, 0.8)
	grad.add_color_stop_rgb (1, 0.9, 0.0, 0.0)
 
	// bucles de rectángulos
	for var i = 1 to 9
		for var j = 1 to 9
			context.rectangle(i/10.0-0.04, j/10.0-0.04, 0.08, 0.08)
			j++
		i++
 
	context.set_source (grad)
	context.fill ()
 
	surface.write_to_png ("gradiente.png")

Imágenes

Las imágenes pueden ser superficies cargadas desde archivos o superficies creadas con Cairo en un destino anterior.

Ejemplo de imagen que recibe varias transformaciones para ser utilizada como patrón de relleno de un rectángulo:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init	
	// crea una superficie (una imagen de 256x256) y un contexto vinculado
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 256, 256)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// imagen de origen (ruta y formato)
	image_path:string = GLib.Path.build_filename (GLib.Path.get_dirname (args[0]) , "genielogo.png")
	image: Cairo.ImageSurface = new Cairo.ImageSurface.from_png (image_path)
 
	// patrón de imagen
	pattern: Cairo.Pattern = new Cairo.Pattern.for_surface (image)
	pattern.set_extend (Cairo.Extend.REPEAT)
 
	// transformaciones sobre la imagen
	context.translate (128.0, 128.0)
	context.rotate (Math.PI / 4)
	context.scale (1 / Math.sqrt (2), 1 / Math.sqrt (2))
	context.translate (-128.0, -128.0)
 
	// escala de coordenadas
	matrix: Cairo.Matrix = Cairo.Matrix.identity ()
	w:int = image.get_width ()
	h:int = image.get_height ()
	matrix.scale (w/256.0 * 5.0, h/256.0 * 5.0)
	pattern.set_matrix (matrix)
 
	context.set_source (pattern)
 
	context.rectangle (0, 0, 256.0, 256.0)
	context.fill ()
 
	// guarda la imagen
	surface.write_to_png ("imagen_patron.png")

Crear rutas

Cairo siempre tiene una ruta activa que se inicia vacía. Además, cada vez que se utiliza Cairo.Context.stroke() para dibujar una ruta y Cairo.Context.fill() para rellenar el interior de la ruta, vuelve a vaciarse para una nueva ruta. Como alternativas a esas funciones, Cairo.Context.stroke_preserve() y Cairo.Context.fill_preserve() no vacían la ruta para que se puede volver a utilizar.

Puntos

Para crear rutas, Cairo utiliza un sistema basado en conectar puntos sucesivos. Por eso, para iniciar una ruta es necesario designar un primer punto, el cual todavía no tiene ningún otro conectado, lo que se hace con Cairo.Context.move_to(). Esto establece el punto de referencia actual sin conectarlo a ningún punto anterior (si lo hubiera). También hay una variante de coordenadas relativas: Cairo.Context.rel_move_to() que establece la nueva referencia en relación al lugar de la referencia actual.

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 120, 120)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// escala de coordenadas 1,0 x 1,0
	context.scale (120, 120)
 
	// rectángulo amarillo con transparencia que ocupa toda la superficie
	context.set_source_rgba (1, 1, 0, 0.3)
	context.rectangle (0, 0, 1, 1)
	context.fill ()
 
	// propiedades de la línea (grosor y color)
	context.set_line_width (0.1)
	context.set_source_rgba (1, 0, 0, 1)
	// coordenadas punto de inicio
	x: double = 0.25
	y: double = 0.25
	context.move_to (x, y)
	// arco que marca el punto con un círculo rojo
	context.arc (x, y, 0.1, 0, 2*Math.PI)
	context.fill ()
 
	// guardamos la imagen como png
	surface.write_to_png ("punto.png")

A partir de este primer punto, podemos realizar otras operaciones que lo actualizan y se conectan a él de distintas formas para crear la ruta. Tenemos disponibles diversos elementos para crear las rutas, como líneas, rectángulos o arcos, entre otros. Podemos dejar la ruta abierta (su punto inicial y su punto final no se encuentran) o cerrada trazando una línea recta hacia el inicio de la ruta. Un ejemplo:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 120, 120)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// escala de coordenadas 1,0 x 1,0
	context.scale (120, 120)
 
	// propiedades de la línea (grosor y color)
	context.set_line_width (0.1)
	context.set_source_rgb (0, 0, 0)
 
	// punto de inicio
	context.move_to (0.25, 0.25)
	// línea a otro punto
	context.line_to (0.5, 0.375)
	// línea a otro punto (coordenadas relativas)
	context.rel_line_to (0.25, -0.125)
	// arco
	context.arc (0.5, 0.5, 0.25 * Math.sqrt(2), -0.25 * Math.PI, 0.25 * Math.PI)
	context.rel_curve_to (-0.25, -0.125, -0.25, 0.125, -0.5, 0)
	// cierra la ruta
	context.close_path ()
 
	// trazamos la ruta
	context.stroke ()
 
	// guardamos la imagen como png
	surface.write_to_png ("ruta.png")

Líneas y rectángulos

Usamos una línea recta para unir un primer punto de referencia con otro nuevo punto que estará en el otro extremo de la línea. La situación de ese nuevo punto de referencia se puede establecer con coordenadas absolutas con Cairo.Context.line_to (x:double, y:double) o con coordenadas relativas con Cairo.Context.rel_line_to (x:double, y:double). Un ejemplo:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen de 200x40) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 200, 40)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// propiedades de la linea (color y grosor))
	context.set_source_rgba (1, 0, 0, 1)
	context.set_line_width (1)
 
	// establecemos los puntos de las líneas
	context.move_to (10, 10)  	// punto de inicio
	context.line_to (190, 10) 	// linea horizontal
	context.line_to (190, 20)	// linea vertical
 
	context.move_to (10, 20)	// punto final
	context.line_to (190, 20)	// línea a punto final
 
	// trazamos la ruta
	context.stroke ()
 
	// guardamos la imagen como png
	surface.write_to_png ("linea.png")

Este código genera un archivo llamado linea.png que contiene esta imagen:

Un ejemplo de un dibujo creado con líneas que obtienen las coordenadas de sus puntos de conexión a través de un bucle que recorre un array multidimensional:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init	
	coord : array of int[,]
	coord = {
		{ 75, 75 }, 
		{ 100, 10 }, 
		{ 125, 75 }, 
		{ 200, 85 },
		{ 150, 125 }, 
		{ 160, 190 },
		{ 100, 150 }, 
		{ 40, 190 },
		{ 50, 125 },
		{ 0, 85 }}
 
	// crea una superficie (una imagen) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 200, 200)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// grosor y color de las lineas
	context.set_line_width (1)
	context.set_source_rgb (1, 1, 0)
 
	context.move_to (0, 85)
 
	for var i = 0 to 9
		context.line_to (coord[i, 0], coord[i, 1])  // crea puntos de referencia
 
	// cierra y rellena
	context.close_path ()
	context.fill()
 
	// traza
	context.stroke ()
 
	// guarda la imagen
	surface.write_to_png ("estrella.png")

Podemos utilizar este dibujo de una estrella para crear efectos muy básicos de luz y sombra utilizando los gradientes que ya hemos visto:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init	
	coord : array of int[,]
	coord = {		
		{ 75, 75 }, 
		{ 100, 10 }, 
		{ 125, 75 }, 
		{ 200, 85 },
		{ 150, 125 }, 
		{ 160, 190 },
		{ 100, 150 }, 
		{ 40, 190 },
		{ 50, 125 },
		{ 0, 85 }}
 
	// crea una superficie (una imagen) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 200, 200)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// fondo
	pat1: Cairo.Pattern = new Cairo.Pattern.linear (0.0, 0.0, 0.0, 256.0)
	pat1.add_color_stop_rgba (0, 0.9, 0.9, 1, 1)
	pat1.add_color_stop_rgba (1, 0, 0, 1, 1)
	context.rectangle (0, 0, 256, 256)
	context.set_source (pat1)
	context.fill ()
 
	// sombra (otra estrella detrás, algo movida y oscura con transparencia)
	context.set_line_width (1)
	context.set_source_rgba (0, 0, 0, 0.5)
	context.move_to (0, 85)
	for var i = 0 to 9
		context.line_to (coord[i, 0]+10, coord[i, 1]+10)
	context.close_path ()
	context.fill()
 
	// estrella
	context.set_line_width (1)
	pat2: Cairo.Pattern = new Cairo.Pattern.radial (75, 100, 10, 75, 100, 200)
	pat2.add_color_stop_rgba (0.0, 1, 1, 1, 1)
	pat2.add_color_stop_rgba (0.25, 1, 1, 0.8, 1)
	pat2.add_color_stop_rgba (0.50, 1, 1, 0.5, 1)
	pat2.add_color_stop_rgba (0.75, 1, 1, 0.2, 1)
	pat2.add_color_stop_rgba (1.0, 0, 0, 0, 1)
	context.set_source (pat2)
	context.move_to (0, 85)
	for var i = 0 to 9
		context.line_to (coord[i, 0], coord[i, 1])
	context.close_path ()
	context.fill()
 
	context.stroke ()
	surface.write_to_png ("estrella_luz.png")

Un ejemplo que combina líneas con rectángulos:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init	
	// crea una superficie (una imagen de 120x120) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 120, 120)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// escala de coordenadas 1,0 x 1,0
	context.scale (120, 120)
 
	// propiedades de la línea (color y grosor)
	context.set_source_rgb (0, 0, 0)
	context.set_line_width (0.2)
 
	// puntos de la línea
	context.move_to (0, 0)
	context.line_to (1, 1)
	context.move_to (1, 0)
	context.line_to (0, 1)
 
	// traza la ruta de la línea
	context.stroke ()
 
	// rectángulo rojo con transparencia
	context.rectangle (0, 0, 0.5, 0.5)
	context.set_source_rgba (1, 0, 0, 0.30)
	context.fill ()
 
	// rectángulo verde con transparencia
	context.rectangle (0, 0.5, 0.5, 0.7)
	context.set_source_rgba (0, 1, 0, 0.60)
	context.fill ()
 
	// rectángulo azul opaco
	context.rectangle (0.5, 0, 0.5, 0.5)
	context.set_source_rgba (0, 0, 1, 1)
	context.fill ()
 
	// guarda imagen en archivo
	surface.write_to_png ("color.png")

Por otra parte, podemos especificar ciertas propiedades de las líneas para modificar su aspecto. Vamos a ver las propiedades que se refieren a sus límites (LineCap), a sus puntos de unión (LineJoin) y a su patrón de dibujo (aunque ésta última no es realmente una propiedad específica de las líneas).

En cuanto a sus límites, las líneas terminan en una especie de tapas o tapones a los que se les puede aplicar tres estilos diferentes: cuadrado, redondo o ninguno.

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 200, 100)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// propiedades de la linea (grosor)
	context.set_line_width (20)
 
	// línea roja con límites cuadrados
	context.set_source_rgba (1, 0, 0, 1)
	context.set_line_cap (Cairo.LineCap.SQUARE)
	context.move_to (20, 20)
	context.rel_line_to (150, 0)
	context.stroke ()
 
	// línea verde con límites redondos
	context.set_source_rgba (0, 1, 0, 1)
	context.set_line_cap (Cairo.LineCap.ROUND)
	context.move_to (20, 50)
	context.rel_line_to (150, 0)
	context.stroke ()
 
	// línea azul sin limites
	context.set_source_rgba (0, 0, 1, 1)
	context.set_line_cap (Cairo.LineCap.BUTT)
	context.move_to (20, 80)
	context.rel_line_to (150, 0)
	context.stroke ()
 
	// guardamos la imagen como png
	surface.write_to_png ("limites.png")

Además, al unirse entre sí, las líneas pueden utilizar tres estilos de unión diferentes: esquinado, biselado y redondo.

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 200, 130)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// grosor de las lineas
	context.set_line_width (10)
 
	// unión roja esquinada
	context.set_source_rgba (1, 0, 0, 1)
	context.set_line_join (Cairo.LineJoin.MITER)
	context.move_to (10, 60)
	context.rel_line_to (150, 0)
	context.rel_line_to (-40, -40)
	context.stroke ()
 
	// unión verde biselada
	context.set_source_rgba (0, 1, 0, 1)
	context.set_line_join (Cairo.LineJoin.BEVEL)
	context.move_to (10, 80)
	context.rel_line_to (150, 0)
	context.rel_line_to (-40, -40)
	context.stroke ()
 
	// unión azul redonda
	context.set_source_rgba (0, 0, 1, 1)
	context.set_line_join (Cairo.LineJoin.ROUND)
	context.move_to (10, 100)
	context.rel_line_to (150, 0)
	context.rel_line_to (-40, -40)
	context.stroke ()
 
	// guardamos la imagen como png
	surface.write_to_png ("angulos.png")

Por último, podemos definir el estilo de las líneas según un patrón de guiones (se dibuja la línea como si se fuera levantando el lápiz del papel) que se establece mediante un array de valores que determinan cuando el lápiz toca o no el papel, y un valor del desplazamiento de ese patrón. Este patrón se va repitiendo en positivo y en negativo (valores inversos) a lo largo de la línea hasta que ésta acaba.

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 200, 80)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// grosor de las lineas
	context.set_line_width (2)
 
	// línea roja punteada
	context.set_dash ({1.0}, 0)  // por cada punto dibujado hay otro no dibujado
	context.set_source_rgba (1, 0, 0, 1)
	context.move_to (10, 20)
	context.rel_line_to (180, 0)
	context.stroke ()
 
	// línea verde con guiones
	context.set_dash ({14.0, 6.0}, 0)  // patrón de 14 puntos dibujados y 6 no, y luego al revés
	context.set_source_rgba (0, 1, 0, 1)
	context.move_to (10, 40)
	context.rel_line_to (180, 0)
	context.stroke ()
 
	// línea azul con puntos y guiones
	context.set_dash ({4.0, 21.0, 4.0}, 5)  // patrón separado por 5 espacios
	context.set_source_rgba (0, 0, 1, 1)
	context.move_to (10, 60)
	context.rel_line_to (180, 0)
	context.stroke ()
 
	// guardamos la imagen como png
	surface.write_to_png ("estilos_lineas.png")

Arcos

Otras opciones para crear segmentos de una ruta son los arcos de un círculo con Cairo.Context.arc() cuando los puntos se conectan en sentido de las agujas del reloj y con Cairo.Context.arc_negative() cuando se conectan en sentido inverso, y las curvas de Bézier con Cairo.Context.curve_to () y con Cairo.Context.rel_curve_to (), que empiezan en el punto de referencia actual y siguen suavemente hacia otros dos puntos.

Un ejemplo que utiliza la ruta de un arco para cortar una parte circular de una imagen, escalarla y guardarla:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen de 256x256) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 256, 256)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// arco
	context.arc (128.0, 128.0, 76.8, 0, 2*Math.PI)
	context.clip ()  	// recorta el arco
	context.new_path () 	// después de clip, la ruta se borró
 
	// imagen de origen (ruta y formato)
	image_path:string = GLib.Path.build_filename (GLib.Path.get_dirname (args[0]) , "paisaje.png")
	image: Cairo.ImageSurface = new Cairo.ImageSurface.from_png (image_path)
 
	// escala
	w:int = image.get_width ()
	h:int= image.get_height ()
	context.scale (256.0/w, 256.0/h)
 
	context.set_source_surface (image, 0, 0)
	context.paint ()
 
	// guarda la nueva imagen
	surface.write_to_png ("paisaje_arco.png")

Este código actúa sobre la primera imagen para obtener un archivo nuevo que contiene la segunda imagen:

Otro ejemplo que combina rectángulo y arco con gradientes lineal y radial:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init	
	// crea una superficie (una imagen de 256x256) y un contexto vinculado
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 256, 256)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// rectángulo con gradiente lineal
	pat1: Cairo.Pattern = new Cairo.Pattern.linear (0.0, 0.0, 0.0, 256.0)
	pat1.add_color_stop_rgba (1, 0, 0, 0, 1)
	pat1.add_color_stop_rgba (0, 1, 1, 1, 1)
	context.rectangle (0, 0, 256, 256)
	context.set_source (pat1)
	context.fill ()
 
	// arco con gradiente radial
	pat2: Cairo.Pattern = new Cairo.Pattern.radial (115.2, 102.4, 25.6, 102.4, 102.4, 128.0)
	pat2.add_color_stop_rgba (0, 1, 1, 1, 1)
	pat2.add_color_stop_rgba (1, 0, 0, 0, 1)
	context.set_source (pat2)
	context.arc (128.0, 128.0, 76.8, 0, 2 * Math.PI)
	context.fill ()
 
	// guarda la imagen
	surface.write_to_png ("arco_gradiente.png")

Texto

Aunque un texto también se puede convertir en una ruta con Cairo.Context.text_path (utf8: string), sin embargo, para evitar problemas de rendimiento especialmente en caso de grandes cantidades de texto, es preferible utilizar Cairo.Context.show_text().

Podemos especificar la fuente y el estilo con Cairo.Context.select_font_face(), y el tamaño con Cairo.Context.set_font_size(). Un ejemplo básico:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen de 440x60) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 440, 60)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// propiedades del texto (color, fuente y tamaño)
	context.set_source_rgb (0.1, 0.1, 0.1)
	context.select_font_face ("Purisa", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD)
	context.set_font_size (50)
 
	// punto de inicio
	context.move_to (20, 50)
 
	context.show_text ("Genie + Cairo")  
 
	// guarda la imagen
	surface.write_to_png ("texto.png")

Pero además de estas propiedades del estilo del texto, para utilizar el texto de manera efectiva es necesario saber de manera exacta su posición y el espacio que ocupa. Los métodos Cairo.Context.font_extents() y Cairo.Context.text_extents() nos proporcionan esa información en base a un punto de referencia inicial situado en la línea de base, un punto de referencia final y un cuadro o caja delimitador (ancho x altura).

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (una imagen de 340x256) y un contexto
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 340, 220)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// texto
	utf8: string = "Genie"
 
	// propiedades del texto (fuente y tamaño)
	context.select_font_face ("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL)
	context.set_font_size (100.0)
 
	// posición del texto
	extents: Cairo.TextExtents
	context.text_extents (utf8, out extents)
	x: double = 25.0
	y: double = 150.0
	context.move_to (x, y)  // coordenadas de inicio de línea de base
	context.show_text (utf8)
 
	// propiedades de la línea (color y grosor)
	context.set_source_rgba (1, 0.2, 0.2, 0.6)
	context.set_line_width (6.0)
	// arco que dibuja punto de inicio
	context.arc (x, y, 10.0, 0, 2*Math.PI)
	context.fill ()
	//línea que rodea el texto
	context.move_to (x, y)
	context.rel_line_to (0, -extents.height)
	context.rel_line_to (extents.width, 0)
	context.rel_line_to (extents.x_bearing, -extents.y_bearing)
	// arco que dibuja punto final	
	context.arc (extents.x_advance+x, y, 10.0, 0, 2*Math.PI)
	// context.arc (extents.width+x, y, 10.0, 0, 2*Math.PI)  //también válido
	context.fill ()
 
	// traza ruta
	context.stroke ()
 
	// guarda la imagen
	surface.write_to_png ("extents.png")

Operadores

Normalmente, cuando dibujamos, los objetos se van colocando como capas unos encima de otros de manera que el más reciente cubre o tapa al más antiguo, pero Cairo ofrece una serie de procedimientos alternativos, llamados operadores (operators) que permiten más posibilidades.

Un operador se llaman así: context.set_operator (Cairo.Operator.valor_operador), donde hay que sustituir valor_operador por uno de los valores posibles.

Los operadores admiten los siguientes valores: CLEAR, SOURCE, OVER, IN, OUT, ATOP, DEST, DEST_OVER, DEST_IN, DEST_OUT, DEST_ATOP, XOR, ADD, SATURATE, MULTIPLY, SCREEN, OVERLAY, DARKEN, LIGHTEN, COLOR_DODGE, COLOR_BURN, HARD_LIGHT, SOFT_LIGHT, DIFFERENCE, EXCLUSION, HSL_HUE, HSL_SATURATION, HSL_COLOR, HSL_LUMINOSITY.

El operador que actúa por defecto (cuando no se especifica otro) es OVER, que muestra una imagen compuesta por objetos uno encima de otro (con transparencias si las tienen).

Puedes comprobar como actúan los distintos operadores si compilas y ejecutas el siguiente código:

Descarga: operador.txt
// compila con valac --pkg cairo nombre_archivo.gs
uses Cairo
 
init
	stdout.printf( "¿Operador? ")
	ope:string = stdin.read_line()
	example(ope)
 
def example(ope:string): bool
 
	surface: Cairo.ImageSurface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 160, 120)
	context: Cairo.Context = new Cairo.Context (surface) 
 
	context.rectangle (0, 0, 120, 90)
	context.set_source_rgba (0.7, 0, 0, 0.8)
	context.fill ()
 
	if ope == "clear"
		context.set_operator (Cairo.Operator.CLEAR)
	else if ope == "source"
		context.set_operator (Cairo.Operator.SOURCE)
	else if ope == "over"
		context.set_operator (Cairo.Operator.OVER)
	else if ope == "in"
		context.set_operator (Cairo.Operator.IN)
	else if ope == "out"
		context.set_operator (Cairo.Operator.OUT)
	else if ope == "atop"
		context.set_operator (Cairo.Operator.ATOP)
 
	else if ope == "dest"
		context.set_operator (Cairo.Operator.DEST)
	else if ope == "dest_over"
		context.set_operator (Cairo.Operator.DEST_OVER)
	else if ope == "dest_in"
		context.set_operator (Cairo.Operator.DEST_IN)
	else if ope == "dest_out"
		context.set_operator (Cairo.Operator.DEST_OUT)
	else if ope == "dest_atop"
		context.set_operator (Cairo.Operator.DEST_ATOP)
 
	else if ope == "xor"
		context.set_operator (Cairo.Operator.XOR)
	else if ope == "add"
		context.set_operator (Cairo.Operator.ADD)
	else if ope == "saturate"
		context.set_operator (Cairo.Operator.SATURATE)
	else if ope == "multiply"
		context.set_operator (Cairo.Operator.MULTIPLY)
	else if ope == "screen"
		context.set_operator (Cairo.Operator.SCREEN)
	else if ope == "overlay"
		context.set_operator (Cairo.Operator.OVERLAY)
	else if ope == "darken"
		context.set_operator (Cairo.Operator.DARKEN)
	else if ope == "lighten"
		context.set_operator (Cairo.Operator.LIGHTEN)
	else if ope == "color_dodge"
		context.set_operator (Cairo.Operator.COLOR_DODGE)
	else if ope == "color_burn"
		context.set_operator (Cairo.Operator.COLOR_BURN)
	else if ope == "hard_light"
		context.set_operator (Cairo.Operator.HARD_LIGHT)
	else if ope == "soft_light"
		context.set_operator (Cairo.Operator.SOFT_LIGHT)
	else if ope == "difference"
		context.set_operator (Cairo.Operator.DIFFERENCE)
	else if ope == "exclusion"
		context.set_operator (Cairo.Operator.EXCLUSION)
	else if ope == "hsl_hue"
		context.set_operator (Cairo.Operator.HSL_HUE)
	else if ope == "hsl_saturation"
		context.set_operator (Cairo.Operator.HSL_SATURATION)
	else if ope == "hsl_color"
		context.set_operator (Cairo.Operator.HSL_COLOR)
	else if ope == "hsl_luminosity"
		context.set_operator (Cairo.Operator.HSL_LUMINOSITY)	
 
	context.rectangle (40, 30, 120, 90);
	context.set_source_rgba (0, 0, 0.9, 0.4);
	context.fill ()
 
	surface.write_to_png ("operator.png")
 
	return true

Al ejecutarlo, el programa te pide en consola el nombre de un operador y si lo reconoce (todo en minúsculas) lo aplica al dibujo de base. El dibujo de base está compuesto por dos rectángulos con transparencia superpuestos:

Si el programa no reconoce el nombre introducido vuelve a dibujar el dibujo base. Algunos ejemplos:

Seleccionar superficie

Una vez que tenemos realizado el dibujo nos queda seleccionar la superficie de destino donde se plasmará nuestra creación.

PNG, PDF, SVG

Hasta ahora todos los dibujos realizados han quedado guardados en archivos de imagen con formato png, pero hay más posibilidades.

Podemos cambiar el formato de salida cambiando el tipo de superficie. Por ejemplo, a partir de un ejemplo anterior, para obtener el mismo texto en un archivo con formato pdf:

// compila con valac --pkg cairo nombre_archivo.gs
 
uses Cairo
 
init
	// crea una superficie (un pdf de 504x648) y un contexto	
	surface: Cairo.PdfSurface = new Cairo.PdfSurface ("texto.pdf", 504, 648)
	context: Cairo.Context = new Cairo.Context (surface)
 
	// propiedades del texto (color, fuente y tamaño)
	context.set_source_rgb (0.1, 0.1, 0.1)
	context.select_font_face ("Purisa", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD)
	context.set_font_size (50)
 
	// punto de inicio
	context.move_to (20, 50)
 
	context.show_text ("Genie + Cairo")

Y para obtenerlo en un archivo svg, tan solo hay que volver a cambiar el tipo de superficie: en el ejemplo anterior cambiamos la línea que crea una superficie pdf por esta otra línea de código:

surface: Cairo.SvgSurface = new Cairo.SvgSurface("svgfile.svg", 440, 60)

GTK+

Otra posibilidad muy interesante a la hora de seleccionar la superficie de destino consiste en una ventana del sistema gracias a la integración de Cairo con Gtk+. Para ello, recurrimos al widget Gtk.DrawingArea que crea un área de dibujo. Añadimos el área de dibujo a una ventana y la vinculamos a una función donde podemos desarrollar todas las herramientas de Cairo.

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
	Cairo
 
init
	Gtk.init (ref args)
	var TestCairo = new Ventana ()
	TestCairo.show_all ()
	Gtk.main ()
 
class Ventana : Window
 
	area: Gtk.DrawingArea
 
	init		
		title = "Test Genie + GTK + Cairo"
		set_default_size (400, 400)
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)
 
		// área de dibujo
		area: Gtk.DrawingArea = new Gtk.DrawingArea ()
		// conecta el área de dibujo al método dibujar
		area.draw.connect (dibujar) 
		// añade el área de dibujo a la ventana
		add (area)
 
	def dibujar (context : Context) : bool		
 
		context.set_source_rgba (1, 0, 0, 1)		
		context.set_line_width (2)
 
		context.move_to (200, 100)
		context.line_to (200, 300)
 
		context.move_to (100, 200)
		context.line_to (300, 200)
 
		context.stroke ()
 
		return true

Si te has dado cuenta, cuando usamos Gtk+ con Cairo no necesitamos pasarle al compilador valac el parámetro --pkg cairo, esto es así porque desde la versión 2.8, la librería Cairo forma parte del sistema GTK+.

Vamos a seguir jugando practicando con esta productiva asociación. Un ejemplo que crea rectángulos y transparencias en un bucle for:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
	Cairo
 
init
	Gtk.init (ref args)
	var TestCairo = new Ventana ()
	TestCairo.show_all ()
	Gtk.main ()
 
class Ventana : Window
 
	area: Gtk.DrawingArea
 
	init		
		title = "Test Genie + GTK + Cairo"
		set_default_size (600, 200)
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)
 
		area: Gtk.DrawingArea = new Gtk.DrawingArea ()		
		area.draw.connect (dibujar)
		add (area)
 
	def dibujar (context : Context) : bool
 
		for var i = 1 to 10
			context.set_source_rgba(1, 0, 0, i*0.1)  // rojo
			context.rectangle(50*i, 20, 40, 40)  // x, y, ancho, alto
			context.fill()
 
			context.set_source_rgba(0, 1, 0, i*0.1)  // verde
			context.rectangle(50*i, 80, 40, 40)  
			context.fill()
 
			context.set_source_rgba(0, 0, 1, i*0.1)  // azul
			context.rectangle(50*i, 140, 40, 40)
			context.fill()
 
		return true

Ejemplo de una sencilla marca de agua sobre una fotografía:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
	Cairo
 
init
	Gtk.init (ref args)
	var TestCairo = new Ventana ()
	TestCairo.show_all ()
	Gtk.main ()
 
class Ventana : Window
 
	area: Gtk.DrawingArea
 
	init		
		title = "Test Genie + GTK + Cairo"
		set_default_size (400, 400)
		window_position = WindowPosition.CENTER		
		set_resizable (false)
		destroy.connect(Gtk.main_quit)
 
		area: Gtk.DrawingArea = new Gtk.DrawingArea ()		
		area.draw.connect (dibujar)
		add (area)
 
	def dibujar (context : Context) : bool
 
		image: Cairo.ImageSurface = new Cairo.ImageSurface.from_png ("paisaje.png")
 
		context.set_source_surface (image, 0, 0)
		context.paint ()
 
		context.set_font_size(16)
		context.set_source_rgb(1 , 1 , 1)
		context.move_to(180, 390)
		context.show_text("Wiki Genie Doc (CC BY 4.0) ")
		context.stroke()
 
		return true

Un ejemplo para graduar el nivel de transparencia de una ventana con Cairo:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
	Cairo
 
init
	Gtk.init (ref args)
	var TestCairo = new Ventana ()
	TestCairo.show_all ()
	// TestCairo.set_opacity (0.5)  // funciona pero OBSOLETO para ventanas
	Gtk.main ()
 
class Ventana : Window
 
	area: Gtk.DrawingArea
	context: Cairo.Context
 
	init		
		title = "Test Genie + GTK + Cairo"
		set_default_size (300, 250)
		window_position = WindowPosition.CENTER
 
		set_app_paintable(true)
		set_visual(Gdk.Screen.get_default().get_rgba_visual())
 
		destroy.connect(Gtk.main_quit)
 
		area: Gtk.DrawingArea = new Gtk.DrawingArea ()
		area.draw.connect (dibujar)
		add (area)
 
	def dibujar (context : Context) : bool
 
		context.set_source_rgba(0.2, 0.2, 0.2, 0.4)
		context.set_operator(Cairo.Operator.SOURCE)
		context.paint()
 
		return true
🔝