Pequeños Múltiples

Había una vez una empresa llamada CitiSent, donde una de las primeras ideas que tuvimos para ofrecer servicios de visualización se refería a comprender y comparar la calidad de vida de las distintas ciudades del país1.

Para construir esta visualización desarrollamos un tesauro2 de 6 grandes temas urbanos que generaban un indicador de percepción a partir del análisis de texto (sentiment analysis) en redes sociales. Cada uno de estos indicadores determina el tamaño de un pétalo, debidamente codificado con un color. La idea es que este sistema de representar (y condensar) la imformación permite comparar muy eficientemente las diferencias y contrastes entre los datos (las disparidades). Esta modalidad se denomina pequenos múltiples (small multiples). Esto es similar a las caras de Chernoff.

show code

CitiPulse model;
Petal current;

color[] c = {
#D63F09,
#FFA94D,
#FAE203,
#8FCB35,
#00828E,
#00426E,
#A64DFF,
#EB12EB,
#AE1D04,
#FF8408,
#DECC02,
#7EB331,
#0F8CA5,
#135589,
#8A27EC,
#CA0DCA,
#870101,
#C46200,
#B8AE01,
#6C9A2E,
#2096C0,
#2D6EAD,
#6C00D7,
#A809A8,
#690808,
#984805,
#929100,
#59802A,
#31A1DA,
#4688D1,
#4C0098,
#860486
};
String file;

float MAX = 120; // radio máximo del pétalo
float offset = 5;
color currentCol = color(255);

void setup() {
int side = 350;
size(350, 350);
smooth();
noStroke();
model = new CitiPulse(width/2, height/2);
populateCitiPulse(model);
current = new Petal();
file = ""+year()+month()+day()+hour()+minute()+".mov";
}

/**
*
* alfa + PI
* beta = ––––------
* 2
*
*
*
* MAX * sin(alfa/2)
* radio = -----------------
* sin(alfa/2) + 1
*
*/

