Arte de pixel con Google Apps Script (y Mandelbrot) · parte 1


Iniciamos con este artículo una serie de publicaciones en las que trataremos de familiarizarnos un poco más con Google Apps Script (como de costumbre, GAS en adelante). De GAS ya he hablado de modo puntual y aislado anteriormente en este blog, por ejemplo aquí y aquí. En esta ocasión, sin embargo, la idea es desarrollar un pequeño ejemplo práctico que toque varios palos de este lenguaje de guiones de Google.

Supongo que podría haber escogido una aplicación de uso más cercana a los intereses de un profe, pero dado que esto se trata en definitiva de aprender a utilizar GAS he preferido buscar algo más vistoso y entretenido.

Sí, lo has adivinado: vamos a programar arte de pixel (o pixel art, como dirían los anglosajones). Aunque en nuestro caso será una hoja de cálculo el lienzo y sus celdas las que harán las veces de píxeles. Si percibís algún tufillo nostálgico relacionado con ordenadores ochentosos estais en lo cierto. Que levante la mano el que programó en sus años mozos en un Spectrum 48K, Amstrad CPC, Commodore 64, Oric Atmos o MSX (yo fui de estos últimos). Pénsándolo bien puede que ese antológico "no podrás, a menos que sepas BASIC" proferido por un ex Goonie en el capítulo 8 de la segunda temporada de Stranger Things me haya dejado un poquito sensible. Pero no nos desviemos, que este es un blog útil, productivo y respetable (o al menos pretende serlo) :-).

Al grano pues. Tras leer ester primer artículo sabrás cómo:
  • Añadir comandos a la barra de menú de una hoja de cálculo de Google para desencadenar la ejecución de guiones GAS.
  • Recorrer las celdas de una hoja de cálculo mediante bucles.
  • Modificar la anchura, altura y color de relleno de las celdas.
A lo largo del desarrollo de este ejemplo voy a tratar de utilizar un código lo más sencillo y comprensible posible, aunque eso suponga eludir ciertas estructuras que podrían ser más compactas y eficientes. De modo análogo, evitaré entrar en detalles que pudieran oscurecer la comprensión de los aspectos fundamentales que se ponen de manifiesto, aún a costa de perder completitud y profundidad. El objetivo no es aprender a construir código del modo más correcto posible, para eso ya hay otros espacios y caminos más adecuados, sino perder el miedo a poner juntas cuatro líneas para automatizar algunas tareas a las que te enfrentes.

Empezando por el principio.

Comenzaremos por crear una nueva hoja de cálculo, abrir el editor de secuencias de comandos y darle nombre a nuestro guión.

Mi primer guión GAS.

Esto de darle nombre al proyecto parece que no tenga importancia... al fin y al cabo, se trata de un código vinculado a la hoja de cálculo ¿no? Verdad, pero solo a medias. Si nos aficionamos a esto de ir creando guiones GAS es buena idea poder identificar fácilmente cuáles tenemos activos en las hojas de cálculo desparramadas en nuestro Google Drive. Para ello podemos dirigirnos a:


Mi cuenta → Aplicaciones con acceso a la cuenta → Administrar las aplicaciones

Allí veremos todas aquellas a las que en un momento u otro les hemos otorgado permisos de ejecución, permisos que naturalmente podremos revocar con un clic.


No busques ahora tu guión en la lista. Ni siquiera lo has ejecutado aún. Cuando lo hagas por primera vez deberás autorizarlo. Es entonces cuando esa autorización quedará registrada aquí.

Antes de poner los dedos sobre las teclas.

Del mismo modo que en otros artículos sobre GAS, tengo que recordarte que tengas muy presente la página que Google dedica a esta herramienta, y muy especialmente en este caso la sección en la que se presentan a modo de referencia las clases pertenecientes a la hoja de cálculo.


Deberías mantener una ventana del navegador permanentemente abierta en este recurso mientras juegas con GAS.

Personalizando los menús de la hoja de cálculo.

Lo primero que vamos a hacer es insertar un comando en el menú de nuestra hoja de cálculo para ejecutar el guión a voluntad. Estos menús personalizados no son persistentes, por lo que tendremos que recurrir al disparador (trigger) onOpen() para colocarlo en su sitio cada vez que se abra el documento. Más sobre estos disparadores aquí.

  • En la línea 4 se obtiene el objeto que representa a la hoja de cálculo actual empleando el método getActiveSpreadSheet().
  • A continuación, la línea 5 crea un nuevo comando de menú denominado Mandelbrot que ejecutará el código de la función cellmandelbrot. No, esta función aún no existe, pero no pasa nada.
  • Por último, la línea 6 inserta efectivamente el comando en el menú superior de la interfaz de usuario de nuestra hoja de cálculo por medio del método addMenu de la hoja de cálculo actual.
