Cairo
Tabla de Contenidos
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.
- Seleccionar una fuente: colores, gradientes, imágenes.
- Posicionar puntos de inserción y crear rutas para dibujar y pintar y para escribir texto.
- 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