jardínBit

Gradientes de color

Existen varias estrategias para aprovechar estructuras repetitivas con el fin de cambiar gradualmente de color.

Partiremos del siguiente ejemplo de código que dibuja una serie de rectángulos alineados verticalmente.

Todas estas técnicas pueden extrapolarse a cualquier dibujo basado en repetición.

// repite 10 veces
for(int i=0; i<10; i=i+1){
  // asigna color
  fill(0);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

1 Escala de grises

Un gradiente de escala de grises consiste en cambiar el valor numérico del canal de luminosidad para cada figura dibujada.

1.1 Con acumulador

Podemos utilizar una variable para el canal de luminosidad, que se incrementa con cada iteración del ciclo:

float tono = 0; // valor inicial del tono

// repite 10 veces
for(int i=0; i<10; i=i+1){
  // asigna tono 
  fill(tono);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);

  // incrementa el canal
  tono = tono + 25;
}

La magnitud del incremento, en combinación con la cantidad de repeticiones y el valor inicial del tono, nos indicará cuál será el rango que tendremos.

En este ejemplo, el tono tomará los siguientes valores: 0, 25, 50, 75, 100, 125, 150, 175, 200.

Entre más repeticiones haya, menos hay que incrementar para que el gradiente abarque toda la composición.

1.1.1 Acumulador en dirección opuesta

¿Qué habría que cambiar de la estructura para que el gradiente fuera en la dirección opuesta?

1.2 Con contador del ciclo

Podemos aprovechar la variable que cuenta las repeticiones del ciclo, escalándola / convirtiéndola a valores que nos funcionen para el tono.

1.2.1 Escala

Tomando en cuenta que el contador del ciclo se incrementa de uno en uno (0, 1, 2, 3, etc), podemos multiplicar esa variable por una cantidad constante para obtener un resultado que se incrementa proporcionalmente:

// repite 10 veces
for(int i=0; i<10; i=i+1){
  // asigna tono en función del contador
  fill( i*25 );
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

En este ejemplo, el tono tomará los siguientes valores: 0, 25, 50, 75, 100, 125, 150, 175, 200.

¿Qué tendríamos que hacer para que el valor inicial no fuera 0?

¿Y qué tendríamos que hacer para que el gradiente fuera en la dirección opuesta?

1.2.2 Mapeo

La función map( ), si bien un poco más compleja, nos puede facilitar la creación de un gradiente en específico

[Mapeo/conversión de valores]

// repite 10 veces
for(int i=0; i<10; i=i+1){
  // convierte a i, que va de 0 a 9
  // a un valor entre 0 y 225
  float tono = map( i, 0, 9,  0, 225);

  // asigna el tono
  fill( tono );
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

Lo ideal es tener claro el significado de los 5 parámetros de map()

Cambiando los últimos dos parámetros alteramos el comportamiento del gradiente:

  // convierte a i, que en este caso va de 0 a 9
  // a un valor entre 100 y 225
  float tono = map( i, 0, 9,  100, 225);

Incluyendo la posibilidad de invertir su dirección:

  // convierte a i, que en este caso va de 0 a 9
  // a un valor entre 200 y 50
  float tono = map( i, 0, 9,  200, 50);

2 Color

Las estrategias para gradientes de color son similares a las previas, pero ahora actuando en tres canales independientes en lugar de solo uno.

2.1 Con acumulador

El mismo principio visto arriba lo podemos utilizar para cambiar un canal de color mientras los otros dos se mantienen constantes.

El rango de valores en el gradiente va a depender de nuevo de:

2.1.1 Acumulador en RGB

Este ejemplo modifica al canal rojo y deja constantes a verde y azul.

float rojo = 0; // valor inicial del canal rojo

// repite 10 veces
for(int i=0; i<10; i=i+1){
  // asigna color
  fill(rojo, 100, 250);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);

  // incrementa el canal rojo
  rojo = rojo + 25;
}

2.1.2 Acumulador en HSB

Este ejemplo modifica al tono (hue) y deja constantes a la saturación y luminosidad.

// modo de color HSB
colorMode(HSB, 360, 100, 100);

float tono = 0; // valor inicial del tono

// repite 10 veces
for(int i=0; i<10; i=i+1){
  // asigna tono 
  fill(tono, 50, 100);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);

  // incrementa el canal
  tono = tono + 30;
}