Aquí tienes el código en formato de texto por si quieres pegarla ya en tu propia hoja de cálculo mientras sigues este artículo.

/* Añade las funciones al menú y las invoca al abrir */

function onOpen() {
  var mihoja = SpreadsheetApp.getActiveSpreadsheet();
  var menu = [{name:"Mandelbrot", functionName:"cellmandelbrot"}];
  mihoja.addMenu("Scripts", menu);
};

No te olvides de guardar los cambios.


Ahora solo tienes que recargar la hoja de cálculo para comprobar que ha aparecido un menú denominado Scripts y, dentro de él, nuestro comando Mandelbrot.


¿Y qué pasa si intentas ejecutarlo? Probémoslo:

Autorizando el guión.

Google (ahora sí), te indicará que es necesario autorizar la ejecución del guión. A continuación mostrará otra advertencia diciendo que no se trata de una aplicación verificada (no procede de la tienda de complementos). Si testarudamente activas el modo avanzado y sigues adelante tendrás la oportunidad de revisar los permisos que se le van a conceder al guión. Por último, el proceso finalizará con un bonito mensaje de error totalmente esperable puesto que la función cellmandelbrot no existe aún.

Todas estas advertencias pueden parecer un incordio pero son relevantes. Los guiones pueden hacer cosas muy útiles pero también auténticas malezas. Ni que decir tiene que ir autorizando código procedente de quién sabe dónde no es buena idea, a menos que hayas revisado personalmente qué es lo que hace con tus archivos y/o confíes totalmente en su editor. Un gran poder conlleva una gran responsabilidad (por supuesto).

Mandelbrot entra en juego ¡a dibujar!

Ahora vamos a convertir esta aburrida hoja de datos en un lienzo. Dibujaremos el conocido conjunto de Mandelbrot.


Aunque en el enlace anterior a la Wikipedia lo explican mucho mejor que yo, me gustaría dedicar unas líneas a esta sorprendente entidad matemática.

El conjunto de Mandelbrot se calcula a partir de una fórmula iterativa en el plano complejo. Si partiendo de un número complejo dado la sucesión calculada resulta estar acotada se dice que ese número pertenece al conjunto. Si por el contrario explota sin control está fuera de él. Para representar esto en pantalla se asocia a cada número complejo (parte real y parte imaginaria) un pixel por medio de sus coordenadas X e Y, y se inicia el cálculo iterativo. Los puntos que pertenecen al conjunto se pintan de negro. Los que no, de blanco. Como no podemos calcular un número infinito de iteraciones se suele establecer un límite. Para obtener las bonitas gradaciones de colores asociadas a este tipo de representaciones gráficas se suele establecer algún tipo de correspondencia entre el número de iteraciones que han sido necesarias para comprobar la no pertenencia de un punto al conjunto y el color con el que se representa.

¿Por qué Mandelbrot? Porque los fractales son unas entidades matemáticas absolutamente fascinantes que han atraído durante años la atención de matemáticos, artistas, diseñadores, ingenierios... Y quedan muy bien en pantalla. Ni más ni menos.

Fin de la licencia artística, proseguimos con GAS.

La función cellmandelbrot().

Primero necesitaremos definir tanto el tamaño de nuestro lienzo (filas y columnas de la hoja de datos) como de sus píxeles (celdas), que asumiremos perfectamente cuadrados. También estableceremos ciertas variables que necesitaremos más adelante y limpiaremos la hoja de datos (al terminar te facilitaré todo el código):


Veamos:
  • El tamaño del lienzo se define en las líneas 15 (columnas) y 16 (filas).
  • El tamaño del píxel (altura y anchura de cada celda) se establece en la línea 17.
  • En la línea 18 se obtiene el objeto que representa a la hoja de datos activa dentro de la hoja de cálculo actual. Anteriormente hicimos algo parecido para identificar precisamente esta hoja de cálculo e insertar en ella nuestro comando en el menú superior (los menús son generales a todas las hojas de datos de una hoja de cálculo). Fíjate en cómo se concatenan ahora los métodos getActiveSpreadsheet() y getActiveSheet(). El primero, aplicado al objeto SpreadsheetApp, proporciona la hoja de cálculo, en tanto que el segundo devuelve la hoja de datos activa. Esto es algo que haremos muy a menudo al programar en GAS.
  • En las filas 19-20 emplearemos los métodos getMaxRows() y getMaxColumns() para determinar el número de filas y columnas que tiene en estos momentos la hoja de datos.
  • Por último, en la fila 24 se elimina tanto el contenido como el formato de la hoja de datos actual utilizando el método clear().