void draw() {
background(255);
noFill();
stroke(#E3E3E3);
strokeWeight(1);
ellipse(width/2, height/2, MAX*2 + offset*2, MAX*2 + offset*2);
model.draw();
}

void keyPressed() {
if (key == 'v') {
for (int i = 0; i < model.petal.length; i++) {
println("petal["+i+"].value = "+model.petal[i].value);
}
println("---------------------------------------------");
}
if (key == ' ') {
populateCitiPulse(model);
}
if (key == 'x') {
exit();
}
if (key == 's') {
saveFrame("img/citipulse-#####.png");
}
}

void mouseMoved() {
currentCol = get(mouseX, mouseY);
}

void mousePressed() {
println(current.name+", cuyo valor es "+current.value);
}

class CitiPulse {

float x, y;
Petal[] petal;

CitiPulse(float x, float y) {
this.x = x;
this.y = y;
}

void draw() {
float offset = 5;
for (int i = 0; i < petal.length; i++) {
pushMatrix();
{
translate(x, y);
rotate(petal[i].alfa * (float)i);
petal[i].be();
}
popMatrix();
}
}
}

void populateCitiPulse(CitiPulse mod) {

int num = round(random(3, 10)); // número de pétalos o términos de primer nivel

float alfa = TWO_PI / (float)num;
float beta = (alfa + PI) / 2;

float radio = (MAX * sin(alfa/2))/(1 + sin(alfa/2));
float ac = MAX - radio;

println(" num = "+num);
println(" alfa = "+alfa+"\t ("+degrees(alfa)+" grados)");
println(" beta = "+beta);
println("radio = "+radio);
float result = radio + ac;
println(" AC = "+ac+"\t radio + AC = "+result);
println("---------------------------------------------");

mod.petal = new Petal[num];

for (int i = 0; i < mod.petal.length; i++) {
mod.petal[i] = new Petal();
mod.petal[i].alfa = alfa;
mod.petal[i].beta = beta;
mod.petal[i].r = radio;
mod.petal[i].ac = ac;
mod.petal[i].x = mod.x;
mod.petal[i].y = mod.y;

// lo único de cada pétalo
mod.petal[i].value = random(.2, 1);
mod.petal[i].name = "Elemento "+i;
mod.petal[i].c = c[i];
}
}

class Petal {
float x, y;
color c, co; // color & color over
float value;
String name;
int seed;

/* los valores a continuación son comunes para todos los pétalos */
float alfa, beta;
float r; // radio
float ac; // distancia desde el centro de la flor al centro del círculo

Petal() {
seed = round(random(99999));
}

Petal(float x, float y, float value, color col, String name) {
this.x = x;
this.y = y;
this.value = value;
this.name = name;
c = col;
}

void be() {
noiseSeed(seed);
value = noise(seed + (float)millis()/6000);

if (over(currentCol)) {
current = this;
co = color(red(c)-25, green(c)-25, blue(c)-25);
draw(0, 0, r + offset, co);
}

draw(offset, 0, r, c);

}

boolean over(color col) {
if (c == col) {
return true;
}
else {
return false;
}
}

void draw(float ox, float oy, float radio, color col) {
fill(col);
noStroke();
pushMatrix();
{
translate(ox, oy);
beginShape();
vertex(0, 0);

for (float t = -beta; t <= beta; t+=.1) {

float xpos = value*cos(t)*radio;
float ypos = value*sin(t)*radio;

vertex(ac*value + abs(r-radio) + xpos, ypos);
}
endShape(CLOSE);
}
popMatrix();
}
}

Este ejemplo de la unidad de visualización puede ser regenerado al presionar ESPACIO, pero es preciso ganar foco sobre el elemento haciendo click sobre él

  1. a este servicio lo llamamos CitiPulse []
  2. Un tesaurio es un diccionario jerárquico de palabras. En el caso de Citipulse definimos 6 grandes categorías: Medioambiente, Movilidad, Entorno Urbano, Sociabilidad, Gobernanza y Economía Local y Oportunidades. Cada una de ella dividias, a su vez, en otras sub-categorías, hasta completar un vocabulario de alrededor de 500 palabras. []

Topologoscopio

En la utopía de la ciudad cívica (en ese espíritu de MediaFranca) más allá del espacio comprendido como la permanente y odiosa disputa, el espacio público se presenta como un símbolo, en la representación permanente de la escena pública (en el sentido teatral).

El espacio público de Valparaíso es un espacio en disputa, como el de tantas otras ciudades. Esta disputa se fija, en mayor o menor medida, en la forma arquitectónica, urbana y paisajística resultante que en su conjunto constituye la ciudad. Pero el espacio urbano es también, por definición, abierto y público: todos tenemos palabra y postura frente a este espacio en un divergencia creativa, expresión de las diversas voluntades y anhelos. Pero esta conversación es invisible y privada, alcanzando sólo la visibilidad producto de la controversia y la disputa.

Planteamos que el desafío urbano actual es recomponer el estatus simbólico del espacio público como “espacio para la representación”; constituyendo diversos hitos escenográficos para dar cabida esta conversación pública (agones), prefigurando la justa y necesaria convergencia que culmina en la forma construida de la ciudad, forma en constante transformación y ajuste. El arquetipo es el ágora griega o el foro romano, hoy ausentes en forma y función. Pero más gravemente, en espíritu. Topologoscopio entonces reúne a la conversación entre ciudadanos, su ciudad comtemplada y su palabra; todo esto en la iscripción de una escena urbana.

La palabra inscrita en el espacio público tiene una tradición de autoridad oficial, en opsición al grafitti; forma subrecticia y rebelde. La palabra espacial-espaciada viene a decir de aquello que debe permanecer para guardarse en la memoria colectiva. Apela al sentido de lo trascendente de la polis.

Aquí, la palabra intencionada puede quedar fija, (o fijada, al menos por un tiempo, en su cuota de permanencia borrada por el clamor de las otras voces, en una suerte de palimpsesto razonado).

Ficha Técnica:

Este proyecto corresponde al trabajo presentado por la e[ad] Escuela de Arquitectura y Diseño PUCV a la muestra “Work in Progress” realizada en la FADEU Facultad de Arquitectura y Estudios Urbanos PUC en abril de 2015.

Equipo

Juan Carlos Jeldes, Rodrigo Saavedra, Valentina Roco, Herbert Spencer

GridSpace

GridSpace is a very small program developed in Processing that creates grid-based compositions. It was first developed back in 2008 and today I’m updating and releasing the code. This program allows you to export bitmaps and PDFs.

Planteamiento

La pintura comienza con la partición ordenada del espacio. La grilla cartesiana representa topológicamente la idea de la continuidad espacial. La gracia de este programa radica en la capacidad de distorsionar el espacio antes de comenzar. El proceso pictórico es básico: líneas que corren dentro de la grilla. La experimentación radica en la manipulación del espacio, no de las grafías o accidentes que ocurren en él, ellas son sólo una excusa para poder ver lo que ocurre con el espacio.
00016543

Árbol Pitagórico

En Febrero recién pasado tuve el honor de formar parte del primer programa de maestría en diseño de interacción, en Costa Rica. En esa ocación tuve el placer de colarme en el curso de Tomás de Camino a de introducción a la programación. Acá les dejo uno de los primeros ejercicios del curso:

show code

/*
 * Recursive Hypotenuse
 * by Herbert Spencer
 * February 2014
 */
 
float ang;  // this is the global angle
int levels; // this is the amount of tree levels or how many times we will call the recursive function
 
void setup() {
  size(510, 510);
  noStroke();
  smooth();
  levels = 3;       // start with something small
  mouseX = width/2; // place the mouse in the middle
}
 
void draw() {
  // white background
  background(255);
 
  // the global angle can be modified by de 'x' movement of the mouse
  ang = map(mouseX, 0, width, -HALF_PI, 0);
 
  // this is where we call the recursive function
  hypotenuse(width/2 - 30, height, width/2 + 30, height, levels);
}
 
// this is the recursive function
// it requeres 4 floats and 1 int. The floats represent the position of 2 points, which define the hypotenuse
// the level defines how many times are we calling this function recursevely
 
void hypotenuse(float x1, float y1, float x2, float y2, int level) {
 
  // determine the length, which is the distance between the points. We'll call it 'side', the side of the square
  float side = dist(x1, y1, x2, y2);
 
  // determine the angle between the two points... not so tricky
  float t = atan2(y2- y1, x2 - x1);
 
  // reset the coordinate system, just to be polite
  pushMatrix();
  {
    // translate our axis to the first point, our new (0,0)
    translate(x1, y1);
 
    // align space to the hypothenuse, using the angle we've just calculated
    rotate(t);
 
    // draw the square
    fill(col(level), 90); // nice color with transparency
    rect(0, -side, side, side);
 
    // translate to the new hypothenuse
    translate(0, -side);
 
    // determine the lengths of the triangle sides given a global angle 'ang'.
    // 'a, b, c'; where 'c' is the hypotenuse. We have 'c', it's the distance between the first two points, ok?
 
    float a = cos(ang) * side;
    float b = sin(ang) * side;
 
    // draw the triangle
    noStroke();
    fill(col(level), 150);
    beginShape();
    {
      vertex(0, 0);                       // first vertex
      vertex(cos(ang) * a, sin(ang) * a); // tricky second vertex
      vertex(side, 0);                    // third vertex
    }
    endShape();
 
    // this is a method to eventually escape from this recursive fuction
    if (level > 0) {
      hypotenuse(0, 0, cos(ang) * a, sin(ang) * a, level-1);
      hypotenuse(cos(ang) * a, sin(ang) * a, side, 0, level-1);
    }
  }
  popMatrix();
}
 
void keyPressed() {
 
  // you can toggle the levels by hitting keys a-z
  if (key == 'a') {
    levels++;
  }
  if (key == 'z') {
    levels--;
  }
 
  // so we keep levels reasonable...
  levels = constrain(levels, 0, 16);
 
  // output to console how many levels do we have at the moment
  println("levels = "+levels);
}
 
color col(int level){
  int n = levels - level;
  float r = 18;
  float g = 134 + 11 * n;
  float b = 255 - 19 * n;
  color c = color(r,g,b);
  return c;
}

Para agregar o quitar niveles de recursión debes presionar las teclas a y z. El código está publicado acá.

Pizarra

Uno de los problemas más frecuentes con que me topo al momento de hacer clases es la incompatibilidad entre el proyector y la pizarra. Cada día, y con más frecuencia, se requiere de un proyector (datashow) para presentar, demostrar código, traer referentes, indicar procedimientos, revisar casos, etc. El proyector ya es –digámoslo así– un commodity indispensable y que se echa de menos con facilidad.

Pero la dimensión que trae el dibujo o el acto de dibujar en la pizarra, en paralelo y reforzando el discurso es fundamental. Sobre todo en diseño. Mostrar un esquema de un Keynote no es igual a dibujarlo. Gracias a DrawSomething hemos aprendido que el proceso del dibujo dice tanto como el dibujo mismo, la secuencia va desplegando el significado porque tiene su propio discurso.

La iluminación tampoco ayuda: el proyector requiere de una luz atenuada, la pizarra, luz incidente (sobre todo si es negra).

Desarrollé, para el curso Imagen Escrita una aplicación elemental en Processing para tener una pizarra proyectable. Esta aplicación es a su vez, la base para desarrollar herramientas aumentadas de dibujo más complejas.

Les comparto esta aplciación porque últimamente me ha servido mucho, incluso en reuniones de diseño, para discutir wireframes, esquemas y demases. Espero que les sirva.

Instrucciones del teclado

  • (m) – cambia de pizarrón negro a pizarra blanca
  • (z) – deshace el último trazo. Se puede usar iterativamente hasta que no quede ninguno.
  • (ESPACIO) – borra todo
  • (s) graba la pizarra a una imagen en la carpeta ‘img’ relativa a la ruta de la aplicación
Actualización: El programa de la Pizarra se encuentra disponible para descargar en Github, entre las mejoras: ahora se pueden hacer líneas rectas con SHIFT y tiene compatibilidad para tablets.

Mesh Network

Esta ilustración fue generada para explicar el funcionamiento de una red enmallada (mesh network) en cuanto figura de distribución y conexión entre nodos. La aplicación práctica de esta red fue discutida en el post anterior, describiendo la necesidad de una arquitectura de red resistente a cataclismos como el pasado terremoto y donde todos los nodos aportan, yendo más allá de ser meros clientes. Una red pública es aquella donde cada participante expande su alcance por el simple hecho de pertenecer.

show code

void setup() {
  size(500, 500);
  NODES = new ArrayList();
  for (int i = 0; i < numNodes; i++) {
    Node n = new Node(random(margin, width-margin), random(margin, height-margin));
    if (random(1) >0.25) {
      n.device = true;
    } else {
      n.r *= 1.5;
    }
    NODES.add(n);
  }
  newNode = false;
  smooth();
}

void draw() {
  background(255);
  if (newNode) {
    NODES.add(current);
    newNode = false;
  }

  for (int i = 0; i < NODES.size (); i++) {
    Node n = (Node)NODES.get(i);
    n.calc();
    n.renderLinks();
  }

  for (int i = 0; i < NODES.size (); i++) {
    Node n = (Node)NODES.get(i);
    n.calc();
    if (n.device) {
      n.renderDevice();
    } else {
      n.renderNode();
    }
  }
}

class Node {
  int id; // the ID of this node
  float x, y; // position
  float r; // radius
  float signal; // ammount of signal
  boolean on; // is it on?
  boolean over; // is the mouse over this one?
  ArrayList nodes; // list of nodes that are connected to this one
  boolean device = false;
  float angle, step;

  Node(float X, float Y) {
    count++;
    id = count;
    x = X;
    y = Y;
    signal = 0;
    on = true;
    nodes = new ArrayList();
    r = 7.0;
    angle = random(TWO_PI);
    step = random(1);
  }

  void renderNode() {
    if (over) {
      noFill();
      stroke(linkColor);
      strokeWeight(.50);
      ellipse(x, y, nodeScope, nodeScope);
      fill(nodeFillColor);
      stroke(nodeStrokeColor);
    } else {
      fill(nodeFillColor, 150);
      stroke(nodeStrokeColor, 50);
    }
    if (on) {
      strokeWeight(3);
    } else {
      strokeWeight(0.5);
    }
    ellipse(x, y, r, r);
  }

  void renderDevice() {
    move();
    if (over) {
      noFill();
      stroke(linkColor);
      strokeWeight(.5);
      ellipse(x, y, nodeScope, nodeScope);
      fill(deviceFillColor);
      stroke(deviceStrokeColor);
    } else {
      fill(deviceFillColor, 150);
      stroke(deviceStrokeColor, 150);
    }
    if (on) {
      strokeWeight(1);
    } else {
      noStroke();
    }
    ellipse(x, y, r, r);
  }

  void renderLinks() {
    if (on) {
      stroke(linkColor, 100);
      for (int i = 0; i < nodes.size (); i++) {
        Node n = (Node)nodes.get(i);
        float d = dist(x, y, n.x, n.y);
        float sw = map(d, r, nodeScope, 5, 0.1);
        sw = constrain(sw, 0.01, 20);
        strokeWeight(sw);
        line(x, y, n.x, n.y);
      }
    }
  }

  void calc() {
    nodes.clear();
    for (int i = 0; i < NODES.size (); i++) {
      Node n = (Node)NODES.get(i);
      if (id != n.id) {
        float nodeDist = dist(x, y, n.x, n.y);
        if (nodeDist <= nodeScope &#038;&#038; n.on) {
          nodes.add(n);
        }
      }
    }
  }

  void move() {
    noiseSeed(id);
    angle += (noise((float)millis()/100.0) - .5) * HALF_PI;

    x += cos(angle)*step;
    y += sin(angle)*step;
  }
}

boolean OVER;
ArrayList NODES;
int numNodes = 50;
int count = 0;
float margin = 100;
float nodeScope = 60;
boolean newNode;
Node current;

color nodeFillColor = #F06E1D;
color nodeStrokeColor = #8E3E0B;
color linkColor = color(86, 115, 124, 80);
color deviceFillColor = #1FCCFF;
color deviceStrokeColor = #02A8D8;

void keyPressed() {
  if (key == 'a') {
    nodeScope += 5;
    println("node scope = "+nodeScope);
  }
  if (key == 'z') {
    nodeScope -= 5;
    println("node scope = "+nodeScope);
  }
  if (key == ' ') {
    for (int i = 0; i < NODES.size (); i++) {
      Node n = (Node)NODES.get(i);
      n.on = !n.on;
    }
  }
  if (key == 'x') {
    setup();
  }
  nodeScope = constrain(nodeScope, 5, width);
}

void mouseMoved() {
  float d;
  for (int i = 0; i < NODES.size (); i++) { 
    Node n = (Node)NODES.get(i); 
    d = dist(mouseX, mouseY, n.x, n.y); 
    if (n.r*1.5 >= d) {
      OVER = true;
      n.over = true;
      current = n;
    } else {
      OVER = false;
      n.over = false;
    }
  }
}

void mouseDragged() {
  if (OVER) {
    if (current.over) {
      current.x = mouseX;
      current.y = mouseY;
    }
  }
}

void mouseReleased() {
  if (current.over && mouseButton==RIGHT) {
    current.on = !current.on;
  }
}

Cómo funciona

  • arrastre nodos para reconfigurar la red
  • teclas a y z modifican el alcance de cada nodo
  • botón derecho sobre el nodo lo enciende o apaga
  • tecla i sirve para invertir encendido/apagado de todos los nodos
  • x regenera la distribución de nodos

Espirales

Estos días no he hecho nada porque me la he pasado en puras reuniones. Mientras escucho, dibujo espirales en mi cuaderno y veo como los pequeños errores, engrosamientos y disminuciones de los espacios se acumulan, amplifican o suavizan.

Veo como las familias de espirales se multiplican, cruzan y entrelazan. Tal vez así la sensación de no hacer nada desaparezca.