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
25;
tono = tono + }
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
// 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()
- la variable a convertir (el contador del ciclo)
- el valor mínimo que esa variable puede tener (normalmente 0)
- el valor máximo que esa variable puede tener (el número de repeticiones menos 1)
- el valor mínimo del nuevo rango
- el valor máximo del nuevo rango
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:
- el valor inicial de la variable
- la cantidad de iteraciones del ciclo
- el incremento en cada iteración
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
25;
rojo = rojo + }
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
30;
tono = tono + }
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
25;
rojo = rojo + 10;
verde = verde + 5;
azul = azul + }
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(240, 17, 88);
color colorInicial = color(246, 250, 61);
color colorFinal =
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
lerpColor(colorInicial, colorFinal, p);
color colorGradiente =
// 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