¿A que esto es más fácil de lo que parecía?

A continuación ajustaremos filas, columnas y celdas:


Aquí entran en juego dos estructuras de control del flujo del programa.

Primeramente nos encontramos con la secuencia condicional if..else, que se utiliza para ejecutar unas acciones u otras en función de determinadas pruebas lógicas, en este caso el valor comparado de ciertas variables.

A continuación, se utiliza la secuencia de repetición for, que permite construir un bucle que ejecuta determinadas instrucciones un número específico de veces: la variable x va tomando valores entre 1 y el valor asignado a celdasX y celdasY, respectivamente, incrementando su valor en 1 al final de cada repetición de las instrucciones entre {..}.

Veámoslo con un poco más de calma:
  • Las lineas 28-29 se encargan de ajustar el número de columnas para que coincida con el establecido en la variable celdasX. Para ello se compara este valor con el número de columnas presentes, insertando o eliminando según sea necesario mediante los métodos insertColumns y deleteColumns de la hoja actual. Ambos reciben como parámetros, dentro de los paréntesis, la posición de la columna en la que realizar la operación y el número de columnas a insertar o eliminara a partir de ella.
  • De modo análogo, las líneas 33-34 hacen lo propio con las filas. En este caso intervienen la variable celdasY y los métodos insertRows y deleteRows.
  • Una vez la hoja de datos tiene ya las dimensiones requeridas, las líneas 38-39 recorren mediante sendos bucles for sus columnas y filas para ajustar respectivamente su anchura y altura por medio de los métodos setColumnWidth y setRowHeight.  En ambos casos el primer parámetro especifica la columna o fila y el segundo el tamaño, en píxeles.
 Por último, trazaremos el conjunto de Mandelbrot correspondiente a las celdas de nuestro lienzo:



Analicemos ahora este fragmento de código:
  •  Las líneas 44-45 establecen el número máximo de iteraciones en el cálculo y el factor de zoom sobre el conjunto. Una cosa muy chula de las representaciones fractales es que podemos adentrarnos más y más en ellas para descubrir regiones inexploradas que presentan un nivel de detalle infinito. Esto se consigue jugando con este último parámetro y desplazando las coordenadas del lienzo. Pero esa es otra historia...
  • El meollo del cálculo se encuentra en las líneas 47-58. No obstante podemos encapsular su complejidad entre corchete abierto y corchete cerrado y simplemente creer que hacen lo que tienen que hacer, que no es otra cosa que determinar si un píxel concreto pertenece o no (dentro de la precisión que nos proporciona el valor fijado para el número máximo de iteraciones) al conjunto de Mandelbrot. Para ello se emplean sendos bucles for anidados que recorren las filas (línea 47) y, dentro de cada una de ellas, todas sus columnas (línea 48). En este fragmento se introduce una nueva estructura de repetición (bucle while). Funcionalmente es similar al for, aunque se diferencia de él en que las instrucciones entre corchetes se ejecutan mientras se cumplan determinadas condiciónes, que en este caso son (a) la que determina la pertenencia o no de cada punto al conjunto de Mandelbrot y (b) la que vigila que no se exceda el número máximo de iteraciones prefijadas en el cálculo.
  • Ya para concluir, las líneas 62-65 colorean cada celda utilizando el método setBackgroundRGB del rango de celdas (compuesto por una única celda) que obtenemos a su vez entubando el método getRange perteneciente al objeto que representa a nuestra hoja de datos. Si el píxel está dentro del conjunto se pinta de negro. En caso contrario se le aplica otro color. Yo he utilizado en esta ocasión una función de mapeado que utiliza el número de iteraciones efectivas empleadas para el cálculo de cada píxel para establecer de manera complementaria las componentes roja y azul del color de fondo, fijando la verde a un valor de 60 para ajustar el tono general de color. Puedes echarle imaginación a esto para conseguir cosas más chulas.
 ¿Y cuál es el resultado? Pues algo como esto:


Retrovistoso ¿verdad?

Aquí tienes la hoja de cálculo utilizada a lo largo del artículo. Obtén una copia (Archivo → Crear una copia) y juega con ella. Puedes modificar las variables celdasX, celdasY, tamanyocelda, maxiter y zoom para generar tus propios conjuntos de Mandelbrot.


En un próximo artículo adecentaremos un poco el código, utilizando funciones para encapsular ciertas partes, y construiremos una pequeña interfaz de usuario en HTML para que resulte más fácil interaccionar con el guión y... bueno, seguro que se me ocurre alguna que otra cosa más.

Comentarios