2.2 Con acumuladores

Podemos extrapolar lo anterior para tener un acumulador por cada canal de color, cada uno con un comportamiento distinto: ya sea con distinto valor inicial, y/o con distinto valor de incremento.

float rojo = 0; // valor inicial del canal rojo
float verde = 50; // valor inicial del canal verde
float azul = 100; // valor inicial del canal azul

// repite 10 veces
for(int i=0; i<10; i=i+1){
  // asigna color
  fill(rojo, verde, azul);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);

  // incrementa los canales con distintos incrementos
  rojo = rojo + 25;
  verde = verde + 10;
  azul = azul + 5;
}

2.3 Con contador del ciclo

Podemos aprovechar el conteo de iteraciones para calcular el valor de los canales de color en cada figura.

2.3.1 Escala por canal

A continuación ejemplos con el mismo comportamiento que los mostrados anteriormente con acumuladores.

Nota las similitudes con esos ejemplos previos: ¿cómo estamos asignando el incremento aquí? ¿y cómo estamos asignando el valor inicial de cada canal?

2.3.1.1 Escala en un canal RGB

for(int i=0; i<10; i=i+1){
  // asigna color en función de i
  fill(i*25, 100, 250);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

2.3.1.2 Escala en un canal HSB

colorMode(HSB, 360, 100, 100);

for(int i=0; i<10; i=i+1){
  // asigna color en función de i
  fill(i*30, 50, 100);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

2.3.1.3 Escala en canales RGB

for(int i=0; i<10; i=i+1){
  // asigna color en función de i
  fill(i*25, i*10+50, i*5+100);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

2.3.2 Mapeo

Conviene utilizar map() para tener más control sobre los valores iniciales y finales en cada canal, sin tener que estar realizando aritmética.

Por ejemplo, supongamos que queremos un gradiente que vaya del color (240, 17, 88) al color (246, 250, 61) en RGB.

Podemos notar que el canal rojo incrementa muy poco, el canal verde disminuye considerabldmente, y el azul incrementa más.

2.3.2.1 Mapeo por canal

Podemos utilizar map() para convertir al contador del ciclo i al valor correspondiente de cada canal.

for(int i=0; i<10; i=i+1){
  // convierte a i, que va de 0 a 9 en este caso
  // al valor de cada canal
  float rojo = map( i, 0, 9, 240, 246); // rojo de 240 a 246
  float verd = map( i, 0, 9, 17, 250); // verde de 17 a 250
  float azul = map( i, 0, 9, 88, 61); // azul de 88 a 61

  fill(rojo, verd, azul);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

2.3.2.2 Interpolación lineal lerpColor()

Processing tiene una función lerpColor() que calcula (interpola) el color que habría entre un color inicial y un color final dados, en una “posición” específica dada por un valor entre 0 y 1.

Si la posición es 0, el color corresponde al inicial, y si la posición es 1, el color corresponde al final. Una posición de 0.5 sería el color exactamente enmedio a los dos, y así sucesivamente.

Podemos usar map() para convertir el contador de iteraciones a un valor entre 0 y 1.

La estructura quedaría así:

// establece colores del gradiente
color colorInicial = color(240, 17, 88);
color colorFinal = color(246, 250, 61);

for(int i=0; i<10; i=i+1){
  // convierte el contador i, que va de 0 a 9 en este caso
  // a un número entre 0 y 1
  float p = map(i, 0, 9, 0, 1);

  // utiliza ese valor para calcular el color correspondiente
  color colorGradiente = lerpColor(colorInicial, colorFinal, p);
  
  // y utiliza el color correspondiente
  fill(colorGradiente);
  
  // dibuja rectángulo y traslada
  rect(0,0, width, 20);
  translate(0, 40);
}

La posición en el gradiente también podría calcularse con aritmética:

float p = i/9.0; // i va de 0 a 9, lo convertimos a un número entre 0 y 1

El punto decimal es importante para tener un resultado de punto flotante y no entera. Otra forma de escribirlo podría ser:

float p = 1.0*i/9; // i va de 0 a 9, lo convertimos a un número entre 0 y 1